From b7fe1c64fd5d410701af48b3a14696beec830608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Sat, 29 Aug 2026 21:50:32 +0200 Subject: [PATCH] FELIX-6759 - PROTOTYPE: vendor plurl and adapt URLHandlers onto it Not for merge. This explores the suggestion in https://github.com/apache/felix-dev/pull/433#issuecomment-3073468820 that Felix and Equinox should share a cooperative way to manage the java.net.URL factory singletons. URLHandlers currently claims the JVM-wide URLStreamHandlerFactory by reflectively swapping a private static field on java.net.URL (SecureAction.swapStaticFieldIfNotClass). Getting a MethodHandles.Lookup trusted enough to do that is the only remaining reason the framework uses sun.misc.Unsafe, and it means the last framework to install itself wins the singleton, so two frameworks cannot coexist in one JVM without clobbering each other. Plurl installs a single cooperative router through the supported URL.setURLStreamHandlerFactory API and lets any number of parties register with it; each registered factory answers shouldHandle(Class) to claim the calling class. That maps onto what URLHandlers.getFrameworkFromContext() already does by hand, and it needs no Unsafe. - Vendors the 11 plurl sources from eclipse-osgi-technology/plurl (commit 6581777) into org.apache.felix.framework.plurl. The only change is the package rename; every license header and copyright notice is preserved verbatim. This mirrors https://github.com/eclipse-equinox/equinox/pull/848, which vendored the same files into org.eclipse.equinox.plurl. - Adds PlurlURLHandlers, adapting URLHandlers to PlurlStreamHandlerFactory and PlurlContentHandlerFactory, with shouldHandle(Class) recognising this framework's bundle class loaders. UNRESOLVED LICENSING QUESTION: every vendored file declares "SPDX-License-Identifier: EPL-2.0" and "Copyright (c) 2025 IBM Corporation", while plurl's own LICENSE and pom.xml declare Apache-2.0. EPL-2.0 is Category B at the ASF and may not be included in an Apache source release, so this cannot be merged as it stands. See framework/src/main/java/org/apache/felix/framework/plurl/README.md. Verified on JDK 25: the framework compiles and its tests are unchanged (the same 8 pre-existing platform specific failures as before this commit). Co-Authored-By: Claude Opus 4.8 --- .../felix/framework/PlurlURLHandlers.java | 124 ++ .../apache/felix/framework/plurl/Plurl.java | 308 ++++ .../plurl/PlurlContentHandlerFactory.java | 23 + .../felix/framework/plurl/PlurlFactory.java | 34 + .../framework/plurl/PlurlStreamHandler.java | 137 ++ .../plurl/PlurlStreamHandlerBase.java | 173 ++ .../plurl/PlurlStreamHandlerFactory.java | 39 + .../apache/felix/framework/plurl/README.md | 57 + .../felix/framework/plurl/impl/CallStack.java | 18 + .../felix/framework/plurl/impl/PlurlImpl.java | 1507 +++++++++++++++++ .../plurl/impl/SecurityManagerCallStack.java | 30 + .../plurl/impl/StackWalkerCallStack.java | 87 + .../framework/plurl/impl/URLToHandler.java | 74 + 13 files changed, 2611 insertions(+) create mode 100644 framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/README.md create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java create mode 100644 framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java 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; + +/** + *

+ * PROTOTYPE (FELIX-6759): adapts Felix' {@link URLHandlers} onto the plurl + * multiplexing URL factories. + *

+ *

+ * {@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: + * + *

+ * PlurlStreamHandlerFactory myStreamFactory = getStreamFactory();
+ * PlurlContentHandlerFactory myContentFactory = getContentFactory();
+ * 
+ * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/addURLStreamHandlerFactory").getContent()).accept(myStreamFactory);
+ * ((Consumer<ContentHandlerFactory>) ("plurl://op/addContentHandlerFactory").getContent()).accept(myContentFactory);
+ *
+ * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/removeURLStreamHandlerFactory").getContent())
+ * 		.accept(myStreamFactory);
+ * ((Consumer<ContentHandlerFactory>) ("plurl://op/removeContentHandlerFactory").getContent()).accept(myContentFactory);
+ * 
+ * + * 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: + * + *

+	 * ((Consumer<Object>) ("plurl://op/plurlRegisterImplementation").getContent()).accept(this);
+	 * 
+ * + * 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: + *

    + *
  1. 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}. + *
  2. 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}.
  3. + *
  4. The {@link #PLURL_PROTOCOL plurl} protocol is available for creating + * {@code URL} objects.
  5. + *
  6. If plurl factories are already installed then this plurl implementation + * is registered as a delegate with the already installed plurl instance.
  7. + *
+ * + * @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: + *

    + *
  1. Reset the original parent factories as the singleton factories of the + * JVM
  2. + *
  3. 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}.
  4. + *
  5. 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.
  6. + *
  7. This plurl instance must release all references to other factories or + * plurl instances.
  8. + *
+ * 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: + * + *
+	 * ((Consumer<Object>) ("plurl://op/plurlRegisterImplementation").getContent()).accept(this);
+	 * 
+ */ + 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: + * + *

+	 * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/addURLStreamHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @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: + * + *

+	 * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/removeURLStreamHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @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: + * + *

+	 * ((Consumer<ContentHandlerFactory>) ("plurl://op/addContentHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @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: + * + *

+	 * ((Consumer<ContentHandlerFactory>) ("plurl://op/removeContentHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @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: + * + *

+	 * --add-opens java.base/java.net=ALL-UNNAMED
+	 * 
+ * + * @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 addImpl = (Consumer) plurl.openConnection().getContent(); + addImpl.accept(this); + this.setFactories = SetFactories.plurlAlreadySet; + return true; + } catch (MalformedURLException e) { + // expected if there is no plurl installed yet. + return false; + } catch (IOException e) { + // could not add our implementation; move on + } + return true; + } + + public synchronized void install(String... forbidden) { + if (setFactories == SetFactories.override || setFactories == SetFactories.primordial) { + // already installed; no-op + return; + } + if (forbidden != null && forbidden.length > 0) { + Set forbiddenSet = new HashSet<>(Arrays.asList(forbidden)); + if (forbiddenSet.contains(PLURL_FORBID_NOTHING)) { + forbiddenProtocols = Collections.emptySet(); + } else { + forbiddenProtocols = forbiddenSet; + } + } + if (checkPlurlProtocol()) { + return; + } + + // sync on URLConnection to prevent more than one thread from setting plurl + synchronized (URLConnection.class) { + // try again with lock + if (checkPlurlProtocol()) { + return; + } + boolean contentHandlerFactorySet = false; + try { + URLConnection.setContentHandlerFactory(new PlurlContentHandlerFactory(null)); + contentHandlerFactorySet = true; + URL.setURLStreamHandlerFactory(new PlurlURLStreamHandlerFactory(null)); + setFactories = SetFactories.primordial; + } catch (Throwable t) { + try { + forceFactories(t, contentHandlerFactorySet); + setFactories = SetFactories.override; + } catch (Exception e) { + String message = "Cannot install the plurl factories. " //$NON-NLS-1$ + + "The java.base module must be configured to open the java.net package for reflection " //$NON-NLS-1$ + + "in order to allow plurl to replace the existing factories. " //$NON-NLS-1$ + + "For example, by using the JVM option: '--add-opens java.base/java.net=ALL-UNNAMED'. "; //$NON-NLS-1$ + throw new IllegalStateException(message, e); + } + } + } + } + + private boolean unregisterPlurlImpl() { + try { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_UNREGISTER_IMPLEMENTATION); + // plurl is already available; try adding ours impl + @SuppressWarnings("unchecked") + Consumer removeImpl = (Consumer) plurl.openConnection().getContent(); + removeImpl.accept(this); + return true; + } catch (MalformedURLException e) { + // expected if there is no plurl installed + return false; + } catch (IOException e) { + // could not remove our implementation; move on + } + return true; + } + + @Override + public synchronized void uninstall() { + if (setFactories == SetFactories.notInstalled) { + // not installed; do nothing + return; + } + if (setFactories == SetFactories.primordial) { + // we don't let primordial Plurls get uninstalled; + // this is a no-op; + return; + } + if (setFactories == SetFactories.plurlAlreadySet) { + // some other plurl is set; unregister ours + unregisterPlurlImpl(); + setFactories = SetFactories.notInstalled; + return; + } + + // set this plurl as not installed + setFactories = SetFactories.notInstalled; + + // find a designate + Object designate = null; + PlurlImplHolder nextHolder = null; + Iterator iHolders = plurlImpls.iterator(); + while (iHolders.hasNext()) { + PlurlImplHolder next = iHolders.next(); + iHolders.remove(); + // pin designate object to avoid GC + designate = next.getImpl(); + if (designate != null) { + nextHolder = next; + break; + } + } + + Exception forceError = null; + try { + forceURLStreamHandlerFactory(false, parentURLStreamHandlerFactory); + } catch (Exception e) { + // this is unexpected since we forced the plurl at install + forceError = e; + } + try { + forceContentHandlerFactory(false, parentContentHandlerFactory); + } catch (Exception e) { + // this is unexpected since we forced the plurl at install + if (forceError != null) { + e.addSuppressed(forceError); + } + forceError = e; + } + if (forceError != null) { + // again, this would be very unexpected since we forced plurl at install + throw new RuntimeException(forceError); + } + + if (nextHolder != null) { + // found a designate; now force it to be installed + nextHolder.install(); + } + // Hack to make sure the designate isn't GC'ed before we call install by using + // the reference after; otherwise the JVM could determine the variable is out of + // scope and therefore allow it to be GC'ed + if (designate != null) { + designate.hashCode(); + } + } + + private void forceFactories(Throwable t, boolean contentHandlerFactorySet) throws Exception { + try { + if (!contentHandlerFactorySet) { + forceContentHandlerFactory(true, null); + } + forceURLStreamHandlerFactory(true, null); + } catch (Exception e) { + e.addSuppressed(t); + throw e; + } + } + + private void forceURLStreamHandlerFactory(boolean installPlurl, URLStreamHandlerFactory forceBack) + throws Exception { + Field factoryField = getStaticField(URL.class, URLStreamHandlerFactory.class); + if (factoryField == null) { + throw new Exception("Could not find URLStreamHandlerFactory field"); //$NON-NLS-1$ + } + // look for a lock to synchronize on + Object lock = getURLStreamHandlerFactoryLock(); + synchronized (lock) { + URLStreamHandlerFactory toSet = installPlurl ? (URLStreamHandlerFactory) factoryField.get(null) : forceBack; + if (installPlurl) { + parentURLStreamHandlerFactory = toSet; + // current factory does not support plurl, ok we'll wrap it + toSet = new PlurlURLStreamHandlerFactory(toSet); + } + + factoryField.set(null, null); + // always attempt to clear the handlers cache + // This allows an optimization for the single framework use-case + resetURLStreamHandlers(); + URL.setURLStreamHandlerFactory(toSet); + } + } + + private Object getURLStreamHandlerFactoryLock() throws IllegalAccessException { + Object lock; + try { + Field streamHandlerLockField = URL.class.getDeclaredField("streamHandlerLock"); //$NON-NLS-1$ + streamHandlerLockField.setAccessible(true); + lock = streamHandlerLockField.get(null); + } catch (NoSuchFieldException noField) { + // could not find the lock, lets sync on the class object + lock = URL.class; + } + return lock; + } + + private void resetURLStreamHandlers() throws IllegalAccessException { + Field handlersField = getStaticField(URL.class, Hashtable.class); + if (handlersField != null) { + @SuppressWarnings("rawtypes") + Hashtable handlers = (Hashtable) handlersField.get(null); + if (handlers != null) { + handlers.clear(); + } + } + } + + private void forceContentHandlerFactory(boolean installPlurl, ContentHandlerFactory forceBack) throws Exception { + Field factoryField = getStaticField(URLConnection.class, java.net.ContentHandlerFactory.class); + if (factoryField == null) { + throw new Exception("Could not find ContentHandlerFactory field"); //$NON-NLS-1$ + } + + ContentHandlerFactory toSet = installPlurl ? (ContentHandlerFactory) factoryField.get(null) : forceBack; + if (installPlurl) { + parentContentHandlerFactory = toSet; + // current factory does not support plurl, ok we'll wrap it + toSet = new PlurlContentHandlerFactory(toSet); + } + // null out the field so that we can successfully call setContentHandlerFactory + factoryField.set(null, null); + // always attempt to clear the handlers cache + // This allows an optimization for the single framework use-case + resetContentHandlers(); + URLConnection.setContentHandlerFactory(toSet); + } + + private void resetContentHandlers() throws IllegalAccessException { + Field handlersField = getStaticField(URLConnection.class, Hashtable.class); + if (handlersField != null) { + @SuppressWarnings("rawtypes") + Hashtable handlers = (Hashtable) handlersField.get(null); + if (handlers != null) { + handlers.clear(); + } + } + } + + public Field getStaticField(Class clazz, Class type) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (isStatic && field.getType().equals(type)) { + field.setAccessible(true); + return field; + } + } + return null; + } + + public PlurlImpl() { + builtinContentHandlerFactoryLoader = ServiceLoader.load(ContentHandlerFactory.class); + builtinURLStreamHandlerFactoryLoader = ServiceLoader.load(URLStreamHandlerFactory.class); + callStack = createCallStack(); + } + + private CallStack createCallStack() { + try { + Class.forName("java.lang.StackWalker"); //$NON-NLS-1$ + return new StackWalkerCallStack(); + } catch (ClassNotFoundException e) { + return new SecurityManagerCallStack(); + } + } + + ContentHandler createContentHandlerImpl(String mimetype, ContentHandler fromParent) { + ContentHandler builtin = findBuiltInContentHandler(mimetype); + if (builtin != null) { + return builtin; + } + // Never return null for content handlers because then + // we will never get called again. + return new PlurlRootContentHandler(mimetype, fromParent); + } + + URLStreamHandler createURLStreamHandlerImpl(String protocol) { + if (forbiddenProtocols.contains(protocol)) { + // to dangerous for these to be overridden + return null; + } + // Check if we are recursing + if (isRecursive(protocol)) { + return null; + } + try { + try { + URLStreamHandler builtin = findBuiltinURLStreamHandler(protocol); + if (builtin != null) { + return builtin; + } + } catch (UnsupportedOperationException e) { + // check if it is a reflective error. If so then we know there is a built-in + // protocol and we know we can never replace it. Let the JVM handle it. + if (e.getCause() instanceof ReflectiveOperationException) { + return null; + } + } + URLStreamHandlerFactoryHolder factoryHolder = findFactory(getURLStreamHandlerFactories()); + if (factoryHolder != null) { + PlurlStreamHandler shouldHandle = factoryHolder.getHandler(protocol); + if (shouldHandle != null) { + return new PlurlRootURLStreamHandler(protocol); + } + } + // Return null if nothing found that should handle the protocol; + // We will get called again if the protocol is asked for again. + return null; + } finally { + releaseRecursive(protocol); + } + } + + + private ContentHandler findBuiltInContentHandler(String mimetype) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ContentHandler run() { + return findBuiltinContentHandlerImpl(mimetype); + } + }); + } + + private URLStreamHandler findBuiltinURLStreamHandler(String protocol) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public URLStreamHandler run() { + return findBuiltinURLStreamHandlerImpl(protocol); + } + }); + } + + ContentHandler findBuiltinContentHandlerImpl(String contentType) { + // first check service loader + for (ContentHandlerFactory f : builtinContentHandlerFactoryLoader) { + ContentHandler h = f.createContentHandler(contentType); + if (h != null) { + return h; + } + } + // now check property + String builtInHandlers = System.getProperty(CONTENT_HANDLER_PKGS); + builtInHandlers = builtInHandlers == null ? DEFAULT_VM_CONTENT_HANDLERS + : DEFAULT_VM_CONTENT_HANDLERS + '|' + builtInHandlers; + + // replace '/' with a '.' and all characters not allowed in a java class name + // with a '_'. + String convertedContentType = contentType.replace('.', '_'); + convertedContentType = convertedContentType.replace('/', '.'); + convertedContentType = convertedContentType.replace('-', '_'); + StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$ + while (tok.hasMoreElements()) { + StringBuilder name = new StringBuilder(); + name.append(tok.nextToken()); + name.append("."); //$NON-NLS-1$ + name.append(convertedContentType); + try { + Class clazz = null; + try { + clazz = Class.forName(name.toString()); + } catch (ClassNotFoundException e) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + if (cl != null) { + clazz = cl.loadClass(name.toString()); + } + } + if (clazz != null) { + return (ContentHandler) clazz.getConstructor().newInstance(); + } + } catch (Exception ex) { + // handle all exceptions here and move on + } + } + return null; + } + + URLStreamHandler findBuiltinURLStreamHandlerImpl(String protocol) { + // first check service loader + for (URLStreamHandlerFactory f : builtinURLStreamHandlerFactoryLoader) { + URLStreamHandler h = f.createURLStreamHandler(protocol); + if (h != null) { + return h; + } + } + // now check property + String builtInHandlers = System.getProperty(PROTOCOL_HANDLER_PKGS); + if (builtInHandlers == null) + return null; + + StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$ + while (tok.hasMoreElements()) { + URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, tok.nextToken()); + if (found != null) { + return found; + } + } + return null; + } + + URLStreamHandler findBuildinURLStreamHandlerImpl(String protocol, String inPackage) { + Class clazz = null; + StringBuilder name = new StringBuilder(); + name.append(inPackage); + name.append("."); //$NON-NLS-1$ + name.append(protocol); + name.append(".Handler"); //$NON-NLS-1$ + try { + try { + clazz = Class.forName(name.toString()); + } catch (ClassNotFoundException e) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + if (cl != null) { + try { + clazz = cl.loadClass(name.toString()); + } catch (ClassNotFoundException e2) { + // ignore + } + } + } + if (clazz != null) { + return (URLStreamHandler) clazz.getConstructor().newInstance(); + } + } catch (ReflectiveOperationException e) { + // probably because the package isn't open for reflection + String message = "The module for class '" + clazz.getName() + "' must be configured to open the '" //$NON-NLS-1$ //$NON-NLS-2$ + + inPackage + '.' + protocol + "' package for reflection to support the handler." //$NON-NLS-1$ + + " For example, by using the JVM option: '--add-opens java.base/" + inPackage + '.' + protocol //$NON-NLS-1$ + + "=ALL-UNNAMED'."; //$NON-NLS-1$ + throw new UnsupportedOperationException(message, e); + } catch (Exception ex) { + // handle all exceptions here and move on + } + return null; + } + + URLConnection plurlOperation(URL u) { + final String path = u.getPath(); + return new URLConnection(u) { + @Override + public void connect() throws IOException { + // do nothing + } + + @Override + public Consumer getContent() throws IOException { + switch (path) { + case PLURL_ADD_URL_STREAM_HANDLER_FACTORY: + return (f) -> add((URLStreamHandlerFactory) f); + case PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY: + return (f) -> remove((URLStreamHandlerFactory) f); + case PLURL_ADD_CONTENT_HANDLER_FACTORY: + return (f) -> add((ContentHandlerFactory) f); + case PLURL_REMOVE_CONTENT_HANDLER_FACTORY: + return (f) -> remove((ContentHandlerFactory) f); + case PLURL_REGISTER_IMPLEMENTATION: + return (p) -> addImpl(p); + case PLURL_UNREGISTER_IMPLEMENTATION: + return (p) -> removeImpl(p); + default: + throw new IOException("Unknown plurl operation: " + path); //$NON-NLS-1$ + } + } + }; + } + + boolean isMultiplexing(List factories) { + return factories.size() > 1; + } + + synchronized void addImpl(Object p) { + if (setFactories == SetFactories.primordial) { + // If this plurl is the primordial factory (no singletons set in the JVM) + // then we don't track other plurl installs because we will never + // remove this plurl from the JVM singleton + return; + } + if (p == this) { + // someone called install again on this Plurl; ignore it + return; + } + List updated = new ArrayList<>(plurlImpls); + // remove any GC'ed handlers or the new impl (incase it is being added again) + updated.removeIf((h) -> h.getImpl() == null || h.getImpl() == p); + // add new impl + updated.add(new PlurlImplHolder(p)); + plurlImpls = updated; + } + + synchronized void removeImpl(Object p) { + List updated = new ArrayList<>(plurlImpls); + // remove the impl and any that got GC'ed + updated.removeIf((h) -> h.getImpl() == p || h.getImpl() == null); + plurlImpls = updated.isEmpty() ? Collections.emptyList() : updated; + } + + synchronized void add(URLStreamHandlerFactory f) { + List updated = new ArrayList<>(streamHandlerFactories); + // remove any GC'ed handlers + updated.removeIf((h) -> h.getFactory() == null); + // add new holder + updated.add(new URLStreamHandlerFactoryHolder(f)); + streamHandlerFactories = updated; + } + + synchronized void remove(URLStreamHandlerFactory f) { + List updated = new ArrayList<>(streamHandlerFactories); + // remove the factory and any that got GC'ed + updated.removeIf((h) -> { + if (h.getFactory() == f || h.getFactory() == null) { + return true; + } + return false; + }); + streamHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated; + } + + synchronized List getURLStreamHandlerFactories() { + return streamHandlerFactories; + } + + synchronized List getContentHandlerFactories() { + return contentHandlerFactories; + } + + synchronized void add(ContentHandlerFactory f) { + List updated = new ArrayList<>(contentHandlerFactories); + // remove any GC'ed handlers + updated.removeIf((h) -> h.getFactory() == null); + // add new holder + updated.add(new ContentHandlerFactoryHolder(f)); + contentHandlerFactories = updated; + } + + synchronized void remove(ContentHandlerFactory f) { + List updated = new ArrayList<>(contentHandlerFactories); + // remove the factory and any that got GC'ed + updated.removeIf((h) -> { + if (h.getFactory() == f || h.getFactory() == null) { + return true; + } + return false; + }); + contentHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated; + } + + ContentHandler findContentHandler(String contentType) { + ContentHandlerFactoryHolder f = findFactory(getContentHandlerFactories()); + if (f != null) { + return f.getHandler(contentType); + } + return null; + } + + PlurlStreamHandler findPlurlStreamHandler(String protocol) { + URLStreamHandlerFactoryHolder f = findFactory(getURLStreamHandlerFactories()); + if (f != null) { + return f.getHandler(protocol); + } + return null; + } + + private F findFactory(List factories) { + int numFactories = factories.size(); + if (numFactories == 1) { + // Handle common case of only one; just use it + return factories.get(0); + } + Class[] callStackClasses = getCallStack(); + for (Class stack : callStackClasses) { + String pName = getPackageName(stack); + if (THIS_PACKAGE.equals(pName) || isSystemClass(pName, stack)) { + continue; + } + for (F f : factories) { + boolean shouldHandle = false; + if (f instanceof PlurlFactory) { + shouldHandle = ((PlurlFactory) f).shouldHandle(stack); + } else { + // use reflection in case this Plurl package isn't visible to the factory impl + try { + shouldHandle = (boolean) findShouldHandle(f.getClass()).invoke(f, stack); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (shouldHandle) { + return f; + } + } + } + // Instead of returning null here, the "first" factory is returned; + // This means the root or "first" factory may provide protocol handlers for call stacks + // that have no classes known to that factory + return numFactories > 0 ? factories.get(0) : null; + } + + Method findShouldHandle(Class clazz) throws NoSuchMethodException { + Method shouldHandle = null; + try { + shouldHandle = clazz.getMethod("shouldHandle", Class.class); //$NON-NLS-1$ + } catch (NoSuchMethodException e) { + // check for legacy hasAuthority method + try { + shouldHandle = clazz.getMethod("hasAuthority", Class.class); //$NON-NLS-1$ + } catch (NoSuchMethodException e1) { + throw e; + } + } + shouldHandle.setAccessible(true); + return shouldHandle; + } + + private String getPackageName(Class clazz) { + String name = clazz.getName(); + int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + return name.substring(0, lastDot); + } + return ""; //$NON-NLS-1$ + } + + private Class[] getCallStack() { + return callStack.getClassContext(); + } + + class PlurlRootContentHandler extends ContentHandler { + private final String contentType; + private final ContentHandler fromParent; + + PlurlRootContentHandler(String contentType, ContentHandler fromParent) { + this.contentType = contentType; + this.fromParent = fromParent; + } + + @Override + public Object getContent(URLConnection uConn) throws IOException { + ContentHandler handler = findContentHandler(contentType); + if (handler != null) { + return handler.getContent(uConn); + } + if (fromParent != null) { + return fromParent.getContent(uConn); + } + return uConn.getInputStream(); + } + } + + public abstract class PlurlFactoryHolder implements PlurlFactory { + private final WeakReference factory; + private final Map handlers = new ConcurrentHashMap<>(); + private final Method shouldHandleMethod; + + public PlurlFactoryHolder(F factory) { + this.factory = new WeakReference<>(factory); + if (factory instanceof PlurlFactory) { + shouldHandleMethod = null; + } else { + try { + shouldHandleMethod = findShouldHandle(factory.getClass()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + F getFactory() { + return factory.get(); + } + + @Override + public boolean shouldHandle(Class clazz) { + F f = factory.get(); + if (f == null) { + return false; + } + if (shouldHandleMethod != null) { + try { + return (boolean) shouldHandleMethod.invoke(f, clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return ((PlurlFactory) f).shouldHandle(clazz); + } + + H getHandler(String type) { + final F f = factory.get(); + if (f == null) { + // clear handlers + handlers.clear(); + // remove GC'ed holders + remove(null); + return null; + } + return handlers.computeIfAbsent(type, (t) -> createHandler(t, f)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + factory.get() + ']' + + handlers; + } + + protected abstract H createHandler(String type, F f); + + protected abstract void remove(F f); + } + + class PlurlImplHolder { + private final WeakReference plurlImpl; + + PlurlImplHolder(Object plurlImpl) { + this.plurlImpl = new WeakReference<>(plurlImpl); + } + + public void install() { + // should be able to reflect on the plurl instance because install is public + Object currentPlurl = plurlImpl.get(); + // should never be null since we pinned the object before calling + try { + Method install = currentPlurl.getClass().getMethod("install", String[].class); //$NON-NLS-1$ + install.invoke(currentPlurl, (Object) forbiddenProtocols.toArray(new String[0])); + } catch (NoSuchMethodException e) { + // should never happen + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); + } + synchronized (PlurlImpl.this) { + try { + URL registerPlurl = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_REGISTER_IMPLEMENTATION); + for (PlurlImplHolder plurlImplHolder : plurlImpls) { + Object p = plurlImplHolder.getImpl(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) registerPlurl.openConnection().getContent(); + addImpl.accept(p); + } + URL addContentHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_ADD_CONTENT_HANDLER_FACTORY); + for (ContentHandlerFactoryHolder contentHandlerFactoryHolder : contentHandlerFactories) { + ContentHandlerFactory c = contentHandlerFactoryHolder.getFactory(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) addContentHandlerFactory.openConnection() + .getContent(); + addImpl.accept(c); + } + URL addURLStreamHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP, + PLURL_ADD_URL_STREAM_HANDLER_FACTORY); + for (URLStreamHandlerFactoryHolder urlStreamHandlerFactoryHolder : streamHandlerFactories) { + URLStreamHandlerFactory s = urlStreamHandlerFactoryHolder.getFactory(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) addURLStreamHandlerFactory.openConnection() + .getContent(); + addImpl.accept(s); + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + plurlImpls.clear(); + contentHandlerFactories.clear(); + streamHandlerFactories.clear(); + parentContentHandlerFactory = null; + parentURLStreamHandlerFactory = null; + } + } + + } + + public Object getImpl() { + return plurlImpl.get(); + } + } + + public class ContentHandlerFactoryHolder extends PlurlFactoryHolder { + public ContentHandlerFactoryHolder(ContentHandlerFactory factory) { + super(factory); + } + + @Override + protected ContentHandler createHandler(String mimetype, ContentHandlerFactory f) { + return f.createContentHandler(mimetype); + } + + @Override + protected void remove(ContentHandlerFactory f) { + PlurlImpl.this.remove(f); + } + } + + public class URLStreamHandlerFactoryHolder extends PlurlFactoryHolder { + public URLStreamHandlerFactoryHolder(URLStreamHandlerFactory factory) { + super(factory); + } + + @Override + protected PlurlStreamHandler createHandler(String protocol, URLStreamHandlerFactory f) { + URLStreamHandler handler = f.createURLStreamHandler(protocol); + if (handler == null) { + return null; + } + if (handler instanceof PlurlStreamHandler) { + return (PlurlStreamHandler) handler; + } + PlurlStreamHandler proxyPlurlStreamHandler = newProxyPlurlStreamHandler(handler); + if (proxyPlurlStreamHandler != null) { + return proxyPlurlStreamHandler; + } + return new PlurlStreamHandlerReflective(handler); + } + + @Override + protected void remove(URLStreamHandlerFactory f) { + PlurlImpl.this.remove(f); + } + } + + PlurlStreamHandler newProxyPlurlStreamHandler(URLStreamHandler handler) { + Class checkClass = handler.getClass(); + while (checkClass != null) { + for (Class handlerInterfaces : checkClass.getInterfaces()) { + // To determine if we can proxy the handler we look for an interface that defines a method + // public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit); + // But we cannot search for it the conventional way because the PlurlSetter may be a different + // copy from ours. + for (Method m : handlerInterfaces.getMethods()) { + if (m.getName().equals("parseURL")) { //$NON-NLS-1$ + Class[] params = m.getParameterTypes(); + if (params.length == 5) { + // check the first param to see if it is a PlurlSetter candidate + Class plurlSetterCandidate = params[0]; + if (plurlSetterCandidate.isInterface()) { + try { + // try finding the appropriate setURL method + plurlSetterCandidate.getMethod("setURL", URL.class, String.class, String.class, Integer.TYPE, String.class, String.class, + String.class, String.class, String.class); + } catch (Exception e) { + // move on to the next interface + continue; + } + } + } else { + // Wrong number of arguments for parseURL; move on to next interface + continue; + } + Class plurlStreamHandlerClass = handlerInterfaces; + Class plurlSetterClass = m.getParameterTypes()[0]; + return new PlurlStreamHandlerProxy(handler, plurlStreamHandlerClass, plurlSetterClass); + } + } + } + checkClass = checkClass.getSuperclass(); + } + return null; + } + + static class PlurlStreamHandlerProxy extends URLStreamHandler implements PlurlStreamHandler { + private final URLStreamHandler handler; + private final Class plurlSetterClass; + private final Method equals; + private final Method getDefaultPort; + private final Method getHostAddress; + private final Method hashCode; + private final Method hostsEqual; + private final Method openConnection; + private final Method openConnectionProxy; + private final Method parseURL; + private final Method sameFile; + private final Method toExternalForm; + final Method setURL; + final Method setURLDeprecated; + + public PlurlStreamHandlerProxy(URLStreamHandler handler, Class plurlUrlHandlerClass, + Class plurlSetterClass) { + this.handler = handler; + this.plurlSetterClass = plurlSetterClass; + openConnection = findMethod(plurlUrlHandlerClass, "openConnection", URL.class); //$NON-NLS-1$ + openConnectionProxy = findMethod(plurlUrlHandlerClass, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$ + parseURL = findMethod(plurlUrlHandlerClass, "parseURL", plurlSetterClass, URL.class, String.class, //$NON-NLS-1$ + Integer.TYPE, Integer.TYPE); + equals = findMethod(plurlUrlHandlerClass, "equals", URL.class, URL.class); //$NON-NLS-1$ + getDefaultPort = findMethod(plurlUrlHandlerClass, "getDefaultPort"); //$NON-NLS-1$ + getHostAddress = findMethod(plurlUrlHandlerClass, "getHostAddress", URL.class); //$NON-NLS-1$ + hashCode = findMethod(plurlUrlHandlerClass, "hashCode", URL.class); //$NON-NLS-1$ + hostsEqual = findMethod(plurlUrlHandlerClass, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$ + sameFile = findMethod(plurlUrlHandlerClass, "sameFile", URL.class, URL.class); //$NON-NLS-1$ + toExternalForm = findMethod(plurlUrlHandlerClass, "toExternalForm", URL.class); //$NON-NLS-1$ + setURL = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, int.class, //$NON-NLS-1$ + String.class, String.class, String.class, String.class, String.class); + setURLDeprecated = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, //$NON-NLS-1$ + int.class, String.class, String.class); + } + + private static Method findMethod(Class plurlUrlHandlerClass, String methodName, Class... args) { + Method result = null; + try { + result = plurlUrlHandlerClass.getDeclaredMethod(methodName, args); + } catch (Exception e) { + throw new RuntimeException(e); + } + return result; + } + + Object invoke(Method m, Object... args) { + try { + return m.invoke(handler, args); + } catch (InvocationTargetException e) { + throw (RuntimeException) e.getTargetException(); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public boolean equals(URL u1, URL u2) { + return (boolean) invoke(equals, u1, u2); + } + + @Override + public int hashCode(URL u) { + return (int) invoke(hashCode, u); + } + + @Override + public boolean hostsEqual(URL u1, URL u2) { + return (boolean) invoke(hostsEqual, u1, u2); + } + + @Override + public int getDefaultPort() { + return (int) invoke(getDefaultPort); + } + + @Override + public InetAddress getHostAddress(URL u) { + return (InetAddress) invoke(getHostAddress, u); + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + return (URLConnection) invoke(openConnection, u); + } + + @Override + public URLConnection openConnection(URL u, Proxy p) throws IOException { + return (URLConnection) invoke(openConnectionProxy, u, p); + } + + @Override + public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) { + setHandler(u, handler); + Object plurlSetterProxy = null; + if (plurlSetter != null) { + plurlSetterProxy = java.lang.reflect.Proxy.newProxyInstance(handler.getClass().getClassLoader(), + new Class[] { plurlSetterClass }, new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("setURL".equals(method.getName())) { //$NON-NLS-1$ + if (args.length == 9) { + plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2], (int) args[3], + (String) args[4], (String) args[5], (String) args[6], (String) args[7], + (String) args[8]); + } else { + plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2], + (int) args[3], null, null, (String) args[4], null, (String) args[5]); + } + } + return null; + } + }); + } + invoke(parseURL, plurlSetterProxy, u, spec, start, limit); + } + + @Override + public boolean sameFile(URL u1, URL u2) { + return (boolean) invoke(sameFile, u1, u2); + } + + @Override + public String toExternalForm(URL u) { + return (String) invoke(toExternalForm, u); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + invoke(setURL, u, protocol, host, port, authority, userInfo, path, query, ref); + } + + @SuppressWarnings("deprecation") + @Override + public void setURL(URL u, String protocol, String host, int port, String file, String ref) { + invoke(setURLDeprecated, u, protocol, host, port, file, ref); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']'; + } + } + + static class PlurlStreamHandlerReflective extends URLStreamHandler implements PlurlStreamHandler { + private final URLStreamHandler handler; + private final Method openConnectionMethod; + private final Method openConnectionProxyMethod; + private final Method parseURLMethod; + private final Method equalsMethod; + private final Method getDefaultPortMethod; + private final Method getHostAddressMethod; + private final Method hashCodeMethod; + private final Method hostsEqualMethod; + private final Method sameFileMethod; + private final Method toExternalFormMethod; + + public PlurlStreamHandlerReflective(URLStreamHandler handler) { + this.handler = handler; + openConnectionMethod = findMethod(handler, "openConnection", URL.class); //$NON-NLS-1$ + openConnectionProxyMethod = findMethod(handler, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$ + parseURLMethod = findMethod(handler, "parseURL", URL.class, String.class, Integer.TYPE, Integer.TYPE); //$NON-NLS-1$ + equalsMethod = findMethod(handler, "equals", URL.class, URL.class); //$NON-NLS-1$ + getDefaultPortMethod = findMethod(handler, "getDefaultPort"); //$NON-NLS-1$ + getHostAddressMethod = findMethod(handler, "getHostAddress", URL.class); //$NON-NLS-1$ + hashCodeMethod = findMethod(handler, "hashCode", URL.class); //$NON-NLS-1$ + hostsEqualMethod = findMethod(handler, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$ + sameFileMethod = findMethod(handler, "sameFile", URL.class, URL.class); //$NON-NLS-1$ + toExternalFormMethod = findMethod(handler, "toExternalForm", URL.class); //$NON-NLS-1$ + if (URL_HANDLER_FIELD == null) { + throw new RuntimeException(getReflectionErrorMessage(handler.getClass())); + } + } + + @SuppressWarnings("unchecked") + private Object invoke(Method m, Object... args) throws T { + try { + return m.invoke(handler, args); + } catch (InvocationTargetException e) { + throw (T) e.getTargetException(); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + private static String getReflectionErrorMessage(Class handlerClass) { + return "The java.base module must be configured to open the java.net package for reflection to support the handler of type " //$NON-NLS-1$ + + '\'' + handlerClass.getName() + "'." //$NON-NLS-1$ + + " For example, by using the JVM option: '--add-opens java.base/java.net=ALL-UNNAMED'. " //$NON-NLS-1$ + + "Another option is to make the class '" + handlerClass.getName() //$NON-NLS-1$ + + "' implement the org.eclipse.equinox.purl.PlurlStreamHandler interface."; //$NON-NLS-1$ + } + + private static Method findMethod(URLStreamHandler h, String methodName, Class... args) { + Method result = null; + Class handlerClass = h.getClass(); + try { + result = handlerClass.getDeclaredMethod(methodName, args); + result.setAccessible(true); + } catch (Exception e1) { + try { + result = URLStreamHandler.class.getDeclaredMethod(methodName, args); + result.setAccessible(true); + } catch (Exception e2) { + // fallback reflection is blocked by Java modules + String message = getReflectionErrorMessage(handlerClass); + throw new RuntimeException(message, e2); + } + } + return result; + } + + @Override + public boolean equals(URL u1, URL u2) { + return (Boolean) invoke(equalsMethod, u1, u2); + } + + @Override + public int hashCode(URL u) { + return (Integer) invoke(hashCodeMethod, u); + } + + @Override + public boolean hostsEqual(URL u1, URL u2) { + return (Boolean) invoke(hostsEqualMethod, u1, u2); + } + + @Override + public int getDefaultPort() { + return (Integer) invoke(getDefaultPortMethod); + } + + @Override + public InetAddress getHostAddress(URL u) { + return (InetAddress) invoke(getHostAddressMethod, u); + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + return (URLConnection) invoke(openConnectionMethod, u); + } + + @Override + public URLConnection openConnection(URL u, Proxy p) throws IOException { + return (URLConnection) invoke(openConnectionProxyMethod, u, p); + } + + @Override + public boolean sameFile(URL u1, URL u2) { + return (Boolean) invoke(sameFileMethod, u1, u2); + } + + @Override + public String toExternalForm(URL u) { + return (String) invoke(toExternalFormMethod, u); + } + + @Override + public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) { + try { + setHandler(u, handler); + invoke(parseURLMethod, u, spec, start, limit); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("deprecation") + @Override + public void setURL(URL u, String protocol, String host, int port, String file, String ref) { + super.setURL(u, protocol, host, port, file, ref); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']'; + } + } + + static final PlurlStreamHandler NULL_HANDLER = new PlurlStreamHandlerBase() { + @Override + public URLConnection openConnection(URL u) throws IOException { + throw new UnsupportedOperationException(); + } + }; + + class PlurlRootURLStreamHandler extends URLStreamHandler implements PlurlSetter { + private final String protocol; + private final AtomicReference builtin = new AtomicReference<>(); + + private PlurlStreamHandler lookupPlurlStreamHandler(URL u) { + if (u != null && isMultiplexing(getURLStreamHandlerFactories())) { + // Record the handler found for the URL; + // This allows to consistently use the same handler for the + // life of the URL object when we are multiplexing. + return urlToHandler.get(u, this::findPlurlStreamHandlerImpl); + } + return findPlurlStreamHandlerImpl(); + } + + private PlurlStreamHandler findPlurlStreamHandlerImpl() { + PlurlStreamHandler h = findPlurlStreamHandler(protocol); + if (h == null) { + h = findBuiltin(); + if (h == null) { + throw new IllegalStateException("No handler found for protocol: " + protocol); //$NON-NLS-1$ + } + } + return h; + } + + private PlurlStreamHandler findBuiltin() { + PlurlStreamHandler result = builtin.updateAndGet((h) -> { + URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, "sun.net.www.protocol"); //$NON-NLS-1$ + if (found == null) { + return NULL_HANDLER; + } + // we can only really do this if java.net is open for reflection + return new PlurlStreamHandlerReflective(found); + }); + return (result == NULL_HANDLER) ? null : result; + } + + PlurlRootURLStreamHandler(String protocol) { + this.protocol = protocol; + } + + @Override + protected boolean equals(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).equals(u1, u2); + } + + @Override + protected int hashCode(URL u) { + return lookupPlurlStreamHandler(u).hashCode(u); + } + + @Override + protected boolean hostsEqual(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).hostsEqual(u1, u2); + } + + @Override + protected int getDefaultPort() { + return lookupPlurlStreamHandler(null).getDefaultPort(); + } + + @Override + protected InetAddress getHostAddress(URL u) { + return lookupPlurlStreamHandler(u).getHostAddress(u); + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return lookupPlurlStreamHandler(u).openConnection(u); + } + + @Override + protected URLConnection openConnection(URL u, Proxy p) throws IOException { + return lookupPlurlStreamHandler(u).openConnection(u, p); + } + + @Override + protected void parseURL(URL u, String spec, int start, int limit) { + PlurlStreamHandler h = lookupPlurlStreamHandler(u); + if (setHandler(u, h)) { + h.parseURL(null, u, spec, start, limit); + } else { + h.parseURL(this, u, spec, start, limit); + } + } + + @Override + protected boolean sameFile(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).sameFile(u1, u2); + } + + @Override + protected String toExternalForm(URL u) { + return lookupPlurlStreamHandler(u).toExternalForm(u); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref); + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java new file mode 100644 index 0000000000..6558ac7951 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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; + +class SecurityManagerCallStack implements CallStack { + // used to get access to the protected SecurityManager#getClassContext method + static class InternalSecurityManager extends SecurityManager { + @Override + public Class[] getClassContext() { + return super.getClassContext(); + } + } + + private static InternalSecurityManager internalSecurityManager = new InternalSecurityManager(); + + public Class[] getClassContext() { + return internalSecurityManager.getClassContext(); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java new file mode 100644 index 0000000000..374ae4b591 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * 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.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +class StackWalkerCallStack implements CallStack { + + static final Class stackWalkerClass; + static final Object stackWalker; + static final Method forEach; + static final Method getDeclaringClass; + static final CallStack fallback; + static { + Class tmpStackWalkerClass = null; + Object tmpStackWalker = null; + Method tmpForEach = null; + Method tmpGetDeclaringClass = null; + CallStack tmpFallback = null; + try { + Class stackWalkerOptionClass = Class.forName("java.lang.StackWalker$Option"); //$NON-NLS-1$ + @SuppressWarnings({ "unchecked", "rawtypes" }) + Object RETAIN_CLASS_REFERENCE = Enum.valueOf((Class) stackWalkerOptionClass, "RETAIN_CLASS_REFERENCE"); //$NON-NLS-1$ + tmpStackWalkerClass = Class.forName("java.lang.StackWalker"); //$NON-NLS-1$ + tmpStackWalker = tmpStackWalkerClass.getMethod("getInstance", stackWalkerOptionClass).invoke(null, //$NON-NLS-1$ + RETAIN_CLASS_REFERENCE); + tmpForEach = tmpStackWalkerClass.getMethod("forEach", Consumer.class); //$NON-NLS-1$ + tmpGetDeclaringClass = Class.forName("java.lang.StackWalker$StackFrame").getMethod("getDeclaringClass"); //$NON-NLS-1$ //$NON-NLS-2$ + } catch (Throwable t) { + // null all out + tmpStackWalkerClass = null; + tmpStackWalker = null; + tmpForEach = null; + tmpGetDeclaringClass = null; + // fallback to security manager + try { + tmpFallback = new SecurityManagerCallStack(); + } catch (Throwable fallbackException) { + // this is bad + fallbackException.printStackTrace(); + } + } + stackWalkerClass = tmpStackWalkerClass; + stackWalker = tmpStackWalker; + forEach = tmpForEach; + getDeclaringClass = tmpGetDeclaringClass; + fallback = tmpFallback; + } + + + public Class[] getClassContext() { + if (fallback != null) { + return fallback.getClassContext(); + } + List> result = new ArrayList<>(); + if (stackWalker != null) { + try { + forEach.invoke(stackWalker, new Consumer() { + public void accept(Object s) { + try { + result.add((Class) getDeclaringClass.invoke(s)); + } catch (Throwable t) { + t.printStackTrace(); + } + } + }); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return result.toArray(new Class[0]); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java new file mode 100644 index 0000000000..6db4d8a818 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * 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.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.felix.framework.plurl.PlurlStreamHandler; + +public class URLToHandler { + class WeakURL extends WeakReference { + private final int hashcode; + + public WeakURL(URL u, ReferenceQueue q) { + super(u, q); + this.hashcode = System.identityHashCode(u); + } + @Override + public int hashCode() { + return hashcode; + } + @Override + public boolean equals(Object obj) { + if (obj instanceof WeakURL) { + return get() == ((WeakURL) obj).get(); + } + return false; + } + } + + final ReferenceQueue queue = new ReferenceQueue<>(); + + Map entries = Collections.synchronizedMap(new HashMap<>()); + + PlurlStreamHandler get(URL u, Supplier h) { + WeakURL lookup = new WeakURL(u, null); + PlurlStreamHandler existing = entries.get(lookup); + if (existing != null) { + return existing; + } + + PlurlStreamHandler result = h == null ? null : h.get(); + if (result != null) { + synchronized (entries) { + PlurlStreamHandler recheck = entries.get(lookup); + if (recheck != null) { + return recheck; + } + entries.put(new WeakURL(u, queue), result); + Object x; + while ((x = queue.poll()) != null) { + entries.remove(x); + } + } + } + return result; + } +}