/*
 * Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.http.server.accesslog;

import static java.util.logging.Level.WARNING;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Logger;

import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.http.Cookie;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.Protocol;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;
import org.glassfish.grizzly.http.util.MimeHeaders;

An AccessLogFormat using a standard vaguely similar and heavily influenced by Apache's own custom access log formats.

As with Apache, the format string specified at construction should be composed of these tokens:

%% The literal percent sign (can also be escaped with back-slash, like "\%"
%a Remote IP-address
%A Local IP-address
%b Size of response in bytes, excluding HTTP headers, using "-" (a dash character) rather than a "0" (zero) when no bytes are sent
%B Size of response in bytes, excluding HTTP headers
%{Foobar}C The contents of cookie "Foobar" in the request sent to the server
%D The time taken to serve the request, in microseconds
%h Remote host name
%{local|remote}h Host name, either "local" or "remote"
%H The request protocol
%{Foobar}i The contents of the "Foobar: ..." header in the request
%m The request method
%{Foobar}o The contents of the "Foobar: ..." header in the response
%p Local port number
%{local|remote}p The port number, either "local" or "remote"
%q The query string, prepended with a "?" (question mark) if a query string exists, otherwise an empty string
%r First line of request, an alias to "%m %U%q %H"
%s Status code
%t The time the request was received, in standard English format (like "[09/Feb/2014:12:00:34 +0900]")
%{[format][@timezone]}t The time the request was received. Both format and timezone are optional
  • When "format" is left unspecified, the default %t format [yyyy/MMM/dd:HH:mm:ss Z] is used
  • When "format" is specified, the given pattern must be a valid SimpleDateFormat pattern.
  • When "@timezone" is left unspecified, the default time zone is used.
  • When "@timezone" is specified, the time zone will be looked up by time zone identifier (note that this will default to GMT if the specified identifier was not recognized).
When the "@" character needs to be used in the format, it must be escaped as "@@"
%T The time taken to serve the request, in seconds
%{...}T The time taken to serve the request. The parameter can be a time unit like:
  • "n", "nano[s]", "nanosec[s]", "nanosecond[s]"
  • "micro[s]", "microsec[s]", "microsecond[s]"
  • "m", "milli[s]", "millisec[s]", "millisecond[s]"
  • "s", "sec[s]", "second[s]"
%u The remote user name
%U The URL path requested, not including any query string
%v The name of the server which served the request
Author:Pier Fumagalli, USRZ.com
/** * An {@link AccessLogFormat} using a standard vaguely similar and heavily influenced by * <a href="http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#formats">Apache's own custom access log * formats</a>. * * <p> * As with Apache, the format string specified at {@linkplain #ApacheLogFormat(String) construction} should be composed * of these tokens: * </p> * * <table> * <tr> * <th><code>%%</code></th> * <td>The literal percent sign (can also be escaped with back-slash, like "<code>\%</code>"</td> * </tr> * <tr> * <th><code>%a</code></th> * <td>Remote IP-address</td> * </tr> * <tr> * <th><code>%A</code></th> * <td>Local IP-address</td> * </tr> * <tr> * <th><code>%b</code></th> * <td>Size of response in bytes, excluding HTTP headers, using "<code>-</code>" (a <em>dash</em> character) rather than * a "<code>0</code>" (<em>zero</em>) when no bytes are sent</td> * </tr> * <tr> * <th><code>%B</code></th> * <td>Size of response in bytes, excluding HTTP headers</td> * </tr> * <tr> * <th><code>%{Foobar}C</code></th> * <td>The contents of cookie "<code>Foobar</code>" in the request sent to the server</td> * </tr> * <tr> * <th><code>%D</code></th> * <td>The time taken to serve the request, in microseconds</td> * </tr> * <tr> * <th><code>%h</code></th> * <td>Remote host name</td> * </tr> * <tr> * <th><code>%{local|remote}h</code></th> * <td>Host name, either "<code>local</code>" or "<code>remote</code>"</td> * </tr> * <tr> * <th><code>%H</code></th> * <td>The request protocol</td> * </tr> * <tr> * <th><code>%{Foobar}i</code></th> * <td>The contents of the "<code>Foobar: ...</code>" header in the request</td> * </tr> * <tr> * <th><code>%m</code></th> * <td>The request method</td> * </tr> * <tr> * <th><code>%{Foobar}o</code></th> * <td>The contents of the "<code>Foobar: ...</code>" header in the response</td> * </tr> * <tr> * <th><code>%p</code></th> * <td>Local port number</td> * </tr> * <tr> * <th><code>%{local|remote}p</code></th> * <td>The port number, either "<code>local</code>" or "<code>remote</code>"</td> * </tr> * <tr> * <th><code>%q</code></th> * <td>The query string, prepended with a "<code>?</code>" (<em>question mark</em>) if a query string exists, otherwise * an empty string</td> * </tr> * <tr> * <th><code>%r</code></th> * <td>First line of request, an alias to "<code>%m %U%q %H</code>"</td> * </tr> * <tr> * <th><code>%s</code></th> * <td>Status code</td> * </tr> * <tr> * <th><code>%t</code></th> * <td>The time the request was received, in standard <em>English</em> format (like * "<code>[09/Feb/2014:12:00:34 +0900]</code>")</td> * </tr> * <tr> * <th><code>%{[format][@timezone]}t</code></th> * <td>The time the request was received. Both <em>format</em> and <em>timezone</em> are optional<br> * <ul> * <li>When "<code>format</code>" is left unspecified, the default <code>%t</code> format * <code>[yyyy/MMM/dd:HH:mm:ss Z]</code> is used</li> * <li>When "<code>format</code>" is specified, the given pattern <b>must</b> be a valid {@link SimpleDateFormat} * pattern.</li> * <li>When "<code>@timezone</code>" is left unspecified, the {@linkplain TimeZone#getDefault() default time zone} is * used.</li> * <li>When "<code>@timezone</code>" is specified, the time zone will be looked up by * {@linkplain TimeZone#getTimeZone(String) time zone identifier} (note that this will default to <em>GMT</em> if the * specified identifier was not recognized).</li> * </ul> * When the "<code>@</code>" character needs to be used in the <em>format</em>, it <b>must</b> be escaped as * "<em>@@</em>"</td> * </tr> * <tr> * <th><code>%T</code></th> * <td>The time taken to serve the request, in seconds</td> * </tr> * <tr> * <th><code>%{...}T</code></th> * <td>The time taken to serve the request. The parameter can be a time unit like: * <ul> * <li>"<code>n</code>", "<code>nano<em>[s]<em></code>", "<code>nanosec<em>[s]<em></code>", * "<code>nanosecond<em>[s]<em></code>"</li> * <li>"<code>micro<em>[s]<em></code>", "<code>microsec<em>[s]<em></code>", "<code>microsecond<em>[s]<em></code>"</li> * <li>"<code>m</code>", "<code>milli<em>[s]<em></code>", "<code>millisec<em>[s]<em></code>", * "<code>millisecond<em>[s]<em></code>"</li> * <li>"<code>s</code>", "<code>sec<em>[s]<em></code>", "<code>second<em>[s]<em></code>"</li> * </ul> * </td> * </tr> * <tr> * <th><code>%u</code></th> * <td>The remote user name</td> * </tr> * <tr> * <th><code>%U</code></th> * <td>The URL path requested, not including any query string</td> * </tr> * <tr> * <th><code>%v</code></th> * <td>The name of the server which served the request</td> * </tr> * </table> * * @author <a href="mailto:pier@usrz.com">Pier Fumagalli</a> * @author <a href="http://www.usrz.com/">USRZ.com</a> */
public class ApacheLogFormat implements AccessLogFormat { /* The UTC time zone */ private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
A String representing our version of Apache's common format.
/** A {@link String} representing our version of Apache's <em>common</em> format. */
public static final String COMMON_FORMAT = "%h - %u %t \"%r\" %s %b";
A String representing our version of Apache's combined format.
/** A {@link String} representing our version of Apache's <em>combined</em> format. */
public static final String COMBINED_FORMAT = "%h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"";
A String representing our version of Apache's common with virtual-hosts format.
/** A {@link String} representing our version of Apache's <em>common with virtual-hosts</em> format. */
public static final String VHOST_COMMON_FORMAT = "%v %h - %u %t \"%r\" %s %b";
A String representing our version of Apache's combined with virtual-hosts format.
/** A {@link String} representing our version of Apache's <em>combined with virtual-hosts</em> format. */
public static final String VHOST_COMBINED_FORMAT = "%v %h - %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"";
A String representing our version of Apache's referer format.
/** A {@link String} representing our version of Apache's <em>referer</em> format. */
public static final String REFERER_FORMAT = "%{Referer}i -> %U";
A String representing our version of Apache's user-agent format.
/** A {@link String} representing our version of Apache's <em>user-agent</em> format. */
public static final String AGENT_FORMAT = "%{User-agent}i";
A format compatible with Apache's common format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>common</em> format. */
public static final ApacheLogFormat COMMON = new ApacheLogFormat(COMMON_FORMAT);
A format compatible with Apache's combined format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>combined</em> format. */
public static final ApacheLogFormat COMBINED = new ApacheLogFormat(COMBINED_FORMAT);
A format compatible with Apache's common with virtual-hosts format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>common with virtual-hosts</em> format. */
public static final ApacheLogFormat VHOST_COMMON = new ApacheLogFormat(VHOST_COMMON_FORMAT);
A format compatible with Apache's combined with virtual-hosts format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>combined with virtual-hosts</em> format. */
public static final ApacheLogFormat VHOST_COMBINED = new ApacheLogFormat(VHOST_COMBINED_FORMAT);
A format compatible with Apache's referer format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>referer</em> format. */
public static final ApacheLogFormat REFERER = new ApacheLogFormat(REFERER_FORMAT);
A format compatible with Apache's user-agent format.
/** A {@linkplain ApacheLogFormat format} compatible with Apache's <em>user-agent</em> format. */
public static final ApacheLogFormat AGENT = new ApacheLogFormat(AGENT_FORMAT);
A format compatible with Apache's common format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>common</em> format set to use the <em>UTC</em> * {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat COMMON_UTC = new ApacheLogFormat(UTC, COMMON_FORMAT);
A format compatible with Apache's combined format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>combined</em> format set to use the <em>UTC</em> * {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat COMBINED_UTC = new ApacheLogFormat(UTC, COMBINED_FORMAT);
A format compatible with Apache's common with virtual-hosts format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>common with virtual-hosts</em> format set to use * the <em>UTC</em> {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat VHOST_COMMON_UTC = new ApacheLogFormat(UTC, VHOST_COMMON_FORMAT);
A format compatible with Apache's combined with virtual-hosts format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>combined with virtual-hosts</em> format set to use * the <em>UTC</em> {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat VHOST_COMBINED_UTC = new ApacheLogFormat(UTC, VHOST_COMBINED_FORMAT);
A format compatible with Apache's referer format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>referer</em> format set to use the <em>UTC</em> * {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat REFERER_UTC = new ApacheLogFormat(UTC, REFERER_FORMAT);
A format compatible with Apache's user-agent format set to use the UTC time zone.
/** * A {@linkplain ApacheLogFormat format} compatible with Apache's <em>user-agent</em> format set to use the <em>UTC</em> * {@linkplain TimeZone time zone}. */
public static final ApacheLogFormat AGENT_UTC = new ApacheLogFormat(UTC, AGENT_FORMAT); /* Log log log, never enough */ private static final Logger LOGGER = Grizzly.logger(HttpServer.class); /* Our list of fields for formatting */ private final List<Field> fields; /* Our timezone */ private final TimeZone timeZone;
Create a new ApacheLogFormat instance by parsing the format from the specified String.
/** * Create a new {@link ApacheLogFormat} instance by parsing the format from the specified {@link String}. */
public ApacheLogFormat(String format) { this(TimeZone.getDefault(), format); }
Create a new ApacheLogFormat instance by parsing the format from the specified String.
/** * Create a new {@link ApacheLogFormat} instance by parsing the format from the specified {@link String}. */
public ApacheLogFormat(TimeZone timeZone, String format) { if (timeZone == null) { throw new NullPointerException("Null time zone"); } fields = new ArrayList<>(); this.timeZone = timeZone; parse(format); } @Override public String format(Response response, Date timeStamp, long responseNanos) { final StringBuilder builder = new StringBuilder(); final Request request = response.getRequest(); for (Field field : fields) { try { field.format(builder, request, response, timeStamp, responseNanos); } catch (Exception exception) { LOGGER.log(WARNING, "Exception formatting access log entry", exception); builder.append('-'); } } return builder.toString(); } String unsafeFormat(Response response, Date timeStamp, long responseNanos) { final StringBuilder builder = new StringBuilder(); final Request request = response.getRequest(); for (Field field : fields) { field.format(builder, request, response, timeStamp, responseNanos); } return builder.toString(); }
Return the normalized format associated with this instance.
/** * Return the <em>normalized</em> format associated with this instance. */
public String getFormat() { final StringBuilder builder = new StringBuilder(); for (Field field : fields) { builder.append(field.toString()); } return builder.toString(); } /* ====================================================================== */ /* PARSING OF FORMAT STRINGS */ /* ====================================================================== */ private void parse(String format) { for (int x = 0; x < format.length(); x++) { switch (format.charAt(x)) { case '\\': x = parseEscape(format, x); break; case '%': x = parseFormat(format, null, x); break; default: addLiteral(format.charAt(x)); } } } private int parseFormat(String format, String parameter, int position) { if (++position < format.length()) { final char field = format.charAt(position); /* Initial check to see if parameter is supposed to be there */ if (parameter != null) { switch (field) { case 'C': break; // parameter is cookie name case 'h': break; // parameter for host ("local" or "remote") case 'i': break; // parameter is request header name case 'o': break; // parameter is response header name case 'p': break; // parameter for port ("local" or "remote") case 't': break; // parameter is simple date format's format case 'T': break; // parameter is scale ("nano", "micro", "milli" or number) default: throw new IllegalArgumentException( "Unsupported parameter \"" + parameter + "\" for field '" + field + "' in [" + format + "] at character " + position); } } switch (field) { case '{': return parseParameter(format, position); case '%': addLiteral('%'); break; // The percent sign case 'a': fields.add(new RemoteAddressField()); break; // Remote IP-address case 'A': fields.add(new LocalAddressField()); break; // Local IP-address case 'b': fields.add(new ResponseSizeField(false)); break; // Size of response in bytes in CLF format case 'B': fields.add(new ResponseSizeField(true)); break; // Size of response in bytes with zeroes /* */ case 'C': fields.add(new RequestCookieField(parameter)); break; // The contents of a cookie case 'D': fields.add(new ResponseTimeField("micro", format, position)); break; // The time taken to serve the request, in microseconds. /* */ case 'h': // Remote (or local if parameterized) host fields.add(parseLocal(parameter, false, field, format, position) ? new LocalHostField() : new RemoteHostField()); break; case 'H': fields.add(new RequestProtocolField()); break; // The request protocol /* */ case 'i': fields.add(new RequestHeaderField(parameter)); break; // A request header case 'm': fields.add(new RequestMethodField()); break; // The request method /* */ case 'o': fields.add(new ResponseHeaderField(parameter)); break; // A response header /* */ case 'p': // Local (or remote if parameterized) port fields.add(parseLocal(parameter, true, field, format, position) ? new LocalPortField() : new RemotePortField()); break; case 'q': fields.add(new RequestQueryField()); break; // The query string (prepended with a ?) case 'r': // First line of request, alias to "%m %U%q %H" fields.add(new RequestMethodField()); addLiteral(' '); fields.add(new RequestURIField()); fields.add(new RequestQueryField()); addLiteral(' '); fields.add(new RequestProtocolField()); break; case 's': fields.add(new ResponseStatusField()); break; // Status. /* */ case 't': fields.add(new RequestTimeField(parameter, timeZone)); break; // The time, in the form given by format, which should be in strftime(3) format. (potentially localized) /* */ case 'T': fields.add(new ResponseTimeField(parameter, format, position)); break; // The time taken to serve the request, in the scale specified. case 'u': fields.add(new RequestUserField()); break; // The URL path requested, not including any query string. case 'U': fields.add(new RequestURIField()); break; // The URL path requested, not including any query string. case 'v': fields.add(new ServerNameField()); break; // The canonical ServerName of the server serving the request. default: throw new IllegalArgumentException("Unsupported field '" + field + "' in [" + format + "] at character " + position); } return position; } throw new IllegalArgumentException("Unterminated field declaration in [" + format + "] at character " + position); } private boolean parseLocal(String parameter, boolean defaultValue, char field, String format, int position) { if (parameter == null) { return defaultValue; } final String p = parameter.trim().toLowerCase(); if (p.equals("local")) { return true; } else if (p.equals("remote")) { return false; } else { throw new IllegalArgumentException( "Unsupported parameter \"" + parameter + "\" for field '" + field + "' in [" + format + "] at character " + position); } } private int parseParameter(String format, int position) { if (++position < format.length()) { final int end = format.indexOf('}', position); if (end == position) { return parseFormat(format, null, end); } else if (end > position) { return parseFormat(format, format.substring(position, end), end); } } throw new IllegalArgumentException("Unterminated format parameter in [" + format + "] at character " + position); } private int parseEscape(String format, int position) { if (++position < format.length()) { final char escaped = format.charAt(position); switch (escaped) { case 't': addLiteral('\t'); break; case 'b': addLiteral('\b'); break; case 'n': addLiteral('\n'); break; case 'r': addLiteral('\r'); break; case 'f': addLiteral('\f'); break; default: addLiteral(escaped); } return position; } throw new IllegalArgumentException("Unterminated escape sequence in [" + format + "] at character " + position); } /* ====================================================================== */ private void addLiteral(char c) { /* See if we can add to the previuos literal field */ if (!fields.isEmpty()) { final Field last = fields.get(fields.size() - 1); if (last instanceof LiteralField) { ((LiteralField) last).append(c); return; } } /* List empty or last field was not a literal, add a new one */ fields.add(new LiteralField(c)); } /* ====================================================================== */ /* FIELD DECLARATIONS */ /* ====================================================================== */ private static abstract class Field { abstract StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos); @Override public abstract String toString(); } private static abstract class AbstractField extends Field { private final char format; private final String parameter; protected AbstractField(char format) { this(format, null); } protected AbstractField(char format, String parameter) { this.format = format; this.parameter = parameter; } @Override public final String toString() { final StringBuilder builder = new StringBuilder().append('%'); if (parameter != null) { builder.append('{').append(parameter).append('}'); } return builder.append(format).toString(); } } /* ====================================================================== */ private abstract static class HeaderField extends AbstractField { final String name; HeaderField(char format, String name) { super(format, name.trim().toLowerCase()); this.name = name.trim().toLowerCase(); } StringBuilder format(StringBuilder builder, MimeHeaders headers) { final Iterator<String> iterator = headers.values(name).iterator(); if (iterator.hasNext()) { builder.append(iterator.next()); } while (iterator.hasNext()) { builder.append("; ").append(iterator.next()); } return builder; } } /* ====================================================================== */ private static class LiteralField extends Field { final StringBuilder contents; LiteralField(char character) { contents = new StringBuilder().append(character); } void append(char character) { contents.append(character); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { return builder.append(contents); } @Override public String toString() { final StringBuilder builder = new StringBuilder(); for (int x = 0; x < contents.length(); x++) { final char character = contents.charAt(x); /* Escape \t \b \n \r \f and %% */ switch (character) { case 't': case 'b': case 'n': case 'r': case 'f': builder.append('\\'); break; case '%': builder.append('%'); break; } builder.append(character); } return builder.toString(); } } /* ====================================================================== */ private static class ServerNameField extends AbstractField { ServerNameField() { super('v'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String name = request.getServerName(); return builder.append(name == null ? "-" : name); } } /* ====================================================================== */ private static class LocalHostField extends AbstractField { LocalHostField() { super('h', "local"); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String host = request.getLocalName(); return builder.append(host == null ? "-" : host); } } /* ====================================================================== */ private static class LocalAddressField extends AbstractField { LocalAddressField() { super('A'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String address = request.getLocalAddr(); return builder.append(address == null ? "-" : address); } } /* ====================================================================== */ private static class LocalPortField extends AbstractField { LocalPortField() { super('p'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final int port = request.getLocalPort(); return builder.append(port < 1 ? "-" : port); } } /* ====================================================================== */ private static class RemoteHostField extends AbstractField { RemoteHostField() { super('h'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String host = request.getRemoteHost(); return builder.append(host == null ? "-" : host); } } /* ====================================================================== */ private static class RemoteAddressField extends AbstractField { RemoteAddressField() { super('a'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String address = request.getRemoteAddr(); return builder.append(address == null ? "-" : address); } } /* ====================================================================== */ private static class RemotePortField extends AbstractField { RemotePortField() { super('p', "remote"); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final int port = request.getRemotePort(); return builder.append(port < 1 ? "-" : port); } } /* ====================================================================== */ private static class RequestTimeField extends Field { private static final String DEFAULT_PATTERN = "[yyyy/MMM/dd:HH:mm:ss Z]"; private final SimpleDateFormatThreadLocal simpleDateFormat; private final TimeZone timeZone; private final String pattern; private final String format; RequestTimeField(String format, TimeZone zone) { this.format = format; if (format == null) { pattern = DEFAULT_PATTERN; timeZone = zone; } else { /* Check for timezone separation */ final int pos = format.lastIndexOf('@'); if (pos < 0 || pos > 0 && format.charAt(pos - 1) == '@') { /* There is no '@' or the last '@' is actually an '@@' */ pattern = format.replace("@@", "@"); timeZone = zone; } else if (pos == 0) { /* We have *ONLY* a time zone specified */ pattern = DEFAULT_PATTERN; timeZone = TimeZone.getTimeZone(format.substring(1)); } else { /* We have both format and time zone */ pattern = format.substring(0, pos).replace("@@", "@"); timeZone = TimeZone.getTimeZone(format.substring(pos + 1)); } } /* Get our simple date format */ simpleDateFormat = new SimpleDateFormatThreadLocal(pattern); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { if (timeStamp == null) { return builder.append('-'); } final SimpleDateFormat format = simpleDateFormat.get(); format.setTimeZone(timeZone); return builder.append(format.format(timeStamp)); } @Override public String toString() { return format == null ? "%t" : "%{" + format + "}t"; } } /* ====================================================================== */ private static class RequestMethodField extends AbstractField { RequestMethodField() { super('m'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final Method method = request.getMethod(); return builder.append(method == null ? "-" : method.toString()); } } /* ====================================================================== */ private static class RequestUserField extends AbstractField { RequestUserField() { super('u'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String user = request.getRemoteUser(); return builder.append(user == null ? "-" : user); } } /* ====================================================================== */ private static class RequestURIField extends AbstractField { RequestURIField() { super('U'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String uri = request.getRequestURI(); return builder.append(uri == null ? "-" : uri); } } /* ====================================================================== */ private static class RequestQueryField extends AbstractField { RequestQueryField() { super('q'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final String query = request.getQueryString(); if (query != null) { builder.append('?').append(query); } return builder; } } /* ====================================================================== */ private static class RequestProtocolField extends AbstractField { RequestProtocolField() { super('H'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final Protocol protocol = request.getProtocol(); if (protocol == null) { return builder.append("-"); } switch (protocol) { case HTTP_0_9: return builder.append("HTTP/0.9"); case HTTP_1_0: return builder.append("HTTP/1.0"); case HTTP_1_1: return builder.append("HTTP/1.1"); default: return builder.append("-"); } } } /* ====================================================================== */ private static class RequestHeaderField extends HeaderField { RequestHeaderField(String name) { super('i', name); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { return this.format(builder, request.getRequest().getHeaders()); } } /* ====================================================================== */ private static class RequestCookieField extends AbstractField { final String name; RequestCookieField(String name) { super('C', name.trim().toLowerCase()); this.name = name.trim().toLowerCase(); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (name.equals(cookie.getName().toLowerCase())) { return builder.append(cookie.getValue()); } } } return builder; } } /* ====================================================================== */ private static class ResponseStatusField extends AbstractField { ResponseStatusField() { super('s'); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final int status = response.getStatus(); if (status < 10) { builder.append('0'); } if (status < 100) { builder.append('0'); } return builder.append(status); } } /* ====================================================================== */ private static class ResponseSizeField extends AbstractField { final String zero; ResponseSizeField(boolean zero) { super(zero ? 'B' : 'b'); this.zero = zero ? "0" : "-"; } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { final long size = response.getContentLengthLong(); return builder.append(size < 1 ? zero : Long.toString(size)); } } /* ====================================================================== */ private static class ResponseTimeField extends Field { private final long scale; ResponseTimeField(String unit, String format, int position) { /* Figure out the scale */ if (unit == null) { scale = 1000000000; return; } String s = unit.trim().toLowerCase(); if (s.equals("n") || s.equals("nano") || s.equals("nanos") || s.equals("nanosec") || s.equals("nanosecs") || s.equals("nanosecond") || s.equals("nanoseconds")) { scale = 1; } else if (s.equals("micro") || s.equals("micros") || s.equals("microsec") || s.equals("microsecs") || s.equals("microsecond") || s.equals("microseconds")) { scale = 1000; } else if (s.equals("m") || s.equals("milli") || s.equals("millis") || s.equals("millisec") || s.equals("millisecs") || s.equals("millisecond") || s.equals("milliseconds")) { scale = 1000000; } else if (s.equals("s") || s.equals("sec") || s.equals("secs") || s.equals("second") || s.equals("seconds")) { scale = 1000000000; } else { throw new IllegalArgumentException("Unsupported time unit \"" + unit + "\" for field 'T' in [" + format + "] at character " + position); } } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { if (responseNanos < 0) { return builder.append('-'); } return builder.append(responseNanos / scale); } @Override public String toString() { final StringBuilder string = new StringBuilder().append('%'); if (scale == 1) { string.append("{n}T"); } else if (scale == 1000) { string.append('D'); } else if (scale == 1000000) { string.append("{m}T"); } else if (scale == 1000000000) { string.append('T'); } else { string.append('{').append(scale).append("}T"); } return string.toString(); } } /* ====================================================================== */ private static class ResponseHeaderField extends HeaderField { ResponseHeaderField(String name) { super('o', name); } @Override StringBuilder format(StringBuilder builder, Request request, Response response, Date timeStamp, long responseNanos) { return this.format(builder, response.getResponse().getHeaders()); } } }