Jetty-12 : Immutable ResourceCollection and Context based mount management (#8337)

* Immutable ResourceCollection with mount management.

* Internal List<Resource> 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<URI> 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<URI>
* 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)
This commit is contained in:
Joakim Erdfelt 2022-07-28 08:59:17 -05:00 committed by GitHub
parent 0f35590ab4
commit c6c0eb0313
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1229 additions and 817 deletions

View File

@ -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[]
}

View File

@ -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<Resource.Mount> mounts = getBeans(Resource.Mount.class);
mounts.forEach((mount) ->
{
IO.close(mount);
removeBean(mount);
});
}
public boolean checkVirtualHost(Request request)

View File

@ -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<Resource> resourceList = Stream.of("one", "two", "three")
.map(basePath::resolve)
.map(Resource::newResource)
.toList();
ResourceCollection rc = Resource.of(resourceList);
List<Resource> 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<Resource> resourceList = Stream.of("one", "two", "three")
.map(basePath::resolve)
.map(Resource::newResource)
.toList();
ResourceCollection rc = Resource.of(resourceList);
List<Resource> 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);

View File

@ -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<String> 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)}
*

View File

@ -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);
}
}
}

View File

@ -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();
/**
* <p>Create a ResourceCollection from an unknown set of URIs.</p>
*
* <p>
* 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.
* </p>
*
* @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<URI> uris)
{
List<Resource> resources = new ArrayList<>();
List<Resource.Mount> mounts = new ArrayList<>();
try
{
// track URIs that have been seen, to avoid adding duplicates.
Set<URI> 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;
}
}
/**
* <p>Mount a URI if it is needed.</p>
*
* @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)
/**
* <p>Make a Resource containing a collection of other resources</p>
* @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<Resource> resources)
{
return "jar:" + jarFile + "!/";
if (resources == null || resources.isEmpty())
throw new IllegalArgumentException("No resources");
return new ResourceCollection(resources);
}
/**
* <p>Make a Resource containing a collection of other resources</p>
* @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 <code>jar:${uri_to_java_archive}!/${internal-reference}</code> 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);
}
/**
* <p>Convert a String into a URI suitable for use as a Resource.</p>
*
* @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<String> list()
public List<String> list() // TODO: should return Path's
{
try (DirectoryStream<Path> 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.
* <p>
* 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.
* </p>
* <p>
* If the result of processing the input segment is a java archive, then its resulting URI will be a mountable URI as `jar:file:...!/`.
* </p>
*
* @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<Resource> fromList(String resources, boolean globDirs) throws IOException
public static List<URI> split(String str)
{
return fromList(resources, globDirs, Resource::newResource);
}
List<URI> uris = new ArrayList<>();
/**
* Parse a delimited String of resource references and
* return the List of Resources instances it represents.
* <p>
* 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.
* </p>
*
* @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<Resource> fromList(String resources, boolean globDirs, ResourceFactory resourceFactory) throws IOException
{
if (StringUtil.isBlank(resources))
{
return Collections.emptyList();
}
List<Resource> 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<String> 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<Path> 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();
}
}

View File

@ -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<Resource> _resources;
/**
* Instantiates an empty resource collection.
* <p>
* This constructor is used when configuring jetty-maven-plugin.
*/
public ResourceCollection()
static class Mount implements Resource.Mount
{
_resources = new ArrayList<>();
private final List<Resource.Mount> _mounts;
private final ResourceCollection _root;
Mount(Collection<Resource> resources, List<Resource.Mount> 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<Resource> _resources;
/**
* Instantiates a new resource collection.
*
* @param resources the resources to be added to collection
*/
public ResourceCollection(Resource... resources)
ResourceCollection(Collection<Resource> resources)
{
this(Arrays.asList(resources));
List<Resource> 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<Resource> resources)
private static void gatherUniqueFlatResourceList(List<Resource> unique, Collection<Resource> 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<Resource> 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<Resource> 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<Resource> list = Resource.fromList(resources, false);
if (list.isEmpty())
{
throw new IllegalArgumentException("String contains no entries");
}
List<Resource> 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) :
* <ul>
@ -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<String> list()
{
assertResourcesSet();
HashSet<String> 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.");
}
}
}

View File

@ -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<Path> 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);

View File

@ -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

View File

@ -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<URI> 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<URI> 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<URI> 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<String, String> 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);
}
}

View File

@ -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<Arguments> jarFileUriCases()
{
List<Arguments> 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<Arguments> 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<URI> 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<URI> 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<URI> 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<URI> 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<URI> 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<URI> 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));
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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())
{

View File

@ -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<String, File> _webInfJarMap = new HashMap<String, File>();
private List<File> _classpathFiles; // webInfClasses+testClasses+webInfJars
private List<URI> _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<File> getClassPathFiles()
public List<URI> 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<String> resources = new ArrayList<String>();
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<URI> 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<File> 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

View File

@ -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());
}
}

View File

@ -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));
}
/**

View File

@ -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<URI> 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);

View File

@ -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<URI> actual = resourceCollection.getResources().stream().filter(Objects::nonNull).map(Resource::getURI).toList();
URI[] expected = new URI[]{base1.toURI(), base2.toURI()};
assertThat(actual, containsInAnyOrder(expected));
}
}

View File

@ -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));
}
}

View File

@ -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())
{

View File

@ -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

View File

@ -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<Resource> metaInfResources = (Collection<Resource>)context.getAttribute(MetaInfConfiguration.METAINF_RESOURCES);
if (metaInfResources == null)
{
metaInfResources = new HashSet<Resource>();
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<Resource> collection = new ArrayList<>();
collection.add(context.getResourceBase());
collection.addAll(metaInfResources);
context.setBaseResource(Resource.of(collection));
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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<String> 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));
}

View File

@ -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<Resource> resources = (Set<Resource>)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<Resource> 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());
}
}

View File

@ -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<ClassFileTransformer> _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<Resource> 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<URI> 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<String> entries = lib.list();
if (entries != null)
{
entries.sort(Comparator.naturalOrder());
Path dir = lib.getPath();
for (String entry : entries)
try (Stream<Path> streamEntries = Files.list(dir))
{
List<Path> 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

View File

@ -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<Resource> _extraClasspath;
private ResourceCollection _extraClasspath;
private Throwable _unavailableException;
private Map<String, String> _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<Resource> getExtraClasspath()
public ResourceCollection getExtraClasspath()
{
return _extraClasspath;
}
@ -1231,21 +1234,23 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
/**
* Set the Extra ClassPath via delimited String.
* <p>
* This is a convenience method for {@link #setExtraClasspath(List)}
* This is a convenience method for {@link #setExtraClasspath(ResourceCollection)}
* </p>
*
* @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<URI> uris = Resource.split(extraClasspath);
Resource.Mount mount = Resource.mountCollection(uris);
addBean(mount); // let doStop() cleanup mount
setExtraClasspath((ResourceCollection)mount.root());
}
public void setExtraClasspath(List<Resource> extraClasspath)
public void setExtraClasspath(ResourceCollection extraClasspath)
{
_extraClasspath = extraClasspath;
}

View File

@ -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);
}

View File

@ -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<Resource> resources = new ArrayList<Resource>();
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");

View File

@ -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<Path> expectedPaths;
List<URI> expectedUris;
try (Stream<Path> 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<Path> actualPaths = new ArrayList<>();
List<URI> 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<Arguments> extraClasspathDir()

View File

@ -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<String, File> _webInfJarMap = new HashMap<String, File>();
private List<File> _classpathFiles; // webInfClasses+testClasses+webInfJars
private List<URI> _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<File> getClassPathFiles()
public List<URI> 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<String> resources = new ArrayList<String>();
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<URI> 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<File> 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

View File

@ -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());
}
}

View File

@ -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));
}
/**

View File

@ -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<URI> 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);

View File

@ -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<URI> actual = resourceCollection.getResources().stream().filter(Objects::nonNull).map(Resource::getURI).toList();
URI[] expected = new URI[]{base1.toURI(), base2.toURI()};
assertThat(actual, containsInAnyOrder(expected));
}
}

View File

@ -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<Resource.Mount> mounts = getBeans(Resource.Mount.class);
mounts.forEach((mount) ->
{
IO.close(mount);
removeBean(mount);
});
}
finally
{

View File

@ -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));
}
}

View File

@ -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<Resource> metaInfResources = (Collection<Resource>)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<Resource> collection = new ArrayList<>();
collection.add(context.getBaseResource());
collection.addAll(metaInfResources);
context.setBaseResource(Resource.of(collection));
}
}

View File

@ -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<String> 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));
}

View File

@ -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<URI> 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<Resource> resources = (Set<Resource>)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<Resource> 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());
}
}

View File

@ -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<String> _extensions = new HashSet<String>();
private String _name = String.valueOf(hashCode());
private final List<ClassFileTransformer> _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<Resource> 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<URI> 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<String> entries = lib.list();
if (entries != null)
{
entries.sort(Comparator.naturalOrder());
Path dir = lib.getPath();
for (String entry : entries)
try (Stream<Path> streamEntries = Files.list(dir))
{
List<Path> 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

View File

@ -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<Resource> _extraClasspath;
private ResourceCollection _extraClasspath;
private Throwable _unavailableException;
private Map<String, String> _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<Configuration.WrapperFunction> 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<Resource> getExtraClasspath()
public ResourceCollection getExtraClasspath()
{
return _extraClasspath;
}
@ -1229,21 +1240,23 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
/**
* Set the Extra ClassPath via delimited String.
* <p>
* This is a convenience method for {@link #setExtraClasspath(List)}
* This is a convenience method for {@link #setExtraClasspath(ResourceCollection)}
* </p>
*
* @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<URI> uris = Resource.split(extraClasspath);
_mountedExtraClasspath = Resource.mountCollection(uris);
setExtraClasspath((ResourceCollection)_mountedExtraClasspath.root());
}
public void setExtraClasspath(List<Resource> extraClasspath)
public void setExtraClasspath(ResourceCollection extraClasspath)
{
_extraClasspath = extraClasspath;
}

View File

@ -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);
}

View File

@ -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<Path> expectedPaths;
List<URI> expectedUris;
try (Stream<Path> 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<Path> actualPaths = new ArrayList<>();
List<URI> 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<Arguments> extraClasspathDir()