/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/server/log/AccessLog.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.server.log;

import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.caucho.config.ConfigException;
import com.caucho.config.Configurable;
import com.caucho.config.types.Bytes;
import com.caucho.config.types.CronType;
import com.caucho.config.types.Period;
import com.caucho.server.http.AbstractHttpRequest;
import com.caucho.server.http.AbstractHttpResponse;
import com.caucho.server.http.CauchoRequest;
import com.caucho.server.http.HttpServletRequestImpl;
import com.caucho.server.http.HttpServletResponseImpl;
import com.caucho.server.util.CauchoSystem;
import com.caucho.util.Alarm;
import com.caucho.util.AlarmListener;
import com.caucho.util.ByteBuffer;
import com.caucho.util.CharBuffer;
import com.caucho.util.CharSegment;
import com.caucho.util.L10N;
import com.caucho.util.QDate;
import com.caucho.util.WeakAlarm;
import com.caucho.vfs.Path;

/**
 * Represents an log of every top-level request to the server.
 */
public class AccessLog extends AbstractAccessLog implements AlarmListener
{
  protected static final L10N L = new L10N(AccessLog.class);
  protected static final Logger log
    = Logger.getLogger(AccessLog.class.getName());

  // Default maximum log size = 1G
  private static final long ROLLOVER_SIZE = 1024L * 1024L * 1024L;
  public static final int BUFFER_SIZE = 64 * 1024;
  
  private String _timeFormat;
  private int _timeFormatSecondOffset = -1;
  private int _timeFormatMinuteOffset = -1;

  private final AccessLogWriter _logWriter = new AccessLogWriter(this);

  private String _format;
  private Segment []_segments;

  private ArrayList<Pattern> _excludeList = new ArrayList<Pattern>();
  private Pattern []_excludes = new Pattern[0];

  private boolean _isAutoFlush;

  private long _autoFlushTime = 60000;

  private final CharBuffer _cb = new CharBuffer();

  private final CharBuffer _timeCharBuffer = new CharBuffer();
  private final ByteBuffer _timeBuffer = new ByteBuffer();
  private long _lastTime;

  private Alarm _alarm = new WeakAlarm(this);
  private boolean _isActive;

  public AccessLog()
  {
    setRolloverSize(new Bytes(ROLLOVER_SIZE));
  }

  /**
   * Sets the access log format.
   */
  public void setFormat(String format)
  {
    _format = format;
  }

  /**
   * Sets the log path
   */
  @Override
  public void setPath(Path path)
  {
    super.setPath(path);

    _logWriter.setPath(path);
  }

  /**
   * Sets the formatted path.
   */
  @Configurable
  @Override
  public void setPathFormat(String pathFormat)
    throws ConfigException
  {
    super.setPathFormat(pathFormat);

    _logWriter.setPathFormat(pathFormat);
  }

  /**
   * Sets the archive name format
   */
  @Configurable
  public void setArchiveFormat(String format)
  {
    _logWriter.setArchiveFormat(format);
  }

  /**
   * Sets the maximum number of rolled logs.
   *
   * @param count maximum count of the log file
   */
  @Configurable
  public void setRolloverCount(int count)
  {
    _logWriter.setRolloverCount(count);
  }

  /**
   * Sets the log rollover cron
   *
   * @param cron the cron string for rollover times
   */
  @Configurable
  public void setRolloverCron(CronType cron)
  {
    _logWriter.setRolloverCron(cron);
  }

  /**
   * Sets the log rollover period, rounded up to the nearest hour.
   *
   * @param period the new rollover period in milliseconds.
   */
  @Configurable
  public void setRolloverPeriod(Period period)
  {
    _logWriter.setRolloverPeriod(period);
  }

  /**
   * Sets the log rollover size, rounded up to the megabyte.
   *
   * @param size maximum size of the log file
   */
  @Configurable
  public void setRolloverSize(Bytes bytes)
  {
    _logWriter.setRolloverSize(bytes);
  }

  /**
   * Sets how often the log rollover will be checked.
   *
   * @param period how often the log rollover will be checked.
   */
  @Configurable
  public void setRolloverCheckTime(Period period)
  {
    _logWriter.setRolloverCheckPeriod(period.getPeriod());
  }

  /**
   * Sets the auto-flush attribute.
   */
  @Configurable
  public void setAutoFlush(boolean isAutoFlush)
  {
    _isAutoFlush =  isAutoFlush;
  }

  boolean isAutoFlush()
  {
    return _isAutoFlush;
  }

  /**
   * Sets the autoFlushTime
   */
  public void setAutoFlushTime(Period period)
  {
    _autoFlushTime = period.getPeriod();
  }

  /**
   * Sets the shared buffer attribute.
   */
  public void setSharedBuffer(boolean isSharedBuffer)
  {
  }

  /**
   * Adds an exclusion pattern.
   */
  @Configurable
  public void addExclude(Pattern pattern)
  {
    _excludeList.add(pattern);
    _excludes = new Pattern[_excludeList.size()];
    _excludeList.toArray(_excludes);
  }

  /**
   * Initialize the log.
   */
  @PostConstruct
  @Override
  public void init()
    throws ServletException, IOException
  {
    _isActive = true;

    if (_alarm != null) {
      if (_autoFlushTime > 0)
        _alarm.queue(_autoFlushTime);
      else
        _alarm.queue(60000);
    }
    

    if (_format == null)
      _format = "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"";

    ArrayList<Segment> segments = parseFormat(_format);

    _segments = new Segment[segments.size()];
    segments.toArray(_segments);

    if (_timeFormat == null || _timeFormat.equals("")) {
      _timeFormat = "[%d/%b/%Y:%H:%M:%S %z]";
      _timeFormatSecondOffset = 0;
      _timeFormatMinuteOffset = 0;
    }

    _logWriter.init();
    // _sharedBufferLock = _logWriter.getBufferLock();

    super.init();
  }

  /**
   * Parses the access log string.
   */
  private ArrayList<Segment> parseFormat(String format)
  {
    ArrayList<Segment> segments = new ArrayList<Segment>();
    CharBuffer cb = new CharBuffer();

    int i = 0;
    while (i < _format.length()) {
      char ch = _format.charAt(i++);

      if (ch != '%' || i >= _format.length()) {
        cb.append((char) ch);
        continue;
      }

      String arg = null;
      ch = _format.charAt(i++);
      if (ch == '>')
        ch = _format.charAt(i++);
      else if (ch == '{') {
        if (cb.length() > 0)
          segments.add(new Segment(this, Segment.TEXT, cb.toString()));
        cb.clear();
        while (i < _format.length() && _format.charAt(i++) != '}')
          cb.append(_format.charAt(i - 1));
        arg = cb.toString();
        cb.clear();

        ch = _format.charAt(i++);
      }

      switch (ch) {
      case 'b': case 'c':
      case 'h': case 'i': case 'l': case 'n':
      case 'r': case 's':
      case 'T': case 'D': case 'o':
      case 'u': case 'U':
      case 'v': case 'S':
        if (cb.length() > 0)
          segments.add(new Segment(this, Segment.TEXT, cb.toString()));
        cb.clear();
        segments.add(new Segment(this, ch, arg));
        break;

      case 't':
        if (cb.length() > 0)
          segments.add(new Segment(this, Segment.TEXT, cb.toString()));
        cb.clear();
        if (arg != null)
          _timeFormat = arg;
        segments.add(new Segment(this, ch, arg));
        break;

      default:
        cb.append('%');
        i--;
        break;
      }
    }

    cb.append(CauchoSystem.getNewlineString());
    segments.add(new Segment(this, Segment.TEXT, cb.toString()));

    return segments;
  }

  /**
   * Logs a request using the current format.
   */
  @Override
  public void log(HttpServletRequest req,
                  HttpServletResponse res,
                  ServletContext application)
    throws IOException
  {
    // server/1kk7
    CauchoRequest cRequest = (CauchoRequest) req;
    HttpServletResponseImpl responseImpl = (HttpServletResponseImpl) res;

    AbstractHttpRequest absRequest = cRequest.getAbstractHttpRequest();
    HttpServletRequestImpl request = absRequest.getRequestFacade();
    AbstractHttpResponse response = responseImpl.getAbstractHttpResponse();

    // skip excluded urls
    if (_excludes.length > 0) {
      byte []data = absRequest.getUriBuffer();
      int sublen = absRequest.getUriLength();

      String uri = new String(data, 0, sublen);

      for (Pattern pattern : _excludes) {
        if (pattern.matcher(uri).find()) {
          return;
        }
      }
    }

    LogBuffer logBuffer = _logWriter.allocateBuffer();

    try {
      byte []buffer = logBuffer.getBuffer();

      int length = log(request, responseImpl, response, buffer, 0, buffer.length);

      logBuffer.setLength(length);

      _logWriter.writeBuffer(logBuffer);
      logBuffer = null;
    } finally {
      if (logBuffer != null)
        _logWriter.freeBuffer(logBuffer);
    }
  }

  /**
   * Logs a request using the current format.
   *
   * @param request the servlet request.
   * @param response the servlet response.
   * @param buffer byte buffer containing the response
   * @param offset buffer starting offset
   * @param length length allowed in the buffer
   *
   * @return the new tail of the buffer
   */
  private int log(HttpServletRequestImpl request,
                  HttpServletResponseImpl responseFacade,
                  AbstractHttpResponse response,
                  byte []buffer, int offset, int length)
    throws IOException
  {
    AbstractHttpRequest absRequest = request.getAbstractHttpRequest();

    int len = _segments.length;
    for (int i = 0; i < len; i++) {
      Segment segment = _segments[i];
      String value = null;
      CharSegment csValue = null;

      switch (segment._code) {
      case Segment.TEXT:
        int sublen = segment._data.length;
        byte []data = segment._data;
        for (int j = 0; j < sublen; j++)
          buffer[offset++] = data[j];
        break;

      case Segment.CHAR:
        buffer[offset++] = segment._ch;
        break;

      case 'b':
        if (responseFacade.getStatus() == 304)
          buffer[offset++] = (byte) '-';
        else
          offset = print(buffer, offset, response.getContentLength());
        break;

        // cookie
      case 'c':
        Cookie cookie = request.getCookie(segment._string);
        if (cookie == null)
          cookie = responseFacade.getCookie(segment._string);
        if (cookie == null)
          buffer[offset++] = (byte) '-';
        else
          offset = print(buffer, offset, cookie.getValue());
        break;

        // set cookie
      case Segment.SET_COOKIE:
        ArrayList<Cookie> cookies = responseFacade.getCookies();
        if (cookies == null || cookies.size() == 0)
          buffer[offset++] = (byte) '-';
        else {
          _cb.clear();
          response.fillCookie(_cb, (Cookie) cookies.get(0), 0, 0, false);

          offset = print(buffer, offset, _cb.getBuffer(), 0, _cb.getLength());
        }
        break;

      case 'h':
        if (isHostnameDnsLookup()) {
          String addrName = request.getRemoteAddr();
          InetAddress addr = InetAddress.getByName(addrName);

          offset = print(buffer, offset, addr.getHostName());
        }
        else
          offset = absRequest.printRemoteAddr(buffer, offset);
        break;

        // input header
      case 'i':
        csValue = absRequest.getHeaderBuffer(segment._string);
        if (csValue == null)
          buffer[offset++] = (byte) '-';
        else
          offset = print(buffer, offset, csValue);
        break;

      case 'l':
        buffer[offset++] = (byte) '-';
        break;

        // request attribute
      case 'n':
        Object oValue = request.getAttribute(segment._string);
        if (oValue == null)
          buffer[offset++] = (byte) '-';
        else
          offset = print(buffer, offset, String.valueOf(oValue));
        break;

        // output header
      case 'o':
        value = response.getHeader(segment._string);
        if (value == null)
          buffer[offset++] = (byte) '-';
        else
          offset = print(buffer, offset, value);
        break;

      case 'r':
        offset = print(buffer, offset, request.getMethod());

        buffer[offset++] = (byte) ' ';

        data = absRequest.getUriBuffer();
        sublen = absRequest.getUriLength();

        // server/02e9
        if (buffer.length - offset - 128 < sublen) {
          sublen = buffer.length - offset - 128;
          System.arraycopy(data, 0, buffer, offset, sublen);
          offset += sublen;
          buffer[offset++] = (byte) '.';
          buffer[offset++] = (byte) '.';
          buffer[offset++] = (byte) '.';
        }
        else {
          System.arraycopy(data, 0, buffer, offset, sublen);
          offset += sublen;
        }

        buffer[offset++] = (byte) ' ';

        offset = print(buffer, offset, request.getProtocol());
        break;

      case 's':
        int status = responseFacade.getStatus();
        buffer[offset++] = (byte) ('0' + (status / 100) % 10);
        buffer[offset++] = (byte) ('0' + (status / 10) % 10);
        buffer[offset++] = (byte) ('0' + status % 10);
        break;
        
      case 'S':
        {
          String sessionId = request.getRequestedSessionId();
          if (! request.isRequestedSessionIdValid() || sessionId == null) {
            buffer[offset++] = (byte) '-';
          }
          else {
            offset = print(buffer, offset, sessionId);
          }
          break;
        }

      case 't':
        long date = Alarm.getCurrentTime();

        if (date / 1000 != _lastTime / 1000)
          fillTime(date);

        sublen = _timeBuffer.getLength();
        data = _timeBuffer.getBuffer();

        synchronized (_timeBuffer) {
          System.arraycopy(data, 0, buffer, offset, sublen);
        }

        offset += sublen;
        break;

      case 'T':
        {
          long startTime = request.getStartTime();
          long endTime = Alarm.getCurrentTime();

          offset = print(buffer, offset, (int) ((endTime - startTime + 500) / 1000));
          break;
        }

      case 'D':
        {
          long startTime = request.getStartTime();
          long endTime = Alarm.getCurrentTime();

          offset = print(buffer, offset, (int) ((endTime - startTime) * 1000));
          break;
        }

      case 'u':
        value = request.getRemoteUser(false);
        if (value == null)
          buffer[offset++] = (byte) '-';
        else {
          buffer[offset++] = (byte) '"';
          offset = print(buffer, offset, value);
          buffer[offset++] = (byte) '"';
        }
        break;

      case 'v':
        value = request.getServerName();
        if (value == null)
          buffer[offset++] = (byte) '-';
        else {
          offset = print(buffer, offset, value);
        }
        break;

      case 'U':
        offset = print(buffer, offset, request.getRequestURI());
        break;

      default:
        throw new IOException();
      }
    }

    return offset;
  }

  /**
   * Prints a CharSegment to the log.
   *
   * @param buffer receiving byte buffer.
   * @param offset offset into the receiving buffer.
   * @param cb the new char segment to be logged.
   * @return the new offset into the byte buffer.
   */
  private int print(byte []buffer, int offset, CharSegment cb)
  {
    char []charBuffer = cb.getBuffer();
    int cbOffset = cb.getOffset();
    int length = cb.getLength();

    // truncate for hacker attacks
    if (buffer.length - offset - 256 < length)
      length =  buffer.length - offset - 256;

    for (int i = length - 1; i >= 0; i--)
      buffer[offset + i] = (byte) charBuffer[cbOffset + i];

    return offset + length;
  }

  /**
   * Prints a String to the log.
   *
   * @param buffer receiving byte buffer.
   * @param offset offset into the receiving buffer.
   * @param s the new string to be logged.
   * @return the new offset into the byte buffer.
   */
  private int print(byte []buffer, int offset, String s)
  {
    int length = s.length();

    _cb.ensureCapacity(length);
    char []cBuf = _cb.getBuffer();

    s.getChars(0, length, cBuf, 0);

    for (int i = length - 1; i >= 0; i--)
      buffer[offset + i] = (byte) cBuf[i];

    return offset + length;
  }

  /**
   * Prints a String to the log.
   *
   * @param buffer receiving byte buffer.
   * @param offset offset into the receiving buffer.
   * @param s the new string to be logged.
   * @return the new offset into the byte buffer.
   */
  private int print(byte []buffer, int offset,
                    char []cb, int cbOff, int length)
  {
    for (int i = length - 1; i >= 0; i--)
      buffer[offset + i] = (byte) cb[cbOff + i];

    return offset + length;
  }

  /**
   * Prints an integer to the log.
   *
   * @param buffer receiving byte buffer.
   * @param offset offset into the receiving buffer.
   * @param v the new integer to be logged.
   * @return the new offset into the byte buffer.
   */
  private int print(byte []buffer, int offset, long v)
  {
    if (v == 0) {
      buffer[offset] = (byte) '0';
      return offset + 1;
    }

    if (v < 0) {
      buffer[offset++] = (byte) '-';
      v = -v;
    }

    int length = 0;
    int exp = 10;

    for (; exp <= v && exp > 0; length++)
      exp = 10 * exp;

    offset += length;
    for (int i = 0; i <= length; i++) {
      buffer[offset - i] = (byte) (v % 10 + '0');
      v = v / 10;
    }

    return offset + 1;
  }

  /**
   * Flushes the log.
   */
  @Override
  public void flush()
  {
    // server/0213, 021q
    _logWriter.flush();
    _logWriter.waitForFlush(5000L);
    _logWriter.rollover();
  }

  /**
   * The alarm listener.
   */
  @Override
  public void handleAlarm(Alarm alarm)
  {
    try {
      flush();
    } finally {
      alarm = _alarm;
      if (alarm != null && _isActive && _autoFlushTime > 0)
        alarm.queue(_autoFlushTime);
    }
  }

  /**
   * Closes the log, flushing the results.
   */
  @Override
  public void destroy()
    throws IOException
  {
    super.destroy();

    _isActive = false;

    Alarm alarm = _alarm;
    _alarm = null;

    if (alarm != null)
      alarm.dequeue();

    flush();

    _logWriter.close();
  }

  /**
   * Fills the time buffer with the formatted time.
   *
   * @param date current time in milliseconds
   */
  private void fillTime(long date)
    throws IOException
  {
    synchronized (_timeBuffer) {
      if (date / 1000 == _lastTime / 1000)
        return;

      if (_timeFormatSecondOffset >= 0
          && date / 3600000 == _lastTime / 3600000) {
        byte []bBuf = _timeBuffer.getBuffer();

        int min = (int) (date / 60000 % 60);
        int sec = (int) (date / 1000 % 60);

        bBuf[_timeFormatMinuteOffset + 0] = (byte) ('0' + min / 10);
        bBuf[_timeFormatMinuteOffset + 1] = (byte) ('0' + min % 10);

        bBuf[_timeFormatSecondOffset + 0] = (byte) ('0' + sec / 10);
        bBuf[_timeFormatSecondOffset + 1] = (byte) ('0' + sec % 10);

        _lastTime = date;

        return;
      }

      _timeCharBuffer.clear();
      QDate.formatLocal(_timeCharBuffer, date, _timeFormat);

      if (_timeFormatSecondOffset >= 0) {
        _timeFormatSecondOffset = _timeCharBuffer.lastIndexOf(':') + 1;
        _timeFormatMinuteOffset = _timeFormatSecondOffset - 3;
      }

      char []cBuf = _timeCharBuffer.getBuffer();
      int length = _timeCharBuffer.getLength();

      _timeBuffer.setLength(length);
      byte []bBuf = _timeBuffer.getBuffer();

      for (int i = length - 1; i >= 0; i--)
        bBuf[i] = (byte) cBuf[i];
    }

    _lastTime = date;
  }

  /**
   * Represents one portion of the access log.
   */
  static class Segment {
    final static int TEXT = 0;
    final static int CHAR = 1;
    final static int SET_COOKIE = 2;

    int _code;
    byte []_data;
    byte _ch;
    String _string;
    AccessLog _log;

    /**
     * Creates a new log segment.
     *
     * @param log the owning log
     * @param code the segment code, telling what kind of segment it is
     * @param string the parameter for the segment code.
     */
    Segment(AccessLog log, int code, String string)
    {
      _log = log;
      _code = code;

      _string = string;
      if (string != null) {
        if (code == 'o' && string.equalsIgnoreCase("Set-Cookie"))
          _code = SET_COOKIE;

        _data = _string.getBytes();
        if (code == TEXT && _string.length() == 1) {
          _ch = (byte) _string.charAt(0);
          _code = CHAR;
        }
      }
    }
  }
}
