From c6c0eb03138703e9b875a551597bbab155d5eb0a Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 28 Jul 2022 08:59:17 -0500 Subject: [PATCH] Jetty-12 : Immutable ResourceCollection and Context based mount management (#8337) * Immutable ResourceCollection with mount management. * Internal List is now immutable so that .getResources() cannot be modified. * Improved Resource.toJarFileUri implementation that keeps the deep archive references * Introducing Resource.mountCollection() methods + ResourceCollection is now a private class + Resource.mountCollection() returns a Mount + Places that use this technique are now putting the Mount in the context's beans for the context to close those mounts. * Cleanup names/comments in FileSystemResourceTest * Adding missing test of attempting to create a Resource from a URI `jar:file:foo.jar!/` while not mounted. * Reworked ResourceCollection behaviors based on feedback. + Eliminated all Resource.mountCollection() methods except the Collection one. * Eliminated Resource.mountIfNeeded(Resource) * Eliminated Resource.fromList implementations * Introduced @gregw Resource.of() implementations * Introduced Resource.split() to honor old split logic from Jetty 9/10/11, with glob support, but now it only converts to List * Remove IOException from Mount.root() method * ResourceCollection now flattens and uniques any nested ResourceCollection entries it encounters * Expanded ResourceCollectionTest to cover more code paths * Add ResourceTest for new split() method * Fixing testcase to use a directory that exists on setExtraClasspath * Increase reliability of WebAppContextTests * Updates for working with webapp.extraClasspath * Introduced Resource.unwrapContainer to help in servlet cases where the raw JAR path should be represented in a ServletContext attribute. * Made FileSystemPool.containerUri just use new Resource.unwrapContainer * webapp MetaData updated to use URIs references to Libs (not File objects) * jetty-ee#-maven-plugin use URIs for its classpath tracking to aid in mounting issues later * webapp extraClasspath supports raw JAR references as well as glob now, supported by Resource.split(String) --- .../server/http/HTTPServerDocs.java | 12 +- .../jetty/server/handler/ContextHandler.java | 10 + .../jetty/server/ResourceCacheTest.java | 32 +- .../org/eclipse/jetty/util/StringUtil.java | 33 ++ .../jetty/util/resource/FileSystemPool.java | 14 +- .../eclipse/jetty/util/resource/Resource.java | 373 +++++++++++++---- .../util/resource/ResourceCollection.java | 252 +++--------- .../util/resource/FileSystemResourceTest.java | 32 +- .../jetty/util/resource/JarResourceTest.java | 30 +- .../util/resource/ResourceCollectionTest.java | 374 ++++++++---------- .../jetty/util/resource/ResourceTest.java | 216 ++++++++++ .../eclipse/jetty/util/resource/one/dir/1.txt | 2 +- .../jetty/util/resource/three/dir/3.txt | 2 +- .../eclipse/jetty/util/resource/two/dir/2.txt | 2 +- .../annotations/TestAnnotationDecorator.java | 19 +- .../ee10/annotations/TestRunAsAnnotation.java | 17 +- .../jetty/ee10/demos/ProxyWebAppTest.java | 2 +- .../plugin/AbstractUnassembledWebAppMojo.java | 3 + .../ee10/maven/plugin/MavenWebAppContext.java | 66 +++- .../plugin/MavenWebInfConfiguration.java | 9 +- .../ee10/maven/plugin/OverlayManager.java | 4 +- .../maven/plugin/WebAppPropertyConverter.java | 9 +- .../plugin/TestWebAppPropertyConverter.java | 11 +- .../osgi/boot/OSGiMetaInfConfiguration.java | 4 +- .../ee10/plus/webapp/EnvConfiguration.java | 2 + .../quickstart/QuickStartConfiguration.java | 2 + .../QuickStartDescriptorProcessor.java | 16 +- .../test/resources/jetty-logging.properties | 4 + .../eclipse/jetty/ee10/webapp/Descriptor.java | 15 +- .../eclipse/jetty/ee10/webapp/MetaData.java | 10 +- .../ee10/webapp/MetaInfConfiguration.java | 35 +- .../jetty/ee10/webapp/WebAppClassLoader.java | 60 ++- .../jetty/ee10/webapp/WebAppContext.java | 23 +- .../ee10/webapp/WebInfConfiguration.java | 5 +- .../jetty/ee10/webapp/OrderingTest.java | 6 +- .../jetty/ee10/webapp/WebAppContextTest.java | 35 +- .../ee9/maven/plugin/MavenWebAppContext.java | 66 +++- .../plugin/MavenWebInfConfiguration.java | 12 +- .../ee9/maven/plugin/OverlayManager.java | 5 +- .../maven/plugin/WebAppPropertyConverter.java | 10 +- .../plugin/TestWebAppPropertyConverter.java | 11 +- .../jetty/ee9/nested/ContextHandler.java | 10 + .../osgi/boot/OSGiMetaInfConfiguration.java | 2 +- .../QuickStartDescriptorProcessor.java | 15 +- .../eclipse/jetty/ee9/webapp/MetaData.java | 12 +- .../ee9/webapp/MetaInfConfiguration.java | 38 +- .../jetty/ee9/webapp/WebAppClassLoader.java | 60 ++- .../jetty/ee9/webapp/WebAppContext.java | 25 +- .../jetty/ee9/webapp/WebInfConfiguration.java | 5 +- .../jetty/ee9/webapp/WebAppContextTest.java | 34 +- 50 files changed, 1229 insertions(+), 817 deletions(-) create mode 100644 jetty-ee10/jetty-ee10-quickstart/src/test/resources/jetty-logging.properties diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index 563f834ec1e..db2ea53ab7b 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -69,7 +69,6 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -779,12 +778,11 @@ public class HTTPServerDocs ResourceHandler handler = new ResourceHandler(); // For multiple directories, use ResourceCollection. - ResourceCollection directories = new ResourceCollection(); - // TODO: how to add paths to ResourceCollection? -// directories.addPath("/path/to/static/resources/"); -// directories.addPath("/another/path/to/static/resources/"); - - handler.setBaseResource(directories); + Resource resource = Resource.of( + Resource.newResource("/path/to/static/resources/"), + Resource.newResource("/another/path/to/static/resources/") + ); + handler.setBaseResource(resource); // end::multipleResourcesHandler[] } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 1eec1bae1a4..7e37e974e21 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.DecoratedObjectFactory; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.URIUtil; @@ -530,6 +531,15 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace { // TODO lots of stuff in previous doStart. Some might go here, but most probably goes to the ServletContentHandler ? _context.call(super::doStop, null); + + // cleanup any Mounts associated with the ContextHandler on stop. + // TODO: but what if the context is restarted? how do we remount? do we care? + java.util.Collection mounts = getBeans(Resource.Mount.class); + mounts.forEach((mount) -> + { + IO.close(mount); + removeBean(mount); + }); } public boolean checkVirtualHost(Request request) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java index 47ff1bd3224..336f5744522 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java @@ -22,6 +22,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; +import java.util.stream.Stream; import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.HttpContent; @@ -31,7 +32,6 @@ import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.junit.jupiter.api.Disabled; @@ -44,6 +44,8 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(WorkDirExtension.class) @@ -112,10 +114,12 @@ public class ResourceCacheTest { Path basePath = createUtilTestResources(workDir.getEmptyPathDir()); - ResourceCollection rc = new ResourceCollection( - Resource.newResource(basePath.resolve("one")), - Resource.newResource(basePath.resolve("two")), - Resource.newResource(basePath.resolve("three"))); + List resourceList = Stream.of("one", "two", "three") + .map(basePath::resolve) + .map(Resource::newResource) + .toList(); + + ResourceCollection rc = Resource.of(resourceList); List r = rc.getResources(); MimeTypes mime = new MimeTypes(); @@ -132,7 +136,7 @@ public class ResourceCacheTest assertEquals(getContent(rc2, "2.txt"), "2 - two"); assertEquals(getContent(rc2, "3.txt"), "3 - three"); - assertEquals(null, getContent(rc3, "1.txt")); + assertNull(getContent(rc3, "1.txt")); assertEquals(getContent(rc3, "2.txt"), "2 - three"); assertEquals(getContent(rc3, "3.txt"), "3 - three"); } @@ -142,10 +146,12 @@ public class ResourceCacheTest { Path basePath = createUtilTestResources(workDir.getEmptyPathDir()); - ResourceCollection rc = new ResourceCollection( - Resource.newResource(basePath.resolve("one")), - Resource.newResource(basePath.resolve("two")), - Resource.newResource(basePath.resolve("three"))); + List resourceList = Stream.of("one", "two", "three") + .map(basePath::resolve) + .map(Resource::newResource) + .toList(); + + ResourceCollection rc = Resource.of(resourceList); List r = rc.getResources(); MimeTypes mime = new MimeTypes(); @@ -170,7 +176,7 @@ public class ResourceCacheTest assertEquals(getContent(rc2, "2.txt"), "2 - two"); assertEquals(getContent(rc2, "3.txt"), "3 - three"); - assertEquals(null, getContent(rc3, "1.txt")); + assertNull(getContent(rc3, "1.txt")); assertEquals(getContent(rc3, "2.txt"), "2 - three"); assertEquals(getContent(rc3, "3.txt"), "3 - three"); } @@ -208,9 +214,9 @@ public class ResourceCacheTest cache.setMaxCachedFileSize(85); cache.setMaxCachedFiles(4); - assertTrue(cache.getContent("does not exist", 4096) == null); + assertNull(cache.getContent("does not exist", 4096)); assertTrue(cache.getContent(names[9], 4096) instanceof ResourceHttpContent); - assertTrue(cache.getContent(names[9], 4096).getBuffer() != null); + assertNotNull(cache.getContent(names[9], 4096).getBuffer()); HttpContent content; content = cache.getContent(names[8], 4096); diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 541629977d1..ebc4697b65a 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -17,8 +17,11 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.StringTokenizer; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.IntStream; @@ -981,6 +984,36 @@ public class StringUtil return csvSplit(s, 1, s.length() - 2); } + /** + * Present the results of a {@link StringTokenizer} as an {@link Iterator} of type {@link String} + * @param input the StringTokenizer input + * @param delim the StringTokenizer delim + * @return the resulting iterator for the StringTokenizer + * @see StringTokenizer#StringTokenizer(java.lang.String, java.lang.String) + */ + public static Iterator tokenizerAsIterator(String input, String delim) + { + if (isBlank(input)) + return Collections.emptyIterator(); + + return new Iterator<>() + { + StringTokenizer tokenizer = new StringTokenizer(input, delim); + + @Override + public boolean hasNext() + { + return tokenizer.hasMoreTokens(); + } + + @Override + public String next() + { + return tokenizer.nextToken(); + } + }; + } + /** * Parse a CSV string using {@link #csvSplit(List, String, int, int)} * diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileSystemPool.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileSystemPool.java index d212da010bc..2103f7a3f55 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileSystemPool.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/FileSystemPool.java @@ -196,15 +196,7 @@ public class FileSystemPool implements Dumpable static URI containerUri(URI uri) { - String scheme = uri.getScheme(); - if ((scheme == null) || !scheme.equalsIgnoreCase("jar")) - return null; - - String spec = uri.getRawSchemeSpecificPart(); - int sep = spec.indexOf("!/"); - if (sep != -1) - spec = spec.substring(0, sep); - return URI.create(spec); + return Resource.unwrapContainer(uri); } private static class Bucket @@ -266,7 +258,7 @@ public class FileSystemPool implements Dumpable private final URI uri; private final Resource root; - private Mount(URI uri) throws IOException + private Mount(URI uri) { this.uri = uri; this.root = Resource.newResource(uri); @@ -291,7 +283,7 @@ public class FileSystemPool implements Dumpable @Override public String toString() { - return getClass().getSimpleName() + " uri=" + uri; + return String.format("%s[uri=%s,root=%s]", getClass().getSimpleName(), uri, root); } } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index ab5c345070d..a36552a3f75 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -26,7 +26,6 @@ import java.nio.file.DirectoryIteratorException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.LinkOption; -import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.ProviderNotFoundException; @@ -37,13 +36,16 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.Set; import java.util.StringTokenizer; import java.util.function.Consumer; +import java.util.stream.Stream; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Index; @@ -77,8 +79,58 @@ public abstract class Resource implements ResourceFactory .with("jar:") .build(); + /** + *

Create a ResourceCollection from an unknown set of URIs.

+ * + *

+ * Use this if you are working with URIs from an unknown source, + * such as a user configuration. As some of the entries + * might need mounting, but we cannot determine that yet. + *

+ * + * @param uris collection of URIs to mount into a {@code ResourceCollection} + * @return the {@link Mount} with a root pointing to the {@link ResourceCollection} + */ + public static Resource.Mount mountCollection(Collection uris) + { + List resources = new ArrayList<>(); + List mounts = new ArrayList<>(); + + try + { + // track URIs that have been seen, to avoid adding duplicates. + Set seenUris = new HashSet<>(); + + for (URI uri : uris) + { + if (seenUris.contains(uri)) + continue; // skip this one + Resource.Mount mount = Resource.mountIfNeeded(uri); + if (mount != null) + { + mounts.add(mount); + resources.add(mount.root()); // use mounted resource that has Path with proper FileSystem reference in it. + } + else + { + resources.add(Resource.newResource(uri)); + } + seenUris.add(uri); + } + + return new ResourceCollection.Mount(resources, mounts); + } + catch (Throwable t) + { + // can't create ResourceCollection.Mount, so let's unmount and rethrow. + mounts.forEach(IO::close); + throw t; + } + } + /** *

Mount a URI if it is needed.

+ * * @param uri The URI to mount that may require a FileSystem (e.g. "jar:file://tmp/some.jar!/directory/file.txt") * @return A reference counted {@link Mount} for that file system or null. Callers should call {@link Mount#close()} once * they no longer require any resources from a mounted resource. @@ -87,11 +139,21 @@ public abstract class Resource implements ResourceFactory */ public static Resource.Mount mountIfNeeded(URI uri) { - if (uri == null || uri.getScheme() == null) + if (uri == null) + return null; + String scheme = uri.getScheme(); + if (scheme == null) + return null; + if (!isArchive(uri)) return null; try { - return (uri.getScheme().equalsIgnoreCase("jar")) ? FileSystemPool.INSTANCE.mount(uri) : null; + if (scheme.equalsIgnoreCase("jar")) + { + return FileSystemPool.INSTANCE.mount(uri); + } + // TODO: review contract, should this be null, or an empty mount? + return null; } catch (IOException ioe) { @@ -109,6 +171,8 @@ public abstract class Resource implements ResourceFactory */ public static Resource.Mount mount(URI uri) throws IOException { + if (!isArchive(uri)) + throw new IllegalArgumentException("URI is not a Java Archive: " + uri); if (!uri.getScheme().equalsIgnoreCase("jar")) throw new IllegalArgumentException("not an allowed URI: " + uri); return FileSystemPool.INSTANCE.mount(uri); @@ -122,27 +186,179 @@ public abstract class Resource implements ResourceFactory */ public static Resource.Mount mountJar(Path path) throws IOException { + if (!isArchive(path)) + throw new IllegalArgumentException("Path is not a Java Archive: " + path); URI pathUri = path.toUri(); if (!pathUri.getScheme().equalsIgnoreCase("file")) - throw new IllegalArgumentException("not an allowed path: " + path); - URI jarUri = URI.create(toJarRoot(pathUri.toString())); + throw new IllegalArgumentException("Not an allowed path: " + path); + URI jarUri = toJarFileUri(pathUri); + if (jarUri == null) + throw new IllegalArgumentException("Not a mountable archive: " + path); return FileSystemPool.INSTANCE.mount(jarUri); } - public static String toJarRoot(String jarFile) + /** + *

Make a Resource containing a collection of other resources

+ * @param resources multiple resources to combine as a single resource. Typically, they are directories. + * @return A Resource of multiple resources. + * @see ResourceCollection + */ + public static ResourceCollection of(List resources) { - return "jar:" + jarFile + "!/"; + if (resources == null || resources.isEmpty()) + throw new IllegalArgumentException("No resources"); + + return new ResourceCollection(resources); } + /** + *

Make a Resource containing a collection of other resources

+ * @param resources multiple resources to combine as a single resource. Typically, they are directories. + * @return A Resource of multiple resources. + * @see ResourceCollection + */ + public static ResourceCollection of(Resource... resources) + { + if (resources == null || resources.length == 0) + throw new IllegalArgumentException("No resources"); + + return new ResourceCollection(List.of(resources)); + } + + /** + * Test if Path is a Java Archive (ends in {@code .jar}, {@code .war}, or {@code .zip}). + * + * @param path the path to test + * @return true if path is a {@link Files#isRegularFile(Path, LinkOption...)} and name ends with {@code .jar}, {@code .war}, or {@code .zip} + */ + public static boolean isArchive(Path path) + { + if (path == null) + return false; + if (!Files.isRegularFile(path)) + return false; + String filename = path.getFileName().toString().toLowerCase(Locale.ENGLISH); + return (filename.endsWith(".jar") || filename.endsWith(".war") || filename.endsWith(".zip")); + } + + /** + * Test if URI is a Java Archive. (ends with {@code .jar}, {@code .war}, or {@code .zip}). + * + * @param uri the URI to test + * @return true if the URI has a path that seems to point to a ({@code .jar}, {@code .war}, or {@code .zip}). + */ + public static boolean isArchive(URI uri) + { + if (uri == null) + return false; + if (uri.getScheme() == null) + return false; + String path = uri.getPath(); + int idxEnd = path == null ? -1 : path.length(); + if (uri.getScheme().equalsIgnoreCase("jar")) + { + String ssp = uri.getRawSchemeSpecificPart(); + path = URI.create(ssp).getPath(); + idxEnd = path.length(); + // look for `!/` split + int jarEnd = path.indexOf("!/"); + if (jarEnd >= 0) + idxEnd = jarEnd; + } + if (path == null) + return false; + int idxLastSlash = path.lastIndexOf('/', idxEnd); + if (idxLastSlash < 0) + return false; // no last slash, can't possibly be a valid jar/war/zip + // look for filename suffix + int idxSuffix = path.lastIndexOf('.', idxEnd); + if (idxSuffix < 0) + return false; // no suffix found, can't possibly be a jar/war/zip + if (idxSuffix < idxLastSlash) + return false; // last dot is before last slash, eg ("/path.to/something") + String suffix = path.substring(idxSuffix, idxEnd).toLowerCase(Locale.ENGLISH); + return suffix.equals(".jar") || suffix.equals(".war") || suffix.equals(".zip"); + } + + /** + * Take an arbitrary URI and provide a URI that is suitable for mounting the URI as a Java FileSystem. + * + * The resulting URI will point to the {@code jar:file://foo.jar!/} said Java Archive (jar, war, or zip) + * + * @param uri the URI to mutate to a {@code jar:file:...} URI. + * @return the jar:${uri_to_java_archive}!/${internal-reference} URI or null if not a Java Archive. + * @see #isArchive(URI) + */ + public static URI toJarFileUri(URI uri) + { + Objects.requireNonNull(uri, "URI"); + String scheme = Objects.requireNonNull(uri.getScheme(), "URI scheme"); + + if (!isArchive(uri)) + return null; + + boolean hasInternalReference = uri.getRawSchemeSpecificPart().indexOf("!/") > 0; + + if (scheme.equalsIgnoreCase("jar")) + { + if (uri.getRawSchemeSpecificPart().startsWith("file:")) + { + // Looking good as a jar:file: URI + if (hasInternalReference) + return uri; // is all good, no changes needed. + else + // add the internal reference indicator to the root of the archive + return URI.create(uri.toASCIIString() + "!/"); + } + } + else if (scheme.equalsIgnoreCase("file")) + { + String rawUri = uri.toASCIIString(); + if (hasInternalReference) + return URI.create("jar:" + rawUri); + else + return URI.create("jar:" + rawUri + "!/"); + } + + // shouldn't be possible to reach this point + throw new IllegalArgumentException("Cannot make %s into `jar:file:` URI".formatted(uri)); + } + + // TODO: will be removed in MultiReleaseJarFile PR, as AnnotationParser is the only thing using this, + // and it doesn't need to recreate the URI that it will already have. public static String toJarPath(String jarFile, String pathInJar) { return "jar:" + jarFile + URIUtil.addPaths("!/", pathInJar); } + /** + * Unwrap a URI to expose its container path reference. + * + * Take out the container archive name URI from a {@code jar:file:${container-name}!/} URI. + * + * @param uri the input URI + * @return the container String if a {@code jar} scheme, or just the URI untouched. + */ + public static URI unwrapContainer(URI uri) + { + Objects.requireNonNull(uri); + + String scheme = uri.getScheme(); + if ((scheme == null) || !scheme.equalsIgnoreCase("jar")) + return uri; + + String spec = uri.getRawSchemeSpecificPart(); + int sep = spec.indexOf("!/"); + if (sep != -1) + spec = spec.substring(0, sep); + return URI.create(spec); + } + /** *

Convert a String into a URI suitable for use as a Resource.

+ * * @param resource If the string starts with one of the ALLOWED_SCHEMES, then it is assumed to be a - * representation of a {@link URI}, otherwise it is treated as a {@link Path}. + * representation of a {@link URI}, otherwise it is treated as a {@link Path}. * @return The {@link URI} form of the resource. */ public static URI toURI(String resource) @@ -348,6 +564,7 @@ public abstract class Resource implements ResourceFactory /** * Return true if the Resource r is contained in the Resource containingResource, either because * containingResource is a folder or a jar file or any form of resource capable of containing other resources. + * * @param r the contained resource * @param containingResource the containing resource * @return true if the Resource is contained, false otherwise @@ -360,6 +577,7 @@ public abstract class Resource implements ResourceFactory /** * Return the Path corresponding to this resource. + * * @return the path. */ public abstract Path getPath(); @@ -367,6 +585,7 @@ public abstract class Resource implements ResourceFactory /** * Return true if this resource is contained in the Resource r, either because * r is a folder or a jar file or any form of resource capable of containing other resources. + * * @param r the containing resource * @return true if this Resource is contained, false otherwise * @throws IOException Problem accessing resource @@ -377,6 +596,7 @@ public abstract class Resource implements ResourceFactory * Return true if the passed Resource represents the same resource as the Resource. * For many resource types, this is equivalent to {@link #equals(Object)}, however * for resources types that support aliasing, this maybe some other check (e.g. {@link java.nio.file.Files#isSameFile(Path, Path)}). + * * @param resource The resource to check * @return true if the passed resource represents the same resource. */ @@ -388,6 +608,7 @@ public abstract class Resource implements ResourceFactory /** * Equivalent to {@link Files#exists(Path, LinkOption...)} with the following parameters: * {@link #getPath()} and {@link LinkOption#NOFOLLOW_LINKS}. + * * @return true if the represented resource exists. */ public boolean exists() @@ -398,6 +619,7 @@ public abstract class Resource implements ResourceFactory /** * Equivalent to {@link Files#isDirectory(Path, LinkOption...)} with the following parameter: * {@link #getPath()}. + * * @return true if the represented resource is a container/directory. */ public boolean isDirectory() @@ -409,6 +631,7 @@ public abstract class Resource implements ResourceFactory * Time resource was last modified. * Equivalent to {@link Files#getLastModifiedTime(Path, LinkOption...)} with the following parameter: * {@link #getPath()} then returning {@link FileTime#toMillis()}. + * * @return the last modified time as milliseconds since unix epoch or * 0 if {@link Files#getLastModifiedTime(Path, LinkOption...)} throws {@link IOException}. */ @@ -430,6 +653,7 @@ public abstract class Resource implements ResourceFactory * Length of the resource. * Equivalent to {@link Files#size(Path)} with the following parameter: * {@link #getPath()}. + * * @return the length of the resource or 0 if {@link Files#size(Path)} throws {@link IOException}. */ public long length() @@ -495,6 +719,7 @@ public abstract class Resource implements ResourceFactory * Deletes the given resource * Equivalent to {@link Files#deleteIfExists(Path)} with the following parameter: * {@link #getPath()}. + * * @return true if the resource was deleted by this method; false if the file could not be deleted because it did not exist * or if {@link Files#deleteIfExists(Path)} throws {@link IOException}. */ @@ -516,6 +741,7 @@ public abstract class Resource implements ResourceFactory * Equivalent to {@link Files#move(Path, Path, CopyOption...)} with the following parameter: * {@link #getPath()}, {@code dest.getPath()} then returning the result of {@link Files#exists(Path, LinkOption...)} * on the {@code Path} returned by {@code move()}. + * * @param dest the destination name for the resource * @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed. */ @@ -544,7 +770,7 @@ public abstract class Resource implements ResourceFactory * {@link IOException} was thrown while building the filename list. * Note: The resource names are not URL encoded. */ - public List list() + public List list() // TODO: should return Path's { try (DirectoryStream dir = Files.newDirectoryStream(getPath())) { @@ -593,7 +819,9 @@ public abstract class Resource implements ResourceFactory // where default resolve behavior would be to treat // that like an absolute path. while (subUriPath.startsWith(URIUtil.SLASH)) + { subUriPath = subUriPath.substring(1); + } URI uri = getURI(); URI resolvedUri; @@ -660,7 +888,7 @@ public abstract class Resource implements ResourceFactory * @return String of HTML * @throws IOException on failure to generate a list. */ - public String getListHTML(String base, boolean parent, String query) throws IOException + public String getListHTML(String base, boolean parent, String query) throws IOException // TODO: move to helper class { // This method doesn't check aliases, so it is OK to canonicalize here. base = URIUtil.normalizePath(base); @@ -1099,96 +1327,72 @@ public abstract class Resource implements ResourceFactory } /** - * Parse a list of String delimited resources and - * return the List of Resources instances it represents. + * Split a string of references, that may be split with {@code ,}, or {@code ;}, or {@code |} into URIs. *

- * Supports glob references that end in {@code /*} or {@code \*}. - * Glob references will only iterate through the level specified and will not traverse - * found directories within the glob reference. + * Each part of the input string could be path references (unix or windows style), or string URI references. + *

+ *

+ * If the result of processing the input segment is a java archive, then its resulting URI will be a mountable URI as `jar:file:...!/`. *

* - * @param resources the comma {@code ,} or semicolon {@code ;} delimited - * String of resource references. - * @param globDirs true to return directories in addition to files at the level of the glob - * @return the list of resources parsed from input string. + * @param str the input string of references + * @see #toJarFileUri(URI) */ - public static List fromList(String resources, boolean globDirs) throws IOException + public static List split(String str) { - return fromList(resources, globDirs, Resource::newResource); - } + List uris = new ArrayList<>(); - /** - * Parse a delimited String of resource references and - * return the List of Resources instances it represents. - *

- * Supports glob references that end in {@code /*} or {@code \*}. - * Glob references will only iterate through the level specified and will not traverse - * found directories within the glob reference. - *

- * - * @param resources the comma {@code ,} or semicolon {@code ;} delimited - * String of resource references. - * @param globDirs true to return directories in addition to files at the level of the glob - * @param resourceFactory the ResourceFactory used to create new Resource references - * @return the list of resources parsed from input string. - */ - public static List fromList(String resources, boolean globDirs, ResourceFactory resourceFactory) throws IOException - { - if (StringUtil.isBlank(resources)) - { - return Collections.emptyList(); - } - - List returnedResources = new ArrayList<>(); - - StringTokenizer tokenizer = new StringTokenizer(resources, StringUtil.DEFAULT_DELIMS); + StringTokenizer tokenizer = new StringTokenizer(str, ",;|"); while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken().trim(); - - // Is this a glob reference? - if (token.endsWith("/*") || token.endsWith("\\*")) + String reference = tokenizer.nextToken(); + try { - String dir = token.substring(0, token.length() - 2); - // Use directory - Resource dirResource = resourceFactory.resolve(dir); - if (dirResource.exists() && dirResource.isDirectory()) + // Is this a glob reference? + if (reference.endsWith("/*") || reference.endsWith("\\*")) { - // To obtain the list of entries - List entries = dirResource.list(); - if (entries != null) + String dir = reference.substring(0, reference.length() - 2); + Path pathDir = Paths.get(dir); + // Use directory + if (Files.exists(pathDir) && Files.isDirectory(pathDir)) { - entries.sort(Comparator.naturalOrder()); - for (String entry : entries) + // To obtain the list of entries + try (Stream listStream = Files.list(pathDir)) { - try - { - Resource resource = dirResource.resolve(entry); - if (!resource.isDirectory()) - { - returnedResources.add(resource); - } - else if (globDirs) - { - returnedResources.add(resource); - } - } - catch (Exception ex) - { - LOG.warn("Bad glob [{}] entry: {}", token, entry, ex); - } + listStream + .filter(Files::isRegularFile) + .filter(Resource::isArchive) + .sorted(Comparator.naturalOrder()) + .forEach(path -> uris.add(toJarFileUri(path.toUri()))); + } + catch (IOException e) + { + throw new RuntimeException("Unable to process directory glob listing: " + reference, e); } } } + else + { + // Simple reference + URI refUri = toURI(reference); + // Is this a Java Archive that can be mounted? + URI jarFileUri = toJarFileUri(refUri); + if (jarFileUri != null) + // add as mountable URI + uris.add(jarFileUri); + else + // add as normal URI + uris.add(refUri); + + } } - else + catch (Exception e) { - // Simple reference, add as-is - returnedResources.add(resourceFactory.resolve(token)); + LOG.warn("Invalid Resource Reference: " + reference); + throw e; } } - - return returnedResources; + return uris; } /** @@ -1196,6 +1400,7 @@ public abstract class Resource implements ResourceFactory * of such mount allowing the use of more {@link Resource}s. * Mounts are {@link Closeable} because they always contain resources (like file descriptors) that must eventually * be released. + * * @see #mount(URI) * @see #mountJar(Path) */ @@ -1203,9 +1408,9 @@ public abstract class Resource implements ResourceFactory { /** * Return the root {@link Resource} made available by this mount. + * * @return the resource. - * @throws IOException Problem accessing resource */ - Resource root() throws IOException; + Resource root(); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java index 542f6942949..59ed1a161d9 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java @@ -21,124 +21,99 @@ import java.net.URI; import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; -import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; /** - * A collection of resources (dirs). - * Allows webapps to have multiple (static) sources. + * A collection of Resources. + * Allows webapps to have multiple sources. * The first resource in the collection is the main resource. * If a resource is not found in the main resource, it looks it up in - * the order the resources were constructed. + * the order the provided in the constructors */ public class ResourceCollection extends Resource { - private List _resources; - - /** - * Instantiates an empty resource collection. - *

- * This constructor is used when configuring jetty-maven-plugin. - */ - public ResourceCollection() + static class Mount implements Resource.Mount { - _resources = new ArrayList<>(); + private final List _mounts; + private final ResourceCollection _root; + + Mount(Collection resources, List mounts) + { + _root = new ResourceCollection(resources); + _mounts = mounts; + } + + @Override + public void close() throws IOException + { + _mounts.forEach(IO::close); + } + + @Override + public Resource root() + { + return _root; + } } + private final List _resources; + /** * Instantiates a new resource collection. * * @param resources the resources to be added to collection */ - public ResourceCollection(Resource... resources) + ResourceCollection(Collection resources) { - this(Arrays.asList(resources)); + List res = new ArrayList<>(); + gatherUniqueFlatResourceList(res, resources); + _resources = Collections.unmodifiableList(res); } - /** - * Instantiates a new resource collection. - * - * @param resources the resources to be added to collection - */ - public ResourceCollection(Collection resources) + private static void gatherUniqueFlatResourceList(List unique, Collection resources) { - _resources = new ArrayList<>(); + if (resources == null || resources.isEmpty()) + throw new IllegalArgumentException("Empty Resource collection"); for (Resource r : resources) { if (r == null) { - continue; + throw new IllegalArgumentException("Null Resource entry encountered"); } - if (r instanceof ResourceCollection) + + if (r instanceof ResourceCollection resourceCollection) { - _resources.addAll(((ResourceCollection)r).getResources()); + gatherUniqueFlatResourceList(unique, resourceCollection.getResources()); } else { - assertResourceValid(r); - _resources.add(r); - } - } - } - - /** - * Instantiates a new resource collection. - * - * @param resources the resource strings to be added to collection - */ - public ResourceCollection(String[] resources) - { - _resources = new ArrayList<>(); - - if (resources == null || resources.length == 0) - { - return; - } - - try - { - for (String strResource : resources) - { - if (strResource == null || strResource.length() == 0) + if (unique.contains(r)) { - throw new IllegalArgumentException("empty/null resource path not supported"); + // skip, already seen + continue; } - Resource resource = Resource.newResource(strResource); - assertResourceValid(resource); - _resources.add(resource); - } - if (_resources.isEmpty()) - { - throw new IllegalArgumentException("resources cannot be empty or null"); + if (!r.exists()) + { + throw new IllegalArgumentException("Does not exist: " + r); + } + + if (!r.isDirectory()) + { + throw new IllegalArgumentException("Not a directory: " + r); + } + unique.add(r); } } - catch (RuntimeException e) - { - throw e; - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - - /** - * Instantiates a new resource collection. - * - * @param csvResources the string containing comma-separated resource strings - * @throws IOException if any listed resource is not valid - */ - public ResourceCollection(String csvResources) throws IOException - { - setResources(csvResources); } /** @@ -152,76 +127,8 @@ public class ResourceCollection extends Resource } /** - * Sets the resource collection's resources. + * Resolves a path against the resource collection. * - * @param res the resources to set - */ - public void setResources(List res) - { - _resources = new ArrayList<>(); - if (res.isEmpty()) - { - return; - } - - _resources.addAll(res); - } - - /** - * Sets the resource collection's resources. - * - * @param resources the new resource array - */ - public void setResources(Resource[] resources) - { - if (resources == null || resources.length == 0) - { - _resources = null; - return; - } - - List res = new ArrayList<>(); - for (Resource resource : resources) - { - assertResourceValid(resource); - res.add(resource); - } - - setResources(res); - } - - /** - * Sets the resources as string of comma-separated values. - * This method should be used when configuring jetty-maven-plugin. - * - * @param resources the comma-separated string containing - * one or more resource strings. - * @throws IOException if unable resource declared is not valid - * @see Resource#fromList(String, boolean) - */ - public void setResources(String resources) throws IOException - { - if (StringUtil.isBlank(resources)) - { - throw new IllegalArgumentException("String is blank"); - } - - List list = Resource.fromList(resources, false); - if (list.isEmpty()) - { - throw new IllegalArgumentException("String contains no entries"); - } - List ret = new ArrayList<>(); - for (Resource resource : list) - { - assertResourceValid(resource); - ret.add(resource); - } - setResources(ret); - } - - /** - * Add a path to the resource collection. * @param subUriPath The path segment to add * @return The resulting resource(s) : *

    @@ -235,8 +142,6 @@ public class ResourceCollection extends Resource @Override public Resource resolve(String subUriPath) throws IOException { - assertResourcesSet(); - if (subUriPath == null) { throw new MalformedURLException("null path"); @@ -281,8 +186,6 @@ public class ResourceCollection extends Resource @Override public boolean exists() { - assertResourcesSet(); - for (Resource r : _resources) { if (r.exists()) @@ -297,7 +200,6 @@ public class ResourceCollection extends Resource @Override public Path getPath() { - assertResourcesSet(); for (Resource r : _resources) { Path p = r.getPath(); @@ -310,8 +212,6 @@ public class ResourceCollection extends Resource @Override public InputStream newInputStream() throws IOException { - assertResourcesSet(); - for (Resource r : _resources) { if (!r.exists()) @@ -332,8 +232,6 @@ public class ResourceCollection extends Resource @Override public ReadableByteChannel newReadableByteChannel() throws IOException { - assertResourcesSet(); - for (Resource r : _resources) { ReadableByteChannel channel = r.newReadableByteChannel(); @@ -348,8 +246,6 @@ public class ResourceCollection extends Resource @Override public String getName() { - assertResourcesSet(); - for (Resource r : _resources) { String name = r.getName(); @@ -364,8 +260,6 @@ public class ResourceCollection extends Resource @Override public URI getURI() { - assertResourcesSet(); - for (Resource r : _resources) { URI uri = r.getURI(); @@ -380,15 +274,12 @@ public class ResourceCollection extends Resource @Override public boolean isDirectory() { - assertResourcesSet(); return true; } @Override public long lastModified() { - assertResourcesSet(); - for (Resource r : _resources) { long lm = r.lastModified(); @@ -412,7 +303,6 @@ public class ResourceCollection extends Resource @Override public List list() { - assertResourcesSet(); HashSet set = new HashSet<>(); for (Resource r : _resources) { @@ -435,8 +325,6 @@ public class ResourceCollection extends Resource @Override public void copyTo(Path destination) throws IOException { - assertResourcesSet(); - // Copy in reverse order for (int r = _resources.size(); r-- > 0; ) { @@ -445,17 +333,14 @@ public class ResourceCollection extends Resource } /** - * @return the list of resources separated by a path separator + * @return the list of resources */ @Override public String toString() { - if (_resources.isEmpty()) - { - return "[]"; - } - - return String.valueOf(_resources); + return _resources.stream() + .map(Resource::getName) + .collect(Collectors.joining(", ", "[", "]")); } @Override @@ -464,25 +349,4 @@ public class ResourceCollection extends Resource // TODO could look at implementing the semantic of is this collection a subset of the Resource r? return false; } - - private void assertResourcesSet() - { - if (_resources == null || _resources.isEmpty()) - { - throw new IllegalStateException("*resources* not set."); - } - } - - private void assertResourceValid(Resource resource) - { - if (resource == null) - { - throw new IllegalStateException("Null resource not supported"); - } - - if (!resource.exists() || !resource.isDirectory()) - { - throw new IllegalArgumentException(resource + " is not an existing directory."); - } - } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java index 46c6de1b50e..f675850aee1 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java @@ -60,7 +60,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.condition.OS.LINUX; import static org.junit.jupiter.api.condition.OS.MAC; @@ -190,7 +189,7 @@ public class FileSystemResourceTest } @Test - public void testAddPathClass() throws Exception + public void testResolvePathClass() throws Exception { Path dir = workDir.getEmptyPathDir(); @@ -206,13 +205,13 @@ public class FileSystemResourceTest } @Test - public void testAddRootPath() throws Exception + public void testResolveRootPath() throws Exception { Path dir = workDir.getEmptyPathDir(); Path subdir = dir.resolve("sub"); Files.createDirectories(subdir); - String readableRootDir = findRootDir(dir.getFileSystem()); + String readableRootDir = findAnyDirectoryOffRoot(dir.getFileSystem()); assumeTrue(readableRootDir != null, "Readable Root Dir found"); Resource base = Resource.newResource(dir); @@ -236,7 +235,7 @@ public class FileSystemResourceTest { Path dir = workDir.getEmptyPathDir(); - String readableRootDir = findRootDir(dir.getFileSystem()); + String readableRootDir = findAnyDirectoryOffRoot(dir.getFileSystem()); assumeTrue(readableRootDir != null, "Readable Root Dir found"); Path subdir = dir.resolve("sub"); @@ -267,9 +266,14 @@ public class FileSystemResourceTest assertThat("Ref O1 contents", toString(refO1), is("hi o-with-two-dots")); } - private String findRootDir(FileSystem fs) + /** + * Best effort discovery a directory off the provided FileSystem. + * @param fs the provided FileSystem. + * @return a directory off the root FileSystem. + */ + private String findAnyDirectoryOffRoot(FileSystem fs) { - // look for a directory off of a root path + // look for anything that's a directory off of any root paths of the provided FileSystem for (Path rootDir : fs.getRootDirectories()) { try (DirectoryStream dir = Files.newDirectoryStream(rootDir)) @@ -284,7 +288,9 @@ public class FileSystemResourceTest } catch (Exception ignored) { - // FIXME why ignoring exceptions?? + // Don't care if there's an error, we'll just try the next possible root directory. + // if no directories are found, then that means the users test environment is + // super odd, and we cannot continue these tests anyway, and are skipped with an assume(). } } @@ -1055,7 +1061,7 @@ public class FileSystemResourceTest } @Test - public void testAddPathWindowsSlash() throws Exception + public void testResolveWindowsSlash() throws Exception { Path dir = workDir.getEmptyPathDir(); Files.createDirectories(dir); @@ -1098,7 +1104,7 @@ public class FileSystemResourceTest } @Test - public void testAddPathWindowsExtensionLess() throws Exception + public void testResolveWindowsExtensionLess() throws Exception { Path dir = workDir.getEmptyPathDir(); Files.createDirectories(dir); @@ -1140,7 +1146,7 @@ public class FileSystemResourceTest } @Test - public void testAddInitialSlash() throws Exception + public void testResolveInitialSlash() throws Exception { Path dir = workDir.getEmptyPathDir(); Files.createDirectories(dir); @@ -1170,7 +1176,7 @@ public class FileSystemResourceTest } @Test - public void testAddInitialDoubleSlash() throws Exception + public void testResolveInitialDoubleSlash() throws Exception { Path dir = workDir.getEmptyPathDir(); Files.createDirectories(dir); @@ -1200,7 +1206,7 @@ public class FileSystemResourceTest } @Test - public void testAddDoubleSlash() throws Exception + public void testResolveDoubleSlash() throws Exception { Path dir = workDir.getEmptyPathDir(); Files.createDirectories(dir); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java index 7f0d2bda75b..1da81d7dfaf 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/JarResourceTest.java @@ -124,24 +124,26 @@ public class JarResourceTest Path testZip = Files.copy(originalTestZip, tempDir.resolve("test.zip")); String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/"; URI uri = URI.create(s); - Resource.Mount mount = Resource.mount(uri); - Resource resource = mount.root(); - assertTrue(resource.exists()); + try (Resource.Mount mount = Resource.mount(uri)) + { + Resource resource = mount.root(); + assertTrue(resource.exists()); - String dump = FileSystemPool.INSTANCE.dump(); - assertThat(dump, containsString("FileSystemPool")); - assertThat(dump, containsString("mounts size=1")); - assertThat(dump, containsString("Mount uri=jar:file:/")); - assertThat(dump, containsString("test.zip!/subdir")); + String dump = FileSystemPool.INSTANCE.dump(); + assertThat(dump, containsString("FileSystemPool")); + assertThat(dump, containsString("mounts size=1")); + assertThat(dump, containsString("Mount[uri=jar:file:/")); + assertThat(dump, containsString("test.zip!/subdir")); - Files.delete(testZip); - FileSystemPool.INSTANCE.sweep(); + Files.delete(testZip); + FileSystemPool.INSTANCE.sweep(); - dump = FileSystemPool.INSTANCE.dump(); - assertThat(dump, containsString("FileSystemPool")); - assertThat(dump, containsString("mounts size=0")); + dump = FileSystemPool.INSTANCE.dump(); + assertThat(dump, containsString("FileSystemPool")); + assertThat(dump, containsString("mounts size=0")); - assertThrows(ClosedFileSystemException.class, resource::exists); + assertThrows(ClosedFileSystemException.class, resource::exists); + } } @Test diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java index c873d3f3761..90c2c7702e0 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java @@ -13,17 +13,23 @@ package org.eclipse.jetty.util.resource; -import java.io.BufferedReader; import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,236 +38,196 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(WorkDirExtension.class) public class ResourceCollectionTest { - public WorkDir workdir; - - @Test - public void testUnsetCollectionThrowsISE() - { - ResourceCollection coll = new ResourceCollection(); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testEmptyResourceArrayThrowsISE() - { - ResourceCollection coll = new ResourceCollection(new Resource[0]); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testResourceArrayWithNullThrowsISE() - { - ResourceCollection coll = new ResourceCollection(new Resource[]{null}); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testEmptyStringArrayThrowsISE() - { - ResourceCollection coll = new ResourceCollection(new String[0]); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testStringArrayWithNullThrowsIAE() - { - assertThrows(IllegalArgumentException.class, - () -> new ResourceCollection(new String[]{null})); - } - - @Test - public void testNullCsvThrowsIAE() - { - assertThrows(IllegalArgumentException.class, () -> - { - String csv = null; - new ResourceCollection(csv); // throws IAE - }); - } - - @Test - public void testEmptyCsvThrowsIAE() - { - assertThrows(IllegalArgumentException.class, () -> - { - String csv = ""; - new ResourceCollection(csv); // throws IAE - }); - } - - @Test - public void testBlankCsvThrowsIAE() - { - assertThrows(IllegalArgumentException.class, () -> - { - String csv = ",,,,"; - new ResourceCollection(csv); // throws IAE - }); - } - - @Test - public void testSetResourceArrayNullThrowsISE() - { - // Create a ResourceCollection with one valid entry - Path path = MavenTestingUtils.getTargetPath(); - Resource resource = Resource.newResource(path); - ResourceCollection coll = new ResourceCollection(resource); - - // Reset collection to invalid state - coll.setResources((Resource[])null); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testSetResourceEmptyThrowsISE() - { - // Create a ResourceCollection with one valid entry - Path path = MavenTestingUtils.getTargetPath(); - Resource resource = Resource.newResource(path); - ResourceCollection coll = new ResourceCollection(resource); - - // Reset collection to invalid state - coll.setResources(new Resource[0]); - - assertThrowIllegalStateException(coll); - } - - @Test - public void testSetResourceAllNullsThrowsISE() - { - // Create a ResourceCollection with one valid entry - Path path = MavenTestingUtils.getTargetPath(); - Resource resource = Resource.newResource(path); - ResourceCollection coll = new ResourceCollection(resource); - - // Reset collection to invalid state - assertThrows(IllegalStateException.class, () -> coll.setResources(new Resource[]{null, null, null})); - - // Ensure not modified. - assertThat(coll.getResources().size(), is(1)); - } - - private void assertThrowIllegalStateException(ResourceCollection coll) - { - assertThrows(IllegalStateException.class, () -> coll.resolve("foo")); - assertThrows(IllegalStateException.class, coll::exists); - assertThrows(IllegalStateException.class, coll::getPath); - assertThrows(IllegalStateException.class, coll::newInputStream); - assertThrows(IllegalStateException.class, coll::newReadableByteChannel); - assertThrows(IllegalStateException.class, coll::getURI); - assertThrows(IllegalStateException.class, coll::getName); - assertThrows(IllegalStateException.class, coll::isDirectory); - assertThrows(IllegalStateException.class, coll::lastModified); - assertThrows(IllegalStateException.class, coll::list); - assertThrows(IllegalStateException.class, () -> - { - Path destPath = workdir.getPathFile("bar"); - coll.copyTo(destPath); - }); - } + public WorkDir workDir; @Test public void testList() throws Exception { - ResourceCollection rc1 = new ResourceCollection( - Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/one/"), - Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/two/"), - Resource.newResource("src/test/resources/org/eclipse/jetty/util/resource/three/")); + Path one = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/one"); + Path two = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two"); + Path three = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/three"); - assertThat(rc1.list(), contains("1.txt", "2.txt", "3.txt", "dir/")); - assertThat(rc1.resolve("dir").list(), contains("1.txt", "2.txt", "3.txt")); - assertThat(rc1.resolve("unknown").list(), nullValue()); - } - - @Test - public void testMultipleSources1() throws Exception - { - ResourceCollection rc1 = new ResourceCollection(new String[]{ - "src/test/resources/org/eclipse/jetty/util/resource/one/", - "src/test/resources/org/eclipse/jetty/util/resource/two/", - "src/test/resources/org/eclipse/jetty/util/resource/three/" - }); - assertEquals(getContent(rc1, "1.txt"), "1 - one"); - assertEquals(getContent(rc1, "2.txt"), "2 - two"); - assertEquals(getContent(rc1, "3.txt"), "3 - three"); - - ResourceCollection rc2 = new ResourceCollection( - "src/test/resources/org/eclipse/jetty/util/resource/one/," + - "src/test/resources/org/eclipse/jetty/util/resource/two/," + - "src/test/resources/org/eclipse/jetty/util/resource/three/" + ResourceCollection rc = Resource.of( + Resource.newResource(one), + Resource.newResource(two), + Resource.newResource(three) ); - assertEquals(getContent(rc2, "1.txt"), "1 - one"); - assertEquals(getContent(rc2, "2.txt"), "2 - two"); - assertEquals(getContent(rc2, "3.txt"), "3 - three"); - } + assertThat(rc.list(), contains("1.txt", "2.txt", "3.txt", "dir/")); + assertThat(rc.resolve("dir").list(), contains("1.txt", "2.txt", "3.txt")); + assertThat(rc.resolve("unknown").list(), nullValue()); - @Test - public void testMergedDir() throws Exception - { - ResourceCollection rc = new ResourceCollection(new String[]{ - "src/test/resources/org/eclipse/jetty/util/resource/one/", - "src/test/resources/org/eclipse/jetty/util/resource/two/", - "src/test/resources/org/eclipse/jetty/util/resource/three/" - }); - - Resource r = rc.resolve("dir"); - assertTrue(r instanceof ResourceCollection); - rc = (ResourceCollection)r; assertEquals(getContent(rc, "1.txt"), "1 - one"); assertEquals(getContent(rc, "2.txt"), "2 - two"); assertEquals(getContent(rc, "3.txt"), "3 - three"); } + @Test + public void testMergedDir() throws Exception + { + Path one = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/one"); + Path two = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two"); + Path three = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/three"); + + ResourceCollection rc = Resource.of( + Resource.newResource(one), + Resource.newResource(two), + Resource.newResource(three) + ); + + // This should return a ResourceCollection with 3 `/dir/` sub-directories. + Resource r = rc.resolve("dir"); + assertTrue(r instanceof ResourceCollection); + rc = (ResourceCollection)r; + assertEquals(getContent(rc, "1.txt"), "1 - one (in dir)"); + assertEquals(getContent(rc, "2.txt"), "2 - two (in dir)"); + assertEquals(getContent(rc, "3.txt"), "3 - three (in dir)"); + } + @Test public void testCopyTo() throws Exception { - ResourceCollection rc = new ResourceCollection(new String[]{ - "src/test/resources/org/eclipse/jetty/util/resource/one/", - "src/test/resources/org/eclipse/jetty/util/resource/two/", - "src/test/resources/org/eclipse/jetty/util/resource/three/" - }); + Path one = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/one"); + Path two = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two"); + Path three = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/three"); - File dest = MavenTestingUtils.getTargetTestingDir("copyto"); - FS.ensureDirExists(dest); - rc.copyTo(dest.toPath()); + ResourceCollection rc = Resource.of( + Resource.newResource(one), + Resource.newResource(two), + Resource.newResource(three) + ); + Path destDir = workDir.getEmptyPathDir(); + rc.copyTo(destDir); - Resource r = Resource.newResource(dest.toURI()); + Resource r = Resource.newResource(destDir); assertEquals(getContent(r, "1.txt"), "1 - one"); assertEquals(getContent(r, "2.txt"), "2 - two"); assertEquals(getContent(r, "3.txt"), "3 - three"); r = r.resolve("dir"); - assertEquals(getContent(r, "1.txt"), "1 - one"); - assertEquals(getContent(r, "2.txt"), "2 - two"); - assertEquals(getContent(r, "3.txt"), "3 - three"); + assertEquals(getContent(r, "1.txt"), "1 - one (in dir)"); + assertEquals(getContent(r, "2.txt"), "2 - two (in dir)"); + assertEquals(getContent(r, "3.txt"), "3 - three (in dir)"); + } - IO.delete(dest); + @Test + public void testResourceCollectionInResourceCollection() + { + Path one = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/one"); + Path two = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two"); + Path three = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/three"); + Path twoDir = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two/dir"); + + ResourceCollection rc1 = Resource.of( + List.of( + Resource.newResource(one), + Resource.newResource(two), + Resource.newResource(three) + ) + ); + + ResourceCollection rc2 = Resource.of( + List.of( + // the original ResourceCollection + rc1, + // a duplicate entry + Resource.newResource(two), + // a new entry + Resource.newResource(twoDir) + ) + ); + + URI[] expected = new URI[] { + one.toUri(), + two.toUri(), + three.toUri(), + twoDir.toUri() + }; + + List actual = new ArrayList<>(); + for (Resource res: rc2.getResources()) + { + actual.add(res.getURI()); + } + assertThat(actual, contains(expected)); + } + + @Test + public void testUserSpaceConfigurationNoGlob() throws Exception + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + Path content = foo.resolve("test.txt"); + Files.writeString(content, "Test"); + + // This represents the user-space raw configuration + String config = String.format("%s,%s,%s", dir, foo, bar); + + // To use this, we need to split it (and optionally honor globs) + List uris = Resource.split(config); + // Now let's create a ResourceCollection from this list of URIs + // Since this is user space, we cannot know ahead of time what + // this list contains, so we mount because we assume there + // will be necessary things to mount + try (Resource.Mount mount = Resource.mountCollection(uris)) + { + ResourceCollection rc = (ResourceCollection)mount.root(); + assertThat(getContent(rc, "test.txt"), is("Test")); + } + } + + @Test + public void testUserSpaceConfigurationWithGlob() throws Exception + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + createJar(bar.resolve("lib-foo.jar"), "/test.txt", "Test inside lib-foo.jar"); + createJar(bar.resolve("lib-zed.jar"), "/testZed.txt", "TestZed inside lib-zed.jar"); + + // This represents the user-space raw configuration with a glob + String config = String.format("%s;%s;%s%s*", dir, foo, bar, File.separator); + + // To use this, we need to split it (and optionally honor globs) + List uris = Resource.split(config); + // Now let's create a ResourceCollection from this list of URIs + // Since this is user space, we cannot know ahead of time what + // this list contains, so we mount because we assume there + // will be necessary things to mount + try (Resource.Mount mount = Resource.mountCollection(uris)) + { + ResourceCollection rc = (ResourceCollection)mount.root(); + assertThat(getContent(rc, "test.txt"), is("Test inside lib-foo.jar")); + assertThat(getContent(rc, "testZed.txt"), is("TestZed inside lib-zed.jar")); + } + } + + private void createJar(Path outputJar, String entryName, String entryContents) throws IOException + { + Map env = new HashMap<>(); + env.put("create", "true"); + + URI uri = URI.create("jar:" + outputJar.toUri().toASCIIString()); + try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) + { + Files.writeString(zipfs.getPath(entryName), entryContents, StandardCharsets.UTF_8); + } } static String getContent(Resource r, String path) throws Exception { - Resource resource = r.resolve(path); - StringBuilder buffer = new StringBuilder(); - try (InputStream in = resource.newInputStream(); - InputStreamReader reader = new InputStreamReader(in); - BufferedReader br = new BufferedReader(reader)) - { - String line; - while ((line = br.readLine()) != null) - { - buffer.append(line); - } - } - return buffer.toString(); + return Files.readString(r.resolve(path).getPath(), StandardCharsets.UTF_8); } } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index fb615a01478..936284e0fad 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -27,6 +27,8 @@ import java.util.stream.Stream; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.IO; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; @@ -35,19 +37,25 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +@ExtendWith(WorkDirExtension.class) public class ResourceTest { private static final boolean DIR = true; @@ -251,6 +259,8 @@ public class ResourceTest return cases.stream(); } + public WorkDir workDir; + @AfterAll public static void tearDown() { @@ -393,4 +403,210 @@ public class ResourceTest assertNotNull(same); assertTrue(same.isAlias()); } + + @Test + public void testJarReferenceAsURINotYetMounted() throws Exception + { + Path jar = MavenTestingUtils.getTestResourcePathFile("example.jar"); + URI jarFileUri = Resource.toJarFileUri(jar.toUri()); + assertNotNull(jarFileUri); + assertThrows(IllegalStateException.class, () -> Resource.newResource(jarFileUri)); + } + + @ParameterizedTest + @ValueSource(strings = { + "file:/home/user/.m2/repository/com/company/1.0/company-1.0.jar", + "jar:file:/home/user/.m2/repository/com/company/1.0/company-1.0.jar!/", + "jar:file:/home/user/.m2/repository/com/company/1.0/company-1.0.jar", + "file:/home/user/install/jetty-home-12.0.0.zip", + "file:/opt/websites/webapps/company.war", + "jar:file:/home/user/.m2/repository/jakarta/servlet/jakarta.servlet-api/6.0.0/jakarta.servlet-api-6.0.0.jar!/META-INF/resources" + }) + public void testIsArchiveUriTrue(String rawUri) + { + assertTrue(Resource.isArchive(URI.create(rawUri)), "Should be detected as a JAR URI: " + rawUri); + } + + @ParameterizedTest + @ValueSource(strings = { + "jar:file:/home/user/project/with.jar/in/path/name", + "file:/home/user/project/directory/", + "file:/home/user/hello.ear", + "/home/user/hello.jar", + "/home/user/app.war" + }) + public void testIsArchiveUriFalse(String rawUri) + { + assertFalse(Resource.isArchive(URI.create(rawUri)), "Should be detected as a JAR URI: " + rawUri); + } + + public static Stream jarFileUriCases() + { + List cases = new ArrayList<>(); + + String expected = "jar:file:/path/company-1.0.jar!/"; + cases.add(Arguments.of("file:/path/company-1.0.jar", expected)); + cases.add(Arguments.of("jar:file:/path/company-1.0.jar", expected)); + cases.add(Arguments.of("jar:file:/path/company-1.0.jar!/", expected)); + cases.add(Arguments.of("jar:file:/path/company-1.0.jar!/META-INF/services", expected + "META-INF/services")); + + expected = "jar:file:/opt/jetty/webapps/app.war!/"; + cases.add(Arguments.of("file:/opt/jetty/webapps/app.war", expected)); + cases.add(Arguments.of("jar:file:/opt/jetty/webapps/app.war", expected)); + cases.add(Arguments.of("jar:file:/opt/jetty/webapps/app.war!/", expected)); + cases.add(Arguments.of("jar:file:/opt/jetty/webapps/app.war!/WEB-INF/classes", expected + "WEB-INF/classes")); + + return cases.stream(); + } + + @ParameterizedTest + @MethodSource("jarFileUriCases") + public void testToJarFileUri(String inputRawUri, String expectedRawUri) + { + URI actual = Resource.toJarFileUri(URI.create(inputRawUri)); + assertNotNull(actual); + assertThat(actual.toASCIIString(), is(expectedRawUri)); + } + + public static Stream unwrapContainerCases() + { + return Stream.of( + Arguments.of("/path/to/foo.jar", "file:///path/to/foo.jar"), + Arguments.of("/path/to/bogus.txt", "file:///path/to/bogus.txt"), + Arguments.of("file:///path/to/zed.jar", "file:///path/to/zed.jar"), + Arguments.of("jar:file:///path/to/bar.jar!/internal.txt", "file:///path/to/bar.jar") + ); + } + + @ParameterizedTest + @MethodSource("unwrapContainerCases") + public void testUnwrapContainer(String inputRawUri, String expected) + { + URI input = Resource.toURI(inputRawUri); + URI actual = Resource.unwrapContainer(input); + assertThat(actual.toASCIIString(), is(expected)); + } + + @Test + public void testSplitSingleJar() + { + // Bad java file.uri syntax + String input = "file:/home/user/lib/acme.jar"; + List uris = Resource.split(input); + String expected = String.format("jar:%s!/", input); + assertThat(uris.get(0).toString(), is(expected)); + } + + @Test + public void testSplitSinglePath() + { + String input = "/home/user/lib/acme.jar"; + List uris = Resource.split(input); + String expected = String.format("jar:file://%s!/", input); + assertThat(uris.get(0).toString(), is(expected)); + } + + @Test + public void testSplitOnComma() + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + + // This represents the user-space raw configuration + String config = String.format("%s,%s,%s", dir, foo, bar); + + // Split using commas + List uris = Resource.split(config); + + URI[] expected = new URI[] { + dir.toUri(), + foo.toUri(), + bar.toUri() + }; + assertThat(uris, contains(expected)); + } + + @Test + public void testSplitOnPipe() + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + + // This represents the user-space raw configuration + String config = String.format("%s|%s|%s", dir, foo, bar); + + // Split using commas + List uris = Resource.split(config); + + URI[] expected = new URI[] { + dir.toUri(), + foo.toUri(), + bar.toUri() + }; + assertThat(uris, contains(expected)); + } + + @Test + public void testSplitOnSemicolon() + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + + // This represents the user-space raw configuration + String config = String.format("%s;%s;%s", dir, foo, bar); + + // Split using commas + List uris = Resource.split(config); + + URI[] expected = new URI[] { + dir.toUri(), + foo.toUri(), + bar.toUri() + }; + assertThat(uris, contains(expected)); + } + + @Test + public void testSplitOnPipeWithGlob() throws IOException + { + Path base = workDir.getEmptyPathDir(); + Path dir = base.resolve("dir"); + FS.ensureDirExists(dir); + Path foo = dir.resolve("foo"); + FS.ensureDirExists(foo); + Path bar = dir.resolve("bar"); + FS.ensureDirExists(bar); + FS.touch(bar.resolve("lib-foo.jar")); + FS.touch(bar.resolve("lib-zed.zip")); + + // This represents the user-space raw configuration with a glob + String config = String.format("%s;%s;%s%s*", dir, foo, bar, File.separator); + + // Split using commas + List uris = Resource.split(config); + + URI[] expected = new URI[] { + dir.toUri(), + foo.toUri(), + // Should see the two archives as `jar:file:` URI entries + Resource.toJarFileUri(bar.resolve("lib-foo.jar").toUri()), + Resource.toJarFileUri(bar.resolve("lib-zed.zip").toUri()) + }; + assertThat(uris, contains(expected)); + } } diff --git a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt index 8676a0fa234..c35eada3780 100644 --- a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt +++ b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/one/dir/1.txt @@ -1 +1 @@ -1 - one \ No newline at end of file +1 - one (in dir) \ No newline at end of file diff --git a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt index b074b8adf8b..add4cbd70ff 100644 --- a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt +++ b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/three/dir/3.txt @@ -1 +1 @@ -3 - three \ No newline at end of file +3 - three (in dir) \ No newline at end of file diff --git a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt index 49a910ec171..d44f9b9dede 100644 --- a/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt +++ b/jetty-core/jetty-util/src/test/resources/org/eclipse/jetty/util/resource/two/dir/2.txt @@ -1 +1 @@ -2 - two \ No newline at end of file +2 - two (in dir) \ No newline at end of file diff --git a/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestAnnotationDecorator.java b/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestAnnotationDecorator.java index 4e5ce96ae24..db2e09cfe44 100644 --- a/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestAnnotationDecorator.java +++ b/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestAnnotationDecorator.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.ee10.annotations; +import java.nio.file.Files; import java.nio.file.Path; import org.eclipse.jetty.ee10.plus.annotation.LifeCycleCallbackCollection; @@ -21,10 +22,13 @@ import org.eclipse.jetty.ee10.servlet.Source; import org.eclipse.jetty.ee10.webapp.MetaData; import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.ee10.webapp.WebDescriptor; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlParser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -32,13 +36,16 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(WorkDirExtension.class) public class TestAnnotationDecorator { + public WorkDir workDir; + public class TestWebDescriptor extends WebDescriptor { - public TestWebDescriptor(MetaData.Complete metadata) + public TestWebDescriptor(Resource resource, MetaData.Complete metadata) { - super(Resource.newResource(Path.of("."))); + super(resource); _metaDataComplete = metadata; } @@ -78,6 +85,10 @@ public class TestAnnotationDecorator @Test public void testAnnotationDecorator() throws Exception { + Path dummyXml = workDir.getEmptyPathDir().resolve("dummy.xml"); + Files.createFile(dummyXml); + Resource dummyXmlResource = Resource.newResource(dummyXml); + assertThrows(NullPointerException.class, () -> { new AnnotationDecorator(null); @@ -96,7 +107,7 @@ public class TestAnnotationDecorator context.removeAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION); //test with BaseHolder metadata, should not introspect with metdata-complete==true - context.getMetaData().setWebDescriptor(new TestWebDescriptor(MetaData.Complete.True)); + context.getMetaData().setWebDescriptor(new TestWebDescriptor(dummyXmlResource, MetaData.Complete.True)); assertTrue(context.getMetaData().isMetaDataComplete()); ServletHolder holder = new ServletHolder(new Source(Source.Origin.DESCRIPTOR, "")); holder.setHeldClass(ServletE.class); @@ -112,7 +123,7 @@ public class TestAnnotationDecorator context.removeAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION); //test with BaseHolder metadata, should introspect with metadata-complete==false - context.getMetaData().setWebDescriptor(new TestWebDescriptor(MetaData.Complete.False)); + context.getMetaData().setWebDescriptor(new TestWebDescriptor(dummyXmlResource, MetaData.Complete.False)); DecoratedObjectFactory.associateInfo(holder); decorator = new AnnotationDecorator(context); decorator.decorate(servlet); diff --git a/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestRunAsAnnotation.java b/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestRunAsAnnotation.java index 173de096fcf..56d9ace9f4b 100644 --- a/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestRunAsAnnotation.java +++ b/jetty-ee10/jetty-ee10-annotations/src/test/java/org/eclipse/jetty/ee10/annotations/TestRunAsAnnotation.java @@ -13,22 +13,32 @@ package org.eclipse.jetty.ee10.annotations; -import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.ee10.webapp.WebDescriptor; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.junit.jupiter.api.Assertions.assertEquals; +@ExtendWith(WorkDirExtension.class) public class TestRunAsAnnotation { + public WorkDir workDir; + @Test public void testRunAsAnnotation() throws Exception { + Path dummyXml = workDir.getEmptyPathDir().resolve("dummy.xml"); + Files.createFile(dummyXml); + Resource dummyXmlResource = Resource.newResource(dummyXml); + WebAppContext wac = new WebAppContext(); //pre-add a servlet but not by descriptor @@ -44,8 +54,7 @@ public class TestRunAsAnnotation holder2.setHeldClass(ServletC.class); holder2.setInitOrder(1); wac.getServletHandler().addServletWithMapping(holder2, "/foo2/*"); - Resource fakeXml = Resource.newResource(new File(MavenTestingUtils.getTargetTestingDir("run-as"), "fake.xml").toPath()); - wac.getMetaData().setOrigin(holder2.getName() + ".servlet.run-as", new WebDescriptor(fakeXml)); + wac.getMetaData().setOrigin(holder2.getName() + ".servlet.run-as", new WebDescriptor(dummyXmlResource)); AnnotationIntrospector parser = new AnnotationIntrospector(wac); RunAsAnnotationHandler handler = new RunAsAnnotationHandler(wac); diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-proxy-webapp/src/test/java/org/eclipse/jetty/ee10/demos/ProxyWebAppTest.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-proxy-webapp/src/test/java/org/eclipse/jetty/ee10/demos/ProxyWebAppTest.java index a5a273ecdca..2c18dfdf680 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-proxy-webapp/src/test/java/org/eclipse/jetty/ee10/demos/ProxyWebAppTest.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-proxy-webapp/src/test/java/org/eclipse/jetty/ee10/demos/ProxyWebAppTest.java @@ -56,7 +56,7 @@ public class ProxyWebAppTest // So, open up server classes here, for purposes of this testcase. webapp.getServerClassMatcher().add("-org.eclipse.jetty.ee10.proxy."); webapp.setWar(MavenTestingUtils.getProjectDirPath("src/main/webapp").toString()); - webapp.setExtraClasspath(MavenTestingUtils.getTargetPath().resolve("classes").toString()); + webapp.setExtraClasspath(MavenTestingUtils.getTargetPath().resolve("test-classes").toString()); server.setHandler(webapp); server.start(); diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/AbstractUnassembledWebAppMojo.java b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/AbstractUnassembledWebAppMojo.java index 97a1e1842e2..062afa81b0c 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/AbstractUnassembledWebAppMojo.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/AbstractUnassembledWebAppMojo.java @@ -176,6 +176,8 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo //Still don't have a web.xml file: try the resourceBase of the webapp, if it is set if (webApp.getDescriptor() == null && webApp.getResourceBase() != null) { + // TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF/web.xml + // TODO: should also never return from a META-INF/versions/#/WEB-INF/web.xml location Resource r = webApp.getResourceBase().resolve("WEB-INF/web.xml"); if (r.exists() && !r.isDirectory()) { @@ -186,6 +188,7 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo //Still don't have a web.xml file: finally try the configured static resource directory if there is one if (webApp.getDescriptor() == null && (webAppSourceDirectory != null)) { + // TODO: fix, use Resource or Path File f = new File(new File(webAppSourceDirectory, "WEB-INF"), "web.xml"); if (f.exists() && f.isFile()) { diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebAppContext.java b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebAppContext.java index 31b52108822..f53e85d129e 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebAppContext.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebAppContext.java @@ -17,12 +17,15 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.MalformedURLException; +import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Stream; import org.eclipse.jetty.ee10.plus.webapp.EnvConfiguration; import org.eclipse.jetty.ee10.quickstart.QuickStartConfiguration; @@ -34,6 +37,7 @@ import org.eclipse.jetty.ee10.webapp.Configuration; import org.eclipse.jetty.ee10.webapp.Configurations; import org.eclipse.jetty.ee10.webapp.MetaInfConfiguration; import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; @@ -69,7 +73,7 @@ public class MavenWebAppContext extends WebAppContext private final Map _webInfJarMap = new HashMap(); - private List _classpathFiles; // webInfClasses+testClasses+webInfJars + private List _classpathUris; // webInfClasses+testClasses+webInfJars private String _jettyEnvXml; @@ -96,6 +100,12 @@ public class MavenWebAppContext extends WebAppContext */ private boolean _baseAppFirst = true; + /** + * Used to track any resource bases that are mounted + * as a result of calling {@link #setResourceBases(String[])} + */ + private Resource.Mount _mountedResourceBases; + public MavenWebAppContext() throws Exception { super(); @@ -123,9 +133,9 @@ public class MavenWebAppContext extends WebAppContext _webInfIncludeJarPattern = pattern; } - public List getClassPathFiles() + public List getClassPathUris() { - return this._classpathFiles; + return this._classpathUris; } public void setJettyEnvXml(String jettyEnvXml) @@ -214,21 +224,27 @@ public class MavenWebAppContext extends WebAppContext * configuration * * @param resourceBases Array of resources strings to set as a - * {@link ResourceCollection}. Each resource string may be a - * comma separated list of resources + * {@link ResourceCollection}. */ public void setResourceBases(String[] resourceBases) { - List resources = new ArrayList(); - for (String rl : resourceBases) + try { - String[] rs = StringUtil.csvSplit(rl); - for (String r : rs) - { - resources.add(r); - } + // TODO: what happens if this is called more than once? + + // This is a user provided list of configurations. + // We have to assume that mounting can happen. + List uris = Stream.of(resourceBases) + .map(URI::create) + .toList(); + _mountedResourceBases = Resource.mountCollection(uris); + + setBaseResource(_mountedResourceBases.root()); + } + catch (Throwable t) + { + throw new IllegalArgumentException("Bad resourceBases: [" + String.join(", ", resourceBases) + "]", t); } - setBaseResource(new ResourceCollection(resources.toArray(new String[resources.size()]))); } public List getWebInfLib() @@ -267,15 +283,21 @@ public class MavenWebAppContext extends WebAppContext // Set up the classes dirs that comprises the equivalent of // WEB-INF/classes - if (_testClasses != null) + if (_testClasses != null && _testClasses.exists()) _webInfClasses.add(_testClasses); - if (_classes != null) + if (_classes != null && _classes.exists()) _webInfClasses.add(_classes); // Set up the classpath - _classpathFiles = new ArrayList<>(); - _classpathFiles.addAll(_webInfClasses); - _classpathFiles.addAll(_webInfJars); + _classpathUris = new ArrayList<>(); + _webInfClasses.forEach(f -> _classpathUris.add(f.toURI())); + _webInfJars.forEach(f -> + { + // ensure our JAR file references are `jar:file:...` URI references + URI jarFileUri = Resource.toJarFileUri(f.toURI()); + // else use file uri as-is + _classpathUris.add(Objects.requireNonNullElseGet(jarFileUri, f::toURI)); + }); // Initialize map containing all jars in /WEB-INF/lib _webInfJarMap.clear(); @@ -321,9 +343,9 @@ public class MavenWebAppContext extends WebAppContext @Override public void doStop() throws Exception { - if (_classpathFiles != null) - _classpathFiles.clear(); - _classpathFiles = null; + if (_classpathUris != null) + _classpathUris.clear(); + _classpathUris = null; _classes = null; _testClasses = null; @@ -348,6 +370,8 @@ public class MavenWebAppContext extends WebAppContext getServletHandler().setFilterMappings(new FilterMapping[0]); getServletHandler().setServlets(new ServletHolder[0]); getServletHandler().setServletMappings(new ServletMapping[0]); + + IO.close(_mountedResourceBases); } @Override diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebInfConfiguration.java b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebInfConfiguration.java index b5665169ca0..d69ee53cc14 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/MavenWebInfConfiguration.java @@ -13,7 +13,7 @@ package org.eclipse.jetty.ee10.maven.plugin; -import java.io.File; +import java.net.URI; import org.eclipse.jetty.ee10.webapp.Configuration; import org.eclipse.jetty.ee10.webapp.WebAppClassLoader; @@ -52,14 +52,13 @@ public class MavenWebInfConfiguration extends WebInfConfiguration MavenWebAppContext jwac = (MavenWebAppContext)context; //put the classes dir and all dependencies into the classpath - if (jwac.getClassPathFiles() != null && context.getClassLoader() instanceof WebAppClassLoader) + if (jwac.getClassPathUris() != null && context.getClassLoader() instanceof WebAppClassLoader loader) { if (LOG.isDebugEnabled()) LOG.debug("Setting up classpath ..."); - WebAppClassLoader loader = (WebAppClassLoader)context.getClassLoader(); - for (File classpath : jwac.getClassPathFiles()) + for (URI uri : jwac.getClassPathUris()) { - loader.addClassPath(classpath.getCanonicalPath()); + loader.addClassPath(uri.toASCIIString()); } } diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/OverlayManager.java b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/OverlayManager.java index 03b3a6cc2f7..8c5779a36dc 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/OverlayManager.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/OverlayManager.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; /** * OverlayManager @@ -63,7 +62,8 @@ public class OverlayManager else resourceBases.add(webApp.getResourceBase()); } - webApp.setBaseResource(new ResourceCollection(resourceBases.toArray(new Resource[resourceBases.size()]))); + + webApp.setBaseResource(Resource.of(resourceBases)); } /** diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/WebAppPropertyConverter.java b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/WebAppPropertyConverter.java index 7235df3e64e..7c7d430f862 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/WebAppPropertyConverter.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/main/java/org/eclipse/jetty/ee10/maven/plugin/WebAppPropertyConverter.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.maven.plugin; import java.io.BufferedWriter; import java.io.File; import java.io.InputStream; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -222,9 +223,13 @@ public class WebAppPropertyConverter str = webAppProperties.getProperty(BASE_DIRS); if (!StringUtil.isBlank(str)) { - ResourceCollection bases = new ResourceCollection(StringUtil.csvSplit(str)); + // This is a use provided list of overlays, which could have mountable entries. + List uris = Resource.split(str); + // TODO: need a better place to close/release this mount. + Resource.Mount mount = Resource.mountCollection(uris); + webApp.addBean(mount); // let jetty-core ContextHandler.doStop() release mount webApp.setWar(null); - webApp.setBaseResource(bases); + webApp.setBaseResource(mount.root()); } str = webAppProperties.getProperty(WAR_FILE); diff --git a/jetty-ee10/jetty-ee10-maven-plugin/src/test/java/org/eclipse/jetty/ee10/maven/plugin/TestWebAppPropertyConverter.java b/jetty-ee10/jetty-ee10-maven-plugin/src/test/java/org/eclipse/jetty/ee10/maven/plugin/TestWebAppPropertyConverter.java index 0b831488176..06c1215f25f 100644 --- a/jetty-ee10/jetty-ee10-maven-plugin/src/test/java/org/eclipse/jetty/ee10/maven/plugin/TestWebAppPropertyConverter.java +++ b/jetty-ee10/jetty-ee10-maven-plugin/src/test/java/org/eclipse/jetty/ee10/maven/plugin/TestWebAppPropertyConverter.java @@ -15,7 +15,10 @@ package org.eclipse.jetty.ee10.maven.plugin; import java.io.File; import java.io.FileInputStream; +import java.net.URI; import java.util.Arrays; +import java.util.List; +import java.util.Objects; import java.util.Properties; import org.eclipse.jetty.ee10.webapp.WebAppContext; @@ -30,6 +33,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -149,7 +153,10 @@ public class TestWebAppPropertyConverter assertEquals(war.getAbsolutePath(), webApp.getWar()); assertEquals(webXml.getAbsolutePath(), webApp.getDescriptor()); assertThat(webApp.getResourceBase(), instanceOf(ResourceCollection.class)); - assertThat(webApp.getResourceBase().toString(), Matchers.containsString(Resource.newResource(base1.toPath()).toString())); - assertThat(webApp.getResourceBase().toString(), Matchers.containsString(Resource.newResource(base2.toPath()).toString())); + + ResourceCollection resourceCollection = (ResourceCollection)webApp.getResourceBase(); + List actual = resourceCollection.getResources().stream().filter(Objects::nonNull).map(Resource::getURI).toList(); + URI[] expected = new URI[]{base1.toURI(), base2.toURI()}; + assertThat(actual, containsInAnyOrder(expected)); } } diff --git a/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/OSGiMetaInfConfiguration.java b/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/OSGiMetaInfConfiguration.java index c3eeba7450c..e15b5a26191 100644 --- a/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/OSGiMetaInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/OSGiMetaInfConfiguration.java @@ -263,8 +263,8 @@ public class OSGiMetaInfConfiguration extends MetaInfConfiguration Resource[] resources = new Resource[1 + prependedResourcesPath.size()]; System.arraycopy(prependedResourcesPath.values().toArray(new Resource[prependedResourcesPath.size()]), 0, resources, 0, prependedResourcesPath.size()); resources[resources.length - 1] = context.getBaseResource(); - //TODO needs WebAppContext ResourceCollection fixed - //context.setBaseResource(new ResourceCollection(resources)); + + context.setBaseResource(Resource.of(resources)); } } diff --git a/jetty-ee10/jetty-ee10-plus/src/main/java/org/eclipse/jetty/ee10/plus/webapp/EnvConfiguration.java b/jetty-ee10/jetty-ee10-plus/src/main/java/org/eclipse/jetty/ee10/plus/webapp/EnvConfiguration.java index 3c899cd8be8..b3c249fb304 100644 --- a/jetty-ee10/jetty-ee10-plus/src/main/java/org/eclipse/jetty/ee10/plus/webapp/EnvConfiguration.java +++ b/jetty-ee10/jetty-ee10-plus/src/main/java/org/eclipse/jetty/ee10/plus/webapp/EnvConfiguration.java @@ -94,6 +94,8 @@ public class EnvConfiguration extends AbstractConfiguration org.eclipse.jetty.util.resource.Resource webInf = context.getWebInf(); if (webInf != null && webInf.isDirectory()) { + // TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF/jetty-env.xml + // TODO: should also never return from a META-INF/versions/#/WEB-INF/jetty-env.xml location org.eclipse.jetty.util.resource.Resource jettyEnv = webInf.resolve("jetty-env.xml"); if (jettyEnv.exists()) { diff --git a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartConfiguration.java b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartConfiguration.java index 76f1f758c1f..98c8018188c 100644 --- a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartConfiguration.java +++ b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartConfiguration.java @@ -247,6 +247,8 @@ public class QuickStartConfiguration extends AbstractConfiguration Resource qstart; if (attr == null || StringUtil.isBlank(attr.toString())) { + // TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF/quickstart-web.xml + // TODO: should also never return from a META-INF/versions/#/WEB-INF/quickstart-web.xml location qstart = webInf.resolve("quickstart-web.xml"); } else diff --git a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartDescriptorProcessor.java b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartDescriptorProcessor.java index 5de4292058a..523498e2014 100644 --- a/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartDescriptorProcessor.java +++ b/jetty-ee10/jetty-ee10-quickstart/src/main/java/org/eclipse/jetty/ee10/quickstart/QuickStartDescriptorProcessor.java @@ -38,7 +38,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.xml.XmlParser; /** @@ -271,18 +270,15 @@ public class QuickStartDescriptorProcessor extends IterativeDescriptorProcessor Collection metaInfResources = (Collection)context.getAttribute(MetaInfConfiguration.METAINF_RESOURCES); if (metaInfResources == null) { - metaInfResources = new HashSet(); + metaInfResources = new HashSet<>(); context.setAttribute(MetaInfConfiguration.METAINF_RESOURCES, metaInfResources); } metaInfResources.add(dir); + //also add to base resource of webapp - Resource[] collection = new Resource[metaInfResources.size() + 1]; - int i = 0; - collection[i++] = context.getResourceBase(); - for (Resource resource : metaInfResources) - { - collection[i++] = resource; - } - context.setBaseResource(new ResourceCollection(collection)); + List collection = new ArrayList<>(); + collection.add(context.getResourceBase()); + collection.addAll(metaInfResources); + context.setBaseResource(Resource.of(collection)); } } diff --git a/jetty-ee10/jetty-ee10-quickstart/src/test/resources/jetty-logging.properties b/jetty-ee10/jetty-ee10-quickstart/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..acf261b85da --- /dev/null +++ b/jetty-ee10/jetty-ee10-quickstart/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +# Jetty Logging using jetty-slf4j-impl +org.eclipse.jetty.LEVEL=INFO +# org.eclipse.jetty.ee10.webapp.LEVEL=DEBUG +# org.eclipse.jetty.ee10.quickstart.LEVEL=DEBUG diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/Descriptor.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/Descriptor.java index 43fd05613d7..16c49249eb0 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/Descriptor.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/Descriptor.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.ee10.webapp; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardOpenOption; @@ -20,9 +21,13 @@ import java.util.Objects; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public abstract class Descriptor { + private static final Logger LOG = LoggerFactory.getLogger(Descriptor.class); + protected Resource _xml; protected XmlParser.Node _root; protected String _dtd; @@ -30,12 +35,15 @@ public abstract class Descriptor public Descriptor(Resource xml) { _xml = Objects.requireNonNull(xml); + if (!_xml.exists()) + throw new IllegalArgumentException("Descriptor does not exist: " + xml); + if (_xml.isDirectory()) + throw new IllegalArgumentException("Descriptor is not a file: " + xml); } public void parse(XmlParser parser) throws Exception { - if (_root == null) { Objects.requireNonNull(parser); @@ -44,6 +52,11 @@ public abstract class Descriptor _root = parser.parse(is); _dtd = parser.getDTD(); } + catch (IOException e) + { + LOG.warn("Unable to parse {}", _xml, e); + throw e; + } } } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaData.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaData.java index 7f98cc9b4c6..459ab48d864 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaData.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaData.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.ee10.webapp; import java.lang.annotation.Annotation; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -455,13 +456,10 @@ public class MetaData { orderedWebInfJars = getWebInfResources(true); List orderedLibs = new ArrayList<>(); - for (Resource webInfJar : orderedWebInfJars) + for (Resource jar: orderedWebInfJars) { - //get just the name of the jar file - String fullname = webInfJar.getName(); - int i = fullname.indexOf(".jar"); - int j = fullname.lastIndexOf("/", i); - orderedLibs.add(fullname.substring(j + 1, i + 4)); + URI uri = Resource.unwrapContainer(jar.getURI()); + orderedLibs.add(uri.getPath()); } context.setAttribute(ServletContext.ORDERED_LIBS, Collections.unmodifiableList(orderedLibs)); } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java index 86eb52a5d56..5b1c9bc231f 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/MetaInfConfiguration.java @@ -45,7 +45,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.PatternMatcher; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -337,14 +336,10 @@ public class MetaInfConfiguration extends AbstractConfiguration Set resources = (Set)context.getAttribute(RESOURCE_DIRS); if (resources != null && !resources.isEmpty()) { - Resource[] collection = new Resource[resources.size() + 1]; - int i = 0; - collection[i++] = context.getResourceBase(); - for (Resource resource : resources) - { - collection[i++] = resource; - } - context.setBaseResource(new ResourceCollection(collection)); + List collection = new ArrayList<>(); + collection.add(context.getResourceBase()); + collection.addAll(resources); + context.setBaseResource(Resource.of(collection)); } } @@ -847,6 +842,7 @@ public class MetaInfConfiguration extends AbstractConfiguration return null; return context.getExtraClasspath() + .getResources() .stream() .filter(this::isFileSupported) .collect(Collectors.toList()); @@ -892,6 +888,7 @@ public class MetaInfConfiguration extends AbstractConfiguration return null; return context.getExtraClasspath() + .getResources() .stream() .filter(Resource::isDirectory) .collect(Collectors.toList()); @@ -912,24 +909,6 @@ public class MetaInfConfiguration extends AbstractConfiguration private boolean isFileSupported(Resource resource) { - try - { - if (resource.isDirectory()) - return false; - - if (resource.getPath() == null) - return false; - } - catch (Throwable t) - { - if (LOG.isDebugEnabled()) - LOG.debug("Bad Resource reference: {}", resource, t); - return false; - } - - String filenameLowercase = resource.getName().toLowerCase(Locale.ENGLISH); - int dot = filenameLowercase.lastIndexOf('.'); - String extension = (dot < 0 ? null : filenameLowercase.substring(dot)); - return (extension != null && (extension.equals(".jar") || extension.equals(".zip"))); + return Resource.isArchive(resource.getURI()); } } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppClassLoader.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppClassLoader.java index 103a28cb468..25a79b1d359 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppClassLoader.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppClassLoader.java @@ -17,8 +17,10 @@ import java.io.IOException; import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; import java.security.CodeSource; import java.security.PermissionCollection; @@ -33,12 +35,12 @@ import java.util.Locale; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; import org.eclipse.jetty.util.ClassVisibilityChecker; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; @@ -78,6 +80,8 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility private String _name = String.valueOf(hashCode()); private final List _transformers = new CopyOnWriteArrayList<>(); + private Resource.Mount _mountedExtraClassPath; + /** * The Context in which the classloader operates. */ @@ -107,7 +111,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility */ boolean isParentLoaderPriority(); - List getExtraClasspath(); + ResourceCollection getExtraClasspath(); boolean isServerResource(String name, URL parentUrl); @@ -189,7 +193,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility if (context.getExtraClasspath() != null) { - for (Resource resource : context.getExtraClasspath()) + for (Resource resource : context.getExtraClasspath().getResources()) { addClassPath(resource); } @@ -267,7 +271,11 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility if (classPath == null) return; - for (Resource resource : Resource.fromList(classPath, false, _context::newResource)) + List uris = Resource.split(classPath); + _mountedExtraClassPath = Resource.mountCollection(uris); + + ResourceCollection rc = (ResourceCollection)_mountedExtraClassPath.root(); + for (Resource resource : rc.getResources()) { addClassPath(resource); } @@ -275,11 +283,19 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility /** * @param file Checks if this file type can be added to the classpath. + * TODO: move to FileID in later PR */ + private boolean isFileSupported(String file) { int dot = file.lastIndexOf('.'); - return dot != -1 && _extensions.contains(file.substring(dot)); + return dot != -1 && _extensions.contains(file.substring(dot).toLowerCase(Locale.ENGLISH)); + } + + // TODO: move to FileID in later PR + private boolean isFileSupported(Path path) + { + return isFileSupported(path.getFileName().toString()); } /** @@ -292,32 +308,35 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility { if (lib.exists() && lib.isDirectory()) { - List entries = lib.list(); - if (entries != null) - { - entries.sort(Comparator.naturalOrder()); + Path dir = lib.getPath(); - for (String entry : entries) + try (Stream streamEntries = Files.list(dir)) + { + List jars = streamEntries + .filter(Files::isRegularFile) + .filter(this::isFileSupported) + .sorted(Comparator.naturalOrder()) + .toList(); + + for (Path jar: jars) { try { - Resource resource = lib.resolve(entry); if (LOG.isDebugEnabled()) - LOG.debug("addJar - {}", resource); - String fnlc = resource.getName().toLowerCase(Locale.ENGLISH); - // don't check if this is a directory (prevents use of symlinks), see Bug 353165 - if (isFileSupported(fnlc)) - { - String jar = URIUtil.encodeSpecific(resource.toString(), ",;"); - addClassPath(jar); - } + LOG.debug("addJar - {}", jar); + URI jarUri = Resource.toJarFileUri(jar.toUri()); + addClassPath(jarUri.toASCIIString()); } catch (Exception ex) { - LOG.warn("Unable to load WEB-INF/lib JAR {}", entry, ex); + LOG.warn("Unable to load WEB-INF/lib JAR {}", jar, ex); } } } + catch (IOException e) + { + LOG.warn("Unable to load WEB-INF/lib JARs: {}", dir, e); + } } } @@ -636,6 +655,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility public void close() throws IOException { super.close(); + IO.close(_mountedExtraClassPath); } @Override diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index f458fbbdcf5..b79300e5abd 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.webapp; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; @@ -129,7 +130,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL private boolean _persistTmpDir = false; private String _war; - private List _extraClasspath; + private ResourceCollection _extraClasspath; private Throwable _unavailableException; private Map _resourceAliases; @@ -803,10 +804,12 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL return null; // Iw there a WEB-INF directory? - Resource webInf = getResourceBase().resolve("WEB-INF/"); + Resource webInf = getResourceBase().resolve("WEB-INF/"); // TODO: what does this do in a collection? if (!webInf.exists() || !webInf.isDirectory()) return null; + // TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF + // TODO: should also never return from a META-INF/versions/#/WEB-INF location return webInf; } @@ -1223,7 +1226,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL */ @Override @ManagedAttribute(value = "extra classpath for context classloader", readonly = true) - public List getExtraClasspath() + public ResourceCollection getExtraClasspath() { return _extraClasspath; } @@ -1231,21 +1234,23 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL /** * Set the Extra ClassPath via delimited String. *

    - * This is a convenience method for {@link #setExtraClasspath(List)} + * This is a convenience method for {@link #setExtraClasspath(ResourceCollection)} *

    * * @param extraClasspath Comma or semicolon separated path of filenames or URLs * pointing to directories or jar files. Directories should end * with '/'. - * @throws IOException if unable to resolve the resources referenced - * @see #setExtraClasspath(List) + * @see #setExtraClasspath(ResourceCollection) */ - public void setExtraClasspath(String extraClasspath) throws IOException + public void setExtraClasspath(String extraClasspath) { - setExtraClasspath(Resource.fromList(extraClasspath, false, this::newResource)); + List uris = Resource.split(extraClasspath); + Resource.Mount mount = Resource.mountCollection(uris); + addBean(mount); // let doStop() cleanup mount + setExtraClasspath((ResourceCollection)mount.root()); } - public void setExtraClasspath(List extraClasspath) + public void setExtraClasspath(ResourceCollection extraClasspath) { _extraClasspath = extraClasspath; } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java index 6b16cd5d6be..ab3284438ce 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java @@ -29,7 +29,6 @@ import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.MountedPathResource; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -452,10 +451,10 @@ public class WebInfConfiguration extends AbstractConfiguration webInf = Resource.newResource(extractedWebInfDir.getCanonicalPath()); - ResourceCollection rc = new ResourceCollection(webInf, webApp); + Resource rc = Resource.of(webInf, webApp); if (LOG.isDebugEnabled()) - LOG.debug("context.resourcebase={}", rc); + LOG.debug("context.baseResource={}", rc); context.setBaseResource(rc); } diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/OrderingTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/OrderingTest.java index f3f81796ea4..19667fe42fb 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/OrderingTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/OrderingTest.java @@ -61,7 +61,7 @@ public class OrderingTest @Override public boolean exists() { - return false; + return true; } @Override @@ -743,12 +743,12 @@ public class OrderingTest public void testRelativeOrderingWithPlainJars() throws Exception { - //B,A,C other jars with no fragments + // B,A,C other jars with no fragments List resources = new ArrayList(); MetaData metaData = new MetaData(); metaData._ordering = new RelativeOrdering(metaData); - //A: after others, before C + // A: after others, before C TestResource jar1 = new TestResource("A"); resources.add(jar1); TestResource r1 = new TestResource("A/web-fragment.xml"); diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java index f3447d6ba96..d8c77a9835f 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/WebAppContextTest.java @@ -14,12 +14,13 @@ package org.eclipse.jetty.ee10.webapp; import java.io.File; +import java.net.URI; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,6 +59,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -486,7 +488,7 @@ public class WebAppContextTest /** * Test using WebAppContext.setExtraClassPath(String) with a reference to a glob */ - @ParameterizedTest(name = "{0}") + @ParameterizedTest @MethodSource("extraClasspathGlob") public void testExtraClasspathGlob(String description, String extraClasspathGlobReference) throws Exception { @@ -510,32 +512,25 @@ public class WebAppContextTest WebAppClassLoader webAppClassLoader = (WebAppClassLoader)contextClassLoader; Path extLibsDir = MavenTestingUtils.getTestResourcePathDir("ext"); extLibsDir = extLibsDir.toAbsolutePath(); - List expectedPaths; + List expectedUris; try (Stream s = Files.list(extLibsDir)) { - expectedPaths = s + expectedUris = s .filter(Files::isRegularFile) - .filter((path) -> path.toString().endsWith(".jar")) + .filter((path) -> path.getFileName().toString().endsWith(".jar")) + .sorted(Comparator.naturalOrder()) + .map(Path::toUri) + .map(Resource::toJarFileUri) .collect(Collectors.toList()); } - List actualPaths = new ArrayList<>(); + List actualURIs = new ArrayList<>(); for (URL url : webAppClassLoader.getURLs()) { - actualPaths.add(Paths.get(url.toURI())); - } - assertThat("[" + description + "] WebAppClassLoader.urls.length", actualPaths.size(), is(expectedPaths.size())); - for (Path expectedPath : expectedPaths) - { - boolean found = false; - for (Path actualPath : actualPaths) - { - if (Files.isSameFile(actualPath, expectedPath)) - { - found = true; - } - } - assertTrue(found, "[" + description + "] Not able to find expected jar in WebAppClassLoader: " + expectedPath); + actualURIs.add(url.toURI()); } + assertThat("[" + description + "] WebAppClassLoader.urls.length", actualURIs.size(), is(expectedUris.size())); + + assertThat(actualURIs, contains(expectedUris.toArray())); } public static Stream extraClasspathDir() diff --git a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebAppContext.java b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebAppContext.java index 2cf9edf01ea..6abddd15ca6 100644 --- a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebAppContext.java +++ b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebAppContext.java @@ -17,12 +17,15 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.MalformedURLException; +import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Stream; import org.eclipse.jetty.ee9.plus.webapp.EnvConfiguration; import org.eclipse.jetty.ee9.quickstart.QuickStartConfiguration; @@ -34,6 +37,7 @@ import org.eclipse.jetty.ee9.webapp.Configuration; import org.eclipse.jetty.ee9.webapp.Configurations; import org.eclipse.jetty.ee9.webapp.MetaInfConfiguration; import org.eclipse.jetty.ee9.webapp.WebAppContext; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; @@ -69,7 +73,7 @@ public class MavenWebAppContext extends WebAppContext private final Map _webInfJarMap = new HashMap(); - private List _classpathFiles; // webInfClasses+testClasses+webInfJars + private List _classpathUris; // webInfClasses+testClasses+webInfJars private String _jettyEnvXml; @@ -96,6 +100,12 @@ public class MavenWebAppContext extends WebAppContext */ private boolean _baseAppFirst = true; + /** + * Used to track any resource bases that are mounted + * as a result of calling {@link #setResourceBases(String[])} + */ + private Resource.Mount _mountedResourceBases; + public MavenWebAppContext() throws Exception { super(); @@ -123,9 +133,9 @@ public class MavenWebAppContext extends WebAppContext _webInfIncludeJarPattern = pattern; } - public List getClassPathFiles() + public List getClassPathUris() { - return this._classpathFiles; + return this._classpathUris; } public void setJettyEnvXml(String jettyEnvXml) @@ -214,21 +224,27 @@ public class MavenWebAppContext extends WebAppContext * configuration * * @param resourceBases Array of resources strings to set as a - * {@link ResourceCollection}. Each resource string may be a - * comma separated list of resources + * {@link ResourceCollection}. */ public void setResourceBases(String[] resourceBases) { - List resources = new ArrayList(); - for (String rl : resourceBases) + try { - String[] rs = StringUtil.csvSplit(rl); - for (String r : rs) - { - resources.add(r); - } + // TODO: what happens if this is called more than once? + + // This is a user provided list of configurations. + // We have to assume that mounting can happen. + List uris = Stream.of(resourceBases) + .map(URI::create) + .toList(); + _mountedResourceBases = Resource.mountCollection(uris); + + setBaseResource(_mountedResourceBases.root()); + } + catch (Throwable t) + { + throw new IllegalArgumentException("Bad resourceBases: [" + String.join(", ", resourceBases) + "]", t); } - setBaseResource(new ResourceCollection(resources.toArray(new String[resources.size()]))); } public List getWebInfLib() @@ -267,15 +283,21 @@ public class MavenWebAppContext extends WebAppContext // Set up the classes dirs that comprises the equivalent of // WEB-INF/classes - if (_testClasses != null) + if (_testClasses != null && _testClasses.exists()) _webInfClasses.add(_testClasses); - if (_classes != null) + if (_classes != null && _classes.exists()) _webInfClasses.add(_classes); // Set up the classpath - _classpathFiles = new ArrayList<>(); - _classpathFiles.addAll(_webInfClasses); - _classpathFiles.addAll(_webInfJars); + _classpathUris = new ArrayList<>(); + _webInfClasses.forEach(f -> _classpathUris.add(f.toURI())); + _webInfJars.forEach(f -> + { + // ensure our JAR file references are `jar:file:...` URI references + URI jarFileUri = Resource.toJarFileUri(f.toURI()); + // else use file uri as-is + _classpathUris.add(Objects.requireNonNullElseGet(jarFileUri, f::toURI)); + }); // Initialize map containing all jars in /WEB-INF/lib _webInfJarMap.clear(); @@ -321,9 +343,9 @@ public class MavenWebAppContext extends WebAppContext @Override public void doStop() throws Exception { - if (_classpathFiles != null) - _classpathFiles.clear(); - _classpathFiles = null; + if (_classpathUris != null) + _classpathUris.clear(); + _classpathUris = null; _classes = null; _testClasses = null; @@ -348,6 +370,8 @@ public class MavenWebAppContext extends WebAppContext getServletHandler().setFilterMappings(new FilterMapping[0]); getServletHandler().setServlets(new ServletHolder[0]); getServletHandler().setServletMappings(new ServletMapping[0]); + + IO.close(_mountedResourceBases); } @Override diff --git a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebInfConfiguration.java b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebInfConfiguration.java index fc9854464a4..b4636bd223b 100644 --- a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebInfConfiguration.java +++ b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/MavenWebInfConfiguration.java @@ -13,7 +13,7 @@ package org.eclipse.jetty.ee9.maven.plugin; -import java.io.File; +import java.net.URI; import org.eclipse.jetty.ee9.webapp.Configuration; import org.eclipse.jetty.ee9.webapp.WebAppClassLoader; @@ -51,15 +51,15 @@ public class MavenWebInfConfiguration extends WebInfConfiguration { MavenWebAppContext jwac = (MavenWebAppContext)context; - //put the classes dir and all dependencies into the classpath - if (jwac.getClassPathFiles() != null && context.getClassLoader() instanceof WebAppClassLoader) + // put the classes dir and all dependencies into the classpath + if (jwac.getClassPathUris() != null && context.getClassLoader() instanceof WebAppClassLoader loader) { if (LOG.isDebugEnabled()) LOG.debug("Setting up classpath ..."); - WebAppClassLoader loader = (WebAppClassLoader)context.getClassLoader(); - for (File classpath : jwac.getClassPathFiles()) + + for (URI uri : jwac.getClassPathUris()) { - loader.addClassPath(classpath.getCanonicalPath()); + loader.addClassPath(uri.toASCIIString()); } } diff --git a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/OverlayManager.java b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/OverlayManager.java index 93c7fb995d8..519fbf49e08 100644 --- a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/OverlayManager.java +++ b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/OverlayManager.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; /** * OverlayManager @@ -63,8 +62,8 @@ public class OverlayManager else resourceBases.add(webApp.getBaseResource()); } - - webApp.setBaseResource(new ResourceCollection(resourceBases.toArray(new Resource[resourceBases.size()]))); + + webApp.setBaseResource(Resource.of(resourceBases)); } /** diff --git a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/WebAppPropertyConverter.java b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/WebAppPropertyConverter.java index e582c39932e..eaa24e16b3d 100644 --- a/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/WebAppPropertyConverter.java +++ b/jetty-ee9/jetty-ee9-maven-plugin/src/main/java/org/eclipse/jetty/ee9/maven/plugin/WebAppPropertyConverter.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.ee9.maven.plugin; import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; import java.io.InputStream; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -223,9 +223,13 @@ public class WebAppPropertyConverter str = webAppProperties.getProperty(BASE_DIRS); if (!StringUtil.isBlank(str)) { - ResourceCollection bases = new ResourceCollection(StringUtil.csvSplit(str)); + // This is a use provided list of overlays, which could have mountable entries. + List uris = Resource.split(str); + // TODO: need a better place to close/release this mount. + Resource.Mount mount = Resource.mountCollection(uris); + webApp.addBean(mount); // let ee9 ContextHandler.doStop() release mount webApp.setWar(null); - webApp.setBaseResource(bases); + webApp.setBaseResource(mount.root()); } str = webAppProperties.getProperty(WAR_FILE); diff --git a/jetty-ee9/jetty-ee9-maven-plugin/src/test/java/org/eclipse/jetty/ee9/maven/plugin/TestWebAppPropertyConverter.java b/jetty-ee9/jetty-ee9-maven-plugin/src/test/java/org/eclipse/jetty/ee9/maven/plugin/TestWebAppPropertyConverter.java index 03d7d786776..05ab5731a93 100644 --- a/jetty-ee9/jetty-ee9-maven-plugin/src/test/java/org/eclipse/jetty/ee9/maven/plugin/TestWebAppPropertyConverter.java +++ b/jetty-ee9/jetty-ee9-maven-plugin/src/test/java/org/eclipse/jetty/ee9/maven/plugin/TestWebAppPropertyConverter.java @@ -15,7 +15,10 @@ package org.eclipse.jetty.ee9.maven.plugin; import java.io.File; import java.io.FileInputStream; +import java.net.URI; import java.util.Arrays; +import java.util.List; +import java.util.Objects; import java.util.Properties; import org.eclipse.jetty.ee9.webapp.WebAppContext; @@ -30,6 +33,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.instanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -149,7 +153,10 @@ public class TestWebAppPropertyConverter assertEquals(war.getAbsolutePath(), webApp.getWar()); assertEquals(webXml.getAbsolutePath(), webApp.getDescriptor()); assertThat(webApp.getBaseResource(), instanceOf(ResourceCollection.class)); - assertThat(webApp.getBaseResource().toString(), Matchers.containsString(Resource.newResource(base1.toPath()).toString())); - assertThat(webApp.getBaseResource().toString(), Matchers.containsString(Resource.newResource(base2.toPath()).toString())); + + ResourceCollection resourceCollection = (ResourceCollection)webApp.getBaseResource(); + List actual = resourceCollection.getResources().stream().filter(Objects::nonNull).map(Resource::getURI).toList(); + URI[] expected = new URI[]{base1.toURI(), base2.toURI()}; + assertThat(actual, containsInAnyOrder(expected)); } } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java index 9f787c7f93c..e846b277dee 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ContextHandler.java @@ -75,6 +75,7 @@ import org.eclipse.jetty.server.handler.ContextRequest; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.ExceptionUtil; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.MultiMap; @@ -815,6 +816,15 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu } } _programmaticListeners.clear(); + + // cleanup any Mounts associated with the ContextHandler on stop. + // TODO: but what if the context is restarted? how do we remount? do we care? + java.util.Collection mounts = getBeans(Resource.Mount.class); + mounts.forEach((mount) -> + { + IO.close(mount); + removeBean(mount); + }); } finally { diff --git a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/OSGiMetaInfConfiguration.java b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/OSGiMetaInfConfiguration.java index 1b8e80c773d..13c07dd67f2 100644 --- a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/OSGiMetaInfConfiguration.java +++ b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/OSGiMetaInfConfiguration.java @@ -263,7 +263,7 @@ public class OSGiMetaInfConfiguration extends MetaInfConfiguration Resource[] resources = new Resource[1 + prependedResourcesPath.size()]; System.arraycopy(prependedResourcesPath.values().toArray(new Resource[prependedResourcesPath.size()]), 0, resources, 0, prependedResourcesPath.size()); resources[resources.length - 1] = context.getBaseResource(); - context.setBaseResource(new ResourceCollection(resources)); + context.setBaseResource(Resource.of(resources)); } } diff --git a/jetty-ee9/jetty-ee9-quickstart/src/main/java/org/eclipse/jetty/ee9/quickstart/QuickStartDescriptorProcessor.java b/jetty-ee9/jetty-ee9-quickstart/src/main/java/org/eclipse/jetty/ee9/quickstart/QuickStartDescriptorProcessor.java index 3ffc316325c..07d448cfa77 100644 --- a/jetty-ee9/jetty-ee9-quickstart/src/main/java/org/eclipse/jetty/ee9/quickstart/QuickStartDescriptorProcessor.java +++ b/jetty-ee9/jetty-ee9-quickstart/src/main/java/org/eclipse/jetty/ee9/quickstart/QuickStartDescriptorProcessor.java @@ -38,7 +38,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.xml.XmlParser; /** @@ -266,6 +265,7 @@ public class QuickStartDescriptorProcessor extends IterativeDescriptorProcessor context.addServletContainerInitializer(sciHolder); } + @SuppressWarnings("unchecked") public void visitMetaInfResource(WebAppContext context, Resource dir) { Collection metaInfResources = (Collection)context.getAttribute(MetaInfConfiguration.METAINF_RESOURCES); @@ -275,14 +275,11 @@ public class QuickStartDescriptorProcessor extends IterativeDescriptorProcessor context.setAttribute(MetaInfConfiguration.METAINF_RESOURCES, metaInfResources); } metaInfResources.add(dir); + //also add to base resource of webapp - Resource[] collection = new Resource[metaInfResources.size() + 1]; - int i = 0; - collection[i++] = context.getBaseResource(); - for (Resource resource : metaInfResources) - { - collection[i++] = resource; - } - context.setBaseResource(new ResourceCollection(collection)); + List collection = new ArrayList<>(); + collection.add(context.getBaseResource()); + collection.addAll(metaInfResources); + context.setBaseResource(Resource.of(collection)); } } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaData.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaData.java index 04a091e41d3..62e02e5afc5 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaData.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaData.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.ee9.webapp; import java.lang.annotation.Annotation; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -446,7 +447,7 @@ public class MetaData { LOG.debug("metadata resolve {}", context); - //Ensure origins is fresh + // Ensure origins is fresh _origins.clear(); // Set the ordered lib attribute @@ -455,13 +456,10 @@ public class MetaData { orderedWebInfJars = getWebInfResources(true); List orderedLibs = new ArrayList<>(); - for (Resource webInfJar : orderedWebInfJars) + for (Resource jar: orderedWebInfJars) { - //get just the name of the jar file - String fullname = webInfJar.getName(); - int i = fullname.indexOf(".jar"); - int j = fullname.lastIndexOf("/", i); - orderedLibs.add(fullname.substring(j + 1, i + 4)); + URI uri = Resource.unwrapContainer(jar.getURI()); + orderedLibs.add(uri.getPath()); } context.setAttribute(ServletContext.ORDERED_LIBS, Collections.unmodifiableList(orderedLibs)); } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaInfConfiguration.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaInfConfiguration.java index 771df0abe4b..485b96ab0e5 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaInfConfiguration.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/MetaInfConfiguration.java @@ -44,7 +44,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.PatternMatcher; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -293,7 +292,6 @@ public class MetaInfConfiguration extends AbstractConfiguration if (jars != null) { List uris = new ArrayList<>(); - int i = 0; for (Resource r : jars) { uris.add(r.getURI()); @@ -325,20 +323,15 @@ public class MetaInfConfiguration extends AbstractConfiguration @Override public void configure(WebAppContext context) throws Exception { - // Look for extra resource @SuppressWarnings("unchecked") Set resources = (Set)context.getAttribute(RESOURCE_DIRS); if (resources != null && !resources.isEmpty()) { - Resource[] collection = new Resource[resources.size() + 1]; - int i = 0; - collection[i++] = context.getBaseResource(); - for (Resource resource : resources) - { - collection[i++] = resource; - } - context.setBaseResource(new ResourceCollection(collection)); + List collection = new ArrayList<>(); + collection.add(context.getBaseResource()); + collection.addAll(resources); + context.setBaseResource(Resource.of(collection)); } } @@ -840,6 +833,7 @@ public class MetaInfConfiguration extends AbstractConfiguration return null; return context.getExtraClasspath() + .getResources() .stream() .filter(this::isFileSupported) .collect(Collectors.toList()); @@ -884,7 +878,7 @@ public class MetaInfConfiguration extends AbstractConfiguration if (context == null || context.getExtraClasspath() == null) return null; - return context.getExtraClasspath() + return context.getExtraClasspath().getResources() .stream() .filter(Resource::isDirectory) .collect(Collectors.toList()); @@ -905,24 +899,6 @@ public class MetaInfConfiguration extends AbstractConfiguration private boolean isFileSupported(Resource resource) { - try - { - if (resource.isDirectory()) - return false; - - if (resource.getPath() == null) - return false; - } - catch (Throwable t) - { - if (LOG.isDebugEnabled()) - LOG.debug("Bad Resource reference: {}", resource, t); - return false; - } - - String filenameLowercase = resource.getName().toLowerCase(Locale.ENGLISH); - int dot = filenameLowercase.lastIndexOf('.'); - String extension = (dot < 0 ? null : filenameLowercase.substring(dot)); - return (extension != null && (extension.equals(".jar") || extension.equals(".zip"))); + return Resource.isArchive(resource.getURI()); } } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppClassLoader.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppClassLoader.java index 5aea1b9988c..890a2e32314 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppClassLoader.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppClassLoader.java @@ -17,8 +17,10 @@ import java.io.IOException; import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; import java.security.CodeSource; import java.security.PermissionCollection; @@ -33,12 +35,12 @@ import java.util.Locale; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; import org.eclipse.jetty.util.ClassVisibilityChecker; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; @@ -77,6 +79,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility private final Set _extensions = new HashSet(); private String _name = String.valueOf(hashCode()); private final List _transformers = new CopyOnWriteArrayList<>(); + private Resource.Mount _mountedExtraClassPath; /** * The Context in which the classloader operates. @@ -107,7 +110,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility */ boolean isParentLoaderPriority(); - List getExtraClasspath(); + ResourceCollection getExtraClasspath(); boolean isServerResource(String name, URL parentUrl); @@ -189,7 +192,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility if (context.getExtraClasspath() != null) { - for (Resource resource : context.getExtraClasspath()) + for (Resource resource : context.getExtraClasspath().getResources()) { addClassPath(resource); } @@ -267,19 +270,32 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility if (classPath == null) return; - for (Resource resource : Resource.fromList(classPath, false, _context::newResource)) + List uris = Resource.split(classPath); + _mountedExtraClassPath = Resource.mountCollection(uris); + + ResourceCollection rc = (ResourceCollection)_mountedExtraClassPath.root(); + for (Resource resource : rc.getResources()) { addClassPath(resource); } + } /** * @param file Checks if this file type can be added to the classpath. + * TODO: move to FileID in later PR */ + private boolean isFileSupported(String file) { int dot = file.lastIndexOf('.'); - return dot != -1 && _extensions.contains(file.substring(dot)); + return dot != -1 && _extensions.contains(file.substring(dot).toLowerCase(Locale.ENGLISH)); + } + + // TODO: move to FileID in later PR + private boolean isFileSupported(Path path) + { + return isFileSupported(path.getFileName().toString()); } /** @@ -292,32 +308,35 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility { if (lib.exists() && lib.isDirectory()) { - List entries = lib.list(); - if (entries != null) - { - entries.sort(Comparator.naturalOrder()); + Path dir = lib.getPath(); - for (String entry : entries) + try (Stream streamEntries = Files.list(dir)) + { + List jars = streamEntries + .filter(Files::isRegularFile) + .filter(this::isFileSupported) + .sorted(Comparator.naturalOrder()) + .toList(); + + for (Path jar: jars) { try { - Resource resource = lib.resolve(entry); if (LOG.isDebugEnabled()) - LOG.debug("addJar - {}", resource); - String fnlc = resource.getName().toLowerCase(Locale.ENGLISH); - // don't check if this is a directory (prevents use of symlinks), see Bug 353165 - if (isFileSupported(fnlc)) - { - String jar = URIUtil.encodeSpecific(resource.toString(), ",;"); - addClassPath(jar); - } + LOG.debug("addJar - {}", jar); + URI jarUri = Resource.toJarFileUri(jar.toUri()); + addClassPath(jarUri.toASCIIString()); } catch (Exception ex) { - LOG.warn("Unable to load WEB-INF/lib JAR {}", entry, ex); + LOG.warn("Unable to load WEB-INF/lib JAR {}", jar, ex); } } } + catch (IOException e) + { + LOG.warn("Unable to load WEB-INF/lib JARs: {}", dir, e); + } } } @@ -636,6 +655,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility public void close() throws IOException { super.close(); + IO.close(_mountedExtraClassPath); } @Override diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java index d7b5bba15c3..7c2527433b6 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebAppContext.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.ee9.webapp; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.security.PermissionCollection; @@ -54,6 +55,7 @@ import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.ExceptionUtil; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; @@ -132,7 +134,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL private boolean _persistTmpDir = false; private String _war; - private List _extraClasspath; + private ResourceCollection _extraClasspath; private Throwable _unavailableException; private Map _resourceAliases; @@ -144,6 +146,8 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL private MetaData _metadata = new MetaData(); private boolean _defaultContextPath = true; + private Resource.Mount _mountedExtraClasspath; + public static WebAppContext getCurrentWebAppContext() { ContextHandler.APIContext context = ContextHandler.getCurrentContext(); @@ -534,6 +538,13 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL } } + @Override + protected void doStop() throws Exception + { + super.doStop(); + IO.close(_mountedExtraClasspath); + } + private void wrapConfigurations() { Collection wrappers = getBeans(Configuration.WrapperFunction.class); @@ -1221,7 +1232,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL */ @Override @ManagedAttribute(value = "extra classpath for context classloader", readonly = true) - public List getExtraClasspath() + public ResourceCollection getExtraClasspath() { return _extraClasspath; } @@ -1229,21 +1240,23 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL /** * Set the Extra ClassPath via delimited String. *

    - * This is a convenience method for {@link #setExtraClasspath(List)} + * This is a convenience method for {@link #setExtraClasspath(ResourceCollection)} *

    * * @param extraClasspath Comma or semicolon separated path of filenames or URLs * pointing to directories or jar files. Directories should end * with '/'. * @throws IOException if unable to resolve the resources referenced - * @see #setExtraClasspath(List) + * @see #setExtraClasspath(ResourceCollection) */ public void setExtraClasspath(String extraClasspath) throws IOException { - setExtraClasspath(Resource.fromList(extraClasspath, false, this::newResource)); + List uris = Resource.split(extraClasspath); + _mountedExtraClasspath = Resource.mountCollection(uris); + setExtraClasspath((ResourceCollection)_mountedExtraClasspath.root()); } - public void setExtraClasspath(List extraClasspath) + public void setExtraClasspath(ResourceCollection extraClasspath) { _extraClasspath = extraClasspath; } diff --git a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebInfConfiguration.java b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebInfConfiguration.java index 20bd16ec291..1fa17a77443 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebInfConfiguration.java +++ b/jetty-ee9/jetty-ee9-webapp/src/main/java/org/eclipse/jetty/ee9/webapp/WebInfConfiguration.java @@ -28,7 +28,6 @@ import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.MountedPathResource; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -450,10 +449,10 @@ public class WebInfConfiguration extends AbstractConfiguration webInf = Resource.newResource(extractedWebInfDir.getCanonicalPath()); - ResourceCollection rc = new ResourceCollection(webInf, webApp); + Resource rc = Resource.of(webInf, webApp); if (LOG.isDebugEnabled()) - LOG.debug("context.resourcebase={}", rc); + LOG.debug("context.baseResource={}", rc); context.setBaseResource(rc); } diff --git a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java index 15cf3697e5e..310a204e1d7 100644 --- a/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java +++ b/jetty-ee9/jetty-ee9-webapp/src/test/java/org/eclipse/jetty/ee9/webapp/WebAppContextTest.java @@ -14,12 +14,13 @@ package org.eclipse.jetty.ee9.webapp; import java.io.File; +import java.net.URI; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -58,6 +59,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -515,32 +517,26 @@ public class WebAppContextTest WebAppClassLoader webAppClassLoader = (WebAppClassLoader)contextClassLoader; Path extLibsDir = MavenTestingUtils.getTargetPath("test-classes/ext"); extLibsDir = extLibsDir.toAbsolutePath(); - List expectedPaths; + + List expectedUris; try (Stream s = Files.list(extLibsDir)) { - expectedPaths = s + expectedUris = s .filter(Files::isRegularFile) - .filter((path) -> path.toString().endsWith(".jar")) + .filter((path) -> path.getFileName().toString().endsWith(".jar")) + .sorted(Comparator.naturalOrder()) + .map(Path::toUri) + .map(Resource::toJarFileUri) .collect(Collectors.toList()); } - List actualPaths = new ArrayList<>(); + List actualURIs = new ArrayList<>(); for (URL url : webAppClassLoader.getURLs()) { - actualPaths.add(Paths.get(url.toURI())); - } - assertThat("[" + description + "] WebAppClassLoader.urls.length", actualPaths.size(), is(expectedPaths.size())); - for (Path expectedPath : expectedPaths) - { - boolean found = false; - for (Path actualPath : actualPaths) - { - if (Files.isSameFile(actualPath, expectedPath)) - { - found = true; - } - } - assertTrue(found, "[" + description + "] Not able to find expected jar in WebAppClassLoader: " + expectedPath); + actualURIs.add(url.toURI()); } + assertThat("[" + description + "] WebAppClassLoader.urls.length", actualURIs.size(), is(expectedUris.size())); + + assertThat(actualURIs, contains(expectedUris.toArray())); } public static Stream extraClasspathDir()