Merge branch 'jetty-10.0.x-5133-webappcontext-extraclasspath-cleanup' into jetty-10.0.x

This commit is contained in:
Joakim Erdfelt 2020-09-22 12:24:42 -05:00
commit dc6bee5c96
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
21 changed files with 356 additions and 379 deletions

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.stream.Collectors;
import org.eclipse.jetty.quickstart.QuickStartConfiguration; import org.eclipse.jetty.quickstart.QuickStartConfiguration;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -337,17 +338,8 @@ public class WebAppPropertyConverter
* @param resources the resources to convert * @param resources the resources to convert
* @return csv string of resource filenames * @return csv string of resource filenames
*/ */
private static String toCSV(Resource[] resources) private static String toCSV(List<Resource> resources)
{ {
StringBuilder rb = new StringBuilder(); return resources.stream().map(Object::toString).collect(Collectors.joining(","));
for (Resource r : resources)
{
if (rb.length() > 0)
rb.append(",");
rb.append(r.toString());
}
return rb.toString();
} }
} }

View File

@ -32,7 +32,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.osgi.boot.utils.BundleFileLocatorHelperFactory; import org.eclipse.jetty.osgi.boot.utils.BundleFileLocatorHelperFactory;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
@ -145,7 +144,7 @@ public class AnnotationParser extends org.eclipse.jetty.annotations.AnnotationPa
} }
}); });
boolean hasDotPath = false; boolean hasDotPath = false;
StringTokenizer tokenizer = new StringTokenizer(bundleClasspath, ",;", false); StringTokenizer tokenizer = new StringTokenizer(bundleClasspath, StringUtil.DEFAULT_DELIMS, false);
while (tokenizer.hasMoreTokens()) while (tokenizer.hasMoreTokens())
{ {
String token = tokenizer.nextToken().trim(); String token = tokenizer.nextToken().trim();

View File

@ -233,7 +233,7 @@ public class ServerInstanceWrapper
Thread.currentThread().setContextClassLoader(libExtClassLoader); Thread.currentThread().setContextClassLoader(libExtClassLoader);
String jettyConfigurationUrls = (String)props.get(OSGiServerConstants.MANAGED_JETTY_XML_CONFIG_URLS); String jettyConfigurationUrls = (String)props.get(OSGiServerConstants.MANAGED_JETTY_XML_CONFIG_URLS);
List<URL> jettyConfigurations = jettyConfigurationUrls != null ? Util.fileNamesAsURLs(jettyConfigurationUrls, Util.DEFAULT_DELIMS) : null; List<URL> jettyConfigurations = jettyConfigurationUrls != null ? Util.fileNamesAsURLs(jettyConfigurationUrls, StringUtil.DEFAULT_DELIMS) : null;
_server = configure(server, jettyConfigurations, props); _server = configure(server, jettyConfigurations, props);
@ -418,7 +418,7 @@ public class ServerInstanceWrapper
List<URL> libURLs = new ArrayList<>(); List<URL> libURLs = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(sharedURLs, ",;", false); StringTokenizer tokenizer = new StringTokenizer(sharedURLs, StringUtil.DEFAULT_DELIMS, false);
while (tokenizer.hasMoreTokens()) while (tokenizer.hasMoreTokens())
{ {
String tok = tokenizer.nextToken(); String tok = tokenizer.nextToken();

View File

@ -20,15 +20,12 @@ package org.eclipse.jetty.osgi.boot.internal.webapp;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -201,22 +198,16 @@ public class OSGiWebappClassLoader extends WebAppClassLoader implements BundleRe
@Override @Override
public void addClassPath(String classPath) throws IOException public void addClassPath(String classPath) throws IOException
{ {
for (Resource resource : Resource.fromList(classPath, false, (path) -> getContext().newResource(path)))
StringTokenizer tokenizer = new StringTokenizer(classPath, ",;");
while (tokenizer.hasMoreTokens())
{ {
String path = tokenizer.nextToken();
Resource resource = getContext().newResource(path);
// Resolve file path if possible
File file = resource.getFile(); File file = resource.getFile();
if (file != null && isAcceptableLibrary(file, JAR_WITH_SUCH_CLASS_MUST_BE_EXCLUDED)) if (file != null && isAcceptableLibrary(file, JAR_WITH_SUCH_CLASS_MUST_BE_EXCLUDED))
{ {
super.addClassPath(path); super.addClassPath(resource);
} }
else else
{ {
LOG.info("Did not add {} to the classloader of the webapp {}", path, getContext()); LOG.info("Did not add {} to the classloader of the webapp {}", resource, getContext());
} }
} }
} }
@ -272,37 +263,4 @@ public class OSGiWebappClassLoader extends WebAppClassLoader implements BundleRe
} }
return true; return true;
} }
private static Field _contextField;
/**
* In the case of the generation of a webapp via a jetty context file we
* need a proper classloader to setup the app before we have the
* WebappContext So we place a fake one there to start with. We replace it
* with the actual webapp context with this method. We also apply the
* extraclasspath there at the same time.
*
* @param webappContext the web app context
*/
public void setWebappContext(WebAppContext webappContext)
{
try
{
if (_contextField == null)
{
_contextField = WebAppClassLoader.class.getDeclaredField("_context");
_contextField.setAccessible(true);
}
_contextField.set(this, webappContext);
if (webappContext.getExtraClasspath() != null)
{
addClassPath(webappContext.getExtraClasspath());
}
}
catch (Throwable t)
{
// humf that will hurt if it does not work.
LOG.warn("Unable to set webappcontext", t);
}
}
} }

View File

@ -35,8 +35,6 @@ import org.osgi.framework.InvalidSyntaxException;
*/ */
public class Util public class Util
{ {
public static final String DEFAULT_DELIMS = ",;";
/** /**
* Create an osgi filter for the given classname and server name. * Create an osgi filter for the given classname and server name.
* *
@ -101,7 +99,7 @@ public class Util
public static List<URL> fileNamesAsURLs(String val, String delims) public static List<URL> fileNamesAsURLs(String val, String delims)
throws Exception throws Exception
{ {
String separators = DEFAULT_DELIMS; String separators = StringUtil.DEFAULT_DELIMS;
if (delims == null) if (delims == null)
delims = separators; delims = separators;

View File

@ -210,7 +210,7 @@ public class CachedContentFactory implements HttpContent.ContentFactory
return (len > 0 && (_useFileMappedBuffer || (len < _maxCachedFileSize && len < _maxCacheSize))); return (len > 0 && (_useFileMappedBuffer || (len < _maxCachedFileSize && len < _maxCacheSize)));
} }
private HttpContent load(String pathInContext, Resource resource, int maxBufferSize) private HttpContent load(String pathInContext, Resource resource, int maxBufferSize) throws IOException
{ {
if (resource == null || !resource.exists()) if (resource == null || !resource.exists())
return null; return null;

View File

@ -50,20 +50,26 @@ public class ResourceContentFactory implements ContentFactory
} }
@Override @Override
public HttpContent getContent(String pathInContext, int maxBufferSize) public HttpContent getContent(String pathInContext, int maxBufferSize) throws IOException
throws IOException
{ {
try try
{ {
// try loading the content from our factory. // try loading the content from our factory.
Resource resource = _factory.getResource(pathInContext); Resource resource = _factory.getResource(pathInContext);
HttpContent loaded = load(pathInContext, resource, maxBufferSize); return load(pathInContext, resource, maxBufferSize);
return loaded;
} }
catch (Throwable t) catch (Throwable t)
{ {
// Any error has potential to reveal fully qualified path // There are many potential Exceptions that can reveal a fully qualified path.
throw (InvalidPathException)new InvalidPathException(pathInContext, "Invalid PathInContext").initCause(t); // See Issue #2560 - Always wrap a Throwable here in an InvalidPathException
// that is limited to only the provided pathInContext.
// The cause (which might reveal a fully qualified path) is still available,
// on the Exception and the logging, but is not reported in normal error page situations.
// This specific exception also allows WebApps to specifically hook into a known / reliable
// Exception type for ErrorPageErrorHandling logic.
InvalidPathException saferException = new InvalidPathException(pathInContext, "Invalid PathInContext");
saferException.initCause(t);
throw saferException;
} }
} }

View File

@ -881,6 +881,6 @@ public class ResourceService
* @param pathInContext the path of the request * @param pathInContext the path of the request
* @return The path of the matching welcome file in context or null. * @return The path of the matching welcome file in context or null.
*/ */
String getWelcomeFile(String pathInContext); String getWelcomeFile(String pathInContext) throws IOException;
} }
} }

View File

@ -1896,6 +1896,13 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return Collections.unmodifiableMap(_localeEncodingMap); return Collections.unmodifiableMap(_localeEncodingMap);
} }
/**
* Attempt to get a Resource from the Context.
*
* @param path the path within the resource to attempt to get
* @return the resource, or null if not available.
* @throws MalformedURLException if unable to form a Resource from the provided path
*/
public Resource getResource(String path) throws MalformedURLException public Resource getResource(String path) throws MalformedURLException
{ {
if (path == null || !path.startsWith(URIUtil.SLASH)) if (path == null || !path.startsWith(URIUtil.SLASH))

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.server.handler; package org.eclipse.jetty.server.handler;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -36,6 +37,7 @@ import org.eclipse.jetty.server.ResourceContentFactory;
import org.eclipse.jetty.server.ResourceService; import org.eclipse.jetty.server.ResourceService;
import org.eclipse.jetty.server.ResourceService.WelcomeFactory; import org.eclipse.jetty.server.ResourceService.WelcomeFactory;
import org.eclipse.jetty.server.handler.ContextHandler.Context; import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.ResourceFactory;
@ -78,7 +80,7 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
} }
@Override @Override
public String getWelcomeFile(String pathInContext) public String getWelcomeFile(String pathInContext) throws IOException
{ {
if (_welcomes == null) if (_welcomes == null)
return null; return null;
@ -87,7 +89,7 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
{ {
String welcomeInContext = URIUtil.addPaths(pathInContext, _welcomes[i]); String welcomeInContext = URIUtil.addPaths(pathInContext, _welcomes[i]);
Resource welcome = getResource(welcomeInContext); Resource welcome = getResource(welcomeInContext);
if (welcome != null && welcome.exists()) if (welcome.exists())
return welcomeInContext; return welcomeInContext;
} }
// not found // not found
@ -140,44 +142,51 @@ public class ResourceHandler extends HandlerWrapper implements ResourceFactory,
} }
@Override @Override
public Resource getResource(String path) public Resource getResource(String path) throws IOException
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("{} getResource({})", _context == null ? _baseResource : _context, path); LOG.debug("{} getResource({})", _context == null ? _baseResource : _context, path);
if (path == null || !path.startsWith("/")) if (StringUtil.isBlank(path))
return null;
try
{ {
Resource r = null; throw new IllegalArgumentException("Path is blank");
}
if (_baseResource != null) if (!path.startsWith("/"))
{
throw new IllegalArgumentException("Path reference invalid: " + path);
}
Resource r = null;
if (_baseResource != null)
{
path = URIUtil.canonicalPath(path);
r = _baseResource.addPath(path);
if (r.isAlias() && (_context == null || !_context.checkAlias(path, r)))
{ {
path = URIUtil.canonicalPath(path); if (LOG.isDebugEnabled())
r = _baseResource.addPath(path); LOG.debug("Rejected alias resource={} alias={}", r, r.getAlias());
throw new IllegalStateException("Rejected alias reference: " + path);
if (r != null && r.isAlias() && (_context == null || !_context.checkAlias(path, r)))
{
if (LOG.isDebugEnabled())
LOG.debug("resource={} alias={}", r, r.getAlias());
return null;
}
} }
else if (_context != null)
r = _context.getResource(path);
if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
r = getStylesheet();
return r;
} }
catch (Exception e) else if (_context != null)
{ {
LOG.debug("Unable to get Resource for {}", path, e); r = _context.getResource(path);
if (r != null)
return r;
} }
return null; if ((r == null || !r.exists()) && path.endsWith("/jetty-dir.css"))
r = getStylesheet();
if (r == null)
{
throw new FileNotFoundException("Resource: " + path);
}
return r;
} }
/** /**

View File

@ -26,6 +26,7 @@ import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.List;
import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpContent;
@ -110,7 +111,7 @@ public class ResourceCacheTest
} }
@Test @Test
public void testMutlipleSources1() throws Exception public void testMultipleSources1() throws Exception
{ {
Path basePath = createUtilTestResources(workDir.getEmptyPathDir()); Path basePath = createUtilTestResources(workDir.getEmptyPathDir());
@ -119,12 +120,12 @@ public class ResourceCacheTest
new PathResource(basePath.resolve("two")), new PathResource(basePath.resolve("two")),
new PathResource(basePath.resolve("three"))); new PathResource(basePath.resolve("three")));
Resource[] r = rc.getResources(); List<Resource> r = rc.getResources();
MimeTypes mime = new MimeTypes(); MimeTypes mime = new MimeTypes();
CachedContentFactory rc3 = new CachedContentFactory(null, r[2], mime, false, false, CompressedContentFormat.NONE); CachedContentFactory rc3 = new CachedContentFactory(null, r.get(2), mime, false, false, CompressedContentFormat.NONE);
CachedContentFactory rc2 = new CachedContentFactory(rc3, r[1], mime, false, false, CompressedContentFormat.NONE); CachedContentFactory rc2 = new CachedContentFactory(rc3, r.get(1), mime, false, false, CompressedContentFormat.NONE);
CachedContentFactory rc1 = new CachedContentFactory(rc2, r[0], mime, false, false, CompressedContentFormat.NONE); CachedContentFactory rc1 = new CachedContentFactory(rc2, r.get(0), mime, false, false, CompressedContentFormat.NONE);
assertEquals(getContent(rc1, "1.txt"), "1 - one"); assertEquals(getContent(rc1, "1.txt"), "1 - one");
assertEquals(getContent(rc1, "2.txt"), "2 - two"); assertEquals(getContent(rc1, "2.txt"), "2 - two");
@ -149,11 +150,11 @@ public class ResourceCacheTest
new PathResource(basePath.resolve("two")), new PathResource(basePath.resolve("two")),
new PathResource(basePath.resolve("three"))); new PathResource(basePath.resolve("three")));
Resource[] r = rc.getResources(); List<Resource> r = rc.getResources();
MimeTypes mime = new MimeTypes(); MimeTypes mime = new MimeTypes();
CachedContentFactory rc3 = new CachedContentFactory(null, r[2], mime, false, false, CompressedContentFormat.NONE); CachedContentFactory rc3 = new CachedContentFactory(null, r.get(2), mime, false, false, CompressedContentFormat.NONE);
CachedContentFactory rc2 = new CachedContentFactory(rc3, r[1], mime, false, false, CompressedContentFormat.NONE) CachedContentFactory rc2 = new CachedContentFactory(rc3, r.get(1), mime, false, false, CompressedContentFormat.NONE)
{ {
@Override @Override
public boolean isCacheable(Resource resource) public boolean isCacheable(Resource resource)
@ -162,7 +163,7 @@ public class ResourceCacheTest
} }
}; };
CachedContentFactory rc1 = new CachedContentFactory(rc2, r[0], mime, false, false, CompressedContentFormat.NONE); CachedContentFactory rc1 = new CachedContentFactory(rc2, r.get(0), mime, false, false, CompressedContentFormat.NONE);
assertEquals(getContent(rc1, "1.txt"), "1 - one"); assertEquals(getContent(rc1, "1.txt"), "1 - one");
assertEquals(getContent(rc1, "2.txt"), "2 - two"); assertEquals(getContent(rc1, "2.txt"), "2 - two");

View File

@ -37,6 +37,7 @@ public class StringUtil
public static final String ALL_INTERFACES = "0.0.0.0"; public static final String ALL_INTERFACES = "0.0.0.0";
public static final String CRLF = "\r\n"; public static final String CRLF = "\r\n";
public static final String DEFAULT_DELIMS = ",;";
public static final String __ISO_8859_1 = "iso-8859-1"; public static final String __ISO_8859_1 = "iso-8859-1";
public static final String __UTF8 = "utf-8"; public static final String __UTF8 = "utf-8";

View File

@ -124,6 +124,6 @@ public class EmptyResource extends Resource
@Override @Override
public Resource addPath(String path) throws IOException, MalformedURLException public Resource addPath(String path) throws IOException, MalformedURLException
{ {
return null; return this;
} }
} }

View File

@ -34,10 +34,13 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.Loader;
@ -412,7 +415,7 @@ public abstract class Resource implements ResourceFactory, Closeable
* given name. * given name.
* *
* @param path The path segment to add, which is not encoded * @param path The path segment to add, which is not encoded
* @return the Resource for the resolved path within this Resource. * @return the Resource for the resolved path within this Resource, never null
* @throws IOException if unable to resolve the path * @throws IOException if unable to resolve the path
* @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed. * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
*/ */
@ -421,22 +424,11 @@ public abstract class Resource implements ResourceFactory, Closeable
/** /**
* Get a resource from within this resource. * Get a resource from within this resource.
* <p>
* This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
* This method satisfied the {@link ResourceFactory} interface.
*/ */
@Override @Override
public Resource getResource(String path) public Resource getResource(String path) throws IOException
{ {
try return addPath(path);
{
return addPath(path);
}
catch (Exception e)
{
LOG.debug("Unable to addPath", e);
return null;
}
} }
// FIXME: this appears to not be used // FIXME: this appears to not be used
@ -923,4 +915,97 @@ public abstract class Resource implements ResourceFactory, Closeable
{ {
return file.toURI().toURL(); return file.toURI().toURL();
} }
/**
* Parse a list of String delimited resources 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
* @return the list of resources parsed from input string.
*/
public static List<Resource> fromList(String resources, boolean globDirs) throws IOException
{
return fromList(resources, globDirs, Resource::newResource);
}
/**
* 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);
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken().trim();
// Is this a glob reference?
if (token.endsWith("/*") || token.endsWith("\\*"))
{
String dir = token.substring(0, token.length() - 2);
// Use directory
Resource dirResource = resourceFactory.getResource(dir);
if (dirResource.exists() && dirResource.isDirectory())
{
// To obtain the list of entries
String[] entries = dirResource.list();
if (entries != null)
{
Arrays.sort(entries);
for (String entry : entries)
{
try
{
Resource resource = dirResource.addPath(entry);
if (!resource.isDirectory())
{
returnedResources.add(resource);
}
else if (globDirs)
{
returnedResources.add(resource);
}
}
catch (Exception ex)
{
LOG.warn("Bad glob [{}] entry: {}", token, entry, ex);
}
}
}
}
}
else
{
// Simple reference, add as-is
returnedResources.add(resourceFactory.getResource(token));
}
}
return returnedResources;
}
} }

View File

@ -25,12 +25,12 @@ import java.net.MalformedURLException;
import java.net.URI; import java.net.URI;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
/** /**
@ -42,7 +42,7 @@ import org.eclipse.jetty.util.URIUtil;
*/ */
public class ResourceCollection extends Resource public class ResourceCollection extends Resource
{ {
private Resource[] _resources; private List<Resource> _resources;
/** /**
* Instantiates an empty resource collection. * Instantiates an empty resource collection.
@ -51,7 +51,7 @@ public class ResourceCollection extends Resource
*/ */
public ResourceCollection() public ResourceCollection()
{ {
_resources = new Resource[0]; _resources = new ArrayList<>();
} }
/** /**
@ -61,7 +61,7 @@ public class ResourceCollection extends Resource
*/ */
public ResourceCollection(Resource... resources) public ResourceCollection(Resource... resources)
{ {
List<Resource> list = new ArrayList<>(); _resources = new ArrayList<>();
for (Resource r : resources) for (Resource r : resources)
{ {
if (r == null) if (r == null)
@ -70,15 +70,25 @@ public class ResourceCollection extends Resource
} }
if (r instanceof ResourceCollection) if (r instanceof ResourceCollection)
{ {
Collections.addAll(list, ((ResourceCollection)r).getResources()); _resources.addAll(((ResourceCollection)r).getResources());
} }
else else
{ {
assertResourceValid(r); assertResourceValid(r);
list.add(r); _resources.add(r);
} }
} }
_resources = list.toArray(new Resource[0]); }
/**
* Instantiates a new resource collection.
*
* @param resources the resources to be added to collection
*/
public ResourceCollection(Collection<Resource> resources)
{
_resources = new ArrayList<>();
_resources.addAll(resources);
} }
/** /**
@ -88,14 +98,13 @@ public class ResourceCollection extends Resource
*/ */
public ResourceCollection(String[] resources) public ResourceCollection(String[] resources)
{ {
_resources = new ArrayList<>();
if (resources == null || resources.length == 0) if (resources == null || resources.length == 0)
{ {
_resources = null;
return; return;
} }
ArrayList<Resource> res = new ArrayList<>();
try try
{ {
for (String strResource : resources) for (String strResource : resources)
@ -106,16 +115,13 @@ public class ResourceCollection extends Resource
} }
Resource resource = Resource.newResource(strResource); Resource resource = Resource.newResource(strResource);
assertResourceValid(resource); assertResourceValid(resource);
res.add(resource); _resources.add(resource);
} }
if (res.isEmpty()) if (_resources.isEmpty())
{ {
_resources = null; throw new IllegalArgumentException("resources cannot be empty or null");
return;
} }
_resources = res.toArray(new Resource[0]);
} }
catch (RuntimeException e) catch (RuntimeException e)
{ {
@ -131,22 +137,39 @@ public class ResourceCollection extends Resource
* Instantiates a new resource collection. * Instantiates a new resource collection.
* *
* @param csvResources the string containing comma-separated resource strings * @param csvResources the string containing comma-separated resource strings
* @throws IOException if any listed resource is not valid
*/ */
public ResourceCollection(String csvResources) public ResourceCollection(String csvResources) throws IOException
{ {
setResourcesAsCSV(csvResources); setResources(csvResources);
} }
/** /**
* Retrieves the resource collection's resources. * Retrieves the resource collection's resources.
* *
* @return the resource array * @return the resource collection
*/ */
public Resource[] getResources() public List<Resource> getResources()
{ {
return _resources; return _resources;
} }
/**
* Sets the resource collection's resources.
*
* @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. * Sets the resource collection's resources.
* *
@ -167,71 +190,37 @@ public class ResourceCollection extends Resource
res.add(resource); res.add(resource);
} }
if (res.isEmpty()) setResources(res);
{
_resources = null;
return;
}
_resources = res.toArray(new Resource[0]);
} }
/** /**
* Sets the resources as string of comma-separated values. * Sets the resources as string of comma-separated values.
* This method should be used when configuring jetty-maven-plugin. * This method should be used when configuring jetty-maven-plugin.
* *
* @param csvResources the comma-separated string containing * @param resources the comma-separated string containing
* one or more resource strings. * one or more resource strings.
* @throws IOException if unable resource declared is not valid
* @see Resource#fromList(String, boolean)
*/ */
public void setResourcesAsCSV(String csvResources) public void setResources(String resources) throws IOException
{ {
if (csvResources == null) if (StringUtil.isBlank(resources))
{ {
throw new IllegalArgumentException("CSV String is null"); throw new IllegalArgumentException("String is blank");
} }
StringTokenizer tokenizer = new StringTokenizer(csvResources, ",;"); List<Resource> list = Resource.fromList(resources, false);
int len = tokenizer.countTokens(); if (list.isEmpty())
if (len == 0)
{ {
throw new IllegalArgumentException("ResourceCollection@setResourcesAsCSV(String) " + throw new IllegalArgumentException("String contains no entries");
" argument must be a string containing one or more comma-separated resource strings.");
} }
List<Resource> ret = new ArrayList<>();
List<Resource> res = new ArrayList<>(); for (Resource resource : list)
try
{ {
while (tokenizer.hasMoreTokens()) assertResourceValid(resource);
{ ret.add(resource);
String token = tokenizer.nextToken().trim();
// TODO: If we want to support CSV tokens with spaces then we should not trim here
// However, if we decide to to this, then CVS formatting/syntax becomes more strict.
if (token.length() == 0)
{
continue; // skip
}
Resource resource = Resource.newResource(token);
assertResourceValid(resource);
res.add(resource);
}
if (res.isEmpty())
{
_resources = null;
return;
}
_resources = res.toArray(new Resource[0]);
}
catch (RuntimeException e)
{
throw e;
}
catch (Exception e)
{
throw new RuntimeException(e);
} }
setResources(ret);
} }
/** /**
@ -245,7 +234,7 @@ public class ResourceCollection extends Resource
if (path == null) if (path == null)
{ {
throw new MalformedURLException(); throw new MalformedURLException("null path");
} }
if (path.length() == 0 || URIUtil.SLASH.equals(path)) if (path.length() == 0 || URIUtil.SLASH.equals(path))
@ -253,51 +242,32 @@ public class ResourceCollection extends Resource
return this; return this;
} }
Resource resource = null;
ArrayList<Resource> resources = null; ArrayList<Resource> resources = null;
int i = 0;
for (; i < _resources.length; i++) // Attempt a simple (single) Resource lookup that exists
for (Resource res : _resources)
{ {
resource = _resources[i].addPath(path); Resource r = res.addPath(path);
if (resource.exists()) if (!r.isDirectory() && r.exists())
{ {
if (resource.isDirectory()) // Return simple (non-directory) Resource
{ return r;
break;
}
return resource;
} }
}
for (i++; i < _resources.length; i++) if (resources == null)
{
Resource r = _resources[i].addPath(path);
if (r.exists() && r.isDirectory())
{ {
if (resources == null) resources = new ArrayList<>();
{
resources = new ArrayList<>();
}
if (resource != null)
{
resources.add(resource);
resource = null;
}
resources.add(r);
} }
resources.add(r);
} }
if (resource != null) if (resources.size() == 1)
{ {
return resource; return resources.get(0);
} }
if (resources != null)
{ return new ResourceCollection(resources);
return new ResourceCollection(resources.toArray(new Resource[0]));
}
return null;
} }
@Override @Override
@ -437,9 +407,8 @@ public class ResourceCollection extends Resource
{ {
Collections.addAll(set, r.list()); Collections.addAll(set, r.list());
} }
String[] result = set.toArray(new String[0]);
Arrays.sort(result); return (String[])set.stream().sorted().toArray();
return result;
} }
@Override @Override
@ -465,9 +434,10 @@ public class ResourceCollection extends Resource
{ {
assertResourcesSet(); assertResourcesSet();
for (int r = _resources.length; r-- > 0; ) // Copy in reverse order
for (int r = _resources.size(); r-- > 0; )
{ {
_resources[r].copyTo(destination); _resources.get(r).copyTo(destination);
} }
} }
@ -477,12 +447,12 @@ public class ResourceCollection extends Resource
@Override @Override
public String toString() public String toString()
{ {
if (_resources == null || _resources.length == 0) if (_resources.isEmpty())
{ {
return "[]"; return "[]";
} }
return String.valueOf(Arrays.asList(_resources)); return String.valueOf(_resources);
} }
@Override @Override
@ -494,7 +464,7 @@ public class ResourceCollection extends Resource
private void assertResourcesSet() private void assertResourcesSet()
{ {
if (_resources == null || _resources.length == 0) if (_resources == null || _resources.isEmpty())
{ {
throw new IllegalStateException("*resources* not set."); throw new IllegalStateException("*resources* not set.");
} }

View File

@ -18,17 +18,26 @@
package org.eclipse.jetty.util.resource; package org.eclipse.jetty.util.resource;
import java.io.IOException;
/** /**
* ResourceFactory. * ResourceFactory.
*/ */
public interface ResourceFactory public interface ResourceFactory
{ {
/** /**
* Get a resource for a path. * Get a Resource from a provided String.
* <p>
* The behavior here is dependent on the
* implementation of ResourceFactory.
* The provided path can be resolved
* against a known Resource, or can
* be a from-scratch Resource.
* </p>
* *
* @param path The path to the resource * @param path The path to the resource
* @return The resource or null * @return The resource, that might not actually exist (yet).
* @throws IOException if unable to create Resource
*/ */
Resource getResource(String path); Resource getResource(String path) throws IOException;
} }

View File

@ -27,6 +27,7 @@ import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.util.Objects;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.AutoLock;
@ -287,10 +288,9 @@ public class URLResource extends Resource
*/ */
@Override @Override
public Resource addPath(String path) public Resource addPath(String path)
throws IOException, MalformedURLException throws IOException
{ {
if (path == null) Objects.requireNonNull(path, "Path may not be null");
return null;
path = URIUtil.canonicalPath(path); path = URIUtil.canonicalPath(path);

View File

@ -113,7 +113,7 @@ public class ResourceCollectionTest
} }
@Test @Test
public void testSetResourceNullThrowsISE() public void testSetResourceArrayNullThrowsISE()
{ {
// Create a ResourceCollection with one valid entry // Create a ResourceCollection with one valid entry
Path path = MavenTestingUtils.getTargetPath(); Path path = MavenTestingUtils.getTargetPath();
@ -121,7 +121,7 @@ public class ResourceCollectionTest
ResourceCollection coll = new ResourceCollection(resource); ResourceCollection coll = new ResourceCollection(resource);
// Reset collection to invalid state // Reset collection to invalid state
coll.setResources(null); coll.setResources((Resource[])null);
assertThrowIllegalStateException(coll); assertThrowIllegalStateException(coll);
} }
@ -152,7 +152,7 @@ public class ResourceCollectionTest
assertThrows(IllegalStateException.class, () -> coll.setResources(new Resource[]{null, null, null})); assertThrows(IllegalStateException.class, () -> coll.setResources(new Resource[]{null, null, null}));
// Ensure not modified. // Ensure not modified.
assertThat(coll.getResources().length, is(1)); assertThat(coll.getResources().size(), is(1));
} }
private void assertThrowIllegalStateException(ResourceCollection coll) private void assertThrowIllegalStateException(ResourceCollection coll)

View File

@ -36,13 +36,12 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.PatternMatcher; import org.eclipse.jetty.util.PatternMatcher;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.resource.EmptyResource; import org.eclipse.jetty.util.resource.EmptyResource;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.resource.ResourceCollection;
@ -802,7 +801,7 @@ public class MetaInfConfiguration extends AbstractConfiguration
* *
* @param context the context to find extra classpath jars in * @param context the context to find extra classpath jars in
* @return the list of Resources with the extra classpath, or null if not found * @return the list of Resources with the extra classpath, or null if not found
* @throws Exception if unable to find the extra classpath jars * @throws Exception if unable to resolve the extra classpath jars
*/ */
protected List<Resource> findExtraClasspathJars(WebAppContext context) protected List<Resource> findExtraClasspathJars(WebAppContext context)
throws Exception throws Exception
@ -810,55 +809,10 @@ public class MetaInfConfiguration extends AbstractConfiguration
if (context == null || context.getExtraClasspath() == null) if (context == null || context.getExtraClasspath() == null)
return null; return null;
List<Resource> jarResources = new ArrayList<>(); return context.getExtraClasspath()
StringTokenizer tokenizer = new StringTokenizer(context.getExtraClasspath(), ",;"); .stream()
while (tokenizer.hasMoreTokens()) .filter(this::isFileSupported)
{ .collect(Collectors.toList());
String token = tokenizer.nextToken().trim();
// Is this a Glob Reference?
if (isGlobReference(token))
{
String dir = token.substring(0, token.length() - 2);
// Use directory
Resource dirResource = context.newResource(dir);
if (dirResource.exists() && dirResource.isDirectory())
{
// To obtain the list of files
String[] entries = dirResource.list();
if (entries != null)
{
Arrays.sort(entries);
for (String entry : entries)
{
try
{
Resource fileResource = dirResource.addPath(entry);
if (isFileSupported(fileResource))
{
jarResources.add(fileResource);
}
}
catch (Exception ex)
{
LOG.warn("Error resolving file {}", entry, ex);
}
}
}
}
}
else
{
// Simple reference, add as-is
Resource resource = context.newResource(token);
if (isFileSupported(resource))
{
jarResources.add(resource);
}
}
}
return jarResources;
} }
/** /**
@ -892,7 +846,7 @@ public class MetaInfConfiguration extends AbstractConfiguration
* *
* @param context the context to look for extra classpaths in * @param context the context to look for extra classpaths in
* @return the list of Resources to the extra classpath * @return the list of Resources to the extra classpath
* @throws Exception if unable to find the extra classpaths * @throws Exception if unable to resolve the extra classpath resources
*/ */
protected List<Resource> findExtraClasspathDirs(WebAppContext context) protected List<Resource> findExtraClasspathDirs(WebAppContext context)
throws Exception throws Exception
@ -900,23 +854,10 @@ public class MetaInfConfiguration extends AbstractConfiguration
if (context == null || context.getExtraClasspath() == null) if (context == null || context.getExtraClasspath() == null)
return null; return null;
List<Resource> dirResources = new ArrayList<>(); return context.getExtraClasspath()
StringTokenizer tokenizer = new StringTokenizer(context.getExtraClasspath(), ",;"); .stream()
while (tokenizer.hasMoreTokens()) .filter(Resource::isDirectory)
{ .collect(Collectors.toList());
String token = tokenizer.nextToken().trim();
// ignore glob references, they only refer to lists of jars/zips anyway
if (!isGlobReference(token))
{
Resource resource = context.newResource(token);
if (resource.exists() && resource.isDirectory())
{
dirResources.add(resource);
}
}
}
return dirResources;
} }
private String uriJarPrefix(URI uri, String suffix) private String uriJarPrefix(URI uri, String suffix)
@ -932,13 +873,23 @@ public class MetaInfConfiguration extends AbstractConfiguration
} }
} }
private boolean isGlobReference(String token)
{
return token.endsWith("/*") || token.endsWith("\\*");
}
private boolean isFileSupported(Resource resource) private boolean isFileSupported(Resource resource)
{ {
try
{
if (resource.isDirectory())
return false;
if (resource.getFile() == 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); String filenameLowercase = resource.getName().toLowerCase(Locale.ENGLISH);
int dot = filenameLowercase.lastIndexOf('.'); int dot = filenameLowercase.lastIndexOf('.');
String extension = (dot < 0 ? null : filenameLowercase.substring(dot)); String extension = (dot < 0 ? null : filenameLowercase.substring(dot));

View File

@ -41,6 +41,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.ClassVisibilityChecker; import org.eclipse.jetty.util.ClassVisibilityChecker;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
@ -87,7 +88,6 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
*/ */
public interface Context extends ClassVisibilityChecker public interface Context extends ClassVisibilityChecker
{ {
/** /**
* Convert a URL or path to a Resource. * Convert a URL or path to a Resource.
* The default implementation * The default implementation
@ -112,7 +112,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
*/ */
boolean isParentLoaderPriority(); boolean isParentLoaderPriority();
String getExtraClasspath(); List<Resource> getExtraClasspath();
boolean isServerResource(String name, URL parentUrl); boolean isServerResource(String name, URL parentUrl);
@ -185,7 +185,7 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions"); String extensions = System.getProperty(WebAppClassLoader.class.getName() + ".extensions");
if (extensions != null) if (extensions != null)
{ {
StringTokenizer tokenizer = new StringTokenizer(extensions, ",;"); StringTokenizer tokenizer = new StringTokenizer(extensions, StringUtil.DEFAULT_DELIMS);
while (tokenizer.hasMoreTokens()) while (tokenizer.hasMoreTokens())
{ {
_extensions.add(tokenizer.nextToken().trim()); _extensions.add(tokenizer.nextToken().trim());
@ -193,7 +193,12 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
} }
if (context.getExtraClasspath() != null) if (context.getExtraClasspath() != null)
addClassPath(context.getExtraClasspath()); {
for (Resource resource : context.getExtraClasspath())
{
addClassPath(resource);
}
}
} }
/** /**
@ -235,7 +240,23 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
} }
else else
{ {
addClassPath(resource.toString()); // Resolve file path if possible
File file = resource.getFile();
if (file != null)
{
URL url = resource.getURI().toURL();
addURL(url);
}
else if (resource.isDirectory())
{
addURL(resource.getURI().toURL());
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Check file exists and is not nested jar: {}", resource);
throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: " + resource);
}
} }
} }
@ -251,53 +272,9 @@ public class WebAppClassLoader extends URLClassLoader implements ClassVisibility
if (classPath == null) if (classPath == null)
return; return;
StringTokenizer tokenizer = new StringTokenizer(classPath, ",;"); for (Resource resource : Resource.fromList(classPath, false, _context::newResource))
while (tokenizer.hasMoreTokens())
{ {
String token = tokenizer.nextToken().trim(); addClassPath(resource);
if (token.endsWith("*"))
{
if (token.length() > 1)
{
token = token.substring(0, token.length() - 1);
Resource resource = _context.newResource(token);
if (LOG.isDebugEnabled())
LOG.debug("Glob Path resource={}", resource);
resource = _context.newResource(token);
addJars(resource);
}
return;
}
Resource resource = _context.newResource(token);
if (LOG.isDebugEnabled())
LOG.debug("Path resource={}", resource);
if (resource.isDirectory() && resource instanceof ResourceCollection)
{
addClassPath(resource);
}
else
{
// Resolve file path if possible
File file = resource.getFile();
if (file != null)
{
URL url = resource.getURI().toURL();
addURL(url);
}
else if (resource.isDirectory())
{
addURL(resource.getURI().toURL());
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Check file exists and is not nested jar: {}", resource);
throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: " + resource);
}
}
} }
} }

View File

@ -31,6 +31,7 @@ import java.util.Collections;
import java.util.EventListener; import java.util.EventListener;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -204,7 +205,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
private boolean _persistTmpDir = false; private boolean _persistTmpDir = false;
private String _war; private String _war;
private String _extraClasspath; private List<Resource> _extraClasspath;
private Throwable _unavailableException; private Throwable _unavailableException;
private Map<String, String> _resourceAliases; private Map<String, String> _resourceAliases;
@ -1248,17 +1249,29 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
*/ */
@Override @Override
@ManagedAttribute(value = "extra classpath for context classloader", readonly = true) @ManagedAttribute(value = "extra classpath for context classloader", readonly = true)
public String getExtraClasspath() public List<Resource> getExtraClasspath()
{ {
return _extraClasspath; return _extraClasspath;
} }
/** /**
* Set the Extra ClassPath via delimited String.
* <p>
* This is a convenience method for {@link #setExtraClasspath(List)}
* </p>
*
* @param extraClasspath Comma or semicolon separated path of filenames or URLs * @param extraClasspath Comma or semicolon separated path of filenames or URLs
* pointing to directories or jar files. Directories should end * pointing to directories or jar files. Directories should end
* with '/'. * with '/'.
* @throws IOException if unable to resolve the resources referenced
* @see #setExtraClasspath(List)
*/ */
public void setExtraClasspath(String extraClasspath) public void setExtraClasspath(String extraClasspath) throws IOException
{
setExtraClasspath(Resource.fromList(extraClasspath, false, this::newResource));
}
public void setExtraClasspath(List<Resource> extraClasspath)
{ {
_extraClasspath = extraClasspath; _extraClasspath = extraClasspath;
} }
@ -1450,11 +1463,12 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
// Should we go to the original war? // Should we go to the original war?
if (resource.isDirectory() && resource instanceof ResourceCollection && !WebAppContext.this.isExtractWAR()) if (resource.isDirectory() && resource instanceof ResourceCollection && !WebAppContext.this.isExtractWAR())
{ {
Resource[] resources = ((ResourceCollection)resource).getResources(); List<Resource> resources = ((ResourceCollection)resource).getResources();
for (int i = resources.length; i-- > 0; ) for (int i = resources.size(); i-- > 0; )
{ {
if (resources[i].getName().startsWith("jar:file")) Resource r = resources.get(i);
return resources[i].getURI().toURL(); if (r.getName().startsWith("jar:file"))
return r.getURI().toURL();
} }
} }