/** * 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); } }
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); } } }