Fix #12128 Combined ClassLoader Resources (#12130)

Fix #12104 by returning a CombinedResource for a ClassLoader resource that has multiple directory matches.
This commit is contained in:
Greg Wilkins 2024-08-05 13:09:53 +10:00 committed by GitHub
parent fe6b14b8fb
commit abab5949ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 85 additions and 36 deletions

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.util.resource;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
@ -20,11 +21,11 @@ import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.StringTokenizer;
import java.util.function.Function;
import org.eclipse.jetty.util.FileID;
import org.eclipse.jetty.util.StringUtil;
@ -197,7 +198,7 @@ public interface ResourceFactory
*
* @param resource the resource name to find in a classloader
* @param searchSystemClassLoader true to search {@link ClassLoader#getSystemResource(String)}, false to skip
* @return The new Resource
* @return The new Resource, which may be a {@link CombinedResource} if multiple directory resources are found.
* @throws IllegalArgumentException if resource name or resulting URL from ClassLoader is invalid.
*/
default Resource newClassLoaderResource(String resource, boolean searchSystemClassLoader)
@ -205,47 +206,60 @@ public interface ResourceFactory
if (StringUtil.isBlank(resource))
throw new IllegalArgumentException("Resource String is invalid: " + resource);
URL url = null;
List<Function<String, URL>> loaders = new ArrayList<>();
loaders.add(Thread.currentThread().getContextClassLoader()::getResource);
loaders.add(ResourceFactory.class.getClassLoader()::getResource);
if (searchSystemClassLoader)
loaders.add(ClassLoader::getSystemResource);
for (Function<String, URL> loader : loaders)
// We need a local interface to combine static and non-static methods
interface Source
{
if (url != null)
break;
Enumeration<URL> getResources(String name) throws IOException;
}
try
List<Source> sources = new ArrayList<>();
sources.add(Thread.currentThread().getContextClassLoader()::getResources);
sources.add(ResourceFactory.class.getClassLoader()::getResources);
if (searchSystemClassLoader)
sources.add(ClassLoader::getSystemResources);
List<Resource> resources = new ArrayList<>();
String[] names = resource.startsWith("/") ? new String[] {resource, resource.substring(1)} : new String[] {resource};
// For each source of resource
for (Source source : sources)
{
// for each variation of the resource name
for (String name : names)
{
url = loader.apply(resource);
if (url == null && resource.startsWith("/"))
url = loader.apply(resource.substring(1));
}
catch (IllegalArgumentException e)
{
// Catches scenario where a bad Windows path like "C:\dev" is
// improperly escaped, which various downstream classloaders
// tend to have a problem with
if (LOG.isTraceEnabled())
LOG.trace("Ignoring bad getResource(): {}", resource, e);
try
{
// Get all matching URLs
Enumeration<URL> urls = source.getResources(name);
while (urls.hasMoreElements())
{
// Get the resource
Resource r = newResource(urls.nextElement().toURI());
// If it is not a directory, then return it as the singular found resource
if (!r.isDirectory())
return r;
// otherwise add it to a list of resource to combine.
resources.add(r);
}
}
catch (Throwable e)
{
// Catches scenario where a bad Windows path like "C:\dev" is
// improperly escaped, which various downstream classloaders
// tend to have a problem with
if (LOG.isTraceEnabled())
LOG.trace("Ignoring bad getResource(): {}", resource, e);
}
}
}
if (url == null)
if (resources.isEmpty())
return null;
try
{
URI uri = url.toURI();
return newResource(uri);
}
catch (URISyntaxException e)
{
throw new IllegalArgumentException("Error creating resource from URL: " + url, e);
}
if (resources.size() == 1)
return resources.get(0);
return combine(resources);
}
/**

View File

@ -19,10 +19,12 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.FS;
@ -31,6 +33,7 @@ import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.URIUtil;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
@ -43,6 +46,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -61,13 +65,44 @@ public class ResourceFactoryTest
"TestData/alphabet.txt", "/TestData/alphabet.txt",
"TestData/", "/TestData/", "TestData", "/TestData"
})
public void testNewClassLoaderResourceExists(String reference)
@Disabled
public void testNewClassLoaderResourceExists(String reference) throws IOException
{
Path alt = workDir.getEmptyPathDir().resolve("alt");
Files.createDirectories(alt.resolve("TestData"));
URI altURI = alt.toUri();
ClassLoader loader = new URLClassLoader(new URL[] {altURI.toURL()});
ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource altResource = resourceFactory.newResource(altURI);
Thread.currentThread().setContextClassLoader(loader);
Resource resource = resourceFactory.newClassLoaderResource(reference);
assertNotNull(resource, "Reference [" + reference + "] should be found");
assertTrue(resource.exists(), "Reference [" + reference + "] -> Resource[" + resource + "] should exist");
if (resource.isDirectory())
{
assertThat(resource, instanceOf(CombinedResource.class));
AtomicBoolean fromWorkDir = new AtomicBoolean();
AtomicBoolean fromResources = new AtomicBoolean();
resource.forEach(r ->
{
if (r.isContainedIn(altResource))
fromWorkDir.set(true);
else
fromResources.set(true);
});
assertTrue(fromWorkDir.get());
assertTrue(fromResources.get());
}
}
finally
{
Thread.currentThread().setContextClassLoader(oldLoader);
workDir.ensureEmpty();
}
}