Package Acme.Serve

Source Code of Acme.Serve.FileServlet

// FileServlet - servlet similar to a standard httpd
//
// Copyright (C)1996,1998 by Jef Poskanzer <jef@acme.com>. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/
//
// All enhancements Copyright (C)1998-2005 by Dmitriy Rogatkin
// http://tjws.sourceforge.net

package Acme.Serve;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.zip.GZIPOutputStream; //import java.util.zip.DeflaterOutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import Acme.Utils;

/// Servlet similar to a standard httpd.
// <P>
// Implements the "GET" and "HEAD" methods for files and directories.
// Handles index.html, index.htm, default.htm, default.html.
// Redirects directory URLs that lack a trailing /.
// Handles If-Modified-Since.
// <P>
// <A HREF="/resources/classes/Acme/Serve/FileServlet.java">Fetch the software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.Z">Fetch the entire Acme package.</A>
// <P>
// @see Acme.Serve.Serve

public class FileServlet extends HttpServlet {
  public static final String DEF_USE_COMPRESSION = "tjws.fileservlet.usecompression";

  // We keep a single throttle table for all instances of the servlet.
  // Normally there is only one instance; the exception is subclasses.
  static Acme.WildcardDictionary throttleTab = null;

  static final String[] DEFAULTINDEXPAGES = { "index.html", "index.htm", "default.htm", "default.html" };

  static final DecimalFormat lengthftm = new DecimalFormat("#");

  static final String BYTES_UNIT = "bytes";

  protected String charSet = Serve.UTF8;// "iso-8859-1";

  private static final boolean logenabled = false;

  //   true;

  private Method canExecute, getFreeSpace;

  private boolean useCompression;

  // / Constructor.
  public FileServlet() {
    try {
      canExecute = File.class.getMethod("canExecute", Utils.EMPTY_CLASSES);
    } catch (SecurityException e) {
    } catch (NoSuchMethodException e) {
    }
    try {
      getFreeSpace = File.class.getMethod("getFreeSpace", Utils.EMPTY_CLASSES);
    } catch (SecurityException e) {
    } catch (NoSuchMethodException e) {
    }
    useCompression = System.getProperty(DEF_USE_COMPRESSION) != null;
  }

  // / Constructor with throttling.
  // @param throttles filename containing throttle settings
  // @param charset used for displaying directory page
  // @see ThrottledOutputStream
  public FileServlet(String throttles, String charset) throws IOException {
    this();
    if (charset != null)
      this.charSet = charset;
    readThrottles(throttles);
  }

  private void readThrottles(String throttles) throws IOException {
    Acme.WildcardDictionary newThrottleTab = ThrottledOutputStream.parseThrottleFile(throttles);
    if (throttleTab == null)
      throttleTab = newThrottleTab;
    else {
      // Merge the new one into the old one.
      Enumeration keys = newThrottleTab.keys();
      Enumeration elements = newThrottleTab.elements();
      while (keys.hasMoreElements()) {
        Object key = keys.nextElement();
        Object element = elements.nextElement();
        throttleTab.put(key, element);
      }
    }
  }

  // / Returns a string containing information about the author, version, and
  // copyright of the servlet.
  public String getServletInfo() {
    return "File servlet similar to httpd";
  }

  // / Services a single request from the client.
  // @param req the servlet request
  // @param req the servlet response
  // @exception ServletException when an exception has occurred
  public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
    boolean headOnly;
    if (req.getMethod().equalsIgnoreCase("get") || req.getAttribute("javax.servlet.forward.request_uri") != null
        || req.getAttribute("javax.servlet.include.request_uri") != null)
      headOnly = false;
    else if (req.getMethod().equalsIgnoreCase("head"))
      headOnly = true;
    else {
      res.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Method "+req.getMethod());
      return;
    }
    req.setCharacterEncoding(Serve.UTF8);
    String path = Utils.canonicalizePath(req.getPathInfo());
    log("Canonical path:"+path);
    //res.setBufferSize(Utils.COPY_BUF_SIZE/2);
    dispatchPathname(req, res, headOnly, path);
  }

  private void dispatchPathname(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path)
      throws IOException {
    log("path trans: " + req.getPathTranslated());
    String filename = req.getPathTranslated() != null ? req.getPathTranslated().replace('/', File.separatorChar)
        : "";
    File file = new File(filename);
    log("retrieving '" + filename + "' for path " + path);
    if (file.exists()) {
      if (!file.isDirectory())
        serveFile(req, res, headOnly, path, file);
      else {
        log("showing dir " + file);
        if (redirectDirectory(req, res, path, file) == false)
          showIdexFile(req, res, headOnly, path, filename);
      }
    } else
      res.sendError(HttpServletResponse.SC_NOT_FOUND);
  }

  private void showIdexFile(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path,
      String parent) throws IOException {
    log("showing index in directory " + parent);
    for (int i = 0; i < DEFAULTINDEXPAGES.length; i++) {
      File indexFile = new File(parent, DEFAULTINDEXPAGES[i]);
      if (indexFile.exists()) {
        serveFile(req, res, headOnly, path, indexFile);
        return;
      }
    }
    // index not found
    serveDirectory(req, res, headOnly, path, new File(parent));
  }

  private void serveFile(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, File file)
      throws IOException {
    log("getting " + file);
    if (logenabled) {
      Enumeration enh = req.getHeaderNames();
      while (enh.hasMoreElements()) {
        String hn = (String) enh.nextElement();
        log("hdr:" + hn + ":" + req.getHeader(hn));
      }
    }
    if (!file.canRead()) {
      res.sendError(HttpServletResponse.SC_FORBIDDEN);
      return;
    } else
      // by Niel Markwick
      try {
        file.getCanonicalPath();
      } catch (Exception e) {
        res.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, exception:" + e);
        return;
      }

    // Handle If-Modified-Since.
    res.setStatus(HttpServletResponse.SC_OK);
    long lastMod = file.lastModified();
    long ifModSince = req.getDateHeader("If-Modified-Since");
    if (ifModSince != -1 && ifModSince >= lastMod) {
      res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
      headOnly = true;
    }
    // TODO add processing If-None-Match, If-Unmodified-Since and If-Match
    String contentType = getServletContext().getMimeType(file.getName());
    if (contentType != null)
      res.setContentType(contentType);
    long flen = file.length();
    // check for range
    String range = req.getHeader("Range");
    long sr = 0;
    long er = -1;
    if (range != null) {
      log("Range:" + range);
      if (range.regionMatches(true, 0, BYTES_UNIT, 0, BYTES_UNIT.length())) {
        int i = range.indexOf('-');
        if (i > 0) {
          try {
            sr = Long.parseLong(range.substring(BYTES_UNIT.length() + 1, i));
            if (sr < 0)
              throw new NumberFormatException("Invalid start range value:" + sr);
            try {
              er = Long.parseLong(range.substring(i + 1));
            } catch (NumberFormatException nfe) {
              er = flen - 1;
            }
          } catch (NumberFormatException nfe) {

          }
        } // else invalid range? ignore?
      } // else other units not supported
      log("range values " + sr + " to " + er);
    }
    long clen = er < 0 ? flen : (er - sr + 1);
    res.setDateHeader("Last-modified", lastMod);

    if (er > 0) {
      if (sr > er || er >= flen) {
        // TODO If-Range presence can change behavior
        res.setHeader("Content-Range", BYTES_UNIT + " */" + flen);
        res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        return;
      }
      res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
      res.setHeader("Content-Range", BYTES_UNIT + " " + sr + '-' + er + '/' + flen);
      log("content-range:" + BYTES_UNIT + " " + sr + '-' + er + '/' + flen);
    }
    // String ifRange = req.getHeader("If-Range");
    // res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
    boolean doCompress = false;
    if (useCompression && contentType != null && contentType.startsWith("text")) {
      if (Utils.isGzipAccepted(req.getHeader("Accept-Encoding")) > 0) {
        res.setHeader("Content-Encoding", "gzip");
        doCompress = true;
      }
    }
    if (doCompress == false || headOnly) {
      if (clen < Integer.MAX_VALUE)
        res.setContentLength((int) clen);
      else
        res.setHeader("Content-Length", Long.toString(clen));
    }
    OutputStream out = null;
    InputStream in = null;
    try {
      if (!headOnly) {
        out = doCompress ? new GZIPOutputStream(res.getOutputStream()) : (OutputStream) res.getOutputStream();
        // Check throttle.
        if (throttleTab != null) {
          ThrottleItem throttleItem = (ThrottleItem) throttleTab.get(path);
          if (throttleItem != null) {
            // !!! Need to count for multiple simultaneous fetches.
            out = new ThrottledOutputStream(out, throttleItem.getMaxBps());
          }
        }

        in = new FileInputStream(file);
        while (sr > 0) {
          long sl = in.skip(sr);
          if (sl > 0)
            sr -= sl;
          else {
            res.sendError(HttpServletResponse.SC_CONFLICT, "Conflict");
            // better can be Internal Server Error
            return;
          }
        }
        copyStream(in, out, clen);
        if (doCompress)
          ((GZIPOutputStream) out).finish();
      }
    } finally {
      if (in != null)
        try {
          in.close();
        } catch (IOException ioe) {
        }
      if (out != null) {
        out.flush();
        out.close();
      }
    }
  }

  // / Copy a file from in to out.
  // Sub-classes can override this in order to do filtering of some sort.
  public void copyStream(InputStream in, OutputStream out, long len) throws IOException {
    Acme.Utils.copyStream(in, out, len);
  }

  private void serveDirectory(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path,
      File file) throws IOException {
    log("indexing " + file);
    if (!file.canRead()) {
      res.sendError(HttpServletResponse.SC_FORBIDDEN);
      return;
    }
    res.setStatus(HttpServletResponse.SC_OK);
    res.setContentType("text/html;charset=" + charSet);
    OutputStream out = res.getOutputStream();
    if (!headOnly) {
      String[] names = file.list();
      if (names == null) {
        res.sendError(HttpServletResponse.SC_FORBIDDEN, "Can't access " + req.getRequestURI());
        return;
      }
      PrintStream p = new PrintStream(new BufferedOutputStream(out), false, charSet); // 1.4
      p.println("<HTML><HEAD>");
      p.println("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=" + charSet + "\">");
      p.println("<TITLE>Index of " + path + "</TITLE>");
      p.println("</HEAD><BODY " + Serve.BGCOLOR);
      p.println("><H2>Index of " + path + "</H2>");
      p.println("<PRE>");
      p.println("mode         bytes  last-changed    name");
      p.println("<HR>");
      // TODO consider not case sensetive search
      Arrays.sort(names);
      long total = 0;
      for (int i = 0; i < names.length; ++i) {
        File aFile = new File(file, names[i]);
        String aFileType;
        long aFileLen;
        if (aFile.isDirectory())
          aFileType = "d";
        else if (aFile.isFile())
          aFileType = "-";
        else
          aFileType = "?";
        String aFileRead = (aFile.canRead() ? "r" : "-");
        String aFileWrite = (aFile.canWrite() ? "w" : "-");
        String aFileExe = "-";
        if (canExecute != null)
          try {
            if (((Boolean) canExecute.invoke(aFile, Utils.EMPTY_OBJECTS)).booleanValue())
              aFileExe = "x";
          } catch (IllegalArgumentException e) {
          } catch (IllegalAccessException e) {
          } catch (InvocationTargetException e) {
          }
        String aFileSize = lengthftm.format(aFileLen = aFile.length());
        total += (aFileLen + 1023) / 1024; //
        while (aFileSize.length() < 12)
          aFileSize = " " + aFileSize;
        String aFileDate = Acme.Utils.lsDateStr(new Date(aFile.lastModified()));
        while (aFileDate.length() < 14)
          aFileDate += " ";
        String aFileDirsuf = (aFile.isDirectory() ? "/" : "");
        String aFileSuf = (aFile.isDirectory() ? "/" : "");
        p.println(aFileType + aFileRead + aFileWrite + aFileExe + "  " + aFileSize + "  " + aFileDate + "  "
            + "<A HREF=\"" + URLEncoder.encode(names[i], charSet) /* 1.4 */
            + aFileDirsuf + "\">" + names[i] + aFileSuf + "</A>");
      }
      if (total != 0)
        total += 3;
      p.println("total " + total);
      p.println("</PRE>");
      p.println("<HR>");
      p.print(Serve.Identification.serverIdHtml);
      p.println("</BODY></HTML>");
      p.flush();
    }
    out.close();
  }

  /**
   *
   * @param req
   *            http request
   * @param res
   *            http response
   * @param path
   *            web path
   * @param file
   *            file system path
   * @return true if redirection required and happened
   * @throws IOException
   *             in redirection
   */
  private boolean redirectDirectory(HttpServletRequest req, HttpServletResponse res, String path, File file)
      throws IOException {
    int pl = path.length();
    if (pl > 0 && path.charAt(pl - 1) != '/') {
      // relative redirect
      int sp = path.lastIndexOf('/');
      if (sp < 0)
        path += '/';
      else
        path = path.substring(sp + 1) + '/';
      log("redirecting dir " + path);
      res.sendRedirect(path);
      return true;
    }
    return false;
  }

  public void log(String msg) {
    if (logenabled)
      super.log(msg);
  }
}
TOP

Related Classes of Acme.Serve.FileServlet

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.