org.eclipse.jetty.server.ResourceListing Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-server Show documentation
Show all versions of jetty-server Show documentation
The core jetty server artifact.
//
// ========================================================================
// 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.server;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollators;
import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility methods to generate a List of paths.
*
* TODO: add XML and JSON versions?
*/
public class ResourceListing
{
public static final Logger LOG = LoggerFactory.getLogger(ResourceListing.class);
/**
* Convert the Resource directory into an XHTML directory listing.
*
* @param resource the resource to build the listing from
* @param base The base URL
* @param parent True if the parent directory should be included
* @param query query params
* @return the XHTML as String
*/
public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
{
// This method doesn't check aliases, so it is OK to canonicalize here.
base = URIUtil.normalizePath(base);
if (base == null)
return null;
if (!Resources.isReadableDirectory(resource))
return null;
List listing = resource.list().stream()
.filter(distinctBy(Resource::getFileName))
.collect(Collectors.toCollection(ArrayList::new));
boolean sortOrderAscending = true;
String sortColumn = "N"; // name (or "M" for Last Modified, or "S" for Size)
// check for query
if (query != null)
{
Fields params = new Fields();
UrlEncoded.decodeUtf8To(query, 0, query.length(), params);
String paramO = params.getValue("O");
String paramC = params.getValue("C");
if (StringUtil.isNotBlank(paramO))
{
switch (paramO)
{
case "A" -> sortOrderAscending = true;
case "D" -> sortOrderAscending = false;
}
}
if (StringUtil.isNotBlank(paramC))
{
if (paramC.equals("N") || paramC.equals("M") || paramC.equals("S"))
{
sortColumn = paramC;
}
}
}
// Perform sort
Comparator super Resource> sort = switch (sortColumn)
{
case "M" -> ResourceCollators.byLastModified(sortOrderAscending);
case "S" -> ResourceCollators.bySize(sortOrderAscending);
default -> ResourceCollators.byFileName(sortOrderAscending);
};
listing.sort(sort);
String decodedBase = URIUtil.decodePath(base);
String title = "Directory: " + deTag(decodedBase);
StringBuilder buf = new StringBuilder(4096);
// Doctype Declaration + XHTML. The spec says the encoding MUST be "utf-8" in all cases at it is ignored;
// see: https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-charset
buf.append("""
""");
// HTML Header
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(" path, investigate if we can use relative links reliably now
buf.append(URIUtil.addPaths(encodedBase, "../"));
buf.append("\">Parent Directory ");
// Last Modified
buf.append("- ");
// Size
buf.append("- ");
buf.append(" \n");
}
// TODO: Use Locale and/or ZoneId from Request?
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM)
.withZone(ZoneId.systemDefault());
for (Resource item : listing)
{
// Listings always return non-composite Resource entries
String name = item.getFileName();
if (StringUtil.isBlank(name))
continue; // a resource either not backed by a filename (eg: MemoryResource), or has no filename (eg: a segment-less root "/")
// Ensure name has a slash if it's a directory
if (item.isDirectory() && !name.endsWith("/"))
name += "/";
// Name
buf.append("");
buf.append(deTag(name));
buf.append(" ");
// Last Modified
buf.append("");
Instant lastModified = item.lastModified();
buf.append(formatter.format(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();
}
/* TODO: see if we can use {@link Collectors#groupingBy} */
private static Predicate distinctBy(Function super T, Object> keyExtractor)
{
HashSet