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:58 UTC

[tomcat] branch 8.5.x updated (96df339 -> 65cb020)

This is an automated email from the ASF dual-hosted git repository.

schultz pushed a change to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git.


    from 96df339  Use spotbugs instead of findbugs
     new 888b769  Add sorting of file listings.
     new 65cb020  Add documentation, changelog for new DefaultServlet options.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../apache/catalina/servlets/DefaultServlet.java   | 538 ++++++++++++++++++++-
 webapps/docs/changelog.xml                         |   7 +
 webapps/docs/default-servlet.xml                   |   6 +
 3 files changed, 543 insertions(+), 8 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 02/02: Add documentation, changelog for new DefaultServlet options.

Posted by sc...@apache.org.
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 65cb0202b3d746609af1a432f909a662ac2aa171
Author: Christopher Schultz <ch...@christopherschultz.net>
AuthorDate: Sun May 5 15:10:12 2019 +0200

    Add documentation, changelog for new DefaultServlet options.
---
 webapps/docs/changelog.xml       | 7 +++++++
 webapps/docs/default-servlet.xml | 6 ++++++
 2 files changed, 13 insertions(+)

diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index a6792d4..b0f6983 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -45,6 +45,13 @@
   issues do not "pop up" wrt. others).
 -->
 <section name="Tomcat 8.5.42 (markt)" rtext="in development">
+  <subsection name="Catalina">
+    <changelog>
+      <add>
+        <bug>57287</bug>: Add file sorting to DefaultServlet (schultz)
+      </add>
+    </changelog>
+  </subsection>
   <subsection name="Other">
     <changelog>
       <update>Switch from FindBugs to SpotBugs. (fschumacher)</update>
diff --git a/webapps/docs/default-servlet.xml b/webapps/docs/default-servlet.xml
index cc9a1ae..d849618 100644
--- a/webapps/docs/default-servlet.xml
+++ b/webapps/docs/default-servlet.xml
@@ -180,6 +180,12 @@ directory listings are disabled and debugging is turned off.
         Should server information be presented in the response sent to clients
         when directory listing is enabled. [true]
   </property>
+  <property name="sortListings">
+        Should the server sort the listings in a directory. [false]
+  </property>
+  <property name="sortDirectoriesFirst">
+        Should the server list all directories before all files. [false]
+  </property>
 </properties>
 </section>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 01/02: Add sorting of file listings.

Posted by sc...@apache.org.
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