From 735048d57b85b3ec82bf369bc41f1f9f1824aa61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Wed, 9 Sep 2026 09:14:37 +0200 Subject: [PATCH 1/3] FELIX-6759 - Vendor plurl for URL handler factory multiplexing plurl multiplexes the JVM URL factory singletons through the supported URL.setURLStreamHandlerFactory API, so the framework no longer has to claim them by reflectively clearing private static fields of java.net.URL. Equinox uses the same mechanism, so a JVM hosting both can share one router. The sources are vendored under org.apache.felix.framework.plurl, byte for byte as upstream except the package rename, because plurl has not been released to Maven Central yet. Once it is, this becomes a provided dependency embedded with Private-Package, the way org.apache.felix.resolver already is, and these files go away. Includes the URL selection support the framework needs, which is merged upstream as eclipse-osgi-technology/plurl#63: every framework instance uses the same bundle: protocol, so the owner can only be identified by the UUID in the URL, which the call stack cannot supply when a bundle: URL is parsed by a caller that is in no bundle. Apache-2.0, credited in the framework NOTICE alongside the OSGi Alliance entry. See org/apache/felix/framework/plurl/README.md for provenance. Co-Authored-By: Claude Opus 5 --- .../main/appended-resources/META-INF/NOTICE | 5 + .../apache/felix/framework/plurl/Plurl.java | 378 ++++ .../plurl/PlurlContentHandlerFactory.java | 27 + .../felix/framework/plurl/PlurlFactory.java | 38 + .../framework/plurl/PlurlStreamHandler.java | 141 ++ .../plurl/PlurlStreamHandlerBase.java | 177 ++ .../plurl/PlurlStreamHandlerFactory.java | 81 + .../apache/felix/framework/plurl/README.md | 78 + .../felix/framework/plurl/impl/CallStack.java | 22 + .../felix/framework/plurl/impl/PlurlImpl.java | 1631 +++++++++++++++++ .../plurl/impl/SecurityManagerCallStack.java | 34 + .../plurl/impl/StackWalkerCallStack.java | 91 + .../framework/plurl/impl/URLToHandler.java | 78 + 13 files changed, 2781 insertions(+) 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/appended-resources/META-INF/NOTICE b/framework/src/main/appended-resources/META-INF/NOTICE index 246e269d11..a89bb7d51a 100644 --- a/framework/src/main/appended-resources/META-INF/NOTICE +++ b/framework/src/main/appended-resources/META-INF/NOTICE @@ -2,3 +2,8 @@ This product includes software developed at The OSGi Alliance (http://www.osgi.org/). Copyright (c) OSGi Alliance (2000, 2020). Licensed under the Apache License 2.0. + +This product includes software developed at +The Eclipse Foundation (https://projects.eclipse.org/projects/technology.osgi-technology). +Copyright (c) Contributors to the Eclipse Foundation. +Licensed under the Apache License 2.0. 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..28e928ee0e --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java @@ -0,0 +1,378 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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.Optional; +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$ + + /** + * The plurl protocol operation to check a capability of the installed plurl + * implementation. The capability name is appended to the operation, and the + * content of the connection is the capability value: + * + *

+	 * Boolean selectsBySpec = (Boolean) new URL("plurl://op/getCapability/selectFactoryBySpec").getContent();
+	 * 
+ * + * The type of the value is documented on each capability constant. + *

+ * An implementation that predates a capability, or this operation, rejects the + * query with an {@link IOException}, so a factory can tell an older + * implementation apart from one that does not have the capability. + * {@link #getCapability(String)} does that for you. + * + * @see #getCapability(String) + */ + public static final String PLURL_GET_CAPABILITY = "getCapability"; //$NON-NLS-1$ + + /** + * The capability reported when the installed plurl implementation consults + * {@link PlurlStreamHandlerFactory#shouldHandleURL(String, String)} while + * selecting a factory. The value is a {@link Boolean}. + *

+ * A factory that can only be identified by the URL, rather than by the protocol + * or the call stack, cannot be routed to correctly without this, so it is worth + * checking for and reporting rather than silently misrouting. + * + * @see PlurlStreamHandlerFactory#shouldHandleURL(String, String) + */ + public static final String PLURL_CAPABILITY_SELECT_BY_SPEC = "selectFactoryBySpec"; //$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); + } + + /** + * Returns the value of the named capability of the {@link #install installed} + * plurl implementation, or an empty {@link Optional} if it does not have it. + *

+ * An implementation that predates the capability, or the + * {@link #PLURL_GET_CAPABILITY} operation itself, rejects the query; that is + * reported here as an empty result, because the absence of an answer is itself + * the answer. Callers should treat an empty result as "not supported" rather than + * as an error. The type of the value is documented on each capability constant. + *

+ * This is a convenience method for using the plurl protocol like this: + * + *

+	 * ((Boolean) ("plurl://op/getCapability/selectFactoryBySpec").getContent());
+	 * 
+ * + * @param capability the name of the capability to check + * @return the capability value, or an empty {@link Optional} if the installed + * plurl implementation does not have it + * @see #PLURL_CAPABILITY_SELECT_BY_SPEC + */ + public static Optional getCapability(String capability) { + try { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, + Plurl.PLURL_GET_CAPABILITY + '/' + capability); + return Optional.ofNullable(plurl.openConnection().getContent()); + } catch (IOException e) { + // No plurl installed, or one without this capability. + return Optional.empty(); + } + } +} 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..3224974b8f --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..520bf92d97 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..6bd2d3ab76 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..e7f490abfe --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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)" + */ + @Override + @Deprecated + 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..eb451e6793 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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); + + /** + * Returns true if this factory should handle the URL being parsed from the given + * spec. This is consulted before the call stack is examined, and lets a factory + * claim a URL that only it can own. + *

+ * Several parties may share one protocol and be distinguishable only by the URL + * itself, for example multiple instances of the same framework where the owner is + * identified by an id in the URL host. Such a URL may also be used by a caller + * that no factory recognizes from the call stack, leaving nothing else to select + * on. + *

+ * The spec is used rather than a {@code URL}, because selection happens while the + * URL is still being parsed: its host and path are not populated yet, and the + * handler is pinned to the URL as soon as parsing begins. Implementations must not + * call back into URL handling, so this method is given only strings. + *

+ * The spec may be relative and carry neither protocol nor host, in which case a + * factory that selects on the URL has nothing to decide with and should return + * false; a relative URL resolved against a context URL keeps the context's handler + * and does not reach this method. + *

+ * The spec may also be null. That is the protocol level question, + * asked before any URL exists, because the JVM asks a plurl implementation once + * per protocol whether it handles that protocol at all and gives it no URL. A + * factory that selects on the URL must answer true for a protocol + * whose URLs it claims, or the protocol is never claimed from the JVM and no URL + * of it is ever parsed. Implementations must therefore not assume a non-null spec. + * + * @param protocol the protocol of the URL being parsed + * @param spec the spec the URL is being parsed from, which may be relative, + * or null when the question is about the protocol alone + * @return true if this factory should handle the URL + * @see Plurl#PLURL_CAPABILITY_SELECT_BY_SPEC + */ + default boolean shouldHandleURL(String protocol, String spec) { + return false; + } +} 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..17b242ac03 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/README.md @@ -0,0 +1,78 @@ + + +# Plurl (vendored) + +## Provenance + +These sources are copied from the Eclipse OSGi Technology **plurl** project: + +- Upstream: https://github.com/eclipse-osgi-technology/plurl +- Originally: https://github.com/tjwatson/plurl-osgi + +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 +copyright notice unchanged. + +This mirrors what Eclipse Equinox did in +https://github.com/eclipse-equinox/equinox/pull/848, which vendored the same files +into `org.eclipse.equinox.plurl`. + +## Licensing + +The sources are **Apache-2.0**: + +``` +Copyright (c) Contributors to the Eclipse Foundation +Licensed under the Apache License, Version 2.0 +SPDX-License-Identifier: Apache-2.0 +``` + +An earlier copy carried EPL-2.0 headers, which would have been a blocker: 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. That was an oversight when the code moved +to the osgi-technology project and has been corrected upstream in +https://github.com/eclipse-osgi-technology/plurl/pull/45. The copy here also includes +the upstream fix from https://github.com/eclipse-osgi-technology/plurl/pull/55. + +## Vendoring is intended to be temporary + +The longer term intention, per +https://github.com/apache/felix-dev/pull/552#issuecomment-5466491363, is a release of +plurl from the osgi-technology project consumed by both Equinox and Felix +**unchanged, in its original package**, rather than copied into each framework. At the +time of writing plurl has no release: its `distributionManagement` points at +`oss.sonatype.org`, which was decommissioned when OSSRH migrated to the Central +portal, so neither a release nor a snapshot is currently resolvable. + +Once a release exists, this directory should be deleted and replaced by a dependency +on the published artifact, embedded into the framework bundle as a private package. + +## Why the framework uses this + +`URLHandlers` used to claim the JVM-wide `java.net.URL` stream handler factory by +reflectively swapping a private static field, and inspected the call stack to work out +which framework instance a call belonged to. Obtaining a `MethodHandles.Lookup` +trusted enough for that swap is the only remaining reason the framework uses +`sun.misc.Unsafe`, and whichever framework installed itself last won the singleton. + +Plurl installs one cooperative router through the supported +`URL.setURLStreamHandlerFactory` API and routes by asking each registered factory +whether a calling class belongs to it, so several frameworks can coexist in one JVM. +See `PlurlURLHandlers` for the Felix side of that. 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..bc672cddcb --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..f23cd6b400 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java @@ -0,0 +1,1631 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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.PlurlStreamHandlerFactory; +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 List builtinContentHandlerFactories; + 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() { + // IMPLEMENTATION NOTE: + // We must do the ServiceLoader lookup for the built-in ContentHandlerFactory + // because the Plurl ContentHandlerFactory must never return null; + // otherwise the Plurl factory will never be called again for the requested + // content type. So a check for built-in handlers must be done before returning + // the Plurl handler. + // This is not necessary for URLStreamHandlerFactory or the new + // URLStreamHandlerProvider that may be available from the JVM because returning + // null from that factory still allows us to be called again if the protocol is + // requested again later. + List serviceLoaderCHFs = new ArrayList<>(); + ServiceLoader.load(ContentHandlerFactory.class).forEach(serviceLoaderCHFs::add); + builtinContentHandlerFactories = Collections.unmodifiableList(serviceLoaderCHFs); + + 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; + } + } + List factories = getURLStreamHandlerFactories(); + // A factory may claim this protocol by URL, which is decided per URL later + // in parseURL. The JVM asks us per protocol and gives us no URL, so ask the + // factories about the protocol alone first; otherwise a protocol only such + // a factory serves is declined here and the URL never gets that far. + for (URLStreamHandlerFactoryHolder holder : factories) { + if (holder.takesOverURLs(protocol) && holder.getHandler(protocol) != null) { + return new PlurlRootURLStreamHandler(protocol); + } + } + URLStreamHandlerFactoryHolder factoryHolder = findFactory(factories); + 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 : builtinContentHandlerFactories) { + 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) { + // check handlers pkgs 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; + } + + /** + * Returns the value of a capability this implementation has, as reported through + * {@link Plurl#PLURL_GET_CAPABILITY}. A capability that is not recognized is + * rejected rather than reported as absent, so that a caller can tell an older + * plurl apart from one that answers. + */ + static Object getCapability(String capability) throws IOException { + if (Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC.equals(capability)) { + return Boolean.TRUE; + } + throw new IOException("Unknown plurl capability: " + capability); //$NON-NLS-1$ + } + + URLConnection plurlOperation(URL u) { + String opPath = u.getPath(); + // Tolerate the leading slash of the documented spec form, so that + // "plurl://op/" works as well as the URL(protocol, host, file) + // form the convenience methods use. + String fullPath = opPath.startsWith("/") ? opPath.substring(1) : opPath; //$NON-NLS-1$ + // Some operations take an argument appended to the operation, for example + // "plurl://op/getCapability/". + int argIdx = fullPath.indexOf('/'); + final String path = argIdx < 0 ? fullPath : fullPath.substring(0, argIdx); + final String arg = argIdx < 0 ? null : fullPath.substring(argIdx + 1); + return new URLConnection(u) { + @Override + public void connect() throws IOException { + // do nothing + } + + @Override + public Object getContent() throws IOException { + switch (path) { + case Plurl.PLURL_GET_CAPABILITY: + return getCapability(arg); + case PLURL_ADD_URL_STREAM_HANDLER_FACTORY: + return (Consumer) (f) -> add((URLStreamHandlerFactory) f); + case PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY: + return (Consumer) (f) -> remove((URLStreamHandlerFactory) f); + case PLURL_ADD_CONTENT_HANDLER_FACTORY: + return (Consumer) (f) -> add((ContentHandlerFactory) f); + case PLURL_REMOVE_CONTENT_HANDLER_FACTORY: + return (Consumer) (f) -> remove((ContentHandlerFactory) f); + case PLURL_REGISTER_IMPLEMENTATION: + return (Consumer) (p) -> addImpl(p); + case PLURL_UNREGISTER_IMPLEMENTATION: + return (Consumer) (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) { + return findPlurlStreamHandler(protocol, null); + } + + PlurlStreamHandler findPlurlStreamHandler(String protocol, String spec) { + URLStreamHandlerFactoryHolder f = findFactory(getURLStreamHandlerFactories(), protocol, spec); + if (f != null) { + return f.getHandler(protocol); + } + return null; + } + + private F findFactory(List factories) { + return findFactory(factories, null, null); + } + + private F findFactory(List factories, String protocol, String spec) { + int numFactories = factories.size(); + if (numFactories == 1) { + // Handle common case of only one; just use it + return factories.get(0); + } + // Give the factories a chance to claim the URL being parsed first. A protocol + // may be shared by several factories that can only be told apart by the URL, + // and the URL may be used by a caller no factory recognizes from the call + // stack. + if (spec != null) { + for (F f : factories) { + if (f instanceof URLStreamHandlerFactoryHolder + && ((URLStreamHandlerFactoryHolder) f).shouldHandleURL(protocol, spec)) { + return f; + } + } + } + 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; + } + + /** + * The optional URL selection method of a factory, or null if it does + * not have one. Unlike {@link #findShouldHandle(Class)} a missing method is not an + * error, because a factory is not required to select on the URL. + */ + static Method findShouldHandleURL(Class clazz) { + try { + Method m = clazz.getMethod("shouldHandleURL", String.class, String.class); //$NON-NLS-1$ + m.setAccessible(true); + return m; + } catch (NoSuchMethodException e) { + return 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 { + private final Method shouldHandleURLMethod; + + public URLStreamHandlerFactoryHolder(URLStreamHandlerFactory factory) { + super(factory); + if (factory instanceof PlurlStreamHandlerFactory) { + shouldHandleURLMethod = null; + } else { + // use reflection in case this Plurl package isn't visible to the + // factory impl; a factory that predates the method has none, which is + // not an error + shouldHandleURLMethod = findShouldHandleURL(factory.getClass()); + } + } + + /** + * Delegates to the wrapped factory the way {@link #shouldHandle(Class)} does. + * Without this, findFactory would only ever ask the holder, which has no + * opinion of its own. + */ + boolean shouldHandleURL(String protocol, String spec) { + URLStreamHandlerFactory f = getFactory(); + if (f == null) { + return false; + } + if (f instanceof PlurlStreamHandlerFactory) { + return ((PlurlStreamHandlerFactory) f).shouldHandleURL(protocol, spec); + } + if (shouldHandleURLMethod == null) { + return false; + } + try { + return (boolean) shouldHandleURLMethod.invoke(f, protocol, spec); + } catch (Exception e) { + return false; + } + } + + /** + * Whether this factory selects on the URL for the given protocol at all. Used + * only to decide whether to claim the protocol from the JVM, which happens + * before any URL exists, so the factory is asked with no spec. + */ + boolean takesOverURLs(String protocol) { + return shouldHandleURL(protocol, null); + } + + @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); + } + + @Override + @Deprecated + 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) { + return lookupPlurlStreamHandler(u, null); + } + + private PlurlStreamHandler lookupPlurlStreamHandler(URL u, String spec) { + 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, () -> findPlurlStreamHandlerImpl(spec)); + } + return findPlurlStreamHandlerImpl(spec); + } + + private PlurlStreamHandler findPlurlStreamHandlerImpl(String spec) { + PlurlStreamHandler h = findPlurlStreamHandler(protocol, spec); + 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, spec); + 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..fac4797af4 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..a65c612dbf --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ +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..d17b2b40f7 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +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; + } +} From 121cc7362c8bbf89970656dd698d8c0f0a63256f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Wed, 9 Sep 2026 09:15:27 +0200 Subject: [PATCH 2/3] FELIX-6759 - Register the framework's URL handling with plurl Each framework instance registers its own factory with the plurl router instead of this class installing itself as the JVM stream and content handler factory. Registering uses the supported URL.setURLStreamHandlerFactory API on a clean JVM, so no reflective write to a java.net static field is left anywhere in the framework. Failing to register does not prevent the framework from starting: as in Equinox, the consequence is that this instance contributes no URL handlers. There is deliberately no fallback to swapping the singleton fields, since removing that is the point. bundle: URLs are claimed by the framework UUID in the URL, read from the spec because plurl has to select a factory before the URL is parsed. Every framework instance uses the same bundle: protocol, so neither the protocol nor the call stack identifies an owner when a bundle: URL is parsed by a caller that is in no bundle. A null spec is the protocol level question plurl asks before any URL exists, and is answered by claiming the protocol. The framework warns at startup when the installed plurl does not support selection by URL, because bundle: URLs are then routed to whichever factory registered first and cannot be resolved. Nothing on the factory side can fix that, so it is reported rather than left to surface later as a failed resource lookup. With plurl owning the JVM factories, URLHandlers is never instantiated, so the dead instance side is removed with it: the constructor that performed the takeover, the cross-classloader framework list rendezvous that plurl now does, the URLStreamHandlerFactory and ContentHandlerFactory implementations, and the built-in handler lookup and caches. What remains is the framework registry that URLHandlersBundleStreamHandler uses to find the owner of a caller. Co-Authored-By: Claude Opus 5 --- .../org/apache/felix/framework/Felix.java | 5 + .../felix/framework/PlurlURLHandlers.java | 312 ++++++++ .../apache/felix/framework/URLHandlers.java | 732 ++---------------- .../URLHandlersBundleStreamHandler.java | 10 +- .../URLHandlersStreamHandlerProxy.java | 22 +- .../felix/framework/PlurlURLHandlersTest.java | 179 +++++ 6 files changed, 590 insertions(+), 670 deletions(-) create mode 100644 framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java create mode 100644 framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java diff --git a/framework/src/main/java/org/apache/felix/framework/Felix.java b/framework/src/main/java/org/apache/felix/framework/Felix.java index f2104d2aa5..c9170b5c5a 100644 --- a/framework/src/main/java/org/apache/felix/framework/Felix.java +++ b/framework/src/main/java/org/apache/felix/framework/Felix.java @@ -5586,6 +5586,11 @@ void releaseGlobalLock() private volatile URLHandlersActivator m_urlHandlersActivator; + URLHandlersActivator getURLHandlersActivator() + { + return m_urlHandlersActivator; + } + void setURLHandlersActivator(URLHandlersActivator urlHandlersActivator) { m_urlHandlersActivator = urlHandlersActivator; 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..ace692cdb7 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java @@ -0,0 +1,312 @@ +/* + * 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.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; +import org.apache.felix.framework.plurl.impl.PlurlImpl; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.BundleReference; + +/** + *

+ * Registers this framework's URL handling with the plurl + * multiplexing factories instead of taking over the JVM singletons directly. + *

+ *

+ * {@link URLHandlers} used to claim the JVM-wide {@code URLStreamHandlerFactory} by + * reflectively clearing a private static field on {@code java.net.URL}, which needed + * a {@code MethodHandles.Lookup} trusted enough to write it and meant the last + * framework to install itself won the singleton. + *

+ *

+ * Plurl installs one cooperative router through the supported + * {@code URL.setURLStreamHandlerFactory} API and asks each registered factory + * {@link #shouldHandle(Class)} to claim a calling class, or + * {@link #shouldHandleURL(String, String)} to claim the URL being parsed. That removes + * the need for {@code URLHandlers.getFrameworkFromContext()} to walk the call stack: + * by the time this factory is consulted, plurl has already established that the + * caller belongs to this framework instance, so + * {@link #createURLStreamHandler(String)} can use {@code m_felix} directly. + *

+ *

+ * See {@code org/apache/felix/framework/plurl/README.md} for the provenance of the + * vendored plurl sources. + *

+ */ +class PlurlURLHandlers implements PlurlStreamHandlerFactory, PlurlContentHandlerFactory +{ + private final Felix m_felix; + private final SecureAction m_secureAction; + + private PlurlURLHandlers(Felix felix, SecureAction secureAction) + { + m_felix = felix; + m_secureAction = secureAction; + } + + /** + * The plurl router is a JVM wide singleton, so it is installed once and shared by + * every framework instance in this JVM. Only the per framework factories are + * added and removed as frameworks come and go; tearing the router down while + * another framework is still registered would break that framework's URLs. + */ + private static Plurl m_router; + private static int m_routerUsers; + + /** + * Installs the plurl router if this is the first framework to need it and + * registers the given framework's factories with it. + *

+ * Failing to register must not prevent the framework from starting; in that case + * this framework simply contributes no URL handlers, which the caller sees as a + * {@code null} return. + */ + static PlurlURLHandlers install(Felix felix, SecureAction secureAction) + { + PlurlURLHandlers handlers = new PlurlURLHandlers(felix, secureAction); + try + { + synchronized (PlurlURLHandlers.class) + { + if (m_router == null) + { + // Installing is what makes the plurl: protocol resolvable, which + // the static Plurl.add(..) calls below go through. Without it they + // fail with "unknown protocol: plurl". + Plurl router = new PlurlImpl(); + router.install(); + m_router = router; + } + m_routerUsers++; + } + + Plurl.add((PlurlStreamHandlerFactory) handlers); + Plurl.add((PlurlContentHandlerFactory) handlers); + warnIfSelectionBySpecUnsupported(felix); + return handlers; + } + catch (Throwable ex) + { + felix.getLogger().log(Logger.LOG_ERROR, + "Unable to register this framework with plurl.", ex); + releaseRouter(); + return null; + } + } + + /** + * Unregisters this framework's factories, and uninstalls the router once the last + * framework using it has gone. + */ + void uninstall() + { + try + { + Plurl.remove((PlurlStreamHandlerFactory) this); + Plurl.remove((PlurlContentHandlerFactory) this); + } + catch (Throwable ex) + { + m_felix.getLogger().log(Logger.LOG_ERROR, + "Unable to unregister this framework from plurl.", ex); + } + finally + { + releaseRouter(); + } + } + + private static void releaseRouter() + { + synchronized (PlurlURLHandlers.class) + { + if (m_routerUsers > 0) + { + m_routerUsers--; + } + if ((m_routerUsers == 0) && (m_router != null)) + { + Plurl router = m_router; + m_router = null; + try + { + router.uninstall(); + } + catch (Throwable ex) + { + // Nothing useful to do; the JVM factories stay as they are. + } + } + } + } + + /** + * Tells plurl whether the given calling class belongs to this framework instance. + *

+ * It is not enough for the class to come from some Felix bundle: two framework + * instances in the same JVM both load classes through a + * {@code BundleWiringImpl.BundleClassLoader}, so the owning framework has to + * match as well. Otherwise one framework would answer lookups for a bundle + * resolved in another. + */ + @Override + public boolean shouldHandle(Class clazz) + { + if (clazz == null) + { + return false; + } + ClassLoader loader = clazz.getClassLoader(); + if (!(loader instanceof BundleReference)) + { + return false; + } + Bundle bundle = ((BundleReference) loader).getBundle(); + return (bundle instanceof BundleImpl) + && (((BundleImpl) bundle).getFramework() == m_felix); + } + + /** + * Warns when the plurl that won the install in this JVM cannot route by the URL. + *

+ * The router is not necessarily the copy this framework brought: it may belong to + * another framework instance, or to an application embedding its own, and it may + * be older than this one. A bundle: URL can only be attributed to a framework by + * the UUID it carries, so where {@link Plurl#PLURL_CAPABILITY_SELECT_BY_SPEC} is + * not supported such a URL is handed to whichever factory registered first, which + * then cannot resolve it. Nothing here can fix that, so say it plainly at startup + * rather than leave it to surface later as a failed resource lookup. + */ + private static void warnIfSelectionBySpecUnsupported(Felix felix) + { + if (!Plurl.getCapability(Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC) + .filter(Boolean.TRUE::equals).isPresent()) + { + felix.getLogger().log(Logger.LOG_WARNING, + "The plurl implementation installed in this JVM does not support" + + " selecting a factory by the URL being parsed (" + + Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC + + "). A bundle: URL parsed by a caller outside of any bundle may be" + + " routed to a different framework instance in this JVM and fail to" + + " resolve."); + } + } + + /** + * Claims bundle: URLs belonging to this framework instance. + *

+ * Every Felix framework in the JVM uses the same bundle: protocol, so the + * protocol alone does not identify an owner; the framework UUID in the URL host + * does. This matters when a URL is re-parsed by a caller that is in no bundle, + * where there is nothing on the call stack for plurl to attribute. + */ + @Override + public boolean shouldHandleURL(String protocol, String spec) + { + if (!FelixConstants.BUNDLE_URL_PROTOCOL.equals(protocol)) + { + return false; + } + if (spec == null) + { + // The protocol level question, asked before any URL exists: bundle: URLs + // are ours to select on. Answering false leaves the protocol unclaimed in + // a JVM where the factory plurl falls back to does not serve it, and then + // no bundle: URL is parsed at all. + return true; + } + String uuid = Util.getFrameworkUUIDFromURL(getHost(protocol, spec)); + return (uuid != null) + && uuid.equals(m_felix._getProperty(Constants.FRAMEWORK_UUID)); + } + + /** + * Returns the host of the spec being parsed, or null if it has none. + * The URL itself cannot be asked, because plurl has to pick a factory before the + * URL has been parsed. + */ + private static String getHost(String protocol, String spec) + { + int start = 0; + if (spec.regionMatches(true, 0, protocol, 0, protocol.length()) + && (spec.length() > protocol.length()) + && (spec.charAt(protocol.length()) == ':')) + { + start = protocol.length() + 1; + } + if (!spec.startsWith("//", start)) + { + return null; + } + start += 2; + int end = start; + while ((end < spec.length()) && ("/?#".indexOf(spec.charAt(end)) < 0)) + { + end++; + } + return spec.substring(start, end); + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) + { + if (FelixConstants.BUNDLE_URL_PROTOCOL.equals(protocol)) + { + // Deliberately not bound to m_felix. The JVM caches one handler per + // protocol for the whole JVM, so a handler pinned to this framework would + // also be used for bundle: URLs belonging to another framework instance, + // which then fail to resolve. The framework is instead resolved per call + // from the UUID in the URL's host, which is what URLHandlers did. + return new URLHandlersBundleStreamHandler(m_secureAction); + } + + // Otherwise serve a URLStreamHandlerService registered in this framework. The + // protocol based proxy resolves the service lazily on each use, which is how + // URLHandlers built these: a service can come and go while a URL object using + // this protocol is still around. Only claim the protocol if a service exists + // now, so that unrelated protocols fall through to plurl's other factories. + if (m_felix.getStreamHandlerService(protocol) != null) + { + return new URLHandlersStreamHandlerProxy(protocol, m_secureAction, null, null); + } + + // Not ours. Returning null lets plurl ask the other registered factories and + // fall back to the JVM built-ins. + return null; + } + + @Override + public ContentHandler createContentHandler(String mimeType) + { + if (m_felix.getContentHandlerService(mimeType) != null) + { + return new URLHandlersContentHandlerProxy(mimeType, m_secureAction, null); + } + return null; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlers.java b/framework/src/main/java/org/apache/felix/framework/URLHandlers.java index 85e80ad824..b1771eb6c6 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlers.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlers.java @@ -18,66 +18,42 @@ */ package org.apache.felix.framework; -import java.lang.reflect.Method; -import java.net.ContentHandler; -import java.net.ContentHandlerFactory; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.net.URLStreamHandlerFactory; import java.util.List; -import java.util.StringTokenizer; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import static org.apache.felix.framework.util.Util.putIfAbsentAndReturn; -import org.apache.felix.framework.util.FelixConstants; import org.apache.felix.framework.util.SecureAction; import org.apache.felix.framework.util.SecurityManagerEx; import org.osgi.framework.Constants; -import org.osgi.service.url.URLStreamHandlerService; /** *

- * This class is a singleton and implements the stream and content handler - * factories for all framework instances executing within the JVM. Any - * calls to retrieve stream or content handlers is routed through this class - * and it acts as a multiplexer for all framework instances. To achieve this, - * all framework instances register with this class when they are created so - * that it can maintain a centralized registry of instances. + * Keeps the registry of framework instances running in this JVM, and answers which + * of them a caller belongs to. *

*

- * When this class receives a request for a stream or content handler, it - * always returns a proxy handler instead of only returning a proxy if a - * handler currently exists. This approach is used for three reasons: + * This class used to be the singleton stream and content handler factory for every + * framework instance in the JVM, installing itself into java.net.URL and + * java.net.URLConnection and multiplexing between instances. Since + * FELIX-6759 that role belongs to the plurl router: each framework registers its own + * {@link PlurlURLHandlers} with plurl, which asks each registered factory whether a + * calling class or a URL belongs to it. Installing the JVM factory ourselves required + * reflectively clearing private static fields of java.net.URL, which no + * longer works without --add-opens, and it left the factories occupied + * before plurl could take them. What remains here is the instance registry, which + * {@code URLHandlersBundleStreamHandler} still uses to find the framework that owns a + * caller when a handler is not bound to one. *

- *
    - *
  1. Potential caching behavior by the JVM of stream handlers does not give - * you a second chance to provide a handler. - *
  2. - *
  3. Due to the dynamic nature of OSGi services, handlers may appear at - * any time, so always creating a proxy makes sense. - *
  4. - *
  5. Since these handler factories service all framework instances, - * some instances may have handlers and others may not, so returning - * a proxy is the only answer that makes sense. - *
  6. - *
*

* It is possible to disable the URL Handlers service by setting the * framework.service.urlhandlers configuration property to false. - * When multiple framework instances are in use, if no framework instances enable - * the URL Handlers service, then the singleton stream and content factories will - * never be set (i.e., URL.setURLStreamHandlerFactory() and - * URLConnection.setContentHandlerFactory()). However, if one instance - * enables URL Handlers service, then the factory methods will be invoked. In - * that case, framework instances that disable the URL Handlers service will - * simply not provide that services to their contained bundles, while framework - * instances with the service enabled will. + * A framework instance that disables it simply contributes no URL handlers, while + * instances with the service enabled still provide them to their own bundles. *

**/ -class URLHandlers implements URLStreamHandlerFactory, ContentHandlerFactory +class URLHandlers { private static final Class[] CLASS_TYPE = new Class[]{Class.class}; @@ -85,585 +61,91 @@ class URLHandlers implements URLStreamHandlerFactory, ContentHandlerFactory private static final SecureAction m_secureAction = new SecureAction(); - private static volatile SecurityManagerEx m_sm = null; - private static volatile URLHandlers m_handler = null; - - // This maps classloaders of URLHandlers in other classloaders to lists of - // their frameworks. - private final static ConcurrentHashMap> m_classloaderToFrameworkLists = new ConcurrentHashMap<>(); + // Initialised eagerly: the constructor that used to set this up is no longer + // invoked, since plurl installs the JVM factories instead of this class. + private static volatile SecurityManagerEx m_sm = new SecurityManagerEx(); // The list to hold all enabled frameworks registered with this handlers private static final CopyOnWriteArrayList m_frameworks = new CopyOnWriteArrayList<>(); private static volatile int m_counter = 0; - private static final ConcurrentHashMap m_contentHandlerCache = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap m_streamHandlerCache = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap m_protocolToURL = new ConcurrentHashMap<>(); + // The plurl registration per framework instance, so it can be uninstalled again + // when the framework stops. + private static final Map m_plurlHandlers = + new ConcurrentHashMap<>(); - private static volatile URLStreamHandlerFactory m_streamHandlerFactory; - private static volatile ContentHandlerFactory m_contentHandlerFactory; - private static final String STREAM_HANDLER_PACKAGE_PROP = "java.protocol.handler.pkgs"; - private static final String DEFAULT_STREAM_HANDLER_PACKAGE = "sun.net.www.protocol|com.ibm.oti.net.www.protocol|gnu.java.net.protocol|wonka.net|com.acunia.wonka.net|org.apache.harmony.luni.internal.net.www.protocol|weblogic.utils|weblogic.net|javax.net.ssl|COM.newmonics.www.protocols"; - private static volatile Object m_rootURLHandlers; - private static final String m_streamPkgs; - private static final ConcurrentHashMap m_builtIn = new ConcurrentHashMap<>(); - private static final boolean m_loaded; - static - { - String pkgs = new SecureAction().getSystemProperty(STREAM_HANDLER_PACKAGE_PROP, ""); - m_streamPkgs = (pkgs.equals("")) - ? DEFAULT_STREAM_HANDLER_PACKAGE - : pkgs + "|" + DEFAULT_STREAM_HANDLER_PACKAGE; - boolean loaded; - try - { - loaded = (null != URLHandlersStreamHandlerProxy.class) && - (null != URLHandlersContentHandlerProxy.class) && (null != URLStreamHandlerService.class) && new URLHandlersStreamHandlerProxy(null, null) != null; - } - catch (Throwable e) { - loaded = false; - } - m_loaded = loaded; - } - private void init(String protocol, URLStreamHandlerFactory factory) - { - try - { - // Try to get it directly from the URL class to if possible - Method getURLStreamHandler = m_secureAction.getDeclaredMethod(URL.class,"getURLStreamHandler", new Class[]{String.class}); - URLStreamHandler handler = (URLStreamHandler) m_secureAction.invoke(getURLStreamHandler, null, new Object[]{protocol}); - addToCache(m_builtIn, protocol, handler); - } - catch (Throwable ex) - { - // Ignore, this is a best effort - try - { - URLStreamHandler handler = getBuiltInStreamHandler(protocol, factory); - if (handler != null) - { - URL url = new URL(protocol, null, -1, "", handler); - addToCache(m_protocolToURL, protocol, url); - } - } - catch (Throwable ex2) - { - // Ignore, this is a best effort (maybe log it or something). - } - } - } - - /** - *

- * Only one instance of this class is created per classloader - * and that one instance is registered as the stream and content handler - * factories for the JVM. Unless, we already register one from a different - * classloader. In this case we attach to this root. - *

- **/ - private URLHandlers() - { - m_sm = new SecurityManagerEx(); - synchronized (URL.class) - { - URLStreamHandlerFactory currentFactory = null; - try - { - currentFactory = (URLStreamHandlerFactory) m_secureAction.swapStaticFieldIfNotClass(URL.class, - URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock"); - } - catch (Throwable ex) - { - // Ignore, this is a best effort (maybe log it or something) - } - init("file", currentFactory); - init("ftp", currentFactory); - init("http", currentFactory); - init("https", currentFactory); - - - // Try to preload the jrt handler as we need it from the jvm on java > 8 - if (getFromCache(m_builtIn, "jrt") == null) - { - try - { - // Try to get it directly from the URL class to if possible - Method getURLStreamHandler = m_secureAction.getDeclaredMethod(URL.class,"getURLStreamHandler", new Class[]{String.class}); - URLStreamHandler handler = (URLStreamHandler) m_secureAction.invoke(getURLStreamHandler, null, new Object[]{"jrt"}); - addToCache(m_builtIn, "jrt", handler); - } - catch (Throwable ex) - { - // Ignore, this is a best effort and try to load the normal way - try - { - getBuiltInStreamHandler("jrt", currentFactory); - } - catch (Throwable ex2) - { - // Ignore, this is a best efforts - } - } - } - - // Try to preload the jar handler as we need it from the jvm on java > 8 - if (getFromCache(m_builtIn, "jar") == null) - { - try - { - // Try to get it directly from the URL class to if possible - Method getURLStreamHandler = m_secureAction.getDeclaredMethod(URL.class,"getURLStreamHandler", new Class[]{String.class}); - URLStreamHandler handler = (URLStreamHandler) m_secureAction.invoke(getURLStreamHandler, null, new Object[]{"jar"}); - addToCache(m_builtIn, "jar", handler); - } - catch (Throwable ex) - { - // Ignore, this is a best effort - try - { - getBuiltInStreamHandler("jar", currentFactory); - } - catch (Throwable ex2) - { - // Ignore, this is a best effort (maybe log it or something) - } - } - } - - if (currentFactory != null) - { - try - { - URL.setURLStreamHandlerFactory(currentFactory); - } - catch (Throwable ex) - { - // Ignore, this is a best effort (maybe log it or something) - } - } - - try - { - URL.setURLStreamHandlerFactory(this); - m_streamHandlerFactory = this; - m_rootURLHandlers = this; - // try to flush the cache (gnu/classpath doesn't do it itself) - try - { - m_secureAction.flush(URL.class, URL.class); - } - catch (Throwable t) - { - // Not much we can do - } - } - catch (Error err) - { - try - { - // there already is a factory set so try to swap it with ours. - m_streamHandlerFactory = (URLStreamHandlerFactory) - m_secureAction.swapStaticFieldIfNotClass(URL.class, - URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock"); - if (m_streamHandlerFactory == null) - { - throw err; - } - if (!m_streamHandlerFactory.getClass().getName().equals(URLHANDLERS_CLASS.getName())) - { - URL.setURLStreamHandlerFactory(this); - m_rootURLHandlers = this; - } - else if (URLHANDLERS_CLASS != m_streamHandlerFactory.getClass()) - { - try - { - m_secureAction.invoke( - m_secureAction.getDeclaredMethod(m_streamHandlerFactory.getClass(), - "registerFrameworkListsForContextSearch", - new Class[]{ClassLoader.class, List.class}), - m_streamHandlerFactory, new Object[]{ URLHANDLERS_CLASS.getClassLoader(), - m_frameworks }); - m_rootURLHandlers = m_streamHandlerFactory; - } - catch (Exception ex) - { - throw new RuntimeException(ex.getMessage()); - } - } - } - catch (Exception e) - { - throw err; - } - } - try - { - URLConnection.setContentHandlerFactory(this); - m_contentHandlerFactory = this; - // try to flush the cache (gnu/classpath doesn't do it itself) - try - { - m_secureAction.flush(URLConnection.class, URLConnection.class); - } - catch (Throwable t) - { - // Not much we can do - } - } - catch (Error err) - { - // there already is a factory set so try to swap it with ours. - try - { - m_contentHandlerFactory = (ContentHandlerFactory) - m_secureAction.swapStaticFieldIfNotClass( - URLConnection.class, ContentHandlerFactory.class, - URLHANDLERS_CLASS, null); - if (m_contentHandlerFactory == null) - { - throw err; - } - if (!m_contentHandlerFactory.getClass().getName().equals( - URLHANDLERS_CLASS.getName())) - { - URLConnection.setContentHandlerFactory(this); - } - } - catch (Exception ex) - { - throw err; - } - } - } - // are we not the new root? - if (!((m_streamHandlerFactory == this) || !URLHANDLERS_CLASS.getName().equals( - m_streamHandlerFactory.getClass().getName()))) - { - m_sm = null; - m_protocolToURL.clear(); - m_builtIn.clear(); - } - } - static void registerFrameworkListsForContextSearch(ClassLoader index, - List frameworkLists) - { - synchronized (URL.class) - { - synchronized (m_classloaderToFrameworkLists) - { - m_classloaderToFrameworkLists.put(index, frameworkLists); - } - } - } - static void unregisterFrameworkListsForContextSearch(ClassLoader index) - { - synchronized (URL.class) - { - synchronized (m_classloaderToFrameworkLists) - { - m_classloaderToFrameworkLists.remove(index); - if (m_classloaderToFrameworkLists.isEmpty() ) - { - synchronized (m_frameworks) - { - if (m_frameworks.isEmpty()) - { - try - { - m_secureAction.swapStaticFieldIfNotClass(URL.class, - URLStreamHandlerFactory.class, null, "streamHandlerLock"); - } - catch (Exception ex) - { - // TODO log this - ex.printStackTrace(); - } - - if (m_streamHandlerFactory.getClass() != URLHANDLERS_CLASS) - { - URL.setURLStreamHandlerFactory(m_streamHandlerFactory); - } - try - { - m_secureAction.swapStaticFieldIfNotClass( - URLConnection.class, ContentHandlerFactory.class, - null, null); - } - catch (Exception ex) - { - // TODO log this - ex.printStackTrace(); - } - - if (m_contentHandlerFactory.getClass() != URLHANDLERS_CLASS) - { - URLConnection.setContentHandlerFactory(m_contentHandlerFactory); - } - } - } - } - } - } - } - private URLStreamHandler getBuiltInStreamHandler(String protocol, URLStreamHandlerFactory factory) - { - URLStreamHandler handler = getFromCache(m_builtIn, protocol); - if (handler != null) - { - return handler; - } - if (factory != null) - { - handler = factory.createURLStreamHandler(protocol); - } - if (handler == null) - { - // Check for built-in handlers for the mime type. - // Iterate over built-in packages. - handler = loadBuiltInStreamHandler(protocol, null); - } - - if (handler == null) - { - handler = loadBuiltInStreamHandler(protocol, ClassLoader.getSystemClassLoader()); - } - - return addToCache(m_builtIn, protocol, handler); - } - - private URLStreamHandler loadBuiltInStreamHandler(String protocol, ClassLoader classLoader) { - StringTokenizer pkgTok = new StringTokenizer(m_streamPkgs, "| "); - while (pkgTok.hasMoreTokens()) - { - String pkg = pkgTok.nextToken().trim(); - String className = pkg + "." + protocol + ".Handler"; - try - { - // If a built-in handler is found then cache and return it - Class handler = m_secureAction.forName(className, classLoader); - if (handler != null) - { - return (URLStreamHandler) handler.newInstance(); - } - } - catch (Throwable ex) - { - // This could be a class not found exception or an - // instantiation exception, not much we can do in either - // case other than ignore it. - } - } - // This is a workaround for android - Starting with 4.1 the built-in core handler - // are not following the normal naming package schema :-( - String androidHandler = null; - if ("file".equalsIgnoreCase(protocol)) - { - androidHandler = "libcore.net.url.FileHandler"; - } - else if ("ftp".equalsIgnoreCase(protocol)) - { - androidHandler = "libcore.net.url.FtpHandler"; - } - else if ("http".equalsIgnoreCase(protocol)) - { - androidHandler = "libcore.net.http.HttpHandler"; - } - else if ("https".equalsIgnoreCase(protocol)) - { - androidHandler = "libcore.net.http.HttpsHandler"; - } - else if ("jar".equalsIgnoreCase(protocol)) - { - androidHandler = "libcore.net.url.JarHandler"; - } - if (androidHandler != null) - { - try - { - // If a built-in handler is found then cache and return it - Class handler = m_secureAction.forName(androidHandler, classLoader); - if (handler != null) - { - return (URLStreamHandler) handler.newInstance(); - } - } - catch (Throwable ex) - { - // This could be a class not found exception or an - // instantiation exception, not much we can do in either - // case other than ignore it. - } - } - return null; - } /** *

- * This is a method implementation for the URLStreamHandlerFactory - * interface. It simply creates a stream handler proxy object for the - * specified protocol. It caches the returned proxy; therefore, subsequent - * requests for the same protocol will receive the same handler proxy. + * Static method that adds a framework instance to the centralized + * instance registry. *

- * @param protocol the protocol for which a stream handler should be returned. - * @return a stream handler proxy for the specified protocol. + * @param framework the framework instance to be added to the instance + * registry. + * @param enable a flag indicating whether or not the framework wants to + * enable the URL Handlers service. **/ - @Override - public URLStreamHandler createURLStreamHandler(String protocol) + public static void registerFrameworkInstance(Felix framework, boolean enable) { - // See if there is a cached stream handler. - // IMPLEMENTATION NOTE: Caching is not strictly necessary for - // stream handlers since the Java runtime caches them. Caching is - // performed for code consistency between stream and content - // handlers and also because caching behavior may not be guaranteed - // across different JRE implementations. - URLStreamHandler handler = getFromCache(m_streamHandlerCache, protocol); - - if (handler != null) - { - return handler; - } - // If this is the framework's "bundle:" protocol, then return - // a handler for that immediately, since no one else can be - // allowed to deal with it. - if (protocol.equals(FelixConstants.BUNDLE_URL_PROTOCOL)) + synchronized (m_frameworks) { - return new URLHandlersBundleStreamHandler(getFrameworkFromContext(), m_secureAction); + if (enable) + { + m_frameworks.add(framework); + } + m_counter++; } - handler = getBuiltInStreamHandler(protocol, - (m_streamHandlerFactory != this) ? m_streamHandlerFactory : null); - - if (handler == null && isJVM(protocol)) + if (enable) { - return null; + // FELIX-6759: this class no longer installs itself as the JVM stream and + // content handler factory. Doing so meant reflectively clearing the + // java.net.URL static fields, and it left the JVM factories occupied by + // the time plurl ran, forcing plurl into deep reflection into java.net + // and failing without --add-opens. Each framework registers its own + // factory with the plurl router instead, on a clean JVM, through the + // supported URL.setURLStreamHandlerFactory API. + registerWithPlurl(framework); } - // If built-in content handler, then create a proxy handler. - return addToCache(m_streamHandlerCache, protocol, - URLHandlersStreamHandlerProxy.wrap(protocol, m_secureAction, - handler, getFromCache(m_protocolToURL, protocol))); - } - - private boolean isJVM(String protocol) - { - return protocol.equals("file") || - protocol.equals("ftp") || - protocol.equals("http") || - protocol.equals("https") || - protocol.equals("jar") || - protocol.equals("jmod") || - protocol.equals("mailto") || - protocol.equals("jrt"); } /** + * Registers the given framework's URL handling with the plurl router. *

- * This is a method implementation for the ContentHandlerFactory - * interface. It simply creates a content handler proxy object for the - * specified mime type. It caches the returned proxy; therefore, subsequent - * requests for the same content type will receive the same handler proxy. - *

- * @param mimeType the mime type for which a content handler should be returned. - * @return a content handler proxy for the specified mime type. - **/ - @Override - public ContentHandler createContentHandler(String mimeType) + * Failing to register must not prevent the framework from starting. As in + * Equinox, the consequence is simply that this framework instance contributes no + * URL handlers; there is deliberately no fallback to swapping the java.net.URL + * singleton fields, since removing that is the point of using plurl. + */ + private static void registerWithPlurl(Felix framework) { - // See if there is a cached stream handler. - // IMPLEMENTATION NOTE: Caching is not strictly necessary for - // stream handlers since the Java runtime caches them. Caching is - // performed for code consistency between stream and content - // handlers and also because caching behavior may not be guaranteed - // across different JRE implementations. - ContentHandler handler = getFromCache(m_contentHandlerCache, mimeType); - - if (handler != null) + PlurlURLHandlers handlers = + PlurlURLHandlers.install(framework, m_secureAction); + if (handlers != null) { - return handler; + m_plurlHandlers.put(framework, handlers); } - - return addToCache(m_contentHandlerCache, mimeType, - new URLHandlersContentHandlerProxy(mimeType, m_secureAction, - (m_contentHandlerFactory != this) ? m_contentHandlerFactory : null)); - } - - private static V addToCache(ConcurrentHashMap cache, K key, V value) - { - return key != null && value != null ? putIfAbsentAndReturn(cache, key, value) : null; - } - - private static V getFromCache(ConcurrentHashMap cache, K key) - { - return key != null ? cache.get(key) : null; } /** - *

- * Static method that adds a framework instance to the centralized - * instance registry. - *

- * @param framework the framework instance to be added to the instance - * registry. - * @param enable a flag indicating whether or not the framework wants to - * enable the URL Handlers service. - **/ - public static void registerFrameworkInstance(Felix framework, boolean enable) + * The plurl registration for the given framework, or null if it is not + * registered. Package private for testing. + */ + static PlurlURLHandlers getPlurlHandlers(Felix framework) { - boolean register = false; - synchronized (m_frameworks) - { - // If the URL Handlers service is not going to be enabled, - // then return immediately. - if (enable) - { - // We need to create an instance if this is the first - // time this method is called, which will set the handler - // factories. - if (m_handler == null ) - { - register = true; - } - else - { - m_frameworks.add(framework); - m_counter++; - } - } - else - { - m_counter++; - } - } - if (register) - { - synchronized (URL.class) - { - synchronized (m_classloaderToFrameworkLists) - { - synchronized (m_frameworks) - { - if (m_handler == null ) - { - m_handler = new URLHandlers(); - } - m_frameworks.add(framework); - m_counter++; - } - } - } - } + return m_plurlHandlers.get(framework); } /** @@ -676,58 +158,19 @@ public static void registerFrameworkInstance(Felix framework, boolean enable) **/ public static void unregisterFrameworkInstance(Object framework) { - boolean unregister = false; - synchronized (m_frameworks) + if (framework instanceof Felix) { - if (m_frameworks.contains(framework)) - { - if (m_frameworks.size() == 1 && m_handler != null) - { - unregister = true; - } - else - { - m_frameworks.remove(framework); - m_counter--; - } - } - else + PlurlURLHandlers handlers = m_plurlHandlers.remove(framework); + if (handlers != null) { - m_counter--; + handlers.uninstall(); } } - if (unregister) - { - synchronized (URL.class) - { - synchronized (m_classloaderToFrameworkLists) - { - synchronized (m_frameworks) - { - m_frameworks.remove(framework); - m_counter--; - if (m_frameworks.isEmpty() && m_handler != null) - { - m_handler = null; - try - { - m_secureAction.invoke(m_secureAction.getDeclaredMethod( - m_rootURLHandlers.getClass(), - "unregisterFrameworkListsForContextSearch", - new Class[]{ ClassLoader.class}), - m_rootURLHandlers, - new Object[] {URLHANDLERS_CLASS.getClassLoader()}); - } - catch (Exception e) - { - // This should not happen - e.printStackTrace(); - } - } - } - } - } + synchronized (m_frameworks) + { + m_frameworks.remove(framework); + m_counter--; } } @@ -746,7 +189,7 @@ public static Object getFrameworkFromContext() { // This is a hack. The idea is to return the only registered framework quickly int attempts = 0; - while (m_classloaderToFrameworkLists.isEmpty() && (m_counter == 1) && (m_frameworks.size() == 1)) + while ((m_counter == 1) && (m_frameworks.size() == 1)) { Object framework = m_frameworks.get(0); @@ -788,12 +231,12 @@ else if (attempts++ > 3) { ClassLoader index = m_secureAction.getClassLoader(targetClassLoader.getClass()); - List frameworks = m_classloaderToFrameworkLists.get(index); + // Only classes loaded by a bundle of a framework from this copy of the + // framework can be ours; another copy in the JVM routes through its own + // plurl factory. + List frameworks = + (index == URLHANDLERS_CLASS.getClassLoader()) ? m_frameworks : null; - if ((frameworks == null) && (index == URLHANDLERS_CLASS.getClassLoader())) - { - frameworks = m_frameworks; - } if (frameworks != null) { // Check the registry of framework instances @@ -833,29 +276,6 @@ public static Object getFrameworkFromContext(String uuid) return framework; } } - for (List frameworks : m_classloaderToFrameworkLists.values()) - { - for (Object framework : frameworks) - { - try - { - if (uuid.equals( - m_secureAction.invoke( - m_secureAction.getDeclaredMethod(framework.getClass(),"getProperty", new Class[]{String.class}), - framework, new Object[]{Constants.FRAMEWORK_UUID}))) - { - return framework; - } - } - catch (Exception ex) - { - // This should not happen but if it does there is - // not much we can do other then ignore it. - // Maybe log this or something. - ex.printStackTrace(); - } - } - } } return getFrameworkFromContext(); } diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java index e8f7a00cb7..d8b96778fc 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java @@ -22,10 +22,12 @@ import java.lang.reflect.Constructor; import java.net.*; +import org.apache.felix.framework.plurl.PlurlStreamHandlerBase; + import org.apache.felix.framework.util.SecureAction; import org.apache.felix.framework.util.Util; -class URLHandlersBundleStreamHandler extends URLStreamHandler +class URLHandlersBundleStreamHandler extends PlurlStreamHandlerBase { private final Object m_framework; private final SecureAction m_action; @@ -43,7 +45,7 @@ public URLHandlersBundleStreamHandler(SecureAction action) } @Override - protected URLConnection openConnection(URL url) throws IOException + public URLConnection openConnection(URL url) throws IOException { Object framework = m_framework; @@ -88,7 +90,7 @@ protected void parseURL(URL u, String spec, int start, int limit) } @Override - protected String toExternalForm(URL u) + public String toExternalForm(URL u) { StringBuilder result = new StringBuilder(); result.append(u.getProtocol()); @@ -114,7 +116,7 @@ protected String toExternalForm(URL u) } @Override - protected java.net.InetAddress getHostAddress(URL u) + public java.net.InetAddress getHostAddress(URL u) { return null; } diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java index d2fa1fe61f..3d0749b319 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java @@ -28,6 +28,8 @@ import java.net.URLConnection; import java.net.URLStreamHandler; +import org.apache.felix.framework.plurl.PlurlStreamHandlerBase; + import org.apache.felix.framework.util.SecureAction; import org.osgi.service.url.URLStreamHandlerService; import org.osgi.service.url.URLStreamHandlerSetter; @@ -54,7 +56,7 @@ * stream handler service at any given time. *

**/ -public class URLHandlersStreamHandlerProxy extends URLStreamHandler +public class URLHandlersStreamHandlerProxy extends PlurlStreamHandlerBase implements URLStreamHandlerSetter, InvocationHandler { private static final Class[] URL_PROXY_CLASS; @@ -140,7 +142,7 @@ static URLStreamHandler wrap(String protocol, SecureAction action, URLStreamHand // URLStreamHandler interface methods. // @Override - protected boolean equals(URL url1, URL url2) + public boolean equals(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) @@ -163,7 +165,7 @@ protected boolean equals(URL url1, URL url2) } @Override - protected int getDefaultPort() + public int getDefaultPort() { Object svc = getStreamHandlerService(); if (svc == null) @@ -185,7 +187,7 @@ protected int getDefaultPort() } @Override - protected InetAddress getHostAddress(URL url) + public InetAddress getHostAddress(URL url) { Object svc = getStreamHandlerService(); if (svc == null) @@ -208,7 +210,7 @@ protected InetAddress getHostAddress(URL url) } @Override - protected int hashCode(URL url) + public int hashCode(URL url) { Object svc = getStreamHandlerService(); if (svc == null) @@ -231,7 +233,7 @@ protected int hashCode(URL url) } @Override - protected boolean hostsEqual(URL url1, URL url2) + public boolean hostsEqual(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) @@ -254,7 +256,7 @@ protected boolean hostsEqual(URL url1, URL url2) } @Override - protected URLConnection openConnection(URL url) throws IOException + public URLConnection openConnection(URL url) throws IOException { Object svc = getStreamHandlerService(); if (svc == null) @@ -308,7 +310,7 @@ protected URLConnection openConnection(URL url) throws IOException } @Override - protected URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException + public URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException { Object svc = getStreamHandlerService(); if (svc == null) @@ -431,7 +433,7 @@ protected void parseURL(URL url, String spec, int start, int limit) } @Override - protected boolean sameFile(URL url1, URL url2) + public boolean sameFile(URL url1, URL url2) { Object svc = getStreamHandlerService(); if (svc == null) @@ -471,7 +473,7 @@ public void setURL( } @Override - protected String toExternalForm(URL url) + public String toExternalForm(URL url) { return toExternalForm(url, getStreamHandlerService()); } diff --git a/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java b/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java new file mode 100644 index 0000000000..d88a148e22 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java @@ -0,0 +1,179 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.framework.plurl.Plurl; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.osgi.framework.Constants; + +/** + * Verifies that a running framework registers its URL handling with plurl + * (FELIX-6759), rather than taking over the java.net.URL singletons by swapping a + * private static field. + */ +class PlurlURLHandlersTest +{ + private Felix m_felix; + private File m_cacheDir; + + @BeforeEach + void setUp() throws Exception + { + m_cacheDir = File.createTempFile("felix-cache", ".dir"); + m_cacheDir.delete(); + m_cacheDir.mkdirs(); + + Map params = new HashMap<>(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + params.put(Constants.FRAMEWORK_STORAGE, m_cacheDir.getAbsolutePath()); + + m_felix = new Felix(params); + m_felix.init(); + m_felix.start(); + } + + @AfterEach + void tearDown() throws Exception + { + if (m_felix != null) + { + m_felix.stop(); + m_felix.waitForStop(10000); + } + deleteDir(m_cacheDir); + } + + /** + * The framework must have registered a factory with the plurl router while + * starting. Before FELIX-6759 nothing called Plurl.install(..), so no registration + * existed at all. + */ + @Test + void registersWithPlurlOnStart() + { + assertThat(URLHandlers.getPlurlHandlers(m_felix)) + .as("framework should have registered its URL handling with plurl") + .isNotNull(); + } + + /** + * Installing plurl is what makes the plurl: protocol resolvable, so being able to + * construct such a URL proves the router really took over the JVM factory rather + * than the registration silently failing. + */ + @Test + void plurlRouterIsInstalledInTheJvm() throws Exception + { + URL url = new URL("plurl", "op", "plurlForbidNothing"); + assertThat(url.getProtocol()).isEqualTo("plurl"); + } + + /** + * shouldHandle must claim only classes belonging to this framework instance. + * Claiming any Felix bundle would mean one framework answering lookups for a + * bundle resolved in another framework in the same JVM. + */ + @Test + void shouldHandleOnlyClaimsThisFrameworksClasses() + { + PlurlURLHandlers handlers = URLHandlers.getPlurlHandlers(m_felix); + assertThat(handlers).isNotNull(); + + // Not loaded by any bundle class loader. + assertThat(handlers.shouldHandle(String.class)) + .as("JDK classes are not owned by a framework").isFalse(); + assertThat(handlers.shouldHandle(getClass())) + .as("test classes are not loaded from a bundle").isFalse(); + assertThat(handlers.shouldHandle((Class) null)) + .as("null must not be claimed").isFalse(); + } + + /** + * The router that routes in this JVM must be one that consults + * shouldHandle(protocol, spec). Felix warns at startup when it is not, because + * bundle: URLs are then routed to whichever factory registered first; this test + * pins the capability so that re-vendoring an older plurl fails here rather than + * silently turning that warning on for every user. + */ + @Test + void installedPlurlSupportsSelectionBySpec() + { + assertThat(Plurl.getCapability(Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC)) + .as("the installed plurl reports what it supports") + .contains(Boolean.TRUE); + } + + /** + * shouldHandleURL(protocol, spec) must claim only this framework's bundle: URLs. + * The framework UUID is in the host, and it is the only thing that identifies an + * owner when the URL is parsed by a caller that is in no bundle. + */ + @Test + void shouldHandleOnlyClaimsThisFrameworksBundleUrls() + { + PlurlURLHandlers handlers = URLHandlers.getPlurlHandlers(m_felix); + assertThat(handlers).isNotNull(); + + String uuid = m_felix._getProperty(Constants.FRAMEWORK_UUID); + assertThat(handlers.shouldHandleURL("bundle", "bundle://" + uuid + "_1.0/resource")) + .as("this framework's own URL").isTrue(); + assertThat(handlers.shouldHandleURL("bundle", "bundle://" + uuid + "_1.0:0/resource")) + .as("host may carry a port").isTrue(); + assertThat(handlers.shouldHandleURL("bundle", "bundle://someotherframework_1.0/resource")) + .as("another framework's URL must not be claimed").isFalse(); + assertThat(handlers.shouldHandleURL("http", "http://" + uuid + "_1.0/resource")) + .as("only the bundle protocol is claimed").isFalse(); + assertThat(handlers.shouldHandleURL("bundle", "bundle:relative/resource")) + .as("a spec with no host cannot be claimed").isFalse(); + assertThat(handlers.shouldHandleURL("bundle", null)) + .as("the protocol level question: bundle: URLs are ours").isTrue(); + } + + private static void deleteDir(File root) + { + if (root == null || !root.exists()) + { + return; + } + File[] children = root.listFiles(); + if (children != null) + { + for (File child : children) + { + deleteDir(child); + } + } + root.delete(); + } +} From 26ac5e666062cdb007d8843494e085f850144943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Wed, 9 Sep 2026 09:15:27 +0200 Subject: [PATCH 3/3] FELIX-6759 - Reduce sun.misc.Unsafe to where reflection is actually refused Fixes the warning reported in this issue: starting Felix on JDK 24 or later printed that sun.misc.Unsafe::staticFieldOffset is terminally deprecated and will be removed. SecureAction loses swapStaticFieldIfNotClass and flush, which had no callers left once URLHandlers stopped taking over the JVM factories. What remains of Unsafe is getAccessor, which is not a memory access shim that can be ported to VarHandle: it defines an accessor class inside java.base so that setAccessible has an in-module caller, and closing that off is the intent of the JDK's integrity work. It is now reached only when ordinary reflection has already been refused, so where the package is open -- the Add-opens of the org.apache.felix.main launcher, or any --add-opens on the command line -- the deprecated method is never called. ClassPathExtenderFactory resolved URLClassLoader.addURL and ClassLoaders$AppClassLoader.appendToClassPathForInstrumentation in its static initialiser and made both accessible there. That runs from ExtensionManager., so every framework instance paid for it whether or not it ever installs an extension bundle. Looking a method up needs no access, only setAccessible does, so the lookup stays and the access is requested in add(File), where the method is invoked. A framework that installs no extension bundle now never calls setAccessible on a java.base member. Verified on JDK 25 with --sun-misc-unsafe-memory-access=deny: the framework starts and only framework extension bundles fail, cleanly. Co-Authored-By: Claude Opus 5 --- .../ext/ClassPathExtenderFactory.java | 12 +- .../felix/framework/util/SecureAction.java | 203 +++++------------- 2 files changed, 69 insertions(+), 146 deletions(-) diff --git a/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java b/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java index f83663c8c4..da62e469ef 100644 --- a/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java +++ b/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java @@ -41,6 +41,14 @@ final class DefaultClassLoaderExtender implements ClassPathExtenderFactory, Clas private static final Method m_addURL; private final ClassLoader m_loader; + // These are only looked up here, not made accessible. Looking a method up + // needs no access, while setAccessible on a java.base member does, and on a + // JVM where the package is not open that is served by defining an accessor + // inside java.base with sun.misc.Unsafe -- which JDK 25 reports as a + // terminally deprecated call. Since this runs whenever a framework instance + // is created, doing it here made every start pay for it, and print that + // warning, even when no extension bundle is ever installed. The access is + // requested in add(File) instead, at the point it is actually used. static { ClassLoader app = ClassLoader.getSystemClassLoader(); @@ -52,7 +60,6 @@ final class DefaultClassLoaderExtender implements ClassPathExtenderFactory, Clas try { append = app.getClass().getDeclaredMethod("appendToClassPathForInstrumentation", String.class); - new SecureAction().setAccesssible(append); break; } catch (Throwable e) @@ -76,7 +83,6 @@ final class DefaultClassLoaderExtender implements ClassPathExtenderFactory, Clas try { addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - new SecureAction().setAccesssible(addURL); } catch (Throwable e) { @@ -128,6 +134,7 @@ public void add(final File file) throws Exception loader = m_loader; synchronized (m_loader) { + new SecureAction().setAccesssible(m_addURL); m_addURL.invoke(m_loader, file.getCanonicalFile().toURI().toURL()); } } @@ -136,6 +143,7 @@ public void add(final File file) throws Exception loader = m_app; synchronized (m_app) { + new SecureAction().setAccesssible(m_append); m_append.invoke(m_app, file.getCanonicalFile().getPath()); } } diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java index 95fffc6f6c..d09cb791c1 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java +++ b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java @@ -33,7 +33,6 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URI; @@ -47,8 +46,6 @@ import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; import java.util.Collection; -import java.util.HashMap; -import java.util.Hashtable; import java.util.Map; import java.util.function.Consumer; import java.util.jar.JarFile; @@ -105,7 +102,6 @@ public class SecureAction result = new byte[0]; } accessor = result; - getAccessor(URL.class); } protected static transient int BUFSIZE = 4096; @@ -339,170 +335,89 @@ public Object getDeclaredField(Class targetClass, String name, Object target) return field.get(target); } - public Object swapStaticFieldIfNotClass(Class targetClazz, - Class targetType, Class condition, String lockName) throws Exception - { - return _swapStaticFieldIfNotClass(targetClazz, targetType, - condition, lockName); - } - private static volatile Consumer m_accessorCache = null; - @SuppressWarnings("unchecked") + private static final Consumer SET_ACCESSIBLE = + objects -> AccessibleObject.setAccessible(objects, true); + private static Consumer getAccessor(Class clazz) { String packageName = clazz.getPackage().getName(); if ("java.net".equals(packageName) || "jdk.internal.loader".equals(packageName)) { - if (m_accessorCache == null) + // Try ordinary reflection first and only smuggle an accessor into + // java.base if the JVM actually refuses. When the package is open to us + // -- as it is under the Add-opens of the org.apache.felix.main launcher, + // or any --add-opens on the command line -- setAccessible just works, and + // reaching for Unsafe would be both unnecessary and noisy: JDK 25 prints + // a terminal deprecation warning for every call to it. + return objects -> { try { - // Use reflection on Unsafe to avoid having to compile against it - Class unsafeClass = Class.forName("sun.misc.Unsafe"); //$NON-NLS-1$ - Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); //$NON-NLS-1$ - // NOTE: deep reflection is allowed on sun.misc package for java 9. - theUnsafe.setAccessible(true); - Object unsafe = theUnsafe.get(null); - Class> result; - try { - Method defineAnonymousClass = unsafeClass.getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class); //$NON-NLS-1$ - result = (Class>) defineAnonymousClass.invoke(unsafe, URL.class, accessor , null); - } - catch (NoSuchMethodException ex) - { - long offset = (long) unsafeClass.getMethod("staticFieldOffset", Field.class) - .invoke(unsafe, MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP")); - - MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafeClass.getMethod("getObject", Object.class, long.class) - .invoke(unsafe, MethodHandles.Lookup.class, offset); - lookup = lookup.in(URL.class); - Class classOption = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); //$NON-NLS-1$ - Object classOptions = Array.newInstance(classOption, 0); - Method defineHiddenClass = MethodHandles.Lookup.class.getMethod("defineHiddenClass", byte[].class, boolean.class, //$NON-NLS-1$ - classOptions.getClass()); - lookup = (MethodHandles.Lookup) defineHiddenClass.invoke(lookup, accessor, Boolean.FALSE, classOptions); - result = (Class>) lookup.lookupClass(); - } - m_accessorCache = result.getConstructor().newInstance(); + SET_ACCESSIBLE.accept(objects); } - catch (Throwable t) + catch (RuntimeException ex) { - m_accessorCache = objects -> AccessibleObject.setAccessible(objects, true); + // InaccessibleObjectException: java.base is not open to us. + unsafeAccessor().accept(objects); } - } - return m_accessorCache; + }; } - else - { - return objects -> AccessibleObject.setAccessible(objects, true); - } - } - - private static Object _swapStaticFieldIfNotClass(Class targetClazz, - Class targetType, Class condition, String lockName) throws Exception + return SET_ACCESSIBLE; + } + + /** + * An accessor defined inside java.base itself, so that {@code setAccessible} is + * called by a class in that module and the access check passes without the + * package being open. + *

+ * This needs {@code sun.misc.Unsafe} to obtain a trusted + * {@code MethodHandles.Lookup}, which is terminally deprecated as of JDK 25, so + * it is only used when ordinary reflection has already failed. + */ + @SuppressWarnings("unchecked") + private static Consumer unsafeAccessor() { - - Object lock = null; - if (lockName != null) + if (m_accessorCache == null) { try { - Field lockField = - targetClazz.getDeclaredField(lockName); - getAccessor(targetClazz).accept(new AccessibleObject[]{lockField}); - lock = lockField.get(null); - } - catch (NoSuchFieldException ex) - { - } - } - if (lock == null) - { - lock = targetClazz; - } - synchronized (lock) - { - Field[] fields = targetClazz.getDeclaredFields(); - - getAccessor(targetClazz).accept(fields); - - Object result = null; - for (int i = 0; (i < fields.length) && (result == null); i++) - { - if (Modifier.isStatic(fields[i].getModifiers()) && - (fields[i].getType() == targetType)) - { - result = fields[i].get(null); - - if (result != null) - { - if ((condition == null) || - !result.getClass().getName().equals(condition.getName())) - { - fields[i].set(null, null); - } - } + // Use reflection on Unsafe to avoid having to compile against it + Class unsafeClass = Class.forName("sun.misc.Unsafe"); //$NON-NLS-1$ + Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); //$NON-NLS-1$ + // NOTE: deep reflection is allowed on sun.misc package for java 9. + theUnsafe.setAccessible(true); + Object unsafe = theUnsafe.get(null); + Class> result; + try { + Method defineAnonymousClass = unsafeClass.getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class); //$NON-NLS-1$ + result = (Class>) defineAnonymousClass.invoke(unsafe, URL.class, accessor , null); } - } - if (result != null) - { - if ((condition == null) || !result.getClass().getName().equals(condition.getName())) + catch (NoSuchMethodException ex) { - // reset cache - for (Field field : fields) { - if (Modifier.isStatic(field.getModifiers()) && - (field.getType() == Hashtable.class)) - { - Hashtable cache = (Hashtable) field.get(null); - if (cache != null) - { - cache.clear(); - } - } - } + long offset = (long) unsafeClass.getMethod("staticFieldOffset", Field.class) + .invoke(unsafe, MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP")); + + MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafeClass.getMethod("getObject", Object.class, long.class) + .invoke(unsafe, MethodHandles.Lookup.class, offset); + lookup = lookup.in(URL.class); + Class classOption = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); //$NON-NLS-1$ + Object classOptions = Array.newInstance(classOption, 0); + Method defineHiddenClass = MethodHandles.Lookup.class.getMethod("defineHiddenClass", byte[].class, boolean.class, //$NON-NLS-1$ + classOptions.getClass()); + lookup = (MethodHandles.Lookup) defineHiddenClass.invoke(lookup, accessor, Boolean.FALSE, classOptions); + result = (Class>) lookup.lookupClass(); } - return result; + m_accessorCache = result.getConstructor().newInstance(); } - } - return null; - } - - public void flush(ClasstargetClazz, Object lock) throws Exception - { - _flush(targetClazz, lock); - } - - private static void _flush(ClasstargetClazz, Object lock) throws Exception - { - synchronized (lock) - { - Field[] fields = targetClazz.getDeclaredFields(); - getAccessor(targetClazz).accept(fields); - // reset cache - for (Field field : fields) { - if (Modifier.isStatic(field.getModifiers()) && - ((field.getType() == Hashtable.class) || (field.getType() == HashMap.class))) - { - if (field.getType() == Hashtable.class) - { - Hashtable cache = (Hashtable) field.get(null); - if (cache != null) - { - cache.clear(); - } - } - else - { - HashMap cache = (HashMap) field.get(null); - if (cache != null) - { - cache.clear(); - } - } - } + catch (Throwable t) + { + // Nothing else to try; let the caller see the failure. + m_accessorCache = SET_ACCESSIBLE; } } + return m_accessorCache; } public void invokeBundleCollisionHook(