diff --git a/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java b/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java new file mode 100644 index 0000000000..4aab139b3d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/PlurlURLHandlers.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.felix.framework; + +import java.io.IOException; +import java.net.ContentHandler; +import java.net.URLStreamHandler; + +import org.apache.felix.framework.plurl.Plurl; +import org.apache.felix.framework.plurl.PlurlContentHandlerFactory; +import org.apache.felix.framework.plurl.PlurlStreamHandlerFactory; + +/** + *

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

+ *

+ * {@code URLHandlers} currently claims the JVM-wide {@code java.net.URL} stream + * handler factory by reflectively swapping a private static field + * ({@code SecureAction.swapStaticFieldIfNotClass}). Obtaining a + * {@code MethodHandles.Lookup} trusted enough to do that is the sole reason the + * framework still uses {@code sun.misc.Unsafe}, and it means whichever framework + * installs itself last wins the singleton - so Felix and Equinox cannot coexist in + * one JVM without clobbering each other. + *

+ *

+ * Plurl instead installs one cooperative router through the supported + * {@code URL.setURLStreamHandlerFactory} API and lets any number of parties + * register with it. Each registered factory answers {@link #shouldHandle(Class)} + * to say whether a given calling class belongs to it; plurl walks the call stack + * and routes accordingly. That maps directly onto what + * {@link URLHandlers#getFrameworkFromContext()} already does. + *

+ *

+ * See {@code org/apache/felix/framework/plurl/README.md} for the provenance of the + * vendored plurl sources and the unresolved licensing question that currently + * blocks this approach. + *

+ */ +class PlurlURLHandlers implements PlurlStreamHandlerFactory, PlurlContentHandlerFactory +{ + private final URLHandlers m_delegate; + + PlurlURLHandlers(URLHandlers delegate) + { + m_delegate = delegate; + } + + /** + * Registers this framework's handlers with the plurl router, installing the + * router first if nobody has yet. + */ + static PlurlURLHandlers install(URLHandlers delegate) throws IOException + { + PlurlURLHandlers handlers = new PlurlURLHandlers(delegate); + Plurl.add((PlurlStreamHandlerFactory) handlers); + Plurl.add((PlurlContentHandlerFactory) handlers); + return handlers; + } + + /** + * Unregisters this framework's handlers, leaving the router in place for any + * other framework instance still using it. + */ + void uninstall() throws IOException + { + Plurl.remove((PlurlStreamHandlerFactory) this); + Plurl.remove((PlurlContentHandlerFactory) this); + } + + /** + * Tells plurl whether the given calling class belongs to this framework. + *

+ * This replaces the call stack walking URLHandlers does today: rather than + * inspecting the stack itself to work out which framework owns the caller, the + * router asks each registered factory about a single candidate class. + */ + @Override + public boolean shouldHandle(Class clazz) + { + if (clazz == null) + { + return false; + } + ClassLoader loader = clazz.getClassLoader(); + if (loader == null) + { + return false; + } + String name = loader.getClass().getName(); + return name.startsWith("org.apache.felix.framework.BundleWiringImpl$BundleClassLoader") + || name.startsWith("org.apache.felix.framework.ModuleImpl$ModuleClassLoader") + || name.equals("org.apache.felix.framework.searchpolicy.ContentClassLoader"); + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) + { + return m_delegate.createURLStreamHandler(protocol); + } + + @Override + public ContentHandler createContentHandler(String mimeType) + { + return m_delegate.createContentHandler(mimeType); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java b/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java new file mode 100644 index 0000000000..fba7df4b96 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/Plurl.java @@ -0,0 +1,308 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +import java.io.IOException; +import java.net.ContentHandlerFactory; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandlerFactory; +import java.util.function.Consumer; + +/** + * Plurl is used to multiplex the URL factory singletons for + * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)} and + * {@link URLConnection#setContentHandlerFactory(ContentHandlerFactory)}. Plurl + * factories may be added and removed using the add and remove methods or using + * the {@link #PLURL_PROTOCOL plurl} protocol. + * + *

+ * The {@link #PLURL_PROTOCOL plurl} protocol allows factories to be added even + * if the installed plurl implementation is not using the same + * org.apache.felix.framework.plurl package as the factories being registered. A plurl + * implementation must handle this case by reflecting on the plurl factories + * that are added. A plurl factory can be added and removed with the plurl + * protocol like this: + * + *

+ * PlurlStreamHandlerFactory myStreamFactory = getStreamFactory();
+ * PlurlContentHandlerFactory myContentFactory = getContentFactory();
+ * 
+ * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/addURLStreamHandlerFactory").getContent()).accept(myStreamFactory);
+ * ((Consumer<ContentHandlerFactory>) ("plurl://op/addContentHandlerFactory").getContent()).accept(myContentFactory);
+ *
+ * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/removeURLStreamHandlerFactory").getContent())
+ * 		.accept(myStreamFactory);
+ * ((Consumer<ContentHandlerFactory>) ("plurl://op/removeContentHandlerFactory").getContent()).accept(myContentFactory);
+ * 
+ * + * The content provided by the plurl protocol is of type {@link Consumer} which + * can take either an {@link URLStreamHandlerFactory} or a + * {@link ContentHandlerFactory} depending on the operation. + * + *

+ * A plurl implementation delegates to the added {@link PlurlFactory} objects. + * To select which {@code PlurlFactory} to delegate the + * {@link PlurlFactory#shouldHandle(Class)} method is used. + *

+ * If only one factory has been added to plurl then that {@code PlurlFactory} is + * used to create the handler. Otherwise each + * {@link PlurlFactory#shouldHandle(Class)} is called for a class in the call + * stack until a factory returns true. If no factory returns true then the next + * class in the call stack is used. If no factory is found after using all + * classes in the call stack then the first factory added is selected. Once a + * factory is selected, it is used to create the requested handler. If the + * selected factory returns a {@code null} handler then no other factory is + * asked to create the handler. + * + * @see #PLURL_ADD_URL_STREAM_HANDLER_FACTORY + * @see #PLURL_ADD_CONTENT_HANDLER_FACTORY + * @see #PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY + * @see #PLURL_REMOVE_CONTENT_HANDLER_FACTORY + */ +public interface Plurl { + /** + * The "plurl" protocol to add and remove plurl factories. + */ + public static final String PLURL_PROTOCOL = "plurl"; //$NON-NLS-1$ + /** + * The host to use for the "plurl" protocol to indicate an operation for adding + * or removing factories. + */ + public static final String PLURL_OP = "op"; //$NON-NLS-1$ + /** + * The plurl protocol operation to add a URLStreamHandlerFactory + */ + public static final String PLURL_ADD_URL_STREAM_HANDLER_FACTORY = "addURLStreamHandlerFactory"; //$NON-NLS-1$ + /** + * The plurl protocol operation to remove a URLStreamHandlerFactory + */ + public static final String PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY = "removeURLStreamHandlerFactory"; //$NON-NLS-1$ + + /** + * The plurl protocol operation to add a ContentStreamHandlerFactory + */ + public static final String PLURL_ADD_CONTENT_HANDLER_FACTORY = "addContentHandlerFactory"; //$NON-NLS-1$ + + /** + * The plurl protocol operation to remove a ContentStreamHandlerFactory + */ + public static final String PLURL_REMOVE_CONTENT_HANDLER_FACTORY = "removeContentHandlerFactory"; //$NON-NLS-1$ + + /** + * An optional plurl protocol operation to register a {@code Plurl} instance + * with the current plurl protocol implementation. This is an optional operation + * that a {@code Plurl} implementation may implement to allow another plurl + * instance to be registered as a delegate. A delegate may be used to install + * the delegate plurl instance when the current plurl gets {@link #uninstall() + * uninstalled}. + */ + public static final String PLURL_REGISTER_IMPLEMENTATION = "plurlRegisterImplementation"; //$NON-NLS-1$ + + /** + * An optional plurl protocol operation to unregister a {@code Plurl} instance + * with the current plurl instance set with the JVM. This is an optional + * operation that a {@code Plurl} implementation may implement to allow another + * plurl instance to be unregistered as a delegate. + */ + public static final String PLURL_UNREGISTER_IMPLEMENTATION = "plurlUnegisterImplementation"; //$NON-NLS-1$ + /** + * The value to use for the {@link #install(String...)} method to indicate that + * no protocols are forbidden. for overriding by plurl handlers. + */ + public static final String PLURL_FORBID_NOTHING = "plurlForbidNothing"; //$NON-NLS-1$ + + /** + * Installs the plurl factories into the JVM singletons. If plurl factories are + * already installed then this plurl instance is + * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with the existing plurl + * instance set with the JVM by using something like the following: + * + *

+	 * ((Consumer<Object>) ("plurl://op/plurlRegisterImplementation").getContent()).accept(this);
+	 * 
+ * + * If the plurl factories cannot be installed then an + * {@code IllegalStateException} is thrown. + *

+ * If the JVM singletons are already set with other factories that are not plurl + * then an attempt is made to override the JVM singletons with this plurl + * instance. This may only be possible if the implementation is allowed to do + * deep reflection on the {@code java.net} package. If the JVM singletons are + * overriden then the original singleton factory instances must be used as + * parent factories of the plurl instance until the plurl instance is + * {@link #uninstall() uninstalled}. if overriding the JVM singletons is not + * possible then an {@link IllegalStateException} is thrown. + *

+ * If the JVM singletons were not overriden then this plurl instance is + * considered the primordial singleton factory for the JVM. Such a plurl + * instance cannot be {@link #uninstall() uninstalled} and will live the + * lifetime of the JVM. + *

+ * When this method returns without throwing an exception then the following + * will be true: + *

    + *
  1. The singleton + * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)} is set with a + * plurl implementation which delegates to the {@link PlurlStreamHandlerFactory} + * objects that have been {@link #add(PlurlStreamHandlerFactory) added}. + *
  2. The singleton + * {@link URLConnection#setContentHandlerFactory(ContentHandlerFactory)} is set + * with a plurl implementation which delegates to the + * {@link PlurlContentHandlerFactory} objects that have been + * {@link #add(PlurlContentHandlerFactory) added}.
  3. + *
  4. The {@link #PLURL_PROTOCOL plurl} protocol is available for creating + * {@code URL} objects.
  5. + *
  6. If plurl factories are already installed then this plurl implementation + * is registered as a delegate with the already installed plurl instance.
  7. + *
+ * + * @param forbidden builtin JVM protocols that cannot be overridden by plurl. If + * no forbidden protocols are specified then the default + * forbidden protocols are 'jar', 'jmod', 'file', and 'jrt'. To + * forbid no protocols then use the value + * {@link #PLURL_FORBID_NOTHING} + * @throws IllegalStateException if the Plurl factories cannot be installed + */ + public void install(String... forbidden); + + /** + * If this plurl instance is the primordial factory for the JVM then uninstall + * is a no-op and the plurl instance will remain set with the JVM for the + * lifetime of the JVM instance. + *

+ * If this plurl is not the primordial factory and is the current plurl set with + * the JVM singletons then this plurl instance must do the following: + *

    + *
  1. Reset the original parent factories as the singleton factories of the + * JVM
  2. + *
  3. If there are any other plurl instances that got + * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with this plurl instance + * then one of the registered plurl instances must be selected to be the next + * delegate plurl instance to {@link #install(String...) install}.
  4. + *
  5. If a delegate plurl instance gets installed then any existing factories + * that were added to this plurl instance must be added to the new delegate + * plurl instance and any {@link #PLURL_REGISTER_IMPLEMENTATION registered} + * plurl instances must be registered with the new delegate plurl instance.
  6. + *
  7. This plurl instance must release all references to other factories or + * plurl instances.
  8. + *
+ * If this plurl instance is not the current plurl set with JVM then this plurl + * {@link #PLURL_REGISTER_IMPLEMENTATION registered} with the existing plurl + * instance set with the JVM by using something like the following: + * + *
+	 * ((Consumer<Object>) ("plurl://op/plurlRegisterImplementation").getContent()).accept(this);
+	 * 
+ */ + public void uninstall(); + + /** + * Adds a {@link PlurlStreamHandlerFactory} to an {@link #install installed} + * plurl implementation. If there is no plurl implementation installed then an + * {@link IOException} is thrown. The plurl implementation must not hold any + * strong references to the factory. If the factory is garbage collected then + * the plurl implementation must behave as if the factory got + * {@link #remove(PlurlStreamHandlerFactory) removed}. + *

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

+	 * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/addURLStreamHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @param factory the PlurlStreamHandlerFactory to add + * @throws IOException if there is no plurl implementation installed or there + * was an error adding the factory + */ + public static void add(PlurlStreamHandlerFactory factory) throws IOException { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_ADD_URL_STREAM_HANDLER_FACTORY); + @SuppressWarnings("unchecked") + Consumer addFactory = (Consumer) plurl.openConnection() + .getContent(); + addFactory.accept(factory); + } + + /** + * Removes a {@link PlurlStreamHandlerFactory} to an {@link #install installed} + * plurl implementation. If there is no plurl implementation installed then an + * {@link IOException} is thrown. + *

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

+	 * ((Consumer<URLStreamHandlerFactory>) ("plurl://op/removeURLStreamHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @param factory the PlurlStreamHandlerFactory to remove + * @throws IOException if there is no plurl implementation installed or there + * was an error removing the factory + */ + public static void remove(PlurlStreamHandlerFactory factory) throws IOException { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY); + @SuppressWarnings("unchecked") + Consumer removeFactory = (Consumer) plurl.openConnection() + .getContent(); + removeFactory.accept(factory); + } + + /** + * Adds a {@link PlurlContentHandlerFactory} from an {@link #install installed} + * plurl implementation. If there is no plurl implementation installed then an + * {@link IOException} is thrown. The plurl implementation must not hold any + * strong references to the factory. If the factory is garbage collected then + * the plurl implementation must behave as if the factory got + * {@link #remove(PlurlContentHandlerFactory) removed}. + *

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

+	 * ((Consumer<ContentHandlerFactory>) ("plurl://op/addContentHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @param factory the PlurlContentHandlerFactory to add + * @throws IOException if there is no plurl implementation installed or there + * was an error adding the factory + */ + public static void add(PlurlContentHandlerFactory factory) throws IOException { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_ADD_CONTENT_HANDLER_FACTORY); + @SuppressWarnings("unchecked") + Consumer addFactory = (Consumer) plurl.openConnection() + .getContent(); + addFactory.accept(factory); + } + + /** + * Removes a {@link PlurlContentHandlerFactory} from an {@link #install + * installed} plurl implementation. If there is no plurl implementation + * installed then an {@link IOException} is thrown. + *

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

+	 * ((Consumer<ContentHandlerFactory>) ("plurl://op/removeContentHandlerFactory").getContent()).accept(factory);
+	 * 
+ * + * @param factory the PlurlContentHandlerFactory to remove + * @throws IOException if there is no plurl implementation installed or there + * was an error removing the factory + */ + public static void remove(PlurlContentHandlerFactory factory) throws IOException { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, Plurl.PLURL_REMOVE_CONTENT_HANDLER_FACTORY); + @SuppressWarnings("unchecked") + Consumer removeFactory = (Consumer) plurl.openConnection() + .getContent(); + removeFactory.accept(factory); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java new file mode 100644 index 0000000000..293edb4739 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +import java.net.ContentHandlerFactory; + +/** + * A {@link ContentHandlerFactory} that also implements {@link PlurlFactory} + */ +public interface PlurlContentHandlerFactory extends ContentHandlerFactory, PlurlFactory { + // a marker interface for a ContentHandlerFactory that implements PlurlFactory +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java new file mode 100644 index 0000000000..e72ae8a16b --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +/** + * A plural factory that can be added to a plurl implementation. A plurl + * implementation uses {@code PlurlFactory} objects to locate a factory to + * provider a handler. + * + * @see Plurl#add(PlurlContentHandlerFactory) + * @see Plurl#add(PlurlStreamHandlerFactory) + */ +public interface PlurlFactory { + /** + * A plurl implementation will call this method with the classes in the call + * stack which are using the java.net APIs to create URL objects for a specific + * type. For example, a protocol or content type. + * + * @param clazz a class in the call stack using the java.net APIs + * @return true if this factory should be used to handle the request + */ + boolean shouldHandle(Class clazz); +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java new file mode 100644 index 0000000000..bad743112d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; + +/** + * The {@code PlurlStreamHandler} interface has public versions of the protected + * {@link java.net.URLStreamHandler} methods. + *

+ * The important differences between this interface and the + * {@code URLStreamHandler} class are that the {@code setURL} method is absent + * and the {@code parseURL} method takes a {@link PlurlSetter} object as the + * first argument. Classes implementing this interface must call the + * {@code setURL} method on the {@code PlurlSetter} object received in the + * {@code parseURL} method instead of {@code URLStreamHandler.setURL} to avoid a + * {@code SecurityException}. + * + * @see PlurlStreamHandlerBase + * + */ +public interface PlurlStreamHandler { + /** + * Interface used by {@code PlurlStreamHandler} objects to call the + * {@code setURL} method on the plurl proxy {@code URLStreamHandler} object. + * + *

+ * Objects of this type are passed to the + * {@link PlurlStreamHandler#parseURL(PlurlSetter, URL, String, int, int)} + * method. Invoking the {@code setURL} method on the + * {@code URLStreamHandlerSetter} object will invoke the {@code setURL} method + * on the plurl proxy {@code URLStreamHandler} object that is actually + * registered with {@code java.net.URL} for the protocol. + * + */ + public interface PlurlSetter { + /** + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)" + */ + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref); + } + + /** + * @see "java.net.URLStreamHandler.equals(URL, URL)" + */ + public boolean equals(URL u1, URL u2); + + /** + * @see "java.net.URLStreamHandler.hashCode(URL)" + */ + public int hashCode(URL u); + + /** + * @see "java.net.URLStreamHandler.hostsEqual(URL, URL)" + */ + public boolean hostsEqual(URL u1, URL u2); + + /** + * @see "java.net.URLStreamHandler.getDefaultPort" + */ + public int getDefaultPort(); + + /** + * @see "java.net.URLStreamHandler.getHostAddress(URL)" + */ + public InetAddress getHostAddress(URL u); + + /** + * @see "java.net.URLStreamHandler.openConnection(URL)" + */ + public URLConnection openConnection(URL u) throws IOException; + + /** + * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)" + */ + public URLConnection openConnection(URL u, Proxy p) throws IOException; + + /** + * @see "java.net.URLStreamHandler.sameFile(URL, URL)" + */ + public boolean sameFile(URL u1, URL u2); + + /** + * @see "java.net.URLStreamHandler.toExternalForm(URL)" + */ + public String toExternalForm(URL u); + + /** + * Parse a URL. This method is called by the {@code URLStreamHandler} proxy + * implemented by plurl, instead of {@code java.net.URLStreamHandler.parseURL}, + * passing a {@code PlurlSetter} object. + * + * @param plurlSetter The object on which {@code setURL} must be invoked for + * this URL. If the setter is {@code null} then the + * {@link PlurlStreamHandler#setURL(URL, String, String, int, String, String, String, String, String)} + * method can be called directly. + * @see "java.net.URLStreamHandler.parseURL" + */ + public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit); + + /** + * If the plurlSetter is not {@code null} from the + * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the + * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)} + * method. Otherwise call {@code super.setURL}. + * + * @see "java.net.URLStreamHandler.setURL" + */ + public void setURL(URL u, String proto, String host, int port, String file, String ref); + + /** + * If the plurlSetter is not {@code null} from the + * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the + * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)} + * method. Otherwise call {@code super.setURL}. + * + * @see "java.net.URLStreamHandler.setURL" + */ + public void setURL(URL u, String proto, String host, int port, String auth, String user, String path, + String query, String ref); +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java new file mode 100644 index 0000000000..226acb847c --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +/** + * Abstract implementation of the {@code PlurlStreamHandler} interface. All + * the methods simply invoke the corresponding methods on + * {@code java.net.URLStreamHandler} except for {@code parseURL} and + * {@code setURL}, which use the {@code PlurlSetter} parameter. + * Subclasses of this abstract class should not need to override the + * {@code setURL} and {@code parseURL(URLStreamHandlerSetter,...)} methods. + + */ +public abstract class PlurlStreamHandlerBase extends URLStreamHandler implements PlurlStreamHandler { + private volatile PlurlSetter plurlSetter; + + /** + * @see "java.net.URLStreamHandler.openConnection(URL)" + */ + @Override + public abstract URLConnection openConnection(URL u) throws IOException; + + /** + * Parse a URL using the {@code PlurlSetter} object. This method sets the + * {@code plurlSetter} field with the specified {@code PlurlSetter} object and + * then calls {@code parseURL(URL,String,int,int)}. + * + * @param setter The object on which the {@code setURL} method must be invoked + * for the specified URL. + * @see "java.net.URLStreamHandler.parseURL" + */ + @Override + public void parseURL(PlurlSetter setter, URL u, String spec, int start, int limit) { + this.plurlSetter = setter; + parseURL(u, spec, start, limit); + } + + /** + * This method calls {@code super.openConnection(URL, Proxy)} + * + * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)" + */ + @Override + public URLConnection openConnection(URL u, Proxy p) throws IOException { + return super.openConnection(u, p); + } + + /** + * This method calls {@code super.toExternalForm}. + * + * @see "java.net.URLStreamHandler.toExternalForm" + */ + @Override + public String toExternalForm(URL u) { + return super.toExternalForm(u); + } + + /** + * This method calls {@code super.equals(URL,URL)}. + * + * @see "java.net.URLStreamHandler.equals(URL,URL)" + */ + @Override + public boolean equals(URL u1, URL u2) { + return super.equals(u1, u2); + } + + /** + * This method calls {@code super.getDefaultPort}. + * + * @see "java.net.URLStreamHandler.getDefaultPort" + */ + @Override + public int getDefaultPort() { + return super.getDefaultPort(); + } + + /** + * This method calls {@code super.getHostAddress}. + * + * @see "java.net.URLStreamHandler.getHostAddress" + */ + @Override + public InetAddress getHostAddress(URL u) { + return super.getHostAddress(u); + } + + /** + * This method calls {@code super.hashCode(URL)}. + * + * @see "java.net.URLStreamHandler.hashCode(URL)" + */ + @Override + public int hashCode(URL u) { + return super.hashCode(u); + } + + /** + * This method calls {@code super.hostsEqual}. + * + * @see "java.net.URLStreamHandler.hostsEqual" + */ + @Override + public boolean hostsEqual(URL u1, URL u2) { + return super.hostsEqual(u1, u2); + } + + /** + * This method calls {@code super.sameFile}. + * + * @see "java.net.URLStreamHandler.sameFile" + */ + @Override + public boolean sameFile(URL u1, URL u2) { + return super.sameFile(u1, u2); + } + + /** + * This method calls + * {@code plurlSetter.setURL(URL,String,String,int,String,String,String,String)}. + * + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String)" + */ + @SuppressWarnings("deprecation") + @Override + public void setURL(URL u, String proto, String host, int port, String file, String ref) { + PlurlSetter current = plurlSetter; + if (current == null) { + // something is calling the handler directly, probably passed it to URL directly + super.setURL(u, proto, host, port, null, null, file, null, ref); + } else { + current.setURL(u, proto, host, port, null, null, file, null, ref); + } + } + + /** + * This method calls + * {@code realHandler.setURL(URL,String,String,int,String,String,String,String)} + * . + * + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)" + */ + @Override + public void setURL(URL u, String proto, String host, int port, String auth, String user, String path, + String query, String ref) { + PlurlSetter current = plurlSetter; + if (current == null) { + // something is calling the handler directly, probably passed it to URL directly + super.setURL(u, proto, host, port, auth, user, path, query, ref); + } else { + current.setURL(u, proto, host, port, auth, user, path, query, ref); + } + } + +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java new file mode 100644 index 0000000000..3bb311c135 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl; + +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +/** + * A {@link URLStreamHandlerFactory} that also implements {@link PlurlFactory} + */ +public interface PlurlStreamHandlerFactory extends URLStreamHandlerFactory, PlurlFactory { + + /** + * A factory is expected to return {@link URLStreamHandler} instances that also + * implement {@link PlurlStreamHandler}. If the returned handler does not + * implement {@link PlurlStreamHandler} then deep reflection is required and the + * JVM may require the "--add-opens" option in order to open the "java.net" + * package for reflection. For example: + * + *

+	 * --add-opens java.base/java.net=ALL-UNNAMED
+	 * 
+ * + * @see URLStreamHandlerFactory#createURLStreamHandler(String) + */ + @Override + URLStreamHandler createURLStreamHandler(String protocol); +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/README.md b/framework/src/main/java/org/apache/felix/framework/plurl/README.md new file mode 100644 index 0000000000..00de0d1e33 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/README.md @@ -0,0 +1,57 @@ +# Plurl (vendored) — PROTOTYPE, NOT FOR RELEASE + +## Provenance + +These sources are copied verbatim from the Eclipse OSGi Technology **plurl** project: + +- Upstream: https://github.com/eclipse-osgi-technology/plurl +- Originally: https://github.com/tjwatson/plurl-osgi +- Copied at commit `6581777`, upstream version `0.1.0-SNAPSHOT` + +The **only** modification is the package rename from `org.eclipse.osgitech.plurl` to +`org.apache.felix.framework.plurl`. Every file keeps its original license header and +its `Copyright (c) 2025 IBM Corporation` notice unchanged. + +This mirrors what Eclipse Equinox did in +https://github.com/eclipse-equinox/equinox/pull/848, which vendored the same 11 files +into `org.eclipse.equinox.plurl`. + +## ⚠️ Unresolved licensing issue + +**This code must not be merged or released in its current state.** + +Every source file here declares: + +``` +SPDX-License-Identifier: EPL-2.0 +Copyright (c) 2025 IBM Corporation +``` + +EPL-2.0 is [Category B](https://www.apache.org/legal/resolved.html#category-b) at the +ASF and **may not be included in an Apache source release**. Equinox was free to +vendor these files because Eclipse projects are EPL-2.0 natively; Apache Felix is not. + +There is reason to believe the headers are an oversight rather than the project's +intent: the plurl repository's own `LICENSE` file and its `pom.xml` both declare +**Apache-2.0**, and only the source headers say EPL-2.0. (Its `NOTICE` file is also a +copy-paste leftover referring to "slf4j-osgi".) + +Before this can go anywhere, one of the following has to happen: + +1. The upstream project relicenses/corrects the source headers to Apache-2.0, so the + files can legitimately live in an Apache source tree; or +2. Felix consumes plurl as a released binary dependency rather than vendored source, + subject to the Category B rules — which additionally requires plurl to be published + to Maven Central, as it currently has no release or tag; or +3. Felix writes its own Apache-2.0 implementation of the same idea. + +## Why we want this at all + +`URLHandlers` currently takes over the JVM-wide `java.net.URL` stream handler factory +by reflectively swapping a private static field, which is what forces +`SecureAction`'s use of `sun.misc.Unsafe` to obtain a trusted +`MethodHandles.Lookup`. That is the only remaining `Unsafe` usage in the framework and +it prevents Felix and Equinox from coexisting in one JVM without clobbering each +other's URL singletons. Plurl replaces that with a cooperative multiplexing factory, +which is the approach suggested in +https://github.com/apache/felix-dev/pull/433#issuecomment-3073468820. diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java new file mode 100644 index 0000000000..e2e18167ad --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl.impl; + +interface CallStack { + Class[] getClassContext(); +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java new file mode 100644 index 0000000000..fc271bd89d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java @@ -0,0 +1,1507 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl.impl; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.ContentHandler; +import java.net.ContentHandlerFactory; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.apache.felix.framework.plurl.Plurl; +import org.apache.felix.framework.plurl.PlurlFactory; +import org.apache.felix.framework.plurl.PlurlStreamHandler; +import org.apache.felix.framework.plurl.PlurlStreamHandler.PlurlSetter; +import org.apache.felix.framework.plurl.PlurlStreamHandlerBase; + +public final class PlurlImpl implements Plurl { + + private static final String PROTOCOL_HANDLER_PKGS = "java.protocol.handler.pkgs"; //$NON-NLS-1$ + private static final String CONTENT_HANDLER_PKGS = "java.content.handler.pkgs"; //$NON-NLS-1$ + private static final String DEFAULT_VM_CONTENT_HANDLERS = "sun.net.www.content"; //$NON-NLS-1$ + volatile Set forbiddenProtocols = new HashSet<>( + Arrays.asList("jar", "jmod", "file", "jrt")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + private static final String THIS_PACKAGE = PlurlImpl.class.getPackage().getName(); + static final String PLURL_STREAM_HANDLER_CLASS_NAME = PlurlStreamHandler.class.getName(); + static final Field URL_HANDLER_FIELD = findUrlHandlerField(); + + private static final Collection systemLoaders; + static { + Collection loaders = new ArrayList<>(); + try { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + // we allow the system cl, but not its parents + cl = cl != null ? cl.getParent() : null; + while (cl != null) { + loaders.add(cl); + cl = cl.getParent(); + } + } catch (Throwable t) { + // ignore as if no loaders + } + systemLoaders = Collections.unmodifiableCollection(loaders); + } + + private static boolean isSystemClass(String pName, final Class clazz) { + if (pName != null && pName.startsWith("jdk.")) { //$NON-NLS-1$ + return true; + } + // we want to ignore classes from the system + ClassLoader cl = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + return clazz.getClassLoader(); + } + }); + return cl == null || systemLoaders.contains(cl); + } + + private static Field findUrlHandlerField() { + Field f = null; + try { + f = URL.class.getDeclaredField("handler"); //$NON-NLS-1$ + } catch (Exception e) { + Field[] fields = URL.class.getDeclaredFields(); + for (Field field : fields) { + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (!isStatic && field.getType().equals(URLStreamHandler.class)) { + f = field; + break; + } + } + } + if (f == null) { + // fallback reflection is blocked by module system + return null; + } + try { + f.setAccessible(true); + return f; + } catch (Exception e) { + // blocked by module system + } + return null; + } + + static boolean setHandler(URL u, Object h) { + if (URL_HANDLER_FIELD == null || !(h instanceof URLStreamHandler)) { + return false; + } + try { + URL_HANDLER_FIELD.set(u, h); + } catch (Exception e) { + // should not happen + throw new IllegalStateException(e); + } + return true; + } + + static enum SetFactories { + notInstalled, // not installed yet + primordial, // there were no factories set until plurl + override, // single factories existed before plurl overrode them + plurlAlreadySet // some other PlurlImpl instance already installed plurl + } + + SetFactories setFactories = SetFactories.notInstalled; + + URLStreamHandlerFactory parentURLStreamHandlerFactory = null; + ContentHandlerFactory parentContentHandlerFactory = null; + + List streamHandlerFactories = Collections.emptyList(); + List contentHandlerFactories = Collections.emptyList(); + List plurlImpls = Collections.emptyList(); + + final ServiceLoader builtinURLStreamHandlerFactoryLoader; + final ServiceLoader builtinContentHandlerFactoryLoader; + final CallStack callStack; + + private final ThreadLocal> creatingProtocols = new ThreadLocal<>(); + final URLToHandler urlToHandler = new URLToHandler(); + + boolean isRecursive(String protocol) { + List protocols = creatingProtocols.get(); + if (protocols == null) { + protocols = new ArrayList<>(1); + creatingProtocols.set(protocols); + } + if (protocols.contains(protocol)) + return true; + protocols.add(protocol); + return false; + } + + void releaseRecursive(String protocol) { + List protocols = creatingProtocols.get(); + protocols.remove(protocol); + } + + public interface LegacyFactory { + public void register(Object factory); + + public void unregister(Object factory); + + public boolean isMultiplexing(); + } + + public class PlurlURLStreamHandlerFactory extends URLStreamHandler + implements URLStreamHandlerFactory, LegacyFactory { + private final URLStreamHandlerFactory parent; + + public PlurlURLStreamHandlerFactory(URLStreamHandlerFactory parent) { + this.parent = parent; + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if (protocol.equals(PLURL_PROTOCOL)) { + return this; + } + URLStreamHandler handler = createURLStreamHandlerImpl(protocol); + if (handler == null && parent != null) { + return parent.createURLStreamHandler(protocol); + } + return handler; + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return plurlOperation(u); + } + + @Override + public void register(Object factory) { + add((URLStreamHandlerFactory) factory); + } + + @Override + public void unregister(Object factory) { + remove((URLStreamHandlerFactory) factory); + } + + @Override + public boolean isMultiplexing() { + return PlurlImpl.this.isMultiplexing(getURLStreamHandlerFactories()); + } + } + + public class PlurlContentHandlerFactory implements ContentHandlerFactory, LegacyFactory { + private final ContentHandlerFactory parent; + + public PlurlContentHandlerFactory(ContentHandlerFactory parent) { + this.parent = parent; + } + @Override + public ContentHandler createContentHandler(String mimetype) { + ContentHandler fromParent = parent == null ? null : parent.createContentHandler(mimetype); + return createContentHandlerImpl(mimetype, fromParent); + } + + @Override + public void register(Object factory) { + add((ContentHandlerFactory) factory); + } + + @Override + public void unregister(Object factory) { + remove((ContentHandlerFactory) factory); + } + + @Override + public boolean isMultiplexing() { + return PlurlImpl.this.isMultiplexing(getContentHandlerFactories()); + } + } + + private boolean checkPlurlProtocol() { + try { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_REGISTER_IMPLEMENTATION); + // plurl is already available; try registering our impl + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) plurl.openConnection().getContent(); + addImpl.accept(this); + this.setFactories = SetFactories.plurlAlreadySet; + return true; + } catch (MalformedURLException e) { + // expected if there is no plurl installed yet. + return false; + } catch (IOException e) { + // could not add our implementation; move on + } + return true; + } + + public synchronized void install(String... forbidden) { + if (setFactories == SetFactories.override || setFactories == SetFactories.primordial) { + // already installed; no-op + return; + } + if (forbidden != null && forbidden.length > 0) { + Set forbiddenSet = new HashSet<>(Arrays.asList(forbidden)); + if (forbiddenSet.contains(PLURL_FORBID_NOTHING)) { + forbiddenProtocols = Collections.emptySet(); + } else { + forbiddenProtocols = forbiddenSet; + } + } + if (checkPlurlProtocol()) { + return; + } + + // sync on URLConnection to prevent more than one thread from setting plurl + synchronized (URLConnection.class) { + // try again with lock + if (checkPlurlProtocol()) { + return; + } + boolean contentHandlerFactorySet = false; + try { + URLConnection.setContentHandlerFactory(new PlurlContentHandlerFactory(null)); + contentHandlerFactorySet = true; + URL.setURLStreamHandlerFactory(new PlurlURLStreamHandlerFactory(null)); + setFactories = SetFactories.primordial; + } catch (Throwable t) { + try { + forceFactories(t, contentHandlerFactorySet); + setFactories = SetFactories.override; + } catch (Exception e) { + String message = "Cannot install the plurl factories. " //$NON-NLS-1$ + + "The java.base module must be configured to open the java.net package for reflection " //$NON-NLS-1$ + + "in order to allow plurl to replace the existing factories. " //$NON-NLS-1$ + + "For example, by using the JVM option: '--add-opens java.base/java.net=ALL-UNNAMED'. "; //$NON-NLS-1$ + throw new IllegalStateException(message, e); + } + } + } + } + + private boolean unregisterPlurlImpl() { + try { + URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_UNREGISTER_IMPLEMENTATION); + // plurl is already available; try adding ours impl + @SuppressWarnings("unchecked") + Consumer removeImpl = (Consumer) plurl.openConnection().getContent(); + removeImpl.accept(this); + return true; + } catch (MalformedURLException e) { + // expected if there is no plurl installed + return false; + } catch (IOException e) { + // could not remove our implementation; move on + } + return true; + } + + @Override + public synchronized void uninstall() { + if (setFactories == SetFactories.notInstalled) { + // not installed; do nothing + return; + } + if (setFactories == SetFactories.primordial) { + // we don't let primordial Plurls get uninstalled; + // this is a no-op; + return; + } + if (setFactories == SetFactories.plurlAlreadySet) { + // some other plurl is set; unregister ours + unregisterPlurlImpl(); + setFactories = SetFactories.notInstalled; + return; + } + + // set this plurl as not installed + setFactories = SetFactories.notInstalled; + + // find a designate + Object designate = null; + PlurlImplHolder nextHolder = null; + Iterator iHolders = plurlImpls.iterator(); + while (iHolders.hasNext()) { + PlurlImplHolder next = iHolders.next(); + iHolders.remove(); + // pin designate object to avoid GC + designate = next.getImpl(); + if (designate != null) { + nextHolder = next; + break; + } + } + + Exception forceError = null; + try { + forceURLStreamHandlerFactory(false, parentURLStreamHandlerFactory); + } catch (Exception e) { + // this is unexpected since we forced the plurl at install + forceError = e; + } + try { + forceContentHandlerFactory(false, parentContentHandlerFactory); + } catch (Exception e) { + // this is unexpected since we forced the plurl at install + if (forceError != null) { + e.addSuppressed(forceError); + } + forceError = e; + } + if (forceError != null) { + // again, this would be very unexpected since we forced plurl at install + throw new RuntimeException(forceError); + } + + if (nextHolder != null) { + // found a designate; now force it to be installed + nextHolder.install(); + } + // Hack to make sure the designate isn't GC'ed before we call install by using + // the reference after; otherwise the JVM could determine the variable is out of + // scope and therefore allow it to be GC'ed + if (designate != null) { + designate.hashCode(); + } + } + + private void forceFactories(Throwable t, boolean contentHandlerFactorySet) throws Exception { + try { + if (!contentHandlerFactorySet) { + forceContentHandlerFactory(true, null); + } + forceURLStreamHandlerFactory(true, null); + } catch (Exception e) { + e.addSuppressed(t); + throw e; + } + } + + private void forceURLStreamHandlerFactory(boolean installPlurl, URLStreamHandlerFactory forceBack) + throws Exception { + Field factoryField = getStaticField(URL.class, URLStreamHandlerFactory.class); + if (factoryField == null) { + throw new Exception("Could not find URLStreamHandlerFactory field"); //$NON-NLS-1$ + } + // look for a lock to synchronize on + Object lock = getURLStreamHandlerFactoryLock(); + synchronized (lock) { + URLStreamHandlerFactory toSet = installPlurl ? (URLStreamHandlerFactory) factoryField.get(null) : forceBack; + if (installPlurl) { + parentURLStreamHandlerFactory = toSet; + // current factory does not support plurl, ok we'll wrap it + toSet = new PlurlURLStreamHandlerFactory(toSet); + } + + factoryField.set(null, null); + // always attempt to clear the handlers cache + // This allows an optimization for the single framework use-case + resetURLStreamHandlers(); + URL.setURLStreamHandlerFactory(toSet); + } + } + + private Object getURLStreamHandlerFactoryLock() throws IllegalAccessException { + Object lock; + try { + Field streamHandlerLockField = URL.class.getDeclaredField("streamHandlerLock"); //$NON-NLS-1$ + streamHandlerLockField.setAccessible(true); + lock = streamHandlerLockField.get(null); + } catch (NoSuchFieldException noField) { + // could not find the lock, lets sync on the class object + lock = URL.class; + } + return lock; + } + + private void resetURLStreamHandlers() throws IllegalAccessException { + Field handlersField = getStaticField(URL.class, Hashtable.class); + if (handlersField != null) { + @SuppressWarnings("rawtypes") + Hashtable handlers = (Hashtable) handlersField.get(null); + if (handlers != null) { + handlers.clear(); + } + } + } + + private void forceContentHandlerFactory(boolean installPlurl, ContentHandlerFactory forceBack) throws Exception { + Field factoryField = getStaticField(URLConnection.class, java.net.ContentHandlerFactory.class); + if (factoryField == null) { + throw new Exception("Could not find ContentHandlerFactory field"); //$NON-NLS-1$ + } + + ContentHandlerFactory toSet = installPlurl ? (ContentHandlerFactory) factoryField.get(null) : forceBack; + if (installPlurl) { + parentContentHandlerFactory = toSet; + // current factory does not support plurl, ok we'll wrap it + toSet = new PlurlContentHandlerFactory(toSet); + } + // null out the field so that we can successfully call setContentHandlerFactory + factoryField.set(null, null); + // always attempt to clear the handlers cache + // This allows an optimization for the single framework use-case + resetContentHandlers(); + URLConnection.setContentHandlerFactory(toSet); + } + + private void resetContentHandlers() throws IllegalAccessException { + Field handlersField = getStaticField(URLConnection.class, Hashtable.class); + if (handlersField != null) { + @SuppressWarnings("rawtypes") + Hashtable handlers = (Hashtable) handlersField.get(null); + if (handlers != null) { + handlers.clear(); + } + } + } + + public Field getStaticField(Class clazz, Class type) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (isStatic && field.getType().equals(type)) { + field.setAccessible(true); + return field; + } + } + return null; + } + + public PlurlImpl() { + builtinContentHandlerFactoryLoader = ServiceLoader.load(ContentHandlerFactory.class); + builtinURLStreamHandlerFactoryLoader = ServiceLoader.load(URLStreamHandlerFactory.class); + callStack = createCallStack(); + } + + private CallStack createCallStack() { + try { + Class.forName("java.lang.StackWalker"); //$NON-NLS-1$ + return new StackWalkerCallStack(); + } catch (ClassNotFoundException e) { + return new SecurityManagerCallStack(); + } + } + + ContentHandler createContentHandlerImpl(String mimetype, ContentHandler fromParent) { + ContentHandler builtin = findBuiltInContentHandler(mimetype); + if (builtin != null) { + return builtin; + } + // Never return null for content handlers because then + // we will never get called again. + return new PlurlRootContentHandler(mimetype, fromParent); + } + + URLStreamHandler createURLStreamHandlerImpl(String protocol) { + if (forbiddenProtocols.contains(protocol)) { + // to dangerous for these to be overridden + return null; + } + // Check if we are recursing + if (isRecursive(protocol)) { + return null; + } + try { + try { + URLStreamHandler builtin = findBuiltinURLStreamHandler(protocol); + if (builtin != null) { + return builtin; + } + } catch (UnsupportedOperationException e) { + // check if it is a reflective error. If so then we know there is a built-in + // protocol and we know we can never replace it. Let the JVM handle it. + if (e.getCause() instanceof ReflectiveOperationException) { + return null; + } + } + URLStreamHandlerFactoryHolder factoryHolder = findFactory(getURLStreamHandlerFactories()); + if (factoryHolder != null) { + PlurlStreamHandler shouldHandle = factoryHolder.getHandler(protocol); + if (shouldHandle != null) { + return new PlurlRootURLStreamHandler(protocol); + } + } + // Return null if nothing found that should handle the protocol; + // We will get called again if the protocol is asked for again. + return null; + } finally { + releaseRecursive(protocol); + } + } + + + private ContentHandler findBuiltInContentHandler(String mimetype) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ContentHandler run() { + return findBuiltinContentHandlerImpl(mimetype); + } + }); + } + + private URLStreamHandler findBuiltinURLStreamHandler(String protocol) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public URLStreamHandler run() { + return findBuiltinURLStreamHandlerImpl(protocol); + } + }); + } + + ContentHandler findBuiltinContentHandlerImpl(String contentType) { + // first check service loader + for (ContentHandlerFactory f : builtinContentHandlerFactoryLoader) { + ContentHandler h = f.createContentHandler(contentType); + if (h != null) { + return h; + } + } + // now check property + String builtInHandlers = System.getProperty(CONTENT_HANDLER_PKGS); + builtInHandlers = builtInHandlers == null ? DEFAULT_VM_CONTENT_HANDLERS + : DEFAULT_VM_CONTENT_HANDLERS + '|' + builtInHandlers; + + // replace '/' with a '.' and all characters not allowed in a java class name + // with a '_'. + String convertedContentType = contentType.replace('.', '_'); + convertedContentType = convertedContentType.replace('/', '.'); + convertedContentType = convertedContentType.replace('-', '_'); + StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$ + while (tok.hasMoreElements()) { + StringBuilder name = new StringBuilder(); + name.append(tok.nextToken()); + name.append("."); //$NON-NLS-1$ + name.append(convertedContentType); + try { + Class clazz = null; + try { + clazz = Class.forName(name.toString()); + } catch (ClassNotFoundException e) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + if (cl != null) { + clazz = cl.loadClass(name.toString()); + } + } + if (clazz != null) { + return (ContentHandler) clazz.getConstructor().newInstance(); + } + } catch (Exception ex) { + // handle all exceptions here and move on + } + } + return null; + } + + URLStreamHandler findBuiltinURLStreamHandlerImpl(String protocol) { + // first check service loader + for (URLStreamHandlerFactory f : builtinURLStreamHandlerFactoryLoader) { + URLStreamHandler h = f.createURLStreamHandler(protocol); + if (h != null) { + return h; + } + } + // now check property + String builtInHandlers = System.getProperty(PROTOCOL_HANDLER_PKGS); + if (builtInHandlers == null) + return null; + + StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$ + while (tok.hasMoreElements()) { + URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, tok.nextToken()); + if (found != null) { + return found; + } + } + return null; + } + + URLStreamHandler findBuildinURLStreamHandlerImpl(String protocol, String inPackage) { + Class clazz = null; + StringBuilder name = new StringBuilder(); + name.append(inPackage); + name.append("."); //$NON-NLS-1$ + name.append(protocol); + name.append(".Handler"); //$NON-NLS-1$ + try { + try { + clazz = Class.forName(name.toString()); + } catch (ClassNotFoundException e) { + ClassLoader cl = ClassLoader.getSystemClassLoader(); + if (cl != null) { + try { + clazz = cl.loadClass(name.toString()); + } catch (ClassNotFoundException e2) { + // ignore + } + } + } + if (clazz != null) { + return (URLStreamHandler) clazz.getConstructor().newInstance(); + } + } catch (ReflectiveOperationException e) { + // probably because the package isn't open for reflection + String message = "The module for class '" + clazz.getName() + "' must be configured to open the '" //$NON-NLS-1$ //$NON-NLS-2$ + + inPackage + '.' + protocol + "' package for reflection to support the handler." //$NON-NLS-1$ + + " For example, by using the JVM option: '--add-opens java.base/" + inPackage + '.' + protocol //$NON-NLS-1$ + + "=ALL-UNNAMED'."; //$NON-NLS-1$ + throw new UnsupportedOperationException(message, e); + } catch (Exception ex) { + // handle all exceptions here and move on + } + return null; + } + + URLConnection plurlOperation(URL u) { + final String path = u.getPath(); + return new URLConnection(u) { + @Override + public void connect() throws IOException { + // do nothing + } + + @Override + public Consumer getContent() throws IOException { + switch (path) { + case PLURL_ADD_URL_STREAM_HANDLER_FACTORY: + return (f) -> add((URLStreamHandlerFactory) f); + case PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY: + return (f) -> remove((URLStreamHandlerFactory) f); + case PLURL_ADD_CONTENT_HANDLER_FACTORY: + return (f) -> add((ContentHandlerFactory) f); + case PLURL_REMOVE_CONTENT_HANDLER_FACTORY: + return (f) -> remove((ContentHandlerFactory) f); + case PLURL_REGISTER_IMPLEMENTATION: + return (p) -> addImpl(p); + case PLURL_UNREGISTER_IMPLEMENTATION: + return (p) -> removeImpl(p); + default: + throw new IOException("Unknown plurl operation: " + path); //$NON-NLS-1$ + } + } + }; + } + + boolean isMultiplexing(List factories) { + return factories.size() > 1; + } + + synchronized void addImpl(Object p) { + if (setFactories == SetFactories.primordial) { + // If this plurl is the primordial factory (no singletons set in the JVM) + // then we don't track other plurl installs because we will never + // remove this plurl from the JVM singleton + return; + } + if (p == this) { + // someone called install again on this Plurl; ignore it + return; + } + List updated = new ArrayList<>(plurlImpls); + // remove any GC'ed handlers or the new impl (incase it is being added again) + updated.removeIf((h) -> h.getImpl() == null || h.getImpl() == p); + // add new impl + updated.add(new PlurlImplHolder(p)); + plurlImpls = updated; + } + + synchronized void removeImpl(Object p) { + List updated = new ArrayList<>(plurlImpls); + // remove the impl and any that got GC'ed + updated.removeIf((h) -> h.getImpl() == p || h.getImpl() == null); + plurlImpls = updated.isEmpty() ? Collections.emptyList() : updated; + } + + synchronized void add(URLStreamHandlerFactory f) { + List updated = new ArrayList<>(streamHandlerFactories); + // remove any GC'ed handlers + updated.removeIf((h) -> h.getFactory() == null); + // add new holder + updated.add(new URLStreamHandlerFactoryHolder(f)); + streamHandlerFactories = updated; + } + + synchronized void remove(URLStreamHandlerFactory f) { + List updated = new ArrayList<>(streamHandlerFactories); + // remove the factory and any that got GC'ed + updated.removeIf((h) -> { + if (h.getFactory() == f || h.getFactory() == null) { + return true; + } + return false; + }); + streamHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated; + } + + synchronized List getURLStreamHandlerFactories() { + return streamHandlerFactories; + } + + synchronized List getContentHandlerFactories() { + return contentHandlerFactories; + } + + synchronized void add(ContentHandlerFactory f) { + List updated = new ArrayList<>(contentHandlerFactories); + // remove any GC'ed handlers + updated.removeIf((h) -> h.getFactory() == null); + // add new holder + updated.add(new ContentHandlerFactoryHolder(f)); + contentHandlerFactories = updated; + } + + synchronized void remove(ContentHandlerFactory f) { + List updated = new ArrayList<>(contentHandlerFactories); + // remove the factory and any that got GC'ed + updated.removeIf((h) -> { + if (h.getFactory() == f || h.getFactory() == null) { + return true; + } + return false; + }); + contentHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated; + } + + ContentHandler findContentHandler(String contentType) { + ContentHandlerFactoryHolder f = findFactory(getContentHandlerFactories()); + if (f != null) { + return f.getHandler(contentType); + } + return null; + } + + PlurlStreamHandler findPlurlStreamHandler(String protocol) { + URLStreamHandlerFactoryHolder f = findFactory(getURLStreamHandlerFactories()); + if (f != null) { + return f.getHandler(protocol); + } + return null; + } + + private F findFactory(List factories) { + int numFactories = factories.size(); + if (numFactories == 1) { + // Handle common case of only one; just use it + return factories.get(0); + } + Class[] callStackClasses = getCallStack(); + for (Class stack : callStackClasses) { + String pName = getPackageName(stack); + if (THIS_PACKAGE.equals(pName) || isSystemClass(pName, stack)) { + continue; + } + for (F f : factories) { + boolean shouldHandle = false; + if (f instanceof PlurlFactory) { + shouldHandle = ((PlurlFactory) f).shouldHandle(stack); + } else { + // use reflection in case this Plurl package isn't visible to the factory impl + try { + shouldHandle = (boolean) findShouldHandle(f.getClass()).invoke(f, stack); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (shouldHandle) { + return f; + } + } + } + // Instead of returning null here, the "first" factory is returned; + // This means the root or "first" factory may provide protocol handlers for call stacks + // that have no classes known to that factory + return numFactories > 0 ? factories.get(0) : null; + } + + Method findShouldHandle(Class clazz) throws NoSuchMethodException { + Method shouldHandle = null; + try { + shouldHandle = clazz.getMethod("shouldHandle", Class.class); //$NON-NLS-1$ + } catch (NoSuchMethodException e) { + // check for legacy hasAuthority method + try { + shouldHandle = clazz.getMethod("hasAuthority", Class.class); //$NON-NLS-1$ + } catch (NoSuchMethodException e1) { + throw e; + } + } + shouldHandle.setAccessible(true); + return shouldHandle; + } + + private String getPackageName(Class clazz) { + String name = clazz.getName(); + int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + return name.substring(0, lastDot); + } + return ""; //$NON-NLS-1$ + } + + private Class[] getCallStack() { + return callStack.getClassContext(); + } + + class PlurlRootContentHandler extends ContentHandler { + private final String contentType; + private final ContentHandler fromParent; + + PlurlRootContentHandler(String contentType, ContentHandler fromParent) { + this.contentType = contentType; + this.fromParent = fromParent; + } + + @Override + public Object getContent(URLConnection uConn) throws IOException { + ContentHandler handler = findContentHandler(contentType); + if (handler != null) { + return handler.getContent(uConn); + } + if (fromParent != null) { + return fromParent.getContent(uConn); + } + return uConn.getInputStream(); + } + } + + public abstract class PlurlFactoryHolder implements PlurlFactory { + private final WeakReference factory; + private final Map handlers = new ConcurrentHashMap<>(); + private final Method shouldHandleMethod; + + public PlurlFactoryHolder(F factory) { + this.factory = new WeakReference<>(factory); + if (factory instanceof PlurlFactory) { + shouldHandleMethod = null; + } else { + try { + shouldHandleMethod = findShouldHandle(factory.getClass()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + F getFactory() { + return factory.get(); + } + + @Override + public boolean shouldHandle(Class clazz) { + F f = factory.get(); + if (f == null) { + return false; + } + if (shouldHandleMethod != null) { + try { + return (boolean) shouldHandleMethod.invoke(f, clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return ((PlurlFactory) f).shouldHandle(clazz); + } + + H getHandler(String type) { + final F f = factory.get(); + if (f == null) { + // clear handlers + handlers.clear(); + // remove GC'ed holders + remove(null); + return null; + } + return handlers.computeIfAbsent(type, (t) -> createHandler(t, f)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + factory.get() + ']' + + handlers; + } + + protected abstract H createHandler(String type, F f); + + protected abstract void remove(F f); + } + + class PlurlImplHolder { + private final WeakReference plurlImpl; + + PlurlImplHolder(Object plurlImpl) { + this.plurlImpl = new WeakReference<>(plurlImpl); + } + + public void install() { + // should be able to reflect on the plurl instance because install is public + Object currentPlurl = plurlImpl.get(); + // should never be null since we pinned the object before calling + try { + Method install = currentPlurl.getClass().getMethod("install", String[].class); //$NON-NLS-1$ + install.invoke(currentPlurl, (Object) forbiddenProtocols.toArray(new String[0])); + } catch (NoSuchMethodException e) { + // should never happen + throw new RuntimeException(e); + } catch (Exception e) { + throw new RuntimeException(e); + } + synchronized (PlurlImpl.this) { + try { + URL registerPlurl = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_REGISTER_IMPLEMENTATION); + for (PlurlImplHolder plurlImplHolder : plurlImpls) { + Object p = plurlImplHolder.getImpl(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) registerPlurl.openConnection().getContent(); + addImpl.accept(p); + } + URL addContentHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_ADD_CONTENT_HANDLER_FACTORY); + for (ContentHandlerFactoryHolder contentHandlerFactoryHolder : contentHandlerFactories) { + ContentHandlerFactory c = contentHandlerFactoryHolder.getFactory(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) addContentHandlerFactory.openConnection() + .getContent(); + addImpl.accept(c); + } + URL addURLStreamHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP, + PLURL_ADD_URL_STREAM_HANDLER_FACTORY); + for (URLStreamHandlerFactoryHolder urlStreamHandlerFactoryHolder : streamHandlerFactories) { + URLStreamHandlerFactory s = urlStreamHandlerFactoryHolder.getFactory(); + @SuppressWarnings("unchecked") + Consumer addImpl = (Consumer) addURLStreamHandlerFactory.openConnection() + .getContent(); + addImpl.accept(s); + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + plurlImpls.clear(); + contentHandlerFactories.clear(); + streamHandlerFactories.clear(); + parentContentHandlerFactory = null; + parentURLStreamHandlerFactory = null; + } + } + + } + + public Object getImpl() { + return plurlImpl.get(); + } + } + + public class ContentHandlerFactoryHolder extends PlurlFactoryHolder { + public ContentHandlerFactoryHolder(ContentHandlerFactory factory) { + super(factory); + } + + @Override + protected ContentHandler createHandler(String mimetype, ContentHandlerFactory f) { + return f.createContentHandler(mimetype); + } + + @Override + protected void remove(ContentHandlerFactory f) { + PlurlImpl.this.remove(f); + } + } + + public class URLStreamHandlerFactoryHolder extends PlurlFactoryHolder { + public URLStreamHandlerFactoryHolder(URLStreamHandlerFactory factory) { + super(factory); + } + + @Override + protected PlurlStreamHandler createHandler(String protocol, URLStreamHandlerFactory f) { + URLStreamHandler handler = f.createURLStreamHandler(protocol); + if (handler == null) { + return null; + } + if (handler instanceof PlurlStreamHandler) { + return (PlurlStreamHandler) handler; + } + PlurlStreamHandler proxyPlurlStreamHandler = newProxyPlurlStreamHandler(handler); + if (proxyPlurlStreamHandler != null) { + return proxyPlurlStreamHandler; + } + return new PlurlStreamHandlerReflective(handler); + } + + @Override + protected void remove(URLStreamHandlerFactory f) { + PlurlImpl.this.remove(f); + } + } + + PlurlStreamHandler newProxyPlurlStreamHandler(URLStreamHandler handler) { + Class checkClass = handler.getClass(); + while (checkClass != null) { + for (Class handlerInterfaces : checkClass.getInterfaces()) { + // To determine if we can proxy the handler we look for an interface that defines a method + // public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit); + // But we cannot search for it the conventional way because the PlurlSetter may be a different + // copy from ours. + for (Method m : handlerInterfaces.getMethods()) { + if (m.getName().equals("parseURL")) { //$NON-NLS-1$ + Class[] params = m.getParameterTypes(); + if (params.length == 5) { + // check the first param to see if it is a PlurlSetter candidate + Class plurlSetterCandidate = params[0]; + if (plurlSetterCandidate.isInterface()) { + try { + // try finding the appropriate setURL method + plurlSetterCandidate.getMethod("setURL", URL.class, String.class, String.class, Integer.TYPE, String.class, String.class, + String.class, String.class, String.class); + } catch (Exception e) { + // move on to the next interface + continue; + } + } + } else { + // Wrong number of arguments for parseURL; move on to next interface + continue; + } + Class plurlStreamHandlerClass = handlerInterfaces; + Class plurlSetterClass = m.getParameterTypes()[0]; + return new PlurlStreamHandlerProxy(handler, plurlStreamHandlerClass, plurlSetterClass); + } + } + } + checkClass = checkClass.getSuperclass(); + } + return null; + } + + static class PlurlStreamHandlerProxy extends URLStreamHandler implements PlurlStreamHandler { + private final URLStreamHandler handler; + private final Class plurlSetterClass; + private final Method equals; + private final Method getDefaultPort; + private final Method getHostAddress; + private final Method hashCode; + private final Method hostsEqual; + private final Method openConnection; + private final Method openConnectionProxy; + private final Method parseURL; + private final Method sameFile; + private final Method toExternalForm; + final Method setURL; + final Method setURLDeprecated; + + public PlurlStreamHandlerProxy(URLStreamHandler handler, Class plurlUrlHandlerClass, + Class plurlSetterClass) { + this.handler = handler; + this.plurlSetterClass = plurlSetterClass; + openConnection = findMethod(plurlUrlHandlerClass, "openConnection", URL.class); //$NON-NLS-1$ + openConnectionProxy = findMethod(plurlUrlHandlerClass, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$ + parseURL = findMethod(plurlUrlHandlerClass, "parseURL", plurlSetterClass, URL.class, String.class, //$NON-NLS-1$ + Integer.TYPE, Integer.TYPE); + equals = findMethod(plurlUrlHandlerClass, "equals", URL.class, URL.class); //$NON-NLS-1$ + getDefaultPort = findMethod(plurlUrlHandlerClass, "getDefaultPort"); //$NON-NLS-1$ + getHostAddress = findMethod(plurlUrlHandlerClass, "getHostAddress", URL.class); //$NON-NLS-1$ + hashCode = findMethod(plurlUrlHandlerClass, "hashCode", URL.class); //$NON-NLS-1$ + hostsEqual = findMethod(plurlUrlHandlerClass, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$ + sameFile = findMethod(plurlUrlHandlerClass, "sameFile", URL.class, URL.class); //$NON-NLS-1$ + toExternalForm = findMethod(plurlUrlHandlerClass, "toExternalForm", URL.class); //$NON-NLS-1$ + setURL = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, int.class, //$NON-NLS-1$ + String.class, String.class, String.class, String.class, String.class); + setURLDeprecated = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, //$NON-NLS-1$ + int.class, String.class, String.class); + } + + private static Method findMethod(Class plurlUrlHandlerClass, String methodName, Class... args) { + Method result = null; + try { + result = plurlUrlHandlerClass.getDeclaredMethod(methodName, args); + } catch (Exception e) { + throw new RuntimeException(e); + } + return result; + } + + Object invoke(Method m, Object... args) { + try { + return m.invoke(handler, args); + } catch (InvocationTargetException e) { + throw (RuntimeException) e.getTargetException(); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public boolean equals(URL u1, URL u2) { + return (boolean) invoke(equals, u1, u2); + } + + @Override + public int hashCode(URL u) { + return (int) invoke(hashCode, u); + } + + @Override + public boolean hostsEqual(URL u1, URL u2) { + return (boolean) invoke(hostsEqual, u1, u2); + } + + @Override + public int getDefaultPort() { + return (int) invoke(getDefaultPort); + } + + @Override + public InetAddress getHostAddress(URL u) { + return (InetAddress) invoke(getHostAddress, u); + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + return (URLConnection) invoke(openConnection, u); + } + + @Override + public URLConnection openConnection(URL u, Proxy p) throws IOException { + return (URLConnection) invoke(openConnectionProxy, u, p); + } + + @Override + public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) { + setHandler(u, handler); + Object plurlSetterProxy = null; + if (plurlSetter != null) { + plurlSetterProxy = java.lang.reflect.Proxy.newProxyInstance(handler.getClass().getClassLoader(), + new Class[] { plurlSetterClass }, new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("setURL".equals(method.getName())) { //$NON-NLS-1$ + if (args.length == 9) { + plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2], (int) args[3], + (String) args[4], (String) args[5], (String) args[6], (String) args[7], + (String) args[8]); + } else { + plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2], + (int) args[3], null, null, (String) args[4], null, (String) args[5]); + } + } + return null; + } + }); + } + invoke(parseURL, plurlSetterProxy, u, spec, start, limit); + } + + @Override + public boolean sameFile(URL u1, URL u2) { + return (boolean) invoke(sameFile, u1, u2); + } + + @Override + public String toExternalForm(URL u) { + return (String) invoke(toExternalForm, u); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + invoke(setURL, u, protocol, host, port, authority, userInfo, path, query, ref); + } + + @SuppressWarnings("deprecation") + @Override + public void setURL(URL u, String protocol, String host, int port, String file, String ref) { + invoke(setURLDeprecated, u, protocol, host, port, file, ref); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']'; + } + } + + static class PlurlStreamHandlerReflective extends URLStreamHandler implements PlurlStreamHandler { + private final URLStreamHandler handler; + private final Method openConnectionMethod; + private final Method openConnectionProxyMethod; + private final Method parseURLMethod; + private final Method equalsMethod; + private final Method getDefaultPortMethod; + private final Method getHostAddressMethod; + private final Method hashCodeMethod; + private final Method hostsEqualMethod; + private final Method sameFileMethod; + private final Method toExternalFormMethod; + + public PlurlStreamHandlerReflective(URLStreamHandler handler) { + this.handler = handler; + openConnectionMethod = findMethod(handler, "openConnection", URL.class); //$NON-NLS-1$ + openConnectionProxyMethod = findMethod(handler, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$ + parseURLMethod = findMethod(handler, "parseURL", URL.class, String.class, Integer.TYPE, Integer.TYPE); //$NON-NLS-1$ + equalsMethod = findMethod(handler, "equals", URL.class, URL.class); //$NON-NLS-1$ + getDefaultPortMethod = findMethod(handler, "getDefaultPort"); //$NON-NLS-1$ + getHostAddressMethod = findMethod(handler, "getHostAddress", URL.class); //$NON-NLS-1$ + hashCodeMethod = findMethod(handler, "hashCode", URL.class); //$NON-NLS-1$ + hostsEqualMethod = findMethod(handler, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$ + sameFileMethod = findMethod(handler, "sameFile", URL.class, URL.class); //$NON-NLS-1$ + toExternalFormMethod = findMethod(handler, "toExternalForm", URL.class); //$NON-NLS-1$ + if (URL_HANDLER_FIELD == null) { + throw new RuntimeException(getReflectionErrorMessage(handler.getClass())); + } + } + + @SuppressWarnings("unchecked") + private Object invoke(Method m, Object... args) throws T { + try { + return m.invoke(handler, args); + } catch (InvocationTargetException e) { + throw (T) e.getTargetException(); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + private static String getReflectionErrorMessage(Class handlerClass) { + return "The java.base module must be configured to open the java.net package for reflection to support the handler of type " //$NON-NLS-1$ + + '\'' + handlerClass.getName() + "'." //$NON-NLS-1$ + + " For example, by using the JVM option: '--add-opens java.base/java.net=ALL-UNNAMED'. " //$NON-NLS-1$ + + "Another option is to make the class '" + handlerClass.getName() //$NON-NLS-1$ + + "' implement the org.eclipse.equinox.purl.PlurlStreamHandler interface."; //$NON-NLS-1$ + } + + private static Method findMethod(URLStreamHandler h, String methodName, Class... args) { + Method result = null; + Class handlerClass = h.getClass(); + try { + result = handlerClass.getDeclaredMethod(methodName, args); + result.setAccessible(true); + } catch (Exception e1) { + try { + result = URLStreamHandler.class.getDeclaredMethod(methodName, args); + result.setAccessible(true); + } catch (Exception e2) { + // fallback reflection is blocked by Java modules + String message = getReflectionErrorMessage(handlerClass); + throw new RuntimeException(message, e2); + } + } + return result; + } + + @Override + public boolean equals(URL u1, URL u2) { + return (Boolean) invoke(equalsMethod, u1, u2); + } + + @Override + public int hashCode(URL u) { + return (Integer) invoke(hashCodeMethod, u); + } + + @Override + public boolean hostsEqual(URL u1, URL u2) { + return (Boolean) invoke(hostsEqualMethod, u1, u2); + } + + @Override + public int getDefaultPort() { + return (Integer) invoke(getDefaultPortMethod); + } + + @Override + public InetAddress getHostAddress(URL u) { + return (InetAddress) invoke(getHostAddressMethod, u); + } + + @Override + public URLConnection openConnection(URL u) throws IOException { + return (URLConnection) invoke(openConnectionMethod, u); + } + + @Override + public URLConnection openConnection(URL u, Proxy p) throws IOException { + return (URLConnection) invoke(openConnectionProxyMethod, u, p); + } + + @Override + public boolean sameFile(URL u1, URL u2) { + return (Boolean) invoke(sameFileMethod, u1, u2); + } + + @Override + public String toExternalForm(URL u) { + return (String) invoke(toExternalFormMethod, u); + } + + @Override + public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) { + try { + setHandler(u, handler); + invoke(parseURLMethod, u, spec, start, limit); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("deprecation") + @Override + public void setURL(URL u, String protocol, String host, int port, String file, String ref) { + super.setURL(u, protocol, host, port, file, ref); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']'; + } + } + + static final PlurlStreamHandler NULL_HANDLER = new PlurlStreamHandlerBase() { + @Override + public URLConnection openConnection(URL u) throws IOException { + throw new UnsupportedOperationException(); + } + }; + + class PlurlRootURLStreamHandler extends URLStreamHandler implements PlurlSetter { + private final String protocol; + private final AtomicReference builtin = new AtomicReference<>(); + + private PlurlStreamHandler lookupPlurlStreamHandler(URL u) { + if (u != null && isMultiplexing(getURLStreamHandlerFactories())) { + // Record the handler found for the URL; + // This allows to consistently use the same handler for the + // life of the URL object when we are multiplexing. + return urlToHandler.get(u, this::findPlurlStreamHandlerImpl); + } + return findPlurlStreamHandlerImpl(); + } + + private PlurlStreamHandler findPlurlStreamHandlerImpl() { + PlurlStreamHandler h = findPlurlStreamHandler(protocol); + if (h == null) { + h = findBuiltin(); + if (h == null) { + throw new IllegalStateException("No handler found for protocol: " + protocol); //$NON-NLS-1$ + } + } + return h; + } + + private PlurlStreamHandler findBuiltin() { + PlurlStreamHandler result = builtin.updateAndGet((h) -> { + URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, "sun.net.www.protocol"); //$NON-NLS-1$ + if (found == null) { + return NULL_HANDLER; + } + // we can only really do this if java.net is open for reflection + return new PlurlStreamHandlerReflective(found); + }); + return (result == NULL_HANDLER) ? null : result; + } + + PlurlRootURLStreamHandler(String protocol) { + this.protocol = protocol; + } + + @Override + protected boolean equals(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).equals(u1, u2); + } + + @Override + protected int hashCode(URL u) { + return lookupPlurlStreamHandler(u).hashCode(u); + } + + @Override + protected boolean hostsEqual(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).hostsEqual(u1, u2); + } + + @Override + protected int getDefaultPort() { + return lookupPlurlStreamHandler(null).getDefaultPort(); + } + + @Override + protected InetAddress getHostAddress(URL u) { + return lookupPlurlStreamHandler(u).getHostAddress(u); + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + return lookupPlurlStreamHandler(u).openConnection(u); + } + + @Override + protected URLConnection openConnection(URL u, Proxy p) throws IOException { + return lookupPlurlStreamHandler(u).openConnection(u, p); + } + + @Override + protected void parseURL(URL u, String spec, int start, int limit) { + PlurlStreamHandler h = lookupPlurlStreamHandler(u); + if (setHandler(u, h)) { + h.parseURL(null, u, spec, start, limit); + } else { + h.parseURL(this, u, spec, start, limit); + } + } + + @Override + protected boolean sameFile(URL u1, URL u2) { + return lookupPlurlStreamHandler(u1).sameFile(u1, u2); + } + + @Override + protected String toExternalForm(URL u) { + return lookupPlurlStreamHandler(u).toExternalForm(u); + } + + @Override + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, + String path, String query, String ref) { + super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref); + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java new file mode 100644 index 0000000000..6558ac7951 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl.impl; + +class SecurityManagerCallStack implements CallStack { + // used to get access to the protected SecurityManager#getClassContext method + static class InternalSecurityManager extends SecurityManager { + @Override + public Class[] getClassContext() { + return super.getClassContext(); + } + } + + private static InternalSecurityManager internalSecurityManager = new InternalSecurityManager(); + + public Class[] getClassContext() { + return internalSecurityManager.getClassContext(); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java new file mode 100644 index 0000000000..374ae4b591 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.apache.felix.framework.plurl.impl; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +class StackWalkerCallStack implements CallStack { + + static final Class stackWalkerClass; + static final Object stackWalker; + static final Method forEach; + static final Method getDeclaringClass; + static final CallStack fallback; + static { + Class tmpStackWalkerClass = null; + Object tmpStackWalker = null; + Method tmpForEach = null; + Method tmpGetDeclaringClass = null; + CallStack tmpFallback = null; + try { + Class stackWalkerOptionClass = Class.forName("java.lang.StackWalker$Option"); //$NON-NLS-1$ + @SuppressWarnings({ "unchecked", "rawtypes" }) + Object RETAIN_CLASS_REFERENCE = Enum.valueOf((Class) stackWalkerOptionClass, "RETAIN_CLASS_REFERENCE"); //$NON-NLS-1$ + tmpStackWalkerClass = Class.forName("java.lang.StackWalker"); //$NON-NLS-1$ + tmpStackWalker = tmpStackWalkerClass.getMethod("getInstance", stackWalkerOptionClass).invoke(null, //$NON-NLS-1$ + RETAIN_CLASS_REFERENCE); + tmpForEach = tmpStackWalkerClass.getMethod("forEach", Consumer.class); //$NON-NLS-1$ + tmpGetDeclaringClass = Class.forName("java.lang.StackWalker$StackFrame").getMethod("getDeclaringClass"); //$NON-NLS-1$ //$NON-NLS-2$ + } catch (Throwable t) { + // null all out + tmpStackWalkerClass = null; + tmpStackWalker = null; + tmpForEach = null; + tmpGetDeclaringClass = null; + // fallback to security manager + try { + tmpFallback = new SecurityManagerCallStack(); + } catch (Throwable fallbackException) { + // this is bad + fallbackException.printStackTrace(); + } + } + stackWalkerClass = tmpStackWalkerClass; + stackWalker = tmpStackWalker; + forEach = tmpForEach; + getDeclaringClass = tmpGetDeclaringClass; + fallback = tmpFallback; + } + + + public Class[] getClassContext() { + if (fallback != null) { + return fallback.getClassContext(); + } + List> result = new ArrayList<>(); + if (stackWalker != null) { + try { + forEach.invoke(stackWalker, new Consumer() { + public void accept(Object s) { + try { + result.add((Class) getDeclaringClass.invoke(s)); + } catch (Throwable t) { + t.printStackTrace(); + } + } + }); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return result.toArray(new Class[0]); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java new file mode 100644 index 0000000000..6db4d8a818 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.apache.felix.framework.plurl.impl; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import org.apache.felix.framework.plurl.PlurlStreamHandler; + +public class URLToHandler { + class WeakURL extends WeakReference { + private final int hashcode; + + public WeakURL(URL u, ReferenceQueue q) { + super(u, q); + this.hashcode = System.identityHashCode(u); + } + @Override + public int hashCode() { + return hashcode; + } + @Override + public boolean equals(Object obj) { + if (obj instanceof WeakURL) { + return get() == ((WeakURL) obj).get(); + } + return false; + } + } + + final ReferenceQueue queue = new ReferenceQueue<>(); + + Map entries = Collections.synchronizedMap(new HashMap<>()); + + PlurlStreamHandler get(URL u, Supplier h) { + WeakURL lookup = new WeakURL(u, null); + PlurlStreamHandler existing = entries.get(lookup); + if (existing != null) { + return existing; + } + + PlurlStreamHandler result = h == null ? null : h.get(); + if (result != null) { + synchronized (entries) { + PlurlStreamHandler recheck = entries.get(lookup); + if (recheck != null) { + return recheck; + } + entries.put(new WeakURL(u, queue), result); + Object x; + while ((x = queue.poll()) != null) { + entries.remove(x); + } + } + } + return result; + } +}