You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by sc...@apache.org on 2019/05/05 13:26:59 UTC
[tomcat] 01/02: Add sorting of file listings.
This is an automated email from the ASF dual-hosted git repository.
schultz pushed a commit to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 888b7699fde58de00e839797c7038ee0b10aaff2
Author: Christopher Schultz <ch...@christopherschultz.net>
AuthorDate: Sun May 5 14:30:26 2019 +0200
Add sorting of file listings.
Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=57287
Back-port from trunk.
---
.../apache/catalina/servlets/DefaultServlet.java | 538 ++++++++++++++++++++-
1 file changed, 530 insertions(+), 8 deletions(-)
diff --git a/java/org/apache/catalina/servlets/DefaultServlet.java b/java/org/apache/catalina/servlets/DefaultServlet.java
index 04cf89d..0bb8e86 100644
--- a/java/org/apache/catalina/servlets/DefaultServlet.java
+++ b/java/org/apache/catalina/servlets/DefaultServlet.java
@@ -35,12 +35,14 @@ import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
-
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
@@ -254,6 +256,15 @@ public class DefaultServlet extends HttpServlet {
*/
protected boolean showServerInfo = true;
+ /**
+ * Flag to determine if resources should be sorted.
+ */
+ protected boolean sortListings = false;
+
+ /**
+ * The sorting manager for sorting files and directories.
+ */
+ protected SortManager sortManager;
// --------------------------------------------------------- Public Methods
@@ -326,6 +337,21 @@ public class DefaultServlet extends HttpServlet {
if (getServletConfig().getInitParameter("showServerInfo") != null) {
showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo"));
}
+
+ if (getServletConfig().getInitParameter("sortListings") != null) {
+ sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings"));
+
+ if(sortListings) {
+ boolean sortDirectoriesFirst;
+ if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) {
+ sortDirectoriesFirst = Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst"));
+ } else {
+ sortDirectoriesFirst = false;
+ }
+
+ sortManager = new SortManager(sortDirectoriesFirst);
+ }
+ }
}
private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) {
@@ -975,7 +1001,7 @@ public class DefaultServlet extends HttpServlet {
// Output via a writer so can't use sendfile or write
// content directly.
if (resource.isDirectory()) {
- renderResult = render(getPathPrefix(request), resource, encoding);
+ renderResult = render(request, getPathPrefix(request), resource, encoding);
} else {
renderResult = resource.getInputStream();
}
@@ -983,7 +1009,7 @@ public class DefaultServlet extends HttpServlet {
} else {
// Output is via an InputStream
if (resource.isDirectory()) {
- renderResult = render(getPathPrefix(request), resource, encoding);
+ renderResult = render(request, getPathPrefix(request), resource, encoding);
} else {
if (!checkSendfile(request, response, resource, contentLength, null)) {
// sendfile not possible so check if resource
@@ -1391,15 +1417,47 @@ public class DefaultServlet extends HttpServlet {
return render(contextPath, resource, null);
}
+ /**
+ * Decide which way to render. HTML or XML.
+ *
+ * @param contextPath The path
+ * @param resource The resource
+ *
+ * @return the input stream with the rendered output
+ *
+ * @throws IOException an IO error occurred
+ * @throws ServletException rendering error
+ *
+ * @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
+ */
+ @Deprecated
protected InputStream render(String contextPath, WebResource resource, String encoding)
throws IOException, ServletException {
+ return render(null, contextPath, resource, encoding);
+ }
+
+ /**
+ * Decide which way to render. HTML or XML.
+ *
+ * @param contextPath The path
+ * @param resource The resource
+ * @param encoding The encoding to use to process the readme (if any)
+ *
+ * @return the input stream with the rendered output
+ *
+ * @throws IOException an IO error occurred
+ * @throws ServletException rendering error
+ */
+ protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
+ throws IOException, ServletException {
+
Source xsltSource = findXsltSource(resource);
if (xsltSource == null) {
- return renderHtml(contextPath, resource, encoding);
+ return renderHtml(request, contextPath, resource, encoding);
}
- return renderXml(contextPath, resource, xsltSource, encoding);
+ return renderXml(request, contextPath, resource, xsltSource, encoding);
}
@@ -1417,6 +1475,7 @@ public class DefaultServlet extends HttpServlet {
* @throws ServletException rendering error
*
* @deprecated Unused. Will be removed in Tomcat 9
+ * @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
*/
@Deprecated
protected InputStream renderXml(String contextPath, WebResource resource, Source xsltSource)
@@ -1424,8 +1483,46 @@ public class DefaultServlet extends HttpServlet {
return renderXml(contextPath, resource, xsltSource, null);
}
+ /**
+ * Return an InputStream to an XML representation of the contents this
+ * directory.
+ *
+ * @param contextPath Context path to which our internal paths are relative
+ * @param resource The associated resource
+ * @param xsltSource The XSL stylesheet
+ *
+ * @return the XML data
+ *
+ * @throws IOException an IO error occurred
+ * @throws ServletException rendering error
+ * @deprecated Unused. Will be removed in Tomcat 9
+ * @deprecated Use {@link #render(HttpServletRequest, String, WebResource, String)} instead
+ */
+ @Deprecated
protected InputStream renderXml(String contextPath, WebResource resource, Source xsltSource,
String encoding)
+ throws ServletException, IOException
+ {
+ return renderXml(null, contextPath, resource, xsltSource, encoding);
+ }
+
+ /**
+ * Return an InputStream to an XML representation of the contents this
+ * directory.
+ *
+ * @param request The HttpServletRequest being served
+ * @param contextPath Context path to which our internal paths are relative
+ * @param resource The associated resource
+ * @param xsltSource The XSL stylesheet
+ * @param encoding The encoding to use to process the readme (if any)
+ *
+ * @return the XML data
+ *
+ * @throws IOException an IO error occurred
+ * @throws ServletException rendering error
+ */
+ protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource, Source xsltSource,
+ String encoding)
throws IOException, ServletException {
StringBuilder sb = new StringBuilder();
@@ -1561,8 +1658,40 @@ public class DefaultServlet extends HttpServlet {
return renderHtml(contextPath, resource, null);
}
+ /**
+ * Return an InputStream to an HTML representation of the contents of this
+ * directory.
+ *
+ * @param contextPath Context path to which our internal paths are relative
+ * @param resource The associated resource
+ *
+ * @return the HTML data
+ *
+ * @throws IOException an IO error occurred
+ *
+ * @deprecated Unused. Will be removed in Tomcat 9
+ * @deprecated Use {@link #renderHtml(HttpServletRequest, String, WebResource, String)} instead
+ */
+ @Deprecated
protected InputStream renderHtml(String contextPath, WebResource resource, String encoding)
throws IOException {
+ return renderHtml(null, contextPath, resource, encoding);
+ }
+
+ /**
+ * Return an InputStream to an HTML representation of the contents of this
+ * directory.
+ *
+ * @param contextPath Context path to which our internal paths are relative
+ * @param resource The associated resource
+ * @param encoding The encoding to use to process the readme (if any)
+ *
+ * @return the HTML data
+ *
+ * @throws IOException an IO error occurred
+ */
+ protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
+ throws IOException {
// Prepare a writer to a buffered area
ByteArrayOutputStream stream = new ByteArrayOutputStream();
@@ -1620,19 +1749,52 @@ public class DefaultServlet extends HttpServlet {
sb.append("<table width=\"100%\" cellspacing=\"0\"" +
" cellpadding=\"5\" align=\"center\">\r\n");
+ SortManager.Order order;
+ if(sortListings && null != request)
+ order = sortManager.getOrder(request.getQueryString());
+ else
+ order = null;
// Render the column headings
sb.append("<tr>\r\n");
sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
- sb.append(sm.getString("directory.filename"));
+ if(sortListings && null != request) {
+ sb.append("<a href=\"?C=N;O=");
+ sb.append(getOrderChar(order, 'N'));
+ sb.append("\">");
+ sb.append(sm.getString("directory.filename"));
+ sb.append("</a>");
+ } else {
+ sb.append(sm.getString("directory.filename"));
+ }
sb.append("</strong></font></td>\r\n");
sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
- sb.append(sm.getString("directory.size"));
+ if(sortListings && null != request) {
+ sb.append("<a href=\"?C=S;O=");
+ sb.append(getOrderChar(order, 'S'));
+ sb.append("\">");
+ sb.append(sm.getString("directory.size"));
+ sb.append("</a>");
+ } else {
+ sb.append(sm.getString("directory.size"));
+ }
sb.append("</strong></font></td>\r\n");
sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
- sb.append(sm.getString("directory.lastModified"));
+ if(sortListings && null != request) {
+ sb.append("<a href=\"?C=M;O=");
+ sb.append(getOrderChar(order, 'M'));
+ sb.append("\">");
+ sb.append(sm.getString("directory.lastModified"));
+ sb.append("</a>");
+ } else {
+ sb.append(sm.getString("directory.lastModified"));
+ }
sb.append("</strong></font></td>\r\n");
sb.append("</tr>");
+ if(null != sortManager && null != request) {
+ sortManager.sort(entries, request.getQueryString());
+ }
+
boolean shade = false;
for (String entry : entries) {
if (entry.equalsIgnoreCase("WEB-INF") ||
@@ -2490,4 +2652,364 @@ public class DefaultServlet extends HttpServlet {
name, publicId, baseURI, systemId));
}
}
+
+ /**
+ * Gets the ordering character to be used for a particular column.
+ *
+ * @param order The order that is currently being applied
+ * @param column The column that will be rendered.
+ *
+ * @return Either 'A' or 'D', to indicate "ascending" or "descending" sort
+ * order.
+ */
+ private char getOrderChar(SortManager.Order order, char column) {
+ if(column == order.column) {
+ if(order.ascending) {
+ return 'D';
+ } else {
+ return 'A';
+ }
+ } else {
+ return 'D';
+ }
+ }
+
+ /**
+ * A class encapsulating the sorting of resources.
+ */
+ private static class SortManager
+ {
+ /**
+ * The default sort.
+ */
+ protected Comparator<WebResource> defaultResourceComparator;
+
+ /**
+ * Comparator to use when sorting resources by name.
+ */
+ protected Comparator<WebResource> resourceNameComparator;
+
+ /**
+ * Comparator to use when sorting files by name, ascending (reverse).
+ */
+ protected Comparator<WebResource> resourceNameComparatorAsc;
+
+ /**
+ * Comparator to use when sorting resources by size.
+ */
+ protected Comparator<WebResource> resourceSizeComparator;
+
+ /**
+ * Comparator to use when sorting files by size, ascending (reverse).
+ */
+ protected Comparator<WebResource> resourceSizeComparatorAsc;
+
+ /**
+ * Comparator to use when sorting resources by last-modified date.
+ */
+ protected Comparator<WebResource> resourceLastModifiedComparator;
+
+ /**
+ * Comparator to use when sorting files by last-modified date, ascending (reverse).
+ */
+ protected Comparator<WebResource> resourceLastModifiedComparatorAsc;
+
+ public SortManager(boolean directoriesFirst) {
+ resourceNameComparator = new ResourceNameComparator();
+ resourceNameComparatorAsc = Collections.reverseOrder(resourceNameComparator);
+ resourceSizeComparator = new ResourceSizeComparator(resourceNameComparator);
+ resourceSizeComparatorAsc = Collections.reverseOrder(resourceSizeComparator);
+ resourceLastModifiedComparator = new ResourceLastModifiedDateComparator(resourceNameComparator);
+ resourceLastModifiedComparatorAsc = Collections.reverseOrder(resourceLastModifiedComparator);
+
+ if(directoriesFirst) {
+ resourceNameComparator = new DirsFirstComparator(resourceNameComparator);
+ resourceNameComparatorAsc = new DirsFirstComparator(resourceNameComparatorAsc);
+ resourceSizeComparator = new DirsFirstComparator(resourceSizeComparator);
+ resourceSizeComparatorAsc = new DirsFirstComparator(resourceSizeComparatorAsc);
+ resourceLastModifiedComparator = new DirsFirstComparator(resourceLastModifiedComparator);
+ resourceLastModifiedComparatorAsc = new DirsFirstComparator(resourceLastModifiedComparatorAsc);
+ }
+
+ defaultResourceComparator = resourceNameComparator;
+ }
+
+ /**
+ * Sorts an array of resources according to an ordering string.
+ *
+ * @param resources The array to sort.
+ * @param order The ordering string.
+ *
+ * @see {@link #getOrder(String)}
+ */
+ public void sort(WebResource[] resources, String order) {
+ Comparator<WebResource> comparator = getComparator(order);
+
+ if(null != comparator)
+ Arrays.sort(resources, comparator);
+ }
+
+ public Comparator<WebResource> getComparator(String order) {
+ return getComparator(getOrder(order));
+ }
+
+ public Comparator<WebResource> getComparator(Order order) {
+ if(null == order)
+ return defaultResourceComparator;
+
+ if('N' == order.column) {
+ if(order.ascending) {
+ return resourceNameComparatorAsc;
+ } else {
+ return resourceNameComparator;
+ }
+ }
+
+ if('S' == order.column) {
+ if(order.ascending) {
+ return resourceSizeComparatorAsc;
+ } else {
+ return resourceSizeComparator;
+ }
+ }
+
+ if('M' == order.column) {
+ if(order.ascending) {
+ return resourceLastModifiedComparatorAsc;
+ } else {
+ return resourceLastModifiedComparator;
+ }
+ }
+
+ return defaultResourceComparator;
+ }
+
+ /**
+ * Gets the Order to apply given an ordering-string. This
+ * ordering-string matches a subset of the ordering-strings
+ * supported by
+ * <a href="https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query">Apache httpd</a>.
+ *
+ * @param order The ordering-string provided by the client.
+ *
+ * @return An Order specifying the column and ascending/descending to
+ * be applied to resources.
+ */
+ public Order getOrder(String order) {
+ if(null == order || 0 == order.trim().length())
+ return Order.DEFAULT;
+
+ String[] options = split(order);
+
+ if(0 == options.length)
+ return Order.DEFAULT;
+
+ char column = '\0';
+ boolean ascending = false;
+
+ for(String option : options) {
+ option = option.trim();
+
+ if(2 < option.length()) {
+ char opt = option.charAt(0);
+ if('C' == opt)
+ column = option.charAt(2);
+ else if('O' == opt)
+ ascending = ('A' == option.charAt(2));
+ }
+ }
+
+ if('N' == column) {
+ if(ascending) {
+ return Order.NAME_ASC;
+ } else {
+ return Order.NAME;
+ }
+ }
+
+ if('S' == column) {
+ if(ascending) {
+ return Order.SIZE_ASC;
+ } else {
+ return Order.SIZE;
+ }
+ }
+
+ if('M' == column) {
+ if(ascending) {
+ return Order.LAST_MODIFIED_ASC;
+ } else {
+ return Order.LAST_MODIFIED;
+ }
+ }
+
+ return Order.DEFAULT;
+ }
+
+ /**
+ * Split a string using a semicolon as a delimiter.
+ *
+ * @param s The string to split.
+ *
+ * @return An array of non-empty/null strings which have been
+ * separated by semicolons.
+ */
+ private String[] split(String s) {
+ if(null == s)
+ return new String[0];
+ s = s.trim();
+ int length = s.length();
+ if(0 == length)
+ return new String[0];
+
+ String[] strings;
+
+ int index = 0;
+ int start = 0;
+ int pos = s.indexOf(';');
+
+ if(0 <= pos) {
+ int len = 2;
+ strings = new String[len]; // Usually just 2 options
+
+ while(0 <= pos) {
+ if(len > 9)
+ pos = length; // Maximum of 10 options
+
+ if(index + 1 > len) {
+ // Expand by double
+ String[] old = strings;
+ strings = new String[old.length << 1];
+ System.arraycopy(old, 0, strings, 0, len);
+ len = len << 1;
+ }
+
+ String option = s.substring(start, pos).trim();
+
+ if(0 < option.length()) {
+ strings[index++] = option;
+ }
+
+ start = pos + 1;
+ pos = s.indexOf(';', start);
+ }
+
+ if(start < length) {
+ String option = s.substring(start).trim();
+
+ if(0 < option.length()) {
+ strings[index] = option;
+ } else {
+ index--;
+ }
+ } else {
+ index--;
+ }
+
+ if(len > index) {
+ // Shrink
+ String[] old = strings;
+ strings = new String[index + 1];
+ System.arraycopy(old, 0, strings, 0, index + 1);
+ }
+ } else {
+ strings = new String[] { s };
+ }
+
+ return strings;
+ }
+
+ public static class Order {
+ final char column;
+ final boolean ascending;
+
+ public Order(char column, boolean ascending) {
+ this.column = column;
+ this.ascending = ascending;
+ }
+
+ public static Order NAME = new Order('N', false);
+ public static Order NAME_ASC = new Order('N', true);
+ public static Order SIZE = new Order('S', false);
+ public static Order SIZE_ASC = new Order('S', true);
+ public static Order LAST_MODIFIED = new Order('M', false);
+ public static Order LAST_MODIFIED_ASC = new Order('M', true);
+
+ public static Order DEFAULT = NAME;
+ }
+ }
+
+ private static class DirsFirstComparator
+ implements Comparator<WebResource>
+ {
+ private final Comparator<WebResource> base;
+
+ public DirsFirstComparator(Comparator<WebResource> core) {
+ this.base = core;
+ }
+
+ @Override
+ public int compare(WebResource r1, WebResource r2) {
+ if(r1.isDirectory()) {
+ if(r2.isDirectory()) {
+ return base.compare(r1, r2);
+ } else {
+ return -1; // r1, directory, first
+ }
+ } else if(r2.isDirectory()) {
+ return 1; // r2, directory, first
+ } else {
+ return base.compare(r1, r2);
+ }
+ }
+ }
+
+ private static class ResourceNameComparator
+ implements Comparator<WebResource>
+ {
+ @Override
+ public int compare(WebResource r1, WebResource r2) {
+ return r1.getName().compareTo(r2.getName());
+ }
+ }
+
+ private static class ResourceSizeComparator
+ implements Comparator<WebResource>
+ {
+ private Comparator<WebResource> base;
+
+ public ResourceSizeComparator(Comparator<WebResource> base) {
+ this.base = base;
+ }
+
+ @Override
+ public int compare(WebResource r1, WebResource r2) {
+ int c = Long.compare(r1.getContentLength(), r2.getContentLength());
+
+ if(0 == c)
+ return base.compare(r1, r2);
+ else
+ return c;
+ }
+ }
+
+ private static class ResourceLastModifiedDateComparator
+ implements Comparator<WebResource>
+ {
+ private Comparator<WebResource> base;
+
+ public ResourceLastModifiedDateComparator(Comparator<WebResource> base) {
+ this.base = base;
+ }
+
+ @Override
+ public int compare(WebResource r1, WebResource r2) {
+ int c = Long.compare(r1.getLastModified(), r2.getLastModified());
+
+ if(0 == c)
+ return base.compare(r1, r2);
+ else
+ return c;
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org