diff --git a/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java b/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java
new file mode 100644
index 0000000000..4aab139b3d
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java
@@ -0,0 +1,124 @@
+/*
+ * 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.felix.framework;
+
+import java.io.IOException;
+import java.net.ContentHandler;
+import java.net.URLStreamHandler;
+
+import org.apache.felix.framework.plurl.Plurl;
+import org.apache.felix.framework.plurl.PlurlContentHandlerFactory;
+import org.apache.felix.framework.plurl.PlurlStreamHandlerFactory;
+
+/**
+ *
+ * {@code URLHandlers} currently claims the JVM-wide {@code java.net.URL} stream
+ * handler factory by reflectively swapping a private static field
+ * ({@code SecureAction.swapStaticFieldIfNotClass}). Obtaining a
+ * {@code MethodHandles.Lookup} trusted enough to do that is the sole reason the
+ * framework still uses {@code sun.misc.Unsafe}, and it means whichever framework
+ * installs itself last wins the singleton - so Felix and Equinox cannot coexist in
+ * one JVM without clobbering each other.
+ *
+ *
+ * Plurl instead installs one cooperative router through the supported
+ * {@code URL.setURLStreamHandlerFactory} API and lets any number of parties
+ * register with it. Each registered factory answers {@link #shouldHandle(Class)}
+ * to say whether a given calling class belongs to it; plurl walks the call stack
+ * and routes accordingly. That maps directly onto what
+ * {@link URLHandlers#getFrameworkFromContext()} already does.
+ *
+ *
+ * See {@code org/apache/felix/framework/plurl/README.md} for the provenance of the
+ * vendored plurl sources and the unresolved licensing question that currently
+ * blocks this approach.
+ *
+ */
+class PlurlURLHandlers implements PlurlStreamHandlerFactory, PlurlContentHandlerFactory
+{
+ private final URLHandlers m_delegate;
+
+ PlurlURLHandlers(URLHandlers delegate)
+ {
+ m_delegate = delegate;
+ }
+
+ /**
+ * Registers this framework's handlers with the plurl router, installing the
+ * router first if nobody has yet.
+ */
+ static PlurlURLHandlers install(URLHandlers delegate) throws IOException
+ {
+ PlurlURLHandlers handlers = new PlurlURLHandlers(delegate);
+ Plurl.add((PlurlStreamHandlerFactory) handlers);
+ Plurl.add((PlurlContentHandlerFactory) handlers);
+ return handlers;
+ }
+
+ /**
+ * Unregisters this framework's handlers, leaving the router in place for any
+ * other framework instance still using it.
+ */
+ void uninstall() throws IOException
+ {
+ Plurl.remove((PlurlStreamHandlerFactory) this);
+ Plurl.remove((PlurlContentHandlerFactory) this);
+ }
+
+ /**
+ * Tells plurl whether the given calling class belongs to this framework.
+ *
+ * This replaces the call stack walking URLHandlers does today: rather than
+ * inspecting the stack itself to work out which framework owns the caller, the
+ * router asks each registered factory about a single candidate class.
+ */
+ @Override
+ public boolean shouldHandle(Class> clazz)
+ {
+ if (clazz == null)
+ {
+ return false;
+ }
+ ClassLoader loader = clazz.getClassLoader();
+ if (loader == null)
+ {
+ return false;
+ }
+ String name = loader.getClass().getName();
+ return name.startsWith("org.apache.felix.framework.BundleWiringImpl$BundleClassLoader")
+ || name.startsWith("org.apache.felix.framework.ModuleImpl$ModuleClassLoader")
+ || name.equals("org.apache.felix.framework.searchpolicy.ContentClassLoader");
+ }
+
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol)
+ {
+ return m_delegate.createURLStreamHandler(protocol);
+ }
+
+ @Override
+ public ContentHandler createContentHandler(String mimeType)
+ {
+ return m_delegate.createContentHandler(mimeType);
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java b/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java
new file mode 100644
index 0000000000..fba7df4b96
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java
@@ -0,0 +1,308 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.io.IOException;
+import java.net.ContentHandlerFactory;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandlerFactory;
+import java.util.function.Consumer;
+
+/**
+ * Plurl is used to multiplex the URL factory singletons for
+ * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)} and
+ * {@link URLConnection#setContentHandlerFactory(ContentHandlerFactory)}. Plurl
+ * factories may be added and removed using the add and remove methods or using
+ * the {@link #PLURL_PROTOCOL plurl} protocol.
+ *
+ *
+ * The {@link #PLURL_PROTOCOL plurl} protocol allows factories to be added even
+ * if the installed plurl implementation is not using the same
+ * org.apache.felix.framework.plurl package as the factories being registered. A plurl
+ * implementation must handle this case by reflecting on the plurl factories
+ * that are added. A plurl factory can be added and removed with the plurl
+ * protocol like this:
+ *
+ *
+ *
+ * The content provided by the plurl protocol is of type {@link Consumer} which
+ * can take either an {@link URLStreamHandlerFactory} or a
+ * {@link ContentHandlerFactory} depending on the operation.
+ *
+ *
+ * A plurl implementation delegates to the added {@link PlurlFactory} objects.
+ * To select which {@code PlurlFactory} to delegate the
+ * {@link PlurlFactory#shouldHandle(Class)} method is used.
+ *
+ * If only one factory has been added to plurl then that {@code PlurlFactory} is
+ * used to create the handler. Otherwise each
+ * {@link PlurlFactory#shouldHandle(Class)} is called for a class in the call
+ * stack until a factory returns true. If no factory returns true then the next
+ * class in the call stack is used. If no factory is found after using all
+ * classes in the call stack then the first factory added is selected. Once a
+ * factory is selected, it is used to create the requested handler. If the
+ * selected factory returns a {@code null} handler then no other factory is
+ * asked to create the handler.
+ *
+ * @see #PLURL_ADD_URL_STREAM_HANDLER_FACTORY
+ * @see #PLURL_ADD_CONTENT_HANDLER_FACTORY
+ * @see #PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY
+ * @see #PLURL_REMOVE_CONTENT_HANDLER_FACTORY
+ */
+public interface Plurl {
+ /**
+ * The "plurl" protocol to add and remove plurl factories.
+ */
+ public static final String PLURL_PROTOCOL = "plurl"; //$NON-NLS-1$
+ /**
+ * The host to use for the "plurl" protocol to indicate an operation for adding
+ * or removing factories.
+ */
+ public static final String PLURL_OP = "op"; //$NON-NLS-1$
+ /**
+ * The plurl protocol operation to add a URLStreamHandlerFactory
+ */
+ public static final String PLURL_ADD_URL_STREAM_HANDLER_FACTORY = "addURLStreamHandlerFactory"; //$NON-NLS-1$
+ /**
+ * The plurl protocol operation to remove a URLStreamHandlerFactory
+ */
+ public static final String PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY = "removeURLStreamHandlerFactory"; //$NON-NLS-1$
+
+ /**
+ * The plurl protocol operation to add a ContentStreamHandlerFactory
+ */
+ public static final String PLURL_ADD_CONTENT_HANDLER_FACTORY = "addContentHandlerFactory"; //$NON-NLS-1$
+
+ /**
+ * The plurl protocol operation to remove a ContentStreamHandlerFactory
+ */
+ public static final String PLURL_REMOVE_CONTENT_HANDLER_FACTORY = "removeContentHandlerFactory"; //$NON-NLS-1$
+
+ /**
+ * An optional plurl protocol operation to register a {@code Plurl} instance
+ * with the current plurl protocol implementation. This is an optional operation
+ * that a {@code Plurl} implementation may implement to allow another plurl
+ * instance to be registered as a delegate. A delegate may be used to install
+ * the delegate plurl instance when the current plurl gets {@link #uninstall()
+ * uninstalled}.
+ */
+ public static final String PLURL_REGISTER_IMPLEMENTATION = "plurlRegisterImplementation"; //$NON-NLS-1$
+
+ /**
+ * An optional plurl protocol operation to unregister a {@code Plurl} instance
+ * with the current plurl instance set with the JVM. This is an optional
+ * operation that a {@code Plurl} implementation may implement to allow another
+ * plurl instance to be unregistered as a delegate.
+ */
+ public static final String PLURL_UNREGISTER_IMPLEMENTATION = "plurlUnegisterImplementation"; //$NON-NLS-1$
+ /**
+ * The value to use for the {@link #install(String...)} method to indicate that
+ * no protocols are forbidden. for overriding by plurl handlers.
+ */
+ public static final String PLURL_FORBID_NOTHING = "plurlForbidNothing"; //$NON-NLS-1$
+
+ /**
+ * Installs the plurl factories into the JVM singletons. If plurl factories are
+ * already installed then this plurl instance is
+ * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with the existing plurl
+ * instance set with the JVM by using something like the following:
+ *
+ *
+ *
+ * If the plurl factories cannot be installed then an
+ * {@code IllegalStateException} is thrown.
+ *
+ * If the JVM singletons are already set with other factories that are not plurl
+ * then an attempt is made to override the JVM singletons with this plurl
+ * instance. This may only be possible if the implementation is allowed to do
+ * deep reflection on the {@code java.net} package. If the JVM singletons are
+ * overriden then the original singleton factory instances must be used as
+ * parent factories of the plurl instance until the plurl instance is
+ * {@link #uninstall() uninstalled}. if overriding the JVM singletons is not
+ * possible then an {@link IllegalStateException} is thrown.
+ *
+ * If the JVM singletons were not overriden then this plurl instance is
+ * considered the primordial singleton factory for the JVM. Such a plurl
+ * instance cannot be {@link #uninstall() uninstalled} and will live the
+ * lifetime of the JVM.
+ *
+ * When this method returns without throwing an exception then the following
+ * will be true:
+ *
+ *
The singleton
+ * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)} is set with a
+ * plurl implementation which delegates to the {@link PlurlStreamHandlerFactory}
+ * objects that have been {@link #add(PlurlStreamHandlerFactory) added}.
+ *
The singleton
+ * {@link URLConnection#setContentHandlerFactory(ContentHandlerFactory)} is set
+ * with a plurl implementation which delegates to the
+ * {@link PlurlContentHandlerFactory} objects that have been
+ * {@link #add(PlurlContentHandlerFactory) added}.
+ *
The {@link #PLURL_PROTOCOL plurl} protocol is available for creating
+ * {@code URL} objects.
+ *
If plurl factories are already installed then this plurl implementation
+ * is registered as a delegate with the already installed plurl instance.
+ *
+ *
+ * @param forbidden builtin JVM protocols that cannot be overridden by plurl. If
+ * no forbidden protocols are specified then the default
+ * forbidden protocols are 'jar', 'jmod', 'file', and 'jrt'. To
+ * forbid no protocols then use the value
+ * {@link #PLURL_FORBID_NOTHING}
+ * @throws IllegalStateException if the Plurl factories cannot be installed
+ */
+ public void install(String... forbidden);
+
+ /**
+ * If this plurl instance is the primordial factory for the JVM then uninstall
+ * is a no-op and the plurl instance will remain set with the JVM for the
+ * lifetime of the JVM instance.
+ *
+ * If this plurl is not the primordial factory and is the current plurl set with
+ * the JVM singletons then this plurl instance must do the following:
+ *
+ *
Reset the original parent factories as the singleton factories of the
+ * JVM
+ *
If there are any other plurl instances that got
+ * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with this plurl instance
+ * then one of the registered plurl instances must be selected to be the next
+ * delegate plurl instance to {@link #install(String...) install}.
+ *
If a delegate plurl instance gets installed then any existing factories
+ * that were added to this plurl instance must be added to the new delegate
+ * plurl instance and any {@link #PLURL_REGISTER_IMPLEMENTATION registered}
+ * plurl instances must be registered with the new delegate plurl instance.
+ *
This plurl instance must release all references to other factories or
+ * plurl instances.
+ *
+ * If this plurl instance is not the current plurl set with JVM then this plurl
+ * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with the existing plurl
+ * instance set with the JVM by using something like the following:
+ *
+ *
+ */
+ public void uninstall();
+
+ /**
+ * Adds a {@link PlurlStreamHandlerFactory} to an {@link #install installed}
+ * plurl implementation. If there is no plurl implementation installed then an
+ * {@link IOException} is thrown. The plurl implementation must not hold any
+ * strong references to the factory. If the factory is garbage collected then
+ * the plurl implementation must behave as if the factory got
+ * {@link #remove(PlurlStreamHandlerFactory) removed}.
+ *
+ * This is a convenience method for using the plurl protocol like this:
+ *
+ *
+ *
+ * @param factory the PlurlStreamHandlerFactory to add
+ * @throws IOException if there is no plurl implementation installed or there
+ * was an error adding the factory
+ */
+ public static void add(PlurlStreamHandlerFactory factory) throws IOException {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_ADD_URL_STREAM_HANDLER_FACTORY);
+ @SuppressWarnings("unchecked")
+ Consumer addFactory = (Consumer) plurl.openConnection()
+ .getContent();
+ addFactory.accept(factory);
+ }
+
+ /**
+ * Removes a {@link PlurlStreamHandlerFactory} to an {@link #install installed}
+ * plurl implementation. If there is no plurl implementation installed then an
+ * {@link IOException} is thrown.
+ *
+ * This is a convenience method for using the plurl protocol like this:
+ *
+ *
+ *
+ * @param factory the PlurlStreamHandlerFactory to remove
+ * @throws IOException if there is no plurl implementation installed or there
+ * was an error removing the factory
+ */
+ public static void remove(PlurlStreamHandlerFactory factory) throws IOException {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY);
+ @SuppressWarnings("unchecked")
+ Consumer removeFactory = (Consumer) plurl.openConnection()
+ .getContent();
+ removeFactory.accept(factory);
+ }
+
+ /**
+ * Adds a {@link PlurlContentHandlerFactory} from an {@link #install installed}
+ * plurl implementation. If there is no plurl implementation installed then an
+ * {@link IOException} is thrown. The plurl implementation must not hold any
+ * strong references to the factory. If the factory is garbage collected then
+ * the plurl implementation must behave as if the factory got
+ * {@link #remove(PlurlContentHandlerFactory) removed}.
+ *
+ * This is a convenience method for using the plurl protocol like this:
+ *
+ *
+ *
+ * @param factory the PlurlContentHandlerFactory to add
+ * @throws IOException if there is no plurl implementation installed or there
+ * was an error adding the factory
+ */
+ public static void add(PlurlContentHandlerFactory factory) throws IOException {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_ADD_CONTENT_HANDLER_FACTORY);
+ @SuppressWarnings("unchecked")
+ Consumer addFactory = (Consumer) plurl.openConnection()
+ .getContent();
+ addFactory.accept(factory);
+ }
+
+ /**
+ * Removes a {@link PlurlContentHandlerFactory} from an {@link #install
+ * installed} plurl implementation. If there is no plurl implementation
+ * installed then an {@link IOException} is thrown.
+ *
+ * This is a convenience method for using the plurl protocol like this:
+ *
+ *
+ *
+ * @param factory the PlurlContentHandlerFactory to remove
+ * @throws IOException if there is no plurl implementation installed or there
+ * was an error removing the factory
+ */
+ public static void remove(PlurlContentHandlerFactory factory) throws IOException {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_REMOVE_CONTENT_HANDLER_FACTORY);
+ @SuppressWarnings("unchecked")
+ Consumer removeFactory = (Consumer) plurl.openConnection()
+ .getContent();
+ removeFactory.accept(factory);
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java
new file mode 100644
index 0000000000..293edb4739
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.net.ContentHandlerFactory;
+
+/**
+ * A {@link ContentHandlerFactory} that also implements {@link PlurlFactory}
+ */
+public interface PlurlContentHandlerFactory extends ContentHandlerFactory, PlurlFactory {
+ // a marker interface for a ContentHandlerFactory that implements PlurlFactory
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java
new file mode 100644
index 0000000000..e72ae8a16b
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+/**
+ * A plural factory that can be added to a plurl implementation. A plurl
+ * implementation uses {@code PlurlFactory} objects to locate a factory to
+ * provider a handler.
+ *
+ * @see Plurl#add(PlurlContentHandlerFactory)
+ * @see Plurl#add(PlurlStreamHandlerFactory)
+ */
+public interface PlurlFactory {
+ /**
+ * A plurl implementation will call this method with the classes in the call
+ * stack which are using the java.net APIs to create URL objects for a specific
+ * type. For example, a protocol or content type.
+ *
+ * @param clazz a class in the call stack using the java.net APIs
+ * @return true if this factory should be used to handle the request
+ */
+ boolean shouldHandle(Class> clazz);
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java
new file mode 100644
index 0000000000..bad743112d
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * The {@code PlurlStreamHandler} interface has public versions of the protected
+ * {@link java.net.URLStreamHandler} methods.
+ *
+ * The important differences between this interface and the
+ * {@code URLStreamHandler} class are that the {@code setURL} method is absent
+ * and the {@code parseURL} method takes a {@link PlurlSetter} object as the
+ * first argument. Classes implementing this interface must call the
+ * {@code setURL} method on the {@code PlurlSetter} object received in the
+ * {@code parseURL} method instead of {@code URLStreamHandler.setURL} to avoid a
+ * {@code SecurityException}.
+ *
+ * @see PlurlStreamHandlerBase
+ *
+ */
+public interface PlurlStreamHandler {
+ /**
+ * Interface used by {@code PlurlStreamHandler} objects to call the
+ * {@code setURL} method on the plurl proxy {@code URLStreamHandler} object.
+ *
+ *
+ * Objects of this type are passed to the
+ * {@link PlurlStreamHandler#parseURL(PlurlSetter, URL, String, int, int)}
+ * method. Invoking the {@code setURL} method on the
+ * {@code URLStreamHandlerSetter} object will invoke the {@code setURL} method
+ * on the plurl proxy {@code URLStreamHandler} object that is actually
+ * registered with {@code java.net.URL} for the protocol.
+ *
+ */
+ public interface PlurlSetter {
+ /**
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)"
+ */
+ public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo,
+ String path, String query, String ref);
+ }
+
+ /**
+ * @see "java.net.URLStreamHandler.equals(URL, URL)"
+ */
+ public boolean equals(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.hashCode(URL)"
+ */
+ public int hashCode(URL u);
+
+ /**
+ * @see "java.net.URLStreamHandler.hostsEqual(URL, URL)"
+ */
+ public boolean hostsEqual(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.getDefaultPort"
+ */
+ public int getDefaultPort();
+
+ /**
+ * @see "java.net.URLStreamHandler.getHostAddress(URL)"
+ */
+ public InetAddress getHostAddress(URL u);
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL)"
+ */
+ public URLConnection openConnection(URL u) throws IOException;
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)"
+ */
+ public URLConnection openConnection(URL u, Proxy p) throws IOException;
+
+ /**
+ * @see "java.net.URLStreamHandler.sameFile(URL, URL)"
+ */
+ public boolean sameFile(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.toExternalForm(URL)"
+ */
+ public String toExternalForm(URL u);
+
+ /**
+ * Parse a URL. This method is called by the {@code URLStreamHandler} proxy
+ * implemented by plurl, instead of {@code java.net.URLStreamHandler.parseURL},
+ * passing a {@code PlurlSetter} object.
+ *
+ * @param plurlSetter The object on which {@code setURL} must be invoked for
+ * this URL. If the setter is {@code null} then the
+ * {@link PlurlStreamHandler#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method can be called directly.
+ * @see "java.net.URLStreamHandler.parseURL"
+ */
+ public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit);
+
+ /**
+ * If the plurlSetter is not {@code null} from the
+ * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the
+ * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method. Otherwise call {@code super.setURL}.
+ *
+ * @see "java.net.URLStreamHandler.setURL"
+ */
+ public void setURL(URL u, String proto, String host, int port, String file, String ref);
+
+ /**
+ * If the plurlSetter is not {@code null} from the
+ * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the
+ * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method. Otherwise call {@code super.setURL}.
+ *
+ * @see "java.net.URLStreamHandler.setURL"
+ */
+ public void setURL(URL u, String proto, String host, int port, String auth, String user, String path,
+ String query, String ref);
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java
new file mode 100644
index 0000000000..226acb847c
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java
@@ -0,0 +1,173 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * Abstract implementation of the {@code PlurlStreamHandler} interface. All
+ * the methods simply invoke the corresponding methods on
+ * {@code java.net.URLStreamHandler} except for {@code parseURL} and
+ * {@code setURL}, which use the {@code PlurlSetter} parameter.
+ * Subclasses of this abstract class should not need to override the
+ * {@code setURL} and {@code parseURL(URLStreamHandlerSetter,...)} methods.
+
+ */
+public abstract class PlurlStreamHandlerBase extends URLStreamHandler implements PlurlStreamHandler {
+ private volatile PlurlSetter plurlSetter;
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL)"
+ */
+ @Override
+ public abstract URLConnection openConnection(URL u) throws IOException;
+
+ /**
+ * Parse a URL using the {@code PlurlSetter} object. This method sets the
+ * {@code plurlSetter} field with the specified {@code PlurlSetter} object and
+ * then calls {@code parseURL(URL,String,int,int)}.
+ *
+ * @param setter The object on which the {@code setURL} method must be invoked
+ * for the specified URL.
+ * @see "java.net.URLStreamHandler.parseURL"
+ */
+ @Override
+ public void parseURL(PlurlSetter setter, URL u, String spec, int start, int limit) {
+ this.plurlSetter = setter;
+ parseURL(u, spec, start, limit);
+ }
+
+ /**
+ * This method calls {@code super.openConnection(URL, Proxy)}
+ *
+ * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)"
+ */
+ @Override
+ public URLConnection openConnection(URL u, Proxy p) throws IOException {
+ return super.openConnection(u, p);
+ }
+
+ /**
+ * This method calls {@code super.toExternalForm}.
+ *
+ * @see "java.net.URLStreamHandler.toExternalForm"
+ */
+ @Override
+ public String toExternalForm(URL u) {
+ return super.toExternalForm(u);
+ }
+
+ /**
+ * This method calls {@code super.equals(URL,URL)}.
+ *
+ * @see "java.net.URLStreamHandler.equals(URL,URL)"
+ */
+ @Override
+ public boolean equals(URL u1, URL u2) {
+ return super.equals(u1, u2);
+ }
+
+ /**
+ * This method calls {@code super.getDefaultPort}.
+ *
+ * @see "java.net.URLStreamHandler.getDefaultPort"
+ */
+ @Override
+ public int getDefaultPort() {
+ return super.getDefaultPort();
+ }
+
+ /**
+ * This method calls {@code super.getHostAddress}.
+ *
+ * @see "java.net.URLStreamHandler.getHostAddress"
+ */
+ @Override
+ public InetAddress getHostAddress(URL u) {
+ return super.getHostAddress(u);
+ }
+
+ /**
+ * This method calls {@code super.hashCode(URL)}.
+ *
+ * @see "java.net.URLStreamHandler.hashCode(URL)"
+ */
+ @Override
+ public int hashCode(URL u) {
+ return super.hashCode(u);
+ }
+
+ /**
+ * This method calls {@code super.hostsEqual}.
+ *
+ * @see "java.net.URLStreamHandler.hostsEqual"
+ */
+ @Override
+ public boolean hostsEqual(URL u1, URL u2) {
+ return super.hostsEqual(u1, u2);
+ }
+
+ /**
+ * This method calls {@code super.sameFile}.
+ *
+ * @see "java.net.URLStreamHandler.sameFile"
+ */
+ @Override
+ public boolean sameFile(URL u1, URL u2) {
+ return super.sameFile(u1, u2);
+ }
+
+ /**
+ * This method calls
+ * {@code plurlSetter.setURL(URL,String,String,int,String,String,String,String)}.
+ *
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String)"
+ */
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setURL(URL u, String proto, String host, int port, String file, String ref) {
+ PlurlSetter current = plurlSetter;
+ if (current == null) {
+ // something is calling the handler directly, probably passed it to URL directly
+ super.setURL(u, proto, host, port, null, null, file, null, ref);
+ } else {
+ current.setURL(u, proto, host, port, null, null, file, null, ref);
+ }
+ }
+
+ /**
+ * This method calls
+ * {@code realHandler.setURL(URL,String,String,int,String,String,String,String)}
+ * .
+ *
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)"
+ */
+ @Override
+ public void setURL(URL u, String proto, String host, int port, String auth, String user, String path,
+ String query, String ref) {
+ PlurlSetter current = plurlSetter;
+ if (current == null) {
+ // something is calling the handler directly, probably passed it to URL directly
+ super.setURL(u, proto, host, port, auth, user, path, query, ref);
+ } else {
+ current.setURL(u, proto, host, port, auth, user, path, query, ref);
+ }
+ }
+
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java
new file mode 100644
index 0000000000..3bb311c135
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+
+/**
+ * A {@link URLStreamHandlerFactory} that also implements {@link PlurlFactory}
+ */
+public interface PlurlStreamHandlerFactory extends URLStreamHandlerFactory, PlurlFactory {
+
+ /**
+ * A factory is expected to return {@link URLStreamHandler} instances that also
+ * implement {@link PlurlStreamHandler}. If the returned handler does not
+ * implement {@link PlurlStreamHandler} then deep reflection is required and the
+ * JVM may require the "--add-opens" option in order to open the "java.net"
+ * package for reflection. For example:
+ *
+ *
+ *
+ * @see URLStreamHandlerFactory#createURLStreamHandler(String)
+ */
+ @Override
+ URLStreamHandler createURLStreamHandler(String protocol);
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/README.md b/framework/src/main/java/org/apache/felix/framework/plurl/README.md
new file mode 100644
index 0000000000..00de0d1e33
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/README.md
@@ -0,0 +1,57 @@
+# Plurl (vendored) — PROTOTYPE, NOT FOR RELEASE
+
+## Provenance
+
+These sources are copied verbatim from the Eclipse OSGi Technology **plurl** project:
+
+- Upstream: https://github.com/eclipse-osgi-technology/plurl
+- Originally: https://github.com/tjwatson/plurl-osgi
+- Copied at commit `6581777`, upstream version `0.1.0-SNAPSHOT`
+
+The **only** modification is the package rename from `org.eclipse.osgitech.plurl` to
+`org.apache.felix.framework.plurl`. Every file keeps its original license header and
+its `Copyright (c) 2025 IBM Corporation` notice unchanged.
+
+This mirrors what Eclipse Equinox did in
+https://github.com/eclipse-equinox/equinox/pull/848, which vendored the same 11 files
+into `org.eclipse.equinox.plurl`.
+
+## ⚠️ Unresolved licensing issue
+
+**This code must not be merged or released in its current state.**
+
+Every source file here declares:
+
+```
+SPDX-License-Identifier: EPL-2.0
+Copyright (c) 2025 IBM Corporation
+```
+
+EPL-2.0 is [Category B](https://www.apache.org/legal/resolved.html#category-b) at the
+ASF and **may not be included in an Apache source release**. Equinox was free to
+vendor these files because Eclipse projects are EPL-2.0 natively; Apache Felix is not.
+
+There is reason to believe the headers are an oversight rather than the project's
+intent: the plurl repository's own `LICENSE` file and its `pom.xml` both declare
+**Apache-2.0**, and only the source headers say EPL-2.0. (Its `NOTICE` file is also a
+copy-paste leftover referring to "slf4j-osgi".)
+
+Before this can go anywhere, one of the following has to happen:
+
+1. The upstream project relicenses/corrects the source headers to Apache-2.0, so the
+ files can legitimately live in an Apache source tree; or
+2. Felix consumes plurl as a released binary dependency rather than vendored source,
+ subject to the Category B rules — which additionally requires plurl to be published
+ to Maven Central, as it currently has no release or tag; or
+3. Felix writes its own Apache-2.0 implementation of the same idea.
+
+## Why we want this at all
+
+`URLHandlers` currently takes over the JVM-wide `java.net.URL` stream handler factory
+by reflectively swapping a private static field, which is what forces
+`SecureAction`'s use of `sun.misc.Unsafe` to obtain a trusted
+`MethodHandles.Lookup`. That is the only remaining `Unsafe` usage in the framework and
+it prevents Felix and Equinox from coexisting in one JVM without clobbering each
+other's URL singletons. Plurl replaces that with a cooperative multiplexing factory,
+which is the approach suggested in
+https://github.com/apache/felix-dev/pull/433#issuecomment-3073468820.
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java
new file mode 100644
index 0000000000..e2e18167ad
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java
@@ -0,0 +1,18 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+interface CallStack {
+ Class>[] getClassContext();
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java
new file mode 100644
index 0000000000..fc271bd89d
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java
@@ -0,0 +1,1507 @@
+/*******************************************************************************
+ * Copyright (c) 2025 IBM Corporation and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.ContentHandler;
+import java.net.ContentHandlerFactory;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.apache.felix.framework.plurl.Plurl;
+import org.apache.felix.framework.plurl.PlurlFactory;
+import org.apache.felix.framework.plurl.PlurlStreamHandler;
+import org.apache.felix.framework.plurl.PlurlStreamHandler.PlurlSetter;
+import org.apache.felix.framework.plurl.PlurlStreamHandlerBase;
+
+public final class PlurlImpl implements Plurl {
+
+ private static final String PROTOCOL_HANDLER_PKGS = "java.protocol.handler.pkgs"; //$NON-NLS-1$
+ private static final String CONTENT_HANDLER_PKGS = "java.content.handler.pkgs"; //$NON-NLS-1$
+ private static final String DEFAULT_VM_CONTENT_HANDLERS = "sun.net.www.content"; //$NON-NLS-1$
+ volatile Set forbiddenProtocols = new HashSet<>(
+ Arrays.asList("jar", "jmod", "file", "jrt")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+ private static final String THIS_PACKAGE = PlurlImpl.class.getPackage().getName();
+ static final String PLURL_STREAM_HANDLER_CLASS_NAME = PlurlStreamHandler.class.getName();
+ static final Field URL_HANDLER_FIELD = findUrlHandlerField();
+
+ private static final Collection systemLoaders;
+ static {
+ Collection loaders = new ArrayList<>();
+ try {
+ ClassLoader cl = ClassLoader.getSystemClassLoader();
+ // we allow the system cl, but not its parents
+ cl = cl != null ? cl.getParent() : null;
+ while (cl != null) {
+ loaders.add(cl);
+ cl = cl.getParent();
+ }
+ } catch (Throwable t) {
+ // ignore as if no loaders
+ }
+ systemLoaders = Collections.unmodifiableCollection(loaders);
+ }
+
+ private static boolean isSystemClass(String pName, final Class> clazz) {
+ if (pName != null && pName.startsWith("jdk.")) { //$NON-NLS-1$
+ return true;
+ }
+ // we want to ignore classes from the system
+ ClassLoader cl = AccessController.doPrivileged(new PrivilegedAction() {
+ @Override
+ public ClassLoader run() {
+ return clazz.getClassLoader();
+ }
+ });
+ return cl == null || systemLoaders.contains(cl);
+ }
+
+ private static Field findUrlHandlerField() {
+ Field f = null;
+ try {
+ f = URL.class.getDeclaredField("handler"); //$NON-NLS-1$
+ } catch (Exception e) {
+ Field[] fields = URL.class.getDeclaredFields();
+ for (Field field : fields) {
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ if (!isStatic && field.getType().equals(URLStreamHandler.class)) {
+ f = field;
+ break;
+ }
+ }
+ }
+ if (f == null) {
+ // fallback reflection is blocked by module system
+ return null;
+ }
+ try {
+ f.setAccessible(true);
+ return f;
+ } catch (Exception e) {
+ // blocked by module system
+ }
+ return null;
+ }
+
+ static boolean setHandler(URL u, Object h) {
+ if (URL_HANDLER_FIELD == null || !(h instanceof URLStreamHandler)) {
+ return false;
+ }
+ try {
+ URL_HANDLER_FIELD.set(u, h);
+ } catch (Exception e) {
+ // should not happen
+ throw new IllegalStateException(e);
+ }
+ return true;
+ }
+
+ static enum SetFactories {
+ notInstalled, // not installed yet
+ primordial, // there were no factories set until plurl
+ override, // single factories existed before plurl overrode them
+ plurlAlreadySet // some other PlurlImpl instance already installed plurl
+ }
+
+ SetFactories setFactories = SetFactories.notInstalled;
+
+ URLStreamHandlerFactory parentURLStreamHandlerFactory = null;
+ ContentHandlerFactory parentContentHandlerFactory = null;
+
+ List streamHandlerFactories = Collections.emptyList();
+ List contentHandlerFactories = Collections.emptyList();
+ List plurlImpls = Collections.emptyList();
+
+ final ServiceLoader builtinURLStreamHandlerFactoryLoader;
+ final ServiceLoader builtinContentHandlerFactoryLoader;
+ final CallStack callStack;
+
+ private final ThreadLocal> creatingProtocols = new ThreadLocal<>();
+ final URLToHandler urlToHandler = new URLToHandler();
+
+ boolean isRecursive(String protocol) {
+ List protocols = creatingProtocols.get();
+ if (protocols == null) {
+ protocols = new ArrayList<>(1);
+ creatingProtocols.set(protocols);
+ }
+ if (protocols.contains(protocol))
+ return true;
+ protocols.add(protocol);
+ return false;
+ }
+
+ void releaseRecursive(String protocol) {
+ List protocols = creatingProtocols.get();
+ protocols.remove(protocol);
+ }
+
+ public interface LegacyFactory {
+ public void register(Object factory);
+
+ public void unregister(Object factory);
+
+ public boolean isMultiplexing();
+ }
+
+ public class PlurlURLStreamHandlerFactory extends URLStreamHandler
+ implements URLStreamHandlerFactory, LegacyFactory {
+ private final URLStreamHandlerFactory parent;
+
+ public PlurlURLStreamHandlerFactory(URLStreamHandlerFactory parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol) {
+ if (protocol.equals(PLURL_PROTOCOL)) {
+ return this;
+ }
+ URLStreamHandler handler = createURLStreamHandlerImpl(protocol);
+ if (handler == null && parent != null) {
+ return parent.createURLStreamHandler(protocol);
+ }
+ return handler;
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return plurlOperation(u);
+ }
+
+ @Override
+ public void register(Object factory) {
+ add((URLStreamHandlerFactory) factory);
+ }
+
+ @Override
+ public void unregister(Object factory) {
+ remove((URLStreamHandlerFactory) factory);
+ }
+
+ @Override
+ public boolean isMultiplexing() {
+ return PlurlImpl.this.isMultiplexing(getURLStreamHandlerFactories());
+ }
+ }
+
+ public class PlurlContentHandlerFactory implements ContentHandlerFactory, LegacyFactory {
+ private final ContentHandlerFactory parent;
+
+ public PlurlContentHandlerFactory(ContentHandlerFactory parent) {
+ this.parent = parent;
+ }
+ @Override
+ public ContentHandler createContentHandler(String mimetype) {
+ ContentHandler fromParent = parent == null ? null : parent.createContentHandler(mimetype);
+ return createContentHandlerImpl(mimetype, fromParent);
+ }
+
+ @Override
+ public void register(Object factory) {
+ add((ContentHandlerFactory) factory);
+ }
+
+ @Override
+ public void unregister(Object factory) {
+ remove((ContentHandlerFactory) factory);
+ }
+
+ @Override
+ public boolean isMultiplexing() {
+ return PlurlImpl.this.isMultiplexing(getContentHandlerFactories());
+ }
+ }
+
+ private boolean checkPlurlProtocol() {
+ try {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_REGISTER_IMPLEMENTATION);
+ // plurl is already available; try registering our impl
+ @SuppressWarnings("unchecked")
+ Consumer