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 0f559dd Add an optional comment field to the Manifest (#75)
0f559dd is described below
commit 0f559dd1c683a45fee8be9fc504acf871aba5b23
Author: Christopher Tubbs <[email protected]>
AuthorDate: Wed Feb 18 17:31:04 2026 -0500
Add an optional comment field to the Manifest (#75)
* Test that the optional comment field can be completely omitted from
the JSON
* Add test to verify that monitor interval and resources are absolutely
required when downloading a manifest (though it's okay to have an
empty set of resources)
* For the CLI tool, use a default comment that has the creation
timestamp, and allow specifying an empty set of URLs
(useful for testing, or phasing out a context still being monitored, etc.)
---
modules/caching-classloader/README.md | 17 +--
.../classloader/ccl/cli/CreateManifest.java | 18 ++-
.../classloader/ccl/manifest/Manifest.java | 36 ++++--
.../ccl/CachingClassLoaderFactoryTest.java | 59 +++++-----
.../accumulo/classloader/ccl/LocalStoreTest.java | 7 +-
.../MiniAccumuloClusterClassLoaderFactoryTest.java | 11 +-
.../classloader/ccl/manifest/ManifestTest.java | 129 +++++++++++++++++++++
7 files changed, 219 insertions(+), 58 deletions(-)
diff --git a/modules/caching-classloader/README.md
b/modules/caching-classloader/README.md
index 549baff..741a5b8 100644
--- a/modules/caching-classloader/README.md
+++ b/modules/caching-classloader/README.md
@@ -44,11 +44,11 @@ downloads from a remote URL.
To use this factory, one must store resource files in a location that can be
specified by a supported URL, and then must create a JSON-formatted manifest
-file that contains a monitoring interval (in seconds, greater than 0), and a
-list of resource URLs along with a checksum for each resource file. This
-manifest file must then be stored somewhere where this factory can download it,
-and use the URL to that file as the `context` parameter for this factory's
-`getClassLoader(String context)` method.
+file that contains an optional comment, a monitoring interval (in seconds,
+greater than 0), and a list of resource URLs along with a checksum for each
+resource file. This manifest file must then be stored somewhere where this
+factory can download it, and use the URL to that file as the `context`
+parameter for this factory's `getClassLoader(String context)` method.
This factory can handle manifest and resource URLs of any type that are
supported by your application via a registered [URLStreamHandlerProvider][1],
@@ -61,6 +61,7 @@ Here is an example manifest:
```json
{
+ "comment": "an optional comment",
"monitorIntervalSeconds": 5,
"resources": [
{
@@ -185,9 +186,9 @@ after approximately a minute.
## Creating a Manifest
-Users may take advantage of the `Manifest.create(int,String,URL[])` method to
-construct a `Manifest` object, programmatically. This will calculate the
-checksums of the classpath elements. `Manifest.toJson()` can be used to
+Users may take advantage of the `Manifest.create(String,int,String,URL[])`
+method to construct a `Manifest` object, programmatically. This will calculate
+the checksums of the classpath elements. `Manifest.toJson()` can be used to
serialize the `Manifest` to a `String` to store in a file.
Alternatively, if this library's jar is built and placed onto Accumulo's
diff --git
a/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
b/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
index 5d6e5a8..f9f9c49 100644
---
a/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
+++
b/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/cli/CreateManifest.java
@@ -20,6 +20,7 @@ package org.apache.accumulo.classloader.ccl.cli;
import java.net.URI;
import java.net.URL;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -34,16 +35,21 @@ import com.google.auto.service.AutoService;
public class CreateManifest implements KeywordExecutable {
static class Opts extends Help {
+ @Parameter(names = {"-c", "--comment"}, required = false,
+ description = "optional comment (default: Created <current ISO-8601
timestamp>)", arity = 1,
+ order = 1)
+ String comment = "Created " + Instant.now().toString();
+
@Parameter(names = {"-i", "--interval"}, required = true,
- description = "monitor interval (in seconds)", arity = 1, order = 1)
+ description = "monitor interval (in seconds)", arity = 1, order = 2)
int monitorInterval;
@Parameter(names = {"-a", "--algorithm"}, required = false,
- description = "checksum algorithm to use (default: SHA-512)", arity =
1, order = 2)
+ description = "checksum algorithm to use (default: SHA-512)", arity =
1, order = 3)
String algorithm = "SHA-512";
- @Parameter(required = true, description = "classpath element URL (<url>[
<url>...])",
- arity = -1, order = 3)
+ @Parameter(required = false, description = "classpath element URLs
([<url>...])", arity = -1,
+ order = 4)
public List<String> files = new ArrayList<>();
}
@@ -69,6 +75,8 @@ public class CreateManifest implements KeywordExecutable {
for (String f : opts.files) {
urls[count++] = URI.create(f).toURL();
}
- System.out.print(Manifest.create(opts.monitorInterval, opts.algorithm,
urls).toJson());
+ System.out
+ .print(Manifest.create(opts.comment, opts.monitorInterval,
opts.algorithm, urls).toJson());
}
+
}
diff --git
a/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
b/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
index af1255f..deb83c5 100644
---
a/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
+++
b/modules/caching-classloader/src/main/java/org/apache/accumulo/classloader/ccl/manifest/Manifest.java
@@ -50,8 +50,8 @@ public class Manifest {
@SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
justification = "user-supplied URL is the intended functionality")
- public static Manifest create(int monitorIntervalSecs, String algorithm,
URL... sources)
- throws IOException {
+ public static Manifest create(String comment, int monitorIntervalSecs,
String algorithm,
+ URL... sources) throws IOException {
// use a LinkedHashSet to preserve the order of the resources
LinkedHashSet<Resource> resources = new LinkedHashSet<>();
for (URL u : sources) {
@@ -60,19 +60,29 @@ public class Manifest {
resources.add(new Resource(u, algorithm, checksum));
}
}
- return new Manifest(monitorIntervalSecs, resources);
+ return new Manifest(comment, monitorIntervalSecs, resources);
}
@SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD",
justification = "user-supplied URL is the intended functionality")
public static Manifest download(final URL url) throws IOException {
+ final Manifest manifest;
try (InputStream is = url.openStream()) {
- var manifest = GSON.fromJson(new InputStreamReader(is, UTF_8),
Manifest.class);
- if (manifest == null) {
- throw new EOFException("InputStream does not contain a valid manifest
at " + url);
- }
- return manifest;
+ manifest = fromStream(is);
+ }
+ if (manifest == null) {
+ throw new EOFException("InputStream does not contain a valid manifest at
" + url);
}
+ return manifest;
+ }
+
+ static Manifest fromStream(InputStream is) {
+ var manifest = GSON.fromJson(new InputStreamReader(is, UTF_8),
Manifest.class);
+ if (manifest == null || manifest.getMonitorIntervalSeconds() <= 0
+ || manifest.getResources() == null) {
+ return null;
+ }
+ return manifest;
}
private static final String SHA_512 = "SHA-512";
@@ -83,16 +93,22 @@ public class Manifest {
// serialized fields for json
// use a LinkedHashSet to preserve the order specified in the file
+ private String comment;
private int monitorIntervalSeconds;
private LinkedHashSet<Resource> resources;
public Manifest() {}
- public Manifest(int monitorIntervalSeconds, LinkedHashSet<Resource>
resources) {
+ public Manifest(String comment, int monitorIntervalSeconds,
LinkedHashSet<Resource> resources) {
+ this.comment = comment; // optional, may be null
Preconditions.checkArgument(monitorIntervalSeconds > 0,
"monitor interval must be greater than zero");
this.monitorIntervalSeconds = monitorIntervalSeconds;
- this.resources = requireNonNull(resources, "resources must be supplied");
+ this.resources = requireNonNull(resources, "resources must be supplied,
but may be empty");
+ }
+
+ public String getComment() {
+ return comment;
}
public int getMonitorIntervalSeconds() {
diff --git
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactoryTest.java
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactoryTest.java
index 082d90c..3e724d3 100644
---
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactoryTest.java
+++
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/CachingClassLoaderFactoryTest.java
@@ -83,7 +83,8 @@ import com.google.gson.JsonSyntaxException;
class CachingClassLoaderFactoryTest {
- protected static final int MONITOR_INTERVAL_SECS = 5;
+ static final String DESC = "test";
+ static final int MONITOR_INTERVAL_SECS = 5;
// MD5 sum for "bad"
private static final String BAD_MD5 = "bae60998ffe4923b131e3d6e4c19993e";
private static MiniDFSCluster hdfs;
@@ -144,7 +145,7 @@ class CachingClassLoaderFactoryTest {
final URL jarCJettyLocation = jetty.getURI().resolve("TestC.jar").toURL();
// manifest with all jars
- var allJars = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
+ var allJars = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
jarBHdfsLocation, jarCJettyLocation, jarDOrigLocation);
String allJarsJson = allJars.toJson();
@@ -216,7 +217,7 @@ class CachingClassLoaderFactoryTest {
var newResources = new LinkedHashSet<Resource>();
var badUrl = URI.create("http://localhost/some/path");
newResources.add(new Resource(badUrl.toURL(), "MD5", BAD_MD5));
- var context2 = new Manifest(MONITOR_INTERVAL_SECS, newResources);
+ var context2 = new Manifest(DESC, MONITOR_INTERVAL_SECS, newResources);
var disallowedContext =
tempDir.resolve("context-with-disallowed-resource-url.json");
Files.writeString(disallowedContext, context2.toJson());
ex = assertThrows(ContextClassLoaderException.class,
@@ -292,6 +293,7 @@ class CachingClassLoaderFactoryTest {
var ex = assertThrows(ContextClassLoaderException.class,
() -> FACTORY.getClassLoader(emptyUrl.toString()));
+ ex.printStackTrace();
assertInstanceOf(UncheckedIOException.class, ex.getCause());
assertInstanceOf(EOFException.class, ex.getCause().getCause());
assertEquals("InputStream does not contain a valid manifest at " +
emptyUrl.toString(),
@@ -301,7 +303,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testInitialInvalidJson() throws Exception {
// Create a new manifest in HDFS, but with invalid content
- var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
// write out invalid json
final var invalid = createManifestFile(fs, "invalid.json",
manifest.toJson().substring(0, 4));
final URL invalidUrl = fs.getUri().resolve(invalid.toUri()).toURL();
@@ -314,7 +316,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testInitial() throws Exception {
- var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
final var initial = createManifestFile(fs, "initial.json",
manifest.toJson());
final URL initialUrl = fs.getUri().resolve(initial.toUri()).toURL();
@@ -337,7 +339,8 @@ class CachingClassLoaderFactoryTest {
Files.copy(jarAPath, jarACopy, REPLACE_EXISTING);
assertTrue(Files.exists(jarACopy));
- var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
+ var manifest =
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
Files.delete(jarACopy);
assertFalse(Files.exists(jarACopy));
@@ -365,7 +368,7 @@ class CachingClassLoaderFactoryTest {
resources.add(new Resource(jarAOrigLocation, "MD5", BAD_MD5));
// remove the file:// prefix from the URL
- String goodJson = new Manifest(MONITOR_INTERVAL_SECS, resources).toJson();
+ String goodJson = new Manifest(DESC, MONITOR_INTERVAL_SECS,
resources).toJson();
String badJson =
goodJson.replace(jarAOrigLocation.toString(),
jarAOrigLocation.toString().substring(6));
assertNotEquals(goodJson, badJson);
@@ -389,7 +392,7 @@ class CachingClassLoaderFactoryTest {
LinkedHashSet<Resource> resources = new LinkedHashSet<>();
resources.add(r);
- var manifest = new Manifest(MONITOR_INTERVAL_SECS, resources);
+ var manifest = new Manifest(DESC, MONITOR_INTERVAL_SECS, resources);
final var initial = createManifestFile(fs, "bad-resource-checksum.json",
manifest.toJson());
final URL initialUrl = fs.getUri().resolve(initial.toUri()).toURL();
@@ -411,7 +414,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdate() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath = createManifestFile(fs, "update.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -423,7 +426,7 @@ class CachingClassLoaderFactoryTest {
testClassFailsToLoad(cl, classD);
// Update the contents
- var update = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarDOrigLocation);
+ var update = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarDOrigLocation);
updateManifestFile(fs, manifestPath, update.toJson());
// wait 2x the monitor interval
@@ -441,7 +444,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateSameClassNameDifferentContent() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath = createManifestFile(fs, "update-same-name.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -453,7 +456,7 @@ class CachingClassLoaderFactoryTest {
testClassFailsToLoad(cl, classD);
// Update the contents
- var update = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarEOrigLocation);
+ var update = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarEOrigLocation);
updateManifestFile(fs, manifestPath, update.toJson());
// wait 2x the monitor interval
@@ -473,7 +476,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateManifestEmpty() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath = createManifestFile(fs, "update-empty.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -503,7 +506,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateNonExistentResource() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath =
createManifestFile(fs, "UpdateNonExistentResource.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -525,7 +528,8 @@ class CachingClassLoaderFactoryTest {
assertFalse(Files.exists(jarACopy));
Files.copy(jarAPath, jarACopy, REPLACE_EXISTING);
assertTrue(Files.exists(jarACopy));
- var manifest2 = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
+ var manifest2 =
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
Files.delete(jarACopy);
assertFalse(Files.exists(jarACopy));
@@ -546,7 +550,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateBadResourceChecksum() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath =
createManifestFile(fs, "UpdateBadResourceChecksum.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -562,7 +566,7 @@ class CachingClassLoaderFactoryTest {
LinkedHashSet<Resource> resources = new LinkedHashSet<>();
resources.add(r);
- var manifest2 = new Manifest(MONITOR_INTERVAL_SECS, resources);
+ var manifest2 = new Manifest(DESC, MONITOR_INTERVAL_SECS, resources);
updateManifestFile(fs, manifestPath, manifest2.toJson());
@@ -581,7 +585,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateBadResourceURL() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath =
createManifestFile(fs, "UpdateBadResourceChecksum.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -596,7 +600,7 @@ class CachingClassLoaderFactoryTest {
// remove the file:// prefix from the URL
LinkedHashSet<Resource> resources = new LinkedHashSet<>();
resources.add(new Resource(jarAOrigLocation, "MD5", BAD_MD5));
- String goodJson = new Manifest(MONITOR_INTERVAL_SECS, resources).toJson();
+ String goodJson = new Manifest(DESC, MONITOR_INTERVAL_SECS,
resources).toJson();
String badJson =
goodJson.replace(jarAOrigLocation.toString(),
jarAOrigLocation.toString().substring(6));
assertNotEquals(goodJson, badJson);
@@ -618,7 +622,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testUpdateInvalidJson() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath = createManifestFile(fs, "update-invalid.json",
manifest.toJson());
final URL updateUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -629,7 +633,7 @@ class CachingClassLoaderFactoryTest {
testClassFailsToLoad(cl, classC);
testClassFailsToLoad(cl, classD);
- var update = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarDOrigLocation);
+ var update = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarDOrigLocation);
updateManifestFile(fs, manifestPath, update.toJson().substring(0, 4));
// wait 2x the monitor interval
@@ -662,7 +666,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testChangingContext() throws Exception {
- var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
+ var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
jarBOrigLocation, jarCOrigLocation, jarDOrigLocation);
final var manifestPath = createManifestFile(fs, "update-changing.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -689,7 +693,7 @@ class CachingClassLoaderFactoryTest {
// Update the contents
var update =
- Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
updatedList.toArray(new URL[0]));
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
updatedList.toArray(new URL[0]));
updateManifestFile(fs, manifestPath, update.toJson());
// wait 2x the monitor interval
@@ -733,7 +737,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testGracePeriod() throws Exception {
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-512", jarAOrigLocation);
final var manifestPath =
createManifestFile(fs, "UpdateNonExistentResource.json",
manifest.toJson());
final URL manifestUrl = fs.getUri().resolve(manifestPath.toUri()).toURL();
@@ -755,7 +759,8 @@ class CachingClassLoaderFactoryTest {
assertFalse(Files.exists(jarACopy));
Files.copy(jarAPath, jarACopy, REPLACE_EXISTING);
assertTrue(Files.exists(jarACopy));
- var manifest2 = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
+ var manifest2 =
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
Files.delete(jarACopy);
assertFalse(Files.exists(jarACopy));
@@ -792,7 +797,7 @@ class CachingClassLoaderFactoryTest {
@Test
public void testExternalFileModification() throws Exception {
- var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
+ var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarAOrigLocation,
jarBOrigLocation, jarCOrigLocation, jarDOrigLocation);
final var manifestPath =
createManifestFile(fs, "update-external-modified.json",
manifest.toJson());
@@ -849,7 +854,7 @@ class CachingClassLoaderFactoryTest {
return null;
});
- var manifest = Manifest.create(100, "SHA-512", jarAOrigLocation,
jarBOrigLocation,
+ var manifest = Manifest.create(null, 100, "SHA-512", jarAOrigLocation,
jarBOrigLocation,
jarCOrigLocation, jarDOrigLocation);
List<Future<?>> futures = new ArrayList<>();
diff --git
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
index ddf024f..0403ee0 100644
---
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
+++
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/LocalStoreTest.java
@@ -19,6 +19,8 @@
package org.apache.accumulo.classloader.ccl;
import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory.createClassLoader;
+import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactoryTest.DESC;
+import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactoryTest.MONITOR_INTERVAL_SECS;
import static org.apache.accumulo.classloader.ccl.LocalStore.MANIFESTS_DIR;
import static org.apache.accumulo.classloader.ccl.LocalStore.RESOURCES_DIR;
import static org.apache.accumulo.classloader.ccl.LocalStore.WORKING_DIR;
@@ -65,7 +67,6 @@ public class LocalStoreTest {
// a mock URL checker that allows all for test
private static final BiConsumer<String,URL> ALLOW_ALL_URLS = (type, url) ->
{};
- private static final int MONITOR_INTERVAL_SECS = 5;
private static MiniDFSCluster hdfs;
private static Server jetty;
private static Manifest manifest;
@@ -108,7 +109,7 @@ public class LocalStoreTest {
resources.add(new Resource(jarCNewLocation, "SHA-1",
TestUtils.computeResourceChecksum("SHA-1", jarCOrigLocation)));
- manifest = new Manifest(MONITOR_INTERVAL_SECS, resources);
+ manifest = new Manifest(DESC, MONITOR_INTERVAL_SECS, resources);
classA = new TestClassInfo("test.TestObjectA", "Hello from A");
classB = new TestClassInfo("test.TestObjectB", "Hello from B");
classC = new TestClassInfo("test.TestObjectC", "Hello from C");
@@ -277,7 +278,7 @@ public class LocalStoreTest {
updatedResources.add(new Resource(jarDOrigLocation, "SHA-512",
TestUtils.computeResourceChecksum("SHA-512", jarDOrigLocation)));
- var update = new Manifest(MONITOR_INTERVAL_SECS, updatedResources);
+ var update = new Manifest(DESC, MONITOR_INTERVAL_SECS, updatedResources);
localStore.storeContext(update);
// Confirm the 3 jars are cached locally
diff --git
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/MiniAccumuloClusterClassLoaderFactoryTest.java
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/MiniAccumuloClusterClassLoaderFactoryTest.java
index 980706c..4db228a 100644
---
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/MiniAccumuloClusterClassLoaderFactoryTest.java
+++
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/MiniAccumuloClusterClassLoaderFactoryTest.java
@@ -23,6 +23,8 @@ import static
java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory.PROP_ALLOWED_URLS;
import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory.PROP_CACHE_DIR;
import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactory.PROP_GRACE_PERIOD;
+import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactoryTest.DESC;
+import static
org.apache.accumulo.classloader.ccl.CachingClassLoaderFactoryTest.MONITOR_INTERVAL_SECS;
import static org.apache.accumulo.classloader.ccl.LocalStore.WORKING_DIR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -99,8 +101,6 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
private static final String ITER_CLASS_NAME =
"org.apache.accumulo.classloader.vfs.examples.ExampleIterator";
- private static final int MONITOR_INTERVAL_SECS =
- CachingClassLoaderFactoryTest.MONITOR_INTERVAL_SECS;
private static URL jarAOrigLocation;
private static URL jarBOrigLocation;
@@ -133,7 +133,7 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
Files.createDirectory(jsonDirPath);
// Create a manifest that only references jar A
- final var manifest = Manifest.create(MONITOR_INTERVAL_SECS, "SHA-256",
jarAOrigLocation);
+ final var manifest = Manifest.create(DESC, MONITOR_INTERVAL_SECS,
"SHA-256", jarAOrigLocation);
final String manifestJson = manifest.toJson();
final File testFile = jsonDirPath.resolve("testManifest.json").toFile();
Files.writeString(testFile.toPath(), manifestJson);
@@ -216,7 +216,8 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
assertTrue(refFiles.stream().anyMatch(p ->
p.endsWith(jarALocalFileName)));
// Update to point to jar B
- final Manifest update = Manifest.create(MONITOR_INTERVAL_SECS,
"SHA-512", jarBOrigLocation);
+ final Manifest update =
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarBOrigLocation);
final String updateJson = update.toJson();
Files.writeString(testFile.toPath(), updateJson);
assertTrue(Files.exists(testFile.toPath()));
@@ -248,7 +249,7 @@ public class MiniAccumuloClusterClassLoaderFactoryTest
extends SharedMiniCluster
assertTrue(Files.exists(jarACopy));
final var update2 =
- Manifest.create(MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
+ Manifest.create(DESC, MONITOR_INTERVAL_SECS, "SHA-512",
jarACopy.toUri().toURL());
Files.delete(jarACopy);
assertTrue(!Files.exists(jarACopy));
diff --git
a/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/manifest/ManifestTest.java
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/manifest/ManifestTest.java
new file mode 100644
index 0000000..1519f7a
--- /dev/null
+++
b/modules/caching-classloader/src/test/java/org/apache/accumulo/classloader/ccl/manifest/ManifestTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.ccl.manifest;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.ByteArrayInputStream;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+class ManifestTest {
+
+ private static String mockJson(boolean withComment, boolean
withMonitorInterval,
+ int withResourceCount) {
+ final String COMMA = ",";
+
+ StringBuilder json = new StringBuilder().append("{");
+ if (withComment) {
+ json.append("'comment': 'an optional comment'");
+ if (withMonitorInterval || withResourceCount >= 0) {
+ json.append(COMMA);
+ }
+ }
+ if (withMonitorInterval) {
+ json.append("'monitorIntervalSeconds': 5");
+ if (withResourceCount >= 0) {
+ json.append(COMMA);
+ }
+ }
+ if (withResourceCount >= 0) {
+ json.append("'resources': [");
+ for (int i = 0; i < withResourceCount; i++) {
+ if (i > 0) {
+ json.append(COMMA);
+ }
+ json.append("{'location': 'file:/home/user/ClassLoaderTestA/" + i +
".jar'").append(COMMA);
+ json.append("'algorithm': 'MOCK',").append("'checksum': '" + i + "'}");
+ }
+ json.append("]");
+ }
+ return json.append("}").toString().replace("'", "\"");
+ }
+
+ @Test
+ void testCreate() throws Exception {
+ var manifest = Manifest.create(null, 27, "SHA-512",
Path.of("pom.xml").toUri().toURL());
+ assertEquals(null, manifest.getComment());
+ assertEquals(27, manifest.getMonitorIntervalSeconds());
+ assertEquals("SHA-512", manifest.getChecksumAlgorithm());
+ assertNotNull(manifest.getChecksum());
+ assertEquals(1, manifest.getResources().size());
+
+ var json = manifest.toJson();
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ var manifest2 = Manifest.fromStream(in);
+ assertNotSame(manifest, manifest2);
+ assertEquals(manifest, manifest2);
+ }
+ }
+
+ @Test
+ void testDeserializing() throws Exception {
+ var json = mockJson(true, true, 3);
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ var manifest = Manifest.fromStream(in);
+ assertEquals("an optional comment", manifest.getComment());
+ assertEquals(5, manifest.getMonitorIntervalSeconds());
+ assertEquals(3, manifest.getResources().size());
+ var resources = manifest.getResources();
+ var iter = resources.iterator();
+ for (int i = 0; i < 3; i++) {
+ var r = iter.next();
+ assertEquals(i + ".jar", r.getFileName());
+ assertEquals("MOCK", r.getAlgorithm());
+ assertEquals(String.valueOf(i), r.getChecksum());
+ }
+ assertFalse(iter.hasNext());
+ }
+
+ // no optional comment
+ json = mockJson(false, true, 3);
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ var manifest = Manifest.fromStream(in);
+ assertNull(manifest.getComment());
+ assertEquals(5, manifest.getMonitorIntervalSeconds());
+ assertEquals(3, manifest.getResources().size());
+ }
+ // no monitor interval
+ json = mockJson(false, false, 3);
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ assertNull(Manifest.fromStream(in));
+ }
+ // empty resources
+ json = mockJson(false, true, 0);
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ var manifest = Manifest.fromStream(in);
+ assertNull(manifest.getComment());
+ assertEquals(5, manifest.getMonitorIntervalSeconds());
+ assertEquals(0, manifest.getResources().size());
+ }
+ // missing resources
+ json = mockJson(false, false, -1);
+ try (var in = new ByteArrayInputStream(json.getBytes(UTF_8))) {
+ assertNull(Manifest.fromStream(in));
+ }
+ }
+}