// Copyright 2008 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.enterprise.connector.servlet; import com.google.common.base.Strings; import com.google.enterprise.connector.logging.NDC; import com.google.enterprise.connector.manager.ConnectorManagerException; import com.google.enterprise.connector.manager.Context; import com.google.enterprise.connector.pusher.FeedFileHandler; import org.springframework.beans.BeansException; import java.io.File; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.util.Date; import java.util.logging.Formatter; import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipOutputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * <p>Admin servlet to retrieve the log files from the Connector Manager. * This servlet allows the user to list available log files, fetch individual * log files, and fetch a ZIP archive of all the log files. At this time * the user may retrieve connector log files, feed log files, and teed feed * files. Access to this servlet is restricted to either localhost or * gsa.feed.host, based upon the HTTP RemoteAddress.</p> * * <p><b>Usage:</b> * <br>To list the available connector log files: * <br><pre> http://[cm_host_addr]/connector-manager/getConnectorLogs</pre> * </p> * <p>To view an individual connector log file: * <br><pre> http://[cm_host_addr]/connector-manager/getConnectorLogs/[log_file_name]</pre> * <br>where [log_file_name] is the name of one log files returned by * the list. For instance, 'google-connectors.0.log'. As a convenience, * the log name may be simply the log file generation number, '0' in the above * example, and it gets automatically expanded.</p> * <p>If connector logging is configured to use the * {@code org.apache.juli.FileHandler}, the date, or trailing fragment of the * date, may be supplied as a convenience. For instance, given the log file * 'google-connectors.2010-02-14.log', the user may request simply '2010-02-14' * or '14'.</p> * * <p>To retrieve a ZIP archive of all the connector log files: * <br><pre> http://[cm_host_addr]/connector-manager/getConnectorLogs/*</pre> * <br>or * <br><pre> http://[cm_host_addr]/connector-manager/getConnectorLogs/ALL</pre> * </p> * * <p><br>To list the available feed log files: * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs</pre></p> * * <p>To view an individual feed log file: * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/[log_file_name]</pre> * <br>where [log_file_name] is the name of one log files returned by the list. * For instance, 'google-connectors.feed0.log'. As a convenience, the log * name may be simply the log file generation number, '0' in the above example, * and it gets automatically expanded.</p> * * <p>To retrieve a ZIP archive of all the feed log files: * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/*</pre> * <br>or * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/ALL</pre></p> * * * <p><br>To list the name and size of the teed feed file: * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile</pre></p> * * <p>To view the teed feed file: * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile/[teed_feed_name]</pre> * <br>or * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile/0</pre> * <br>where [teed_feed_name] is the base filename of the teed feed file. * <br>WARNING: The teed feed file can be HUGE. It is suggested you either * request a manageable byte range (see below) or fetch the ZIP archive file * (which may still be HUGE).</p> * * <p>To retrieve a ZIP archive of the teed feed file: * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile/*</pre> * <br>or * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile/ALL</pre> * <br>or * <br><pre> http://[cm_host_addr]/connector-manager/getTeedFeedFile/[teed_feed_name].zip</pre> * <br>where [teed_feed_name] is the filename of the teed feed file.</p> * * * <p><br><b>Byte Range Support:</b> * This servet supports a subset of the RFC 2616 byte range specification * to retrieve portions of the log files. Since the connector logs are * 50MB each and the teedFeedFile can be gigabytes, requesting a portion * of the log may be prudent. This servlet supports byte range specifier * in either the HTTP Range: header or in the Query fragment of the request. * </p> * <p>For instance: * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/0?bytes=0-1000</pre> * <br>returns the first 1001 bytes of the current feed log.</p> * <br> * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/0?bytes=-1000</pre> * <br>returns the last 1000 bytes (the tail) of the current feed log. * <br> * <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogs/0?bytes=1000-</pre> * <br>returns everything after the first 1000 bytes of the current feed log. * </p> * <p>Multipart byte ranges are NOT supported (ie bytes=0-100,1000-2000). * Byte range requests for log listing pages and ZIP archive files are * ignored.</p> * * * <p><br><b>Redirects and curl:</b> * When using shorthand file specifications, like generation numbers, * 'ALL', or '*', this servlet returns a redirect to the actual filename. * This allows the browser, wget, or curl to pull the true filename off * the redirected URL so that it can name the file when storing locally. * If using curl to retrieve the files, please to use 'curl -L' to tell * curl to follow the redirect. Unfortunately, when using 'curl -O' to * save the file locally, curl uses the pre-redirected name, rather than * the post-redirected name when naming the local file. This forces you * to use 'curl -L -o output_filename' anyway, so you might as well * specify the full filename in the URL to begin with.</p> * * <p>Wget handles redirects appropriately without intervention, and names * the saved file as expected.</p> * * * <p><br><b>Compressed Content-Encodings:</b> * When serving up individual logs, this servlet supports compressing * the output stream using the gzip or deflate Content-Encodings, if * the client accepts them. When using curl, you can enable compressed * Content-Encoding by specifying 'curl --compressed ...'.</p> */ public class GetConnectorLogs extends HttpServlet { private static Logger LOGGER = Logger.getLogger(GetConnectorLogs.class.getName()); /** * Retrieves the log files for a connector instance. * * @param req * @param res * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, FileNotFoundException { doGet(req, res); } /** * Retrieves the log files for a connector instance. * * @param req * @param res * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, FileNotFoundException { // Make sure this requester is OK if (!RemoteAddressFilter.getInstance() .allowed(RemoteAddressFilter.Access.RED, req.getRemoteAddr())) { res.sendError(HttpServletResponse.SC_FORBIDDEN); return; } NDC.pushAppend("Support"); try { handleDoGet(req, res); } finally { NDC.pop(); } } /** * Retrieves the log files for a connector instance. * * @param req * @param res * @throws IOException */ // TODO: This extracted method is now testable, so write some tests. private void handleDoGet(HttpServletRequest req, HttpServletResponse res) throws IOException, FileNotFoundException { // Are we retrieving Connector logs, Feed logs, or TeedFeed file? LogHandler handler; String logsTag; try { Context context = Context.getInstance(); if (req.getServletPath().indexOf("Teed") > 0) { handler = new TeedFeedHandler(context); logsTag = ServletUtil.XMLTAG_TEED_FEED; } else if (req.getServletPath().indexOf("Feed") > 0) { handler = new FeedLogHandler(context); logsTag = ServletUtil.XMLTAG_FEED_LOGS; } else { handler = newConnectorLogHandler(); logsTag = ServletUtil.XMLTAG_CONNECTOR_LOGS; } } catch (ConnectorManagerException cme) { LOGGER.warning(cme.getMessage()); res.setContentType(ServletUtil.MIMETYPE_XML); PrintWriter out = res.getWriter(); try { // TODO: These should really be new ConnectorMessageCodes ServletUtil.writeResponse(out, new ConnectorMessageCode( ConnectorMessageCode.EXCEPTION_HTTP_SERVLET, req.getServletPath() + " - " + cme.getMessage())); } finally { out.close(); } return; } // Fetch the name of the log file to return. If none is specified, // return a list of the available log files. getPathInfo() returns // items with a leading '/', so we want to pull off only the basename. // WARNING: For security reasons, the PathInfo parameter must never // be passed directly to a File() or shell command. We are pulling // of the base filename part of the PathInfo and restricting file // retrievals to the log file directory as configured for the // Connector Manager. String logName = baseName(req.getPathInfo()); // If no log file is specified, return a list available logs. if (Strings.isNullOrEmpty(logName)) { res.setContentType(ServletUtil.MIMETYPE_XML); PrintWriter out = res.getWriter(); try { showLogNames(handler, logsTag, out); } finally { out.close(); } } // If the user asks for all logs, return a ZIP archive. else if ("ALL".equalsIgnoreCase(logName) || "*".equals(logName)) { res.sendRedirect(res.encodeRedirectURL(handler.getArchiveName())); return; } else if (logName.equalsIgnoreCase(handler.getArchiveName())) { res.setContentType(ServletUtil.MIMETYPE_ZIP); ServletOutputStream out = res.getOutputStream(); try { fetchAllLogs(handler, out); } finally { out.close(); } } // If the user asks for a specific log, return only that one. else { // The user can ask for a log file by its generation (%g) number. // If they do that, fileHandlerLogFile() will expand it to the // full name of the file. We then force a redirect to the // actual logfile name. This is so wget or curl can assign // the correct name to the file. File logFile = handler.getLogFile(logName); if (!logName.equals(logFile.getName())) { String url = res.encodeRedirectURL(logFile.getName()); String query = req.getQueryString(); if (!Strings.isNullOrEmpty(query)) { url += '?' + query; } res.sendRedirect(url); return; } // Did the user ask for a byte range? ByteRange range; try { if ((range = ByteRange.parseByteRange(req)) != null) { res.addHeader("Content-Range", range.contentRange(logFile.length())); } } catch (IllegalArgumentException iae) { res.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, iae.toString()); return; } // Specify either text/plain or xml content type, based on log format. if (handler.isXmlFormat()) { res.setContentType(ServletUtil.MIMETYPE_XML); } else { res.setContentType(ServletUtil.MIMETYPE_TEXT_PLAIN); } OutputStream out = getCompressedOutputStream(req, res); try { fetchLog(logFile, range, out); } finally { out.close(); } } } /** * Specialized {@code doTrace} method that constructs an XML representation * of the given request and returns it as the response. */ @Override protected void doTrace(HttpServletRequest req, HttpServletResponse res) throws IOException { ServletDump.dumpServletRequest(req, res); } /** * Try to encode the response output stream with a compression mechanism * the client supports. Returns the standard ServletOutputStream if the * client does not support gzip or deflate encodings. * * Known Limitations: Does not take into account encoding weights, * especially 'q=0'. * * @param req an HttpServletRequest * @param res an HttpServletResponse * @returns outputStream, possibly of a compressed encoding. */ private static OutputStream getCompressedOutputStream(HttpServletRequest req, HttpServletResponse res) throws IOException { String encodings = req.getHeader("Accept-Encoding"); if ((encodings != null) && (encodings.length() > 0)) { encodings = encodings.toLowerCase(); if (encodings.indexOf("gzip") >= 0) { res.setHeader("Content-Encoding", "gzip"); return new GZIPOutputStream(res.getOutputStream()); } else if (encodings.indexOf("deflate") >= 0) { res.setHeader("Content-Encoding", "deflate"); return new ZipOutputStream(res.getOutputStream()); } } return res.getOutputStream(); } /** * Send the requested log file. * * @param logFile log File to be retrieved. * @param range ByteRange depicting a portion of file, may be null. * @param out OutputStream to which to write the log file. * @throws FileNotFoundException, IOException */ private static void fetchLog(File logFile, ByteRange range, OutputStream out) throws FileNotFoundException, IOException { long startPos = 0; long length = logFile.length(); if (range != null) { startPos = range.actualStartPosition(length); length = range.actualLength(length); } byte[] buf = new byte[1024 * 1024]; RandomAccessFile in = new RandomAccessFile(logFile, "r"); if (startPos > 0) { in.seek(startPos); } while (length > 0) { int byteCount = in.read(buf, 0, (length > buf.length) ? buf.length : (int)length); if (byteCount > 0) { out.write(buf, 0, byteCount); length -= byteCount; } else break; } in.close(); } /** * Send a ZIP image containing all the log files. * * @param handler LogHandler access to either Connector logs or Feed logs. * @param out OutputStream to which to write the archived log files. * @throws FileNotFoundException, IOException, ZipException */ private static void fetchAllLogs(LogHandler handler, OutputStream out) throws FileNotFoundException, IOException, ZipException { File[] logs = handler.listLogs(); if (logs != null) { ZipOutputStream zout = new ZipOutputStream(out); for (int i = 0; i < logs.length; i++) { ZipEntry zentry = new ZipEntry(logs[i].getName()); zentry.setSize(logs[i].length()); zentry.setTime(logs[i].lastModified()); zout.putNextEntry(zentry); fetchLog(logs[i], null, zout); zout.closeEntry(); } zout.finish(); } } /** * Send the list of the available log files. * * @param handler LogHandler access to either Connector logs or Feed logs. * @param tag xml tag to put around list of logs. * @param out PrintWriter where the response is written */ private static void showLogNames(LogHandler handler, String tag, PrintWriter out) { ServletUtil.writeRootTag(out, false); ServletUtil.writeMessageCode(out, new ConnectorMessageCode()); ServletUtil.writeXMLTag(out, 1, tag, false); File[] logs = handler.listLogs(); if (logs != null) { for (int i = 0; i < logs.length; i++) { ServletUtil.writeXMLTag(out, 2, ServletUtil.XMLTAG_LOG, false); ServletUtil.writeXMLElement(out, 3, ServletUtil.XMLTAG_NAME, logs[i].getName()); ServletUtil.writeXMLElement(out, 3, ServletUtil.XMLTAG_SIZE, String.valueOf(logs[i].length())); ServletUtil.writeXMLElement(out, 3, ServletUtil.XMLTAG_LAST_MODIFIED, String.valueOf(new Date(logs[i].lastModified()))); ServletUtil.writeXMLTag(out, 2, ServletUtil.XMLTAG_LOG, true); } } ServletUtil.writeXMLTag(out, 1, tag, true); ServletUtil.writeRootTag(out, true); } /** * Return the base filename part of the pattern or log name. * For instance "/x/y/z" returns "z", "/x/y/" returns "". * * @param name unix-style pathname or pattern. * @return the base filename (may be null or empty) */ private static String baseName(String name) { if (name != null) { // FileHandler patterns use '/' as separatorChar by default. int sep = name.lastIndexOf('/'); // If no '/', then look for system separatorChar. if ((sep == -1) && (File.separatorChar != '/')) { sep = name.lastIndexOf(File.separatorChar); } return name.substring(sep + 1); } return null; } /** * Return the directory name part of the pattern or log name. * For instance "/x/y/z" returns "/x/y/", "/x/y/" returns "/x/y/". * * @param name unix-style pathname or pattern. * @return the base filename (may be null or empty) */ private static String directoryName(String name) { if (name != null) { // FileHandler patterns use '/' as separatorChar by default. int sep = name.lastIndexOf('/'); // If no '/', then look for system separatorChar. if ((sep == -1) && (File.separatorChar != '/')) { sep = name.lastIndexOf(File.separatorChar); } return name.substring(0, sep + 1); } return null; } /** * This describes a byte range specification, as per RFC2616. */ private static class ByteRange { static final long UNSPECIFIED = -1; public long startPosition = UNSPECIFIED; public long endPosition = UNSPECIFIED; public ByteRange(long startPosition, long endPosition) { this.startPosition = startPosition; this.endPosition = endPosition; } /** * Extract a byte range request from either the HTTP header or the * Query fragment. The syntax for each is identical: bytes=start-end * Does not support multi-part ranges. * * @param req an HttpServletRequest * @throws IllegalArgumentException if the range does not look * like a valid RFC2616 ranges-specifier, or if a multi-part range * was specified. */ public static ByteRange parseByteRange(HttpServletRequest req) throws IllegalArgumentException { // First look for byte range specification in the request Query fragment. String bytes = req.getParameter("bytes"); if (bytes == null) { // Next, look for byte range specification in the HTTP header. if ((bytes = req.getHeader("Range")) == null) { // no Range header given either. return null; } if (bytes.startsWith("bytes=")) { bytes = bytes.substring(6).trim(); } else { throw new IllegalArgumentException(bytes); } } // Now extract the actual start and stop byte values. long startPosition = UNSPECIFIED; long endPosition = UNSPECIFIED; int dash = bytes.indexOf('-'); if (dash != -1) { try { if (dash > 0) { startPosition = Long.parseLong(bytes.substring(0, dash)); } if (++dash < bytes.length()) { endPosition = Long.parseLong(bytes.substring(dash)); } } catch (NumberFormatException nfe) { throw new IllegalArgumentException(bytes); } } // One or the other may be unspecified, but not both. // If both are specified, start must not exceed end. if (((startPosition == UNSPECIFIED) && (endPosition == UNSPECIFIED)) || ((endPosition != UNSPECIFIED) && (startPosition > endPosition))) { throw new IllegalArgumentException(bytes); } return new ByteRange(startPosition, endPosition); } /** * Return the number of bytes to read. * * @param fileSize total number of bytes in file. * @return actual number of bytes in requested range. */ public long actualLength(long fileSize) { if (startPosition == UNSPECIFIED) { // 'tail' request - ie last n bytes of file. return (endPosition < fileSize) ? endPosition : fileSize; } else if (startPosition >= fileSize) { // start position at or after end-of-file. return 0; } else if ((endPosition == UNSPECIFIED) || (endPosition >= fileSize)) { // from startPosition to end-of-file. return fileSize - startPosition; } else { // ranges are inclusive. return endPosition - startPosition + 1; } } /** * Return the actual startPosition in the file. * * @param fileSize total number of bytes in file. * @return actual starting location to seek to. */ public long actualStartPosition(long fileSize) { if (startPosition == UNSPECIFIED) { return (fileSize < endPosition) ? 0 : fileSize - endPosition; } else { return (startPosition >= fileSize) ? fileSize : startPosition; } } /** * Return the Content-Range response header value. * @param fileSize total number of bytes in file. * @return actual starting location to seek to. */ public String contentRange(long fileSize) { long start = actualStartPosition(fileSize); long end = start + actualLength(fileSize) - 1; return "bytes " + start + '-' + end + '/' + fileSize; } } // Get Connector Log FileHandler configuration from logging.properies. private static LogHandler newConnectorLogHandler() throws ConnectorManagerException { LogManager logMgr = LogManager.getLogManager(); String[] handlers = logMgr.getProperty("handlers").split("[, ]+"); String handler = null; for (int i = 0; i < handlers.length; i++) { if (handlers[i].indexOf("FileHandler") >= 0) { handler = handlers[i]; break; } } if (handler == null) { throw new ConnectorManagerException( "Unable to retrieve Connector Logging configuration. There" + " is no FileHandler configuration in logging.properties."); } if (handler.indexOf("org.apache.juli.FileHandler") >= 0) { return new JuliLogHandler(logMgr, handler); } else { return new JavaUtilLogHandler(logMgr, handler); } } /** * Abstract access to either the Connector Logs or the Feed Logs. */ private static interface LogHandler { /** * Return an array of all the existing log Files for this LogHandler. */ public File[] listLogs(); /** * Return a File object representing the directory containing the logs. * * @returns File object representing the log directory. */ public File getLogDirectory(); /** * Return a File object representing the named Log File. * * This is designed to only construct file paths into the * logging directory used by the Logging FileHandler. * This prevents arbitrary files from being fetched from * elsewhere in the file system. * * @param logName * @returns File object representing the named Log File. */ public File getLogFile(String logName); /** * Return the filename to give ZIP archives of this handler's log files. */ public String getArchiveName(); /** * Return {@code true} if this handler's log files are in XML format, * {@code false} otherwise. */ public boolean isXmlFormat(); } /** * Abstract access to either the Connector Logs or the Feed Logs. * The Connector logs have their FileHandler configuration specified * in logging.properties; whereas the Feed logs have their FileHandler * configuration specified in the Spring applicationContext.xml file. */ private static class JavaUtilLogHandler implements LogHandler { boolean isXMLFormat = true; String pattern = "%h/java%u.log"; private File logDirectory; public JavaUtilLogHandler() {} public JavaUtilLogHandler(LogManager logManager, String handler) throws ConnectorManagerException { pattern = logManager.getProperty(handler + ".pattern"); if ((pattern == null) || (pattern.length() == 0)) { throw new ConnectorManagerException( "Unable to retrieve Connector Logging configuration. Please" + "check the FileHandler configuration in logging.properties."); } String formatter = logManager.getProperty(handler + ".formatter"); isXMLFormat = (formatter == null || (formatter.toUpperCase().indexOf("XML") >= 0)); } @Override public File[] listLogs() { return getLogDirectory().listFiles(new JavaUtilLogFilenameFilter(pattern)); } @Override public File getLogDirectory() { if (logDirectory != null) { return logDirectory; } // Known Limitations: Doesn't handle %u or %g in the directoryName part // of the FileHandler pattern. String dirName = directoryName(pattern); if (dirName == null || dirName.length() == 0) { // No path part to pattern? Look for log files in current directory. dirName = System.getProperty("user.dir"); } else { int len = dirName.length(); StringBuilder buf = new StringBuilder(2 * len); for (int i = 0; i < len; i++) { char c = dirName.charAt(i); // % is the lead-in quote character for FileHandler patterns. // Replace the %h, %t, and %% substitution patterns with their // resolved values. if (c == '%') { try { c = dirName.charAt(++i); } catch (IndexOutOfBoundsException ignored) { // % at end? Just preserve it. } if (c == '%') { buf.append(c); } else if (c == 'h') { buf.append(System.getProperty("user.home")); } else if (c == 't') { buf.append(System.getProperty("java.io.tmpdir")); } else { buf.append('%').append(c); } } else { buf.append(c); } } dirName = buf.toString(); } logDirectory = new File(dirName); return logDirectory; } @Override public File getLogFile(String logName) { // The caller may specify simply the log generation number // (the value of %g for the specific file). If so, build // a logName from that. if (Pattern.matches("[0-9]+", logName)) { // Pull off the base filename pattern. String basePattern = baseName(pattern); // Assume no duplicate file collisions with shorthand request. basePattern = basePattern.replaceAll("%u", "0"); // Replace the generation placeholder with the supplied number. if (basePattern.indexOf("%g") >= 0) { logName = basePattern.replaceAll("%g", logName); } else { logName = basePattern + '.' + logName; // implicit %g rule. } } else { // The logName was not a generation number. // Assume it is the actual log file name. // Don't allow full or relative pathnames. logName = new File(logName).getName(); } return new File(getLogDirectory(), logName); } @Override public String getArchiveName() { // Only take the filename part of the path. String fhPattern = baseName(pattern); int len = fhPattern.length(); StringBuilder buf = new StringBuilder(2 * len); int i; for (i = 0; i < len; i++) { char c = fhPattern.charAt(i); // % is the lead-in quote character for FileHandler patterns. if (c == '%') { try { c = fhPattern.charAt(++i); } catch (IndexOutOfBoundsException ignored) { // % at end? Just preserve it. } if (c == '%') { buf.append(c); } else if ("guth".indexOf(c) < 0) { // drop %g, %u, %t, %h buf.append('%').append(c); } } else { buf.append(c); } } // Stripping out parts may have left us with a name like ..log. while ((i = buf.indexOf("..")) >= 0) { buf.deleteCharAt(i); } // Pluralize .log -> -logs as a convenience. if ((i = buf.lastIndexOf(".log")) >= 0) { buf.replace(i, i + 4, "-logs"); } // Add ZIP filename extension. buf.append(".zip"); return buf.toString(); } @Override public boolean isXmlFormat() { return this.isXMLFormat; } /** * A FilenameFilter for java.util.logging.FileHandler log files. */ private static class JavaUtilLogFilenameFilter implements FilenameFilter { private Pattern regexPattern = null; /** * Convert a java.util.logging.FileHandler.pattern into a * java.util.regex.Pattern that could be used in a FilenameFilter. * The regex pattern only represents the filename part at the end * of the FileHandler pattern path (the stuff after the last '/'). * * @param fhPattern a java.util.logging.FileHandler.pattern */ public JavaUtilLogFilenameFilter(String fhPattern) { // Only take the filename part of the path. fhPattern = baseName(fhPattern); int len = fhPattern.length(); StringBuilder buf = new StringBuilder(2 * len); for (int i = 0; i < len; i++) { char c = fhPattern.charAt(i); // % is the lead-in quote character for FileHandler patterns. if (c == '%') { try { c = fhPattern.charAt(++i); } catch (IndexOutOfBoundsException ignored) { // % at end? Just preserve it. } if (c == '%') { buf.append(c); } else if ((c == 'g') || (c == 'u')) { buf.append("[0-9]+"); } else { buf.append('%').append(c); } } else if ("[](){}-^$*+?.,\\".indexOf(c) >= 0) { // Quote any regex special chars that might appear in the filename. buf.append('\\').append(c); } else { buf.append(c); } } // FileHandler patterns can optionally implicitly add %g and %u, // each preceded by dots. Be generous and look for those too. // Technically not stringent, but good enough for our use. buf.append("[0-9\\.]*"); // Compile the pattern for use by the matcher. regexPattern = Pattern.compile(buf.toString()); } /** * Tests if the specified file matches the regexPattern. * * @param dir the directory containing the file. * @param fileName a file in the directory. * @returns true if the fileName matches the pattern, false otherwise. */ public boolean accept(File dir, String fileName) { return regexPattern.matcher(fileName).matches(); } } } /** * A LogHandler for logs created by the org.apache.juli.LogManager. */ private static class JuliLogHandler implements LogHandler { File logDirectory; String logPrefix; String logSuffix; boolean isXMLFormat; public JuliLogHandler(LogManager logManager, String handler) { // Looks like we are using the JULI FileHandler. String dir = logManager.getProperty(handler + ".directory"); if (dir == null) { dir = "logs"; } logDirectory = new File(dir); logPrefix = logManager.getProperty(handler + ".prefix"); if (logPrefix == null) { logPrefix = "juli."; } logSuffix = logManager.getProperty(handler + ".suffix"); if (logSuffix == null) { logSuffix = ".log"; } String formatter = logManager.getProperty(handler + ".formatter"); isXMLFormat = (formatter == null || (formatter.toUpperCase().indexOf("XML") >= 0)); } @Override public File[] listLogs() { return getLogDirectory().listFiles(new JuliLogFilenameFilter()); } @Override public File getLogDirectory() { return logDirectory; } @Override public File getLogFile(String logName) { // The caller may specify simply the log date id. // If so, build a logName from that. if (Pattern.matches("[0-9-]+", logName)) { if (logName.length() < 10) { // This looks like a fragment of a date. File[] files = getLogDirectory().listFiles(new JuliLogFilenameFilter(logName)); if (files.length > 0) { logName = baseName(files[0].getName()); } } else { logName = logPrefix + logName + logSuffix; } } else { // The logName was not a date. Assume it is the actual // log file name. Don't allow full or relative pathnames. logName = new File(logName).getName(); } return new File(getLogDirectory(), logName); } @Override public String getArchiveName() { StringBuilder buf = new StringBuilder(logPrefix); // Strip trailing '.' from prefix, if present. if (logPrefix.endsWith(".")) { buf.setLength(buf.length() - 1); } // Pluralize .log -> -logs as a convenience. if (logSuffix.equalsIgnoreCase(".log")) { buf.append("-logs"); } else { buf.append(logSuffix); } // Add ZIP filename extension. buf.append(".zip"); return buf.toString(); } @Override public boolean isXmlFormat() { return this.isXMLFormat; } /** * A FilenameFilter for org.apache.juli.FileHandler log files. */ private class JuliLogFilenameFilter implements FilenameFilter { private Pattern regexPattern = null; public JuliLogFilenameFilter() { this(""); } // Constructor that allows a fragment of the date. public JuliLogFilenameFilter(String fragment) { StringBuilder buf = new StringBuilder(); appendEscaped(buf, logPrefix); buf.append("[0-9-]+"); buf.append(fragment); appendEscaped(buf, logSuffix); regexPattern = Pattern.compile(buf.toString()); } /** * Append {@code src} to {@code buf}, escaping special characters. */ private void appendEscaped(StringBuilder buf, String src) { for (char c : src.toCharArray()) { // Quote any regex special chars that might appear in the source. if ("[](){}-^$*+?.,\\".indexOf(c) >= 0) { buf.append('\\'); } buf.append(c); } } /** * Tests if the specified file matches the regexPattern. * * @param dir the directory containing the file. * @param fileName a file in the directory. * @returns true if the fileName matches the pattern, false otherwise. */ public boolean accept(File dir, String fileName) { return regexPattern.matcher(fileName).matches(); } } } /** * A LogHandler for feedlogs, as configured in applicationContext.properties. */ private static class FeedLogHandler extends JavaUtilLogHandler { public FeedLogHandler(Context context) throws ConnectorManagerException { try { FeedFileHandler ffh = (FeedFileHandler) context.getApplicationContext() .getBean("FeedHandler", FeedFileHandler.class); String ffhPattern = ffh.getPattern(); if (ffhPattern != null && ffhPattern.length() > 0) { super.pattern = ffhPattern; } Formatter formatter = ffh.getFormatter(); isXMLFormat = (formatter == null || (formatter.getClass().getName().toUpperCase().indexOf("XML") >= 0)); } catch (BeansException be) { throw new ConnectorManagerException( "Unable to retrieve Feed Logging configuration: " + be.toString()); } } } /** * A LogHandler for Teed Feed File, as configured in * applicationContext.properties. * At this time, there is only one teedFeedFile is specified, so this is * a pretty trivial LogHandler implementation. */ private static class TeedFeedHandler implements LogHandler { String teedFeedFile; public TeedFeedHandler(Context context) throws ConnectorManagerException { teedFeedFile = context.getTeedFeedFile(); if ((teedFeedFile == null) || (teedFeedFile.length() == 0)) { throw new ConnectorManagerException( "Unable to retrieve Teed Feed File configuration. The teedFeedFile" + " property is not defined in applicationContext.properties."); } } @Override public File[] listLogs() { return new File[] { new File(teedFeedFile) }; } @Override public File getLogDirectory() { File parent = (new File(teedFeedFile)).getParentFile(); if (parent != null) { return parent; } else { throw new IllegalStateException( "The teedFeedFile does not specify a parent directory."); } } @Override public File getLogFile(String logName) { return new File(teedFeedFile); } @Override public String getArchiveName() { return new File(teedFeedFile).getName() + ".zip"; } @Override public boolean isXmlFormat() { // Even though the teedFeedFile is XML format, it is malformed - // especially when byte-served. return false; } } }