org.eclipse.jetty.util.resource.Resource Maven / Gradle / Ivy
Show all versions of jetty-util Show documentation
//
// ========================================================================
// 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.util.resource;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Abstract resource class.
*
* This class provides a resource abstraction, where a resource may be
* a file, a URL or an entry in a jar file.
*
*/
public abstract class Resource implements ResourceFactory, Closeable
{
private static final Logger LOG = LoggerFactory.getLogger(Resource.class);
public static boolean __defaultUseCaches = true;
volatile Object _associate;
/**
* Change the default setting for url connection caches.
* Subsequent URLConnections will use this default.
*
* @param useCaches true to enable URL connection caches, false otherwise.
*/
public static void setDefaultUseCaches(boolean useCaches)
{
__defaultUseCaches = useCaches;
}
public static boolean getDefaultUseCaches()
{
return __defaultUseCaches;
}
/**
* Attempt to resolve the real path of a Resource to potentially remove any symlinks causing the Resource to be an alias.
* @param resource the resource to resolve.
* @return a new Resource resolved to the real path of the original Resource, or the original resource if it was not an alias.
*/
public static Resource resolveAlias(Resource resource)
{
if (!resource.isAlias())
return resource;
try
{
File file = resource.getFile();
if (file != null)
return Resource.newResource(file.toPath().toRealPath());
}
catch (IOException e)
{
if (LOG.isDebugEnabled())
LOG.debug("resolve alias failed", e);
}
return resource;
}
/**
* Construct a resource from a uri.
*
* @param uri A URI.
* @return A Resource object.
* @throws MalformedURLException Problem accessing URI
*/
public static Resource newResource(URI uri)
throws MalformedURLException
{
return newResource(uri.toURL());
}
/**
* Construct a resource from a url.
*
* @param url A URL.
* @return A Resource object.
*/
public static Resource newResource(URL url)
{
return newResource(url, __defaultUseCaches);
}
/**
* Construct a resource from a url.
*
* @param url the url for which to make the resource
* @param useCaches true enables URLConnection caching if applicable to the type of resource
* @return a new resource from the given URL
*/
static Resource newResource(URL url, boolean useCaches)
{
if (url == null)
return null;
String urlString = url.toExternalForm();
if (urlString.startsWith("file:"))
{
try
{
return new PathResource(url);
}
catch (Exception e)
{
if (LOG.isDebugEnabled())
LOG.warn("Bad PathResource: {}", url, e);
else
LOG.warn("Bad PathResource: {} {}", url, e.toString());
return new BadResource(url, e.toString());
}
}
else if (urlString.startsWith("jar:file:"))
{
return new JarFileResource(url, useCaches);
}
else if (urlString.startsWith("jar:"))
{
return new JarResource(url, useCaches);
}
return new URLResource(url, null, useCaches);
}
/**
* Construct a resource from a string.
*
* @param resource A URL or filename.
* @return A Resource object.
* @throws MalformedURLException Problem accessing URI
*/
public static Resource newResource(String resource) throws IOException
{
return newResource(resource, __defaultUseCaches);
}
/**
* Construct a resource from a string.
*
* @param resource A URL or filename.
* @param useCaches controls URLConnection caching
* @return A Resource object.
* @throws MalformedURLException Problem accessing URI
*/
public static Resource newResource(String resource, boolean useCaches) throws IOException
{
URL url;
try
{
// Try to format as a URL?
url = new URL(resource);
}
catch (MalformedURLException e)
{
if (!resource.startsWith("ftp:") &&
!resource.startsWith("file:") &&
!resource.startsWith("jar:"))
{
// It's likely a file/path reference.
return new PathResource(Paths.get(resource));
}
else
{
LOG.warn("Bad Resource: {}", resource);
throw e;
}
}
return newResource(url, useCaches);
}
public static Resource newResource(File file)
{
return new PathResource(file.toPath());
}
/**
* Construct a Resource from provided path
*
* @param path the path
* @return the Resource for the provided path
* @since 9.4.10
*/
public static Resource newResource(Path path)
{
return new PathResource(path);
}
/**
* Construct a system resource from a string.
* The resource is tried as classloader resource before being
* treated as a normal resource.
*
* @param resource Resource as string representation
* @return The new Resource
* @throws IOException Problem accessing resource.
*/
public static Resource newSystemResource(String resource) throws IOException
{
URL url = null;
// Try to format as a URL?
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader != null)
{
try
{
url = loader.getResource(resource);
if (url == null && resource.startsWith("/"))
url = loader.getResource(resource.substring(1));
}
catch (IllegalArgumentException e)
{
LOG.trace("IGNORED", e);
// Catches scenario where a bad Windows path like "C:\dev" is
// improperly escaped, which various downstream classloaders
// tend to have a problem with
url = null;
}
}
if (url == null)
{
loader = Resource.class.getClassLoader();
if (loader != null)
{
url = loader.getResource(resource);
if (url == null && resource.startsWith("/"))
url = loader.getResource(resource.substring(1));
}
}
if (url == null)
{
url = ClassLoader.getSystemResource(resource);
if (url == null && resource.startsWith("/"))
url = ClassLoader.getSystemResource(resource.substring(1));
}
if (url == null)
return null;
return newResource(url);
}
/**
* Find a classpath resource.
*
* @param resource the relative name of the resource
* @return Resource or null
*/
public static Resource newClassPathResource(String resource)
{
return newClassPathResource(resource, true, false);
}
/**
* Find a classpath resource.
* The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
* found, then the {@link Loader#getResource(String)} method is used.
* If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
* Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
*
* @param name The relative name of the resource
* @param useCaches True if URL caches are to be used.
* @param checkParents True if forced searching of parent Classloaders is performed to work around
* loaders with inverted priorities
* @return Resource or null
*/
public static Resource newClassPathResource(String name, boolean useCaches, boolean checkParents)
{
URL url = Resource.class.getResource(name);
if (url == null)
url = Loader.getResource(name);
if (url == null)
return null;
return newResource(url, useCaches);
}
public static boolean isContainedIn(Resource r, Resource containingResource) throws MalformedURLException
{
return r.isContainedIn(containingResource);
}
public abstract boolean isContainedIn(Resource r) throws MalformedURLException;
/**
* Return true if the passed Resource represents the same resource as the Resource.
* For many resource types, this is equivalent to {@link #equals(Object)}, however
* for resources types that support aliasing, this maybe some other check (e.g. {@link java.nio.file.Files#isSameFile(Path, Path)}).
* @param resource The resource to check
* @return true if the passed resource represents the same resource.
*/
public boolean isSame(Resource resource)
{
return equals(resource);
}
/**
* Release any temporary resources held by the resource.
*/
@Override
public abstract void close();
/**
* @return true if the represented resource exists.
*/
public abstract boolean exists();
/**
* @return true if the represented resource is a container/directory.
*/
public abstract boolean isDirectory();
/**
* Time resource was last modified.
*
* @return the last modified time as milliseconds since unix epoch
*/
public abstract long lastModified();
/**
* Length of the resource.
*
* @return the length of the resource
*/
public abstract long length();
/**
* URI representing the resource.
*
* @return an URI representing the given resource
*/
public abstract URI getURI();
/**
* File representing the given resource.
*
* @return an File representing the given resource or NULL if this
* is not possible.
* @throws IOException if unable to get the resource due to permissions
*/
public abstract File getFile()
throws IOException;
/**
* The name of the resource.
*
* @return the name of the resource
*/
public abstract String getName();
/**
* Input stream to the resource
*
* @return an input stream to the resource
* @throws IOException if unable to open the input stream
*/
public abstract InputStream getInputStream()
throws IOException;
/**
* Readable ByteChannel for the resource.
*
* @return an readable bytechannel to the resource or null if one is not available.
* @throws IOException if unable to open the readable bytechannel for the resource.
*/
public abstract ReadableByteChannel getReadableByteChannel()
throws IOException;
/**
* Deletes the given resource
*
* @return true if resource was found and successfully deleted, false if resource didn't exist or was unable to
* be deleted.
* @throws SecurityException if unable to delete due to permissions
*/
public abstract boolean delete()
throws SecurityException;
/**
* Rename the given resource
*
* @param dest the destination name for the resource
* @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed.
* @throws SecurityException if unable to rename due to permissions
*/
public abstract boolean renameTo(Resource dest)
throws SecurityException;
/**
* list of resource names contained in the given resource.
* Ordering is unspecified, so callers may wish to sort the return value to ensure deterministic behavior.
*
* @return a list of resource names contained in the given resource, or null.
* Note: The resource names are not URL encoded.
*/
public abstract String[] list();
/**
* Returns the resource contained inside the current resource with the
* given name, which may or may not exist.
*
* @param path The path segment to add, which is not encoded. The path may be non canonical, but if so then
* the resulting Resource will return true from {@link #isAlias()}.
* @return the Resource for the resolved path within this Resource, never null
* @throws IOException if unable to resolve the path
* @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed, or
* a relative path attempts to access above the root resource.
*/
public abstract Resource addPath(String path)
throws IOException, MalformedURLException;
/**
* Get a resource from within this resource.
*/
@Override
public Resource getResource(String path) throws IOException
{
return addPath(path);
}
// FIXME: this appears to not be used
@SuppressWarnings("javadoc")
public Object getAssociate()
{
return _associate;
}
// FIXME: this appear to not be used
@SuppressWarnings("javadoc")
public void setAssociate(Object o)
{
_associate = o;
}
/**
* @return true if this Resource is an alias to another real Resource
*/
public boolean isAlias()
{
return getAlias() != null;
}
/**
* @return The canonical Alias of this resource or null if none.
*/
public URI getAlias()
{
return null;
}
/**
* Get the resource list as a HTML directory listing.
*
* @param base The base URL
* @param parent True if the parent directory should be included
* @param query query params
* @return String of HTML
* @throws IOException on failure to generate a list.
*/
public String getListHTML(String base, boolean parent, String query) throws IOException
{
// This method doesn't check aliases, so it is OK to canonicalize here.
base = URIUtil.canonicalPath(base);
if (base == null || !isDirectory())
return null;
String[] rawListing = list();
if (rawListing == null)
{
return null;
}
boolean sortOrderAscending = true;
String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size)
// check for query
if (query != null)
{
MultiMap params = new MultiMap<>();
UrlEncoded.decodeUtf8To(query, 0, query.length(), params);
String paramO = params.getString("O");
String paramC = params.getString("C");
if (StringUtil.isNotBlank(paramO))
{
if (paramO.equals("A"))
{
sortOrderAscending = true;
}
else if (paramO.equals("D"))
{
sortOrderAscending = false;
}
}
if (StringUtil.isNotBlank(paramC))
{
if (paramC.equals("N") || paramC.equals("M") || paramC.equals("S"))
{
sortColumn = paramC;
}
}
}
// Gather up entries
List items = new ArrayList<>();
for (String l : rawListing)
{
Resource item = addPath(l);
items.add(item);
}
// Perform sort
if (sortColumn.equals("M"))
{
items.sort(ResourceCollators.byLastModified(sortOrderAscending));
}
else if (sortColumn.equals("S"))
{
items.sort(ResourceCollators.bySize(sortOrderAscending));
}
else
{
items.sort(ResourceCollators.byName(sortOrderAscending));
}
String decodedBase = URIUtil.decodePath(base);
String title = "Directory: " + deTag(decodedBase);
StringBuilder buf = new StringBuilder(4096);
// Doctype Declaration (HTML5)
buf.append("\n");
buf.append("\n");
// HTML Header
buf.append("\n");
buf.append("\n");
buf.append("\n");
buf.append("");
buf.append(title);
buf.append(" \n");
buf.append("\n");
// HTML Body
buf.append("\n");
buf.append("").append(title).append("
\n");
// HTML Table
final String ARROW_DOWN = " ⇩";
final String ARROW_UP = " ⇧";
buf.append("\n");
buf.append("\n");
String arrow = "";
String order = "A";
if (sortColumn.equals("N"))
{
if (sortOrderAscending)
{
order = "D";
arrow = ARROW_UP;
}
else
{
order = "A";
arrow = ARROW_DOWN;
}
}
buf.append("");
buf.append("Name").append(arrow);
buf.append(" ");
arrow = "";
order = "A";
if (sortColumn.equals("M"))
{
if (sortOrderAscending)
{
order = "D";
arrow = ARROW_UP;
}
else
{
order = "A";
arrow = ARROW_DOWN;
}
}
buf.append("");
buf.append("Last Modified").append(arrow);
buf.append(" ");
arrow = "";
order = "A";
if (sortColumn.equals("S"))
{
if (sortOrderAscending)
{
order = "D";
arrow = ARROW_UP;
}
else
{
order = "A";
arrow = ARROW_DOWN;
}
}
buf.append("");
buf.append("Size").append(arrow);
buf.append(" \n");
buf.append("\n");
buf.append("\n");
String encodedBase = hrefEncodeURI(base);
if (parent)
{
// Name
buf.append("Parent Directory ");
// Last Modified
buf.append("- ");
// Size
buf.append("- ");
buf.append(" \n");
}
DateFormat dfmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
DateFormat.MEDIUM);
for (Resource item : items)
{
String name = item.getFileName();
if (StringUtil.isBlank(name))
{
continue; // skip
}
if (item.isDirectory() && !name.endsWith("/"))
{
name += URIUtil.SLASH;
}
// Name
buf.append("");
buf.append(deTag(name));
buf.append(" ");
buf.append(" ");
// Last Modified
buf.append("");
long lastModified = item.lastModified();
if (lastModified > 0)
{
buf.append(dfmt.format(new Date(item.lastModified())));
}
buf.append(" ");
// Size
buf.append("");
long length = item.length();
if (length >= 0)
{
buf.append(String.format("%,d bytes", item.length()));
}
buf.append(" \n");
}
buf.append("\n");
buf.append("
\n");
buf.append("\n");
return buf.toString();
}
/**
* Get the raw (decoded if possible) Filename for this Resource.
* This is the last segment of the path.
*
* @return the raw / decoded filename for this resource
*/
private String getFileName()
{
try
{
// if a Resource supports File
File file = getFile();
if (file != null)
{
return file.getName();
}
}
catch (Throwable ignored)
{
}
// All others use raw getName
try
{
String rawName = getName(); // gets long name "/foo/bar/xxx"
int idx = rawName.lastIndexOf('/');
if (idx == rawName.length() - 1)
{
// hit a tail slash, aka a name for a directory "/foo/bar/"
idx = rawName.lastIndexOf('/', idx - 1);
}
String encodedFileName;
if (idx >= 0)
{
encodedFileName = rawName.substring(idx + 1);
}
else
{
encodedFileName = rawName; // entire name
}
return UrlEncoded.decodeString(encodedFileName, 0, encodedFileName.length(), UTF_8);
}
catch (Throwable ignored)
{
}
return null;
}
/**
* Encode any characters that could break the URI string in an HREF.
* Such as ">Link
*
* The above example would parse incorrectly on various browsers as the "<" or '"' characters
* would end the href attribute value string prematurely.
*
* @param raw the raw text to encode.
* @return the defanged text.
*/
private static String hrefEncodeURI(String raw)
{
StringBuffer buf = null;
loop:
for (int i = 0; i < raw.length(); i++)
{
char c = raw.charAt(i);
switch (c)
{
case '\'':
case '"':
case '<':
case '>':
buf = new StringBuffer(raw.length() << 1);
break loop;
default:
break;
}
}
if (buf == null)
return raw;
for (int i = 0; i < raw.length(); i++)
{
char c = raw.charAt(i);
switch (c)
{
case '"':
buf.append("%22");
break;
case '\'':
buf.append("%27");
break;
case '<':
buf.append("%3C");
break;
case '>':
buf.append("%3E");
break;
default:
buf.append(c);
break;
}
}
return buf.toString();
}
private static String deTag(String raw)
{
return StringUtil.sanitizeXmlString(raw);
}
/**
* Copy the Resource to the new destination file.
*
* Will not replace existing destination file.
*
* @param destination the destination file to create
* @throws IOException if unable to copy the resource
*/
public void copyTo(File destination)
throws IOException
{
if (destination.exists())
throw new IllegalArgumentException(destination + " exists");
// attempt simple file copy
File src = getFile();
if (src != null)
{
Files.copy(src.toPath(), destination.toPath(),
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
return;
}
// use old school stream based copy
try (InputStream in = getInputStream();
OutputStream out = new FileOutputStream(destination))
{
IO.copy(in, out);
}
}
/**
* Generate a weak ETag reference for this Resource.
*
* @return the weak ETag reference for this resource.
*/
public String getWeakETag()
{
return getWeakETag("");
}
public String getWeakETag(String suffix)
{
StringBuilder b = new StringBuilder(32);
b.append("W/\"");
String name = getName();
int length = name.length();
long lhash = 0;
for (int i = 0; i < length; i++)
{
lhash = 31 * lhash + name.charAt(i);
}
Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
b.append(encoder.encodeToString(longToBytes(lastModified() ^ lhash)));
b.append(encoder.encodeToString(longToBytes(length() ^ lhash)));
b.append(suffix);
b.append('"');
return b.toString();
}
private static byte[] longToBytes(long value)
{
byte[] result = new byte[Long.BYTES];
for (int i = Long.BYTES - 1; i >= 0; i--)
{
result[i] = (byte)(value & 0xFF);
value >>= 8;
}
return result;
}
public Collection getAllResources()
{
try
{
ArrayList deep = new ArrayList<>();
{
String[] list = list();
if (list != null)
{
for (String i : list)
{
Resource r = addPath(i);
if (r.isDirectory())
deep.addAll(r.getAllResources());
else
deep.add(r);
}
}
}
return deep;
}
catch (Exception e)
{
throw new IllegalStateException(e);
}
}
/**
* Generate a properly encoded URL from a {@link File} instance.
*
* @param file Target file.
* @return URL of the target file.
* @throws MalformedURLException if unable to convert File to URL
*/
public static URL toURL(File file) throws MalformedURLException
{
return file.toURI().toURL();
}
/**
* Parse a list of String delimited resources and
* return the List of Resources instances it represents.
*
* 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.
*
*
* @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 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.
*
* 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.
*
*
* @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 fromList(String resources, boolean globDirs, ResourceFactory resourceFactory) throws IOException
{
if (StringUtil.isBlank(resources))
{
return Collections.emptyList();
}
List 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;
}
}