Exemplo n.º 1
0
/**
 * A wrapper around a {@link HttpServletResponse} which attempts to detect the type of output
 * acquired from the servlet chain and apply a stylesheet to it if all conditions mentioned in
 * {@link XSLTFilter} are met.
 */
final class XSLTFilterServletResponse extends HttpServletResponseWrapper {
  private static final Logger log =
      org.slf4j.LoggerFactory.getLogger(XSLTFilterServletResponse.class);

  /**
   * If true, the stream will be passed verbatim to the next filter. This usually happens when the
   * output has a mime type different than <code>text/xml</code>.
   */
  private boolean passthrough;

  /** The actual {@link HttpServletResponse}. */
  private HttpServletResponse origResponse = null;

  /** The actual {@link HttpServletRequest}. */
  private HttpServletRequest origRequest;

  /**
   * The {@link ServletOutputStream} returned from {@link #getOutputStream()} or <code>null</code>.
   */
  private ServletOutputStream stream = null;

  /** The {@link PrintWriter} returned frmo {@link #getWriter()} or <code>null</code>. */
  private PrintWriter writer = null;

  /** A pool of stylesheets used for XSLT processing. */
  private TemplatesPool transformers;

  /** Servlet context for resolving local paths. */
  private ServletContext context;

  /**
   * Creates an XSLT filter servlet response for a single request, wrapping a given {@link
   * HttpServletResponse}.
   *
   * @param response The original chain's {@link HttpServletResponse}.
   * @param request The original chain's {@link HttpServletRequest}.
   * @param transformers A pool of transformers to be used with this request.
   */
  public XSLTFilterServletResponse(
      HttpServletResponse response,
      HttpServletRequest request,
      ServletContext context,
      TemplatesPool transformers) {
    super(response);

    this.origResponse = response;
    this.transformers = transformers;
    this.origRequest = request;
    this.context = context;
  }

  /** We override this method to detect XML data streams. */
  public void setContentType(String contentType) {
    // Check if XSLT processing has been suppressed for this request.
    final boolean processingSuppressed = processingSuppressed(origRequest);

    if (processingSuppressed) {
      // Processing is suppressed.
      log.debug("XSLT processing disabled for the request.");
    }

    if (!processingSuppressed
        && (contentType.startsWith("text/xml") || contentType.startsWith("application/xml"))) {
      /*
       * We have an XML data stream. Set the real response to proper content type.
       * TODO: Should we make the encoding a configurable setting?
       */
      origResponse.setContentType("text/html; charset=UTF-8");
    } else {
      /*
       * The input is something we won't process anyway, so simply passthrough all
       * data directly to the output stream.
       */
      if (!processingSuppressed) {
        log.info(
            "Content type is not text/xml or application/xml (" + contentType + "), passthrough.");
      }

      origResponse.setContentType(contentType);
      passthrough = true;

      // If the output stream is already initialized, passthrough everything.
      if (stream != null && stream instanceof DeferredOutputStream) {
        try {
          ((DeferredOutputStream) stream).passthrough(origResponse.getOutputStream());
        } catch (IOException e) {
          ((DeferredOutputStream) stream).setExceptionOnNext(e);
        }
      }
    }
  }

  /**
   * Return <code>true</code> if the original request contained XSLT suppressing key.
   *
   * @see XSLTFilterConstants#NO_XSLT_PROCESSING
   */
  private boolean processingSuppressed(HttpServletRequest origRequest2) {
    return (origRequest.getAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING) != null)
        | (origRequest.getParameter(XSLTFilterConstants.NO_XSLT_PROCESSING) != null);
  }

  /** We do not delegate content length because it will most likely change. */
  public void setContentLength(final int length) {
    log.debug("Original content length (ignored): " + length);
  }

  /** Flush the internal buffers. This only works if XSLT transformation is suppressed. */
  public void flushBuffer() throws IOException {
    this.stream.flush();
  }

  /**
   * Return the byte output stream for this response. This is either the original stream or a
   * buffered stream.
   *
   * @exception IllegalStateException Thrown when character stream has been already initialized
   *     ({@link #getWriter()}).
   */
  public ServletOutputStream getOutputStream() throws IOException {
    if (writer != null) {
      throw new IllegalStateException(
          "Character stream has been already initialized. Use streams consequently.");
    }

    if (stream != null) {
      return stream;
    }

    if (passthrough) {
      stream = origResponse.getOutputStream();
    } else {
      stream = new DeferredOutputStream();
    }

    return stream;
  }

  /**
   * Return the character output stream for this response. This is either the original stream or a
   * buffered stream.
   *
   * @exception IllegalStateException Thrown when byte stream has been already initialized ({@link
   *     #getOutputStream()}).
   */
  public PrintWriter getWriter() throws IOException {
    if (stream != null) {
      throw new IllegalStateException(
          "Byte stream has been already initialized. Use streams consequently.");
    }

    if (writer != null) {
      return writer;
    }

    if (passthrough) {
      writer = this.origResponse.getWriter();
      return writer;
    }

    /*
     * TODO: The character encoding should be extracted in {@link #setContentType()},
     * saved somewhere locally and used here. The response's character encoding may be
     * different (depends on the stylesheet).
     */
    final String charEnc = origResponse.getCharacterEncoding();

    this.stream = new DeferredOutputStream();
    if (charEnc != null) {
      writer = new PrintWriter(new OutputStreamWriter(stream, charEnc));
    } else {
      writer = new PrintWriter(stream);
    }

    return writer;
  }

  /**
   * This method must be invoked at the end of processing. The streams are closed and their content
   * is analyzed. Actual XSLT processing takes place here.
   */
  @SuppressWarnings("unchecked")
  void finishResponse() throws IOException {
    if (writer != null) {
      writer.close();
    } else {
      if (stream != null) stream.close();
    }

    /*
     * If we're not in passthrough mode, then we need to finalize XSLT transformation.
     */
    if (false == passthrough) {
      if (stream != null) {
        final byte[] bytes = ((DeferredOutputStream) stream).getBytes();
        final boolean processingSuppressed =
            (origRequest.getAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING) != null)
                | (origRequest.getParameter(XSLTFilterConstants.NO_XSLT_PROCESSING) != null);

        if (processingSuppressed) {
          // Just copy the buffered data to the output directly.
          final OutputStream os = origResponse.getOutputStream();
          os.write(bytes);
          os.close();
        } else {
          // Otherwise apply XSLT transformation to it.
          try {
            processWithXslt(
                bytes,
                (Map<String, Object>) origRequest.getAttribute(XSLTFilterConstants.XSLT_PARAMS_MAP),
                origResponse);
          } catch (TransformerException e) {
            final Throwable t = unwrapCause(e);
            if (t instanceof IOException) {
              throw (IOException) t;
            }

            filterError("Error applying stylesheet.", e);
          }
        }
      }
    }
  }

  /** Unwraps original throwable from the transformer/ SAX stack. */
  private Throwable unwrapCause(TransformerException e) {
    Throwable t;

    if (e.getException() != null) {
      t = e.getException();
    } else if (e.getCause() != null) {
      t = e.getCause();
    } else {
      return e;
    }

    do {
      if (t instanceof IOException) {
        // break early on IOException
        return t;
      } else if (t.getCause() != null) {
        t = t.getCause();
      } else if (t instanceof SAXException && ((SAXException) t).getException() != null) {
        t = ((SAXException) t).getException();
      } else {
        return t;
      }
    } while (true);
  }

  /**
   * Process the byte array (input XML) with the XSLT stylesheet and push the result to the output
   * stream.
   */
  private void processWithXslt(
      byte[] bytes, final Map<String, Object> stylesheetParams, final HttpServletResponse response)
      throws TransformerConfigurationException, TransformerException, IOException {
    final TransformingDocumentHandler docHandler;
    try {
      final XMLReader reader = XMLReaderFactory.createXMLReader();

      docHandler =
          new TransformingDocumentHandler(origRequest, context, stylesheetParams, transformers);

      docHandler.setContentTypeListener(
          new IContentTypeListener() {
            public void setContentType(String contentType, String encoding) {
              if (encoding == null) {
                response.setContentType(contentType);
              } else {
                response.setContentType(contentType + "; charset=" + encoding);
              }
              try {
                docHandler.setTransformationResult(new StreamResult(response.getOutputStream()));
              } catch (IOException e) {
                throw new RuntimeException("Could not open output stream.");
              }
            }
          });

      reader.setContentHandler(docHandler);
      try {
        reader.parse(new InputSource(new ByteArrayInputStream(bytes)));
      } finally {
        docHandler.cleanup();
      }
    } catch (SAXException e) {
      final Exception nested = e.getException();
      if (nested != null) {
        if (nested instanceof IOException) {
          throw (IOException) nested;
        } else if (nested instanceof TransformerException) {
          throw (TransformerException) nested;
        }
      }
      throw new TransformerException("Input parsing exception.", e);
    }
  }

  /**
   * Attempts to send an internal server error HTTP error, if possible. Otherwise simply pushes the
   * exception message to the output stream.
   *
   * @param message Message to be printed to the logger and to the output stream.
   * @param t Exception that caused the error.
   */
  protected void filterError(String message, Throwable t) {
    log.error("XSLT filter error: " + message, t);
    if (false == origResponse.isCommitted()) {
      // Reset the buffer and previous status code.
      origResponse.reset();
      origResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      origResponse.setContentType("text/html; charset=UTF-8");
    }

    // Response committed. Just push the error to the output stream.
    try {
      final OutputStream os = origResponse.getOutputStream();
      final PrintWriter osw = new PrintWriter(new OutputStreamWriter(os, "iso8859-1"));
      osw.write("<html><body><!-- " + XSLTFilterConstants.ERROR_TOKEN + " -->");
      osw.write("<h1 style=\"color: red; margin-top: 1em;\">");
      osw.write("Internal server exception");
      osw.write("</h1>");
      osw.write("<b>URI</b>: " + origRequest.getRequestURI() + "\n<br/><br/>");
      serializeException(osw, t);
      if (t instanceof ServletException && ((ServletException) t).getRootCause() != null) {
        osw.write("<br/><br/><h2>ServletException root cause:</h2>");
        serializeException(osw, ((ServletException) t).getRootCause());
      }
      osw.write("</body></html>");
      osw.flush();
    } catch (IOException e) {
      // Not much to do in such case (connection broken most likely).
      log.debug("Filter error could not be returned to client.");
    }
  }

  /** Utility method to serialize an exception and its stack trace to simple HTML. */
  private final void serializeException(PrintWriter osw, Throwable t) {
    osw.write("<b>Exception</b>: " + t.toString() + "\n<br/><br/>");
    osw.write("<b>Stack trace:</b>");
    osw.write(
        "<pre style=\"margin: 1px solid red; padding: 3px; font-family: sans-serif; font-size: small;\">");
    t.printStackTrace(osw);
    osw.write("</pre>");
  }

  /** */
  private void detectErrorResponse(int errorCode) {
    if (errorCode != HttpServletResponse.SC_ACCEPTED) {
      origRequest.setAttribute(XSLTFilterConstants.NO_XSLT_PROCESSING, Boolean.TRUE);
    }
  }

  /** */
  public void sendError(int errorCode) throws IOException {
    detectErrorResponse(errorCode);
    super.sendError(errorCode);
  }

  /** */
  public void sendError(int errorCode, String message) throws IOException {
    detectErrorResponse(errorCode);
    super.sendError(errorCode, message);
  }

  /** */
  public void setStatus(int statusCode) {
    detectErrorResponse(statusCode);
    super.setStatus(statusCode);
  }

  /** */
  public void setStatus(int statusCode, String message) {
    detectErrorResponse(statusCode);
    super.setStatus(statusCode, message);
  }
}
Exemplo n.º 2
0
public class ServletUtil {

  public static final String CONTENT_TEXT = "text/plain; charset=utf-8";

  // bogus status returns for our logging
  public static final int STATUS_CLIENT_ABORT = 1000;
  public static final int STATUS_FORWARDED = 1001;
  public static final int STATUS_FORWARD_FAILURE = 1002;

  private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ServletUtil.class);
  private static boolean isDebugInit = false;

  private static String contextPath = null;
  private static String rootPath = null;
  private static String contentPath = null;

  /**
   * @param context the Servlet context.
   * @deprecated Now handled in TdsContext.init().
   */
  public static void initContext(ServletContext context) {
    //    setContextPath(context);
    if (contextPath == null) {
      // Servlet 2.5 allows the following.
      // contextPath = servletContext.getContextPath();
      String tmpContextPath =
          context.getInitParameter("ContextPath"); // cannot be overridden in the ThreddsConfig file
      if (tmpContextPath == null) tmpContextPath = "thredds";
      contextPath = "/" + tmpContextPath;
    }
    //    setRootPath(context);
    if (rootPath == null) {
      rootPath = context.getRealPath("/");
      rootPath = rootPath.replace('\\', '/');
    }

    //    setContentPath();
    if (contentPath == null) {
      String tmpContentPath = "../../content" + getContextPath() + "/";
      File cf = new File(getRootPath(), tmpContentPath);
      try {
        contentPath = cf.getCanonicalPath() + "/";
        contentPath = contentPath.replace('\\', '/');
      } catch (IOException e) {
        throw new RuntimeException(e.getMessage());
      }
    }

    //    initDebugging(context);
    initDebugging(context);
  }

  public static void setContextPath(String newContextPath) {
    contextPath = newContextPath;
  }

  public static void setRootPath(String newRootPath) {
    rootPath = newRootPath;
  }

  public static void setContentPath(String newContentPath) {
    contentPath = newContentPath;
    if (!contentPath.endsWith("/")) contentPath = contentPath + "/";
  }

  public static void initDebugging(ServletContext webapp) {
    if (isDebugInit) return;
    isDebugInit = true;

    String debugOn = webapp.getInitParameter("DebugOn");
    if (debugOn != null) {
      StringTokenizer toker = new StringTokenizer(debugOn);
      while (toker.hasMoreTokens()) Debug.set(toker.nextToken(), true);
    }
  }

  /**
   * Return the real path on the servers file system that corresponds to the root document ("/") on
   * the given servlet.
   *
   * @return the real path on the servers file system that corresponds to the root document ("/") on
   *     the given servlet.
   */
  public static String getRootPath() {
    return rootPath;
  }

  /**
   * Return the context path for the given servlet. Note - ToDo: Why not just use
   * ServletContext.getServletContextName()?
   *
   * @return the context path for the given servlet.
   */
  public static String getContextPath() {
    return contextPath;
  }

  /**
   * Return the content path for the given servlet.
   *
   * @return the content path for the given servlet.
   */
  public static String getContentPath() {
    return contentPath;
  }

  /**
   * Return the default/initial content path for the given servlet. The content of which is copied
   * to the content path when the web app is first installed.
   *
   * @return the default/initial content path for the given servlet.
   */
  public static String getInitialContentPath() {
    return getRootPath() + "/WEB-INF/altContent/startup/";
  }

  /**
   * Return the file path dealing with leading and trailing path seperators (which must be a slash
   * ("/")) for the given directory and file paths.
   *
   * <p>Note: Dealing with path strings is fragile. ToDo: Switch from using path strings to
   * java.io.Files.
   *
   * @param dirPath the directory path.
   * @param filePath the file path.
   * @return a full file path with the given directory and file paths.
   */
  public static String formFilename(String dirPath, String filePath) {
    if ((dirPath == null) || (filePath == null)) return null;

    if (filePath.startsWith("/")) filePath = filePath.substring(1);

    return dirPath.endsWith("/") ? dirPath + filePath : dirPath + "/" + filePath;
  }

  /**
   * Handle a request for a raw/static file (i.e., not a catalog or dataset request).
   *
   * <p>Look in the content (user) directory then the root (distribution) directory for a file that
   * matches the given path and, if found, return it as the content of the HttpServletResponse. If
   * the file is forbidden (i.e., the path contains a "..", "WEB-INF", or "META-INF" directory),
   * send a HttpServletResponse.SC_FORBIDDEN response. If no file matches the request (including an
   * "index.html" file if the path ends in "/"), send an HttpServletResponse.SC_NOT_FOUND..
   *
   * <p>
   *
   * <ol>
   *   <li>Make sure the path does not contain ".." directories.
   *   <li>Make sure the path does not contain "WEB-INF" or "META-INF".
   *   <li>Check for requested file in the content directory (if the path is a directory, make sure
   *       the path ends with "/" and check for an "index.html" file).
   *   <li>Check for requested file in the root directory (if the path is a directory, make sure the
   *       path ends with "/" and check for an "index.html" file). </ol
   *
   * @param path the requested path
   * @param servlet the servlet handling the request
   * @param req the HttpServletRequest
   * @param res the HttpServletResponse
   * @throws IOException if can't complete request due to IO problems.
   */
  public static void handleRequestForRawFile(
      String path, HttpServlet servlet, HttpServletRequest req, HttpServletResponse res)
      throws IOException {
    // Don't allow ".." directories in path.
    if (path.indexOf("/../") != -1
        || path.equals("..")
        || path.startsWith("../")
        || path.endsWith("/..")) {
      res.sendError(HttpServletResponse.SC_FORBIDDEN, "Path cannot contain \"..\" directory.");
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_FORBIDDEN, -1));
      return;
    }

    // Don't allow access to WEB-INF or META-INF directories.
    String upper = path.toUpperCase();
    if (upper.indexOf("WEB-INF") != -1 || upper.indexOf("META-INF") != -1) {
      res.sendError(
          HttpServletResponse.SC_FORBIDDEN, "Path cannot contain \"WEB-INF\" or \"META-INF\".");
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_FORBIDDEN, -1));
      return;
    }

    // Find a regular file
    File regFile = null;
    // Look in content directory for regular file.
    File cFile = new File(ServletUtil.formFilename(getContentPath(), path));
    if (cFile.exists()) {
      if (cFile.isDirectory()) {
        if (!path.endsWith("/")) {
          String newPath = req.getRequestURL().append("/").toString();
          ServletUtil.sendPermanentRedirect(newPath, req, res);
        }
        // If request path is a directory, check for index.html file.
        cFile = new File(cFile, "index.html");
        if (cFile.exists() && !cFile.isDirectory()) regFile = cFile;
      }
      // If not a directory, use this file.
      else regFile = cFile;
    }

    if (regFile == null) {
      // Look in root directory.
      File rFile = new File(ServletUtil.formFilename(getRootPath(), path));
      if (rFile.exists()) {
        if (rFile.isDirectory()) {
          if (!path.endsWith("/")) {
            String newPath = req.getRequestURL().append("/").toString();
            ServletUtil.sendPermanentRedirect(newPath, req, res);
          }
          rFile = new File(rFile, "index.html");
          if (rFile.exists() && !rFile.isDirectory()) regFile = rFile;
        } else regFile = rFile;
      }
    }

    if (regFile == null) {
      res.sendError(HttpServletResponse.SC_NOT_FOUND); // 404
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, -1));
      return;
    }

    ServletUtil.returnFile(servlet, req, res, regFile, null);
  }

  /**
   * Handle an explicit request for a content directory file (path must start with "/content/".
   *
   * <p>Note: As these requests will show the configuration files for the server, these requests
   * should be covered by security constraints.
   *
   * <p>
   *
   * <ol>
   *   <li>Make sure the path does not contain ".." directories.
   *   <li>Check for the requested file in the content directory. </ol
   *
   * @param path the requested path (must start with "/content/")
   * @param servlet the servlet handling the request
   * @param req the HttpServletRequest
   * @param res the HttpServletResponse
   * @throws IOException if can't complete request due to IO problems.
   */
  public static void handleRequestForContentFile(
      String path, HttpServlet servlet, HttpServletRequest req, HttpServletResponse res)
      throws IOException {
    handleRequestForContentOrRootFile("/content/", path, servlet, req, res);
  }

  /**
   * Handle an explicit request for a root directory file (path must start with "/root/".
   *
   * <p>Note: As these requests will show the configuration files for the server, these requests
   * should be covered by security constraints.
   *
   * <p>
   *
   * <ol>
   *   <li>Make sure the path does not contain ".." directories.
   *   <li>Check for the requested file in the root directory. </ol
   *
   * @param path the requested path (must start with "/root/")
   * @param servlet the servlet handling the request
   * @param req the HttpServletRequest
   * @param res the HttpServletResponse
   * @throws IOException if can't complete request due to IO problems.
   */
  public static void handleRequestForRootFile(
      String path, HttpServlet servlet, HttpServletRequest req, HttpServletResponse res)
      throws IOException {
    handleRequestForContentOrRootFile("/root/", path, servlet, req, res);
  }

  /**
   * Convenience routine used by handleRequestForContentFile() and handleRequestForRootFile().
   *
   * @param pathPrefix
   * @param path
   * @param servlet
   * @param req request
   * @param res response
   * @throws IOException on IO error
   */
  private static void handleRequestForContentOrRootFile(
      String pathPrefix,
      String path,
      HttpServlet servlet,
      HttpServletRequest req,
      HttpServletResponse res)
      throws IOException {
    if (!pathPrefix.equals("/content/") && !pathPrefix.equals("/root/")) {
      log.error(
          "handleRequestForContentFile(): The path prefix <"
              + pathPrefix
              + "> must be \"/content/\" or \"/root/\".");
      throw new IllegalArgumentException("Path prefix must be \"/content/\" or \"/root/\".");
    }

    if (!path.startsWith(pathPrefix)) {
      log.error(
          "handleRequestForContentFile(): path <"
              + path
              + "> must start with \""
              + pathPrefix
              + "\".");
      throw new IllegalArgumentException("Path must start with \"" + pathPrefix + "\".");
    }

    // Don't allow ".." directories in path.
    if (path.indexOf("/../") != -1
        || path.equals("..")
        || path.startsWith("../")
        || path.endsWith("/..")) {
      res.sendError(HttpServletResponse.SC_FORBIDDEN, "Path cannot contain \"..\" directory.");
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_FORBIDDEN, -1));
      return;
    }

    // Find the requested file.
    File file =
        new File(
            ServletUtil.formFilename(getContentPath(), path.substring(pathPrefix.length() - 1)));
    if (file.exists()) {
      // Do not allow request for a directory.
      if (file.isDirectory()) {
        if (!path.endsWith("/")) {
          String redirectPath = req.getRequestURL().append("/").toString();
          ServletUtil.sendPermanentRedirect(redirectPath, req, res);
          return;
        }

        int i = HtmlWriter.getInstance().writeDirectory(res, file, path);
        int status = i == 0 ? HttpServletResponse.SC_NOT_FOUND : HttpServletResponse.SC_OK;
        log.info(UsageLog.closingMessageForRequestContext(status, i));

        return;
      }

      // Return the requested file.
      ServletUtil.returnFile(servlet, req, res, file, null);
    } else {
      // Requested file not found.
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, -1));
      res.sendError(HttpServletResponse.SC_NOT_FOUND); // 404
    }
  }

  /**
   * Send a permanent redirect (HTTP status 301 "Moved Permanently") response with the given target
   * path.
   *
   * <p>The given target path may be relative or absolute. If it is relative, it will be resolved
   * against the request URL.
   *
   * @param targetPath the path to which the client is redirected.
   * @param req the HttpServletRequest
   * @param res the HttpServletResponse
   * @throws IOException if can't write the response.
   */
  public static void sendPermanentRedirect(
      String targetPath, HttpServletRequest req, HttpServletResponse res) throws IOException {
    // Absolute URL needed so resolve the target path against the request URL.
    URI uri;
    try {
      uri = new URI(req.getRequestURL().toString());
    } catch (URISyntaxException e) {
      log.error(
          "sendPermanentRedirect(): Bad syntax on request URL <" + req.getRequestURL() + ">.", e);
      log.info(
          "sendPermanentRedirect(): "
              + UsageLog.closingMessageForRequestContext(
                  HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 0));
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      return;
    }
    String absolutePath = uri.resolve(targetPath).toString();
    absolutePath = res.encodeRedirectURL(absolutePath);

    res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
    res.addHeader("Location", absolutePath);

    String title = "Permanently Moved - 301";
    String body =
        new StringBuilder()
            .append("<p>")
            .append("The requested URL <")
            .append(req.getRequestURL())
            .append("> has been permanently moved (HTTP status code 301).")
            .append(" Instead, please use the following URL: <a href=\"")
            .append(absolutePath)
            .append("\">")
            .append(absolutePath)
            .append("</a>.")
            .append("</p>")
            .toString();
    String htmlResp =
        new StringBuilder()
            .append(HtmlWriter.getInstance().getHtmlDoctypeAndOpenTag())
            .append("<head><title>")
            .append(title)
            .append("</title></head><body>")
            .append("<h1>")
            .append(title)
            .append("</h1>")
            .append(body)
            .append("</body></html>")
            .toString();

    log.info("sendPermanentRedirect(): redirect to " + absolutePath);
    log.info(
        "sendPermanentRedirect(): "
            + UsageLog.closingMessageForRequestContext(
                HttpServletResponse.SC_MOVED_PERMANENTLY, htmlResp.length()));

    // Write the catalog out.
    PrintWriter out = res.getWriter();
    res.setContentType("text/html");
    out.print(htmlResp);
    out.flush();
  }

  /**
   * Write a file to the response stream.
   *
   * @param servlet called from here
   * @param contentPath file root path
   * @param path file path reletive to the root
   * @param req the request
   * @param res the response
   * @param contentType content type, or null
   * @throws IOException on write error
   */
  public static void returnFile(
      HttpServlet servlet,
      String contentPath,
      String path,
      HttpServletRequest req,
      HttpServletResponse res,
      String contentType)
      throws IOException {

    String filename = ServletUtil.formFilename(contentPath, path);

    log.debug("returnFile(): returning file <" + filename + ">.");
    // No file, nothing to view
    if (filename == null) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    // dontallow ..
    if (filename.indexOf("..") != -1) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_FORBIDDEN, 0));
      res.sendError(HttpServletResponse.SC_FORBIDDEN);
      return;
    }

    // dont allow access to WEB-INF or META-INF
    String upper = filename.toUpperCase();
    if (upper.indexOf("WEB-INF") != -1 || upper.indexOf("META-INF") != -1) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_FORBIDDEN, 0));
      res.sendError(HttpServletResponse.SC_FORBIDDEN);
      return;
    }

    returnFile(servlet, req, res, new File(filename), contentType);
  }

  private static FileCacheRaf fileCacheRaf;

  public static void setFileCache(FileCacheRaf fileCache) {
    fileCacheRaf = fileCache;
  }

  public static FileCacheRaf getFileCache() {
    return fileCacheRaf;
  }

  /**
   * Write a file to the response stream. Handles Range requests.
   *
   * @param servlet called from here
   * @param req the request
   * @param res the response
   * @param file to serve
   * @param contentType content type, if null, will try to guess
   * @throws IOException on write error
   */
  public static void returnFile(
      HttpServlet servlet,
      HttpServletRequest req,
      HttpServletResponse res,
      File file,
      String contentType)
      throws IOException {

    // No file, nothing to view
    if (file == null) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    // check that it exists
    if (!file.exists()) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      res.sendError(HttpServletResponse.SC_NOT_FOUND);
      return;
    }

    // not a directory
    if (!file.isFile()) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_BAD_REQUEST, 0));
      res.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }

    // Set the type of the file
    String filename = file.getPath();
    if (null == contentType) {
      if (filename.endsWith(".html")) contentType = "text/html; charset=iso-8859-1";
      else if (filename.endsWith(".xml")) contentType = "text/xml; charset=iso-8859-1";
      else if (filename.endsWith(".txt") || (filename.endsWith(".log"))) contentType = CONTENT_TEXT;
      else if (filename.indexOf(".log.") > 0) contentType = CONTENT_TEXT;
      else if (filename.endsWith(".nc")) contentType = "application/x-netcdf";
      else contentType = servlet.getServletContext().getMimeType(filename);

      if (contentType == null) contentType = "application/octet-stream";
    }

    returnFile(req, res, file, contentType);
  }

  /**
   * Write a file to the response stream. Handles Range requests.
   *
   * @param req request
   * @param res response
   * @param file must exists and not be a directory
   * @param contentType must not be null
   * @throws IOException or error
   */
  public static void returnFile(
      HttpServletRequest req, HttpServletResponse res, File file, String contentType)
      throws IOException {
    res.setContentType(contentType);

    // see if its a Range Request
    boolean isRangeRequest = false;
    long startPos = 0, endPos = Long.MAX_VALUE;
    String rangeRequest = req.getHeader("Range");
    if (rangeRequest != null) { // bytes=12-34 or bytes=12-
      int pos = rangeRequest.indexOf("=");
      if (pos > 0) {
        int pos2 = rangeRequest.indexOf("-");
        if (pos2 > 0) {
          String startString = rangeRequest.substring(pos + 1, pos2);
          String endString = rangeRequest.substring(pos2 + 1);
          startPos = Long.parseLong(startString);
          if (endString.length() > 0) endPos = Long.parseLong(endString) + 1;
          isRangeRequest = true;
        }
      }
    }

    // set content length
    long fileSize = file.length();
    long contentLength = fileSize;
    if (isRangeRequest) {
      endPos = Math.min(endPos, fileSize);
      contentLength = endPos - startPos;
    }

    if (contentLength > Integer.MAX_VALUE)
      res.addHeader(
          "Content-Length", Long.toString(contentLength)); // allow content length > MAX_INT
    else res.setContentLength((int) contentLength); // note HEAD only allows this

    String filename = file.getPath();
    boolean debugRequest = Debug.isSet("returnFile");
    if (debugRequest)
      log.debug(
          "returnFile(): filename = "
              + filename
              + " contentType = "
              + contentType
              + " contentLength = "
              + contentLength);

    // indicate we allow Range Requests
    res.addHeader("Accept-Ranges", "bytes");

    if (req.getMethod().equals("HEAD")) {
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, 0));
      return;
    }

    try {

      if (isRangeRequest) {
        // set before content is sent
        res.addHeader("Content-Range", "bytes " + startPos + "-" + (endPos - 1) + "/" + fileSize);
        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

        FileCacheRaf.Raf craf = null;
        try {
          craf = fileCacheRaf.acquire(filename);
          IO.copyRafB(
              craf.getRaf(), startPos, contentLength, res.getOutputStream(), new byte[60000]);
          log.info(
              "returnFile(): "
                  + UsageLog.closingMessageForRequestContext(
                      HttpServletResponse.SC_PARTIAL_CONTENT, contentLength));
          return;
        } finally {
          if (craf != null) fileCacheRaf.release(craf);
        }
      }

      // Return the file
      ServletOutputStream out = res.getOutputStream();
      IO.copyFileB(file, out, 60000);
      res.flushBuffer();
      out.close();
      if (debugRequest) log.debug("returnFile(): returnFile ok = " + filename);
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, contentLength));
    }

    // @todo Split up this exception handling: those from file access vs those from dealing with
    // response
    //       File access: catch and res.sendError()
    //       response: don't catch (let bubble up out of doGet() etc)
    catch (FileNotFoundException e) {
      log.error("returnFile(): FileNotFoundException= " + filename);
      log.info(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_NOT_FOUND);
    } catch (java.net.SocketException e) {
      log.info("returnFile(): SocketException sending file: " + filename + " " + e.getMessage());
      log.info("returnFile(): " + UsageLog.closingMessageForRequestContext(STATUS_CLIENT_ABORT, 0));
    } catch (IOException e) {
      String eName =
          e.getClass().getName(); // dont want compile time dependency on ClientAbortException
      if (eName.equals("org.apache.catalina.connector.ClientAbortException")) {
        log.info(
            "returnFile(): ClientAbortException while sending file: "
                + filename
                + " "
                + e.getMessage());
        log.info(
            "returnFile(): " + UsageLog.closingMessageForRequestContext(STATUS_CLIENT_ABORT, 0));
        return;
      }

      log.error("returnFile(): IOException (" + e.getClass().getName() + ") sending file ", e);
      log.error(
          "returnFile(): "
              + UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      if (!res.isCommitted())
        res.sendError(HttpServletResponse.SC_NOT_FOUND, "Problem sending file: " + e.getMessage());
    }
  }

  /**
   * Send given content string as the HTTP response.
   *
   * @param contents the string to return as the HTTP response.
   * @param res the HttpServletResponse
   * @throws IOException if an I/O error occurs while writing the response.
   */
  public static void returnString(String contents, HttpServletResponse res) throws IOException {

    try {
      ServletOutputStream out = res.getOutputStream();
      IO.copy(new ByteArrayInputStream(contents.getBytes()), out);
      log.info(
          UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_OK, contents.length()));
    } catch (IOException e) {
      log.error(" IOException sending string: ", e);
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_NOT_FOUND, 0));
      res.sendError(HttpServletResponse.SC_NOT_FOUND, "Problem sending string: " + e.getMessage());
    }
  }

  /**
   * Return the request URL relative to the server (i.e., starting with the context path).
   *
   * @param req request
   * @return URL relative to the server
   */
  public static String getReletiveURL(HttpServletRequest req) {
    return req.getContextPath() + req.getServletPath() + req.getPathInfo();
  }

  /**
   * Forward this request to the CatalogServices servlet ("/catalog.html").
   *
   * @param req request
   * @param res response
   * @throws IOException on IO error
   * @throws ServletException other error
   */
  public static void forwardToCatalogServices(HttpServletRequest req, HttpServletResponse res)
      throws IOException, ServletException {

    String reqs = "catalog=" + getReletiveURL(req);
    String query = req.getQueryString();
    if (query != null) reqs = reqs + "&" + query;
    log.info("forwardToCatalogServices(): request string = \"/catalog.html?" + reqs + "\"");

    // dispatch to CatalogHtml servlet
    RequestForwardUtils.forwardRequestRelativeToCurrentContext("/catalog.html?" + reqs, req, res);
  }

  public static boolean saveFile(
      HttpServlet servlet,
      String contentPath,
      String path,
      HttpServletRequest req,
      HttpServletResponse res) {

    // @todo Need to use logServerAccess() below here.
    boolean debugRequest = Debug.isSet("SaveFile");
    if (debugRequest) log.debug(" saveFile(): path= " + path);

    String filename = contentPath + path; // absolute path
    File want = new File(filename);

    // backup current version if it exists
    int version = getBackupVersion(want.getParent(), want.getName());
    String fileSave = filename + "~" + version;
    File file = new File(filename);
    if (file.exists()) {
      try {
        IO.copyFile(filename, fileSave);
      } catch (IOException e) {
        log.error(
            "saveFile(): Unable to save copy of file "
                + filename
                + " to "
                + fileSave
                + "\n"
                + e.getMessage());
        return false;
      }
    }

    // save new file
    try {
      OutputStream out = new BufferedOutputStream(new FileOutputStream(filename));
      IO.copy(req.getInputStream(), out);
      out.close();
      if (debugRequest) log.debug("saveFile(): ok= " + filename);
      res.setStatus(HttpServletResponse.SC_CREATED);
      log.info(UsageLog.closingMessageForRequestContext(HttpServletResponse.SC_CREATED, -1));
      return true;
    } catch (IOException e) {
      log.error(
          "saveFile(): Unable to PUT file " + filename + " to " + fileSave + "\n" + e.getMessage());
      return false;
    }
  }

  private static int getBackupVersion(String dirName, String fileName) {
    int maxN = 0;
    File dir = new File(dirName);
    if (!dir.exists()) return -1;

    String[] files = dir.list();
    if (null == files) return -1;

    for (String name : files) {
      if (name.indexOf(fileName) < 0) continue;
      int pos = name.indexOf('~');
      if (pos < 0) continue;
      String ver = name.substring(pos + 1);
      int n = 0;
      try {
        n = Integer.parseInt(ver);
      } catch (NumberFormatException e) {
        log.error("Format Integer error on backup filename= " + ver);
      }
      maxN = Math.max(n, maxN);
    }
    return maxN + 1;
  }

  public static boolean copyDir(String fromDir, String toDir) throws IOException {
    File contentFile = new File(toDir + ".INIT");
    if (!contentFile.exists()) {
      IO.copyDirTree(fromDir, toDir);
      contentFile.createNewFile();
      return true;
    }
    return false;
  }

  /**
   * ************************************************************************ Sends an error to the
   * client.
   *
   * @param t The exception that caused the problem.
   * @param res The <code>HttpServletResponse</code> for the client.
   */
  public static void handleException(Throwable t, HttpServletResponse res) {
    try {
      String message = t.getMessage();
      if (message == null) message = "NULL message " + t.getClass().getName();
      if (Debug.isSet("trustedMode")) { // security issue: only show stack if trusted
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        PrintStream ps = new PrintStream(bs);
        t.printStackTrace(ps);
        message = new String(bs.toByteArray());
      }
      log.info(
          UsageLog.closingMessageForRequestContext(
              HttpServletResponse.SC_BAD_REQUEST, message.length()));
      log.error("handleException", t);
      t.printStackTrace(); // debugging - log.error not showing stack trace !!
      if (!res.isCommitted()) res.sendError(HttpServletResponse.SC_BAD_REQUEST, message);

    } catch (Throwable e) {
      log.error("handleException() had problem reporting Exception", e);
      t.printStackTrace();
    }
  }

  public static void showServerInfo(PrintStream out) {
    out.println("Server Info");
    out.println(
        " getDocumentBuilderFactoryVersion(): "
            + XMLEntityResolver.getDocumentBuilderFactoryVersion());
    out.println();

    Properties sysp = System.getProperties();
    Enumeration e = sysp.propertyNames();
    List<String> list = Collections.list(e);
    Collections.sort(list);

    out.println("System Properties:");
    for (String name : list) {
      String value = System.getProperty(name);
      out.println("  " + name + " = " + value);
    }
    out.println();
  }

  public static void showServletInfo(HttpServlet servlet, PrintStream out) {
    out.println("Servlet Info");
    out.println(" getServletName(): " + servlet.getServletName());
    out.println(" getRootPath(): " + getRootPath());
    out.println(" Init Parameters:");
    Enumeration params = servlet.getInitParameterNames();
    while (params.hasMoreElements()) {
      String name = (String) params.nextElement();
      out.println("  " + name + ": " + servlet.getInitParameter(name));
    }
    out.println();

    ServletContext context = servlet.getServletContext();
    out.println("Context Info");

    try {
      out.println(" context.getResource('/'): " + context.getResource("/"));
    } catch (java.net.MalformedURLException e) {
    } // cant happen
    out.println(" context.getServerInfo(): " + context.getServerInfo());
    out.println("  name: " + getServerInfoName(context.getServerInfo()));
    out.println("  version: " + getServerInfoVersion(context.getServerInfo()));

    out.println(" context.getInitParameterNames():");
    params = context.getInitParameterNames();
    while (params.hasMoreElements()) {
      String name = (String) params.nextElement();
      out.println("  " + name + ": " + context.getInitParameter(name));
    }

    out.println(" context.getAttributeNames():");
    params = context.getAttributeNames();
    while (params.hasMoreElements()) {
      String name = (String) params.nextElement();
      out.println("  context.getAttribute(\"" + name + "\"): " + context.getAttribute(name));
    }

    out.println();
  }

  /**
   * Show the pieces of the request, for debugging
   *
   * @param req the HttpServletRequest
   * @return parsed request
   */
  public static String getRequestParsed(HttpServletRequest req) {
    return req.getRequestURI()
        + " = "
        + req.getContextPath()
        + "(context), "
        + req.getServletPath()
        + "(servletPath), "
        + req.getPathInfo()
        + "(pathInfo), "
        + req.getQueryString()
        + "(query)";
  }

  /**
   * This is the server part, eg http://motherlode:8080
   *
   * @param req the HttpServletRequest
   * @return request server
   */
  public static String getRequestServer(HttpServletRequest req) {
    return req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort();
  }

  /**
   * This is everything except the query string
   *
   * @param req the HttpServletRequest
   * @return parsed request base
   */
  public static String getRequestBase(HttpServletRequest req) {
    // return "http://"+req.getServerName()+":"+ req.getServerPort()+req.getRequestURI();
    return req.getRequestURL().toString();
  }

  /**
   * The request base as a URI
   *
   * @param req the HttpServletRequest
   * @return parsed request as a URI
   */
  public static URI getRequestURI(HttpServletRequest req) {
    try {
      return new URI(getRequestBase(req));
    } catch (URISyntaxException e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * servletPath + pathInfo
   *
   * @param req the HttpServletRequest
   * @return parsed request servletPath + pathInfo
   */
  public static String getRequestPath(HttpServletRequest req) {
    StringBuffer buff = new StringBuffer();
    if (req.getServletPath() != null) buff.append(req.getServletPath());
    if (req.getPathInfo() != null) buff.append(req.getPathInfo());
    return buff.toString();
  }

  /**
   * The entire request including query string
   *
   * @param req the HttpServletRequest
   * @return entire parsed request
   */
  public static String getRequest(HttpServletRequest req) {
    String query = req.getQueryString();
    return getRequestBase(req) + (query == null ? "" : "?" + query);
  }

  /**
   * Return the value of the given parameter for the given request. Should only be used if the
   * parameter is known to only have one value. If used on a multi-valued parameter, the first value
   * is returned.
   *
   * @param req the HttpServletRequest
   * @param paramName the name of the parameter to find.
   * @return the value of the given parameter for the given request.
   */
  public static String getParameterIgnoreCase(HttpServletRequest req, String paramName) {
    Enumeration e = req.getParameterNames();
    while (e.hasMoreElements()) {
      String s = (String) e.nextElement();
      if (s.equalsIgnoreCase(paramName)) return req.getParameter(s);
    }
    return null;
  }

  /**
   * Return the values of the given parameter (ignoring case) for the given request.
   *
   * @param req the HttpServletRequest
   * @param paramName the name of the parameter to find.
   * @return the values of the given parameter for the given request.
   */
  public static String[] getParameterValuesIgnoreCase(HttpServletRequest req, String paramName) {
    Enumeration e = req.getParameterNames();
    while (e.hasMoreElements()) {
      String s = (String) e.nextElement();
      if (s.equalsIgnoreCase(paramName)) return req.getParameterValues(s);
    }
    return null;
  }

  public static String getFileURL(String filename) {
    filename = filename.replace('\\', '/');
    filename = StringUtil.replace(filename, ' ', "+");
    return "file:" + filename;
  }

  /**
   * Show details about the request
   *
   * @param servlet used to get teh servlet context, may be null
   * @param req the request
   * @return string showing the details of the request.
   */
  public static String showRequestDetail(HttpServlet servlet, HttpServletRequest req) {
    StringBuilder sbuff = new StringBuilder();

    sbuff.append("Request Info\n");
    sbuff.append(" req.getServerName(): ").append(req.getServerName()).append("\n");
    sbuff.append(" req.getServerPort(): ").append(req.getServerPort()).append("\n");
    sbuff.append(" req.getContextPath:").append(req.getContextPath()).append("\n");
    sbuff.append(" req.getServletPath:").append(req.getServletPath()).append("\n");
    sbuff.append(" req.getPathInfo:").append(req.getPathInfo()).append("\n");
    sbuff.append(" req.getQueryString:").append(req.getQueryString()).append("\n");
    sbuff
        .append(" getQueryStringDecoded:")
        .append(EscapeStrings.urlDecode(req.getQueryString()))
        .append("\n");
    /*try {
      sbuff.append(" getQueryStringDecoded:").append(URLDecoder.decode(req.getQueryString(), "UTF-8")).append("\n");
    } catch (UnsupportedEncodingException e1) {
      e1.printStackTrace();
    }*/
    sbuff.append(" req.getRequestURI:").append(req.getRequestURI()).append("\n");
    sbuff.append(" getRequestBase:").append(getRequestBase(req)).append("\n");
    sbuff.append(" getRequestServer:").append(getRequestServer(req)).append("\n");
    sbuff.append(" getRequest:").append(getRequest(req)).append("\n");
    sbuff.append("\n");

    sbuff.append(" req.getPathTranslated:").append(req.getPathTranslated()).append("\n");
    String path = req.getPathTranslated();
    if ((path != null) && (servlet != null)) {
      ServletContext context = servlet.getServletContext();
      sbuff.append(" getMimeType:").append(context.getMimeType(path)).append("\n");
    }
    sbuff.append("\n");
    sbuff.append(" req.getScheme:").append(req.getScheme()).append("\n");
    sbuff.append(" req.getProtocol:").append(req.getProtocol()).append("\n");
    sbuff.append(" req.getMethod:").append(req.getMethod()).append("\n");
    sbuff.append("\n");
    sbuff.append(" req.getContentType:").append(req.getContentType()).append("\n");
    sbuff.append(" req.getContentLength:").append(req.getContentLength()).append("\n");

    sbuff.append(" req.getRemoteAddr():").append(req.getRemoteAddr());
    try {
      sbuff
          .append(" getRemoteHost():")
          .append(java.net.InetAddress.getByName(req.getRemoteHost()).getHostName())
          .append("\n");
    } catch (java.net.UnknownHostException e) {
      sbuff.append(" getRemoteHost():").append(e.getMessage()).append("\n");
    }
    sbuff.append(" getRemoteUser():").append(req.getRemoteUser()).append("\n");

    sbuff.append("\n");
    sbuff.append("Request Parameters:\n");
    Enumeration params = req.getParameterNames();
    while (params.hasMoreElements()) {
      String name = (String) params.nextElement();
      String values[] = req.getParameterValues(name);
      if (values != null) {
        for (int i = 0; i < values.length; i++) {
          sbuff
              .append("  ")
              .append(name)
              .append("  (")
              .append(i)
              .append("): ")
              .append(values[i])
              .append("\n");
        }
      }
    }
    sbuff.append("\n");

    sbuff.append("Request Headers:\n");
    Enumeration names = req.getHeaderNames();
    while (names.hasMoreElements()) {
      String name = (String) names.nextElement();
      Enumeration values = req.getHeaders(name); // support multiple values
      if (values != null) {
        while (values.hasMoreElements()) {
          String value = (String) values.nextElement();
          sbuff.append("  ").append(name).append(": ").append(value).append("\n");
        }
      }
    }
    sbuff.append(" ------------------\n");

    return sbuff.toString();
  }

  public static String showRequestHeaders(HttpServletRequest req) {
    StringBuilder sbuff = new StringBuilder();
    sbuff.append("Request Headers:\n");
    Enumeration names = req.getHeaderNames();
    while (names.hasMoreElements()) {
      String name = (String) names.nextElement();
      Enumeration values = req.getHeaders(name); // support multiple values
      if (values != null) {
        while (values.hasMoreElements()) {
          String value = (String) values.nextElement();
          sbuff.append("  ").append(name).append(": ").append(value).append("\n");
        }
      }
    }
    return sbuff.toString();
  }

  public static void showSession(HttpServletRequest req, HttpServletResponse res, PrintStream out) {

    // res.setContentType("text/html");

    // Get the current session object, create one if necessary
    HttpSession session = req.getSession();

    // Increment the hit count for this page. The value is saved
    // in this client's session under the name "snoop.count".
    Integer count = (Integer) session.getAttribute("snoop.count");
    if (count == null) {
      count = 1;
    } else count = count + 1;
    session.setAttribute("snoop.count", count);

    out.println(HtmlWriter.getInstance().getHtmlDoctypeAndOpenTag());
    out.println("<HEAD><TITLE>SessionSnoop</TITLE></HEAD>");
    out.println("<BODY><H1>Session Snoop</H1>");

    // Display the hit count for this page
    out.println(
        "You've visited this page " + count + ((!(count.intValue() != 1)) ? " time." : " times."));

    out.println("<P>");

    out.println("<H3>Here is your saved session data:</H3>");
    Enumeration atts = session.getAttributeNames();
    while (atts.hasMoreElements()) {
      String name = (String) atts.nextElement();
      out.println(name + ": " + session.getAttribute(name) + "<BR>");
    }

    out.println("<H3>Here are some vital stats on your session:</H3>");
    out.println("Session id: " + session.getId() + " <I>(keep it secret)</I><BR>");
    out.println("New session: " + session.isNew() + "<BR>");
    out.println("Timeout: " + session.getMaxInactiveInterval());
    out.println("<I>(" + session.getMaxInactiveInterval() / 60 + " minutes)</I><BR>");
    out.println("Creation time: " + session.getCreationTime());
    out.println("<I>(" + new Date(session.getCreationTime()) + ")</I><BR>");
    out.println("Last access time: " + session.getLastAccessedTime());
    out.println("<I>(" + new Date(session.getLastAccessedTime()) + ")</I><BR>");

    out.println(
        "Requested session ID from cookie: " + req.isRequestedSessionIdFromCookie() + "<BR>");
    out.println("Requested session ID from URL: " + req.isRequestedSessionIdFromURL() + "<BR>");
    out.println("Requested session ID valid: " + req.isRequestedSessionIdValid() + "<BR>");

    out.println("<H3>Test URL Rewriting</H3>");
    out.println("Click <A HREF=\"" + res.encodeURL(req.getRequestURI()) + "\">here</A>");
    out.println("to test that session tracking works via URL");
    out.println("rewriting even when cookies aren't supported.");

    out.println("</BODY></HTML>");
  }

  public static void showSession(HttpServletRequest req, PrintStream out) {

    // res.setContentType("text/html");

    // Get the current session object, create one if necessary
    HttpSession session = req.getSession();

    out.println("Session id: " + session.getId());
    out.println(" session.isNew(): " + session.isNew());
    out.println(" session.getMaxInactiveInterval(): " + session.getMaxInactiveInterval() + " secs");
    out.println(
        " session.getCreationTime(): "
            + session.getCreationTime()
            + " ("
            + new Date(session.getCreationTime())
            + ")");
    out.println(
        " session.getLastAccessedTime(): "
            + session.getLastAccessedTime()
            + " ("
            + new Date(session.getLastAccessedTime())
            + ")");
    out.println(" req.isRequestedSessionIdFromCookie: " + req.isRequestedSessionIdFromCookie());
    out.println(" req.isRequestedSessionIdFromURL: " + req.isRequestedSessionIdFromURL());
    out.println(" req.isRequestedSessionIdValid: " + req.isRequestedSessionIdValid());

    out.println("Saved session Attributes:");
    Enumeration atts = session.getAttributeNames();
    while (atts.hasMoreElements()) {
      String name = (String) atts.nextElement();
      out.println(" " + name + ": " + session.getAttribute(name) + "<BR>");
    }
  }

  public static String showSecurity(HttpServletRequest req, String role) {
    StringBuilder sbuff = new StringBuilder();

    sbuff.append("Security Info\n");
    sbuff.append(" req.getRemoteUser(): ").append(req.getRemoteUser()).append("\n");
    sbuff.append(" req.getUserPrincipal(): ").append(req.getUserPrincipal()).append("\n");
    sbuff
        .append(" req.isUserInRole(")
        .append(role)
        .append("):")
        .append(req.isUserInRole(role))
        .append("\n");
    sbuff.append(" ------------------\n");

    return sbuff.toString();
  }

  /* from luca / ageci code, portResolver, portMapper not known
  static public void getSecureRedirect(HttpServletRequest req) {
      String pathInfo = req.getPathInfo();
      String queryString = req.getQueryString();
      String contextPath = req.getContextPath();
      String destination = req.getServletPath() + ((pathInfo ==   null) ? "" : pathInfo)
          + ((queryString == null) ? "" : ("?" + queryString));
      String redirectUrl = contextPath;

      Integer httpPort = new Integer(portResolver.getServerPort(req));
      Integer httpsPort = portMapper.lookupHttpsPort(httpPort);
      if (httpsPort != null) {
          boolean includePort = true;
          if (httpsPort.intValue() == 443) {
              includePort = false;
          }
          redirectUrl = "https://" + req.getServerName() +   ((includePort) ? (":" + httpsPort) : "") + contextPath
              + destination;
      }
  } */

  private static String getServerInfoName(String serverInfo) {
    int slash = serverInfo.indexOf('/');
    if (slash == -1) return serverInfo;
    else return serverInfo.substring(0, slash);
  }

  private static String getServerInfoVersion(String serverInfo) {
    // Version info is everything between the slash and the space
    int slash = serverInfo.indexOf('/');
    if (slash == -1) return null;
    int space = serverInfo.indexOf(' ', slash);
    if (space == -1) space = serverInfo.length();
    return serverInfo.substring(slash + 1, space);
  }

  public static void showThreads(PrintStream pw) {
    Thread current = Thread.currentThread();
    ThreadGroup group = current.getThreadGroup();
    while (true) {
      if (group.getParent() == null) break;
      group = group.getParent();
    }
    showThreads(pw, group, current);
  }

  private static void showThreads(PrintStream pw, ThreadGroup g, Thread current) {
    int nthreads = g.activeCount();
    pw.println("\nThread Group = " + g.getName() + " activeCount= " + nthreads);
    Thread[] tarray = new Thread[nthreads];
    int n = g.enumerate(tarray, false);

    for (int i = 0; i < n; i++) {
      Thread thread = tarray[i];
      ClassLoader loader = thread.getContextClassLoader();
      String loaderName = (loader == null) ? "Default" : loader.getClass().getName();
      Thread.State state = thread.getState();
      long id = thread.getId();
      pw.print("   " + id + " " + thread.getName() + " " + state + " " + loaderName);
      if (thread == current) pw.println(" **** CURRENT ***");
      else pw.println();
    }

    int ngroups = g.activeGroupCount();
    ThreadGroup[] garray = new ThreadGroup[ngroups];
    int ng = g.enumerate(garray, false);
    for (int i = 0; i < ng; i++) {
      ThreadGroup nested = garray[i];
      showThreads(pw, nested, current);
    }
  }
}