This is an automated email from the ASF dual-hosted git repository.
ctubbsii pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo-classloaders.git
The following commit(s) were added to refs/heads/main by this push:
new 438b16b Replace resolvers with agnostic URL support (#51)
438b16b is described below
commit 438b16b54f645240b97b99888a71d71810cdea8b
Author: Christopher Tubbs <[email protected]>
AuthorDate: Wed Jan 21 20:58:05 2026 -0500
Replace resolvers with agnostic URL support (#51)
Closes #47
* Register new URLStreamHandlerProvider
* Register a provider for the hdfs: URL stream handling, rather than
override the system factory
* Also fix SpotBugs and rename classes to match the naming convention
from the classes they override (e.g. "URLStreamHandler" instead of
"UrlStreamHandler")
* Remove unneeded call to constructor in test
* Replace resolvers with agnostic URL support
* Remove resolvers (keep test coverage for file, http, and hdfs URL
types)
* Use url.openStream()
* Be agnostic to different URL types
* Depend on an HdfsURLStreamHandlerProvider for hdfs URL support
* Place hdfs provider in its own module/jar
* Clean up related POM and README stuff
This fixes #47
This supersedes #48 and #49 alternate fixes
* Remove redundant precondition
* Fix test and remove redundant log
* Make the DeduplicationCacheTest more strict, and keep a strong
reference in a background thread, so the weak values don't get garbage
collected too early
* Use Object to test DeduplicationCache, since the values are not
actually used
* Move a log message to avoid a redundant message about discontinued
monitoring of a ContextDefinition location
* Be less aggressive with System.gc in test
---------
Co-authored-by: Dave Marion <[email protected]>
---
modules/hdfs-urlstreamhandler-provider/.gitignore | 36 ++++++++
modules/hdfs-urlstreamhandler-provider/README.md | 31 +++++++
modules/hdfs-urlstreamhandler-provider/pom.xml | 62 ++++++++++++++
.../HdfsURLStreamHandlerProvider.java | 96 ++++++++++++++++++++++
modules/local-caching-classloader/README.md | 13 ++-
modules/local-caching-classloader/pom.xml | 33 ++++----
.../lcc/LocalCachingContextClassLoaderFactory.java | 11 +--
.../lcc/definition/ContextDefinition.java | 17 ++--
.../classloader/lcc/definition/Resource.java | 13 +++
.../classloader/lcc/resolvers/FileResolver.java | 80 ------------------
.../lcc/resolvers/HdfsFileResolver.java | 60 --------------
.../lcc/resolvers/HttpFileResolver.java | 45 ----------
.../lcc/resolvers/LocalFileResolver.java | 62 --------------
.../accumulo/classloader/lcc/util/LocalStore.java | 21 +++--
.../LocalCachingContextClassLoaderFactoryTest.java | 41 +++++----
.../MiniAccumuloClusterClassLoaderFactoryTest.java | 8 +-
.../apache/accumulo/classloader/lcc/TestUtils.java | 18 +++-
.../FileResolversTest.java => URLTypesTest.java} | 59 ++++---------
.../lcc/util/DeduplicationCacheTest.java | 68 ++++++++-------
.../classloader/lcc/util/LocalStoreTest.java | 70 +++++++++-------
modules/vfs-class-loader/pom.xml | 19 +++++
pom.xml | 12 ++-
22 files changed, 451 insertions(+), 424 deletions(-)
diff --git a/modules/hdfs-urlstreamhandler-provider/.gitignore
b/modules/hdfs-urlstreamhandler-provider/.gitignore
new file mode 100644
index 0000000..55d7f58
--- /dev/null
+++ b/modules/hdfs-urlstreamhandler-provider/.gitignore
@@ -0,0 +1,36 @@
+#
+# 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
+#
+# https://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.
+#
+
+# Maven ignores
+/target/
+
+# IDE ignores
+/.settings/
+/.project
+/.classpath
+/.pydevproject
+/.idea
+/*.iml
+/*.ipr
+/*.iws
+/nbproject/
+/nbactions.xml
+/nb-configuration.xml
+/.vscode/
+/.factorypath
diff --git a/modules/hdfs-urlstreamhandler-provider/README.md
b/modules/hdfs-urlstreamhandler-provider/README.md
new file mode 100644
index 0000000..a3b27ea
--- /dev/null
+++ b/modules/hdfs-urlstreamhandler-provider/README.md
@@ -0,0 +1,31 @@
+<!--
+
+ 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
+
+ https://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.
+
+-->
+# URLStreamHandlerProvider
+
+This library contains a single class that implements
+[URLStreamHandlerProvider][1], to provide a stream handler for `hdfs:` URL
+types that uses a default Hadoop `Configuration` from your class path. This can
+be placed on your classpath to automatically be registered using the Java
+`ServiceLoader` when handling `hdfs:` URLs. This libary is unnecessary if
+another library on your classpath provides an equivalent handler.
+
+
+[1]:
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/spi/URLStreamHandlerProvider.html
diff --git a/modules/hdfs-urlstreamhandler-provider/pom.xml
b/modules/hdfs-urlstreamhandler-provider/pom.xml
new file mode 100644
index 0000000..fb6114d
--- /dev/null
+++ b/modules/hdfs-urlstreamhandler-provider/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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
+
+ https://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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.accumulo</groupId>
+ <artifactId>classloader-extras</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <artifactId>hdfs-urlstreamhandler-provider</artifactId>
+ <name>HDFS URLStreamHandlerProvider</name>
+ <dependencies>
+ <dependency>
+ <!-- needed for build checks, but not for runtime -->
+ <groupId>com.github.spotbugs</groupId>
+ <artifactId>spotbugs-annotations</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <!-- needed for annotation processor during compile, but not after -->
+ <groupId>com.google.auto.service</groupId>
+ <artifactId>auto-service</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-client-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <!-- provided by accumulo -->
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git
a/modules/hdfs-urlstreamhandler-provider/src/main/java/org/apache/accumulo/classloader/hdfsurlstreamhandlerprovider/HdfsURLStreamHandlerProvider.java
b/modules/hdfs-urlstreamhandler-provider/src/main/java/org/apache/accumulo/classloader/hdfsurlstreamhandlerprovider/HdfsURLStreamHandlerProvider.java
new file mode 100644
index 0000000..aff19e1
--- /dev/null
+++
b/modules/hdfs-urlstreamhandler-provider/src/main/java/org/apache/accumulo/classloader/hdfsurlstreamhandlerprovider/HdfsURLStreamHandlerProvider.java
@@ -0,0 +1,96 @@
+/*
+ * 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
+ *
+ * https://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.accumulo.classloader.hdfsurlstreamhandlerprovider;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.spi.URLStreamHandlerProvider;
+import java.util.function.Supplier;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.auto.service.AutoService;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Suppliers;
+
+@AutoService(URLStreamHandlerProvider.class)
+public class HdfsURLStreamHandlerProvider extends URLStreamHandlerProvider {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(HdfsURLStreamHandlerProvider.class);
+
+ private static class HdfsURLConnection extends URLConnection {
+
+ private final Supplier<Configuration> conf =
Suppliers.memoize(Configuration::new);
+
+ private InputStream is;
+
+ public HdfsURLConnection(URL url) {
+ super(requireNonNull(url, "null url argument"));
+ }
+
+ @Override
+ public void connect() throws IOException {
+ Preconditions.checkState(is == null, "Already connected");
+ LOG.debug("Connecting to {}", url);
+ final URI uri;
+ try {
+ uri = url.toURI();
+ } catch (URISyntaxException e) {
+ // this came from a URL that should be RFC2396-compliant, so it
shouldn't happen
+ throw new IllegalArgumentException(e);
+ }
+ var fs = FileSystem.get(uri, conf.get());
+ is = fs.open(new Path(uri));
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ if (is == null) {
+ connect();
+ }
+ return is;
+ }
+ }
+
+ private static class HdfsURLStreamHandler extends URLStreamHandler {
+ @Override
+ protected HdfsURLConnection openConnection(URL url) throws IOException {
+ return new HdfsURLConnection(url);
+ }
+ }
+
+ private final Supplier<URLStreamHandler> handler =
Suppliers.memoize(HdfsURLStreamHandler::new);
+
+ @Override
+ public URLStreamHandler createURLStreamHandler(String protocol) {
+ return protocol.equals("hdfs") ? handler.get() : null;
+ }
+
+}
diff --git a/modules/local-caching-classloader/README.md
b/modules/local-caching-classloader/README.md
index cb5589c..8aecacf 100644
--- a/modules/local-caching-classloader/README.md
+++ b/modules/local-caching-classloader/README.md
@@ -47,8 +47,12 @@ This context definition file must then be stored somewhere
where this factory
can download it, and use the URL to that context definition file as the
`context` parameter for this factory's `getClassLoader(String context)` method.
-This factory can handle context and resource URLs that use the `file`, `hdfs`,
-`http`, or `https` URL scheme.
+This factory can handle context and resource URLs of any type that are
+supported by your application via a registered [URLStreamHandlerProvider][1],
+such as the built-in `file:` and `http:` types. A provider that handles `hdfs:`
+URL types must be provided by the user. This may be provided by the Apache
+Hadoop project, or by another library. A reference implementation is available
+[elsewhere in this project][2].
Here is an example context definition file:
@@ -210,6 +214,9 @@ To use this with Accumulo:
1. Set the following Accumulo properties:
*
`general.context.class.loader.factory=org.apache.accumulo.classloader.lcc.LocalCachingContextClassLoaderFactory`
* `general.custom.classloader.lcc.cache.dir=file://path/to/some/directory`
-2. Set the following table property:
+2. Set the following table property to link to a context definition file. For
example:
*
`table.class.loader.context=(file|hdfs|http|https)://path/to/context/definition.json`
+
+[1]:
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/spi/URLStreamHandlerProvider.html
+[2]:
https://github.com/apache/accumulo-classloaders/tree/main/modules/hdfs-urlstreamhandler-provider
diff --git a/modules/local-caching-classloader/pom.xml
b/modules/local-caching-classloader/pom.xml
index 0428499..5f0ad98 100644
--- a/modules/local-caching-classloader/pom.xml
+++ b/modules/local-caching-classloader/pom.xml
@@ -29,21 +29,6 @@
</parent>
<artifactId>local-caching-classloader</artifactId>
<name>classloader-extras-local-caching</name>
- <properties>
- <accumulo.build.extraTestArgs>--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED --add-opens
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens
java.management/sun.management=ALL-UNNAMED --add-opens
java.base/java.security=ALL-UNNAMED --add-opens
java.base/java.lang.reflect=ALL-UNNAMED --add-opens
java.base/java.util.concurrent=ALL-UNNAMED --add-opens ja [...]
-
<eclipseFormatterStyle>../../src/build/eclipse-codestyle.xml</eclipseFormatterStyle>
- <failsafe.excludedGroups />
- <failsafe.failIfNoSpecifiedTests>false</failsafe.failIfNoSpecifiedTests>
- <failsafe.forkCount>1</failsafe.forkCount>
- <failsafe.groups />
- <failsafe.reuseForks>false</failsafe.reuseForks>
-
<maven.test.redirectTestOutputToFile>true</maven.test.redirectTestOutputToFile>
- <surefire.excludedGroups />
- <surefire.failIfNoSpecifiedTests>false</surefire.failIfNoSpecifiedTests>
- <surefire.forkCount>1</surefire.forkCount>
- <surefire.groups />
- <surefire.reuseForks>false</surefire.reuseForks>
- </properties>
<dependencies>
<dependency>
<!-- needed for build checks, but not for runtime -->
@@ -110,6 +95,12 @@
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.accumulo</groupId>
+ <artifactId>hdfs-urlstreamhandler-provider</artifactId>
+ <scope>runtime</scope>
+ <optional>true</optional>
+ </dependency>
<dependency>
<groupId>org.apache.accumulo</groupId>
<artifactId>accumulo-minicluster</artifactId>
@@ -181,6 +172,18 @@
</artifactItems>
</configuration>
</execution>
+ <execution>
+ <id>analyze</id>
+ <goals>
+ <goal>analyze-only</goal>
+ </goals>
+ <configuration>
+ <ignoredUnusedDeclaredDependencies combine.children="append">
+ <!-- ignore false positive runtime dependencies -->
+
<unused>org.apache.accumulo:hdfs-urlstreamhandler-provider:*</unused>
+ </ignoredUnusedDeclaredDependencies>
+ </configuration>
+ </execution>
</executions>
</plugin>
<plugin>
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java
index 2c185a6..13d6a38 100644
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java
+++
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactory.java
@@ -71,8 +71,8 @@ import com.google.common.base.Stopwatch;
* discontinue until the next use of that context. Each resource is defined by
a URL to the file and
* an expected SHA-256 checksum.
* <p>
- * The URLs supplied for the context definition file and for the resources can
use one of the
- * following URL schemes: file, http, https, or hdfs.
+ * The URLs supplied for the context definition file and for the resources may
use any URL type with
+ * a registered provider in your application, such as: file, http, https, or
hdfs.
* <p>
* As this class processes the ContextDefinition, it fetches the contents of
the resource from the
* resource URL and caches it in a directory on the local filesystem. This
class uses the value of
@@ -249,6 +249,10 @@ public class LocalCachingContextClassLoaderFactory
implements ContextClassLoader
ContextDefinition currentDef =
contextDefs.compute(contextLocation, (contextLocationKey,
previousDefinition) -> {
if (previousDefinition == null) {
+ // context has been removed from the map, no need to check for
update
+ LOG.debug(
+ "ContextDefinition for context {} not present, no longer
monitoring for changes",
+ contextLocation);
return null;
}
// check for any classloaders still in the cache that were created
for a context
@@ -262,9 +266,6 @@ public class LocalCachingContextClassLoaderFactory
implements ContextClassLoader
return previousDefinition;
});
if (currentDef == null) {
- // context has been removed from the map, no need to check for update
- LOG.debug("ContextDefinition for context {} not present, no longer
monitoring for changes",
- contextLocation);
return;
}
long nextInterval = interval;
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java
index 8c13fee..4278e25 100644
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java
+++
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/ContextDefinition.java
@@ -36,11 +36,8 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
-import org.apache.accumulo.classloader.lcc.resolvers.FileResolver;
import org.apache.accumulo.core.cli.Help;
import org.apache.accumulo.start.spi.KeywordExecutable;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import com.beust.jcommander.Parameter;
import com.google.auto.service.AutoService;
@@ -49,6 +46,8 @@ import com.google.common.base.Suppliers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
@AutoService(KeywordExecutable.class)
public class ContextDefinition implements KeywordExecutable {
@@ -66,12 +65,13 @@ public class ContextDefinition implements KeywordExecutable
{
private static final Gson GSON =
new GsonBuilder().disableJdkUnsafe().setPrettyPrinting().create();
+ @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
+ justification = "user-supplied URL is the intended functionality")
public static ContextDefinition create(int monitorIntervalSecs, URL...
sources)
throws IOException {
LinkedHashSet<Resource> resources = new LinkedHashSet<>();
for (URL u : sources) {
- FileResolver resolver = FileResolver.resolve(u);
- try (InputStream is = resolver.getInputStream()) {
+ try (InputStream is = u.openStream()) {
String checksum = DIGESTER.digestAsHex(is);
resources.add(new Resource(u, checksum));
}
@@ -79,9 +79,10 @@ public class ContextDefinition implements KeywordExecutable {
return new ContextDefinition(monitorIntervalSecs, resources);
}
+ @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
+ justification = "user-supplied URL is the intended functionality")
public static ContextDefinition fromRemoteURL(final URL url) throws
IOException {
- final FileResolver resolver = FileResolver.resolve(url);
- try (InputStream is = resolver.getInputStream()) {
+ try (InputStream is = url.openStream()) {
var def = GSON.fromJson(new InputStreamReader(is, UTF_8),
ContextDefinition.class);
if (def == null) {
throw new EOFException("InputStream does not contain a valid
ContextDefinition at " + url);
@@ -160,8 +161,6 @@ public class ContextDefinition implements KeywordExecutable
{
@Override
public void execute(String[] args) throws Exception {
- URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory(new
Configuration()));
-
Opts opts = new Opts();
opts.parseArgs(ContextDefinition.class.getName(), args);
URL[] urls = new URL[opts.files.size()];
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java
index d31694b..70afdde 100644
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java
+++
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/definition/Resource.java
@@ -19,6 +19,7 @@
package org.apache.accumulo.classloader.lcc.definition;
import java.net.URL;
+import java.nio.file.Path;
import java.util.Objects;
public class Resource {
@@ -37,6 +38,18 @@ public class Resource {
return location;
}
+ public String getFileName() {
+ var nameAsPath = Path.of(location.getPath()).getFileName();
+ if (nameAsPath == null) {
+ return "unknown";
+ }
+ String name = nameAsPath.toString();
+ if (name.isBlank()) {
+ return "unknown";
+ }
+ return name;
+ }
+
public String getChecksum() {
return checksum;
}
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java
deleted file mode 100644
index e32db52..0000000
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolver.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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
- *
- * https://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.accumulo.classloader.lcc.resolvers;
-
-import static java.util.Objects.hash;
-import static java.util.Objects.requireNonNull;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.Objects;
-
-public abstract class FileResolver {
-
- public static FileResolver resolve(URL url) throws IOException {
- requireNonNull(url, "URL must be supplied");
- switch (url.getProtocol()) {
- case "http":
- case "https":
- return new HttpFileResolver(url);
- case "file":
- return new LocalFileResolver(url);
- case "hdfs":
- return new HdfsFileResolver(url);
- default:
- throw new IOException("Unhandled protocol: " + url.getProtocol());
- }
- }
-
- private final URL url;
-
- protected FileResolver(URL url) {
- this.url = url;
- }
-
- protected URL getURL() {
- return this.url;
- }
-
- public abstract String getFileName();
-
- public abstract InputStream getInputStream() throws IOException;
-
- @Override
- public int hashCode() {
- return hash(url);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- FileResolver other = (FileResolver) obj;
- return Objects.equals(url, other.url);
- }
-
-}
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java
deleted file mode 100644
index 6eda92d..0000000
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HdfsFileResolver.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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
- *
- * https://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.accumulo.classloader.lcc.resolvers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.Path;
-
-public final class HdfsFileResolver extends FileResolver {
-
- private final Configuration hadoopConf = new Configuration();
- private final FileSystem fs;
- private final Path path;
-
- protected HdfsFileResolver(URL url) throws IOException {
- super(url);
- try {
- final URI uri = url.toURI();
- this.fs = FileSystem.get(uri, hadoopConf);
- this.path = fs.makeQualified(new Path(uri));
- if (!fs.exists(this.path)) {
- throw new IOException("File: " + url + " does not exist.");
- }
- } catch (URISyntaxException e) {
- throw new IOException("Error creating URI from url: " + url, e);
- }
- }
-
- @Override
- public String getFileName() {
- return this.path.getName();
- }
-
- @Override
- public InputStream getInputStream() throws IOException {
- return fs.open(path);
- }
-}
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java
deleted file mode 100644
index 68da39c..0000000
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/HttpFileResolver.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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
- *
- * https://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.accumulo.classloader.lcc.resolvers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-public final class HttpFileResolver extends FileResolver {
-
- protected HttpFileResolver(URL url) throws IOException {
- super(url);
- }
-
- @Override
- public String getFileName() {
- String path = getURL().getPath();
- return path.substring(path.lastIndexOf("/") + 1);
- }
-
- @Override
- @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
- justification = "user-supplied URL is the intended functionality")
- public InputStream getInputStream() throws IOException {
- return getURL().openStream();
- }
-}
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java
deleted file mode 100644
index ece5914..0000000
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/resolvers/LocalFileResolver.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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
- *
- * https://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.accumulo.classloader.lcc.resolvers;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-public final class LocalFileResolver extends FileResolver {
-
- private final File file;
-
- public LocalFileResolver(URL url) throws IOException {
- super(url);
- if (url.getHost() != null && !url.getHost().isBlank()) {
- throw new IOException(
- "Unsupported file url, only local files are supported. host = " +
url.getHost());
- }
- try {
- final URI uri = url.toURI();
- final Path path = Path.of(uri);
- if (Files.notExists(Path.of(uri))) {
- throw new IOException("File: " + url + " does not exist.");
- }
- file = path.toFile();
- } catch (URISyntaxException e) {
- throw new IOException("Error creating URI from url: " + url, e);
- }
- }
-
- @Override
- public String getFileName() {
- return file.getName();
- }
-
- @Override
- public InputStream getInputStream() throws IOException {
- return new BufferedInputStream(Files.newInputStream(file.toPath()));
- }
-}
diff --git
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LocalStore.java
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LocalStore.java
index 96e1ed5..329fac4 100644
---
a/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LocalStore.java
+++
b/modules/local-caching-classloader/src/main/java/org/apache/accumulo/classloader/lcc/util/LocalStore.java
@@ -50,10 +50,11 @@ import java.util.regex.Pattern;
import org.apache.accumulo.classloader.lcc.definition.ContextDefinition;
import org.apache.accumulo.classloader.lcc.definition.Resource;
-import org.apache.accumulo.classloader.lcc.resolvers.FileResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
/**
* A simple storage service backed by a local file system for storing
downloaded
* {@link ContextDefinition} files and the {@link Resource} objects it
references.
@@ -112,9 +113,10 @@ public final class LocalStore {
// extension, and will instead just append the checksum to the original file
name
private static Pattern fileNamesWithExtensionPattern =
Pattern.compile("^(.*[^.].*)[.]([^.]+)$");
- public static String localResourceName(String remoteFileName, String
checksum) {
- requireNonNull(remoteFileName);
- requireNonNull(checksum);
+ public static String localResourceName(Resource r) {
+ requireNonNull(r);
+ String remoteFileName = r.getFileName();
+ String checksum = r.getChecksum();
var matcher = fileNamesWithExtensionPattern.matcher(remoteFileName);
if (matcher.matches()) {
return String.format("%s-%s.%s", matcher.group(1), checksum,
matcher.group(2));
@@ -195,8 +197,7 @@ public final class LocalStore {
*/
private Path storeResource(final Resource resource) throws IOException {
final URL url = resource.getLocation();
- final FileResolver source = FileResolver.resolve(url);
- final String baseName = localResourceName(source.getFileName(),
resource.getChecksum());
+ final String baseName = localResourceName(resource);
final Path destinationPath = resourcesDir.resolve(baseName);
final Path tempPath = resourcesDir.resolve(tempName(baseName));
final Path downloadingProgressPath = resourcesDir.resolve("." + baseName +
".downloading");
@@ -232,7 +233,7 @@ public final class LocalStore {
return null;
}
- var task = new FutureTask<Void>(() -> downloadFile(source, tempPath,
resource), null);
+ var task = new FutureTask<Void>(() -> downloadFile(tempPath, resource),
null);
var t = new Thread(task);
t.setDaemon(true);
t.setName("downloading " + url + " to " + tempPath);
@@ -292,11 +293,13 @@ public final class LocalStore {
private static final int DL_BUFF_SIZE = 1024 * 1024;
- private void downloadFile(FileResolver source, Path tempPath, Resource
resource) {
+ @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
+ justification = "user-supplied URL is the intended functionality")
+ private void downloadFile(Path tempPath, Resource resource) {
// CREATE_NEW ensures the temporary file name is unique for this attempt
// SYNC ensures file integrity on each write, in case of system failure.
Buffering minimizes
// system calls te read/write data which minimizes the number of syncs.
- try (var in = new BufferedInputStream(source.getInputStream(),
DL_BUFF_SIZE);
+ try (var in = new BufferedInputStream(resource.getLocation().openStream(),
DL_BUFF_SIZE);
var out = new BufferedOutputStream(Files.newOutputStream(tempPath,
CREATE_NEW, WRITE, SYNC),
DL_BUFF_SIZE)) {
in.transferTo(out);
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java
index 47bd6d7..8a0cb31 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/LocalCachingContextClassLoaderFactoryTest.java
@@ -33,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.fail;
import java.io.EOFException;
import java.io.File;
-import java.io.IOException;
+import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -52,13 +52,11 @@ import java.util.stream.Collectors;
import org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo;
import org.apache.accumulo.classloader.lcc.definition.ContextDefinition;
import org.apache.accumulo.classloader.lcc.definition.Resource;
-import org.apache.accumulo.classloader.lcc.resolvers.FileResolver;
import org.apache.accumulo.classloader.lcc.util.LocalStore;
import org.apache.accumulo.core.conf.ConfigurationCopy;
import
org.apache.accumulo.core.spi.common.ContextClassLoaderFactory.ContextClassLoaderException;
import org.apache.accumulo.core.util.ConfigurationImpl;
import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.AfterAll;
@@ -115,7 +113,6 @@ public class LocalCachingContextClassLoaderFactoryTest {
// Put B into HDFS
hdfs = TestUtils.getMiniCluster();
- URL.setURLStreamHandlerFactory(new
FsUrlStreamHandlerFactory(hdfs.getConfiguration(0)));
fs = hdfs.getFileSystem();
assertTrue(fs.mkdirs(new org.apache.hadoop.fs.Path("/contextB")));
@@ -277,7 +274,16 @@ public class LocalCachingContextClassLoaderFactoryTest {
var ex = assertThrows(ContextClassLoaderException.class,
() -> FACTORY.getClassLoader(initialDefUrl.toString()));
- assertTrue(ex.getMessage().endsWith("jarACopy.jar does not exist."),
ex::getMessage);
+ boolean foundExpectedException = false;
+ var cause = ex.getCause();
+ do {
+ foundExpectedException =
+ cause instanceof FileNotFoundException &&
cause.getMessage().contains("jarACopy.jar");
+ if (cause != null) {
+ cause = cause.getCause();
+ }
+ } while (!foundExpectedException && cause != null);
+ assertTrue(foundExpectedException, "Could not find expected
FileNotFoundException");
}
@Test
@@ -714,8 +720,16 @@ public class LocalCachingContextClassLoaderFactoryTest {
var ex = assertThrows(ContextClassLoaderException.class,
() -> localFactory.getClassLoader(updateDefUrl.toString()));
- assertTrue(ex.getMessage().endsWith("jarACopy.jar does not exist."),
ex::getMessage);
-
+ boolean foundExpectedException = false;
+ var cause = ex.getCause();
+ do {
+ foundExpectedException =
+ cause instanceof FileNotFoundException &&
cause.getMessage().contains("jarACopy.jar");
+ if (cause != null) {
+ cause = cause.getCause();
+ }
+ } while (!foundExpectedException && cause != null);
+ assertTrue(foundExpectedException, "Could not find expected
FileNotFoundException");
}
@Test
@@ -733,16 +747,9 @@ public class LocalCachingContextClassLoaderFactoryTest {
testClassLoads(cl, classD);
var resources = tempDir.resolve("base").resolve("resources");
- List<Path> files = def.getResources().stream().map(r -> {
- String basename;
- try {
- basename = FileResolver.resolve(r.getLocation()).getFileName();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- var checksum = r.getChecksum();
- return resources.resolve(LocalStore.localResourceName(basename,
checksum));
- }).limit(2).collect(Collectors.toList());
+ List<Path> files =
+ def.getResources().stream().map(r ->
resources.resolve(LocalStore.localResourceName(r)))
+ .limit(2).collect(Collectors.toList());
assertEquals(2, files.size());
// overwrite one downloaded jar with others content
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java
index 07909eb..b364354 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/MiniAccumuloClusterClassLoaderFactoryTest.java
@@ -60,7 +60,6 @@ import javax.management.remote.JMXServiceURL;
import org.apache.accumulo.classloader.lcc.definition.ContextDefinition;
import org.apache.accumulo.classloader.lcc.definition.Resource;
import org.apache.accumulo.classloader.lcc.jmx.ContextClassLoadersMXBean;
-import org.apache.accumulo.classloader.lcc.resolvers.FileResolver;
import org.apache.accumulo.classloader.lcc.util.LocalStore;
import org.apache.accumulo.core.client.Accumulo;
import org.apache.accumulo.core.client.AccumuloClient;
@@ -167,8 +166,7 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
assertTrue(Files.exists(testContextDefFile.toPath()));
Resource jarAResource = testContextDef.getResources().iterator().next();
- String jarALocalFileName = LocalStore.localResourceName(
- FileResolver.resolve(jarAResource.getLocation()).getFileName(),
jarAResource.getChecksum());
+ String jarALocalFileName = LocalStore.localResourceName(jarAResource);
final String[] names = this.getUniqueNames(1);
try (AccumuloClient client =
@@ -254,9 +252,7 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
assertTrue(Files.exists(testContextDefFile.toPath()));
Resource jarBResource =
testContextDefUpdate.getResources().iterator().next();
- String jarBLocalFileName = LocalStore.localResourceName(
- FileResolver.resolve(jarBResource.getLocation()).getFileName(),
- jarBResource.getChecksum());
+ String jarBLocalFileName = LocalStore.localResourceName(jarBResource);
// Wait 2x the monitor interval
Thread.sleep(2 * MONITOR_INTERVAL_SECS * 1000);
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java
index ca0043f..d625a68 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/TestUtils.java
@@ -30,8 +30,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
@@ -173,9 +176,18 @@ public class TestUtils {
}
}
- public static String getFileName(URL url) {
- String path = url.getPath();
- return path.substring(path.lastIndexOf("/") + 1);
+ @SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
+ justification = "user-supplied URL is the intended functionality")
+ public static long getFileSize(URL url) throws IOException {
+ try (InputStream is = url.openStream()) {
+ return IOUtils.consume(is);
+ }
+ }
+ public static long getFileSize(Path p) throws IOException {
+ try (InputStream is = Files.newInputStream(p, StandardOpenOption.READ)) {
+ return IOUtils.consume(is);
+ }
}
+
}
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/URLTypesTest.java
similarity index 55%
rename from
modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java
rename to
modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/URLTypesTest.java
index 8be9907..54a122f 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/resolvers/FileResolversTest.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/URLTypesTest.java
@@ -16,74 +16,51 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.accumulo.classloader.lcc.resolvers;
+package org.apache.accumulo.classloader.lcc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
-import java.io.InputStream;
import java.net.URL;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import org.apache.accumulo.classloader.lcc.TestUtils;
-import org.apache.commons.io.IOUtils;
import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class FileResolversTest {
-
- private static final Logger LOG =
LoggerFactory.getLogger(FileResolversTest.class);
-
- private long getFileSize(Path p) throws IOException {
- try (InputStream is = Files.newInputStream(p, StandardOpenOption.READ)) {
- return IOUtils.consume(is);
- }
- }
+/**
+ * Test a variety of URL types. Specifically, test file:, http:, and hdfs:
types.
+ */
+public class URLTypesTest {
- private long getFileSize(FileResolver resolver) throws IOException {
- try (InputStream is = resolver.getInputStream()) {
- return IOUtils.consume(is);
- }
- }
+ private static final Logger LOG =
LoggerFactory.getLogger(URLTypesTest.class);
@Test
public void testLocalFile() throws Exception {
- URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar");
+ URL jarPath = URLTypesTest.class.getResource("/HelloWorld.jar");
assertNotNull(jarPath);
var p = Path.of(jarPath.toURI());
- final long origFileSize = getFileSize(p);
- FileResolver resolver = FileResolver.resolve(jarPath);
- assertTrue(resolver instanceof LocalFileResolver);
- assertEquals(jarPath, resolver.getURL());
- assertEquals("HelloWorld.jar", resolver.getFileName());
- assertEquals(origFileSize, getFileSize(resolver));
+ final long origFileSize = TestUtils.getFileSize(p);
+ assertEquals(origFileSize, TestUtils.getFileSize(jarPath));
}
@Test
public void testHttpFile() throws Exception {
- URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar");
+ URL jarPath = URLTypesTest.class.getResource("/HelloWorld.jar");
assertNotNull(jarPath);
var p = Path.of(jarPath.toURI());
- final long origFileSize = getFileSize(p);
+ final long origFileSize = TestUtils.getFileSize(p);
Server jetty = TestUtils.getJetty(p.getParent());
LOG.debug("Jetty listening at: {}", jetty.getURI());
URL httpPath = jetty.getURI().resolve("HelloWorld.jar").toURL();
- FileResolver resolver = FileResolver.resolve(httpPath);
- assertTrue(resolver instanceof HttpFileResolver);
- assertEquals(httpPath, resolver.getURL());
- assertEquals("HelloWorld.jar", resolver.getFileName());
- assertEquals(origFileSize, getFileSize(resolver));
+ assertEquals(origFileSize, TestUtils.getFileSize(httpPath));
jetty.stop();
jetty.join();
@@ -92,10 +69,10 @@ public class FileResolversTest {
@Test
public void testHdfsFile() throws Exception {
- URL jarPath = FileResolversTest.class.getResource("/HelloWorld.jar");
+ URL jarPath = URLTypesTest.class.getResource("/HelloWorld.jar");
assertNotNull(jarPath);
var p = Path.of(jarPath.toURI());
- final long origFileSize = getFileSize(p);
+ final long origFileSize = TestUtils.getFileSize(p);
MiniDFSCluster cluster = TestUtils.getMiniCluster();
try {
@@ -105,16 +82,10 @@ public class FileResolversTest {
fs.copyFromLocalFile(new org.apache.hadoop.fs.Path(jarPath.toURI()),
dst);
assertTrue(fs.exists(dst));
- URL.setURLStreamHandlerFactory(new
FsUrlStreamHandlerFactory(cluster.getConfiguration(0)));
-
URL fullPath = new URL(fs.getUri().toString() + dst.toUri().toString());
LOG.info("Path to hdfs file: {}", fullPath);
- FileResolver resolver = FileResolver.resolve(fullPath);
- assertTrue(resolver instanceof HdfsFileResolver);
- assertEquals(fullPath, resolver.getURL());
- assertEquals("HelloWorld.jar", resolver.getFileName());
- assertEquals(origFileSize, getFileSize(resolver));
+ assertEquals(origFileSize, TestUtils.getFileSize(fullPath));
} catch (IOException e) {
throw new RuntimeException("Error setting up mini cluster", e);
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCacheTest.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCacheTest.java
index 723ce61..5d17fea 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCacheTest.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/DeduplicationCacheTest.java
@@ -18,57 +18,63 @@
*/
package org.apache.accumulo.classloader.lcc.util;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.net.URL;
-import java.net.URLClassLoader;
import java.time.Duration;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
import org.junit.jupiter.api.Test;
import com.github.benmanes.caffeine.cache.RemovalCause;
-import com.github.benmanes.caffeine.cache.RemovalListener;
public class DeduplicationCacheTest {
@Test
public void testCollectionNotification() throws Exception {
- final AtomicBoolean listenerCalled = new AtomicBoolean(false);
-
- final RemovalListener<String,URLClassLoader> removalListener =
- (key, value, cause) -> listenerCalled.compareAndSet(false, cause ==
RemovalCause.COLLECTED);
-
- final DeduplicationCache<String,URL[],URLClassLoader> cache = new
DeduplicationCache<>(
- LccUtils::createClassLoader, Duration.ofSeconds(5), removalListener);
-
- final URL jarAOrigLocation =
-
DeduplicationCacheTest.class.getResource("/ClassLoaderTestA/TestA.jar");
- assertNotNull(jarAOrigLocation);
-
- Thread t = new Thread(() -> cache.computeIfAbsent("TEST", () -> new URL[]
{jarAOrigLocation}));
+ final var endBackgroundThread = new CountDownLatch(1);
+ final var removalCauseQueue = new LinkedBlockingQueue<RemovalCause>();
+
+ final var cache = new DeduplicationCache<String,Object,Object>((k, p) ->
new Object(),
+ Duration.ofSeconds(1), (key, value, cause) ->
removalCauseQueue.add(cause));
+
+ var createdCacheEntry = new CountDownLatch(1);
+ Thread t = new Thread(() -> {
+ var value = cache.computeIfAbsent("TEST", () -> new Object());
+ createdCacheEntry.countDown();
+ try {
+ // hold a strong reference in the background thread long enough to
check the weak cache
+ endBackgroundThread.await();
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+ assertNotNull(value);
+ });
t.start();
- t.join();
+ createdCacheEntry.await();
- boolean exists = cache.anyMatch("TEST"::equals);
- assertTrue(exists);
+ assertTrue(cache.anyMatch("TEST"::equals));
- // sleep twice as long as the access time duration in the strong reference
cache
- Thread.sleep(10_000);
- exists = cache.anyMatch("TEST"::equals);
- assertTrue(exists); // This is true because it's coming from the weak
reference cache
+ // wait for expiration from strong cache
+ assertEquals(RemovalCause.EXPIRED, removalCauseQueue.take());
+ assertTrue(removalCauseQueue.isEmpty());
- // sleep twice as long as the access time duration in the strong reference
cache
- Thread.sleep(10_000);
- System.gc();
+ // should still exist in the weak values cache
+ assertTrue(cache.anyMatch("TEST"::equals));
+ endBackgroundThread.countDown();
- exists = cache.anyMatch("TEST"::equals);
- assertFalse(exists);
- assertTrue(listenerCalled.get());
+ t.join();
+ // wait for it to be garbage collected (checking the cache triggers
cleanup)
+ while (cache.anyMatch("TEST"::equals)) {
+ System.gc();
+ Thread.sleep(50);
+ }
+ assertEquals(RemovalCause.COLLECTED, removalCauseQueue.take());
+ assertTrue(removalCauseQueue.isEmpty());
}
}
diff --git
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/LocalStoreTest.java
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/LocalStoreTest.java
index 995ff77..86e6d94 100644
---
a/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/LocalStoreTest.java
+++
b/modules/local-caching-classloader/src/test/java/org/apache/accumulo/classloader/lcc/util/LocalStoreTest.java
@@ -40,7 +40,6 @@ import
org.apache.accumulo.classloader.lcc.TestUtils.TestClassInfo;
import org.apache.accumulo.classloader.lcc.definition.ContextDefinition;
import org.apache.accumulo.classloader.lcc.definition.Resource;
import org.apache.hadoop.fs.FileSystem;
-import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.AfterAll;
@@ -83,7 +82,6 @@ public class LocalStoreTest {
final var dst = new org.apache.hadoop.fs.Path("/contextB/TestB.jar");
fs.copyFromLocalFile(new
org.apache.hadoop.fs.Path(jarBOrigLocation.toURI()), dst);
assertTrue(fs.exists(dst));
- URL.setURLStreamHandlerFactory(new
FsUrlStreamHandlerFactory(hdfs.getConfiguration(0)));
final URL jarBNewLocation = new URL(fs.getUri().toString() +
dst.toUri().toString());
// Put C into Jetty
@@ -153,33 +151,47 @@ public class LocalStoreTest {
assertTrue(Files.exists(baseCacheDir));
}
+ private static Resource rsrc(String filename, String checksum) {
+ return new Resource() {
+ @Override
+ public String getFileName() {
+ return filename;
+ }
+
+ @Override
+ public String getChecksum() {
+ return checksum;
+ }
+ };
+ }
+
@Test
public void testLocalFileName() {
// regular war
- assertEquals("f1-chk1.war", LocalStore.localResourceName("f1.war",
"chk1"));
+ assertEquals("f1-chk1.war", LocalStore.localResourceName(rsrc("f1.war",
"chk1")));
// dotfile war
- assertEquals(".f1-chk1.war", LocalStore.localResourceName(".f1.war",
"chk1"));
+ assertEquals(".f1-chk1.war", LocalStore.localResourceName(rsrc(".f1.war",
"chk1")));
// regular jar (has multiple dots)
- assertEquals("f2-1.0-chk2.jar", LocalStore.localResourceName("f2-1.0.jar",
"chk2"));
+ assertEquals("f2-1.0-chk2.jar",
LocalStore.localResourceName(rsrc("f2-1.0.jar", "chk2")));
// dotfile jar (has multiple dots)
- assertEquals(".f2-1.0-chk2.jar",
LocalStore.localResourceName(".f2-1.0.jar", "chk2"));
+ assertEquals(".f2-1.0-chk2.jar",
LocalStore.localResourceName(rsrc(".f2-1.0.jar", "chk2")));
// regular file with no suffix
- assertEquals("f3-chk3", LocalStore.localResourceName("f3", "chk3"));
+ assertEquals("f3-chk3", LocalStore.localResourceName(rsrc("f3", "chk3")));
// weird files with trailing dots and no file suffix
- assertEquals("f4.-chk4", LocalStore.localResourceName("f4.", "chk4"));
- assertEquals("f4..-chk4", LocalStore.localResourceName("f4..", "chk4"));
- assertEquals("f4...-chk4", LocalStore.localResourceName("f4...", "chk4"));
+ assertEquals("f4.-chk4", LocalStore.localResourceName(rsrc("f4.",
"chk4")));
+ assertEquals("f4..-chk4", LocalStore.localResourceName(rsrc("f4..",
"chk4")));
+ assertEquals("f4...-chk4", LocalStore.localResourceName(rsrc("f4...",
"chk4")));
// weird dotfiles that don't really have a suffix
- assertEquals(".f5-chk5", LocalStore.localResourceName(".f5", "chk5"));
- assertEquals("..f5-chk5", LocalStore.localResourceName("..f5", "chk5"));
+ assertEquals(".f5-chk5", LocalStore.localResourceName(rsrc(".f5",
"chk5")));
+ assertEquals("..f5-chk5", LocalStore.localResourceName(rsrc("..f5",
"chk5")));
// weird files with weird dots, but do have a valid suffix
- assertEquals("f6.-chk6.jar", LocalStore.localResourceName("f6..jar",
"chk6"));
- assertEquals("f6..-chk6.jar", LocalStore.localResourceName("f6...jar",
"chk6"));
- assertEquals(".f6-chk6.jar", LocalStore.localResourceName(".f6.jar",
"chk6"));
- assertEquals("..f6-chk6.jar", LocalStore.localResourceName("..f6.jar",
"chk6"));
- assertEquals(".f6.-chk6.jar", LocalStore.localResourceName(".f6..jar",
"chk6"));
- assertEquals("..f6.-chk6.jar", LocalStore.localResourceName("..f6..jar",
"chk6"));
+ assertEquals("f6.-chk6.jar", LocalStore.localResourceName(rsrc("f6..jar",
"chk6")));
+ assertEquals("f6..-chk6.jar",
LocalStore.localResourceName(rsrc("f6...jar", "chk6")));
+ assertEquals(".f6-chk6.jar", LocalStore.localResourceName(rsrc(".f6.jar",
"chk6")));
+ assertEquals("..f6-chk6.jar",
LocalStore.localResourceName(rsrc("..f6.jar", "chk6")));
+ assertEquals(".f6.-chk6.jar",
LocalStore.localResourceName(rsrc(".f6..jar", "chk6")));
+ assertEquals("..f6.-chk6.jar",
LocalStore.localResourceName(rsrc("..f6..jar", "chk6")));
}
@Test
@@ -191,10 +203,8 @@ public class LocalStoreTest {
assertTrue(Files.exists(baseCacheDir));
assertTrue(Files.exists(baseCacheDir.resolve("contexts").resolve(def.getChecksum()
+ ".json")));
for (Resource r : def.getResources()) {
- String filename = TestUtils.getFileName(r.getLocation());
- String checksum = r.getChecksum();
- assertTrue(Files.exists(baseCacheDir.resolve("resources")
- .resolve(LocalStore.localResourceName(filename, checksum))));
+ assertTrue(
+
Files.exists(baseCacheDir.resolve("resources").resolve(LocalStore.localResourceName(r))));
}
}
@@ -237,17 +247,15 @@ public class LocalStoreTest {
assertTrue(
Files.exists(baseCacheDir.resolve("contexts").resolve(updatedDef.getChecksum()
+ ".json")));
for (Resource r : updatedDef.getResources()) {
- String filename = TestUtils.getFileName(r.getLocation());
- assertFalse(filename.contains("C"));
- String checksum = r.getChecksum();
- assertTrue(Files.exists(baseCacheDir.resolve("resources")
- .resolve(LocalStore.localResourceName(filename, checksum))));
+ assertFalse(r.getFileName().contains("C"));
+ assertTrue(
+
Files.exists(baseCacheDir.resolve("resources").resolve(LocalStore.localResourceName(r))));
}
- String filename = TestUtils.getFileName(removedResource.getLocation());
- assertTrue(filename.contains("C"), "cache location should still contain
'C'");
- assertTrue(Files.exists(baseCacheDir.resolve("resources")
- .resolve(LocalStore.localResourceName(filename,
removedResource.getChecksum()))));
+ assertTrue(removedResource.getFileName().contains("C"),
+ "cache location should still contain 'C'");
+ assertTrue(Files.exists(
+
baseCacheDir.resolve("resources").resolve(LocalStore.localResourceName(removedResource))));
final var updatedContextClassLoader = LccUtils.createClassLoader("url",
urls);
diff --git a/modules/vfs-class-loader/pom.xml b/modules/vfs-class-loader/pom.xml
index b827e4d..e4ed0af 100644
--- a/modules/vfs-class-loader/pom.xml
+++ b/modules/vfs-class-loader/pom.xml
@@ -156,6 +156,25 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>analyze</id>
+ <goals>
+ <goal>analyze-only</goal>
+ </goals>
+ <configuration>
+ <ignoredUnusedDeclaredDependencies combine.children="append">
+ <!-- ignore false positive runtime dependencies -->
+ <unused>org.apache.commons:commons-vfs2-hdfs:*</unused>
+
<unused>org.apache.httpcomponents.client5:httpclient5:*</unused>
+ </ignoredUnusedDeclaredDependencies>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/pom.xml b/pom.xml
index 0335e9d..16766a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,7 @@
<module>modules/example-iterators-b</module>
<module>modules/vfs-class-loader</module>
<module>modules/local-caching-classloader</module>
+ <module>modules/hdfs-urlstreamhandler-provider</module>
</modules>
<scm>
<connection>scm:git:https://gitbox.apache.org/repos/asf/accumulo-classloaders.git</connection>
@@ -147,6 +148,11 @@ under the License.
<type>pom</type>
<scope>import</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.accumulo</groupId>
+ <artifactId>hdfs-urlstreamhandler-provider</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
@@ -367,16 +373,14 @@ under the License.
</goals>
<configuration>
<failOnWarning>true</failOnWarning>
- <ignoredUsedUndeclaredDependencies>
+ <ignoredUsedUndeclaredDependencies combine.children="append">
<!-- auto-service-annotations is transitive via auto-service
-->
<undeclared>com.google.auto.service:auto-service-annotations:jar:*</undeclared>
</ignoredUsedUndeclaredDependencies>
- <ignoredUnusedDeclaredDependencies>
+ <ignoredUnusedDeclaredDependencies combine.children="append">
<!-- auto-service used by the compiler for annotation
processing, not by code -->
<unused>com.google.auto.service:auto-service:jar:*</unused>
<!-- ignore false positive runtime dependencies -->
- <unused>org.apache.commons:commons-vfs2-hdfs:*</unused>
-
<unused>org.apache.httpcomponents.client5:httpclient5:*</unused>
<unused>org.apache.logging.log4j:log4j-slf4j2-impl:*</unused>
<!-- spotbugs annotations may or may not be used in each
module -->
<unused>com.github.spotbugs:spotbugs-annotations:jar:*</unused>