/*
 * 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.CharArrayWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpSession;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.ServerInfo;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;

An implementation of the W3c Extended Log File Format. See http://www.w3.org/TR/WD-logfile.html for more information about the format. The following fields are supported:
  • c-dns: Client hostname (or ip address if enableLookups for the connector is false)
  • c-ip: Client ip address
  • bytes: bytes served
  • cs-method: request method
  • cs-uri: The full uri requested
  • cs-uri-query: The query string
  • cs-uri-stem: The uri without query string
  • date: The date in yyyy-mm-dd format for GMT
  • s-dns: The server dns entry
  • s-ip: The server ip address
  • cs(XXX): The value of header XXX from client to server
  • sc(XXX): The value of header XXX from server to client
  • sc-status: The status code
  • time: Time the request was served
  • time-taken: Time (in seconds) taken to serve the request
  • x-threadname: Current request thread name (can compare later with stacktraces)
  • x-A(XXX): Pull XXX attribute from the servlet context
  • x-C(XXX): Pull the first cookie of the name XXX
  • x-O(XXX): Pull the all response header values XXX
  • x-R(XXX): Pull XXX attribute from the servlet request
  • x-S(XXX): Pull XXX attribute from the session
  • x-P(...): Call request.getParameter(...) and URLencode it. Helpful to capture certain POST parameters.
  • For any of the x-H(...) the following method will be called from the HttpServletRequest object
  • x-H(authType): getAuthType
  • x-H(characterEncoding): getCharacterEncoding
  • x-H(contentLength): getContentLength
  • x-H(locale): getLocale
  • x-H(protocol): getProtocol
  • x-H(remoteUser): getRemoteUser
  • x-H(requestedSessionId): getRequestedSessionId
  • x-H(requestedSessionIdFromCookie): isRequestedSessionIdFromCookie
  • x-H(requestedSessionIdValid): isRequestedSessionIdValid
  • x-H(scheme): getScheme
  • x-H(secure): isSecure

Log rotation can be on or off. This is dictated by the rotatable property.

For UNIX users, another field called checkExists 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.

For JMX junkies, a public method called rotate 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.

Conditional logging is also supported. This can be done with the condition property. If the value returned from ServletRequest.getAttribute(condition) yields a non-null value, the logging will be skipped.

For extended attributes coming from a getAttribute() call, it is you responsibility to ensure there are no newline or control characters.

Author:Peter Rossbach
/** * An implementation of the W3c Extended Log File Format. See * http://www.w3.org/TR/WD-logfile.html for more information about the format. * * The following fields are supported: * <ul> * <li><code>c-dns</code>: Client hostname (or ip address if * <code>enableLookups</code> for the connector is false)</li> * <li><code>c-ip</code>: Client ip address</li> * <li><code>bytes</code>: bytes served</li> * <li><code>cs-method</code>: request method</li> * <li><code>cs-uri</code>: The full uri requested</li> * <li><code>cs-uri-query</code>: The query string</li> * <li><code>cs-uri-stem</code>: The uri without query string</li> * <li><code>date</code>: The date in yyyy-mm-dd format for GMT</li> * <li><code>s-dns</code>: The server dns entry </li> * <li><code>s-ip</code>: The server ip address</li> * <li><code>cs(XXX)</code>: The value of header XXX from client to server</li> * <li><code>sc(XXX)</code>: The value of header XXX from server to client </li> * <li><code>sc-status</code>: The status code</li> * <li><code>time</code>: Time the request was served</li> * <li><code>time-taken</code>: Time (in seconds) taken to serve the request</li> * <li><code>x-threadname</code>: Current request thread name (can compare later with stacktraces)</li> * <li><code>x-A(XXX)</code>: Pull XXX attribute from the servlet context </li> * <li><code>x-C(XXX)</code>: Pull the first cookie of the name XXX </li> * <li><code>x-O(XXX)</code>: Pull the all response header values XXX </li> * <li><code>x-R(XXX)</code>: Pull XXX attribute from the servlet request </li> * <li><code>x-S(XXX)</code>: Pull XXX attribute from the session </li> * <li><code>x-P(...)</code>: Call request.getParameter(...) * and URLencode it. Helpful to capture * certain POST parameters. * </li> * <li>For any of the x-H(...) the following method will be called from the * HttpServletRequest object </li> * <li><code>x-H(authType)</code>: getAuthType </li> * <li><code>x-H(characterEncoding)</code>: getCharacterEncoding </li> * <li><code>x-H(contentLength)</code>: getContentLength </li> * <li><code>x-H(locale)</code>: getLocale</li> * <li><code>x-H(protocol)</code>: getProtocol </li> * <li><code>x-H(remoteUser)</code>: getRemoteUser</li> * <li><code>x-H(requestedSessionId)</code>: getRequestedSessionId</li> * <li><code>x-H(requestedSessionIdFromCookie)</code>: * isRequestedSessionIdFromCookie </li> * <li><code>x-H(requestedSessionIdValid)</code>: * isRequestedSessionIdValid</li> * <li><code>x-H(scheme)</code>: getScheme</li> * <li><code>x-H(secure)</code>: isSecure</li> * </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>condition</code> property. * If the value returned from ServletRequest.getAttribute(condition) * yields a non-null value, the logging will be skipped. * </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 Peter Rossbach */
public class ExtendedAccessLogValve extends AccessLogValve { private static final Log log = LogFactory.getLog(ExtendedAccessLogValve.class); // ----------------------------------------------------- Instance Variables
The descriptive information about this implementation.
/** * The descriptive information about this implementation. */
protected static final String extendedAccessLogInfo = "org.apache.catalina.valves.ExtendedAccessLogValve/2.1"; // -------------------------------------------------------- Private Methods
Wrap the incoming value with double quotes (") and escape any double quotes appearing in the value using two double quotes (""). @param value - The value to wrap @return '-' if null. Otherwise, toString() will be called on the object and the value will be wrapped in quotes and any quotes will be escaped with 2 sets of quotes.
/** * Wrap the incoming value with double quotes (") and escape any double * quotes appearing in the value using two double quotes (""). * * @param value - The value to wrap * @return '-' if null. Otherwise, toString() will * be called on the object and the value will be wrapped * in quotes and any quotes will be escaped with 2 * sets of quotes. */
static String wrap(Object value) { String svalue; // Does the value contain a " ? If so must encode it if (value == null || "-".equals(value)) { return "-"; } try { svalue = value.toString(); } catch (Throwable e) { ExceptionUtils.handleThrowable(e); /* Log error */ return "-"; } /* Wrap all values in double quotes. */ StringBuilder buffer = new StringBuilder(svalue.length() + 2); buffer.append('\"'); int i = 0; while (i < svalue.length()) { int j = svalue.indexOf('\"', i); if (j == -1) { buffer.append(svalue.substring(i)); i = svalue.length(); } else { buffer.append(svalue.substring(i, j + 1)); buffer.append('"'); i = j + 1; } } buffer.append('\"'); return buffer.toString(); }
Open the new log file for the date specified by dateStamp.
/** * Open the new log file for the date specified by <code>dateStamp</code>. */
@Override protected synchronized void open() { super.open(); if (currentLogFile.length()==0) { writer.println("#Fields: " + pattern); writer.println("#Version: 2.0"); writer.println("#Software: " + ServerInfo.getServerInfo()); } } // ------------------------------------------------------ Lifecycle Methods protected static class DateElement implements AccessLogElement { // Milli-seconds in 24 hours private static final long INTERVAL = (1000 * 60 * 60 * 24); private static final ThreadLocal<ElementTimestampStruct> currentDate = ThreadLocal.withInitial(() -> new ElementTimestampStruct("yyyy-MM-dd")); @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { ElementTimestampStruct eds = currentDate.get(); long millis = eds.currentTimestamp.getTime(); if (date.getTime() > (millis + INTERVAL -1) || date.getTime() < millis) { eds.currentTimestamp.setTime( date.getTime() - (date.getTime() % INTERVAL)); eds.currentTimestampString = eds.currentTimestampFormat.format(eds.currentTimestamp); } buf.append(eds.currentTimestampString); } } protected static class TimeElement implements AccessLogElement { // Milli-seconds in a second private static final long INTERVAL = 1000; private static final ThreadLocal<ElementTimestampStruct> currentTime = ThreadLocal.withInitial(() -> new ElementTimestampStruct("HH:mm:ss")); @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { ElementTimestampStruct eds = currentTime.get(); long millis = eds.currentTimestamp.getTime(); if (date.getTime() > (millis + INTERVAL -1) || date.getTime() < millis) { eds.currentTimestamp.setTime( date.getTime() - (date.getTime() % INTERVAL)); eds.currentTimestampString = eds.currentTimestampFormat.format(eds.currentTimestamp); } buf.append(eds.currentTimestampString); } } protected static class RequestHeaderElement implements AccessLogElement { private final String header; public RequestHeaderElement(String header) { this.header = header; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getHeader(header))); } } protected static class ResponseHeaderElement implements AccessLogElement { private final String header; public ResponseHeaderElement(String header) { this.header = header; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(response.getHeader(header))); } } protected static class ServletContextElement implements AccessLogElement { private final String attribute; public ServletContextElement(String attribute) { this.attribute = attribute; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getContext().getServletContext() .getAttribute(attribute))); } } protected static class CookieElement implements AccessLogElement { private final String name; public CookieElement(String name) { this.name = name; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { Cookie[] c = request.getCookies(); for (int i = 0; c != null && i < c.length; i++) { if (name.equals(c[i].getName())) { buf.append(wrap(c[i].getValue())); } } } }
write a specific response header - x-O(xxx)
/** * write a specific response header - x-O(xxx) */
protected static class ResponseAllHeaderElement implements AccessLogElement { private final String header; public ResponseAllHeaderElement(String header) { this.header = header; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { if (null != response) { Iterator<String> iter = response.getHeaders(header).iterator(); if (iter.hasNext()) { StringBuilder buffer = new StringBuilder(); boolean first = true; while (iter.hasNext()) { if (first) { first = false; } else { buffer.append(','); } buffer.append(iter.next()); } buf.append(wrap(buffer.toString())); } return ; } buf.append('-'); } } protected static class RequestAttributeElement implements AccessLogElement { private final String attribute; public RequestAttributeElement(String attribute) { this.attribute = attribute; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getAttribute(attribute))); } } protected static class SessionAttributeElement implements AccessLogElement { private final String attribute; public SessionAttributeElement(String attribute) { this.attribute = attribute; } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { HttpSession session = null; if (request != null) { session = request.getSession(false); if (session != null) { buf.append(wrap(session.getAttribute(attribute))); } } } } protected static class RequestParameterElement implements AccessLogElement { private final String parameter; public RequestParameterElement(String parameter) { this.parameter = parameter; }
urlEncode the given string. If null or empty, return null.
/** * urlEncode the given string. If null or empty, return null. */
private String urlEncode(String value) { if (null==value || value.length()==0) { return null; } try { return URLEncoder.encode(value, "UTF-8"); } catch (UnsupportedEncodingException e) { // Should never happen - all JVMs are required to support UTF-8 return null; } } @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(urlEncode(request.getParameter(parameter)))); } } protected static class PatternTokenizer { private final StringReader sr; private StringBuilder buf = new StringBuilder(); private boolean ended = false; private boolean subToken; private boolean parameter; public PatternTokenizer(String str) { sr = new StringReader(str); } public boolean hasSubToken() { return subToken; } public boolean hasParameter() { return parameter; } public String getToken() throws IOException { if(ended) { return null ; } String result = null; subToken = false; parameter = false; int c = sr.read(); while (c != -1) { switch (c) { case ' ': result = buf.toString(); buf = new StringBuilder(); buf.append((char) c); return result; case '-': result = buf.toString(); buf = new StringBuilder(); subToken = true; return result; case '(': result = buf.toString(); buf = new StringBuilder(); parameter = true; return result; case ')': result = buf.toString(); buf = new StringBuilder(); break; default: buf.append((char) c); } c = sr.read(); } ended = true; if (buf.length() != 0) { return buf.toString(); } else { return null; } } public String getParameter()throws IOException { String result; if (!parameter) { return null; } parameter = false; int c = sr.read(); while (c != -1) { if (c == ')') { result = buf.toString(); buf = new StringBuilder(); return result; } buf.append((char) c); c = sr.read(); } return null; } public String getWhiteSpaces() throws IOException { if(isEnded()) { return "" ; } StringBuilder whiteSpaces = new StringBuilder(); if (buf.length() > 0) { whiteSpaces.append(buf); buf = new StringBuilder(); } int c = sr.read(); while (Character.isWhitespace((char) c)) { whiteSpaces.append((char) c); c = sr.read(); } if (c == -1) { ended = true; } else { buf.append((char) c); } return whiteSpaces.toString(); } public boolean isEnded() { return ended; } public String getRemains() throws IOException { StringBuilder remains = new StringBuilder(); for(int c = sr.read(); c != -1; c = sr.read()) { remains.append((char) c); } return remains.toString(); } } @Override protected AccessLogElement[] createLogElements() { if (log.isDebugEnabled()) { log.debug("decodePattern, pattern =" + pattern); } List<AccessLogElement> list = new ArrayList<>(); PatternTokenizer tokenizer = new PatternTokenizer(pattern); try { // Ignore leading whitespace. tokenizer.getWhiteSpaces(); if (tokenizer.isEnded()) { log.info(sm.getString("extendedAccessLogValve.emptyPattern")); return null; } String token = tokenizer.getToken(); while (token != null) { if (log.isDebugEnabled()) { log.debug("token = " + token); } AccessLogElement element = getLogElement(token, tokenizer); if (element == null) { break; } list.add(element); String whiteSpaces = tokenizer.getWhiteSpaces(); if (whiteSpaces.length() > 0) { list.add(new StringElement(whiteSpaces)); } if (tokenizer.isEnded()) { break; } token = tokenizer.getToken(); } if (log.isDebugEnabled()) { log.debug("finished decoding with element size of: " + list.size()); } return list.toArray(new AccessLogElement[0]); } catch (IOException e) { log.error(sm.getString("extendedAccessLogValve.patternParseError", pattern), e); return null; } } protected AccessLogElement getLogElement(String token, PatternTokenizer tokenizer) throws IOException { if ("date".equals(token)) { return new DateElement(); } else if ("time".equals(token)) { if (tokenizer.hasSubToken()) { String nextToken = tokenizer.getToken(); if ("taken".equals(nextToken)) { return new ElapsedTimeElement(false, false); } } else { return new TimeElement(); } } else if ("bytes".equals(token)) { return new ByteSentElement(true); } else if ("cached".equals(token)) { /* I don't know how to evaluate this! */ return new StringElement("-"); } else if ("c".equals(token)) { String nextToken = tokenizer.getToken(); if ("ip".equals(nextToken)) { return new RemoteAddrElement(); } else if ("dns".equals(nextToken)) { return new HostElement(); } } else if ("s".equals(token)) { String nextToken = tokenizer.getToken(); if ("ip".equals(nextToken)) { return new LocalAddrElement(getIpv6Canonical()); } else if ("dns".equals(nextToken)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String value; try { value = InetAddress.getLocalHost().getHostName(); } catch (Throwable e) { ExceptionUtils.handleThrowable(e); value = "localhost"; } buf.append(value); } }; } } else if ("cs".equals(token)) { return getClientToServerElement(tokenizer); } else if ("sc".equals(token)) { return getServerToClientElement(tokenizer); } else if ("sr".equals(token) || "rs".equals(token)) { return getProxyElement(tokenizer); } else if ("x".equals(token)) { return getXParameterElement(tokenizer); } log.error(sm.getString("extendedAccessLogValve.decodeError", token)); return null; } protected AccessLogElement getClientToServerElement( PatternTokenizer tokenizer) throws IOException { if (tokenizer.hasSubToken()) { String token = tokenizer.getToken(); if ("method".equals(token)) { return new MethodElement(); } else if ("uri".equals(token)) { if (tokenizer.hasSubToken()) { token = tokenizer.getToken(); if ("stem".equals(token)) { return new RequestURIElement(); } else if ("query".equals(token)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String query = request.getQueryString(); if (query != null) { buf.append(query); } else { buf.append('-'); } } }; } } else { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { String query = request.getQueryString(); if (query == null) { buf.append(request.getRequestURI()); } else { buf.append(request.getRequestURI()); buf.append('?'); buf.append(request.getQueryString()); } } }; } } } else if (tokenizer.hasParameter()) { String parameter = tokenizer.getParameter(); if (parameter == null) { log.error(sm.getString("extendedAccessLogValve.noClosing")); return null; } return new RequestHeaderElement(parameter); } log.error(sm.getString("extendedAccessLogValve.decodeError", tokenizer.getRemains())); return null; } protected AccessLogElement getServerToClientElement( PatternTokenizer tokenizer) throws IOException { if (tokenizer.hasSubToken()) { String token = tokenizer.getToken(); if ("status".equals(token)) { return new HttpStatusCodeElement(); } else if ("comment".equals(token)) { return new StringElement("?"); } } else if (tokenizer.hasParameter()) { String parameter = tokenizer.getParameter(); if (parameter == null) { log.error(sm.getString("extendedAccessLogValve.noClosing")); return null; } return new ResponseHeaderElement(parameter); } log.error(sm.getString("extendedAccessLogValve.decodeError", tokenizer.getRemains())); return null; } protected AccessLogElement getProxyElement(PatternTokenizer tokenizer) throws IOException { String token = null; if (tokenizer.hasSubToken()) { tokenizer.getToken(); return new StringElement("-"); } else if (tokenizer.hasParameter()) { tokenizer.getParameter(); return new StringElement("-"); } log.error(sm.getString("extendedAccessLogValve.decodeError", token)); return null; } protected AccessLogElement getXParameterElement(PatternTokenizer tokenizer) throws IOException { if (!tokenizer.hasSubToken()) { log.error(sm.getString("extendedAccessLogValve.badXParam")); return null; } String token = tokenizer.getToken(); if ("threadname".equals(token)) { return new ThreadNameElement(); } if (!tokenizer.hasParameter()) { log.error(sm.getString("extendedAccessLogValve.badXParam")); return null; } String parameter = tokenizer.getParameter(); if (parameter == null) { log.error(sm.getString("extendedAccessLogValve.noClosing")); return null; } if ("A".equals(token)) { return new ServletContextElement(parameter); } else if ("C".equals(token)) { return new CookieElement(parameter); } else if ("R".equals(token)) { return new RequestAttributeElement(parameter); } else if ("S".equals(token)) { return new SessionAttributeElement(parameter); } else if ("H".equals(token)) { return getServletRequestElement(parameter); } else if ("P".equals(token)) { return new RequestParameterElement(parameter); } else if ("O".equals(token)) { return new ResponseAllHeaderElement(parameter); } log.error(sm.getString("extendedAccessLogValve.badXParamValue", token)); return null; } protected AccessLogElement getServletRequestElement(String parameter) { if ("authType".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getAuthType())); } }; } else if ("remoteUser".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getRemoteUser())); } }; } else if ("requestedSessionId".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getRequestedSessionId())); } }; } else if ("requestedSessionIdFromCookie".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap("" + request.isRequestedSessionIdFromCookie())); } }; } else if ("requestedSessionIdValid".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap("" + request.isRequestedSessionIdValid())); } }; } else if ("contentLength".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap("" + request.getContentLengthLong())); } }; } else if ("characterEncoding".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getCharacterEncoding())); } }; } else if ("locale".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getLocale())); } }; } else if ("protocol".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap(request.getProtocol())); } }; } else if ("scheme".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(request.getScheme()); } }; } else if ("secure".equals(parameter)) { return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Date date, Request request, Response response, long time) { buf.append(wrap("" + request.isSecure())); } }; } log.error(sm.getString("extendedAccessLogValve.badXParamValue", parameter)); return null; } private static class ElementTimestampStruct { private final Date currentTimestamp = new Date(0); private final SimpleDateFormat currentTimestampFormat; private String currentTimestampString; ElementTimestampStruct(String format) { currentTimestampFormat = new SimpleDateFormat(format, Locale.US); currentTimestampFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } } }