Jetty 12.0.x overlay combined resources (#10760)

* Fix overlays with CombinedResource
* Improvements to CombinedResource for AnnotationParser
* Added `contains` and `getPathTo` methods to the Resource API, so they can be used by the AnnotationParser
* Fixed numerous bugs in CombinedResource list and getAllResources
* handles cross file system copy as well.
* Reduced fallback to URI string manipulation.

---------

Co-authored-by: gregw <gregw@webtide.com>
Co-authored-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Jan Bartel 2023-10-25 22:49:48 +02:00 committed by GitHub
parent 1c2d7cee07
commit 3ae43961b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1277 additions and 251 deletions

View File

@ -99,7 +99,7 @@ public class ResourceListing
{
case "M" -> ResourceCollators.byLastModified(sortOrderAscending);
case "S" -> ResourceCollators.bySize(sortOrderAscending);
default -> ResourceCollators.byName(sortOrderAscending);
default -> ResourceCollators.byFileName(sortOrderAscending);
};
listing.sort(sort);

View File

@ -19,6 +19,7 @@ import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* Simple, yet surprisingly common utility methods for identifying various file types commonly seen and worked with in a
@ -26,6 +27,8 @@ import java.util.Locale;
*/
public class FileID
{
private static final Pattern NUMBER = Pattern.compile("[0-9]+");
/**
* Retrieve the basename of a path. This is the name of the
* last segment of the path, with any dot suffix (e.g. ".war") removed
@ -356,7 +359,7 @@ public class FileID
}
/**
* Predicate to select all class files
* Predicate to test for class files
*
* @param path the path to test
* @return true if the filename ends with {@code .class}
@ -369,7 +372,7 @@ public class FileID
String filename = fileNamePath.toString();
// has to end in ".class"
if (!filename.toLowerCase(Locale.ENGLISH).endsWith(".class"))
if (!StringUtil.asciiEndsWithIgnoreCase(filename, ".class"))
return false;
// is it a valid class filename?
int start = 0;
@ -395,20 +398,16 @@ public class FileID
* Predicate useful for {@code Stream<Path>} to exclude hidden paths following
* filesystem rules for hidden directories and files.
*
* @param base the base path to search from (anything above this path is not evaluated)
* @param path the path to evaluate
* @param path the possibly relative path to evaluate
* @return true if hidden by FileSystem rules, false if not
* @see Files#isHidden(Path)
*/
public static boolean isHidden(Path base, Path path)
public static boolean isHidden(Path path)
{
// Work with the path in relative form, from the base onwards to the path
Path relative = base.relativize(path);
int count = relative.getNameCount();
int count = path.getNameCount();
for (int i = 0; i < count; i++)
{
Path segment = relative.getName(i);
Path segment = path.getName(i);
String segmentName = segment.toString();
@ -430,10 +429,24 @@ public class FileID
// ignore, if filesystem gives us an error, we cannot make the call on hidden status
}
}
return false;
}
/**
* Predicate useful for {@code Stream<Path>} to exclude hidden paths following
* filesystem rules for hidden directories and files.
*
* @param base the base path to search from (anything above this path is not evaluated)
* @param path the path to evaluate
* @return true if hidden by FileSystem rules, false if not
* @see Files#isHidden(Path)
*/
public static boolean isHidden(Path base, Path path)
{
// Work with the path in relative form, from the base onwards to the path
return isHidden(base.relativize(path));
}
/**
* Is the URI pointing to a Java Archive (JAR) File (not directory)
*
@ -482,13 +495,13 @@ public class FileID
if (path.getNameCount() < 3)
return false;
Path path0 = path.getName(0);
Path path1 = path.getName(1);
Path path2 = path.getName(2);
if (!StringUtil.asciiEqualsIgnoreCase("META-INF", path.getName(0).toString()))
return false;
return (path0.toString().equals("META-INF") &&
path1.toString().equals("versions") &&
path2.getFileName().toString().matches("[0-9]+"));
if (!StringUtil.asciiEqualsIgnoreCase("versions", path.getName(1).toString()))
return false;
return NUMBER.matcher(path.getName(2).getFileName().toString()).matches();
}
/**
@ -516,13 +529,28 @@ public class FileID
* @param path the path to test
* @return true if not a {@code module-info.class} file
*/
public static boolean isNotModuleInfoClass(Path path)
public static boolean isModuleInfoClass(Path path)
{
Path filenameSegment = path.getFileName();
if (filenameSegment == null)
return true;
return false;
return !filenameSegment.toString().equalsIgnoreCase("module-info.class");
return filenameSegment.toString().equalsIgnoreCase("module-info.class");
}
/**
* Predicate to skip {@code module-info.class} files.
*
* <p>
* This is a simple test against the last path segment using {@link Path#getFileName()}
* </p>
*
* @param path the path to test
* @return true if not a {@code module-info.class} file
*/
public static boolean isNotModuleInfoClass(Path path)
{
return !isModuleInfoClass(path);
}
/**

View File

@ -14,20 +14,31 @@
package org.eclipse.jetty.util.resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
/**
* Multiple resource directories presented as a single Resource.
* Multiple {@link Resource} directories presented as a single overlayed {@link Resource} directory.
* <p>This class differs from a {@link List}&lt;{@link Resource}&gt;, as a relative path can {@link #resolve(String) resolve}
* to only a single {@link Resource} in a {@code CombinedResource}, whilst it may resolve to multiple in a
* {@link List}&lt;{@link Resource}&gt;.</p>
*/
public class CombinedResource extends Resource
{
@ -252,24 +263,58 @@ public class CombinedResource extends Resource
@Override
public List<Resource> list()
{
List<Resource> result = new ArrayList<>();
for (Resource r : _resources)
Map<String, Resource> results = new TreeMap<>();
for (Resource base : _resources)
{
if (r.isDirectory())
result.addAll(r.list());
else
result.add(r);
for (Resource r : base.list())
{
if (r.isDirectory())
results.computeIfAbsent(r.getFileName(), this::resolve);
else
results.putIfAbsent(r.getFileName(), r);
}
}
return result;
return new ArrayList<>(results.values());
}
@Override
public void copyTo(Path destination) throws IOException
{
// Copy in reverse order
for (int r = _resources.size(); r-- > 0; )
// This method could be implemented with the simple:
// List<Resource> entries = getResources();
// for (int r = entries.size(); r-- > 0; )
// entries.get(r).copyTo(destination);
// However, that may copy large overlayed resources. The implementation below avoids that:
Collection<Resource> all = getAllResources();
for (Resource r : all)
{
_resources.get(r).copyTo(destination);
Path relative = getPathTo(r);
Path pathTo = Objects.equals(relative.getFileSystem(), destination.getFileSystem())
? destination.resolve(relative)
: resolveDifferentFileSystem(destination, relative);
if (r.isDirectory())
{
ensureDirExists(pathTo);
}
else
{
ensureDirExists(pathTo.getParent());
Path pathFrom = r.getPath();
if (pathFrom != null)
{
Files.copy(pathFrom, pathTo, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
else
{
// use old school stream based copy
try (InputStream in = r.newInputStream(); OutputStream out = Files.newOutputStream(pathTo))
{
IO.copy(in, out);
}
}
}
}
}
@ -302,9 +347,60 @@ public class CombinedResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean contains(Resource other)
{
// TODO could look at implementing the semantic of is this collection a subset of the Resource r?
return false;
// Every resource from the (possibly combined) other resource ...
loop: for (Resource o : other)
{
// Must be contained in at least one of this resources
for (Resource r : _resources)
{
if (r.contains(o))
continue loop;
}
// A resource of the other did not match any in this
return false;
}
return true;
}
@Override
public Path getPathTo(Resource other)
{
Path otherPath = other.getPath();
// If the other resource has a single Path
if (otherPath != null)
{
// return true it's relative location to the first matching resource.
for (Resource r : _resources)
{
Path path = r.getPath();
if (otherPath.startsWith(path))
return path.relativize(otherPath);
}
return null;
}
// otherwise the other resource must also be some kind of combined resource.
// So every resource in the other combined must have the same relative relationship to us
Path relative = null;
loop : for (Resource o : other)
{
for (Resource r : _resources)
{
if (o.getPath().startsWith(r.getPath()))
{
Path rel = r.getPath().relativize(o.getPath());
if (relative == null)
relative = rel;
else if (!relative.equals(rel))
return null;
continue loop;
}
}
return null;
}
return relative;
}
}

View File

@ -75,7 +75,7 @@ public class MemoryResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
// memory resource can never be contained in another memory resource
return false;

View File

@ -47,9 +47,9 @@ public class MountedPathResource extends PathResource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
return URIUtil.unwrapContainer(r.getURI()).equals(containerUri);
return URIUtil.unwrapContainer(container.getURI()).equals(containerUri);
}
public Path getContainerPath()

View File

@ -16,14 +16,10 @@ package org.eclipse.jetty.util.resource;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryIteratorException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
@ -204,6 +200,22 @@ public class PathResource extends Resource
return path;
}
@Override
public boolean contains(Resource other)
{
if (other == null)
return false;
Path thisPath = getPath();
if (thisPath == null)
throw new UnsupportedOperationException("Resources without a Path must implement contains");
Path otherPath = other.getPath();
return otherPath != null &&
otherPath.getFileSystem().equals(thisPath.getFileSystem()) &&
otherPath.startsWith(thisPath);
}
public Path getRealPath()
{
resolveAlias();
@ -343,14 +355,6 @@ public class PathResource extends Resource
}
}
@Override
public boolean isContainedIn(Resource r)
{
if (r == null)
return false;
return r.getClass() == PathResource.class && path.startsWith(r.getPath());
}
/**
* <p>
* Perform a check of the original Path and as-requested URI to determine
@ -489,16 +493,6 @@ public class PathResource extends Resource
}
}
@Override
public void copyTo(Path destination) throws IOException
{
// TODO reconcile this impl with super's
if (isDirectory())
Files.walkFileTree(this.path, new TreeCopyFileVisitor(this.path, destination));
else
Files.copy(this.path, destination);
}
/**
* Ensure Path to URI is sane when it returns a directory reference.
*
@ -547,39 +541,4 @@ public class PathResource extends Resource
{
return this.uri.toASCIIString();
}
private static class TreeCopyFileVisitor extends SimpleFileVisitor<Path>
{
private final String relativeTo;
private final Path target;
public TreeCopyFileVisitor(Path relativeTo, Path target)
{
this.relativeTo = relativeTo.getRoot().relativize(relativeTo).toString();
this.target = target;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
Path resolvedTarget = target.resolve(dir.getRoot().resolve(relativeTo).relativize(dir).toString());
if (Files.notExists(resolvedTarget))
Files.createDirectories(resolvedTarget);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
Path resolvedTarget = target.resolve(file.getRoot().resolve(relativeTo).relativize(file).toString());
Files.copy(file, resolvedTarget, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc)
{
return FileVisitResult.CONTINUE;
}
}
}

View File

@ -28,8 +28,15 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
@ -41,6 +48,7 @@ import org.eclipse.jetty.util.IO;
*/
public abstract class Resource implements Iterable<Resource>
{
private static final Logger LOG = LoggerFactory.getLogger(Resource.class);
private static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
public static String dump(Resource resource)
@ -54,7 +62,7 @@ public abstract class Resource implements Iterable<Resource>
/**
* Return the Path corresponding to this resource.
*
* @return the path.
* @return the path or null if there is no Path representation.
*/
public abstract Path getPath();
@ -62,14 +70,78 @@ public abstract class Resource implements Iterable<Resource>
* 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
* @param container the containing resource
* @return true if this Resource is contained, false otherwise
* @see #contains(Resource)
*/
public abstract boolean isContainedIn(Resource r);
public boolean isContainedIn(Resource container)
{
return container != null && container.contains(this);
}
/**
* Return true if this resource deeply contains the other Resource. This resource must be
* a directory or a jar file or any form of resource capable of containing other resources.
*
* @param other the resource
* @return true if this Resource is deeply contains the other Resource, false otherwise
* @see #isContainedIn(Resource)
*/
public boolean contains(Resource other)
{
if (other == null)
return false;
URI thisURI = getURI();
if (thisURI == null)
throw new UnsupportedOperationException("Resources without a URI must implement contains");
URI otherURI = other.getURI();
if (otherURI == null)
return false;
// Different schemes? not a chance it contains the other
if (!StringUtil.asciiEqualsIgnoreCase(thisURI.getScheme(), otherURI.getScheme()))
return false;
// Different authorities? not a valid contains() check
if (!Objects.equals(thisURI.getAuthority(), otherURI.getAuthority()))
return false;
// Ensure that if `file` scheme is used, it's using a consistent convention to allow for startsWith check
String thisURIString = URIUtil.correctFileURI(thisURI).toASCIIString();
String otherURIString = URIUtil.correctFileURI(otherURI).toASCIIString();
return otherURIString.startsWith(thisURIString) &&
(thisURIString.length() == otherURIString.length() || otherURIString.charAt(thisURIString.length()) == '/');
}
/**
* Get the relative path from this Resource to a possibly contained resource.
* @param other The other resource that may be contained in this resource
* @return a relative Path representing the path from this resource to the other resource,
* or null if not able to represent other resources as relative to this resource
*/
public Path getPathTo(Resource other)
{
Path thisPath = getPath();
if (thisPath == null)
throw new UnsupportedOperationException("Resources without a Path must implement getPathTo");
if (!contains(other))
return null;
Path otherPath = other.getPath();
if (otherPath == null)
return null;
return thisPath.relativize(otherPath);
}
/**
* <p>Return an Iterator of all Resource's referenced in this Resource.</p>
* <p>This is meaningful if you have a Composite Resource, otherwise it will be a single entry Iterator of this resource.</p>
* <p>This is meaningful if you have a {@link CombinedResource},
* otherwise it will be a single entry Iterator of this resource.</p>
*
* @return the iterator of Resources.
*/
@ -127,14 +199,14 @@ public abstract class Resource implements Iterable<Resource>
/**
* URI representing the resource.
*
* @return a URI representing the given resource
* @return a URI representing the given resource, or null if there is no URI representation of the resource.
*/
public abstract URI getURI();
/**
* The full name of the resource.
*
* @return the full name of the resource, or null if not backed by a Path
* @return the full name of the resource, or null if there is no name for the resource.
*/
public abstract String getName();
@ -143,7 +215,8 @@ public abstract class Resource implements Iterable<Resource>
*
* <p>This is the last segment of the path.</p>
*
* @return the filename of the resource, or "" if there are no path segments (eg: path of "/"), or null if resource has no path.
* @return the filename of the resource, or "" if there are no path segments (eg: path of "/"), or null if resource
* cannot determine a filename.
* @see Path#getFileName()
*/
public abstract String getFileName();
@ -222,49 +295,126 @@ public abstract class Resource implements Iterable<Resource>
}
/**
* Copy the Resource to the new destination file.
* <p>
* Will not replace existing destination file.
* Copy the Resource to the new destination file or directory
*
* @param destination the destination file to create
* @param destination the destination file to create or directory to use.
* @throws IOException if unable to copy the resource
*/
public void copyTo(Path destination)
throws IOException
{
if (Files.exists(destination))
throw new IllegalArgumentException(destination + " exists");
// attempt simple file copy
Path src = getPath();
if (src != null)
if (src == null)
{
// TODO ATOMIC_MOVE seems useless for a copy and REPLACE_EXISTING contradicts the
// javadoc that explicitly states "Will not replace existing destination file."
Files.copy(src, destination,
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
if (!isDirectory())
{
// use old school stream based copy
try (InputStream in = newInputStream(); OutputStream out = Files.newOutputStream(destination))
{
IO.copy(in, out);
}
return;
}
throw new UnsupportedOperationException("Directory Resources without a Path must implement copyTo");
}
// Do we have to copy a single file?
if (Files.isRegularFile(src))
{
// Is the destination a directory?
if (Files.isDirectory(destination))
{
// to a directory, preserve the filename
Path destPath = destination.resolve(src.getFileName().toString());
Files.copy(src, destPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
else
{
// to a file, use destination as-is
Files.copy(src, destination, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
return;
}
// use old school stream based copy
try (InputStream in = newInputStream();
OutputStream out = Files.newOutputStream(destination))
// At this point this PathResource is a directory.
assert isDirectory();
BiFunction<Path, Path, Path> resolver = src.getFileSystem().equals(destination.getFileSystem())
? Path::resolve
: Resource::resolveDifferentFileSystem;
try (Stream<Path> entriesStream = Files.walk(src))
{
IO.copy(in, out);
for (Iterator<Path> pathIterator = entriesStream.iterator(); pathIterator.hasNext();)
{
Path path = pathIterator.next();
if (src.equals(path))
continue;
Path relative = src.relativize(path);
Path destPath = resolver.apply(destination, relative);
if (LOG.isDebugEnabled())
LOG.debug("CopyTo: {} > {}", path, destPath);
if (Files.isDirectory(path))
{
ensureDirExists(destPath);
}
else
{
ensureDirExists(destPath.getParent());
Files.copy(path, destPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
static Path resolveDifferentFileSystem(Path path, Path relative)
{
for (Path segment : relative)
path = path.resolve(segment.toString());
return path;
}
void ensureDirExists(Path dir) throws IOException
{
if (Files.exists(dir))
{
if (!Files.isDirectory(dir))
{
throw new IOException("Conflict, unable to create directory where file exists: " + dir);
}
return;
}
Files.createDirectories(dir);
}
/**
* Get a deep collection of contained resources.
* @return A collection of all Resources deeply contained within this resource if it is a directory,
* otherwise an empty collection is returned.
*/
public Collection<Resource> getAllResources()
{
try
{
List<Resource> children = list();
if (children == null || children.isEmpty())
return List.of();
boolean noDepth = true;
for (Iterator<Resource> i = children.iterator(); noDepth && i.hasNext(); )
noDepth = !i.next().isDirectory();
if (noDepth)
return children;
ArrayList<Resource> deep = new ArrayList<>();
for (Resource r: list())
for (Resource r: children)
{
deep.add(r);
if (r.isDirectory())
deep.addAll(r.getAllResources());
else
deep.add(r);
}
return deep;
}

View File

@ -17,10 +17,11 @@ import java.text.Collator;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.Objects;
public class ResourceCollators
{
private static Comparator<? super Resource> BY_NAME_ASCENDING =
private static final Comparator<? super Resource> BY_NAME_ASCENDING =
new Comparator<>()
{
private final Collator collator = Collator.getInstance(Locale.ENGLISH);
@ -28,11 +29,11 @@ public class ResourceCollators
@Override
public int compare(Resource o1, Resource o2)
{
return collator.compare(o1.getName(), o2.getName());
return collator.compare(Objects.requireNonNullElse(o1.getName(), ""), Objects.requireNonNullElse(o2.getName(), ""));
}
};
private static Comparator<? super Resource> BY_FILENAME_ASCENDING =
private static final Comparator<? super Resource> BY_FILENAME_ASCENDING =
new Comparator<>()
{
private final Collator collator = Collator.getInstance(Locale.ENGLISH);
@ -44,22 +45,22 @@ public class ResourceCollators
}
};
private static Comparator<? super Resource> BY_NAME_DESCENDING =
private static final Comparator<? super Resource> BY_NAME_DESCENDING =
Collections.reverseOrder(BY_NAME_ASCENDING);
private static Comparator<? super Resource> BY_FILENAME_DESCENDING =
private static final Comparator<? super Resource> BY_FILENAME_DESCENDING =
Collections.reverseOrder(BY_FILENAME_ASCENDING);
private static Comparator<? super Resource> BY_LAST_MODIFIED_ASCENDING =
private static final Comparator<? super Resource> BY_LAST_MODIFIED_ASCENDING =
Comparator.comparing(Resource::lastModified);
private static Comparator<? super Resource> BY_LAST_MODIFIED_DESCENDING =
private static final Comparator<? super Resource> BY_LAST_MODIFIED_DESCENDING =
Collections.reverseOrder(BY_LAST_MODIFIED_ASCENDING);
private static Comparator<? super Resource> BY_SIZE_ASCENDING =
private static final Comparator<? super Resource> BY_SIZE_ASCENDING =
Comparator.comparingLong(Resource::length);
private static Comparator<? super Resource> BY_SIZE_DESCENDING =
private static final Comparator<? super Resource> BY_SIZE_DESCENDING =
Collections.reverseOrder(BY_SIZE_ASCENDING);
public static Comparator<? super Resource> byLastModified(boolean sortOrderAscending)

View File

@ -155,7 +155,7 @@ public class URLResourceFactory implements ResourceFactory
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
// compare starting URIs?
return false;

View File

@ -419,7 +419,9 @@ public class FileIDTest
"/META-INF/versions/9/foo.txt",
"/META-INF/versions/17/org/eclipse/demo/Util.class",
"/META-INF/versions/17/WEB-INF/web.xml",
"/META-INF/versions/10/module-info.class"
"/META-INF/versions/10/module-info.class",
"/meta-inf/versions/10/Foo.class",
"/meta-inf/VERSIONS/10/Zed.class",
})
public void testIsMetaInfVersions(String input) throws IOException
{
@ -510,8 +512,6 @@ public class FileIDTest
"/META-INF/services/versions/foo.txt",
"/META-INF/versions/", // root, no version
"/META-INF/versions/Zed.class", // root, no version
"/meta-inf/versions/10/Foo.class", // not following case sensitivity rules in Java spec
"/meta-inf/VERSIONS/10/Zed.class", // not following case sensitivity rules in Java spec
})
public void testNotMetaInfVersions(String input) throws IOException
{

View File

@ -25,7 +25,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
@ -46,15 +45,15 @@ import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(WorkDirExtension.class)
public class CombinedResourceTest
{
public WorkDir workDir;
private final ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable();
@ -75,7 +74,6 @@ public class CombinedResourceTest
@Test
public void testList() throws Exception
{
Path testBaseDir = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource");
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");
@ -86,33 +84,38 @@ public class CombinedResourceTest
resourceFactory.newResource(three)
);
Function<Resource, String> relativizeToTestResources = (r) -> testBaseDir.toUri().relativize(r.getURI()).toASCIIString();
List<Resource> listing = rc.list();
List<String> listingFilenames = listing.stream().map(relativizeToTestResources).toList();
List<String> relative = listing.stream()
.map(rc::getPathTo)
.map(Path::toString)
.toList();
String[] expected = new String[] {
"one/dir/",
"one/1.txt",
"two/2.txt",
"two/dir/",
"two/1.txt",
"three/3.txt",
"three/2.txt",
"three/dir/"
"1.txt",
"2.txt",
"3.txt",
"dir"
};
assertThat(relative, containsInAnyOrder(expected));
assertThat(listingFilenames, containsInAnyOrder(expected));
for (Resource r : listing)
{
if ("dir".equals(r.getFileName()))
assertThat(r, instanceOf(CombinedResource.class));
}
listingFilenames = rc.resolve("dir").list().stream().map(relativizeToTestResources).toList();
relative = rc.resolve("dir").list().stream()
.map(rc::getPathTo)
.map(Path::toString)
.toList();
expected = new String[] {
"one/dir/1.txt",
"two/dir/2.txt",
"three/dir/3.txt"
"dir/1.txt",
"dir/2.txt",
"dir/3.txt"
};
assertThat(listingFilenames, containsInAnyOrder(expected));
assertThat(relative, containsInAnyOrder(expected));
Resource unk = rc.resolve("unknown");
assertNull(unk);
@ -137,11 +140,10 @@ public class CombinedResourceTest
// This should return a ResourceCollection with 3 `/dir/` sub-directories.
Resource r = rc.resolve("dir");
assertTrue(r instanceof CombinedResource);
rc = (CombinedResource)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)");
assertThat(r, instanceOf(CombinedResource.class));
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)");
}
@Test
@ -169,6 +171,125 @@ public class CombinedResourceTest
assertEquals(getContent(r, "3.txt"), "3 - three (in dir)");
}
/**
* Test of CombinedResource.copyTo(Resource) where the CombinedResource is a mix
* of FileSystem types.
*/
@Test
public void testCopyToDifferentFileSystem() throws Exception
{
Path testDir = workDir.getEmptyPathDir();
// Create a JAR file with contents
Path testJar = testDir.resolve("test.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI jarUri = URIUtil.uriJarPrefix(testJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(jarUri, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("test.txt"), "Contents of test.txt", StandardCharsets.UTF_8);
Path deepDir = root.resolve("deep/dir/foo");
Files.createDirectories(deepDir);
Files.writeString(deepDir.resolve("foo.txt"), "Contents of foo.txt", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource archiveResource = resourceFactory.newResource(jarUri);
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");
// A CombinedResource that has a mix of FileSystem types
Resource rc = ResourceFactory.combine(
archiveResource,
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three)
);
Path destDir = testDir.resolve("dest");
Files.createDirectory(destDir);
rc.copyTo(destDir);
Resource r = resourceFactory.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 (in dir)");
assertEquals(getContent(r, "2.txt"), "2 - two (in dir)");
assertEquals(getContent(r, "3.txt"), "3 - three (in dir)");
}
}
@Test
public void testMixedResourceCollectionGetPathTo() throws IOException
{
Path testDir = workDir.getEmptyPathDir();
// Create a JAR file with contents
Path testJar = testDir.resolve("test.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI jarUri = URIUtil.uriJarPrefix(testJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(jarUri, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("3.txt"), "Contents of 3.txt from JAR", StandardCharsets.UTF_8);
Files.writeString(root.resolve("4.txt"), "Contents of 4.txt from JAR", StandardCharsets.UTF_8);
Path dir = root.resolve("dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("2.txt"), "Contents of dir/2.txt from JAR", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource archiveResource = resourceFactory.newResource(jarUri);
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");
// A CombinedResource that has a mix of FileSystem types
Resource rc = ResourceFactory.combine(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three),
archiveResource
);
List<String> actual = new ArrayList<>();
for (Resource candidate : rc.getAllResources())
{
// Skip directories
if (candidate.isDirectory())
continue;
// Get the path relative to the base resource
Path relative = rc.getPathTo(candidate); // should not throw an exception
actual.add(relative.toString());
}
String[] expected = {
"1.txt",
"2.txt",
"3.txt",
"4.txt",
"dir/1.txt",
"dir/2.txt",
"dir/3.txt"
};
assertThat(actual, contains(expected));
}
}
@Test
public void testResourceCollectionInResourceCollection()
{
@ -342,6 +463,353 @@ public class CombinedResourceTest
assertThat(getContent(rc, "testZed.txt"), is("TestZed inside lib-zed.jar"));
}
/**
* Tests of {@link CombinedResource#contains(Resource)} consisting of only simple PathResources,
* where the "other" Resource is a simple Resource (like a PathResource)
*/
@Test
public void testContainsSimple()
{
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 four = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/four");
Resource composite = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three)
)
);
Resource oneTxt = composite.resolve("1.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(oneTxt), is(true));
Resource dir = composite.resolve("dir");
assertThat(dir, notNullValue());
assertThat(composite.contains(dir), is(true));
Resource threeTxt = composite.resolve("dir/3.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(threeTxt), is(true));
assertThat(dir.contains(threeTxt), is(true));
Resource fourth = resourceFactory.newResource(four);
// some negations
assertThat(oneTxt.contains(composite), is(false));
assertThat(threeTxt.contains(composite), is(false));
assertThat(oneTxt.contains(dir), is(false));
assertThat(threeTxt.contains(dir), is(false));
assertThat(dir.contains(composite), is(false));
assertThat(composite.contains(fourth), is(false));
assertThat(dir.contains(fourth), is(false));
}
/**
* Tests of {@link CombinedResource#contains(Resource)} consisting of mixed PathResources types (file system and jars),
* testing against "other" single Resource (not a CombinedResource)
*/
@Test
public void testMixedContainsSimple() throws IOException
{
Path testDir = workDir.getEmptyPathDir();
// Create a JAR file with contents
Path testJar = testDir.resolve("test.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI jarUri = URIUtil.uriJarPrefix(testJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(jarUri, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("1.txt"), "Contents of 1.txt from TEST JAR", StandardCharsets.UTF_8);
Path dir = root.resolve("dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("2.txt"), "Contents of 2.txt from TEST JAR", StandardCharsets.UTF_8);
}
// Create a JAR that is never part of the CombinedResource.
Path unusedJar = testDir.resolve("unused.jar");
URI unusedJarURI = URIUtil.uriJarPrefix(unusedJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(unusedJarURI, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("unused.txt"), "Contents of unused.txt from UNUSED JAR", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource archiveResource = resourceFactory.newResource(jarUri);
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 four = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/four");
Resource composite = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three),
archiveResource
)
);
Resource oneTxt = composite.resolve("1.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(oneTxt), is(true));
Resource dir = composite.resolve("dir");
assertThat(dir, notNullValue());
assertThat(composite.contains(dir), is(true));
Resource threeTxt = composite.resolve("dir/3.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(threeTxt), is(true));
assertThat(dir.contains(threeTxt), is(true));
// some negations
Resource fourth = resourceFactory.newResource(four);
Resource fourText = fourth.resolve("four.txt");
assertThat(oneTxt.contains(composite), is(false));
assertThat(threeTxt.contains(composite), is(false));
assertThat(oneTxt.contains(dir), is(false));
assertThat(threeTxt.contains(dir), is(false));
assertThat(dir.contains(composite), is(false));
assertThat(composite.contains(fourth), is(false));
assertThat(composite.contains(fourText), is(false));
assertThat(dir.contains(fourth), is(false));
Resource unused = resourceFactory.newResource(unusedJarURI);
assertThat(composite.contains(unused), is(false));
Resource unusedText = unused.resolve("unused.txt");
assertThat(composite.contains(unusedText), is(false));
}
}
/**
* Tests of {@link CombinedResource#contains(Resource)} consisting of mixed PathResources types (file system and jars),
* testing against "other" which are CombinedResource instances of their own.
*/
@Test
public void testMixedContainsOtherCombinedResource() throws IOException
{
Path testDir = workDir.getEmptyPathDir();
// Create a JAR file with contents
Path testJar = testDir.resolve("test.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI jarUri = URIUtil.uriJarPrefix(testJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(jarUri, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("1.txt"), "Contents of 1.txt from TEST JAR", StandardCharsets.UTF_8);
Path dir = root.resolve("dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("2.txt"), "Contents of dir/2.txt from TEST JAR", StandardCharsets.UTF_8);
}
// Create a JAR that is never part of the CombinedResource.
Path unusedJar = testDir.resolve("unused.jar");
URI unusedJarURI = URIUtil.uriJarPrefix(unusedJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(unusedJarURI, env))
{
Path root = zipfs.getPath("/");
Files.writeString(root.resolve("unused.txt"), "Contents of unused.txt from UNUSED JAR", StandardCharsets.UTF_8);
Path dir = root.resolve("dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("un.txt"), "Contents of dir/un.txt from TEST JAR", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource archiveResource = resourceFactory.newResource(jarUri);
Resource unused = resourceFactory.newResource(unusedJarURI);
Resource one = resourceFactory.newResource(MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/one"));
Resource two = resourceFactory.newResource(MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/two"));
Resource three = resourceFactory.newResource(MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/three"));
Resource four = resourceFactory.newResource(MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/four"));
Resource composite = ResourceFactory.combine(
List.of(
one,
two,
three,
archiveResource
)
);
Resource other = ResourceFactory.combine(
List.of(
one.resolve("dir"),
two.resolve("dir"),
three.resolve("dir")
)
);
assertThat(composite.contains(other), is(true));
other = ResourceFactory.combine(
List.of(
one.resolve("dir"),
two.resolve("dir"),
three.resolve("dir"),
archiveResource.resolve("dir")
)
);
assertThat(composite.contains(other), is(true));
// some negations
other = ResourceFactory.combine(
List.of(
one.resolve("dir"),
four.resolve("dir") // not in composite
)
);
assertThat(composite.contains(other), is(false));
other = ResourceFactory.combine(
List.of(
archiveResource.resolve("dir"),
unused.resolve("dir")
)
);
assertThat(composite.contains(other), is(false));
}
}
@Test
public void testContainsAndPathTo()
{
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 four = MavenTestingUtils.getTestResourcePathDir("org/eclipse/jetty/util/resource/four");
Resource composite = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three)
)
);
Resource compositeAlt = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(four)
)
);
Resource fourth = resourceFactory.newResource(four);
Resource oneTxt = composite.resolve("1.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(oneTxt), is(true));
Path rel = composite.getPathTo(oneTxt);
assertThat(rel, notNullValue());
assertThat(rel.getNameCount(), is(1));
assertThat(rel.getName(0).toString(), is("1.txt"));
Resource dir = composite.resolve("dir");
assertThat(dir, notNullValue());
assertThat(composite.contains(dir), is(true));
rel = composite.getPathTo(dir);
assertThat(rel, notNullValue());
assertThat(rel.getNameCount(), is(1));
assertThat(rel.getName(0).toString(), is("dir"));
Resource threeTxt = composite.resolve("dir/3.txt");
assertThat(oneTxt, notNullValue());
assertThat(composite.contains(threeTxt), is(true));
assertThat(dir.contains(threeTxt), is(true));
rel = composite.getPathTo(threeTxt);
assertThat(rel, notNullValue());
assertThat(rel.getNameCount(), is(2));
assertThat(rel.getName(0).toString(), is("dir"));
assertThat(rel.getName(1).toString(), is("3.txt"));
// some negations
assertThat(oneTxt.contains(composite), is(false));
assertThat(threeTxt.contains(composite), is(false));
assertThat(oneTxt.contains(dir), is(false));
assertThat(threeTxt.contains(dir), is(false));
assertThat(dir.contains(composite), is(false));
assertThat(composite.contains(fourth), is(false));
assertThat(dir.contains(fourth), is(false));
Resource dirAlt = compositeAlt.resolve("dir");
assertThat(compositeAlt.contains(dirAlt), is(true));
assertThat(composite.contains(dirAlt), is(false));
assertThat(composite.getPathTo(dirAlt), nullValue());
}
@Test
public void testGetFileName()
{
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");
Resource composite = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three)
)
);
assertThat(composite.getFileName(), nullValue());
Resource dir = composite.resolve("dir");
assertThat(dir.getFileName(), is("dir"));
}
@Test
public void testGetAllResources()
{
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");
Resource composite = ResourceFactory.combine(
List.of(
resourceFactory.newResource(one),
resourceFactory.newResource(two),
resourceFactory.newResource(three)
)
);
assertThat(composite.getAllResources().stream().map(composite::getPathTo).map(Path::toString).toList(), containsInAnyOrder(
"1.txt",
"2.txt",
"3.txt",
"dir",
"dir/1.txt",
"dir/2.txt",
"dir/3.txt"
));
}
private void createJar(Path outputJar, String entryName, String entryContents) throws IOException
{
Map<String, String> env = new HashMap<>();

View File

@ -359,6 +359,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir);
Resource res = base.resolve("foo");
assertThat("is contained in", res.isContainedIn(base), is(true));
assertThat(base.contains(res), is(true));
assertThat(base.getPathTo(res).getNameCount(), is(1));
assertThat(base.getPathTo(res).getName(0).toString(), is("foo"));
}
@Test

View File

@ -110,12 +110,13 @@ public class MountedPathResourceTest
public void testZipFileName()
{
Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
String s = "jar:" + testZip.toUri().toASCIIString() + "!/subdir/numbers";
URI uri = URI.create(s);
URI uri = testZip.toUri();
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource r = resourceFactory.newResource(uri);
Resource r = resourceFactory.newJarFileResource(uri);
assertThat(r.getFileName(), is(""));
r = r.resolve("subdir/numbers");
assertTrue(Resources.isReadableFile(r));
assertThat(r.getFileName(), is("numbers"));
}
@ -125,12 +126,13 @@ public class MountedPathResourceTest
public void testJarFileName()
{
Path testZip = MavenPaths.findTestResourceFile("jar-file-resource.jar");
String s = "jar:" + testZip.toUri().toASCIIString() + "!/rez/deep/zzz";
URI uri = URI.create(s);
URI uri = testZip.toUri();
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource r = resourceFactory.newResource(uri);
Resource r = resourceFactory.newJarFileResource(uri);
assertThat(r.getFileName(), is(""));
r = r.resolve("rez/deep/zzz");
assertTrue(Resources.isReadableFile(r));
assertThat(r.getFileName(), is("zzz"));
}
@ -199,7 +201,7 @@ public class MountedPathResourceTest
}
@Test
public void testJarFileGetAllResoures()
public void testJarFileGetAllResources()
throws Exception
{
Path testZip = MavenTestingUtils.getTestResourcePathFile("TestData/test.zip");
@ -210,7 +212,14 @@ public class MountedPathResourceTest
Resource r = resourceFactory.newResource(uri);
Collection<Resource> deep = r.getAllResources();
assertEquals(4, deep.size());
assertThat(deep.stream().map(r::getPathTo).map(Path::toString).toList(),
containsInAnyOrder(
"numbers",
"subsubdir",
"subsubdir/numbers",
"subsubdir/alphabet",
"alphabet"
));
}
}

View File

@ -24,7 +24,9 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.toolchain.test.FS;
@ -41,6 +43,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
@ -144,6 +147,95 @@ public class PathResourceTest
assertThat("Resource.getFileName", resource.getFileName(), is("example.jar"));
}
@Test
public void testContains(WorkDir workDir) throws IOException
{
Path testPath = workDir.getEmptyPathDir();
Path fooJar = testPath.resolve("foo.jar");
Path barJar = testPath.resolve("bar.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI fooJarUri = URIUtil.uriJarPrefix(fooJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(fooJarUri, env))
{
Path root = zipfs.getPath("/");
Path dir = root.resolve("deep/dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("foo.txt"), "Contents of foo.txt in foo.jar", StandardCharsets.UTF_8);
}
URI barJarUri = URIUtil.uriJarPrefix(barJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(barJarUri, env))
{
Path root = zipfs.getPath("/");
Path dir = root.resolve("deep/dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("bar.txt"), "Contents of bar.txt in bar.jar", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource fooResource = resourceFactory.newResource(fooJarUri);
Resource barResource = resourceFactory.newResource(barJarUri);
Resource fooText = fooResource.resolve("deep/dir/foo.txt");
assertTrue(fooResource.contains(fooText));
Resource barText = barResource.resolve("deep/dir/bar.txt");
assertTrue(barResource.contains(barText));
assertFalse(fooResource.contains(barText));
assertFalse(barResource.contains(fooText));
}
}
@Test
public void testGetPathTo(WorkDir workDir) throws IOException
{
Path testPath = workDir.getEmptyPathDir();
Path fooJar = testPath.resolve("foo.jar");
Path barJar = testPath.resolve("bar.jar");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI fooJarUri = URIUtil.uriJarPrefix(fooJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(fooJarUri, env))
{
Path root = zipfs.getPath("/");
Path dir = root.resolve("deep/dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("foo.txt"), "Contents of foo.txt in foo.jar", StandardCharsets.UTF_8);
}
URI barJarUri = URIUtil.uriJarPrefix(barJar.toUri(), "!/");
try (FileSystem zipfs = FileSystems.newFileSystem(barJarUri, env))
{
Path root = zipfs.getPath("/");
Path dir = root.resolve("deep/dir");
Files.createDirectories(dir);
Files.writeString(dir.resolve("bar.txt"), "Contents of bar.txt in bar.jar", StandardCharsets.UTF_8);
}
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource fooResource = resourceFactory.newResource(fooJarUri);
Resource barResource = resourceFactory.newResource(barJarUri);
Resource fooText = fooResource.resolve("deep/dir/foo.txt");
Path fooPathRel = fooResource.getPathTo(fooText);
assertThat(fooPathRel.toString(), is("deep/dir/foo.txt"));
Resource barText = barResource.resolve("deep/dir/bar.txt");
Path barPathRel = barResource.getPathTo(barText);
assertThat(barPathRel.toString(), is("deep/dir/bar.txt"));
// Attempt to getPathTo cross Resource will return null.
Path crossPathText = fooResource.getPathTo(barText);
assertNull(crossPathText);
}
}
@Test
public void testJarFileIsAliasFile(WorkDir workDir) throws IOException
{
@ -445,6 +537,62 @@ public class PathResourceTest
}
}
/**
* Test to ensure that a symlink loop will not trip up the Resource.getAllResources() implementation.
*/
@Test
public void testGetAllResourcesSymlinkLoop(WorkDir workDir) throws Exception
{
Path testPath = workDir.getEmptyPathDir();
Path base = testPath.resolve("base");
Path deep = base.resolve("deep");
Path deeper = deep.resolve("deeper");
FS.ensureDirExists(deeper);
Files.writeString(deeper.resolve("test.txt"), "This is the deeper TEST TXT", StandardCharsets.UTF_8);
Files.writeString(base.resolve("foo.txt"), "This is the FOO TXT in the Base dir", StandardCharsets.UTF_8);
boolean symlinkSupported;
try
{
Path bar = deeper.resolve("bar");
// Create symlink from ${base}/deep/deeper/bar/ to ${base}/deep/
Files.createSymbolicLink(bar, deep);
symlinkSupported = true;
}
catch (UnsupportedOperationException | FileSystemException e)
{
symlinkSupported = false;
}
assumeTrue(symlinkSupported, "Symlink not supported");
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(base);
Collection<Resource> allResources = resource.getAllResources();
Resource[] expected = {
resource.resolve("deep/"),
resource.resolve("deep/deeper/"),
resource.resolve("deep/deeper/bar/"),
resource.resolve("deep/deeper/test.txt"),
resource.resolve("foo.txt")
};
List<String> actual = allResources.stream()
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
assertThat(allResources, containsInAnyOrder(expected));
}
}
@Test
public void testBrokenSymlink(WorkDir workDir) throws Exception
{

View File

@ -211,7 +211,7 @@ public class ResourceFactoryTest
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
return false;
}

View File

@ -385,6 +385,8 @@ public class ResourceTest
Resource resourceDir = resourceBase.resolve("/foo");
assertTrue(Resources.exists(resourceDir));
assertThat(resourceDir.getURI(), is(foo.toUri()));
assertTrue(resourceBase.contains(resourceDir));
assertThat(resourceBase.getPathTo(resourceDir).getName(0).toString(), is("foo"));
}
@Test

View File

@ -1,6 +1,7 @@
#org.eclipse.jetty.util.LEVEL=DEBUG
#org.eclipse.jetty.util.Utf8Appendable.LEVEL=DEBUG
#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
#org.eclipse.jetty.util.resource.PathResource.LEVEL=DEBUG
#org.eclipse.jetty.util.thread.QueuedThreadPool.LEVEL=DEBUG
#org.eclipse.jetty.util.thread.ReservedThreadExecutor.LEVEL=DEBUG
#org.eclipse.jetty.util.thread.SerializedInvoker$NamedRunnable.LEVEL=DEBUG

View File

@ -24,7 +24,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.FileID;
@ -133,7 +132,7 @@ public class AnnotationParser
* Convert internal names to simple names.
*
* @param list the list of internal names
* @return the list of simple names
* @return the array of simple names
*/
public static String[] normalize(String[] list)
{
@ -581,31 +580,38 @@ public class AnnotationParser
*/
protected void parseDir(Set<? extends Handler> handlers, Resource dirResource) throws Exception
{
Path dir = dirResource.getPath();
if (LOG.isDebugEnabled())
LOG.debug("Scanning dir {}", dir.toUri());
LOG.debug("Scanning dir {}", dirResource);
assert dirResource.isDirectory();
ExceptionUtil.MultiException multiException = new ExceptionUtil.MultiException();
try (Stream<Path> classStream = Files.walk(dir))
for (Resource candidate : dirResource.getAllResources())
{
classStream
.filter(Files::isRegularFile)
.filter((path) -> !FileID.isHidden(dir, path))
.filter(FileID::isNotMetaInfVersions)
.filter(FileID::isNotModuleInfoClass)
.filter(FileID::isClassFile)
.forEach(classFile ->
{
try
{
parseClass(handlers, dirResource, classFile);
}
catch (Exception ex)
{
multiException.add(new RuntimeException("Error scanning entry " + ex, ex));
}
});
// Skip directories
if (candidate.isDirectory())
continue;
// Get the path relative to the base resource
Path relative = dirResource.getPathTo(candidate);
// select only relative non-hidden class files that are not modules nor versions
if (relative == null ||
FileID.isHidden(relative) ||
FileID.isMetaInfVersions(relative) ||
FileID.isModuleInfoClass(relative) ||
!FileID.isClassFile(relative))
continue;
try
{
parseClass(handlers, dirResource, candidate.getPath());
}
catch (Exception ex)
{
multiException.add(new RuntimeException("Error scanning entry " + ex, ex));
}
}
multiException.ifExceptionThrow();

View File

@ -0,0 +1,19 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.its.jetty_simple_base_webapp;
public class Foo
{
}

View File

@ -0,0 +1,5 @@
<html>
<body>
<h1>Base Index</h1>
</body>
</html>

View File

@ -74,6 +74,8 @@
<artifactId>jetty-simple-base-webapp</artifactId>
<includes>
<include>index.html</include>
<include>base/**</include>
<include>WEB-INF/**</include>
</includes>
</overlay>
<overlay/>
@ -100,8 +102,8 @@
<systemPropertyVariables>
<jetty.port.file>${jetty.port.file}</jetty.port.file>
<context.path>/setbycontextxml</context.path>
<contentCheck>Simple Base Webapp Index</contentCheck>
<pathToCheck>/index.html</pathToCheck>
<contentCheck>Base Index</contentCheck>
<pathToCheck>/base/index.html</pathToCheck>
<maven.it.name>${project.groupId}:${project.artifactId}</maven.it.name>
</systemPropertyVariables>
<dependenciesToScan>

View File

@ -0,0 +1,18 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.its.jetty_simple_webapp;
public class Bar
{
}

View File

@ -130,11 +130,27 @@ public class MavenResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
if (_resource == null)
return false;
return _resource.isContainedIn(r);
return _resource.isContainedIn(container);
}
@Override
public boolean contains(Resource other)
{
if (_resource == null)
return false;
return _resource.contains(other);
}
@Override
public Path getPathTo(Resource other)
{
if (_resource == null)
return null;
return _resource.getPathTo(other);
}
@Override

View File

@ -15,12 +15,16 @@ package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
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.ResourceFactory;
@ -123,17 +127,21 @@ public class OverlayManager
* @return the location to which it was unpacked
* @throws IOException if there is an IO problem
*/
protected Resource unpackOverlay(Overlay overlay)
protected Resource unpackOverlay(Overlay overlay)
throws IOException
{
if (overlay.getResource() == null)
return null; //nothing to unpack
//Get the name of the overlayed war and unpack it to a dir of the
//same name in the temporary directory
String name = overlay.getResource().getFileName();
//same name in the temporary directory.
//We know it is a war because it came from the maven repo
assert overlay.getResource() instanceof MountedPathResource;
Path p = Paths.get(URIUtil.unwrapContainer(overlay.getResource().getURI()));
String name = p.getName(p.getNameCount() - 1).toString();
name = name.replace('.', '_');
File overlaysDir = new File(warPlugin.getProject().getBuild().getDirectory(), "jetty_overlays");
File dir = new File(overlaysDir, name);

View File

@ -132,9 +132,21 @@ public class SelectiveJarResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
return _delegate.isContainedIn(r);
return _delegate.isContainedIn(container);
}
@Override
public boolean contains(Resource other)
{
return _delegate.contains(other);
}
@Override
public Path getPathTo(Resource other)
{
return _delegate.getPathTo(other);
}
@Override

View File

@ -401,24 +401,19 @@ public class MetaData
*/
private Resource getEnclosingResource(List<Resource> resources, Resource resource)
{
Resource enclosingResource = null;
try
{
for (Resource r : resources)
{
if (resource.isContainedIn(r))
{
enclosingResource = r;
break;
}
if (r.contains(resource))
return r;
}
return enclosingResource;
}
catch (Exception e)
{
LOG.warn("Not contained within?", e);
return null;
}
return null;
}
public void addDescriptorProcessor(DescriptorProcessor p)

View File

@ -772,8 +772,8 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
return null;
// Is there a WEB-INF directory anywhere in the Resource Base?
// ResourceBase could be a ResourceCollection
// The result could be a ResourceCollection with multiple WEB-INF directories
// ResourceBase could be a CombinedResource
// The result could be a CombinedResource with multiple WEB-INF directories
// Can return from WEB-INF/lib/foo.jar!/WEB-INF
// Can also never return from a META-INF/versions/#/WEB-INF location
Resource webInf = getBaseResource().resolve("WEB-INF/");

View File

@ -24,7 +24,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.FileID;
@ -578,31 +577,38 @@ public class AnnotationParser
*/
protected void parseDir(Set<? extends Handler> handlers, Resource dirResource) throws Exception
{
Path dir = dirResource.getPath();
if (LOG.isDebugEnabled())
LOG.debug("Scanning dir {}", dir.toUri());
LOG.debug("Scanning dir {}", dirResource);
assert dirResource.isDirectory();
ExceptionUtil.MultiException multiException = new ExceptionUtil.MultiException();
try (Stream<Path> classStream = Files.walk(dir))
for (Resource candidate : dirResource.getAllResources())
{
classStream
.filter(Files::isRegularFile)
.filter((path) -> !FileID.isHidden(dir, path))
.filter(FileID::isNotMetaInfVersions)
.filter(FileID::isNotModuleInfoClass)
.filter(FileID::isClassFile)
.forEach(classFile ->
{
try
{
parseClass(handlers, dirResource, classFile);
}
catch (Exception ex)
{
multiException.add(new RuntimeException("Error scanning entry " + ex, ex));
}
});
// Skip directories
if (candidate.isDirectory())
continue;
// Get the path relative to the base resource
Path relative = dirResource.getPathTo(candidate);
// select only relative non-hidden class files that are not modules nor versions
if (relative == null ||
FileID.isHidden(relative) ||
FileID.isMetaInfVersions(relative) ||
FileID.isModuleInfoClass(relative) ||
!FileID.isClassFile(relative))
continue;
try
{
parseClass(handlers, dirResource, candidate.getPath());
}
catch (Exception ex)
{
multiException.add(new RuntimeException("Error scanning entry " + ex, ex));
}
}
multiException.ifExceptionThrow();

View File

@ -0,0 +1,19 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.its.jetty_simple_base_webapp;
public class Foo
{
}

View File

@ -0,0 +1,5 @@
<html>
<body>
<h1>Base Index</h1>
</body>
</html>

View File

@ -74,6 +74,8 @@
<artifactId>jetty-simple-base-webapp</artifactId>
<includes>
<include>index.html</include>
<include>base/**</include>
<include>WEB-INF/**</include>
</includes>
</overlay>
<overlay/>
@ -100,8 +102,8 @@
<systemPropertyVariables>
<jetty.port.file>${jetty.port.file}</jetty.port.file>
<context.path>/setbycontextxml</context.path>
<contentCheck>Simple Base Webapp Index</contentCheck>
<pathToCheck>/index.html</pathToCheck>
<contentCheck>Base Index</contentCheck>
<pathToCheck>/base/index.html</pathToCheck>
<maven.it.name>${project.groupId}:${project.artifactId}</maven.it.name>
</systemPropertyVariables>
<dependenciesToScan>

View File

@ -0,0 +1,18 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.its.jetty_simple_webapp;
public class Bar
{
}

View File

@ -130,11 +130,27 @@ public class MavenResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
if (_resource == null)
return false;
return _resource.isContainedIn(r);
return _resource.isContainedIn(container);
}
@Override
public boolean contains(Resource other)
{
if (_resource == null)
return false;
return _resource.contains(other);
}
@Override
public Path getPathTo(Resource other)
{
if (_resource == null)
return null;
return _resource.getPathTo(other);
}
@Override

View File

@ -15,12 +15,16 @@ package org.eclipse.jetty.ee9.maven.plugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
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.ResourceFactory;
@ -133,9 +137,12 @@ public class OverlayManager
//Get the name of the overlayed war and unpack it to a dir of the
//same name in the temporary directory
String name = overlay.getResource().getFileName();
//We know it is a war because it came from the maven repo
assert overlay.getResource() instanceof MountedPathResource;
Path p = Paths.get(URIUtil.unwrapContainer(overlay.getResource().getURI()));
String name = p.getName(p.getNameCount() - 1).toString();
name = name.replace('.', '_');
File overlaysDir = new File(warPlugin.getProject().getBuild().getDirectory(), "jetty_overlays");
File dir = new File(overlaysDir, name);

View File

@ -132,9 +132,21 @@ public class SelectiveJarResource extends Resource
}
@Override
public boolean isContainedIn(Resource r)
public boolean isContainedIn(Resource container)
{
return _delegate.isContainedIn(r);
return _delegate.isContainedIn(container);
}
@Override
public boolean contains(Resource other)
{
return _delegate.contains(other);
}
@Override
public Path getPathTo(Resource other)
{
return _delegate.getPathTo(other);
}
@Override

View File

@ -401,24 +401,19 @@ public class MetaData
*/
private Resource getEnclosingResource(List<Resource> resources, Resource resource)
{
Resource enclosingResource = null;
try
{
for (Resource r : resources)
{
if (resource.isContainedIn(r))
{
enclosingResource = r;
break;
}
if (r.contains(resource))
return r;
}
return enclosingResource;
}
catch (Exception e)
{
LOG.warn("Not contained within?", e);
return null;
}
return null;
}
public void addDescriptorProcessor(DescriptorProcessor p)