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

View File

@ -19,10 +19,12 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jetty.toolchain.test.FS; 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.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith; 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.contains;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -61,13 +65,44 @@ public class ResourceFactoryTest
"TestData/alphabet.txt", "/TestData/alphabet.txt", "TestData/alphabet.txt", "/TestData/alphabet.txt",
"TestData/", "/TestData/", "TestData", "/TestData" "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()) try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{ {
Resource altResource = resourceFactory.newResource(altURI);
Thread.currentThread().setContextClassLoader(loader);
Resource resource = resourceFactory.newClassLoaderResource(reference); Resource resource = resourceFactory.newClassLoaderResource(reference);
assertNotNull(resource, "Reference [" + reference + "] should be found"); assertNotNull(resource, "Reference [" + reference + "] should be found");
assertTrue(resource.exists(), "Reference [" + reference + "] -> Resource[" + resource + "] should exist"); 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();
} }
} }