0001: /*
0002: * Copyright 1999-2001,2004 The Apache Software Foundation.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016:
0017: package org.apache.catalina.valves;
0018:
0019: import java.io.File;
0020: import java.io.FileWriter;
0021: import java.io.IOException;
0022: import java.io.PrintWriter;
0023: import java.net.InetAddress;
0024: import java.net.URLEncoder;
0025: import java.text.DecimalFormat;
0026: import java.text.SimpleDateFormat;
0027: import java.util.Date;
0028: import java.util.Iterator;
0029: import java.util.LinkedList;
0030: import java.util.TimeZone;
0031:
0032: import javax.servlet.ServletException;
0033: import javax.servlet.ServletRequest;
0034: import javax.servlet.http.Cookie;
0035: import javax.servlet.http.HttpServletRequest;
0036: import javax.servlet.http.HttpSession;
0037:
0038: import org.apache.catalina.HttpResponse;
0039: import org.apache.catalina.Lifecycle;
0040: import org.apache.catalina.LifecycleException;
0041: import org.apache.catalina.LifecycleListener;
0042: import org.apache.catalina.Request;
0043: import org.apache.catalina.Response;
0044: import org.apache.catalina.ValveContext;
0045: import org.apache.catalina.util.LifecycleSupport;
0046: import org.apache.catalina.util.ServerInfo;
0047: import org.apache.catalina.util.StringManager;
0048: import org.apache.commons.logging.Log;
0049: import org.apache.commons.logging.LogFactory;
0050:
0051: /**
0052: * An implementation of the W3c Extended Log File Format. See
0053: * http://www.w3.org/TR/WD-logfile.html for more information about the format.
0054: *
0055: * The following fields are supported:
0056: * <ul>
0057: * <li><code>c-dns</code>: Client hostname</li>
0058: * <li><code>c-ip</code>: Client ip address</li>
0059: * <li><code>bytes</code>: bytes served</li>
0060: * <li><code>cs-method</code>: request method</li>
0061: * <li><code>cs-uri</code>: The full uri requested</li>
0062: * <li><code>cs-uri-query</code>: The query string</li>
0063: * <li><code>cs-uri-stem</code>: The uri without query string</li>
0064: * <li><code>date</code>: The date in yyyy-mm-dd format for GMT</li>
0065: * <li><code>s-dns</code>: The server dns entry </li>
0066: * <li><code>s-ip</code>: The server ip address</li>
0067: * <li><code>cs(XXX)</code>: The value of header XXX from client to server</li>
0068: * <li><code>sc(XXX)</code>: The value of header XXX from server to client </li>
0069: * <li><code>sc-status</code>: The status code</li>
0070: * <li><code>time</code>: Time the request was served</li>
0071: * <li><code>time-taken</code>: Time (in seconds) taken to serve the request</li>
0072: * <li><code>x-A(XXX)</code>: Pull XXX attribute from the servlet context </li>
0073: * <li><code>x-C(XXX)</code>: Pull the first cookie of the name XXX </li>
0074: * <li><code>x-R(XXX)</code>: Pull XXX attribute from the servlet request </li>
0075: * <li><code>x-S(XXX)</code>: Pull XXX attribute from the session </li>
0076: * <li><code>x-P(...)</code>: Call request.getParameter(...)
0077: * and URLencode it. Helpful to capture
0078: * certain POST parameters.
0079: * </li>
0080: * <li>For any of the x-H(...) the following method will be called from the
0081: * HttpServletRequestObject </li>
0082: * <li><code>x-H(authType)</code>: getAuthType </li>
0083: * <li><code>x-H(characterEncoding)</code>: getCharacterEncoding </li>
0084: * <li><code>x-H(contentLength)</code>: getContentLength </li>
0085: * <li><code>x-H(locale)</code>: getLocale</li>
0086: * <li><code>x-H(protocol)</code>: getProtocol </li>
0087: * <li><code>x-H(remoteUser)</code>: getRemoteUser</li>
0088: * <li><code>x-H(requestedSessionId)</code>: getGequestedSessionId</li>
0089: * <li><code>x-H(requestedSessionIdFromCookie)</code>:
0090: * isRequestedSessionIdFromCookie </li>
0091: * <li><code>x-H(requestedSessionIdValid)</code>:
0092: * isRequestedSessionIdValid</li>
0093: * <li><code>x-H(scheme)</code>: getScheme</li>
0094: * <li><code>x-H(secure)</code>: isSecure</li>
0095: * </ul>
0096: *
0097: *
0098: *
0099: * <p>
0100: * Log rotation can be on or off. This is dictated by the rotatable
0101: * property.
0102: * </p>
0103: *
0104: * <p>
0105: * For UNIX users, another field called <code>checkExists</code>is also
0106: * available. If set to true, the log file's existence will be checked before
0107: * each logging. This way an external log rotator can move the file
0108: * somewhere and tomcat will start with a new file.
0109: * </p>
0110: *
0111: * <p>
0112: * For JMX junkies, a public method called </code>rotate</code> has
0113: * been made available to allow you to tell this instance to move
0114: * the existing log file to somewhere else start writing a new log file.
0115: * </p>
0116: *
0117: * <p>
0118: * Conditional logging is also supported. This can be done with the
0119: * <code>condition</code> property.
0120: * If the value returned from ServletRequest.getAttribute(condition)
0121: * yields a non-null value. The logging will be skipped.
0122: * </p>
0123: *
0124: * <p>
0125: * For extended attributes coming from a getAttribute() call,
0126: * it is you responsibility to ensure there are no newline or
0127: * control characters.
0128: * </p>
0129: *
0130: *
0131: * @author Tim Funk
0132: * @version $Revision: 1.8.2.1 $ $Date: 2004/08/21 15:49:56 $
0133: */
0134:
0135: public final class ExtendedAccessLogValve extends ValveBase implements
0136: Lifecycle {
0137:
0138: // ----------------------------------------------------------- Constructors
0139:
0140: /**
0141: * Construct a new instance of this class with default property values.
0142: */
0143: public ExtendedAccessLogValve() {
0144:
0145: super ();
0146:
0147: }
0148:
0149: // ----------------------------------------------------- Instance Variables
0150: private static Log log = LogFactory
0151: .getLog(ExtendedAccessLogValve.class);
0152:
0153: /**
0154: * The descriptive information about this implementation.
0155: */
0156: protected static final String info = "org.apache.catalina.valves.ExtendedAccessLogValve/1.0";
0157:
0158: /**
0159: * The lifecycle event support for this component.
0160: */
0161: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
0162:
0163: /**
0164: * The string manager for this package.
0165: */
0166: private StringManager sm = StringManager
0167: .getManager(Constants.Package);
0168:
0169: /**
0170: * Has this component been started yet?
0171: */
0172: private boolean started = false;
0173:
0174: /**
0175: * The as-of date for the currently open log file, or a zero-length
0176: * string if there is no open log file.
0177: */
0178: private String dateStamp = "";
0179:
0180: /**
0181: * The PrintWriter to which we are currently logging, if any.
0182: */
0183: private PrintWriter writer = null;
0184:
0185: /**
0186: * The formatter for the date contained in the file name.
0187: */
0188: private SimpleDateFormat fileDateFormatter = null;
0189:
0190: /**
0191: * A date formatter to format a Date into a date in the format
0192: * "yyyy-MM-dd".
0193: */
0194: private SimpleDateFormat dateFormatter = null;
0195:
0196: /**
0197: * A date formatter to format a Date into a time in the format
0198: * "kk:mm:ss" (kk is a 24-hour representation of the hour).
0199: */
0200: private SimpleDateFormat timeFormatter = null;
0201:
0202: /**
0203: * Time taken formatter for 3 decimal places.
0204: */
0205: private DecimalFormat timeTakenFormatter = null;
0206:
0207: /**
0208: * My ip address. Look it up once and remember it. Dump this if we can
0209: * determine another reliable way to get server ip address since this
0210: * server may have many ip's.
0211: */
0212: private String myIpAddress = null;
0213:
0214: /**
0215: * My dns name. Look it up once and remember it. Dump this if we can
0216: * determine another reliable way to get server name address since this
0217: * server may have many ip's.
0218: */
0219: private String myDNSName = null;
0220:
0221: /**
0222: * Holder for all of the fields to log after the pattern is decoded.
0223: */
0224: private FieldInfo[] fieldInfos;
0225:
0226: /**
0227: * The current log file we are writing to. Helpful when checkExists
0228: * is true.
0229: */
0230: private File currentLogFile = null;
0231:
0232: /**
0233: * The system time when we last updated the Date that this valve
0234: * uses for log lines.
0235: */
0236: private Date currentDate = null;
0237:
0238: /**
0239: * Instant when the log daily rotation was last checked.
0240: */
0241: private long rotationLastChecked = 0L;
0242:
0243: /**
0244: * The directory in which log files are created.
0245: */
0246: private String directory = "logs";
0247:
0248: /**
0249: * The pattern used to format our access log lines.
0250: */
0251: private String pattern = null;
0252:
0253: /**
0254: * The prefix that is added to log file filenames.
0255: */
0256: private String prefix = "access_log.";
0257:
0258: /**
0259: * Should we rotate our log file? Default is true (like old behavior)
0260: */
0261: private boolean rotatable = true;
0262:
0263: /**
0264: * The suffix that is added to log file filenames.
0265: */
0266: private String suffix = "";
0267:
0268: /**
0269: * Are we doing conditional logging. default false.
0270: */
0271: private String condition = null;
0272:
0273: /**
0274: * Do we check for log file existence? Helpful if an external
0275: * agent renames the log file so we can automagically recreate it.
0276: */
0277: private boolean checkExists = false;
0278:
0279: /**
0280: * Date format to place in log file name. Use at your own risk!
0281: */
0282: private String fileDateFormat = null;
0283:
0284: // ------------------------------------------------------------- Properties
0285:
0286: /**
0287: * Return the directory in which we create log files.
0288: */
0289: public String getDirectory() {
0290:
0291: return (directory);
0292:
0293: }
0294:
0295: /**
0296: * Set the directory in which we create log files.
0297: *
0298: * @param directory The new log file directory
0299: */
0300: public void setDirectory(String directory) {
0301:
0302: this .directory = directory;
0303:
0304: }
0305:
0306: /**
0307: * Return descriptive information about this implementation.
0308: */
0309: public String getInfo() {
0310:
0311: return (info);
0312:
0313: }
0314:
0315: /**
0316: * Return the format pattern.
0317: */
0318: public String getPattern() {
0319:
0320: return (this .pattern);
0321:
0322: }
0323:
0324: /**
0325: * Set the format pattern, first translating any recognized alias.
0326: *
0327: * @param pattern The new pattern pattern
0328: */
0329: public void setPattern(String pattern) {
0330:
0331: FieldInfo[] f = decodePattern(pattern);
0332: if (f != null) {
0333: this .pattern = pattern;
0334: this .fieldInfos = f;
0335: }
0336: }
0337:
0338: /**
0339: * Return the log file prefix.
0340: */
0341: public String getPrefix() {
0342:
0343: return (prefix);
0344:
0345: }
0346:
0347: /**
0348: * Set the log file prefix.
0349: *
0350: * @param prefix The new log file prefix
0351: */
0352: public void setPrefix(String prefix) {
0353:
0354: this .prefix = prefix;
0355:
0356: }
0357:
0358: /**
0359: * Return true if logs are automatically rotated.
0360: */
0361: public boolean isRotatable() {
0362:
0363: return rotatable;
0364:
0365: }
0366:
0367: /**
0368: * Set the value is we should we rotate the logs
0369: *
0370: * @param rotatable true is we should rotate.
0371: */
0372: public void setRotatable(boolean rotatable) {
0373:
0374: this .rotatable = rotatable;
0375:
0376: }
0377:
0378: /**
0379: * Return the log file suffix.
0380: */
0381: public String getSuffix() {
0382:
0383: return (suffix);
0384:
0385: }
0386:
0387: /**
0388: * Set the log file suffix.
0389: *
0390: * @param suffix The new log file suffix
0391: */
0392: public void setSuffix(String suffix) {
0393:
0394: this .suffix = suffix;
0395:
0396: }
0397:
0398: /**
0399: * Return whether the attribute name to look for when
0400: * performing conditional loggging. If null, every
0401: * request is logged.
0402: */
0403: public String getCondition() {
0404:
0405: return condition;
0406:
0407: }
0408:
0409: /**
0410: * Set the ServletRequest.attribute to look for to perform
0411: * conditional logging. Set to null to log everything.
0412: *
0413: * @param condition Set to null to log everything
0414: */
0415: public void setCondition(String condition) {
0416:
0417: this .condition = condition;
0418:
0419: }
0420:
0421: /**
0422: * Check for file existence before logging.
0423: */
0424: public boolean isCheckExists() {
0425:
0426: return checkExists;
0427:
0428: }
0429:
0430: /**
0431: * Set whether to check for log file existence before logging.
0432: *
0433: * @param checkExists true meaning to check for file existence.
0434: */
0435: public void setCheckExists(boolean checkExists) {
0436:
0437: this .checkExists = checkExists;
0438:
0439: }
0440:
0441: /**
0442: * Return the date format date based log rotation.
0443: */
0444: public String getFileDateFormat() {
0445: return fileDateFormat;
0446: }
0447:
0448: /**
0449: * Set the date format date based log rotation.
0450: */
0451: public void setFileDateFormat(String fileDateFormat) {
0452: this .fileDateFormat = fileDateFormat;
0453: }
0454:
0455: // --------------------------------------------------------- Public Methods
0456:
0457: /**
0458: * Log a message summarizing the specified request and response, according
0459: * to the format specified by the <code>pattern</code> property.
0460: *
0461: * @param request Request being processed
0462: * @param response Response being processed
0463: * @param context The valve context used to invoke the next valve
0464: * in the current processing pipeline
0465: *
0466: * @exception IOException if an input/output error has occurred
0467: * @exception ServletException if a servlet error has occurred
0468: */
0469: public void invoke(Request request, Response response,
0470: ValveContext context) throws IOException, ServletException {
0471:
0472: // Pass this request on to the next valve in our pipeline
0473: long endTime;
0474: long runTime;
0475: long startTime = System.currentTimeMillis();
0476:
0477: context.invokeNext(request, response);
0478:
0479: endTime = System.currentTimeMillis();
0480: runTime = endTime - startTime;
0481:
0482: if (fieldInfos == null || condition != null
0483: && null != request.getRequest().getAttribute(condition)) {
0484: return;
0485: }
0486:
0487: Date date = getDate(endTime);
0488: StringBuffer result = new StringBuffer();
0489:
0490: for (int i = 0; fieldInfos != null && i < fieldInfos.length; i++) {
0491: switch (fieldInfos[i].type) {
0492: case FieldInfo.DATA_CLIENT:
0493: if (FieldInfo.FIELD_IP == fieldInfos[i].location)
0494: result.append(request.getRequest().getRemoteAddr());
0495: else if (FieldInfo.FIELD_DNS == fieldInfos[i].location)
0496: result.append(request.getRequest().getRemoteHost());
0497: else
0498: result.append("?WTF?"); /* This should never happen! */
0499: break;
0500: case FieldInfo.DATA_SERVER:
0501: if (FieldInfo.FIELD_IP == fieldInfos[i].location)
0502: result.append(myIpAddress);
0503: else if (FieldInfo.FIELD_DNS == fieldInfos[i].location)
0504: result.append(myDNSName);
0505: else
0506: result.append("?WTF?"); /* This should never happen! */
0507: break;
0508: case FieldInfo.DATA_REMOTE:
0509: result.append('?'); /* I don't know how to handle these! */
0510: break;
0511: case FieldInfo.DATA_CLIENT_TO_SERVER:
0512: result
0513: .append(getClientToServer(fieldInfos[i],
0514: request));
0515: break;
0516: case FieldInfo.DATA_SERVER_TO_CLIENT:
0517: result
0518: .append(getServerToClient(fieldInfos[i],
0519: response));
0520: break;
0521: case FieldInfo.DATA_SERVER_TO_RSERVER:
0522: result.append('-');
0523: break;
0524: case FieldInfo.DATA_RSERVER_TO_SERVER:
0525: result.append('-');
0526: break;
0527: case FieldInfo.DATA_APP_SPECIFIC:
0528: result.append(getAppSpecific(fieldInfos[i], request));
0529: break;
0530: case FieldInfo.DATA_SPECIAL:
0531: if (FieldInfo.SPECIAL_DATE == fieldInfos[i].location)
0532: result.append(dateFormatter.format(date));
0533: else if (FieldInfo.SPECIAL_TIME_TAKEN == fieldInfos[i].location)
0534: result.append(timeTakenFormatter
0535: .format(runTime / 1000d));
0536: else if (FieldInfo.SPECIAL_TIME == fieldInfos[i].location)
0537: result.append(timeFormatter.format(date));
0538: else if (FieldInfo.SPECIAL_BYTES == fieldInfos[i].location) {
0539: int length = response.getContentCount();
0540: if (length > 0)
0541: result.append(length);
0542: else
0543: result.append("-");
0544: } else if (FieldInfo.SPECIAL_CACHED == fieldInfos[i].location)
0545: result.append('-'); /* I don't know how to evaluate this! */
0546: else
0547: result.append("?WTF?"); /* This should never happen! */
0548: break;
0549: default:
0550: result.append("?WTF?"); /* This should never happen! */
0551: }
0552:
0553: if (fieldInfos[i].postWhiteSpace != null) {
0554: result.append(fieldInfos[i].postWhiteSpace);
0555: }
0556: }
0557: log(result.toString(), date);
0558:
0559: }
0560:
0561: /**
0562: * Rename the existing log file to something else. Then open the
0563: * old log file name up once again. Intended to be called by a JMX
0564: * agent.
0565: *
0566: *
0567: * @param newFileName The file name to move the log file entry to
0568: * @return true if a file was rotated with no error
0569: */
0570: public synchronized boolean rotate(String newFileName) {
0571:
0572: if (currentLogFile != null) {
0573: File holder = currentLogFile;
0574: close();
0575: try {
0576: holder.renameTo(new File(newFileName));
0577: } catch (Throwable e) {
0578: log.error("rotate failed", e);
0579: }
0580:
0581: /* Make sure date is correct */
0582: currentDate = new Date(System.currentTimeMillis());
0583: dateStamp = fileDateFormatter.format(currentDate);
0584:
0585: open();
0586: return true;
0587: } else {
0588: return false;
0589: }
0590:
0591: }
0592:
0593: // -------------------------------------------------------- Private Methods
0594:
0595: /**
0596: * Return the client to server data.
0597: * @param fieldInfo The field to decode.
0598: * @param request The object we pull data from.
0599: * @return The appropriate value.
0600: */
0601: private String getClientToServer(FieldInfo fieldInfo,
0602: Request request) {
0603:
0604: ServletRequest sr = request.getRequest();
0605: HttpServletRequest hsr = null;
0606: if (sr instanceof HttpServletRequest)
0607: hsr = (HttpServletRequest) sr;
0608:
0609: switch (fieldInfo.location) {
0610: case FieldInfo.FIELD_METHOD:
0611: return hsr.getMethod();
0612: case FieldInfo.FIELD_URI:
0613: if (null == hsr.getQueryString())
0614: return hsr.getRequestURI();
0615: else
0616: return hsr.getRequestURI() + "?" + hsr.getQueryString();
0617: case FieldInfo.FIELD_URI_STEM:
0618: return hsr.getRequestURI();
0619: case FieldInfo.FIELD_URI_QUERY:
0620: if (null == hsr.getQueryString())
0621: return "-";
0622: return hsr.getQueryString();
0623: case FieldInfo.FIELD_HEADER:
0624: return wrap(hsr.getHeader(fieldInfo.value));
0625: default:
0626: ;
0627: }
0628:
0629: return "-";
0630:
0631: }
0632:
0633: /**
0634: * Return the server to client data.
0635: * @param fieldInfo The field to decode.
0636: * @param response The object we pull data from.
0637: * @return The appropriate value.
0638: */
0639: private String getServerToClient(FieldInfo fieldInfo,
0640: Response response) {
0641: HttpResponse r = (HttpResponse) response;
0642: switch (fieldInfo.location) {
0643: case FieldInfo.FIELD_STATUS:
0644: return "" + r.getStatus();
0645: case FieldInfo.FIELD_COMMENT:
0646: return "?"; /* Not coded yet*/
0647: case FieldInfo.FIELD_HEADER:
0648: return wrap(r.getHeader(fieldInfo.value));
0649: default:
0650: ;
0651: }
0652:
0653: return "-";
0654:
0655: }
0656:
0657: /**
0658: * Get app specific data.
0659: * @param fieldInfo The field to decode
0660: * @param request Where we will pull the data from.
0661: * @return The appropriate value
0662: */
0663: private String getAppSpecific(FieldInfo fieldInfo, Request request) {
0664:
0665: ServletRequest sr = request.getRequest();
0666: HttpServletRequest hsr = null;
0667: if (sr instanceof HttpServletRequest)
0668: hsr = (HttpServletRequest) sr;
0669:
0670: switch (fieldInfo.xType) {
0671: case FieldInfo.X_PARAMETER:
0672: return wrap(urlEncode(sr.getParameter(fieldInfo.value)));
0673: case FieldInfo.X_REQUEST:
0674: return wrap(sr.getAttribute(fieldInfo.value));
0675: case FieldInfo.X_SESSION:
0676: HttpSession session = null;
0677: if (hsr != null) {
0678: session = hsr.getSession(false);
0679: if (session != null)
0680: return wrap(session.getAttribute(fieldInfo.value));
0681: }
0682: break;
0683: case FieldInfo.X_COOKIE:
0684: Cookie[] c = hsr.getCookies();
0685: for (int i = 0; c != null && i < c.length; i++) {
0686: if (fieldInfo.value.equals(c[i].getName())) {
0687: return wrap(c[i].getValue());
0688: }
0689: }
0690: case FieldInfo.X_APP:
0691: return wrap(request.getContext().getServletContext()
0692: .getAttribute(fieldInfo.value));
0693: case FieldInfo.X_SERVLET_REQUEST:
0694: if (fieldInfo.location == FieldInfo.X_LOC_AUTHTYPE) {
0695: return wrap(hsr.getAuthType());
0696: } else if (fieldInfo.location == FieldInfo.X_LOC_REMOTEUSER) {
0697: return wrap(hsr.getRemoteUser());
0698: } else if (fieldInfo.location == FieldInfo.X_LOC_REQUESTEDSESSIONID) {
0699: return wrap(hsr.getRequestedSessionId());
0700: } else if (fieldInfo.location == FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE) {
0701: return wrap("" + hsr.isRequestedSessionIdFromCookie());
0702: } else if (fieldInfo.location == FieldInfo.X_LOC_REQUESTEDSESSIONIDVALID) {
0703: return wrap("" + hsr.isRequestedSessionIdValid());
0704: } else if (fieldInfo.location == FieldInfo.X_LOC_CONTENTLENGTH) {
0705: return wrap("" + hsr.getContentLength());
0706: } else if (fieldInfo.location == FieldInfo.X_LOC_CHARACTERENCODING) {
0707: return wrap(hsr.getCharacterEncoding());
0708: } else if (fieldInfo.location == FieldInfo.X_LOC_LOCALE) {
0709: return wrap(hsr.getLocale());
0710: } else if (fieldInfo.location == FieldInfo.X_LOC_PROTOCOL) {
0711: return wrap(hsr.getProtocol());
0712: } else if (fieldInfo.location == FieldInfo.X_LOC_SCHEME) {
0713: return wrap(hsr.getScheme());
0714: } else if (fieldInfo.location == FieldInfo.X_LOC_SECURE) {
0715: return wrap("" + hsr.isSecure());
0716: }
0717: break;
0718: default:
0719: ;
0720: }
0721:
0722: return "-";
0723:
0724: }
0725:
0726: /**
0727: * urlEncode the given string. If null or empty, return null.
0728: */
0729: private String urlEncode(String value) {
0730: if (null == value || value.length() == 0) {
0731: return null;
0732: }
0733: return URLEncoder.encode(value);
0734: }
0735:
0736: /**
0737: * Wrap the incoming value into quotes and escape any inner
0738: * quotes with double quotes.
0739: *
0740: * @param value - The value to wrap quotes around
0741: * @return '-' if empty of null. Otherwise, toString() will
0742: * be called on the object and the value will be wrapped
0743: * in quotes and any quotes will be escaped with 2
0744: * sets of quotes.
0745: */
0746: private String wrap(Object value) {
0747:
0748: String svalue;
0749: // Does the value contain a " ? If so must encode it
0750: if (value == null || "-".equals(value))
0751: return "-";
0752:
0753: try {
0754: svalue = value.toString();
0755: if ("".equals(svalue))
0756: return "-";
0757: } catch (Throwable e) {
0758: /* Log error */
0759: return "-";
0760: }
0761:
0762: /* Wrap all quotes in double quotes. */
0763: StringBuffer buffer = new StringBuffer(svalue.length() + 2);
0764: buffer.append('"');
0765: int i = 0;
0766: while (i < svalue.length()) {
0767: int j = svalue.indexOf('"', i);
0768: if (j == -1) {
0769: buffer.append(svalue.substring(i));
0770: i = svalue.length();
0771: } else {
0772: buffer.append(svalue.substring(i, j + 1));
0773: buffer.append('"');
0774: i = j + 2;
0775: }
0776: }
0777:
0778: buffer.append('"');
0779: return buffer.toString();
0780:
0781: }
0782:
0783: /**
0784: * Close the currently open log file (if any)
0785: */
0786: private synchronized void close() {
0787:
0788: if (writer == null)
0789: return;
0790: writer.flush();
0791: writer.close();
0792: writer = null;
0793: currentLogFile = null;
0794:
0795: }
0796:
0797: /**
0798: * Log the specified message to the log file, switching files if the date
0799: * has changed since the previous log call.
0800: *
0801: * @param message Message to be logged
0802: * @param date the current Date object (so this method doesn't need to
0803: * create a new one)
0804: */
0805: private void log(String message, Date date) {
0806:
0807: if (rotatable) {
0808: // Only do a logfile switch check once a second, max.
0809: long systime = System.currentTimeMillis();
0810: if ((systime - rotationLastChecked) > 1000) {
0811:
0812: // We need a new currentDate
0813: currentDate = new Date(systime);
0814: rotationLastChecked = systime;
0815:
0816: // Check for a change of date
0817: String tsDate = fileDateFormatter.format(currentDate);
0818:
0819: // If the date has changed, switch log files
0820: if (!dateStamp.equals(tsDate)) {
0821: synchronized (this ) {
0822: if (!dateStamp.equals(tsDate)) {
0823: close();
0824: dateStamp = tsDate;
0825: open();
0826: }
0827: }
0828: }
0829: }
0830: }
0831:
0832: /* In case something external rotated the file instead */
0833: if (checkExists) {
0834: synchronized (this ) {
0835: if (currentLogFile != null && !currentLogFile.exists()) {
0836: try {
0837: close();
0838: } catch (Throwable e) {
0839: log.info("at least this wasn't swallowed", e);
0840: }
0841:
0842: /* Make sure date is correct */
0843: currentDate = new Date(System.currentTimeMillis());
0844: dateStamp = fileDateFormatter.format(currentDate);
0845:
0846: open();
0847: }
0848: }
0849: }
0850:
0851: // Log this message
0852: if (writer != null) {
0853: writer.println(message);
0854: }
0855:
0856: }
0857:
0858: /**
0859: * Open the new log file for the date specified by <code>dateStamp</code>.
0860: */
0861: private synchronized void open() {
0862:
0863: // Create the directory if necessary
0864: File dir = new File(directory);
0865: if (!dir.isAbsolute())
0866: dir = new File(System.getProperty("catalina.base"),
0867: directory);
0868: dir.mkdirs();
0869:
0870: // Open the current log file
0871: try {
0872: String pathname;
0873:
0874: // If no rotate - no need for dateStamp in fileName
0875: if (rotatable) {
0876: pathname = dir.getAbsolutePath() + File.separator
0877: + prefix + dateStamp + suffix;
0878: } else {
0879: pathname = dir.getAbsolutePath() + File.separator
0880: + prefix + suffix;
0881: }
0882:
0883: currentLogFile = new File(pathname);
0884: writer = new PrintWriter(new FileWriter(pathname, true),
0885: true);
0886: if (currentLogFile.length() == 0) {
0887: writer.println("#Fields: " + pattern);
0888: writer.println("#Version: 1.0");
0889: writer.println("#Software: "
0890: + ServerInfo.getServerInfo());
0891: }
0892:
0893: } catch (IOException e) {
0894: writer = null;
0895: currentLogFile = null;
0896: }
0897:
0898: }
0899:
0900: /**
0901: * This method returns a Date object that is accurate to within one
0902: * second. If a thread calls this method to get a Date and it's been
0903: * less than 1 second since a new Date was created, this method
0904: * simply gives out the same Date again so that the system doesn't
0905: * spend time creating Date objects unnecessarily.
0906: */
0907: private Date getDate(long systime) {
0908: /* Avoid extra call to System.currentTimeMillis(); */
0909: if (0 == systime) {
0910: systime = System.currentTimeMillis();
0911: }
0912:
0913: // Only create a new Date once per second, max.
0914: if ((systime - currentDate.getTime()) > 1000) {
0915: currentDate.setTime(systime);
0916: }
0917:
0918: return currentDate;
0919:
0920: }
0921:
0922: // ------------------------------------------------------ Lifecycle Methods
0923:
0924: /**
0925: * Add a lifecycle event listener to this component.
0926: *
0927: * @param listener The listener to add
0928: */
0929: public void addLifecycleListener(LifecycleListener listener) {
0930:
0931: lifecycle.addLifecycleListener(listener);
0932:
0933: }
0934:
0935: /**
0936: * Get the lifecycle listeners associated with this lifecycle. If this
0937: * Lifecycle has no listeners registered, a zero-length array is returned.
0938: */
0939: public LifecycleListener[] findLifecycleListeners() {
0940:
0941: return lifecycle.findLifecycleListeners();
0942:
0943: }
0944:
0945: /**
0946: * Remove a lifecycle event listener from this component.
0947: *
0948: * @param listener The listener to add
0949: */
0950: public void removeLifecycleListener(LifecycleListener listener) {
0951:
0952: lifecycle.removeLifecycleListener(listener);
0953:
0954: }
0955:
0956: /**
0957: * Prepare for the beginning of active use of the public methods of this
0958: * component. This method should be called after <code>configure()</code>,
0959: * and before any of the public methods of the component are utilized.
0960: *
0961: * @exception LifecycleException if this component detects a fatal error
0962: * that prevents this component from being used
0963: */
0964: public void start() throws LifecycleException {
0965:
0966: // Validate and update our current component state
0967: if (started)
0968: throw new LifecycleException(sm
0969: .getString("extendedAccessLogValve.alreadyStarted"));
0970: lifecycle.fireLifecycleEvent(START_EVENT, null);
0971: started = true;
0972:
0973: // Initialize the timeZone, Date formatters, and currentDate
0974: TimeZone tz = TimeZone.getTimeZone("GMT");
0975: dateFormatter = new SimpleDateFormat("yyyy-MM-dd");
0976: dateFormatter.setTimeZone(tz);
0977: timeFormatter = new SimpleDateFormat("HH:mm:ss");
0978: timeFormatter.setTimeZone(tz);
0979: currentDate = new Date(System.currentTimeMillis());
0980: if (fileDateFormat == null || fileDateFormat.length() == 0)
0981: fileDateFormat = "yyyy-MM-dd";
0982: fileDateFormatter = new SimpleDateFormat(fileDateFormat);
0983: dateStamp = fileDateFormatter.format(currentDate);
0984: timeTakenFormatter = new DecimalFormat("0.000");
0985:
0986: /* Everybody say ick ... ick */
0987: try {
0988: InetAddress inetAddress = InetAddress.getLocalHost();
0989: myIpAddress = inetAddress.getHostAddress();
0990: myDNSName = inetAddress.getHostName();
0991: } catch (Throwable e) {
0992: myIpAddress = "127.0.0.1";
0993: myDNSName = "localhost";
0994: }
0995:
0996: open();
0997:
0998: }
0999:
1000: /**
1001: * Gracefully terminate the active use of the public methods of this
1002: * component. This method should be the last one called on a given
1003: * instance of this component.
1004: *
1005: * @exception LifecycleException if this component detects a fatal error
1006: * that needs to be reported
1007: */
1008: public void stop() throws LifecycleException {
1009:
1010: // Validate and update our current component state
1011: if (!started)
1012: throw new LifecycleException(sm
1013: .getString("extendedAccessLogValve.notStarted"));
1014: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
1015: started = false;
1016:
1017: close();
1018:
1019: }
1020:
1021: /**
1022: * Decode the given pattern. Is public so a pattern may
1023: * allows to be validated.
1024: * @param fields The pattern to decode
1025: * @return null on error. Otherwise array of decoded fields
1026: */
1027: public FieldInfo[] decodePattern(String fields) {
1028:
1029: if (log.isDebugEnabled())
1030: log.debug("decodePattern, fields=" + fields);
1031:
1032: LinkedList list = new LinkedList();
1033:
1034: //Ignore leading whitespace.
1035: int i = 0;
1036: for (; i < fields.length()
1037: && Character.isWhitespace(fields.charAt(i)); i++)
1038: ;
1039:
1040: if (i >= fields.length()) {
1041: log.info("fields was just empty or whitespace");
1042: return null;
1043: }
1044:
1045: int j;
1046: while (i < fields.length()) {
1047: if (log.isDebugEnabled())
1048: log.debug("fields.substring(i)=" + fields.substring(i));
1049:
1050: FieldInfo currentFieldInfo = new FieldInfo();
1051:
1052: if (fields.startsWith("date", i)) {
1053: currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
1054: currentFieldInfo.location = FieldInfo.SPECIAL_DATE;
1055: i += "date".length();
1056: } else if (fields.startsWith("time-taken", i)) {
1057: currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
1058: currentFieldInfo.location = FieldInfo.SPECIAL_TIME_TAKEN;
1059: i += "time-taken".length();
1060: } else if (fields.startsWith("time", i)) {
1061: currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
1062: currentFieldInfo.location = FieldInfo.SPECIAL_TIME;
1063: i += "time".length();
1064: } else if (fields.startsWith("bytes", i)) {
1065: currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
1066: currentFieldInfo.location = FieldInfo.SPECIAL_BYTES;
1067: i += "bytes".length();
1068: } else if (fields.startsWith("cached", i)) {
1069: currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
1070: currentFieldInfo.location = FieldInfo.SPECIAL_CACHED;
1071: i += "cached".length();
1072: } else if (fields.startsWith("c-ip", i)) {
1073: currentFieldInfo.type = FieldInfo.DATA_CLIENT;
1074: currentFieldInfo.location = FieldInfo.FIELD_IP;
1075: i += "c-ip".length();
1076: } else if (fields.startsWith("c-dns", i)) {
1077: currentFieldInfo.type = FieldInfo.DATA_CLIENT;
1078: currentFieldInfo.location = FieldInfo.FIELD_DNS;
1079: i += "c-dns".length();
1080: } else if (fields.startsWith("s-ip", i)) {
1081: currentFieldInfo.type = FieldInfo.DATA_SERVER;
1082: currentFieldInfo.location = FieldInfo.FIELD_IP;
1083: i += "s-ip".length();
1084: } else if (fields.startsWith("s-dns", i)) {
1085: currentFieldInfo.type = FieldInfo.DATA_SERVER;
1086: currentFieldInfo.location = FieldInfo.FIELD_DNS;
1087: i += "s-dns".length();
1088: } else if (fields.startsWith("cs", i)) {
1089: i = decode(fields, i + 2, currentFieldInfo,
1090: FieldInfo.DATA_CLIENT_TO_SERVER);
1091: if (i < 0)
1092: return null;
1093: } else if (fields.startsWith("sc", i)) {
1094: i = decode(fields, i + 2, currentFieldInfo,
1095: FieldInfo.DATA_SERVER_TO_CLIENT);
1096: if (i < 0)
1097: return null;
1098: } else if (fields.startsWith("sr", i)) {
1099: i = decode(fields, i + 2, currentFieldInfo,
1100: FieldInfo.DATA_SERVER_TO_RSERVER);
1101: if (i < 0)
1102: return null;
1103: } else if (fields.startsWith("rs", i)) {
1104: i = decode(fields, i + 2, currentFieldInfo,
1105: FieldInfo.DATA_RSERVER_TO_SERVER);
1106: if (i < 0)
1107: return null;
1108: } else if (fields.startsWith("x", i)) {
1109: i = decodeAppSpecific(fields, i, currentFieldInfo);
1110: } else {
1111: // Unable to decode ...
1112: log.error("unable to decode with rest of chars being: "
1113: + fields.substring(i));
1114: return null;
1115: }
1116:
1117: // By this point we should have the field, get the whitespace
1118: j = i;
1119: for (; j < fields.length()
1120: && Character.isWhitespace(fields.charAt(j)); j++)
1121: ;
1122:
1123: if (j >= fields.length()) {
1124: if (j == i) {
1125: // Special case - end of string
1126: currentFieldInfo.postWhiteSpace = "";
1127: } else {
1128: currentFieldInfo.postWhiteSpace = fields
1129: .substring(i);
1130: i = j;
1131: }
1132: } else {
1133: currentFieldInfo.postWhiteSpace = fields
1134: .substring(i, j);
1135: i = j;
1136: }
1137:
1138: list.add(currentFieldInfo);
1139: }
1140:
1141: i = 0;
1142: FieldInfo[] f = new FieldInfo[list.size()];
1143: for (Iterator k = list.iterator(); k.hasNext();)
1144: f[i++] = (FieldInfo) k.next();
1145:
1146: if (log.isDebugEnabled())
1147: log.debug("finished decoding with length of: " + i);
1148:
1149: return f;
1150: }
1151:
1152: /**
1153: * Decode the cs or sc fields.
1154: * Returns negative on error.
1155: *
1156: * @param fields The pattern to decode
1157: * @param i The string index where we are decoding.
1158: * @param fieldInfo Where to store the results
1159: * @param type The type we are decoding.
1160: * @return -1 on error. Otherwise the new String index.
1161: */
1162: private int decode(String fields, int i, FieldInfo fieldInfo,
1163: short type) {
1164:
1165: if (fields.startsWith("-status", i)) {
1166: fieldInfo.location = FieldInfo.FIELD_STATUS;
1167: i += "-status".length();
1168: } else if (fields.startsWith("-comment", i)) {
1169: fieldInfo.location = FieldInfo.FIELD_COMMENT;
1170: i += "-comment".length();
1171: } else if (fields.startsWith("-uri-query", i)) {
1172: fieldInfo.location = FieldInfo.FIELD_URI_QUERY;
1173: i += "-uri-query".length();
1174: } else if (fields.startsWith("-uri-stem", i)) {
1175: fieldInfo.location = FieldInfo.FIELD_URI_STEM;
1176: i += "-uri-stem".length();
1177: } else if (fields.startsWith("-uri", i)) {
1178: fieldInfo.location = FieldInfo.FIELD_URI;
1179: i += "-uri".length();
1180: } else if (fields.startsWith("-method", i)) {
1181: fieldInfo.location = FieldInfo.FIELD_METHOD;
1182: i += "-method".length();
1183: } else if (fields.startsWith("(", i)) {
1184: fieldInfo.location = FieldInfo.FIELD_HEADER;
1185: i++; /* Move past the ( */
1186: int j = fields.indexOf(')', i);
1187: if (j == -1) { /* Not found */
1188: log.error("No closing ) found for in decode");
1189: return -1;
1190: }
1191: fieldInfo.value = fields.substring(i, j);
1192: i = j + 1; // Move pointer past ) */
1193: } else {
1194: log.error("The next characters couldn't be decoded: "
1195: + fields.substring(i));
1196: return -1;
1197: }
1198:
1199: fieldInfo.type = type;
1200: return i;
1201:
1202: }
1203:
1204: /**
1205: * Decode app specific log entry.
1206: *
1207: * Special fields are of the form:
1208: * x-C(...) - For cookie
1209: * x-A(...) - Value in servletContext
1210: * x-S(...) - Value in session
1211: * x-R(...) - Value in servletRequest
1212: * @param fields The pattern to decode
1213: * @param i The string index where we are decoding.
1214: * @param fieldInfo Where to store the results
1215: * @return -1 on error. Otherwise the new String index.
1216: */
1217: private int decodeAppSpecific(String fields, int i,
1218: FieldInfo fieldInfo) {
1219:
1220: fieldInfo.type = FieldInfo.DATA_APP_SPECIFIC;
1221: /* Move past 'x-' */
1222: i += 2;
1223:
1224: if (i >= fields.length()) {
1225: log.error("End of line reached before decoding x- param");
1226: return -1;
1227: }
1228:
1229: switch (fields.charAt(i)) {
1230: case 'A':
1231: fieldInfo.xType = FieldInfo.X_APP;
1232: break;
1233: case 'C':
1234: fieldInfo.xType = FieldInfo.X_COOKIE;
1235: break;
1236: case 'R':
1237: fieldInfo.xType = FieldInfo.X_REQUEST;
1238: break;
1239: case 'S':
1240: fieldInfo.xType = FieldInfo.X_SESSION;
1241: break;
1242: case 'H':
1243: fieldInfo.xType = FieldInfo.X_SERVLET_REQUEST;
1244: break;
1245: case 'P':
1246: fieldInfo.xType = FieldInfo.X_PARAMETER;
1247: break;
1248: default:
1249: return -1;
1250: }
1251:
1252: /* test that next char is a ( */
1253: if (i + 1 != fields.indexOf('(', i)) {
1254: log
1255: .error("x param in wrong format. Needs to be 'x-#(...)' read the docs!");
1256: return -1;
1257: }
1258: i += 2; /* Move inside of the () */
1259:
1260: /* Look for ending ) and return error if not found. */
1261: int j = fields.indexOf(')', i);
1262: if (j == -1) {
1263: log.error("x param in wrong format. No closing ')'!");
1264: return -1;
1265: }
1266:
1267: fieldInfo.value = fields.substring(i, j);
1268:
1269: if (fieldInfo.xType == FieldInfo.X_SERVLET_REQUEST) {
1270: if ("authType".equals(fieldInfo.value)) {
1271: fieldInfo.location = FieldInfo.X_LOC_AUTHTYPE;
1272: } else if ("remoteUser".equals(fieldInfo.value)) {
1273: fieldInfo.location = FieldInfo.X_LOC_REMOTEUSER;
1274: } else if ("requestedSessionId".equals(fieldInfo.value)) {
1275: fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONID;
1276: } else if ("requestedSessionIdFromCookie"
1277: .equals(fieldInfo.value)) {
1278: fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE;
1279: } else if ("requestedSessionIdValid"
1280: .equals(fieldInfo.value)) {
1281: fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONIDVALID;
1282: } else if ("contentLength".equals(fieldInfo.value)) {
1283: fieldInfo.location = FieldInfo.X_LOC_CONTENTLENGTH;
1284: } else if ("characterEncoding".equals(fieldInfo.value)) {
1285: fieldInfo.location = FieldInfo.X_LOC_CHARACTERENCODING;
1286: } else if ("locale".equals(fieldInfo.value)) {
1287: fieldInfo.location = FieldInfo.X_LOC_LOCALE;
1288: } else if ("protocol".equals(fieldInfo.value)) {
1289: fieldInfo.location = FieldInfo.X_LOC_PROTOCOL;
1290: } else if ("scheme".equals(fieldInfo.value)) {
1291: fieldInfo.location = FieldInfo.X_LOC_SCHEME;
1292: } else if ("secure".equals(fieldInfo.value)) {
1293: fieldInfo.location = FieldInfo.X_LOC_SECURE;
1294: } else {
1295: log
1296: .error("x param for servlet request, couldn't decode value: "
1297: + fieldInfo.location);
1298: return -1;
1299: }
1300: }
1301:
1302: return j + 1;
1303:
1304: }
1305:
1306: }
1307:
1308: /**
1309: * A simple helper for decoding the pattern.
1310: */
1311: class FieldInfo {
1312: /*
1313: The goal of the constants listed below is to make the construction of the log
1314: entry as quick as possible via numerci decodings of the methods to call instead
1315: of performing many String comparisons on each logging request.
1316: */
1317:
1318: /* Where the data is located. */
1319: static final short DATA_CLIENT = 0;
1320: static final short DATA_SERVER = 1;
1321: static final short DATA_REMOTE = 2;
1322: static final short DATA_CLIENT_TO_SERVER = 3;
1323: static final short DATA_SERVER_TO_CLIENT = 4;
1324: static final short DATA_SERVER_TO_RSERVER = 5; /* Here to honor the spec. */
1325: static final short DATA_RSERVER_TO_SERVER = 6; /* Here to honor the spec. */
1326: static final short DATA_APP_SPECIFIC = 7;
1327: static final short DATA_SPECIAL = 8;
1328:
1329: /* The type of special fields. */
1330: static final short SPECIAL_DATE = 1;
1331: static final short SPECIAL_TIME_TAKEN = 2;
1332: static final short SPECIAL_TIME = 3;
1333: static final short SPECIAL_BYTES = 4;
1334: static final short SPECIAL_CACHED = 5;
1335:
1336: /* Where to pull the data for prefixed values */
1337: static final short FIELD_IP = 1;
1338: static final short FIELD_DNS = 2;
1339: static final short FIELD_STATUS = 3;
1340: static final short FIELD_COMMENT = 4;
1341: static final short FIELD_METHOD = 5;
1342: static final short FIELD_URI = 6;
1343: static final short FIELD_URI_STEM = 7;
1344: static final short FIELD_URI_QUERY = 8;
1345: static final short FIELD_HEADER = 9;
1346:
1347: /* Application Specific parameters */
1348: static final short X_REQUEST = 1; /* For x app specific */
1349: static final short X_SESSION = 2; /* For x app specific */
1350: static final short X_COOKIE = 3; /* For x app specific */
1351: static final short X_APP = 4; /* For x app specific */
1352: static final short X_SERVLET_REQUEST = 5; /* For x app specific */
1353: static final short X_PARAMETER = 6; /* For x app specific */
1354:
1355: static final short X_LOC_AUTHTYPE = 1;
1356: static final short X_LOC_REMOTEUSER = 2;
1357: static final short X_LOC_REQUESTEDSESSIONID = 3;
1358: static final short X_LOC_REQUESTEDSESSIONIDFROMCOOKIE = 4;
1359: static final short X_LOC_REQUESTEDSESSIONIDVALID = 5;
1360: static final short X_LOC_CONTENTLENGTH = 6;
1361: static final short X_LOC_CHARACTERENCODING = 7;
1362: static final short X_LOC_LOCALE = 8;
1363: static final short X_LOC_PROTOCOL = 9;
1364: static final short X_LOC_SCHEME = 10;
1365: static final short X_LOC_SECURE = 11;
1366:
1367: /** The field type */
1368: short type;
1369:
1370: /** Where to pull the data from? Icky variable name. */
1371: short location;
1372:
1373: /** The x- specific place to pull the data from. */
1374: short xType;
1375:
1376: /** The field value if needed. Needed for headers and app specific. */
1377: String value;
1378:
1379: /** Any white space after this field? Put it here. */
1380: String postWhiteSpace = null;
1381:
1382: }
|