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