/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.catalina.valves;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;

import org.apache.catalina.AccessLog;
import org.apache.catalina.Globals;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardHost;
import org.apache.coyote.RequestInfo;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.B2CConverter;

import cn.how2j.lazycat.util.CatalinaUtil;

/**
 * <p>
 * Implementation of the <b>Valve</b> interface that generates a web server
 * access log with the detailed line contents matching a configurable pattern.
 * The syntax of the available patterns is similar to that supported by the
 * <a href="http://httpd.apache.org/">Apache HTTP Server</a>
 * <code>mod_log_config</code> module. As an additional feature, automatic
 * rollover of log files when the date changes is also supported.
 * </p>
 *
 * <p>
 * Patterns for the logged message may include constant text or any of the
 * following replacement strings, for which the corresponding information from
 * the specified Response is substituted:
 * </p>
 * <ul>
 * <li><b>%a</b> - Remote IP address
 * <li><b>%A</b> - Local IP address
 * <li><b>%b</b> - Bytes sent, excluding HTTP headers, or '-' if no bytes were
 * sent
 * <li><b>%B</b> - Bytes sent, excluding HTTP headers
 * <li><b>%h</b> - Remote host name (or IP address if <code>enableLookups</code>
 * for the connector is false)
 * <li><b>%H</b> - Request protocol
 * <li><b>%l</b> - Remote logical username from identd (always returns '-')
 * <li><b>%m</b> - Request method
 * <li><b>%p</b> - Local port
 * <li><b>%q</b> - Query string (prepended with a '?' if it exists, otherwise an
 * empty string
 * <li><b>%r</b> - First line of the request
 * <li><b>%s</b> - HTTP status code of the response
 * <li><b>%S</b> - User session ID
 * <li><b>%t</b> - Date and time, in Common Log Format format
 * <li><b>%u</b> - Remote user that was authenticated
 * <li><b>%U</b> - Requested URL path
 * <li><b>%v</b> - Local server name
 * <li><b>%D</b> - Time taken to process the request, in millis
 * <li><b>%T</b> - Time taken to process the request, in seconds
 * <li><b>%I</b> - current Request thread name (can compare later with
 * stacktraces)
 * </ul>
 * <p>
 * In addition, the caller can specify one of the following aliases for commonly
 * utilized patterns:
 * </p>
 * <ul>
 * <li><b>common</b> - <code>%h %l %u %t "%r" %s %b</code>
 * <li><b>combined</b> -
 * <code>%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"</code>
 * </ul>
 *
 * <p>
 * There is also support to write information from the cookie, incoming header,
 * the Session or something else in the ServletRequest.<br>
 * It is modeled after the <a href="http://httpd.apache.org/">Apache HTTP
 * Server</a> log configuration syntax:
 * </p>
 * <ul>
 * <li><code>%{xxx}i</code> for incoming headers
 * <li><code>%{xxx}o</code> for outgoing response headers
 * <li><code>%{xxx}c</code> for a specific cookie
 * <li><code>%{xxx}r</code> xxx is an attribute in the ServletRequest
 * <li><code>%{xxx}s</code> xxx is an attribute in the HttpSession
 * <li><code>%{xxx}t</code> xxx is an enhanced SimpleDateFormat pattern (see
 * Configuration Reference document for details on supported time patterns)
 * </ul>
 *
 * <p>
 * Log rotation can be on or off. This is dictated by the <code>rotatable</code>
 * property.
 * </p>
 *
 * <p>
 * For UNIX users, another field called <code>checkExists</code> is also
 * available. If set to true, the log file's existence will be checked before
 * each logging. This way an external log rotator can move the file somewhere
 * and Tomcat will start with a new file.
 * </p>
 *
 * <p>
 * For JMX junkies, a public method called <code>rotate</code> has been made
 * available to allow you to tell this instance to move the existing log file to
 * somewhere else and start writing a new log file.
 * </p>
 *
 * <p>
 * Conditional logging is also supported. This can be done with the
 * <code>conditionUnless</code> and <code>conditionIf</code> properties. If the
 * value returned from ServletRequest.getAttribute(conditionUnless) yields a
 * non-null value, the logging will be skipped. If the value returned from
 * ServletRequest.getAttribute(conditionIf) yields the null value, the logging
 * will be skipped. The <code>condition</code> attribute is synonym for
 * <code>conditionUnless</code> and is provided for backwards compatibility.
 * </p>
 *
 * <p>
 * For extended attributes coming from a getAttribute() call, it is you
 * responsibility to ensure there are no newline or control characters.
 * </p>
 *
 * @author Craig R. McClanahan
 * @author Jason Brittain
 * @author Remy Maucherat
 * @author Takayuki Kaneko
 * @author Peter Rossbach
 */
public class AccessLogValve extends ValveBase implements AccessLog {

	private static final Log log = LogFactory.getLog(AccessLogValve.class);

	/**
	 * The list of our format types.
	 */
	private static enum FormatType {
		CLF, SEC, MSEC, MSEC_FRAC, SDF
	}

	/**
	 * The list of our port types.
	 */
	private static enum PortType {
		LOCAL, REMOTE
	}

	// ------------------------------------------------------ Constructor
	public AccessLogValve() {
		super(true);
	}

	// ----------------------------------------------------- Instance Variables

	/**
	 * The as-of date for the currently open log file, or a zero-length string
	 * if there is no open log file.
	 */
	private volatile String dateStamp = "";

	/**
	 * The directory in which log files are created.
	 */
	private String directory = "logs";

	/**
	 * The descriptive information about this implementation.
	 */
	protected static final String info = "org.apache.catalina.valves.AccessLogValve/2.2";

	/**
	 * enabled this component
	 */
	protected boolean enabled = true;

	/**
	 * The pattern used to format our access log lines.
	 */
	protected String pattern = null;

	/**
	 * The prefix that is added to log file filenames.
	 */
	protected String prefix = "access_log.";

	/**
	 * Should we rotate our log file? Default is true (like old behavior)
	 */
	protected boolean rotatable = true;

	/**
	 * Should we defer inclusion of the date stamp in the file name until rotate
	 * time? Default is false.
	 */
	protected boolean renameOnRotate = false;

	/**
	 * Buffered logging.
	 */
	private boolean buffered = true;

	/**
	 * The suffix that is added to log file filenames.
	 */
	protected String suffix = "";

	/**
	 * The PrintWriter to which we are currently logging, if any.
	 */
	protected PrintWriter writer = null;

	/**
	 * A date formatter to format a Date using the format given by
	 * <code>fileDateFormat</code>.
	 */
	protected SimpleDateFormat fileDateFormatter = null;

	/**
	 * The size of our global date format cache
	 */
	private static final int globalCacheSize = 300;

	/**
	 * The size of our thread local date format cache
	 */
	private static final int localCacheSize = 60;

	/**
	 * The current log file we are writing to. Helpful when checkExists is true.
	 */
	protected File currentLogFile = null;

	/**
	 * <p>
	 * Cache structure for formatted timestamps based on seconds.
	 * </p>
	 *
	 * <p>
	 * The cache consists of entries for a consecutive range of seconds. The
	 * length of the range is configurable. It is implemented based on a cyclic
	 * buffer. New entries shift the range.
	 * </p>
	 *
	 * <p>
	 * There is one cache for the CLF format (the access log standard format)
	 * and a HashMap of caches for additional formats used by SimpleDateFormat.
	 * </p>
	 *
	 * <p>
	 * Although the cache supports specifying a locale when retrieving a
	 * formatted timestamp, each format will always use the locale given when
	 * the format was first used. New locales can only be used for new formats.
	 * The CLF format will always be formatted using the locale
	 * <code>en_US</code>.
	 * </p>
	 *
	 * <p>
	 * The cache is not threadsafe. It can be used without synchronization via
	 * thread local instances, or with synchronization as a global cache.
	 * </p>
	 *
	 * <p>
	 * The cache can be created with a parent cache to build a cache hierarchy.
	 * Access to the parent cache is threadsafe.
	 * </p>
	 *
	 * <p>
	 * This class uses a small thread local first level cache and a bigger
	 * synchronized global second level cache.
	 * </p>
	 */
	protected static class DateFormatCache {

		protected class Cache {

			/* CLF log format */
			private static final String cLFFormat = "dd/MMM/yyyy:HH:mm:ss Z";

			/* Second used to retrieve CLF format in most recent invocation */
			private long previousSeconds = Long.MIN_VALUE;
			/* Value of CLF format retrieved in most recent invocation */
			private String previousFormat = "";

			/* First second contained in cache */
			private long first = Long.MIN_VALUE;
			/* Last second contained in cache */
			private long last = Long.MIN_VALUE;
			/* Index of "first" in the cyclic cache */
			private int offset = 0;
			/* Helper object to be able to call SimpleDateFormat.format(). */
			private final Date currentDate = new Date();

			protected final String cache[];
			private SimpleDateFormat formatter;
			private boolean isCLF = false;

			private Cache parent = null;

			private Cache(Cache parent) {
				this(null, parent);
			}

			private Cache(String format, Cache parent) {
				this(format, null, parent);
			}

			private Cache(String format, Locale loc, Cache parent) {
				cache = new String[cacheSize];
				for (int i = 0; i < cacheSize; i++) {
					cache[i] = null;
				}
				if (loc == null) {
					loc = cacheDefaultLocale;
				}
				if (format == null) {
					isCLF = true;
					format = cLFFormat;
					formatter = new SimpleDateFormat(format, Locale.US);
				} else {
					formatter = new SimpleDateFormat(format, loc);
				}
				formatter.setTimeZone(TimeZone.getDefault());
				this.parent = parent;
			}

			private String getFormatInternal(long time) {

				long seconds = time / 1000;

				/*
				 * First step: if we have seen this timestamp during the
				 * previous call, and we need CLF, return the previous value.
				 */
				if (seconds == previousSeconds) {
					return previousFormat;
				}

				/* Second step: Try to locate in cache */
				previousSeconds = seconds;
				int index = (offset + (int) (seconds - first)) % cacheSize;
				if (index < 0) {
					index += cacheSize;
				}
				if (seconds >= first && seconds <= last) {
					if (cache[index] != null) {
						/* Found, so remember for next call and return. */
						previousFormat = cache[index];
						return previousFormat;
					}

					/*
					 * Third step: not found in cache, adjust cache and add item
					 */
				} else if (seconds >= last + cacheSize || seconds <= first - cacheSize) {
					first = seconds;
					last = first + cacheSize - 1;
					index = 0;
					offset = 0;
					for (int i = 1; i < cacheSize; i++) {
						cache[i] = null;
					}
				} else if (seconds > last) {
					for (int i = 1; i < seconds - last; i++) {
						cache[(index + cacheSize - i) % cacheSize] = null;
					}
					first = seconds - (cacheSize - 1);
					last = seconds;
					offset = (index + 1) % cacheSize;
				} else if (seconds < first) {
					for (int i = 1; i < first - seconds; i++) {
						cache[(index + i) % cacheSize] = null;
					}
					first = seconds;
					last = seconds + (cacheSize - 1);
					offset = index;
				}

				/*
				 * Last step: format new timestamp either using parent cache or
				 * locally.
				 */
				if (parent != null) {
					synchronized (parent) {
						previousFormat = parent.getFormatInternal(time);
					}
				} else {
					currentDate.setTime(time);
					previousFormat = formatter.format(currentDate);
					if (isCLF) {
						StringBuilder current = new StringBuilder(32);
						current.append('[');
						current.append(previousFormat);
						current.append(']');
						previousFormat = current.toString();
					}
				}
				cache[index] = previousFormat;
				return previousFormat;
			}
		}

		/* Number of cached entries */
		private int cacheSize = 0;

		private final Locale cacheDefaultLocale;
		private final DateFormatCache parent;
		protected final Cache cLFCache;
		private final HashMap<String, Cache> formatCache = new HashMap<String, Cache>();

		protected DateFormatCache(int size, Locale loc, DateFormatCache parent) {
			cacheSize = size;
			cacheDefaultLocale = loc;
			this.parent = parent;
			Cache parentCache = null;
			if (parent != null) {
				synchronized (parent) {
					parentCache = parent.getCache(null, null);
				}
			}
			cLFCache = new Cache(parentCache);
		}

		private Cache getCache(String format, Locale loc) {
			Cache cache;
			if (format == null) {
				cache = cLFCache;
			} else {
				cache = formatCache.get(format);
				if (cache == null) {
					Cache parentCache = null;
					if (parent != null) {
						synchronized (parent) {
							parentCache = parent.getCache(format, loc);
						}
					}
					cache = new Cache(format, loc, parentCache);
					formatCache.put(format, cache);
				}
			}
			return cache;
		}

		public String getFormat(long time) {
			return cLFCache.getFormatInternal(time);
		}

		public String getFormat(String format, Locale loc, long time) {
			return getCache(format, loc).getFormatInternal(time);
		}
	}

	/**
	 * Global date format cache.
	 */
	private static final DateFormatCache globalDateCache = new DateFormatCache(globalCacheSize, Locale.getDefault(),
			null);

	/**
	 * Thread local date format cache.
	 */
	private static final ThreadLocal<DateFormatCache> localDateCache = new ThreadLocal<DateFormatCache>() {
		@Override
		protected DateFormatCache initialValue() {
			return new DateFormatCache(localCacheSize, Locale.getDefault(), globalDateCache);
		}
	};

	/**
	 * The system time when we last updated the Date that this valve uses for
	 * log lines.
	 */
	private static final ThreadLocal<Date> localDate = new ThreadLocal<Date>() {
		@Override
		protected Date initialValue() {
			return new Date();
		}
	};

	/**
	 * Resolve hosts.
	 */
	private boolean resolveHosts = false;

	/**
	 * Instant when the log daily rotation was last checked.
	 */
	private volatile long rotationLastChecked = 0L;

	/**
	 * Do we check for log file existence? Helpful if an external agent renames
	 * the log file so we can automagically recreate it.
	 */
	private boolean checkExists = false;

	/**
	 * Are we doing conditional logging. default null. It is the value of
	 * <code>conditionUnless</code> property.
	 */
	protected String condition = null;

	/**
	 * Are we doing conditional logging. default null. It is the value of
	 * <code>conditionIf</code> property.
	 */
	protected String conditionIf = null;

	/**
	 * Date format to place in log file name.
	 */
	protected String fileDateFormat = "yyyy-MM-dd";

	/**
	 * Name of locale used to format timestamps in log entries and in log file
	 * name suffix.
	 */
	protected String localeName = Locale.getDefault().toString();

	/**
	 * Locale used to format timestamps in log entries and in log file name
	 * suffix.
	 */
	protected Locale locale = Locale.getDefault();

	/**
	 * Character set used by the log file. If it is <code>null</code>, the
	 * system default character set will be used. An empty string will be
	 * treated as <code>null</code> when this property is assigned.
	 */
	protected String encoding = null;

	/**
	 * Array of AccessLogElement, they will be used to make log message.
	 */
	protected AccessLogElement[] logElements = null;

	/**
	 * Should this valve set request attributes for IP address, hostname,
	 * protocol and port used for the request. Default is <code>false</code>.
	 * 
	 * @see #setRequestAttributesEnabled(boolean)
	 */
	protected boolean requestAttributesEnabled = false;

	// ------------------------------------------------------------- Properties

	/**
	 * @return Returns the enabled.
	 */
	public boolean getEnabled() {
		return enabled;
	}

	/**
	 * {@inheritDoc} Default is <code>false</code>.
	 */
	@Override
	public void setRequestAttributesEnabled(boolean requestAttributesEnabled) {
		this.requestAttributesEnabled = requestAttributesEnabled;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean getRequestAttributesEnabled() {
		return requestAttributesEnabled;
	}

	/**
	 * @param enabled
	 *            The enabled to set.
	 */
	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	/**
	 * Return the directory in which we create log files.
	 */
	public String getDirectory() {
		return (directory);
	}

	/**
	 * Set the directory in which we create log files.
	 *
	 * @param directory
	 *            The new log file directory
	 */
	public void setDirectory(String directory) {
		this.directory = directory;
	}

	/**
	 * Return descriptive information about this implementation.
	 */
	@Override
	public String getInfo() {
		return (info);
	}

	/**
	 * Return the format pattern.
	 */
	public String getPattern() {
		return (this.pattern);
	}

	/**
	 * Set the format pattern, first translating any recognized alias.
	 *
	 * @param pattern
	 *            The new pattern
	 */
	public void setPattern(String pattern) {
		if (pattern == null) {
			this.pattern = "";
		} else if (pattern.equals(Constants.AccessLog.COMMON_ALIAS)) {
			this.pattern = Constants.AccessLog.COMMON_PATTERN;
		} else if (pattern.equals(Constants.AccessLog.COMBINED_ALIAS)) {
			this.pattern = Constants.AccessLog.COMBINED_PATTERN;
		} else {
			this.pattern = pattern;
		}
		logElements = createLogElements();
	}

	/**
	 * Check for file existence before logging.
	 */
	public boolean isCheckExists() {

		return checkExists;

	}

	/**
	 * Set whether to check for log file existence before logging.
	 *
	 * @param checkExists
	 *            true meaning to check for file existence.
	 */
	public void setCheckExists(boolean checkExists) {

		this.checkExists = checkExists;

	}

	/**
	 * Return the log file prefix.
	 */
	public String getPrefix() {
		return (prefix);
	}

	/**
	 * Set the log file prefix.
	 *
	 * @param prefix
	 *            The new log file prefix
	 */
	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}

	/**
	 * Should we rotate the access log.
	 *
	 * @return <code>true</code> if the access log should be rotated
	 */
	public boolean isRotatable() {
		return rotatable;
	}

	/**
	 * Configure whether the access log should be rotated.
	 *
	 * @param rotatable
	 *            true if the log should be rotated
	 */
	public void setRotatable(boolean rotatable) {
		this.rotatable = rotatable;
	}

	/**
	 * Should we defer inclusion of the date stamp in the file name until rotate
	 * time
	 */
	public boolean isRenameOnRotate() {
		return renameOnRotate;
	}

	/**
	 * Set the value if we should defer inclusion of the date stamp in the file
	 * name until rotate time
	 *
	 * @param renameOnRotate
	 *            true if defer inclusion of date stamp
	 */
	public void setRenameOnRotate(boolean renameOnRotate) {
		this.renameOnRotate = renameOnRotate;
	}

	/**
	 * Is the logging buffered
	 */
	public boolean isBuffered() {
		return buffered;
	}

	/**
	 * Set the value if the logging should be buffered
	 *
	 * @param buffered
	 *            true if buffered.
	 */
	public void setBuffered(boolean buffered) {
		this.buffered = buffered;
	}

	/**
	 * Return the log file suffix.
	 */
	public String getSuffix() {
		return (suffix);
	}

	/**
	 * Set the log file suffix.
	 *
	 * @param suffix
	 *            The new log file suffix
	 */
	public void setSuffix(String suffix) {
		this.suffix = suffix;
	}

	/**
	 * Set the resolve hosts flag.
	 *
	 * @param resolveHosts
	 *            The new resolve hosts value
	 * @deprecated Unused, removed in Tomcat 8. See
	 *             org.apache.catalina.connector.Connector.setEnableLookups(
	 *             boolean).
	 */
	@Deprecated
	public void setResolveHosts(boolean resolveHosts) {
		this.resolveHosts = resolveHosts;
	}

	/**
	 * Get the value of the resolve hosts flag.
	 * 
	 * @deprecated Unused, removed in Tomcat 8. See
	 *             org.apache.catalina.connector.Connector.setEnableLookups(
	 *             boolean).
	 */
	@Deprecated
	public boolean isResolveHosts() {
		return resolveHosts;
	}

	/**
	 * Return whether the attribute name to look for when performing conditional
	 * logging. If null, every request is logged.
	 */
	public String getCondition() {
		return condition;
	}

	/**
	 * Set the ServletRequest.attribute to look for to perform conditional
	 * logging. Set to null to log everything.
	 *
	 * @param condition
	 *            Set to null to log everything
	 */
	public void setCondition(String condition) {
		this.condition = condition;
	}

	/**
	 * Return whether the attribute name to look for when performing conditional
	 * logging. If null, every request is logged.
	 */
	public String getConditionUnless() {
		return getCondition();
	}

	/**
	 * Set the ServletRequest.attribute to look for to perform conditional
	 * logging. Set to null to log everything.
	 *
	 * @param condition
	 *            Set to null to log everything
	 */
	public void setConditionUnless(String condition) {
		setCondition(condition);
	}

	/**
	 * Return whether the attribute name to look for when performing conditional
	 * logging. If null, every request is logged.
	 */
	public String getConditionIf() {
		return conditionIf;
	}

	/**
	 * Set the ServletRequest.attribute to look for to perform conditional
	 * logging. Set to null to log everything.
	 *
	 * @param condition
	 *            Set to null to log everything
	 */
	public void setConditionIf(String condition) {
		this.conditionIf = condition;
	}

	/**
	 * Return the date format date based log rotation.
	 */
	public String getFileDateFormat() {
		return fileDateFormat;
	}

	/**
	 * Set the date format date based log rotation.
	 */
	public void setFileDateFormat(String fileDateFormat) {
		String newFormat;
		if (fileDateFormat == null) {
			newFormat = "";
		} else {
			newFormat = fileDateFormat;
		}
		this.fileDateFormat = newFormat;

		synchronized (this) {
			fileDateFormatter = new SimpleDateFormat(newFormat, Locale.US);
			fileDateFormatter.setTimeZone(TimeZone.getDefault());
		}
	}

	/**
	 * Return the locale used to format timestamps in log entries and in log
	 * file name suffix.
	 */
	public String getLocale() {
		return localeName;
	}

	/**
	 * Set the locale used to format timestamps in log entries and in log file
	 * name suffix. Changing the locale is only supported as long as the
	 * AccessLogValve has not logged anything. Changing the locale later can
	 * lead to inconsistent formatting.
	 *
	 * @param localeName
	 *            The locale to use.
	 */
	public void setLocale(String localeName) {
		this.localeName = localeName;
		locale = findLocale(localeName, locale);
	}

	/**
	 * Return the character set name that is used to write the log file.
	 *
	 * @return Character set name, or <code>null</code> if the system default
	 *         character set is used.
	 */
	public String getEncoding() {
		return encoding;
	}

	/**
	 * Set the character set that is used to write the log file.
	 *
	 * @param encoding
	 *            The name of the character set.
	 */
	public void setEncoding(String encoding) {
		if (encoding != null && encoding.length() > 0) {
			this.encoding = encoding;
		} else {
			this.encoding = null;
		}
	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Execute a periodic task, such as reloading, etc. This method will be
	 * invoked inside the classloading context of this container. Unexpected
	 * throwables will be caught and logged.
	 */
	@Override
	public synchronized void backgroundProcess() {
		if (getState().isAvailable() && getEnabled() && writer != null && buffered) {
			writer.flush();
		}
	}

	/**
	 * Log a message summarizing the specified request and response, according
	 * to the format specified by the <code>pattern</code> property.
	 *
	 * @param request
	 *            Request being processed
	 * @param response
	 *            Response being processed
	 *
	 * @exception IOException
	 *                if an input/output error has occurred
	 * @exception ServletException
	 *                if a servlet error has occurred
	 */
	@Override
	public void invoke(Request request, Response response) throws IOException, ServletException {
		getNext().invoke(request, response);
	}

	@Override
	public void log(Request request, Response response, long time) {

		
		
		
		if (!getState().isAvailable() || !getEnabled() || logElements == null
				|| condition != null && null != request.getRequest().getAttribute(condition)
				|| conditionIf != null && null == request.getRequest().getAttribute(conditionIf)) {
			return;
		}

		/**
		 * XXX This is a bit silly, but we want to have start and stop time and
		 * duration consistent. It would be better to keep start and stop simply
		 * in the request and/or response object and remove time (duration) from
		 * the interface.
		 */
		long start = request.getCoyoteRequest().getStartTime();
		Date date = getDate(start + time);

		StringBuilder result = new StringBuilder(128);

		for (int i = 0; i < logElements.length; i++) {
			logElements[i].addElement(result, date, request, response, time);
		}

		log(result.toString());
	}

	/**
	 * Rotate the log file if necessary.
	 */
	public void rotate() {
		if (rotatable) {
			// Only do a logfile switch check once a second, max.
			long systime = System.currentTimeMillis();
			if ((systime - rotationLastChecked) > 1000) {
				synchronized (this) {
					if ((systime - rotationLastChecked) > 1000) {
						rotationLastChecked = systime;

						String tsDate;
						// Check for a change of date
						tsDate = fileDateFormatter.format(new Date(systime));

						// If the date has changed, switch log files
						if (!dateStamp.equals(tsDate)) {
							close(true);
							dateStamp = tsDate;
							open();
						}
					}
				}
			}
		}
	}

	/**
	 * Rename the existing log file to something else. Then open the old log
	 * file name up once again. Intended to be called by a JMX agent.
	 *
	 *
	 * @param newFileName
	 *            The file name to move the log file entry to
	 * @return true if a file was rotated with no error
	 */
	public synchronized boolean rotate(String newFileName) {

		if (currentLogFile != null) {
			File holder = currentLogFile;
			close(false);
			try {
				holder.renameTo(new File(newFileName));
			} catch (Throwable e) {
				ExceptionUtils.handleThrowable(e);
				log.error(sm.getString("accessLogValve.rotateFail"), e);
			}

			/* Make sure date is correct */
			dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis()));

			open();
			return true;
		} else {
			return false;
		}

	}

	// -------------------------------------------------------- Private Methods

	/**
	 * Create a File object based on the current log file name. Directories are
	 * created as needed but the underlying file is not created or opened.
	 *
	 * @param useDateStamp
	 *            include the timestamp in the file name.
	 * @return the log file object
	 */
	private File getLogFile(boolean useDateStamp) {

		// Create the directory if necessary
		File dir = new File(directory);
		if (!dir.isAbsolute()) {
			dir = new File(System.getProperty(Globals.CATALINA_BASE_PROP), directory);
		}
		if (!dir.mkdirs() && !dir.isDirectory()) {
			log.error(sm.getString("accessLogValve.openDirFail", dir));
		}

		// Calculate the current log file name
		File pathname;
		if (useDateStamp) {
			pathname = new File(dir.getAbsoluteFile(), prefix + dateStamp + suffix);
		} else {
			pathname = new File(dir.getAbsoluteFile(), prefix + suffix);
		}
		File parent = pathname.getParentFile();
		if (!parent.mkdirs() && !parent.isDirectory()) {
			log.error(sm.getString("accessLogValve.openDirFail", parent));
		}
		return pathname;
	}

	/**
	 * Move a current but rotated log file back to the unrotated one. Needed if
	 * date stamp inclusion is deferred to rotation time.
	 */
	private void restore() {
		File newLogFile = getLogFile(false);
		File rotatedLogFile = getLogFile(true);
		if (rotatedLogFile.exists() && !newLogFile.exists() && !rotatedLogFile.equals(newLogFile)) {
			try {
				if (!rotatedLogFile.renameTo(newLogFile)) {
					log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile));
				}
			} catch (Throwable e) {
				ExceptionUtils.handleThrowable(e);
				log.error(sm.getString("accessLogValve.renameFail", rotatedLogFile, newLogFile), e);
			}
		}
	}

	/**
	 * Close the currently open log file (if any)
	 *
	 * @param rename
	 *            Rename file to final name after closing
	 */
	private synchronized void close(boolean rename) {
		if (writer == null) {
			return;
		}
		writer.flush();
		writer.close();
		if (rename && renameOnRotate) {
			File newLogFile = getLogFile(true);
			if (!newLogFile.exists()) {
				try {
					if (!currentLogFile.renameTo(newLogFile)) {
						log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile));
					}
				} catch (Throwable e) {
					ExceptionUtils.handleThrowable(e);
					log.error(sm.getString("accessLogValve.renameFail", currentLogFile, newLogFile), e);
				}
			} else {
				log.error(sm.getString("accessLogValve.alreadyExists", currentLogFile, newLogFile));
			}
		}
		writer = null;
		dateStamp = "";
		currentLogFile = null;
	}

	/**
	 * Log the specified message to the log file, switching files if the date
	 * has changed since the previous log call.
	 *
	 * @param message
	 *            Message to be logged
	 */
	public void log(String message) {

		rotate();

		/* In case something external rotated the file instead */
		if (checkExists) {
			synchronized (this) {
				if (currentLogFile != null && !currentLogFile.exists()) {
					try {
						close(false);
					} catch (Throwable e) {
						ExceptionUtils.handleThrowable(e);
						log.info(sm.getString("accessLogValve.closeFail"), e);
					}

					/* Make sure date is correct */
					dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis()));

					open();
				}
			}
		}

		// Log this message
		synchronized (this) {
			if (writer != null) {
				writer.println(message);
				if (!buffered) {
					writer.flush();
				}
			}
		}

	}

	/**
	 * Open the new log file for the date specified by <code>dateStamp</code>.
	 */
	protected synchronized void open() {
		// Open the current log file
		// If no rotate - no need for dateStamp in fileName
		File pathname = getLogFile(rotatable && !renameOnRotate);

		Charset charset = null;
		if (encoding != null) {
			try {
				charset = B2CConverter.getCharset(encoding);
			} catch (UnsupportedEncodingException ex) {
				log.error(sm.getString("accessLogValve.unsupportedEncoding", encoding), ex);
			}
		}
		if (charset == null) {
			charset = Charset.defaultCharset();
		}

		try {
			writer = new PrintWriter(
					new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pathname, true), charset), 128000),
					false);

			currentLogFile = pathname;
		} catch (IOException e) {
			writer = null;
			currentLogFile = null;
			log.error(sm.getString("accessLogValve.openFail", pathname), e);
		}
	}

	/**
	 * This method returns a ThreadLocal Date object that is set to the
	 * specified time. This saves creating a new Date object for every request.
	 *
	 * @return Date
	 */
	private static Date getDate(long systime) {
		Date date = localDate.get();
		date.setTime(systime);
		return date;
	}

	/**
	 * Find a locale by name
	 */
	protected static Locale findLocale(String name, Locale fallback) {
		if (name == null || name.isEmpty()) {
			return Locale.getDefault();
		} else {
			for (Locale l : Locale.getAvailableLocales()) {
				if (name.equals(l.toString())) {
					return (l);
				}
			}
		}
		log.error(sm.getString("accessLogValve.invalidLocale", name));
		return fallback;
	}

	/**
	 * Start this component and implement the requirements of
	 * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
	 *
	 * @exception LifecycleException
	 *                if this component detects a fatal error that prevents this
	 *                component from being used
	 */
	@Override
	protected synchronized void startInternal() throws LifecycleException {

		// Initialize the Date formatters
		String format = getFileDateFormat();
		fileDateFormatter = new SimpleDateFormat(format, Locale.US);
		fileDateFormatter.setTimeZone(TimeZone.getDefault());
		dateStamp = fileDateFormatter.format(new Date(System.currentTimeMillis()));
		if (rotatable && renameOnRotate) {
			restore();
		}
		open();

		setState(LifecycleState.STARTING);
	}

	/**
	 * Stop this component and implement the requirements of
	 * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
	 *
	 * @exception LifecycleException
	 *                if this component detects a fatal error that prevents this
	 *                component from being used
	 */
	@Override
	protected synchronized void stopInternal() throws LifecycleException {

		setState(LifecycleState.STOPPING);
		close(false);
	}

	/**
	 * AccessLogElement writes the partial message into the buffer.
	 */
	protected interface AccessLogElement {
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time);

	}

	/**
	 * write thread name - %I
	 */
	protected static class ThreadNameElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			RequestInfo info = request.getCoyoteRequest().getRequestProcessor();
			if (info != null) {
				buf.append(info.getWorkerThreadName());
			} else {
				buf.append("-");
			}
		}
	}

	/**
	 * write local IP address - %A
	 */
	protected static class LocalAddrElement implements AccessLogElement {

		private static final String LOCAL_ADDR_VALUE;

		static {
			String init;
			try {
				init = InetAddress.getLocalHost().getHostAddress();
			} catch (Throwable e) {
				ExceptionUtils.handleThrowable(e);
				init = "127.0.0.1";
			}
			LOCAL_ADDR_VALUE = init;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			buf.append(LOCAL_ADDR_VALUE);
		}
	}

	/**
	 * write remote IP address - %a
	 */
	protected class RemoteAddrElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (requestAttributesEnabled) {
				Object addr = request.getAttribute(REMOTE_ADDR_ATTRIBUTE);
				if (addr == null) {
					buf.append(request.getRemoteAddr());
				} else {
					buf.append(addr);
				}
			} else {
				buf.append(request.getRemoteAddr());
			}
		}
	}

	/**
	 * write remote host name - %h
	 */
	protected class HostElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			String value = null;
			if (requestAttributesEnabled) {
				Object host = request.getAttribute(REMOTE_HOST_ATTRIBUTE);
				if (host != null) {
					value = host.toString();
				}
			}
			if (value == null || value.length() == 0) {
				value = request.getRemoteHost();
			}
			if (value == null || value.length() == 0) {
				value = "-";
			}
			buf.append(value);
		}
	}

	/**
	 * write remote logical username from identd (always returns '-') - %l
	 */
	protected static class LogicalUserNameElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			buf.append('-');
		}
	}

	/**
	 * write request protocol - %H
	 */
	protected class ProtocolElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (requestAttributesEnabled) {
				Object proto = request.getAttribute(PROTOCOL_ATTRIBUTE);
				if (proto == null) {
					buf.append(request.getProtocol());
				} else {
					buf.append(proto);
				}
			} else {
				buf.append(request.getProtocol());
			}
		}
	}

	/**
	 * write remote user that was authenticated (if any), else '-' - %u
	 */
	protected static class UserElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (request != null) {
				String value = request.getRemoteUser();
				if (value != null) {
					buf.append(value);
				} else {
					buf.append('-');
				}
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * write date and time, in configurable format (default CLF) - %t or
	 * %{format}t
	 */
	protected class DateAndTimeElement implements AccessLogElement {

		/**
		 * Format prefix specifying request start time
		 */
		private static final String requestStartPrefix = "begin";

		/**
		 * Format prefix specifying response end time
		 */
		private static final String responseEndPrefix = "end";

		/**
		 * Separator between optional prefix and rest of format
		 */
		private static final String prefixSeparator = ":";

		/**
		 * Special format for seconds since epoch
		 */
		private static final String secFormat = "sec";

		/**
		 * Special format for milliseconds since epoch
		 */
		private static final String msecFormat = "msec";

		/**
		 * Special format for millisecond part of timestamp
		 */
		private static final String msecFractionFormat = "msec_frac";

		/**
		 * The patterns we use to replace "S" and "SSS" millisecond formatting
		 * of SimpleDateFormat by our own handling
		 */
		private static final String msecPattern = "{#}";
		private static final String trippleMsecPattern = msecPattern + msecPattern + msecPattern;

		/* Our format description string, null if CLF */
		private String format = null;
		/*
		 * Whether to use begin of request or end of response as the timestamp
		 */
		private boolean usesBegin = false;
		/* The format type */
		private FormatType type = FormatType.CLF;
		/* Whether we need to postprocess by adding milliseconds */
		private boolean usesMsecs = false;

		protected DateAndTimeElement() {
			this(null);
		}

		/**
		 * Replace the millisecond formatting character 'S' by some dummy
		 * characters in order to make the resulting formatted time stamps
		 * cacheable. We replace the dummy chars later with the actual
		 * milliseconds because that's relatively cheap.
		 */
		private String tidyFormat(String format) {
			boolean escape = false;
			StringBuilder result = new StringBuilder();
			int len = format.length();
			char x;
			for (int i = 0; i < len; i++) {
				x = format.charAt(i);
				if (escape || x != 'S') {
					result.append(x);
				} else {
					result.append(msecPattern);
					usesMsecs = true;
				}
				if (x == '\'') {
					escape = !escape;
				}
			}
			return result.toString();
		}

		protected DateAndTimeElement(String header) {
			format = header;
			if (format != null) {
				if (format.equals(requestStartPrefix)) {
					usesBegin = true;
					format = "";
				} else if (format.startsWith(requestStartPrefix + prefixSeparator)) {
					usesBegin = true;
					format = format.substring(6);
				} else if (format.equals(responseEndPrefix)) {
					usesBegin = false;
					format = "";
				} else if (format.startsWith(responseEndPrefix + prefixSeparator)) {
					usesBegin = false;
					format = format.substring(4);
				}
				if (format.length() == 0) {
					type = FormatType.CLF;
				} else if (format.equals(secFormat)) {
					type = FormatType.SEC;
				} else if (format.equals(msecFormat)) {
					type = FormatType.MSEC;
				} else if (format.equals(msecFractionFormat)) {
					type = FormatType.MSEC_FRAC;
				} else {
					type = FormatType.SDF;
					format = tidyFormat(format);
				}
			}
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			long timestamp = date.getTime();
			long frac;
			if (usesBegin) {
				timestamp -= time;
			}
			switch (type) {
			case CLF:
				buf.append(localDateCache.get().getFormat(timestamp));
				break;
			case SEC:
				buf.append(timestamp / 1000);
				break;
			case MSEC:
				buf.append(timestamp);
				break;
			case MSEC_FRAC:
				frac = timestamp % 1000;
				if (frac < 100) {
					if (frac < 10) {
						buf.append('0');
						buf.append('0');
					} else {
						buf.append('0');
					}
				}
				buf.append(frac);
				break;
			case SDF:
				String temp = localDateCache.get().getFormat(format, locale, timestamp);
				if (usesMsecs) {
					frac = timestamp % 1000;
					StringBuilder trippleMsec = new StringBuilder(4);
					if (frac < 100) {
						if (frac < 10) {
							trippleMsec.append('0');
							trippleMsec.append('0');
						} else {
							trippleMsec.append('0');
						}
					}
					trippleMsec.append(frac);
					temp = temp.replace(trippleMsecPattern, trippleMsec);
					temp = temp.replace(msecPattern, Long.toString(frac));
				}
				buf.append(temp);
				break;
			}
		}
	}

	/**
	 * write first line of the request (method and request URI) - %r
	 */
	protected static class RequestElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (request != null) {
				String method = request.getMethod();
				if (method == null) {
					// No method means no request line
					buf.append('-');
				} else {
					buf.append(request.getMethod());
					buf.append(' ');
					buf.append(request.getRequestURI());
					if (request.getQueryString() != null) {
						buf.append('?');
						buf.append(request.getQueryString());
					}
					buf.append(' ');
					buf.append(request.getProtocol());
				}
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * write HTTP status code of the response - %s
	 */
	protected static class HttpStatusCodeElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (response != null) {
				buf.append(response.getStatus());
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * write local or remote port for request connection - %p and %{xxx}p
	 */
	protected class PortElement implements AccessLogElement {

		/**
		 * Type of port to log
		 */
		private static final String localPort = "local";
		private static final String remotePort = "remote";

		private final PortType portType;

		public PortElement() {
			portType = PortType.LOCAL;
		}

		public PortElement(String type) {
			if (type.equals(localPort)) {
				portType = PortType.LOCAL;
			} else if (type.equals(remotePort)) {
				portType = PortType.REMOTE;
			} else {
				portType = PortType.LOCAL;
				log.error(sm.getString("accessLogValve.invalidPortType", type));
			}
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (requestAttributesEnabled && portType == PortType.LOCAL) {
				Object port = request.getAttribute(SERVER_PORT_ATTRIBUTE);
				if (port == null) {
					buf.append(request.getServerPort());
				} else {
					buf.append(port);
				}
			} else {
				if (portType == PortType.LOCAL) {
					buf.append(Integer.toString(request.getServerPort()));
				} else {
					buf.append(Integer.toString(request.getRemotePort()));
				}
			}
		}
	}

	/**
	 * write bytes sent, excluding HTTP headers - %b, %B
	 */
	protected static class ByteSentElement implements AccessLogElement {
		private final boolean conversion;

		/**
		 * if conversion is true, write '-' instead of 0 - %b
		 */
		public ByteSentElement(boolean conversion) {
			this.conversion = conversion;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			// Don't need to flush since trigger for log message is after the
			// response has been committed
			long length = response.getBytesWritten(false);
			if (length <= 0) {
				// Protect against nulls and unexpected types as these values
				// may be set by untrusted applications
				Object start = request.getAttribute(Globals.SENDFILE_FILE_START_ATTR);
				if (start instanceof Long) {
					Object end = request.getAttribute(Globals.SENDFILE_FILE_END_ATTR);
					if (end instanceof Long) {
						length = ((Long) end).longValue() - ((Long) start).longValue();
					}
				}
			}
			if (length <= 0 && conversion) {
				buf.append('-');
			} else {
				buf.append(length);
			}
		}
	}

	/**
	 * write request method (GET, POST, etc.) - %m
	 */
	protected static class MethodElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (request != null) {
				buf.append(request.getMethod());
			}
		}
	}

	/**
	 * write time taken to process the request - %D, %T
	 */
	protected static class ElapsedTimeElement implements AccessLogElement {
		private final boolean millis;

		/**
		 * if millis is true, write time in millis - %D if millis is false,
		 * write time in seconds - %T
		 */
		public ElapsedTimeElement(boolean millis) {
			this.millis = millis;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (millis) {
				buf.append(time);
			} else {
				// second
				buf.append(time / 1000);
				buf.append('.');
				int remains = (int) (time % 1000);
				buf.append(remains / 100);
				remains = remains % 100;
				buf.append(remains / 10);
				buf.append(remains % 10);
			}
		}
	}

	/**
	 * write time until first byte is written (commit time) in millis - %F
	 */
	protected static class FirstByteTimeElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			long commitTime = response.getCoyoteResponse().getCommitTime();
			if (commitTime == -1) {
				buf.append('-');
			} else {
				long delta = commitTime - request.getCoyoteRequest().getStartTime();
				buf.append(Long.toString(delta));
			}
		}
	}

	/**
	 * write Query string (prepended with a '?' if it exists) - %q
	 */
	protected static class QueryElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			String query = null;
			if (request != null) {
				query = request.getQueryString();
			}
			if (query != null) {
				buf.append('?');
				buf.append(query);
			}
		}
	}

	/**
	 * write user session ID - %S
	 */
	protected static class SessionIdElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (request == null) {
				buf.append('-');
			} else {
				Session session = request.getSessionInternal(false);
				if (session == null) {
					buf.append('-');
				} else {
					buf.append(session.getIdInternal());
				}
			}
		}
	}

	/**
	 * write requested URL path - %U
	 */
	protected static class RequestURIElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (request != null) {
				buf.append(request.getRequestURI());
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * write local server name - %v
	 */
	protected static class LocalServerNameElement implements AccessLogElement {
		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			buf.append(request.getServerName());
		}
	}

	/**
	 * write any string
	 */
	protected static class StringElement implements AccessLogElement {
		private final String str;

		public StringElement(String str) {
			this.str = str;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			buf.append(str);
		}
	}

	/**
	 * write incoming headers - %{xxx}i
	 */
	protected static class HeaderElement implements AccessLogElement {
		private final String header;

		public HeaderElement(String header) {
			this.header = header;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			Enumeration<String> iter = request.getHeaders(header);
			if (iter.hasMoreElements()) {
				buf.append(iter.nextElement());
				while (iter.hasMoreElements()) {
					buf.append(',').append(iter.nextElement());
				}
				return;
			}
			buf.append('-');
		}
	}

	/**
	 * write a specific cookie - %{xxx}c
	 */
	protected static class CookieElement implements AccessLogElement {
		private final String header;

		public CookieElement(String header) {
			this.header = header;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			String value = "-";
			Cookie[] c = request.getCookies();
			if (c != null) {
				for (int i = 0; i < c.length; i++) {
					if (header.equals(c[i].getName())) {
						value = c[i].getValue();
						break;
					}
				}
			}
			buf.append(value);
		}
	}

	/**
	 * write a specific response header - %{xxx}o
	 */
	protected static class ResponseHeaderElement implements AccessLogElement {
		private final String header;

		public ResponseHeaderElement(String header) {
			this.header = header;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			if (null != response) {
				Iterator<String> iter = response.getHeaders(header).iterator();
				if (iter.hasNext()) {
					buf.append(iter.next());
					while (iter.hasNext()) {
						buf.append(',').append(iter.next());
					}
					return;
				}
			}
			buf.append('-');
		}
	}

	/**
	 * write an attribute in the ServletRequest - %{xxx}r
	 */
	protected static class RequestAttributeElement implements AccessLogElement {
		private final String header;

		public RequestAttributeElement(String header) {
			this.header = header;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			Object value = null;
			if (request != null) {
				value = request.getAttribute(header);
			} else {
				value = "??";
			}
			if (value != null) {
				if (value instanceof String) {
					buf.append((String) value);
				} else {
					buf.append(value.toString());
				}
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * write an attribute in the HttpSession - %{xxx}s
	 */
	protected static class SessionAttributeElement implements AccessLogElement {
		private final String header;

		public SessionAttributeElement(String header) {
			this.header = header;
		}

		@Override
		public void addElement(StringBuilder buf, Date date, Request request, Response response, long time) {
			Object value = null;
			if (null != request) {
				HttpSession sess = request.getSession(false);
				if (null != sess) {
					value = sess.getAttribute(header);
				}
			} else {
				value = "??";
			}
			if (value != null) {
				if (value instanceof String) {
					buf.append((String) value);
				} else {
					buf.append(value.toString());
				}
			} else {
				buf.append('-');
			}
		}
	}

	/**
	 * parse pattern string and create the array of AccessLogElement
	 */
	protected AccessLogElement[] createLogElements() {
		List<AccessLogElement> list = new ArrayList<AccessLogElement>();
		boolean replace = false;
		StringBuilder buf = new StringBuilder();
		for (int i = 0; i < pattern.length(); i++) {
			char ch = pattern.charAt(i);
			if (replace) {
				/*
				 * For code that processes {, the behavior will be ... if I do
				 * not encounter a closing } - then I ignore the {
				 */
				if ('{' == ch) {
					StringBuilder name = new StringBuilder();
					int j = i + 1;
					for (; j < pattern.length() && '}' != pattern.charAt(j); j++) {
						name.append(pattern.charAt(j));
					}
					if (j + 1 < pattern.length()) {
						/* the +1 was to account for } which we increment now */
						j++;
						list.add(createAccessLogElement(name.toString(), pattern.charAt(j)));
						i = j; /* Since we walked more than one character */
					} else {
						// D'oh - end of string - pretend we never did this
						// and do processing the "old way"
						list.add(createAccessLogElement(ch));
					}
				} else {
					list.add(createAccessLogElement(ch));
				}
				replace = false;
			} else if (ch == '%') {
				replace = true;
				list.add(new StringElement(buf.toString()));
				buf = new StringBuilder();
			} else {
				buf.append(ch);
			}
		}
		if (buf.length() > 0) {
			list.add(new StringElement(buf.toString()));
		}
		return list.toArray(new AccessLogElement[0]);
	}

	/**
	 * create an AccessLogElement implementation which needs an element name
	 */
	protected AccessLogElement createAccessLogElement(String name, char pattern) {
		switch (pattern) {
		case 'i':
			return new HeaderElement(name);
		case 'c':
			return new CookieElement(name);
		case 'o':
			return new ResponseHeaderElement(name);
		case 'p':
			return new PortElement(name);
		case 'r':
			return new RequestAttributeElement(name);
		case 's':
			return new SessionAttributeElement(name);
		case 't':
			return new DateAndTimeElement(name);
		default:
			return new StringElement("???");
		}
	}

	/**
	 * create an AccessLogElement implementation
	 */
	protected AccessLogElement createAccessLogElement(char pattern) {
		switch (pattern) {
		case 'a':
			return new RemoteAddrElement();
		case 'A':
			return new LocalAddrElement();
		case 'b':
			return new ByteSentElement(true);
		case 'B':
			return new ByteSentElement(false);
		case 'D':
			return new ElapsedTimeElement(true);
		case 'F':
			return new FirstByteTimeElement();
		case 'h':
			return new HostElement();
		case 'H':
			return new ProtocolElement();
		case 'l':
			return new LogicalUserNameElement();
		case 'm':
			return new MethodElement();
		case 'p':
			return new PortElement();
		case 'q':
			return new QueryElement();
		case 'r':
			return new RequestElement();
		case 's':
			return new HttpStatusCodeElement();
		case 'S':
			return new SessionIdElement();
		case 't':
			return new DateAndTimeElement();
		case 'T':
			return new ElapsedTimeElement(false);
		case 'u':
			return new UserElement();
		case 'U':
			return new RequestURIElement();
		case 'v':
			return new LocalServerNameElement();
		case 'I':
			return new ThreadNameElement();
		default:
			return new StringElement("???" + pattern + "???");
		}
	}
}