+ * An implementation that predates the capability, or the
+ * {@link #PLURL_GET_CAPABILITY} operation itself, rejects the query; that is
+ * reported here as an empty result, because the absence of an answer is itself
+ * the answer. Callers should treat an empty result as "not supported" rather than
+ * as an error. The type of the value is documented on each capability constant.
+ *
+ * This is a convenience method for using the plurl protocol like this:
+ *
+ *
getCapability(String capability) {
+ try {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP,
+ Plurl.PLURL_GET_CAPABILITY + '/' + capability);
+ return Optional.ofNullable(plurl.openConnection().getContent());
+ } catch (IOException e) {
+ // No plurl installed, or one without this capability.
+ return Optional.empty();
+ }
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java
new file mode 100644
index 0000000000..3224974b8f
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlContentHandlerFactory.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.net.ContentHandlerFactory;
+
+/**
+ * A {@link ContentHandlerFactory} that also implements {@link PlurlFactory}
+ */
+public interface PlurlContentHandlerFactory extends ContentHandlerFactory, PlurlFactory {
+ // a marker interface for a ContentHandlerFactory that implements PlurlFactory
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java
new file mode 100644
index 0000000000..520bf92d97
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlFactory.java
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+/**
+ * A plural factory that can be added to a plurl implementation. A plurl
+ * implementation uses {@code PlurlFactory} objects to locate a factory to
+ * provider a handler.
+ *
+ * @see Plurl#add(PlurlContentHandlerFactory)
+ * @see Plurl#add(PlurlStreamHandlerFactory)
+ */
+public interface PlurlFactory {
+ /**
+ * A plurl implementation will call this method with the classes in the call
+ * stack which are using the java.net APIs to create URL objects for a specific
+ * type. For example, a protocol or content type.
+ *
+ * @param clazz a class in the call stack using the java.net APIs
+ * @return true if this factory should be used to handle the request
+ */
+ boolean shouldHandle(Class> clazz);
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java
new file mode 100644
index 0000000000..6bd2d3ab76
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandler.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * The {@code PlurlStreamHandler} interface has public versions of the protected
+ * {@link java.net.URLStreamHandler} methods.
+ *
+ * The important differences between this interface and the
+ * {@code URLStreamHandler} class are that the {@code setURL} method is absent
+ * and the {@code parseURL} method takes a {@link PlurlSetter} object as the
+ * first argument. Classes implementing this interface must call the
+ * {@code setURL} method on the {@code PlurlSetter} object received in the
+ * {@code parseURL} method instead of {@code URLStreamHandler.setURL} to avoid a
+ * {@code SecurityException}.
+ *
+ * @see PlurlStreamHandlerBase
+ *
+ */
+public interface PlurlStreamHandler {
+ /**
+ * Interface used by {@code PlurlStreamHandler} objects to call the
+ * {@code setURL} method on the plurl proxy {@code URLStreamHandler} object.
+ *
+ *
+ * Objects of this type are passed to the
+ * {@link PlurlStreamHandler#parseURL(PlurlSetter, URL, String, int, int)}
+ * method. Invoking the {@code setURL} method on the
+ * {@code URLStreamHandlerSetter} object will invoke the {@code setURL} method
+ * on the plurl proxy {@code URLStreamHandler} object that is actually
+ * registered with {@code java.net.URL} for the protocol.
+ *
+ */
+ public interface PlurlSetter {
+ /**
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)"
+ */
+ public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo,
+ String path, String query, String ref);
+ }
+
+ /**
+ * @see "java.net.URLStreamHandler.equals(URL, URL)"
+ */
+ public boolean equals(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.hashCode(URL)"
+ */
+ public int hashCode(URL u);
+
+ /**
+ * @see "java.net.URLStreamHandler.hostsEqual(URL, URL)"
+ */
+ public boolean hostsEqual(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.getDefaultPort"
+ */
+ public int getDefaultPort();
+
+ /**
+ * @see "java.net.URLStreamHandler.getHostAddress(URL)"
+ */
+ public InetAddress getHostAddress(URL u);
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL)"
+ */
+ public URLConnection openConnection(URL u) throws IOException;
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)"
+ */
+ public URLConnection openConnection(URL u, Proxy p) throws IOException;
+
+ /**
+ * @see "java.net.URLStreamHandler.sameFile(URL, URL)"
+ */
+ public boolean sameFile(URL u1, URL u2);
+
+ /**
+ * @see "java.net.URLStreamHandler.toExternalForm(URL)"
+ */
+ public String toExternalForm(URL u);
+
+ /**
+ * Parse a URL. This method is called by the {@code URLStreamHandler} proxy
+ * implemented by plurl, instead of {@code java.net.URLStreamHandler.parseURL},
+ * passing a {@code PlurlSetter} object.
+ *
+ * @param plurlSetter The object on which {@code setURL} must be invoked for
+ * this URL. If the setter is {@code null} then the
+ * {@link PlurlStreamHandler#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method can be called directly.
+ * @see "java.net.URLStreamHandler.parseURL"
+ */
+ public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit);
+
+ /**
+ * If the plurlSetter is not {@code null} from the
+ * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the
+ * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method. Otherwise call {@code super.setURL}.
+ *
+ * @see "java.net.URLStreamHandler.setURL"
+ */
+ public void setURL(URL u, String proto, String host, int port, String file, String ref);
+
+ /**
+ * If the plurlSetter is not {@code null} from the
+ * {@link #parseURL(PlurlSetter, URL, String, int, int)} then call the
+ * {@link PlurlSetter#setURL(URL, String, String, int, String, String, String, String, String)}
+ * method. Otherwise call {@code super.setURL}.
+ *
+ * @see "java.net.URLStreamHandler.setURL"
+ */
+ public void setURL(URL u, String proto, String host, int port, String auth, String user, String path,
+ String query, String ref);
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java
new file mode 100644
index 0000000000..e7f490abfe
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerBase.java
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * Abstract implementation of the {@code PlurlStreamHandler} interface. All
+ * the methods simply invoke the corresponding methods on
+ * {@code java.net.URLStreamHandler} except for {@code parseURL} and
+ * {@code setURL}, which use the {@code PlurlSetter} parameter.
+ * Subclasses of this abstract class should not need to override the
+ * {@code setURL} and {@code parseURL(URLStreamHandlerSetter,...)} methods.
+
+ */
+public abstract class PlurlStreamHandlerBase extends URLStreamHandler implements PlurlStreamHandler {
+ private volatile PlurlSetter plurlSetter;
+
+ /**
+ * @see "java.net.URLStreamHandler.openConnection(URL)"
+ */
+ @Override
+ public abstract URLConnection openConnection(URL u) throws IOException;
+
+ /**
+ * Parse a URL using the {@code PlurlSetter} object. This method sets the
+ * {@code plurlSetter} field with the specified {@code PlurlSetter} object and
+ * then calls {@code parseURL(URL,String,int,int)}.
+ *
+ * @param setter The object on which the {@code setURL} method must be invoked
+ * for the specified URL.
+ * @see "java.net.URLStreamHandler.parseURL"
+ */
+ @Override
+ public void parseURL(PlurlSetter setter, URL u, String spec, int start, int limit) {
+ this.plurlSetter = setter;
+ parseURL(u, spec, start, limit);
+ }
+
+ /**
+ * This method calls {@code super.openConnection(URL, Proxy)}
+ *
+ * @see "java.net.URLStreamHandler.openConnection(URL, Proxy)"
+ */
+ @Override
+ public URLConnection openConnection(URL u, Proxy p) throws IOException {
+ return super.openConnection(u, p);
+ }
+
+ /**
+ * This method calls {@code super.toExternalForm}.
+ *
+ * @see "java.net.URLStreamHandler.toExternalForm"
+ */
+ @Override
+ public String toExternalForm(URL u) {
+ return super.toExternalForm(u);
+ }
+
+ /**
+ * This method calls {@code super.equals(URL,URL)}.
+ *
+ * @see "java.net.URLStreamHandler.equals(URL,URL)"
+ */
+ @Override
+ public boolean equals(URL u1, URL u2) {
+ return super.equals(u1, u2);
+ }
+
+ /**
+ * This method calls {@code super.getDefaultPort}.
+ *
+ * @see "java.net.URLStreamHandler.getDefaultPort"
+ */
+ @Override
+ public int getDefaultPort() {
+ return super.getDefaultPort();
+ }
+
+ /**
+ * This method calls {@code super.getHostAddress}.
+ *
+ * @see "java.net.URLStreamHandler.getHostAddress"
+ */
+ @Override
+ public InetAddress getHostAddress(URL u) {
+ return super.getHostAddress(u);
+ }
+
+ /**
+ * This method calls {@code super.hashCode(URL)}.
+ *
+ * @see "java.net.URLStreamHandler.hashCode(URL)"
+ */
+ @Override
+ public int hashCode(URL u) {
+ return super.hashCode(u);
+ }
+
+ /**
+ * This method calls {@code super.hostsEqual}.
+ *
+ * @see "java.net.URLStreamHandler.hostsEqual"
+ */
+ @Override
+ public boolean hostsEqual(URL u1, URL u2) {
+ return super.hostsEqual(u1, u2);
+ }
+
+ /**
+ * This method calls {@code super.sameFile}.
+ *
+ * @see "java.net.URLStreamHandler.sameFile"
+ */
+ @Override
+ public boolean sameFile(URL u1, URL u2) {
+ return super.sameFile(u1, u2);
+ }
+
+ /**
+ * This method calls
+ * {@code plurlSetter.setURL(URL,String,String,int,String,String,String,String)}.
+ *
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String)"
+ */
+ @Override
+ @Deprecated
+ public void setURL(URL u, String proto, String host, int port, String file, String ref) {
+ PlurlSetter current = plurlSetter;
+ if (current == null) {
+ // something is calling the handler directly, probably passed it to URL directly
+ super.setURL(u, proto, host, port, null, null, file, null, ref);
+ } else {
+ current.setURL(u, proto, host, port, null, null, file, null, ref);
+ }
+ }
+
+ /**
+ * This method calls
+ * {@code realHandler.setURL(URL,String,String,int,String,String,String,String)}
+ * .
+ *
+ * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)"
+ */
+ @Override
+ public void setURL(URL u, String proto, String host, int port, String auth, String user, String path,
+ String query, String ref) {
+ PlurlSetter current = plurlSetter;
+ if (current == null) {
+ // something is calling the handler directly, probably passed it to URL directly
+ super.setURL(u, proto, host, port, auth, user, path, query, ref);
+ } else {
+ current.setURL(u, proto, host, port, auth, user, path, query, ref);
+ }
+ }
+
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java
new file mode 100644
index 0000000000..0b85976754
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/PlurlStreamHandlerFactory.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl;
+
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+
+/**
+ * A {@link URLStreamHandlerFactory} that also implements {@link PlurlFactory}
+ */
+public interface PlurlStreamHandlerFactory extends URLStreamHandlerFactory, PlurlFactory {
+
+ /**
+ * A factory is expected to return {@link URLStreamHandler} instances that also
+ * implement {@link PlurlStreamHandler}. If the returned handler does not
+ * implement {@link PlurlStreamHandler} then deep reflection is required and the
+ * JVM may require the "--add-opens" option in order to open the "java.net"
+ * package for reflection. For example:
+ *
+ *
+ * --add-opens java.base/java.net=ALL-UNNAMED
+ *
+ *
+ * @see URLStreamHandlerFactory#createURLStreamHandler(String)
+ */
+ @Override
+ URLStreamHandler createURLStreamHandler(String protocol);
+
+ /**
+ * Returns true if this factory should handle the URL being parsed from the given
+ * spec. This is consulted before the call stack is examined, and lets a factory
+ * claim a URL that only it can own.
+ *
+ * Several parties may share one protocol and be distinguishable only by the URL
+ * itself, for example multiple instances of the same framework where the owner is
+ * identified by an id in the URL host. Such a URL may also be used by a caller
+ * that no factory recognizes from the call stack, leaving nothing else to select
+ * on.
+ *
+ * The spec is used rather than a {@code URL}, because selection happens while the
+ * URL is still being parsed: its host and path are not populated yet, and the
+ * handler is pinned to the URL as soon as parsing begins. Implementations must not
+ * call back into URL handling, so this method is given only strings.
+ *
+ * The spec may be relative and carry neither protocol nor host, in which case a
+ * factory that selects on the URL has nothing to decide with and should return
+ * false; a relative URL resolved against a context URL keeps the context's handler
+ * and does not reach this method.
+ *
+ * @param protocol the protocol of the URL being parsed
+ * @param spec the spec the URL is being parsed from, which may be relative
+ * @return true if this factory should handle the URL
+ * @see Plurl#PLURL_CAPABILITY_SELECT_BY_SPEC
+ */
+ default boolean shouldHandleURL(String protocol, String spec) {
+ return false;
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/README.md b/framework/src/main/java/org/apache/felix/framework/plurl/README.md
new file mode 100644
index 0000000000..17b242ac03
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/README.md
@@ -0,0 +1,78 @@
+
+
+# Plurl (vendored)
+
+## Provenance
+
+These sources are copied from the Eclipse OSGi Technology **plurl** project:
+
+- Upstream: https://github.com/eclipse-osgi-technology/plurl
+- Originally: https://github.com/tjwatson/plurl-osgi
+
+The **only** modification is the package rename from `org.eclipse.osgitech.plurl` to
+`org.apache.felix.framework.plurl`. Every file keeps its original license header and
+copyright notice unchanged.
+
+This mirrors what Eclipse Equinox did in
+https://github.com/eclipse-equinox/equinox/pull/848, which vendored the same files
+into `org.eclipse.equinox.plurl`.
+
+## Licensing
+
+The sources are **Apache-2.0**:
+
+```
+Copyright (c) Contributors to the Eclipse Foundation
+Licensed under the Apache License, Version 2.0
+SPDX-License-Identifier: Apache-2.0
+```
+
+An earlier copy carried EPL-2.0 headers, which would have been a blocker: EPL-2.0 is
+[Category B](https://www.apache.org/legal/resolved.html#category-b) at the ASF and may
+not be included in an Apache source release. That was an oversight when the code moved
+to the osgi-technology project and has been corrected upstream in
+https://github.com/eclipse-osgi-technology/plurl/pull/45. The copy here also includes
+the upstream fix from https://github.com/eclipse-osgi-technology/plurl/pull/55.
+
+## Vendoring is intended to be temporary
+
+The longer term intention, per
+https://github.com/apache/felix-dev/pull/552#issuecomment-5466491363, is a release of
+plurl from the osgi-technology project consumed by both Equinox and Felix
+**unchanged, in its original package**, rather than copied into each framework. At the
+time of writing plurl has no release: its `distributionManagement` points at
+`oss.sonatype.org`, which was decommissioned when OSSRH migrated to the Central
+portal, so neither a release nor a snapshot is currently resolvable.
+
+Once a release exists, this directory should be deleted and replaced by a dependency
+on the published artifact, embedded into the framework bundle as a private package.
+
+## Why the framework uses this
+
+`URLHandlers` used to claim the JVM-wide `java.net.URL` stream handler factory by
+reflectively swapping a private static field, and inspected the call stack to work out
+which framework instance a call belonged to. Obtaining a `MethodHandles.Lookup`
+trusted enough for that swap is the only remaining reason the framework uses
+`sun.misc.Unsafe`, and whichever framework installed itself last won the singleton.
+
+Plurl installs one cooperative router through the supported
+`URL.setURLStreamHandlerFactory` API and routes by asking each registered factory
+whether a calling class belongs to it, so several frameworks can coexist in one JVM.
+See `PlurlURLHandlers` for the Felix side of that.
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java
new file mode 100644
index 0000000000..bc672cddcb
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/CallStack.java
@@ -0,0 +1,22 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+interface CallStack {
+ Class>[] getClassContext();
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java
new file mode 100644
index 0000000000..c1ce34fd24
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/PlurlImpl.java
@@ -0,0 +1,1633 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.ContentHandler;
+import java.net.ContentHandlerFactory;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.apache.felix.framework.plurl.Plurl;
+import org.apache.felix.framework.plurl.PlurlFactory;
+import org.apache.felix.framework.plurl.PlurlStreamHandler;
+import org.apache.felix.framework.plurl.PlurlStreamHandlerFactory;
+import org.apache.felix.framework.plurl.PlurlStreamHandler.PlurlSetter;
+import org.apache.felix.framework.plurl.PlurlStreamHandlerBase;
+
+public final class PlurlImpl implements Plurl {
+
+ private static final String PROTOCOL_HANDLER_PKGS = "java.protocol.handler.pkgs"; //$NON-NLS-1$
+ private static final String CONTENT_HANDLER_PKGS = "java.content.handler.pkgs"; //$NON-NLS-1$
+ private static final String DEFAULT_VM_CONTENT_HANDLERS = "sun.net.www.content"; //$NON-NLS-1$
+ volatile Set forbiddenProtocols = new HashSet<>(
+ Arrays.asList("jar", "jmod", "file", "jrt")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+ private static final String THIS_PACKAGE = PlurlImpl.class.getPackage().getName();
+ static final String PLURL_STREAM_HANDLER_CLASS_NAME = PlurlStreamHandler.class.getName();
+ static final Field URL_HANDLER_FIELD = findUrlHandlerField();
+
+ private static final Collection systemLoaders;
+ static {
+ Collection loaders = new ArrayList<>();
+ try {
+ ClassLoader cl = ClassLoader.getSystemClassLoader();
+ // we allow the system cl, but not its parents
+ cl = cl != null ? cl.getParent() : null;
+ while (cl != null) {
+ loaders.add(cl);
+ cl = cl.getParent();
+ }
+ } catch (Throwable t) {
+ // ignore as if no loaders
+ }
+ systemLoaders = Collections.unmodifiableCollection(loaders);
+ }
+
+ private static boolean isSystemClass(String pName, final Class> clazz) {
+ if (pName != null && pName.startsWith("jdk.")) { //$NON-NLS-1$
+ return true;
+ }
+ // we want to ignore classes from the system
+ ClassLoader cl = AccessController.doPrivileged(new PrivilegedAction() {
+ @Override
+ public ClassLoader run() {
+ return clazz.getClassLoader();
+ }
+ });
+ return cl == null || systemLoaders.contains(cl);
+ }
+
+ private static Field findUrlHandlerField() {
+ Field f = null;
+ try {
+ f = URL.class.getDeclaredField("handler"); //$NON-NLS-1$
+ } catch (Exception e) {
+ Field[] fields = URL.class.getDeclaredFields();
+ for (Field field : fields) {
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ if (!isStatic && field.getType().equals(URLStreamHandler.class)) {
+ f = field;
+ break;
+ }
+ }
+ }
+ if (f == null) {
+ // fallback reflection is blocked by module system
+ return null;
+ }
+ try {
+ f.setAccessible(true);
+ return f;
+ } catch (Exception e) {
+ // blocked by module system
+ }
+ return null;
+ }
+
+ static boolean setHandler(URL u, Object h) {
+ if (URL_HANDLER_FIELD == null || !(h instanceof URLStreamHandler)) {
+ return false;
+ }
+ try {
+ URL_HANDLER_FIELD.set(u, h);
+ } catch (Exception e) {
+ // should not happen
+ throw new IllegalStateException(e);
+ }
+ return true;
+ }
+
+ static enum SetFactories {
+ notInstalled, // not installed yet
+ primordial, // there were no factories set until plurl
+ override, // single factories existed before plurl overrode them
+ plurlAlreadySet // some other PlurlImpl instance already installed plurl
+ }
+
+ SetFactories setFactories = SetFactories.notInstalled;
+
+ URLStreamHandlerFactory parentURLStreamHandlerFactory = null;
+ ContentHandlerFactory parentContentHandlerFactory = null;
+
+ List streamHandlerFactories = Collections.emptyList();
+ List contentHandlerFactories = Collections.emptyList();
+ List plurlImpls = Collections.emptyList();
+
+ final List builtinContentHandlerFactories;
+ final CallStack callStack;
+
+ private final ThreadLocal> creatingProtocols = new ThreadLocal<>();
+ final URLToHandler urlToHandler = new URLToHandler();
+
+ boolean isRecursive(String protocol) {
+ List protocols = creatingProtocols.get();
+ if (protocols == null) {
+ protocols = new ArrayList<>(1);
+ creatingProtocols.set(protocols);
+ }
+ if (protocols.contains(protocol))
+ return true;
+ protocols.add(protocol);
+ return false;
+ }
+
+ void releaseRecursive(String protocol) {
+ List protocols = creatingProtocols.get();
+ protocols.remove(protocol);
+ }
+
+ public interface LegacyFactory {
+ public void register(Object factory);
+
+ public void unregister(Object factory);
+
+ public boolean isMultiplexing();
+ }
+
+ public class PlurlURLStreamHandlerFactory extends URLStreamHandler
+ implements URLStreamHandlerFactory, LegacyFactory {
+ private final URLStreamHandlerFactory parent;
+
+ public PlurlURLStreamHandlerFactory(URLStreamHandlerFactory parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol) {
+ if (protocol.equals(PLURL_PROTOCOL)) {
+ return this;
+ }
+ URLStreamHandler handler = createURLStreamHandlerImpl(protocol);
+ if (handler == null && parent != null) {
+ return parent.createURLStreamHandler(protocol);
+ }
+ return handler;
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return plurlOperation(u);
+ }
+
+ @Override
+ public void register(Object factory) {
+ add((URLStreamHandlerFactory) factory);
+ }
+
+ @Override
+ public void unregister(Object factory) {
+ remove((URLStreamHandlerFactory) factory);
+ }
+
+ @Override
+ public boolean isMultiplexing() {
+ return PlurlImpl.this.isMultiplexing(getURLStreamHandlerFactories());
+ }
+ }
+
+ public class PlurlContentHandlerFactory implements ContentHandlerFactory, LegacyFactory {
+ private final ContentHandlerFactory parent;
+
+ public PlurlContentHandlerFactory(ContentHandlerFactory parent) {
+ this.parent = parent;
+ }
+ @Override
+ public ContentHandler createContentHandler(String mimetype) {
+ ContentHandler fromParent = parent == null ? null : parent.createContentHandler(mimetype);
+ return createContentHandlerImpl(mimetype, fromParent);
+ }
+
+ @Override
+ public void register(Object factory) {
+ add((ContentHandlerFactory) factory);
+ }
+
+ @Override
+ public void unregister(Object factory) {
+ remove((ContentHandlerFactory) factory);
+ }
+
+ @Override
+ public boolean isMultiplexing() {
+ return PlurlImpl.this.isMultiplexing(getContentHandlerFactories());
+ }
+ }
+
+ private boolean checkPlurlProtocol() {
+ try {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_REGISTER_IMPLEMENTATION);
+ // plurl is already available; try registering our impl
+ @SuppressWarnings("unchecked")
+ Consumer addImpl = (Consumer) plurl.openConnection().getContent();
+ addImpl.accept(this);
+ this.setFactories = SetFactories.plurlAlreadySet;
+ return true;
+ } catch (MalformedURLException e) {
+ // expected if there is no plurl installed yet.
+ return false;
+ } catch (IOException e) {
+ // could not add our implementation; move on
+ }
+ return true;
+ }
+
+ public synchronized void install(String... forbidden) {
+ if (setFactories == SetFactories.override || setFactories == SetFactories.primordial) {
+ // already installed; no-op
+ return;
+ }
+ if (forbidden != null && forbidden.length > 0) {
+ Set forbiddenSet = new HashSet<>(Arrays.asList(forbidden));
+ if (forbiddenSet.contains(PLURL_FORBID_NOTHING)) {
+ forbiddenProtocols = Collections.emptySet();
+ } else {
+ forbiddenProtocols = forbiddenSet;
+ }
+ }
+ if (checkPlurlProtocol()) {
+ return;
+ }
+
+ // sync on URLConnection to prevent more than one thread from setting plurl
+ synchronized (URLConnection.class) {
+ // try again with lock
+ if (checkPlurlProtocol()) {
+ return;
+ }
+ boolean contentHandlerFactorySet = false;
+ try {
+ URLConnection.setContentHandlerFactory(new PlurlContentHandlerFactory(null));
+ contentHandlerFactorySet = true;
+ URL.setURLStreamHandlerFactory(new PlurlURLStreamHandlerFactory(null));
+ setFactories = SetFactories.primordial;
+ } catch (Throwable t) {
+ try {
+ forceFactories(t, contentHandlerFactorySet);
+ setFactories = SetFactories.override;
+ } catch (Exception e) {
+ String message = "Cannot install the plurl factories. " //$NON-NLS-1$
+ + "The java.base module must be configured to open the java.net package for reflection " //$NON-NLS-1$
+ + "in order to allow plurl to replace the existing factories. " //$NON-NLS-1$
+ + "For example, by using the JVM option: '--add-opens java.base/java.net=ALL-UNNAMED'. "; //$NON-NLS-1$
+ throw new IllegalStateException(message, e);
+ }
+ }
+ }
+ }
+
+ private boolean unregisterPlurlImpl() {
+ try {
+ URL plurl = new URL(Plurl.PLURL_PROTOCOL, Plurl.PLURL_OP, PLURL_UNREGISTER_IMPLEMENTATION);
+ // plurl is already available; try adding ours impl
+ @SuppressWarnings("unchecked")
+ Consumer removeImpl = (Consumer) plurl.openConnection().getContent();
+ removeImpl.accept(this);
+ return true;
+ } catch (MalformedURLException e) {
+ // expected if there is no plurl installed
+ return false;
+ } catch (IOException e) {
+ // could not remove our implementation; move on
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized void uninstall() {
+ if (setFactories == SetFactories.notInstalled) {
+ // not installed; do nothing
+ return;
+ }
+ if (setFactories == SetFactories.primordial) {
+ // we don't let primordial Plurls get uninstalled;
+ // this is a no-op;
+ return;
+ }
+ if (setFactories == SetFactories.plurlAlreadySet) {
+ // some other plurl is set; unregister ours
+ unregisterPlurlImpl();
+ setFactories = SetFactories.notInstalled;
+ return;
+ }
+
+ // set this plurl as not installed
+ setFactories = SetFactories.notInstalled;
+
+ // find a designate
+ Object designate = null;
+ PlurlImplHolder nextHolder = null;
+ Iterator iHolders = plurlImpls.iterator();
+ while (iHolders.hasNext()) {
+ PlurlImplHolder next = iHolders.next();
+ iHolders.remove();
+ // pin designate object to avoid GC
+ designate = next.getImpl();
+ if (designate != null) {
+ nextHolder = next;
+ break;
+ }
+ }
+
+ Exception forceError = null;
+ try {
+ forceURLStreamHandlerFactory(false, parentURLStreamHandlerFactory);
+ } catch (Exception e) {
+ // this is unexpected since we forced the plurl at install
+ forceError = e;
+ }
+ try {
+ forceContentHandlerFactory(false, parentContentHandlerFactory);
+ } catch (Exception e) {
+ // this is unexpected since we forced the plurl at install
+ if (forceError != null) {
+ e.addSuppressed(forceError);
+ }
+ forceError = e;
+ }
+ if (forceError != null) {
+ // again, this would be very unexpected since we forced plurl at install
+ throw new RuntimeException(forceError);
+ }
+
+ if (nextHolder != null) {
+ // found a designate; now force it to be installed
+ nextHolder.install();
+ }
+ // Hack to make sure the designate isn't GC'ed before we call install by using
+ // the reference after; otherwise the JVM could determine the variable is out of
+ // scope and therefore allow it to be GC'ed
+ if (designate != null) {
+ designate.hashCode();
+ }
+ }
+
+ private void forceFactories(Throwable t, boolean contentHandlerFactorySet) throws Exception {
+ try {
+ if (!contentHandlerFactorySet) {
+ forceContentHandlerFactory(true, null);
+ }
+ forceURLStreamHandlerFactory(true, null);
+ } catch (Exception e) {
+ e.addSuppressed(t);
+ throw e;
+ }
+ }
+
+ private void forceURLStreamHandlerFactory(boolean installPlurl, URLStreamHandlerFactory forceBack)
+ throws Exception {
+ Field factoryField = getStaticField(URL.class, URLStreamHandlerFactory.class);
+ if (factoryField == null) {
+ throw new Exception("Could not find URLStreamHandlerFactory field"); //$NON-NLS-1$
+ }
+ // look for a lock to synchronize on
+ Object lock = getURLStreamHandlerFactoryLock();
+ synchronized (lock) {
+ URLStreamHandlerFactory toSet = installPlurl ? (URLStreamHandlerFactory) factoryField.get(null) : forceBack;
+ if (installPlurl) {
+ parentURLStreamHandlerFactory = toSet;
+ // current factory does not support plurl, ok we'll wrap it
+ toSet = new PlurlURLStreamHandlerFactory(toSet);
+ }
+
+ factoryField.set(null, null);
+ // always attempt to clear the handlers cache
+ // This allows an optimization for the single framework use-case
+ resetURLStreamHandlers();
+ URL.setURLStreamHandlerFactory(toSet);
+ }
+ }
+
+ private Object getURLStreamHandlerFactoryLock() throws IllegalAccessException {
+ Object lock;
+ try {
+ Field streamHandlerLockField = URL.class.getDeclaredField("streamHandlerLock"); //$NON-NLS-1$
+ streamHandlerLockField.setAccessible(true);
+ lock = streamHandlerLockField.get(null);
+ } catch (NoSuchFieldException noField) {
+ // could not find the lock, lets sync on the class object
+ lock = URL.class;
+ }
+ return lock;
+ }
+
+ private void resetURLStreamHandlers() throws IllegalAccessException {
+ Field handlersField = getStaticField(URL.class, Hashtable.class);
+ if (handlersField != null) {
+ @SuppressWarnings("rawtypes")
+ Hashtable, ?> handlers = (Hashtable) handlersField.get(null);
+ if (handlers != null) {
+ handlers.clear();
+ }
+ }
+ }
+
+ private void forceContentHandlerFactory(boolean installPlurl, ContentHandlerFactory forceBack) throws Exception {
+ Field factoryField = getStaticField(URLConnection.class, java.net.ContentHandlerFactory.class);
+ if (factoryField == null) {
+ throw new Exception("Could not find ContentHandlerFactory field"); //$NON-NLS-1$
+ }
+
+ ContentHandlerFactory toSet = installPlurl ? (ContentHandlerFactory) factoryField.get(null) : forceBack;
+ if (installPlurl) {
+ parentContentHandlerFactory = toSet;
+ // current factory does not support plurl, ok we'll wrap it
+ toSet = new PlurlContentHandlerFactory(toSet);
+ }
+ // null out the field so that we can successfully call setContentHandlerFactory
+ factoryField.set(null, null);
+ // always attempt to clear the handlers cache
+ // This allows an optimization for the single framework use-case
+ resetContentHandlers();
+ URLConnection.setContentHandlerFactory(toSet);
+ }
+
+ private void resetContentHandlers() throws IllegalAccessException {
+ Field handlersField = getStaticField(URLConnection.class, Hashtable.class);
+ if (handlersField != null) {
+ @SuppressWarnings("rawtypes")
+ Hashtable, ?> handlers = (Hashtable) handlersField.get(null);
+ if (handlers != null) {
+ handlers.clear();
+ }
+ }
+ }
+
+ public Field getStaticField(Class> clazz, Class> type) {
+ Field[] fields = clazz.getDeclaredFields();
+ for (Field field : fields) {
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ if (isStatic && field.getType().equals(type)) {
+ field.setAccessible(true);
+ return field;
+ }
+ }
+ return null;
+ }
+
+ public PlurlImpl() {
+ // IMPLEMENTATION NOTE:
+ // We must do the ServiceLoader lookup for the built-in ContentHandlerFactory
+ // because the Plurl ContentHandlerFactory must never return null;
+ // otherwise the Plurl factory will never be called again for the requested
+ // content type. So a check for built-in handlers must be done before returning
+ // the Plurl handler.
+ // This is not necessary for URLStreamHandlerFactory or the new
+ // URLStreamHandlerProvider that may be available from the JVM because returning
+ // null from that factory still allows us to be called again if the protocol is
+ // requested again later.
+ List serviceLoaderCHFs = new ArrayList<>();
+ ServiceLoader.load(ContentHandlerFactory.class).forEach(serviceLoaderCHFs::add);
+ builtinContentHandlerFactories = Collections.unmodifiableList(serviceLoaderCHFs);
+
+ callStack = createCallStack();
+ }
+
+ private CallStack createCallStack() {
+ try {
+ Class.forName("java.lang.StackWalker"); //$NON-NLS-1$
+ return new StackWalkerCallStack();
+ } catch (ClassNotFoundException e) {
+ return new SecurityManagerCallStack();
+ }
+ }
+
+ ContentHandler createContentHandlerImpl(String mimetype, ContentHandler fromParent) {
+ ContentHandler builtin = findBuiltInContentHandler(mimetype);
+ if (builtin != null) {
+ return builtin;
+ }
+ // Never return null for content handlers because then
+ // we will never get called again.
+ return new PlurlRootContentHandler(mimetype, fromParent);
+ }
+
+ URLStreamHandler createURLStreamHandlerImpl(String protocol) {
+ if (forbiddenProtocols.contains(protocol)) {
+ // to dangerous for these to be overridden
+ return null;
+ }
+ // Check if we are recursing
+ if (isRecursive(protocol)) {
+ return null;
+ }
+ try {
+ try {
+ URLStreamHandler builtin = findBuiltinURLStreamHandler(protocol);
+ if (builtin != null) {
+ return builtin;
+ }
+ } catch (UnsupportedOperationException e) {
+ // check if it is a reflective error. If so then we know there is a built-in
+ // protocol and we know we can never replace it. Let the JVM handle it.
+ if (e.getCause() instanceof ReflectiveOperationException) {
+ return null;
+ }
+ }
+ List factories = getURLStreamHandlerFactories();
+ URLStreamHandlerFactoryHolder factoryHolder = findFactory(factories);
+ if (factoryHolder != null) {
+ PlurlStreamHandler shouldHandle = factoryHolder.getHandler(protocol);
+ if (shouldHandle != null) {
+ return new PlurlRootURLStreamHandler(protocol);
+ }
+ }
+ // The JVM asks per protocol and gives us no URL, so the factory the call
+ // stack points at is not necessarily the one that will serve the URL: a
+ // factory may claim this protocol by URL instead, which is only decided
+ // later, per URL, in parseURL. Claim the protocol if any factory that could
+ // take it over serves it, otherwise the URL never gets that far.
+ for (URLStreamHandlerFactoryHolder holder : factories) {
+ if (holder != factoryHolder && holder.takesOverURLs(protocol)
+ && holder.getHandler(protocol) != null) {
+ return new PlurlRootURLStreamHandler(protocol);
+ }
+ }
+ // Return null if nothing found that should handle the protocol;
+ // We will get called again if the protocol is asked for again.
+ return null;
+ } finally {
+ releaseRecursive(protocol);
+ }
+ }
+
+
+ private ContentHandler findBuiltInContentHandler(String mimetype) {
+ return AccessController.doPrivileged(new PrivilegedAction() {
+ @Override
+ public ContentHandler run() {
+ return findBuiltinContentHandlerImpl(mimetype);
+ }
+ });
+ }
+
+ private URLStreamHandler findBuiltinURLStreamHandler(String protocol) {
+ return AccessController.doPrivileged(new PrivilegedAction() {
+ @Override
+ public URLStreamHandler run() {
+ return findBuiltinURLStreamHandlerImpl(protocol);
+ }
+ });
+ }
+
+ ContentHandler findBuiltinContentHandlerImpl(String contentType) {
+ // first check service loader
+ for (ContentHandlerFactory f : builtinContentHandlerFactories) {
+ ContentHandler h = f.createContentHandler(contentType);
+ if (h != null) {
+ return h;
+ }
+ }
+ // now check property
+ String builtInHandlers = System.getProperty(CONTENT_HANDLER_PKGS);
+ builtInHandlers = builtInHandlers == null ? DEFAULT_VM_CONTENT_HANDLERS
+ : DEFAULT_VM_CONTENT_HANDLERS + '|' + builtInHandlers;
+
+ // replace '/' with a '.' and all characters not allowed in a java class name
+ // with a '_'.
+ String convertedContentType = contentType.replace('.', '_');
+ convertedContentType = convertedContentType.replace('/', '.');
+ convertedContentType = convertedContentType.replace('-', '_');
+ StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$
+ while (tok.hasMoreElements()) {
+ StringBuilder name = new StringBuilder();
+ name.append(tok.nextToken());
+ name.append("."); //$NON-NLS-1$
+ name.append(convertedContentType);
+ try {
+ Class> clazz = null;
+ try {
+ clazz = Class.forName(name.toString());
+ } catch (ClassNotFoundException e) {
+ ClassLoader cl = ClassLoader.getSystemClassLoader();
+ if (cl != null) {
+ clazz = cl.loadClass(name.toString());
+ }
+ }
+ if (clazz != null) {
+ return (ContentHandler) clazz.getConstructor().newInstance();
+ }
+ } catch (Exception ex) {
+ // handle all exceptions here and move on
+ }
+ }
+ return null;
+ }
+
+ URLStreamHandler findBuiltinURLStreamHandlerImpl(String protocol) {
+ // check handlers pkgs property
+ String builtInHandlers = System.getProperty(PROTOCOL_HANDLER_PKGS);
+ if (builtInHandlers == null)
+ return null;
+
+ StringTokenizer tok = new StringTokenizer(builtInHandlers, "|"); //$NON-NLS-1$
+ while (tok.hasMoreElements()) {
+ URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, tok.nextToken());
+ if (found != null) {
+ return found;
+ }
+ }
+ return null;
+ }
+
+ URLStreamHandler findBuildinURLStreamHandlerImpl(String protocol, String inPackage) {
+ Class> clazz = null;
+ StringBuilder name = new StringBuilder();
+ name.append(inPackage);
+ name.append("."); //$NON-NLS-1$
+ name.append(protocol);
+ name.append(".Handler"); //$NON-NLS-1$
+ try {
+ try {
+ clazz = Class.forName(name.toString());
+ } catch (ClassNotFoundException e) {
+ ClassLoader cl = ClassLoader.getSystemClassLoader();
+ if (cl != null) {
+ try {
+ clazz = cl.loadClass(name.toString());
+ } catch (ClassNotFoundException e2) {
+ // ignore
+ }
+ }
+ }
+ if (clazz != null) {
+ return (URLStreamHandler) clazz.getConstructor().newInstance();
+ }
+ } catch (ReflectiveOperationException e) {
+ // probably because the package isn't open for reflection
+ String message = "The module for class '" + clazz.getName() + "' must be configured to open the '" //$NON-NLS-1$ //$NON-NLS-2$
+ + inPackage + '.' + protocol + "' package for reflection to support the handler." //$NON-NLS-1$
+ + " For example, by using the JVM option: '--add-opens java.base/" + inPackage + '.' + protocol //$NON-NLS-1$
+ + "=ALL-UNNAMED'."; //$NON-NLS-1$
+ throw new UnsupportedOperationException(message, e);
+ } catch (Exception ex) {
+ // handle all exceptions here and move on
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value of a capability this implementation has, as reported through
+ * {@link Plurl#PLURL_GET_CAPABILITY}. A capability that is not recognized is
+ * rejected rather than reported as absent, so that a caller can tell an older
+ * plurl apart from one that answers.
+ */
+ static Object getCapability(String capability) throws IOException {
+ if (Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC.equals(capability)) {
+ return Boolean.TRUE;
+ }
+ throw new IOException("Unknown plurl capability: " + capability); //$NON-NLS-1$
+ }
+
+ URLConnection plurlOperation(URL u) {
+ String opPath = u.getPath();
+ // Tolerate the leading slash of the documented spec form, so that
+ // "plurl://op/" works as well as the URL(protocol, host, file)
+ // form the convenience methods use.
+ String fullPath = opPath.startsWith("/") ? opPath.substring(1) : opPath; //$NON-NLS-1$
+ // Some operations take an argument appended to the operation, for example
+ // "plurl://op/getCapability/".
+ int argIdx = fullPath.indexOf('/');
+ final String path = argIdx < 0 ? fullPath : fullPath.substring(0, argIdx);
+ final String arg = argIdx < 0 ? null : fullPath.substring(argIdx + 1);
+ return new URLConnection(u) {
+ @Override
+ public void connect() throws IOException {
+ // do nothing
+ }
+
+ @Override
+ public Object getContent() throws IOException {
+ switch (path) {
+ case Plurl.PLURL_GET_CAPABILITY:
+ return getCapability(arg);
+ case PLURL_ADD_URL_STREAM_HANDLER_FACTORY:
+ return (Consumer) (f) -> add((URLStreamHandlerFactory) f);
+ case PLURL_REMOVE_URL_STREAM_HANDLER_FACTORY:
+ return (Consumer) (f) -> remove((URLStreamHandlerFactory) f);
+ case PLURL_ADD_CONTENT_HANDLER_FACTORY:
+ return (Consumer) (f) -> add((ContentHandlerFactory) f);
+ case PLURL_REMOVE_CONTENT_HANDLER_FACTORY:
+ return (Consumer) (f) -> remove((ContentHandlerFactory) f);
+ case PLURL_REGISTER_IMPLEMENTATION:
+ return (Consumer) (p) -> addImpl(p);
+ case PLURL_UNREGISTER_IMPLEMENTATION:
+ return (Consumer) (p) -> removeImpl(p);
+ default:
+ throw new IOException("Unknown plurl operation: " + path); //$NON-NLS-1$
+ }
+ }
+ };
+ }
+
+ boolean isMultiplexing(List> factories) {
+ return factories.size() > 1;
+ }
+
+ synchronized void addImpl(Object p) {
+ if (setFactories == SetFactories.primordial) {
+ // If this plurl is the primordial factory (no singletons set in the JVM)
+ // then we don't track other plurl installs because we will never
+ // remove this plurl from the JVM singleton
+ return;
+ }
+ if (p == this) {
+ // someone called install again on this Plurl; ignore it
+ return;
+ }
+ List updated = new ArrayList<>(plurlImpls);
+ // remove any GC'ed handlers or the new impl (incase it is being added again)
+ updated.removeIf((h) -> h.getImpl() == null || h.getImpl() == p);
+ // add new impl
+ updated.add(new PlurlImplHolder(p));
+ plurlImpls = updated;
+ }
+
+ synchronized void removeImpl(Object p) {
+ List updated = new ArrayList<>(plurlImpls);
+ // remove the impl and any that got GC'ed
+ updated.removeIf((h) -> h.getImpl() == p || h.getImpl() == null);
+ plurlImpls = updated.isEmpty() ? Collections.emptyList() : updated;
+ }
+
+ synchronized void add(URLStreamHandlerFactory f) {
+ List updated = new ArrayList<>(streamHandlerFactories);
+ // remove any GC'ed handlers
+ updated.removeIf((h) -> h.getFactory() == null);
+ // add new holder
+ updated.add(new URLStreamHandlerFactoryHolder(f));
+ streamHandlerFactories = updated;
+ }
+
+ synchronized void remove(URLStreamHandlerFactory f) {
+ List updated = new ArrayList<>(streamHandlerFactories);
+ // remove the factory and any that got GC'ed
+ updated.removeIf((h) -> {
+ if (h.getFactory() == f || h.getFactory() == null) {
+ return true;
+ }
+ return false;
+ });
+ streamHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated;
+ }
+
+ synchronized List getURLStreamHandlerFactories() {
+ return streamHandlerFactories;
+ }
+
+ synchronized List getContentHandlerFactories() {
+ return contentHandlerFactories;
+ }
+
+ synchronized void add(ContentHandlerFactory f) {
+ List updated = new ArrayList<>(contentHandlerFactories);
+ // remove any GC'ed handlers
+ updated.removeIf((h) -> h.getFactory() == null);
+ // add new holder
+ updated.add(new ContentHandlerFactoryHolder(f));
+ contentHandlerFactories = updated;
+ }
+
+ synchronized void remove(ContentHandlerFactory f) {
+ List updated = new ArrayList<>(contentHandlerFactories);
+ // remove the factory and any that got GC'ed
+ updated.removeIf((h) -> {
+ if (h.getFactory() == f || h.getFactory() == null) {
+ return true;
+ }
+ return false;
+ });
+ contentHandlerFactories = updated.isEmpty() ? Collections.emptyList() : updated;
+ }
+
+ ContentHandler findContentHandler(String contentType) {
+ ContentHandlerFactoryHolder f = findFactory(getContentHandlerFactories());
+ if (f != null) {
+ return f.getHandler(contentType);
+ }
+ return null;
+ }
+
+ PlurlStreamHandler findPlurlStreamHandler(String protocol) {
+ return findPlurlStreamHandler(protocol, null);
+ }
+
+ PlurlStreamHandler findPlurlStreamHandler(String protocol, String spec) {
+ URLStreamHandlerFactoryHolder f = findFactory(getURLStreamHandlerFactories(), protocol, spec);
+ if (f != null) {
+ return f.getHandler(protocol);
+ }
+ return null;
+ }
+
+ private F findFactory(List factories) {
+ return findFactory(factories, null, null);
+ }
+
+ private F findFactory(List factories, String protocol, String spec) {
+ int numFactories = factories.size();
+ if (numFactories == 1) {
+ // Handle common case of only one; just use it
+ return factories.get(0);
+ }
+ // Give the factories a chance to claim the URL being parsed first. A protocol
+ // may be shared by several factories that can only be told apart by the URL,
+ // and the URL may be used by a caller no factory recognizes from the call
+ // stack.
+ if (spec != null) {
+ for (F f : factories) {
+ if (f instanceof URLStreamHandlerFactoryHolder
+ && ((URLStreamHandlerFactoryHolder) f).shouldHandleURL(protocol, spec)) {
+ return f;
+ }
+ }
+ }
+ Class>[] callStackClasses = getCallStack();
+ for (Class> stack : callStackClasses) {
+ String pName = getPackageName(stack);
+ if (THIS_PACKAGE.equals(pName) || isSystemClass(pName, stack)) {
+ continue;
+ }
+ for (F f : factories) {
+ boolean shouldHandle = false;
+ if (f instanceof PlurlFactory) {
+ shouldHandle = ((PlurlFactory) f).shouldHandle(stack);
+ } else {
+ // use reflection in case this Plurl package isn't visible to the factory impl
+ try {
+ shouldHandle = (boolean) findShouldHandle(f.getClass()).invoke(f, stack);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if (shouldHandle) {
+ return f;
+ }
+ }
+ }
+ // Instead of returning null here, the "first" factory is returned;
+ // This means the root or "first" factory may provide protocol handlers for call stacks
+ // that have no classes known to that factory
+ return numFactories > 0 ? factories.get(0) : null;
+ }
+
+ /**
+ * The optional URL selection method of a factory, or null if it does
+ * not have one. Unlike {@link #findShouldHandle(Class)} a missing method is not an
+ * error, because a factory is not required to select on the URL.
+ */
+ static Method findShouldHandleURL(Class> clazz) {
+ try {
+ Method m = clazz.getMethod("shouldHandleURL", String.class, String.class); //$NON-NLS-1$
+ m.setAccessible(true);
+ return m;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ Method findShouldHandle(Class> clazz) throws NoSuchMethodException {
+ Method shouldHandle = null;
+ try {
+ shouldHandle = clazz.getMethod("shouldHandle", Class.class); //$NON-NLS-1$
+ } catch (NoSuchMethodException e) {
+ // check for legacy hasAuthority method
+ try {
+ shouldHandle = clazz.getMethod("hasAuthority", Class.class); //$NON-NLS-1$
+ } catch (NoSuchMethodException e1) {
+ throw e;
+ }
+ }
+ shouldHandle.setAccessible(true);
+ return shouldHandle;
+ }
+
+ private String getPackageName(Class> clazz) {
+ String name = clazz.getName();
+ int lastDot = name.lastIndexOf('.');
+ if (lastDot >= 0) {
+ return name.substring(0, lastDot);
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ private Class>[] getCallStack() {
+ return callStack.getClassContext();
+ }
+
+ class PlurlRootContentHandler extends ContentHandler {
+ private final String contentType;
+ private final ContentHandler fromParent;
+
+ PlurlRootContentHandler(String contentType, ContentHandler fromParent) {
+ this.contentType = contentType;
+ this.fromParent = fromParent;
+ }
+
+ @Override
+ public Object getContent(URLConnection uConn) throws IOException {
+ ContentHandler handler = findContentHandler(contentType);
+ if (handler != null) {
+ return handler.getContent(uConn);
+ }
+ if (fromParent != null) {
+ return fromParent.getContent(uConn);
+ }
+ return uConn.getInputStream();
+ }
+ }
+
+ public abstract class PlurlFactoryHolder implements PlurlFactory {
+ private final WeakReference factory;
+ private final Map handlers = new ConcurrentHashMap<>();
+ private final Method shouldHandleMethod;
+
+ public PlurlFactoryHolder(F factory) {
+ this.factory = new WeakReference<>(factory);
+ if (factory instanceof PlurlFactory) {
+ shouldHandleMethod = null;
+ } else {
+ try {
+ shouldHandleMethod = findShouldHandle(factory.getClass());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ F getFactory() {
+ return factory.get();
+ }
+
+ @Override
+ public boolean shouldHandle(Class> clazz) {
+ F f = factory.get();
+ if (f == null) {
+ return false;
+ }
+ if (shouldHandleMethod != null) {
+ try {
+ return (boolean) shouldHandleMethod.invoke(f, clazz);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return ((PlurlFactory) f).shouldHandle(clazz);
+ }
+
+ H getHandler(String type) {
+ final F f = factory.get();
+ if (f == null) {
+ // clear handlers
+ handlers.clear();
+ // remove GC'ed holders
+ remove(null);
+ return null;
+ }
+ return handlers.computeIfAbsent(type, (t) -> createHandler(t, f));
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + factory.get() + ']'
+ + handlers;
+ }
+
+ protected abstract H createHandler(String type, F f);
+
+ protected abstract void remove(F f);
+ }
+
+ class PlurlImplHolder {
+ private final WeakReference plurlImpl;
+
+ PlurlImplHolder(Object plurlImpl) {
+ this.plurlImpl = new WeakReference<>(plurlImpl);
+ }
+
+ public void install() {
+ // should be able to reflect on the plurl instance because install is public
+ Object currentPlurl = plurlImpl.get();
+ // should never be null since we pinned the object before calling
+ try {
+ Method install = currentPlurl.getClass().getMethod("install", String[].class); //$NON-NLS-1$
+ install.invoke(currentPlurl, (Object) forbiddenProtocols.toArray(new String[0]));
+ } catch (NoSuchMethodException e) {
+ // should never happen
+ throw new RuntimeException(e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ synchronized (PlurlImpl.this) {
+ try {
+ URL registerPlurl = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_REGISTER_IMPLEMENTATION);
+ for (PlurlImplHolder plurlImplHolder : plurlImpls) {
+ Object p = plurlImplHolder.getImpl();
+ @SuppressWarnings("unchecked")
+ Consumer addImpl = (Consumer) registerPlurl.openConnection().getContent();
+ addImpl.accept(p);
+ }
+ URL addContentHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP, PLURL_ADD_CONTENT_HANDLER_FACTORY);
+ for (ContentHandlerFactoryHolder contentHandlerFactoryHolder : contentHandlerFactories) {
+ ContentHandlerFactory c = contentHandlerFactoryHolder.getFactory();
+ @SuppressWarnings("unchecked")
+ Consumer addImpl = (Consumer) addContentHandlerFactory.openConnection()
+ .getContent();
+ addImpl.accept(c);
+ }
+ URL addURLStreamHandlerFactory = new URL(PLURL_PROTOCOL, PLURL_OP,
+ PLURL_ADD_URL_STREAM_HANDLER_FACTORY);
+ for (URLStreamHandlerFactoryHolder urlStreamHandlerFactoryHolder : streamHandlerFactories) {
+ URLStreamHandlerFactory s = urlStreamHandlerFactoryHolder.getFactory();
+ @SuppressWarnings("unchecked")
+ Consumer addImpl = (Consumer) addURLStreamHandlerFactory.openConnection()
+ .getContent();
+ addImpl.accept(s);
+ }
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ plurlImpls.clear();
+ contentHandlerFactories.clear();
+ streamHandlerFactories.clear();
+ parentContentHandlerFactory = null;
+ parentURLStreamHandlerFactory = null;
+ }
+ }
+
+ }
+
+ public Object getImpl() {
+ return plurlImpl.get();
+ }
+ }
+
+ public class ContentHandlerFactoryHolder extends PlurlFactoryHolder {
+ public ContentHandlerFactoryHolder(ContentHandlerFactory factory) {
+ super(factory);
+ }
+
+ @Override
+ protected ContentHandler createHandler(String mimetype, ContentHandlerFactory f) {
+ return f.createContentHandler(mimetype);
+ }
+
+ @Override
+ protected void remove(ContentHandlerFactory f) {
+ PlurlImpl.this.remove(f);
+ }
+ }
+
+ public class URLStreamHandlerFactoryHolder extends PlurlFactoryHolder {
+ private final Method shouldHandleURLMethod;
+
+ public URLStreamHandlerFactoryHolder(URLStreamHandlerFactory factory) {
+ super(factory);
+ if (factory instanceof PlurlStreamHandlerFactory) {
+ shouldHandleURLMethod = null;
+ } else {
+ // use reflection in case this Plurl package isn't visible to the
+ // factory impl; a factory that predates the method has none, which is
+ // not an error
+ shouldHandleURLMethod = findShouldHandleURL(factory.getClass());
+ }
+ }
+
+ /**
+ * Delegates to the wrapped factory the way {@link #shouldHandle(Class)} does.
+ * Without this, findFactory would only ever ask the holder, which has no
+ * opinion of its own.
+ */
+ boolean shouldHandleURL(String protocol, String spec) {
+ URLStreamHandlerFactory f = getFactory();
+ if (f == null) {
+ return false;
+ }
+ if (f instanceof PlurlStreamHandlerFactory) {
+ return ((PlurlStreamHandlerFactory) f).shouldHandleURL(protocol, spec);
+ }
+ if (shouldHandleURLMethod == null) {
+ return false;
+ }
+ try {
+ return (boolean) shouldHandleURLMethod.invoke(f, protocol, spec);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Whether this factory selects on the URL for the given protocol at all. Used
+ * only to decide whether to claim the protocol from the JVM, which happens
+ * before any URL exists, so the factory is asked with no spec.
+ */
+ boolean takesOverURLs(String protocol) {
+ return shouldHandleURL(protocol, null);
+ }
+
+ @Override
+ protected PlurlStreamHandler createHandler(String protocol, URLStreamHandlerFactory f) {
+ URLStreamHandler handler = f.createURLStreamHandler(protocol);
+ if (handler == null) {
+ return null;
+ }
+ if (handler instanceof PlurlStreamHandler) {
+ return (PlurlStreamHandler) handler;
+ }
+ PlurlStreamHandler proxyPlurlStreamHandler = newProxyPlurlStreamHandler(handler);
+ if (proxyPlurlStreamHandler != null) {
+ return proxyPlurlStreamHandler;
+ }
+ return new PlurlStreamHandlerReflective(handler);
+ }
+
+ @Override
+ protected void remove(URLStreamHandlerFactory f) {
+ PlurlImpl.this.remove(f);
+ }
+ }
+
+ PlurlStreamHandler newProxyPlurlStreamHandler(URLStreamHandler handler) {
+ Class> checkClass = handler.getClass();
+ while (checkClass != null) {
+ for (Class> handlerInterfaces : checkClass.getInterfaces()) {
+ // To determine if we can proxy the handler we look for an interface that defines a method
+ // public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit);
+ // But we cannot search for it the conventional way because the PlurlSetter may be a different
+ // copy from ours.
+ for (Method m : handlerInterfaces.getMethods()) {
+ if (m.getName().equals("parseURL")) { //$NON-NLS-1$
+ Class>[] params = m.getParameterTypes();
+ if (params.length == 5) {
+ // check the first param to see if it is a PlurlSetter candidate
+ Class> plurlSetterCandidate = params[0];
+ if (plurlSetterCandidate.isInterface()) {
+ try {
+ // try finding the appropriate setURL method
+ plurlSetterCandidate.getMethod("setURL", URL.class, String.class, String.class, Integer.TYPE, String.class, String.class,
+ String.class, String.class, String.class);
+ } catch (Exception e) {
+ // move on to the next interface
+ continue;
+ }
+ }
+ } else {
+ // Wrong number of arguments for parseURL; move on to next interface
+ continue;
+ }
+ Class> plurlStreamHandlerClass = handlerInterfaces;
+ Class> plurlSetterClass = m.getParameterTypes()[0];
+ return new PlurlStreamHandlerProxy(handler, plurlStreamHandlerClass, plurlSetterClass);
+ }
+ }
+ }
+ checkClass = checkClass.getSuperclass();
+ }
+ return null;
+ }
+
+ static class PlurlStreamHandlerProxy extends URLStreamHandler implements PlurlStreamHandler {
+ private final URLStreamHandler handler;
+ private final Class> plurlSetterClass;
+ private final Method equals;
+ private final Method getDefaultPort;
+ private final Method getHostAddress;
+ private final Method hashCode;
+ private final Method hostsEqual;
+ private final Method openConnection;
+ private final Method openConnectionProxy;
+ private final Method parseURL;
+ private final Method sameFile;
+ private final Method toExternalForm;
+ final Method setURL;
+ final Method setURLDeprecated;
+
+ public PlurlStreamHandlerProxy(URLStreamHandler handler, Class> plurlUrlHandlerClass,
+ Class> plurlSetterClass) {
+ this.handler = handler;
+ this.plurlSetterClass = plurlSetterClass;
+ openConnection = findMethod(plurlUrlHandlerClass, "openConnection", URL.class); //$NON-NLS-1$
+ openConnectionProxy = findMethod(plurlUrlHandlerClass, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$
+ parseURL = findMethod(plurlUrlHandlerClass, "parseURL", plurlSetterClass, URL.class, String.class, //$NON-NLS-1$
+ Integer.TYPE, Integer.TYPE);
+ equals = findMethod(plurlUrlHandlerClass, "equals", URL.class, URL.class); //$NON-NLS-1$
+ getDefaultPort = findMethod(plurlUrlHandlerClass, "getDefaultPort"); //$NON-NLS-1$
+ getHostAddress = findMethod(plurlUrlHandlerClass, "getHostAddress", URL.class); //$NON-NLS-1$
+ hashCode = findMethod(plurlUrlHandlerClass, "hashCode", URL.class); //$NON-NLS-1$
+ hostsEqual = findMethod(plurlUrlHandlerClass, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$
+ sameFile = findMethod(plurlUrlHandlerClass, "sameFile", URL.class, URL.class); //$NON-NLS-1$
+ toExternalForm = findMethod(plurlUrlHandlerClass, "toExternalForm", URL.class); //$NON-NLS-1$
+ setURL = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, int.class, //$NON-NLS-1$
+ String.class, String.class, String.class, String.class, String.class);
+ setURLDeprecated = findMethod(plurlUrlHandlerClass, "setURL", URL.class, String.class, String.class, //$NON-NLS-1$
+ int.class, String.class, String.class);
+ }
+
+ private static Method findMethod(Class> plurlUrlHandlerClass, String methodName, Class>... args) {
+ Method result = null;
+ try {
+ result = plurlUrlHandlerClass.getDeclaredMethod(methodName, args);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return result;
+ }
+
+ Object invoke(Method m, Object... args) {
+ try {
+ return m.invoke(handler, args);
+ } catch (InvocationTargetException e) {
+ throw (RuntimeException) e.getTargetException();
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean equals(URL u1, URL u2) {
+ return (boolean) invoke(equals, u1, u2);
+ }
+
+ @Override
+ public int hashCode(URL u) {
+ return (int) invoke(hashCode, u);
+ }
+
+ @Override
+ public boolean hostsEqual(URL u1, URL u2) {
+ return (boolean) invoke(hostsEqual, u1, u2);
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return (int) invoke(getDefaultPort);
+ }
+
+ @Override
+ public InetAddress getHostAddress(URL u) {
+ return (InetAddress) invoke(getHostAddress, u);
+ }
+
+ @Override
+ public URLConnection openConnection(URL u) throws IOException {
+ return (URLConnection) invoke(openConnection, u);
+ }
+
+ @Override
+ public URLConnection openConnection(URL u, Proxy p) throws IOException {
+ return (URLConnection) invoke(openConnectionProxy, u, p);
+ }
+
+ @Override
+ public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) {
+ setHandler(u, handler);
+ Object plurlSetterProxy = null;
+ if (plurlSetter != null) {
+ plurlSetterProxy = java.lang.reflect.Proxy.newProxyInstance(handler.getClass().getClassLoader(),
+ new Class[] { plurlSetterClass }, new InvocationHandler() {
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if ("setURL".equals(method.getName())) { //$NON-NLS-1$
+ if (args.length == 9) {
+ plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2], (int) args[3],
+ (String) args[4], (String) args[5], (String) args[6], (String) args[7],
+ (String) args[8]);
+ } else {
+ plurlSetter.setURL((URL) args[0], (String) args[1], (String) args[2],
+ (int) args[3], null, null, (String) args[4], null, (String) args[5]);
+ }
+ }
+ return null;
+ }
+ });
+ }
+ invoke(parseURL, plurlSetterProxy, u, spec, start, limit);
+ }
+
+ @Override
+ public boolean sameFile(URL u1, URL u2) {
+ return (boolean) invoke(sameFile, u1, u2);
+ }
+
+ @Override
+ public String toExternalForm(URL u) {
+ return (String) invoke(toExternalForm, u);
+ }
+
+ @Override
+ public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo,
+ String path, String query, String ref) {
+ invoke(setURL, u, protocol, host, port, authority, userInfo, path, query, ref);
+ }
+
+ @Override
+ @Deprecated
+ public void setURL(URL u, String protocol, String host, int port, String file, String ref) {
+ invoke(setURLDeprecated, u, protocol, host, port, file, ref);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']';
+ }
+ }
+
+ static class PlurlStreamHandlerReflective extends URLStreamHandler implements PlurlStreamHandler {
+ private final URLStreamHandler handler;
+ private final Method openConnectionMethod;
+ private final Method openConnectionProxyMethod;
+ private final Method parseURLMethod;
+ private final Method equalsMethod;
+ private final Method getDefaultPortMethod;
+ private final Method getHostAddressMethod;
+ private final Method hashCodeMethod;
+ private final Method hostsEqualMethod;
+ private final Method sameFileMethod;
+ private final Method toExternalFormMethod;
+
+ public PlurlStreamHandlerReflective(URLStreamHandler handler) {
+ this.handler = handler;
+ openConnectionMethod = findMethod(handler, "openConnection", URL.class); //$NON-NLS-1$
+ openConnectionProxyMethod = findMethod(handler, "openConnection", URL.class, Proxy.class); //$NON-NLS-1$
+ parseURLMethod = findMethod(handler, "parseURL", URL.class, String.class, Integer.TYPE, Integer.TYPE); //$NON-NLS-1$
+ equalsMethod = findMethod(handler, "equals", URL.class, URL.class); //$NON-NLS-1$
+ getDefaultPortMethod = findMethod(handler, "getDefaultPort"); //$NON-NLS-1$
+ getHostAddressMethod = findMethod(handler, "getHostAddress", URL.class); //$NON-NLS-1$
+ hashCodeMethod = findMethod(handler, "hashCode", URL.class); //$NON-NLS-1$
+ hostsEqualMethod = findMethod(handler, "hostsEqual", URL.class, URL.class); //$NON-NLS-1$
+ sameFileMethod = findMethod(handler, "sameFile", URL.class, URL.class); //$NON-NLS-1$
+ toExternalFormMethod = findMethod(handler, "toExternalForm", URL.class); //$NON-NLS-1$
+ if (URL_HANDLER_FIELD == null) {
+ throw new RuntimeException(getReflectionErrorMessage(handler.getClass()));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Object invoke(Method m, Object... args) throws T {
+ try {
+ return m.invoke(handler, args);
+ } catch (InvocationTargetException e) {
+ throw (T) e.getTargetException();
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ private static String getReflectionErrorMessage(Class extends URLStreamHandler> 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 extends URLStreamHandler> handlerClass = h.getClass();
+ try {
+ result = handlerClass.getDeclaredMethod(methodName, args);
+ result.setAccessible(true);
+ } catch (Exception e1) {
+ try {
+ result = URLStreamHandler.class.getDeclaredMethod(methodName, args);
+ result.setAccessible(true);
+ } catch (Exception e2) {
+ // fallback reflection is blocked by Java modules
+ String message = getReflectionErrorMessage(handlerClass);
+ throw new RuntimeException(message, e2);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(URL u1, URL u2) {
+ return (Boolean) invoke(equalsMethod, u1, u2);
+ }
+
+ @Override
+ public int hashCode(URL u) {
+ return (Integer) invoke(hashCodeMethod, u);
+ }
+
+ @Override
+ public boolean hostsEqual(URL u1, URL u2) {
+ return (Boolean) invoke(hostsEqualMethod, u1, u2);
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return (Integer) invoke(getDefaultPortMethod);
+ }
+
+ @Override
+ public InetAddress getHostAddress(URL u) {
+ return (InetAddress) invoke(getHostAddressMethod, u);
+ }
+
+ @Override
+ public URLConnection openConnection(URL u) throws IOException {
+ return (URLConnection) invoke(openConnectionMethod, u);
+ }
+
+ @Override
+ public URLConnection openConnection(URL u, Proxy p) throws IOException {
+ return (URLConnection) invoke(openConnectionProxyMethod, u, p);
+ }
+
+ @Override
+ public boolean sameFile(URL u1, URL u2) {
+ return (Boolean) invoke(sameFileMethod, u1, u2);
+ }
+
+ @Override
+ public String toExternalForm(URL u) {
+ return (String) invoke(toExternalFormMethod, u);
+ }
+
+ @Override
+ public void parseURL(PlurlSetter plurlSetter, URL u, String spec, int start, int limit) {
+ try {
+ setHandler(u, handler);
+ invoke(parseURLMethod, u, spec, start, limit);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setURL(URL u, String protocol, String host, int port, String file, String ref) {
+ super.setURL(u, protocol, host, port, file, ref);
+ }
+
+ @Override
+ public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo,
+ String path, String query, String ref) {
+ super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '@' + System.identityHashCode(this) + '[' + handler + ']';
+ }
+ }
+
+ static final PlurlStreamHandler NULL_HANDLER = new PlurlStreamHandlerBase() {
+ @Override
+ public URLConnection openConnection(URL u) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ class PlurlRootURLStreamHandler extends URLStreamHandler implements PlurlSetter {
+ private final String protocol;
+ private final AtomicReference builtin = new AtomicReference<>();
+
+ private PlurlStreamHandler lookupPlurlStreamHandler(URL u) {
+ return lookupPlurlStreamHandler(u, null);
+ }
+
+ private PlurlStreamHandler lookupPlurlStreamHandler(URL u, String spec) {
+ if (u != null && isMultiplexing(getURLStreamHandlerFactories())) {
+ // Record the handler found for the URL;
+ // This allows to consistently use the same handler for the
+ // life of the URL object when we are multiplexing.
+ return urlToHandler.get(u, () -> findPlurlStreamHandlerImpl(spec));
+ }
+ return findPlurlStreamHandlerImpl(spec);
+ }
+
+ private PlurlStreamHandler findPlurlStreamHandlerImpl(String spec) {
+ PlurlStreamHandler h = findPlurlStreamHandler(protocol, spec);
+ if (h == null) {
+ h = findBuiltin();
+ if (h == null) {
+ throw new IllegalStateException("No handler found for protocol: " + protocol); //$NON-NLS-1$
+ }
+ }
+ return h;
+ }
+
+ private PlurlStreamHandler findBuiltin() {
+ PlurlStreamHandler result = builtin.updateAndGet((h) -> {
+ URLStreamHandler found = findBuildinURLStreamHandlerImpl(protocol, "sun.net.www.protocol"); //$NON-NLS-1$
+ if (found == null) {
+ return NULL_HANDLER;
+ }
+ // we can only really do this if java.net is open for reflection
+ return new PlurlStreamHandlerReflective(found);
+ });
+ return (result == NULL_HANDLER) ? null : result;
+ }
+
+ PlurlRootURLStreamHandler(String protocol) {
+ this.protocol = protocol;
+ }
+
+ @Override
+ protected boolean equals(URL u1, URL u2) {
+ return lookupPlurlStreamHandler(u1).equals(u1, u2);
+ }
+
+ @Override
+ protected int hashCode(URL u) {
+ return lookupPlurlStreamHandler(u).hashCode(u);
+ }
+
+ @Override
+ protected boolean hostsEqual(URL u1, URL u2) {
+ return lookupPlurlStreamHandler(u1).hostsEqual(u1, u2);
+ }
+
+ @Override
+ protected int getDefaultPort() {
+ return lookupPlurlStreamHandler(null).getDefaultPort();
+ }
+
+ @Override
+ protected InetAddress getHostAddress(URL u) {
+ return lookupPlurlStreamHandler(u).getHostAddress(u);
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u) throws IOException {
+ return lookupPlurlStreamHandler(u).openConnection(u);
+ }
+
+ @Override
+ protected URLConnection openConnection(URL u, Proxy p) throws IOException {
+ return lookupPlurlStreamHandler(u).openConnection(u, p);
+ }
+
+ @Override
+ protected void parseURL(URL u, String spec, int start, int limit) {
+ PlurlStreamHandler h = lookupPlurlStreamHandler(u, spec);
+ if (setHandler(u, h)) {
+ h.parseURL(null, u, spec, start, limit);
+ } else {
+ h.parseURL(this, u, spec, start, limit);
+ }
+ }
+
+ @Override
+ protected boolean sameFile(URL u1, URL u2) {
+ return lookupPlurlStreamHandler(u1).sameFile(u1, u2);
+ }
+
+ @Override
+ protected String toExternalForm(URL u) {
+ return lookupPlurlStreamHandler(u).toExternalForm(u);
+ }
+
+ @Override
+ public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo,
+ String path, String query, String ref) {
+ super.setURL(u, protocol, host, port, authority, userInfo, path, query, ref);
+ }
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java
new file mode 100644
index 0000000000..fac4797af4
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/SecurityManagerCallStack.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+class SecurityManagerCallStack implements CallStack {
+ // used to get access to the protected SecurityManager#getClassContext method
+ static class InternalSecurityManager extends SecurityManager {
+ @Override
+ public Class>[] getClassContext() {
+ return super.getClassContext();
+ }
+ }
+
+ private static InternalSecurityManager internalSecurityManager = new InternalSecurityManager();
+
+ public Class>[] getClassContext() {
+ return internalSecurityManager.getClassContext();
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java
new file mode 100644
index 0000000000..a65c612dbf
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/StackWalkerCallStack.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+package org.apache.felix.framework.plurl.impl;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+class StackWalkerCallStack implements CallStack {
+
+ static final Class> stackWalkerClass;
+ static final Object stackWalker;
+ static final Method forEach;
+ static final Method getDeclaringClass;
+ static final CallStack fallback;
+ static {
+ Class> tmpStackWalkerClass = null;
+ Object tmpStackWalker = null;
+ Method tmpForEach = null;
+ Method tmpGetDeclaringClass = null;
+ CallStack tmpFallback = null;
+ try {
+ Class> stackWalkerOptionClass = Class.forName("java.lang.StackWalker$Option"); //$NON-NLS-1$
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ Object RETAIN_CLASS_REFERENCE = Enum.valueOf((Class) stackWalkerOptionClass, "RETAIN_CLASS_REFERENCE"); //$NON-NLS-1$
+ tmpStackWalkerClass = Class.forName("java.lang.StackWalker"); //$NON-NLS-1$
+ tmpStackWalker = tmpStackWalkerClass.getMethod("getInstance", stackWalkerOptionClass).invoke(null, //$NON-NLS-1$
+ RETAIN_CLASS_REFERENCE);
+ tmpForEach = tmpStackWalkerClass.getMethod("forEach", Consumer.class); //$NON-NLS-1$
+ tmpGetDeclaringClass = Class.forName("java.lang.StackWalker$StackFrame").getMethod("getDeclaringClass"); //$NON-NLS-1$ //$NON-NLS-2$
+ } catch (Throwable t) {
+ // null all out
+ tmpStackWalkerClass = null;
+ tmpStackWalker = null;
+ tmpForEach = null;
+ tmpGetDeclaringClass = null;
+ // fallback to security manager
+ try {
+ tmpFallback = new SecurityManagerCallStack();
+ } catch (Throwable fallbackException) {
+ // this is bad
+ fallbackException.printStackTrace();
+ }
+ }
+ stackWalkerClass = tmpStackWalkerClass;
+ stackWalker = tmpStackWalker;
+ forEach = tmpForEach;
+ getDeclaringClass = tmpGetDeclaringClass;
+ fallback = tmpFallback;
+ }
+
+
+ public Class>[] getClassContext() {
+ if (fallback != null) {
+ return fallback.getClassContext();
+ }
+ List> result = new ArrayList<>();
+ if (stackWalker != null) {
+ try {
+ forEach.invoke(stackWalker, new Consumer() {
+ public void accept(Object s) {
+ try {
+ result.add((Class>) getDeclaringClass.invoke(s));
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ });
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ return result.toArray(new Class>[0]);
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java
new file mode 100644
index 0000000000..d17b2b40f7
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/plurl/impl/URLToHandler.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * Copyright (c) Contributors to the Eclipse Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *******************************************************************************/
+
+package org.apache.felix.framework.plurl.impl;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.felix.framework.plurl.PlurlStreamHandler;
+
+public class URLToHandler {
+ class WeakURL extends WeakReference {
+ private final int hashcode;
+
+ public WeakURL(URL u, ReferenceQueue q) {
+ super(u, q);
+ this.hashcode = System.identityHashCode(u);
+ }
+ @Override
+ public int hashCode() {
+ return hashcode;
+ }
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof WeakURL) {
+ return get() == ((WeakURL) obj).get();
+ }
+ return false;
+ }
+ }
+
+ final ReferenceQueue queue = new ReferenceQueue<>();
+
+ Map entries = Collections.synchronizedMap(new HashMap<>());
+
+ PlurlStreamHandler get(URL u, Supplier h) {
+ WeakURL lookup = new WeakURL(u, null);
+ PlurlStreamHandler existing = entries.get(lookup);
+ if (existing != null) {
+ return existing;
+ }
+
+ PlurlStreamHandler result = h == null ? null : h.get();
+ if (result != null) {
+ synchronized (entries) {
+ PlurlStreamHandler recheck = entries.get(lookup);
+ if (recheck != null) {
+ return recheck;
+ }
+ entries.put(new WeakURL(u, queue), result);
+ Object x;
+ while ((x = queue.poll()) != null) {
+ entries.remove(x);
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
index 95fffc6f6c..d09cb791c1 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
@@ -33,7 +33,6 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URI;
@@ -47,8 +46,6 @@
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.Hashtable;
import java.util.Map;
import java.util.function.Consumer;
import java.util.jar.JarFile;
@@ -105,7 +102,6 @@ public class SecureAction
result = new byte[0];
}
accessor = result;
- getAccessor(URL.class);
}
protected static transient int BUFSIZE = 4096;
@@ -339,170 +335,89 @@ public Object getDeclaredField(Class> targetClass, String name, Object target)
return field.get(target);
}
- public Object swapStaticFieldIfNotClass(Class> targetClazz,
- Class> targetType, Class> condition, String lockName) throws Exception
- {
- return _swapStaticFieldIfNotClass(targetClazz, targetType,
- condition, lockName);
- }
-
private static volatile Consumer m_accessorCache = null;
- @SuppressWarnings("unchecked")
+ private static final Consumer SET_ACCESSIBLE =
+ objects -> AccessibleObject.setAccessible(objects, true);
+
private static Consumer getAccessor(Class> clazz)
{
String packageName = clazz.getPackage().getName();
if ("java.net".equals(packageName) || "jdk.internal.loader".equals(packageName))
{
- if (m_accessorCache == null)
+ // Try ordinary reflection first and only smuggle an accessor into
+ // java.base if the JVM actually refuses. When the package is open to us
+ // -- as it is under the Add-opens of the org.apache.felix.main launcher,
+ // or any --add-opens on the command line -- setAccessible just works, and
+ // reaching for Unsafe would be both unnecessary and noisy: JDK 25 prints
+ // a terminal deprecation warning for every call to it.
+ return objects ->
{
try
{
- // Use reflection on Unsafe to avoid having to compile against it
- Class> unsafeClass = Class.forName("sun.misc.Unsafe"); //$NON-NLS-1$
- Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); //$NON-NLS-1$
- // NOTE: deep reflection is allowed on sun.misc package for java 9.
- theUnsafe.setAccessible(true);
- Object unsafe = theUnsafe.get(null);
- Class> result;
- try {
- Method defineAnonymousClass = unsafeClass.getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class); //$NON-NLS-1$
- result = (Class>) defineAnonymousClass.invoke(unsafe, URL.class, accessor , null);
- }
- catch (NoSuchMethodException ex)
- {
- long offset = (long) unsafeClass.getMethod("staticFieldOffset", Field.class)
- .invoke(unsafe, MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"));
-
- MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafeClass.getMethod("getObject", Object.class, long.class)
- .invoke(unsafe, MethodHandles.Lookup.class, offset);
- lookup = lookup.in(URL.class);
- Class> classOption = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); //$NON-NLS-1$
- Object classOptions = Array.newInstance(classOption, 0);
- Method defineHiddenClass = MethodHandles.Lookup.class.getMethod("defineHiddenClass", byte[].class, boolean.class, //$NON-NLS-1$
- classOptions.getClass());
- lookup = (MethodHandles.Lookup) defineHiddenClass.invoke(lookup, accessor, Boolean.FALSE, classOptions);
- result = (Class>) lookup.lookupClass();
- }
- m_accessorCache = result.getConstructor().newInstance();
+ SET_ACCESSIBLE.accept(objects);
}
- catch (Throwable t)
+ catch (RuntimeException ex)
{
- m_accessorCache = objects -> AccessibleObject.setAccessible(objects, true);
+ // InaccessibleObjectException: java.base is not open to us.
+ unsafeAccessor().accept(objects);
}
- }
- return m_accessorCache;
+ };
}
- else
- {
- return objects -> AccessibleObject.setAccessible(objects, true);
- }
- }
-
- private static Object _swapStaticFieldIfNotClass(Class> targetClazz,
- Class> targetType, Class> condition, String lockName) throws Exception
+ return SET_ACCESSIBLE;
+ }
+
+ /**
+ * An accessor defined inside java.base itself, so that {@code setAccessible} is
+ * called by a class in that module and the access check passes without the
+ * package being open.
+ *
+ * This needs {@code sun.misc.Unsafe} to obtain a trusted
+ * {@code MethodHandles.Lookup}, which is terminally deprecated as of JDK 25, so
+ * it is only used when ordinary reflection has already failed.
+ */
+ @SuppressWarnings("unchecked")
+ private static Consumer unsafeAccessor()
{
-
- Object lock = null;
- if (lockName != null)
+ if (m_accessorCache == null)
{
try
{
- Field lockField =
- targetClazz.getDeclaredField(lockName);
- getAccessor(targetClazz).accept(new AccessibleObject[]{lockField});
- lock = lockField.get(null);
- }
- catch (NoSuchFieldException ex)
- {
- }
- }
- if (lock == null)
- {
- lock = targetClazz;
- }
- synchronized (lock)
- {
- Field[] fields = targetClazz.getDeclaredFields();
-
- getAccessor(targetClazz).accept(fields);
-
- Object result = null;
- for (int i = 0; (i < fields.length) && (result == null); i++)
- {
- if (Modifier.isStatic(fields[i].getModifiers()) &&
- (fields[i].getType() == targetType))
- {
- result = fields[i].get(null);
-
- if (result != null)
- {
- if ((condition == null) ||
- !result.getClass().getName().equals(condition.getName()))
- {
- fields[i].set(null, null);
- }
- }
+ // Use reflection on Unsafe to avoid having to compile against it
+ Class> unsafeClass = Class.forName("sun.misc.Unsafe"); //$NON-NLS-1$
+ Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); //$NON-NLS-1$
+ // NOTE: deep reflection is allowed on sun.misc package for java 9.
+ theUnsafe.setAccessible(true);
+ Object unsafe = theUnsafe.get(null);
+ Class> result;
+ try {
+ Method defineAnonymousClass = unsafeClass.getMethod("defineAnonymousClass", Class.class, byte[].class, Object[].class); //$NON-NLS-1$
+ result = (Class>) defineAnonymousClass.invoke(unsafe, URL.class, accessor , null);
}
- }
- if (result != null)
- {
- if ((condition == null) || !result.getClass().getName().equals(condition.getName()))
+ catch (NoSuchMethodException ex)
{
- // reset cache
- for (Field field : fields) {
- if (Modifier.isStatic(field.getModifiers()) &&
- (field.getType() == Hashtable.class))
- {
- Hashtable,?> cache = (Hashtable) field.get(null);
- if (cache != null)
- {
- cache.clear();
- }
- }
- }
+ long offset = (long) unsafeClass.getMethod("staticFieldOffset", Field.class)
+ .invoke(unsafe, MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"));
+
+ MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafeClass.getMethod("getObject", Object.class, long.class)
+ .invoke(unsafe, MethodHandles.Lookup.class, offset);
+ lookup = lookup.in(URL.class);
+ Class> classOption = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); //$NON-NLS-1$
+ Object classOptions = Array.newInstance(classOption, 0);
+ Method defineHiddenClass = MethodHandles.Lookup.class.getMethod("defineHiddenClass", byte[].class, boolean.class, //$NON-NLS-1$
+ classOptions.getClass());
+ lookup = (MethodHandles.Lookup) defineHiddenClass.invoke(lookup, accessor, Boolean.FALSE, classOptions);
+ result = (Class>) lookup.lookupClass();
}
- return result;
+ m_accessorCache = result.getConstructor().newInstance();
}
- }
- return null;
- }
-
- public void flush(Class>targetClazz, Object lock) throws Exception
- {
- _flush(targetClazz, lock);
- }
-
- private static void _flush(Class>targetClazz, Object lock) throws Exception
- {
- synchronized (lock)
- {
- Field[] fields = targetClazz.getDeclaredFields();
- getAccessor(targetClazz).accept(fields);
- // reset cache
- for (Field field : fields) {
- if (Modifier.isStatic(field.getModifiers()) &&
- ((field.getType() == Hashtable.class) || (field.getType() == HashMap.class)))
- {
- if (field.getType() == Hashtable.class)
- {
- Hashtable,?> cache = (Hashtable) field.get(null);
- if (cache != null)
- {
- cache.clear();
- }
- }
- else
- {
- HashMap,?> cache = (HashMap) field.get(null);
- if (cache != null)
- {
- cache.clear();
- }
- }
- }
+ catch (Throwable t)
+ {
+ // Nothing else to try; let the caller see the failure.
+ m_accessorCache = SET_ACCESSIBLE;
}
}
+ return m_accessorCache;
}
public void invokeBundleCollisionHook(
diff --git a/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java b/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java
new file mode 100644
index 0000000000..b784ca11c5
--- /dev/null
+++ b/framework/src/test/java/org/apache/felix/framework/PlurlURLHandlersTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.framework;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.felix.framework.plurl.Plurl;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.osgi.framework.Constants;
+
+/**
+ * Verifies that a running framework registers its URL handling with plurl
+ * (FELIX-6759), rather than taking over the java.net.URL singletons by swapping a
+ * private static field.
+ */
+class PlurlURLHandlersTest
+{
+ private Felix m_felix;
+ private File m_cacheDir;
+
+ @BeforeEach
+ void setUp() throws Exception
+ {
+ m_cacheDir = File.createTempFile("felix-cache", ".dir");
+ m_cacheDir.delete();
+ m_cacheDir.mkdirs();
+
+ Map params = new HashMap<>();
+ params.put(Constants.FRAMEWORK_SYSTEMPACKAGES,
+ "org.osgi.framework; version=1.4.0,"
+ + "org.osgi.service.packageadmin; version=1.2.0,"
+ + "org.osgi.service.startlevel; version=1.1.0,"
+ + "org.osgi.util.tracker; version=1.3.3,"
+ + "org.osgi.service.url; version=1.0.0");
+ params.put(Constants.FRAMEWORK_STORAGE, m_cacheDir.getAbsolutePath());
+
+ m_felix = new Felix(params);
+ m_felix.init();
+ m_felix.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception
+ {
+ if (m_felix != null)
+ {
+ m_felix.stop();
+ m_felix.waitForStop(10000);
+ }
+ deleteDir(m_cacheDir);
+ }
+
+ /**
+ * The framework must have registered a factory with the plurl router while
+ * starting. Before FELIX-6759 nothing called Plurl.install(..), so no registration
+ * existed at all.
+ */
+ @Test
+ void registersWithPlurlOnStart()
+ {
+ assertThat(URLHandlers.getPlurlHandlers(m_felix))
+ .as("framework should have registered its URL handling with plurl")
+ .isNotNull();
+ }
+
+ /**
+ * Installing plurl is what makes the plurl: protocol resolvable, so being able to
+ * construct such a URL proves the router really took over the JVM factory rather
+ * than the registration silently failing.
+ */
+ @Test
+ void plurlRouterIsInstalledInTheJvm() throws Exception
+ {
+ URL url = new URL("plurl", "op", "plurlForbidNothing");
+ assertThat(url.getProtocol()).isEqualTo("plurl");
+ }
+
+ /**
+ * shouldHandle must claim only classes belonging to this framework instance.
+ * Claiming any Felix bundle would mean one framework answering lookups for a
+ * bundle resolved in another framework in the same JVM.
+ */
+ @Test
+ void shouldHandleOnlyClaimsThisFrameworksClasses()
+ {
+ PlurlURLHandlers handlers = URLHandlers.getPlurlHandlers(m_felix);
+ assertThat(handlers).isNotNull();
+
+ // Not loaded by any bundle class loader.
+ assertThat(handlers.shouldHandle(String.class))
+ .as("JDK classes are not owned by a framework").isFalse();
+ assertThat(handlers.shouldHandle(getClass()))
+ .as("test classes are not loaded from a bundle").isFalse();
+ assertThat(handlers.shouldHandle((Class>) null))
+ .as("null must not be claimed").isFalse();
+ }
+
+ /**
+ * The router that routes in this JVM must be one that consults
+ * shouldHandle(protocol, spec). Felix warns at startup when it is not, because
+ * bundle: URLs are then routed to whichever factory registered first; this test
+ * pins the capability so that re-vendoring an older plurl fails here rather than
+ * silently turning that warning on for every user.
+ */
+ @Test
+ void installedPlurlSupportsSelectionBySpec()
+ {
+ assertThat(Plurl.getCapability(Plurl.PLURL_CAPABILITY_SELECT_BY_SPEC))
+ .as("the installed plurl reports what it supports")
+ .contains(Boolean.TRUE);
+ }
+
+ /**
+ * shouldHandleURL(protocol, spec) must claim only this framework's bundle: URLs.
+ * The framework UUID is in the host, and it is the only thing that identifies an
+ * owner when the URL is parsed by a caller that is in no bundle.
+ */
+ @Test
+ void shouldHandleOnlyClaimsThisFrameworksBundleUrls()
+ {
+ PlurlURLHandlers handlers = URLHandlers.getPlurlHandlers(m_felix);
+ assertThat(handlers).isNotNull();
+
+ String uuid = m_felix._getProperty(Constants.FRAMEWORK_UUID);
+ assertThat(handlers.shouldHandleURL("bundle", "bundle://" + uuid + "_1.0/resource"))
+ .as("this framework's own URL").isTrue();
+ assertThat(handlers.shouldHandleURL("bundle", "bundle://" + uuid + "_1.0:0/resource"))
+ .as("host may carry a port").isTrue();
+ assertThat(handlers.shouldHandleURL("bundle", "bundle://someotherframework_1.0/resource"))
+ .as("another framework's URL must not be claimed").isFalse();
+ assertThat(handlers.shouldHandleURL("http", "http://" + uuid + "_1.0/resource"))
+ .as("only the bundle protocol is claimed").isFalse();
+ assertThat(handlers.shouldHandleURL("bundle", "bundle:relative/resource"))
+ .as("a spec with no host cannot be claimed").isFalse();
+ assertThat(handlers.shouldHandleURL("bundle", null))
+ .as("null must not be claimed").isFalse();
+ }
+
+ private static void deleteDir(File root)
+ {
+ if (root == null || !root.exists())
+ {
+ return;
+ }
+ File[] children = root.listFiles();
+ if (children != null)
+ {
+ for (File child : children)
+ {
+ deleteDir(child);
+ }
+ }
+ root.delete();
+ }
+}