FileManager Class

package javaxt.express;
import javaxt.express.utils.DateUtils;
import javaxt.http.servlet.HttpServletRequest;
import javaxt.http.servlet.HttpServletResponse;
import static javaxt.utils.Console.console;
import javaxt.utils.ThreadPool;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import static javaxt.xml.DOM.*;

//******************************************************************************
//**  FileManager
//******************************************************************************
/**
 *   Used to serve up static files (html, javascript, css, images, etc).
 *   Ensures static files are cached properly by browsers by appending
 *   version numbers to css and js files. Also updates js and css resources
 *   referenced inside html files and application-specific xml files. Redirects
 *   requests to the most recent version of each file. Sends 304 responses as
 *   required.
 *
 ******************************************************************************/

public class FileManager {

    private javaxt.io.Directory web;
    private String[] welcomeFiles = new String[]{"index.html", "index.htm", "default.htm"};


  //**************************************************************************
  //** Constructor
  //**************************************************************************
    public FileManager(javaxt.io.Directory web){
        this.web = web;
    }


  //**************************************************************************
  //** getFile
  //**************************************************************************
  /** Returns a file that best matches the given HttpServletRequest. See the
   *  other getFile() method for more information.
   */
    public java.io.File getFile(HttpServletRequest request){

      //Get path from url, excluding servlet path and leading "/" character
        String path = request.getPathInfo();
        if (path!=null) path = path.substring(1);

        return getFile(path);
    }


  //**************************************************************************
  //** getFile
  //**************************************************************************
  /** Returns a file that best matches the given path. If the path represents
   *  a directory, searches for welcome files in the directory (e.g.
   *  "index.html"). Returns null if a file is not found.
   */
    public java.io.File getFile(String path){
        if (path==null) path = "";


      //Construct a list of possible file paths
        ArrayList<String> files = new ArrayList<>();
        files.add(web + path);
        if (path.length()>0 && !path.endsWith("/")) path+="/";
        for (String welcomeFile : welcomeFiles){
            files.add(web + path + welcomeFile);
        }



      //Loop through all the possible file combinations
        for (String str : files){

          //Ensure that the path doesn't have any illegal directives
            str = str.replace("\\", "/");
            if (str.contains("..") || str.contains("/.") ||
                str.toLowerCase().contains("/keystore")){
                continue;
            }



          //Send file if it exists
            java.io.File file = new java.io.File(str);
            if (file.exists() && file.isFile() && !file.isHidden()){
                return file;
            }
        }

        return null;
    }


  //**************************************************************************
  //** sendFile
  //**************************************************************************
  /** Used to send a file to the client.
   */
    public void sendFile(HttpServletRequest request, HttpServletResponse response)
        throws IOException{

      //Get path from url, excluding servlet path and leading "/" character
        String path = request.getPathInfo();
        if (path!=null) path = path.substring(1);

      //Send file
        sendFile(path, request, response);
    }


  //**************************************************************************
  //** sendFile
  //**************************************************************************
  /** Used to send a file to the client.
   */
    public void sendFile(String path, HttpServletRequest request, HttpServletResponse response)
        throws IOException {

        java.io.File file = getFile(path);
        if (file!=null) _sendFile(file, request, response);
        else response.setStatus(404);
    }


  //**************************************************************************
  //** sendFile
  //**************************************************************************
  /** Used to send a file to the client.
   */
    public void sendFile(java.io.File file, HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        if (file.exists() && file.isFile() && !file.isHidden()){
            _sendFile(file, request, response);
        }
        else{
            response.setStatus(404);
        }
    }


  //**************************************************************************
  //** sendFile
  //**************************************************************************
  /** Used to send a file to the client.
   */
    public void sendFile(javaxt.io.File file, HttpServletRequest request, HttpServletResponse response)
        throws IOException {
        this.sendFile(file.toFile(), request, response);
    }


  //**************************************************************************
  //** sendFile
  //**************************************************************************
  /** Used to send a file to the client. Does not check if the file exists or
   *  is valid. Only returns 200 and 3XX responses. It is up to the caller to
   *  pass in a valid file and return errors to the client (e.g. 404).
   */
    private void _sendFile(java.io.File file, HttpServletRequest request, HttpServletResponse response)
        throws IOException {

        String name = file.getName();
        int idx = name.lastIndexOf(".");
        if (idx > -1){
            String ext = name.substring(idx+1).toLowerCase();

            if (ext.equals("js") || ext.equals("css")){


              //Add version number to javascript and css files to ensure
              //proper caching. Otherwise, browsers like Chrome may not
              //return the correct file to the client.
                javaxt.utils.URL url = new javaxt.utils.URL(request.getURL());
                long currVersion = new javaxt.utils.Date(file.lastModified()).toLong();
                long requestedVersion = 0;
                try{ requestedVersion = Long.parseLong(url.getParameter("v")); }
                catch(Exception e){}

                if (requestedVersion < currVersion){
                    url.setParameter("v", currVersion+"");
                    response.sendRedirect(url.toString(), true);
                    return;
                }
                else if (requestedVersion==currVersion){
                    response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
                }

            }
            else if (ext.equals("htm") || ext.equals("html")){


              //Extract html from file
                javaxt.io.File htmlFile = new javaxt.io.File(file);
                String html = htmlFile.getText();



              //Instantiate html parser and get header
                javaxt.html.Parser parser = new javaxt.html.Parser(html);
                javaxt.html.Element head = parser.getElementByTagName("head");
                String header = head.getOuterHTML();



              //Generate XML with scripts and links found in the header
                ArrayList<String> headerNodes = new ArrayList<>();
                HashMap<Integer, Integer> updates = new HashMap<>();
                StringBuilder str = new StringBuilder();
                str.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\r\n");
                str.append("<links>\r\n");
                for (javaxt.html.Element el : head.getChildNodes()){

                    String tagName = el.getName();
                    if (tagName==null) continue;

                    tagName = tagName.toLowerCase();
                    String url = null;
                    if (tagName.equals("script")){
                        url = el.getAttribute("src");
                    }
                    else if (tagName.equals("link")){
                        url = el.getAttribute("href");
                    }

                    if (!(url==null || url.isEmpty())){

                        String t = url.toLowerCase();
                        if (!t.startsWith("http://") && !t.startsWith("https://") && !t.startsWith("//")){

                            str.append("\r\n");
                            str.append(el.toString());
                            if (!el.isClosed()){
                                str.append("</" + el.getName() + ">");
                            }

                            updates.put(updates.size(), headerNodes.size());
                        }

                    }
                    headerNodes.add(el.toString());
                }
                str.append("</links>");
                org.w3c.dom.Document xml = createDocument(str.toString());



              //Update links in the XML
                long lastUpdate;
                try{
                    lastUpdate = updateLinks(htmlFile, xml);
                }
                catch(Exception e){
                    throw new RuntimeException(e);
                }



              //Update header nodes
                Node[] nodes = getNodes(getOuterNode(xml).getChildNodes());

                HashSet<String> simpleNodes = new HashSet<>();
                for (Node node : nodes){
                    String nodeName = node.getNodeName().toLowerCase();
                    if (nodeName.equals("script")) {
                        String url = getAttributeValue(node, "src");
                        if (url.length()==0) continue;
                        simpleNodes.add("script|"+url);
                    }
                    else if (nodeName.equals("link")){
                        String url = getAttributeValue(node, "href");
                        if (url.length()==0) continue;
                        simpleNodes.add("link|"+url);
                    }
                }

                for (int i=0; i<nodes.length; i++){
                    Node node = nodes[i];
                    String nodeName = node.getNodeName().toLowerCase();


                  //Convert node into an HTML string
                    String txt = "";
                    if (nodeName.equals("scripts")){ //wildcard replacements
                        for (Node n : getElementsByTagName("script", node)){

                          //Skip node if a similar node is found in the header
                            String url = getAttributeValue(n, "src");
                            if (simpleNodes.contains("script|"+url)) continue;


                          //Add node
                            txt += updateTag(n) + "\r\n";
                        }
                    }
                    else if (nodeName.equals("links")){ //wildcard replacements
                        for (Node n : getElementsByTagName("link", node)){

                          //Skip node if a similar node is found in the header
                            String url = getAttributeValue(n, "href");
                            if (simpleNodes.contains("link|"+url)) continue;


                          //Add node
                            txt += updateTag(n) + "\r\n";
                        }
                    }
                    else{
                        txt = updateTag(node);
                    }


                  //Replace entry in headerNodes
                    int x = updates.get(i);
                    headerNodes.set(x, txt);
                }



              //Replace header in the html document
                str = new StringBuilder("<head>");
                for (String s : headerNodes){
                    str.append("\r\n");
                    str.append(s);

                }
                str.append("\r\n</head>");
                html = html.replace(header, str.toString());



              //Set content type and send response
                response.setContentType("text/html");
                sendResponse(html, lastUpdate, request, response);
                return;
            }
            else if (ext.equals("xml")){


              //Check whether the xml file is a javaxt-specific file
              //with CSS and JS includes. If so, add version numbers to
              //the js and css files sourced in the xml document.
                javaxt.io.File xmlFile = new javaxt.io.File(file);
                org.w3c.dom.Document xml = xmlFile.getXML();
                String outerNode = getOuterNode(xml).getNodeName();
                if (outerNode.equals("application") || outerNode.equals("includes")){


                  //Update links to scripts and css files
                    long lastUpdate;
                    try{
                        lastUpdate = updateLinks(xmlFile, xml);
                    }
                    catch(Exception e){
                        throw new RuntimeException(e);
                    }


                  //Set content type and send response
                    response.setContentType("application/xml");
                    sendResponse(getText(xml), lastUpdate, request, response);
                    return;
                }

            }
        }


      //Send file
        response.write(file, javaxt.io.File.getContentType(file.getName()), true);

    }


  //**************************************************************************
  //** sendResponse
  //**************************************************************************
  /** Sends a given string to the client. Transparently handles caching using
   *  "ETag" and "Last-Modified" headers.
   *  @param date UTC date in milliseconds since January 1, 1970, 00:00:00 UTC
   */
    public void sendResponse(String html, long date, HttpServletRequest request,
        HttpServletResponse response) throws IOException {


      //Set response headers
        long size = html.length();
        String eTag = "W/\"" + size + "-" + date + "\"";
        response.setHeader("ETag", eTag);
        response.setHeader("Last-Modified", DateUtils.getDate(date)); //Sat, 23 Oct 2010 13:04:28 GMT
        //this.setHeader("Cache-Control", "max-age=315360000");
        //this.setHeader("Expires", "Sun, 30 Sep 2018 16:23:15 GMT  ");



      //Return 304/Not Modified response if we can...
        String matchTag = request.getHeader("if-none-match");
        String cacheControl = request.getHeader("cache-control");
        if (matchTag==null) matchTag = "";
        if (cacheControl==null) cacheControl = "";
        if (cacheControl.equalsIgnoreCase("no-cache")==false){
            if (eTag.equalsIgnoreCase(matchTag)){
                //System.out.println("Sending 304 Response!");
                response.setStatus(304);
                return;
            }
            else{
              //Internet Explorer 6 uses "if-modified-since" instead of "if-none-match"
                matchTag = request.getHeader("if-modified-since");
                if (matchTag!=null){
                    for (String tag: matchTag.split(";")){
                        if (tag.trim().equalsIgnoreCase(response.getHeader("Last-Modified"))){
                            //System.out.println("Sending 304 Response!");
                            response.setStatus(304);
                            return;
                        }
                    }
                }

            }
        }


        response.write(html);
    }


  //**************************************************************************
  //** updateLinks
  //**************************************************************************
  /** Used to update links to scripts and css files by appending a version
   *  number to the urls (?v=12345678). This operation is multi-threaded to
   *  improve performance.
   *  @return Long representing a timestamp associated with the most recent
   *  file update
   */
    public long updateLinks(javaxt.io.File xmlFile, org.w3c.dom.Document xml) throws Exception {

      //Generate list of nodes
        ArrayList<Node> includes = new ArrayList<>();
        for (Node node : getElementsByTagName("script", xml)) includes.add(node);
        for (Node node : getElementsByTagName("link", xml)) includes.add(node);


      //Update links
        long t = updateLinks(xmlFile, includes);



      //Replace nested nodes
        ArrayList<Node> orgNodes = new ArrayList<>();
        for (Node node : getElementsByTagName("script", xml)) orgNodes.add(node);
        for (Node node : getElementsByTagName("link", xml)) orgNodes.add(node);
        for (int i=0; i<includes.size(); i++){
            Node node = includes.get(i);
            Node orgNode = orgNodes.get(i);
            Node parentNode = orgNode.getParentNode();
            String nodeName = node.getNodeName().toLowerCase();
            if (nodeName.equals("scripts") || nodeName.equals("links")){
                node = xml.adoptNode(node);
                parentNode.insertBefore(node, orgNode);
                parentNode.removeChild(orgNode);
            }
        }


        return t;
    }


  //**************************************************************************
  //** updateLinks
  //**************************************************************************
    public long updateLinks(javaxt.io.File xmlFile, ArrayList<Node> nodes) throws Exception {

      //Start building unique list of file dates
        ConcurrentHashMap<Long, Boolean> uniqueDates = new ConcurrentHashMap<>();
        if (xmlFile.exists()) uniqueDates.put(xmlFile.getDate().getTime(), true);


      //Instantiate the ThreadPool
        ThreadPool pool = new ThreadPool(4){
            public void process(Object obj){
                Node node = (Node) obj;
                String nodeName = node.getNodeName().toLowerCase();
                if (nodeName.equals("script")){

                  //Get String src = getAttributeValue(node, "src");link
                    String src = getAttributeValue(node, "src");
                    if (src.length()==0) return;
                    //console.log(src);


                  //Update link
                    try{
                        String path = getPath(src);
                        if (path.contains("*")){ //Special case for wildcard links


                          //Create new xml document
                            StringBuilder str = new StringBuilder();
                            str.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\r\n");
                            str.append("<scripts>\r\n");
                            for (String link : getLinks(src, path)){
                                str.append("<script src=\"");
                                str.append(link);
                                str.append("\"></script>\r\n");
                            }
                            str.append("</scripts>");
                            //console.log(str);


                          //Replace original node with new nodes
                            org.w3c.dom.Document xml = createDocument(str.toString());
                            synchronized(nodes){
                                int idx = nodes.indexOf(node);
                                nodes.set(idx, getOuterNode(xml));
                            }

                        }
                        else{ //Append version number to the path
                            javaxt.io.File jsFile = new javaxt.io.File(path);
                            if (jsFile.exists()){
                                long lastModified = jsFile.getLastModifiedTime().getTime();
                                long currVersion = new javaxt.utils.Date(lastModified).toLong();
                                setAttributeValue(node, "src" , src + "?v=" + currVersion);
                                addDate(lastModified);
                            }
                        }
                    }
                    catch(Exception e){
                        //e.printStackTrace();
                        //System.out.println("Invalid path? " + src);
                    }
                }
                else if (nodeName.equals("link")){

                  //Update links to css files
                    String href = getAttributeValue(node, "href");
                    String type = getAttributeValue(node, "type");
                    String rel = getAttributeValue(node, "rel");
                    boolean isStyleSheet = type.equalsIgnoreCase("text/css");
                    if (!isStyleSheet) isStyleSheet = rel.equalsIgnoreCase("stylesheet");
                    if (href.length()>0 && isStyleSheet){

                        try{
                            String path = getPath(href);
                            if (path.contains("*")){ //Special case for wildcard links


                              //Create new xml document
                                StringBuilder str = new StringBuilder();
                                str.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\r\n");
                                str.append("<links>\r\n");
                                for (String link : getLinks(href, path)){

                                    str.append("<link href=\"");
                                    str.append(link);
                                    str.append("\" rel=\"stylesheet\" />\r\n");

                                }
                                str.append("</links>");
                                //console.log(str);


                              //Replace original node with new nodes
                                org.w3c.dom.Document xml = createDocument(str.toString());
                                synchronized(nodes){
                                    int idx = nodes.indexOf(node);
                                    nodes.set(idx, getOuterNode(xml));
                                }

                            }
                            else{
                                javaxt.io.File cssFile = new javaxt.io.File(path);
                                if (cssFile.exists()){ //Append version number to the path
                                    long lastModified = cssFile.getLastModifiedTime().getTime();
                                    long currVersion = new javaxt.utils.Date(lastModified).toLong();
                                    setAttributeValue(node, "href" , href + "?v=" + currVersion);
                                    addDate(lastModified);
                                }
                            }
                        }
                        catch(Exception e){
                            //e.printStackTrace();
                            //System.out.println("Invalid path? " + href);
                        }
                    }
                }
            }


            private ArrayList<String> getLinks(String src, String path) throws Exception {

              //Get file path
                javaxt.io.File f = new javaxt.io.File(path);
                javaxt.io.Directory d = f.getDirectory();
                String search = f.getName();


              //Build relative path to the files
                String basePath = src.substring(0, src.indexOf("*"));
                int x = d.toString().replace("\\", "/").lastIndexOf(basePath);


              //Create new xml document
                ArrayList<String> links = new ArrayList<>();
                for (javaxt.io.File file : d.getFiles(search, true)){
                    long lastModified = file.getLastModifiedTime().getTime();
                    long currVersion = new javaxt.utils.Date(lastModified).toLong();
                    addDate(lastModified);

                    String p = file.getDirectory().toString().replace("\\", "/").substring(x);
                    links.add(p + file.getName() + "?v=" + currVersion);
                }
                return links;
            }


            private String getPath(String relPath){
                if (relPath.startsWith("/")){
                    relPath = relPath.substring(1);
                    if (relPath.startsWith("/")) throw new RuntimeException();
                    return web + relPath;
                }
                else{
                    return xmlFile.MapPath(relPath);
                }
            }

            private void addDate(long lastModified) throws Exception {
                java.util.HashSet<Long> dates = (java.util.HashSet<Long>) get("dates");
                if (dates==null){
                    dates = new java.util.HashSet<>();
                    set("dates", dates);
                }
                dates.add(lastModified);
            }

            public void exit(){
                java.util.HashSet<Long> dates = (java.util.HashSet<Long>) get("dates");
                if (dates==null || dates.isEmpty()) return;
                synchronized(uniqueDates){
                    java.util.Iterator<Long> it = dates.iterator();
                    while (it.hasNext()){
                        uniqueDates.put(it.next(), true);
                    }
                    uniqueDates.notify();
                }
            }


        }.start();


      //Insert records
        for (Node node : nodes){
            pool.add(node);
        }


      //Notify the pool that we have finished added records and Wait for threads to finish
        pool.done();
        pool.join();


      //Get most recent file date
        java.util.TreeSet<Long> dates = new java.util.TreeSet<>();
        dates.addAll(uniqueDates.keySet());
        return dates.last();
    }


  //**************************************************************************
  //** updateNodes
  //**************************************************************************
  /** Adds empty comment blocks to "childless" nodes to prevent self-enclosing
   *  tags.
   */
    public void updateNodes(NodeList nodes, org.w3c.dom.Document xml){
        for (int i=0; i<nodes.getLength(); i++){
            Node node = nodes.item(i);
            if (node.getNodeType()==1){
                if (hasChildren(node)){
                    updateNodes(node.getChildNodes(), xml);
                }
                else{
                    updateNode(node, xml);
                }
            }
        }
    }


  //**************************************************************************
  //** updateNode
  //**************************************************************************
  /** Adds an empty comment block to a node to prevent self-enclosing tags.
   */
    public void updateNode(Node node, org.w3c.dom.Document xml){
        try{
            node.appendChild(xml.createComment(" "));
        }
        catch(Exception e){
            //System.out.println(node.getNodeName());
        }
    }


  //**************************************************************************
  //** updateTag
  //**************************************************************************
  /** Returns a HTML string for a given node. Replaces any self-enclosing tags
   *  as needed.
   */
    private String updateTag(Node node){
        String txt = getText(node);
        String nodeName = node.getNodeName().toLowerCase();
        if (txt.endsWith("/>") && nodeName.equals("script")){
            txt = txt.substring(0, txt.length()-2);
            txt += "></" + nodeName + ">";
        }
        return txt;
    }

}