content: /*
   * 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.
   */
  
  /* Portions copyright (C) The Apache Software Foundation. All rights reserved.
   *
   * This software is published under the terms of the Apache Software
   * License version 1.1, a copy of which has been included with this
   * distribution in the LICENSE.txt file.  */
  
  package org.apache.hadoop.chukwa.inputtools.log4j;
  
  
  import java.io.File;
  import java.io.FilenameFilter;
  import java.io.IOException;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.Collections;
  import java.util.Date;
  import java.util.GregorianCalendar;
  import java.util.List;
  import java.util.Locale;
  import java.util.TimeZone;
  import java.util.regex.Pattern;
  
  import org.apache.hadoop.chukwa.datacollection.controller.ChukwaAgentController;
  import org.apache.hadoop.chukwa.datacollection.controller.ClientFinalizer;
  import org.apache.hadoop.chukwa.util.AdaptorNamingUtils;
  import org.apache.hadoop.chukwa.util.RecordConstants;
  import org.apache.log4j.FileAppender;
  import org.apache.log4j.Layout;
  import org.apache.log4j.Logger;
  import org.apache.log4j.helpers.LogLog;
  import org.apache.log4j.spi.LoggingEvent;
  
  /**
      ChukwaDailyRollingFileAppender is a slightly modified version of
      DailyRollingFileAppender, with modified versions of its
      <code>subAppend()</code> and <code>rollOver()</code> functions. 
      We would have preferred to sub-class DailyRollingFileAppender but
      its authors clearly did not intend that to be a viable option since
      they made too much of the class private or package-private
  
      DailyRollingFileAppender extends {@link FileAppender} so that the
      underlying file is rolled over at a user chosen frequency.
  
      <p>The rolling schedule is specified by the <b>DatePattern</b>
      option. This pattern should follow the {@link SimpleDateFormat}
      conventions. In particular, you <em>must</em> escape literal text
      within a pair of single quotes. A formatted version of the date
      pattern is used as the suffix for the rolled file name.</p>
  
      <p>For example, if the <b>File</b> option is set to
      <code>/foo/bar.log</code> and the <b>DatePattern</b> set to
      <code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging
      file <code>/foo/bar.log</code> will be copied to
      <code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-17
      will continue in <code>/foo/bar.log</code> until it rolls over
      the next day.</p>
  
      <p>Is is possible to specify monthly, weekly, half-daily, daily,
      hourly, or minutely rollover schedules.</p>
  
      <table border="1" cellpadding="2" summary="">
      <tr>
      <th>DatePattern</th>
      <th>Rollover schedule</th>
      <th>Example</th></tr>
  
      <tr>
      <td><code>'.'yyyy-MM</code>
      <td>Rollover at the beginning of each month</td>
  
      <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be
      copied to <code>/foo/bar.log.2002-05</code>. Logging for the month
      of June will be output to <code>/foo/bar.log</code> until it is
      also rolled over the next month.</td></tr>
  
      <tr>
      <td><code>'.'yyyy-ww</code>
  
      <td>Rollover at the first day of each week. The first day of the
      week depends on the locale.</td>
  
      <td>Assuming the first day of the week is Sunday, on Saturday
      midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be
      copied to <i>/foo/bar.log.2002-23</i>.  Logging for the 24th week
      of 2002 will be output to <code>/foo/bar.log</code> until it is
      rolled over the next week.</td></tr>
  
      <tr>
      <td><code>'.'yyyy-MM-dd</code>
  
      <td>Rollover at midnight each day.</td>
  
      <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will
      be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the
      9th day of March will be output to <code>/foo/bar.log</code> until
      it is rolled over the next day.</td></tr>
  
      <tr>
      <td><code>'.'yyyy-MM-dd-a</code>
  
      <td>Rollover at midnight and midday of each day.</td>
  
      <td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be
      copied to <code>/foo/bar.log.2002-03-09-AM</code>. Logging for the
      afternoon of the 9th will be output to <code>/foo/bar.log</code>
      until it is rolled over at midnight.</td></tr>
  
      <tr>
      <td><code>'.'yyyy-MM-dd-HH</code>
  
      <td>Rollover at the top of every hour.</td>
  
      <td>At approximately 11:00.000 o'clock on March 9th, 2002,
      <code>/foo/bar.log</code> will be copied to
      <code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour
      of the 9th of March will be output to <code>/foo/bar.log</code>
      until it is rolled over at the beginning of the next hour.</td></tr>
  
  
      <tr>
      <td><code>'.'yyyy-MM-dd-HH-mm</code></td>
  
      <td>Rollover at the beginning of every minute.</td>
  
      <td>At approximately 11:23,000, on March 9th, 2001,
      <code>/foo/bar.log</code> will be copied to
      <code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute
      of 11:23 (9th of March) will be output to
      <code>/foo/bar.log</code> until it is rolled over the next minute.</td></tr>
      </table>
  
      <p>Do not use the colon ":" character in anywhere in the
      <b>DatePattern</b> option. The text before the colon is interpeted
      as the protocol specificaion of a URL which is probably not what
      you want.</p> */
  
  public class ChukwaDailyRollingFileAppender extends FileAppender {
  
    static Logger log = Logger.getLogger(ChukwaDailyRollingFileAppender.class);
    // The code assumes that the following constants are in a increasing
    // sequence.
    static final int TOP_OF_TROUBLE = -1;
    static final int TOP_OF_MINUTE = 0;
    static final int TOP_OF_HOUR = 1;
    static final int HALF_DAY = 2;
    static final int TOP_OF_DAY = 3;
    static final int TOP_OF_WEEK = 4;
    static final int TOP_OF_MONTH = 5;
  
    static final String adaptorType = ChukwaAgentController.CharFileTailUTF8NewLineEscaped;
  
    static final Object lock = new Object();
    static String lastRotation = "";
  
    /**
     * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning
     * daily rollover.
     */
    private String datePattern = "'.'yyyy-MM-dd";
  
    /**
     * The log file will be renamed to the value of the scheduledFilename variable
     * when the next interval is entered. For example, if the rollover period is
     * one hour, the log file will be renamed to the value of "scheduledFilename"
     * at the beginning of the next hour.
     * 
     * The precise time when a rollover occurs depends on logging activity.
     */
    private String scheduledFilename;
  
    /**
     * The next time we estimate a rollover should occur.
     */
    private long nextCheck = System.currentTimeMillis() - 1;
  
    /**
     * Regex to select log files to be deleted
     */
    private String cleanUpRegex = null;
  
    /**
     * Set the maximum number of backup files to keep around.
     */
    private int maxBackupIndex = 10;
  
    private ClientFinalizer clientFinalizer = null;
    
    boolean hasBeenActivated = false;
    Date now = new Date();
  
    SimpleDateFormat sdf;
  
    RollingCalendar rc = new RollingCalendar();
  
    int checkPeriod = TOP_OF_TROUBLE;
  
    ChukwaAgentController chukwaClient;
    boolean chukwaClientIsNull = true;
    static final Object chukwaLock = new Object();
  
    String chukwaClientHostname;
    int chukwaClientPortNum;
    long chukwaClientConnectNumRetry;
    long chukwaClientConnectRetryInterval;
  
    String recordType;
  
    // The gmtTimeZone is used only in computeCheckPeriod() method.
    static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
  
    /**
     * The default constructor does nothing.
     * @throws IOException if constructor initialization error
     */
    public ChukwaDailyRollingFileAppender() throws IOException {
      super();
    }
  
  /**
       Instantiate a <code>DailyRollingFileAppender</code> and open the
       file designated by <code>filename</code>. The opened filename will
       become the output destination for this appender.
   * @param layout is logging layout
   * @param filename is the filename to write logs
   * @param datePattern is the date pattern of log suffix
   * @throws IOException if constructor initialization error
  
     */
    public ChukwaDailyRollingFileAppender(Layout layout, String filename,
                                          String datePattern) throws IOException {
      super(layout, filename, true);
      System.out
          .println("Daily Rolling File Appender successfully registered file with agent: "
              + filename);
      this.datePattern = datePattern;
    }
  
    /**
     * The <b>DatePattern</b> takes a string in the same format as expected by
     * {@link SimpleDateFormat}. This options determines the rollover schedule.
     * @param pattern is date formatting pattern
     */
    public void setDatePattern(String pattern) {
      datePattern = pattern;
    }
  
    /** Returns the value of the <b>DatePattern</b> option. 
     * @return date pattern
     */
    public String getDatePattern() {
      return datePattern;
    }
  
    public String getRecordType() {
      if (recordType != null)
        return recordType;
      else
        return "unknown";
    }
  
    public void setRecordType(String recordType) {
      this.recordType = recordType;
    }
  
    public void activateOptions() {
      
      // Prevent early initialisation
      if (!hasBeenActivated)
      { return;}
      
      super.activateOptions();
      if (datePattern != null && fileName != null) {
        now.setTime(System.currentTimeMillis());
        sdf = new SimpleDateFormat(datePattern);
        int type = computeCheckPeriod();
        printPeriodicity(type);
        rc.setType(type);
        File file = new File(fileName);
        scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));
  
      } else {
        LogLog
            .error("Either File or DatePattern options are not set for appender ["
                + name + "].");
      }
    }
  
    void printPeriodicity(int type) {
      switch (type) {
      case TOP_OF_MINUTE:
        LogLog.debug("Appender [" + name + "] to be rolled every minute.");
        break;
      case TOP_OF_HOUR:
        LogLog
            .debug("Appender [" + name + "] to be rolled on top of every hour.");
        break;
      case HALF_DAY:
        LogLog.debug("Appender [" + name
            + "] to be rolled at midday and midnight.");
        break;
      case TOP_OF_DAY:
        LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
        break;
      case TOP_OF_WEEK:
        LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
        break;
      case TOP_OF_MONTH:
        LogLog.debug("Appender [" + name
            + "] to be rolled at start of every month.");
        break;
      default:
        LogLog.warn("Unknown periodicity for appender [" + name + "].");
      }
    }
  
    // This method computes the roll over period by looping over the
    // periods, starting with the shortest, and stopping when the r0 is
    // different from from r1, where r0 is the epoch formatted according
    // the datePattern (supplied by the user) and r1 is the
    // epoch+nextMillis(i) formatted according to datePattern. All date
    // formatting is done in GMT and not local format because the test
    // logic is based on comparisons relative to 1970-01-01 00:00:00
    // GMT (the epoch).
  
    int computeCheckPeriod() {
      RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone,
          Locale.ENGLISH);
      // set sate to 1970-01-01 00:00:00 GMT
      Date epoch = new Date(0);
      if (datePattern != null) {
        for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
          SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
          simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in
                                                     // GMT
          String r0 = simpleDateFormat.format(epoch);
          rollingCalendar.setType(i);
          Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
          String r1 = simpleDateFormat.format(next);
          // System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
          if (r0 != null && r1 != null && !r0.equals(r1)) {
            return i;
          }
        }
      }
      return TOP_OF_TROUBLE; // Deliberately head for trouble...
    }
  
    /**
     * Rollover the current file to a new file.
     */
    void rollOver() throws IOException {
  
      /* Compute filename, but only if datePattern is specified */
      if (datePattern == null) {
        errorHandler.error("Missing DatePattern option in rollOver().");
        return;
      }
  
      String datedFilename = fileName + sdf.format(now);
      // It is too early to roll over because we are still within the
      // bounds of the current interval. Rollover will occur once the
      // next interval is reached.
      if (scheduledFilename.equals(datedFilename)) {
        return;
      }
  
      // close current file, and rename it to datedFilename
      this.closeFile();
  
      File target = new File(scheduledFilename);
      if (target.exists()) {
        if(!target.delete()) {
          LogLog.warn("Unable to remove: "+target.getAbsolutePath());
        };
      }
  
      File file = new File(fileName);
  
      boolean result = file.renameTo(target);
      if (result) {
        LogLog.debug(fileName + " -> " + scheduledFilename);
      } else {
        LogLog.error("Failed to rename [" + fileName + "] to ["
            + scheduledFilename + "].");
      }
  
      try {
        // This will also close the file. This is OK since multiple
        // close operations are safe.
        this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
      } catch (IOException e) {
        errorHandler.error("setFile(" + fileName + ", false) call failed.");
      }
      scheduledFilename = datedFilename;
      cleanUp();
    }
  
    public synchronized String getCleanUpRegex() {
      return cleanUpRegex;
    }
  
    protected synchronized void setCleanUpRegex(String cleanUpRegex) {
      this.cleanUpRegex = cleanUpRegex;
    }
  
    public int getMaxBackupIndex() {
      return maxBackupIndex;
    }
  
    public void setMaxBackupIndex(int maxBackupIndex) {
      this.maxBackupIndex = maxBackupIndex;
    }
  
    protected synchronized void cleanUp() {
      String regex = "";
      try {
        File actualFile = new File(fileName);
  
        String directoryName = actualFile.getParent();
        String actualFileName = actualFile.getName();
        File dirList = new File(directoryName);
  
        if (cleanUpRegex == null || !cleanUpRegex.contains("$fileName")) {
          LogLog
              .error("cleanUpRegex == null || !cleanUpRegex.contains(\"$fileName\")");
          return;
        }
        regex = cleanUpRegex.replace("$fileName", actualFileName);
        String[] dirFiles = dirList.list(new LogFilter(actualFileName, regex));
  
        List<String> files = new ArrayList<String>();
        if(dirFiles!=null) {
          for (String file : dirFiles) {
            files.add(file);
          }
        }
        Collections.sort(files);
  
        while (files.size() > maxBackupIndex) {
          String file = files.remove(0);
          File f = new File(directoryName + "/" + file);
          if(!f.delete()) {
            LogLog.warn("Cannot remove: " + file);
          }
        }
      } catch (Exception e) {
        errorHandler
            .error("cleanUp(" + fileName + "," + regex + ") call failed.");
      }
    }
  
    private static class LogFilter implements FilenameFilter {
      private Pattern p = null;
      private String logFile = null;
  
      public LogFilter(String logFile, String regex) {
        this.logFile = logFile;
        p = Pattern.compile(regex);
      }
  
      @Override
      public boolean accept(File dir, String name) {
        // ignore current log file
        if (name.intern() == this.logFile.intern()) {
          return false;
        }
        // ignore file without the same prefix
        if (!name.startsWith(logFile)) {
          return false;
        }
        return p.matcher(name).find();
      }
    }
  
    /**
     * Fix for late-initialisation
     */
    @Override
    protected boolean checkEntryConditions() {
      synchronized(chukwaLock) {
        if (!hasBeenActivated) {
          hasBeenActivated = true;
          activateOptions();
        }
      }
      return super.checkEntryConditions();
    }
  
    /**
     * This method differentiates DailyRollingFileAppender from its super class.
     * 
     * <p>Before actually logging, this method will check whether it is time to do
     * a rollover. If it is, it will schedule the next rollover time and then
     * rollover.
     * */
    protected void subAppend(LoggingEvent event) {
      try {
        // we set up the chukwa adaptor here because this is the first
        // point which is called after all setters have been called with
        // their values from the log4j.properties file, in particular we
        // needed to give setCukwaClientPortNum() and -Hostname() a shot
  
        // Make sure only one thread can do this
        // and use the boolean to avoid the first level locking
        if (chukwaClientIsNull) {
          synchronized (chukwaLock) {
  
            String log4jFileName = getFile(); 
            String recordType = getRecordType();
  
            long currentLength = 0L;
            try {
              File fooLog = new File(log4jFileName);
              log4jFileName = fooLog.getAbsolutePath();
              currentLength = fooLog.length();
            } catch (Throwable e) {
              log.warn("Exception while trying to get current file size for " + log4jFileName);
              currentLength = 0L;
            }
  
            if (chukwaClient == null) {
              if (getChukwaClientHostname() != null
                  && getChukwaClientPortNum() != 0) {
                chukwaClient = new ChukwaAgentController(
                    getChukwaClientHostname(), getChukwaClientPortNum());
                log.debug("setup adaptor with hostname "
                    + getChukwaClientHostname() + " and portnum "
                    + getChukwaClientPortNum());
              } else {
                chukwaClient = new ChukwaAgentController();
                log
                .debug("setup adaptor with no args, which means it used its defaults");
              }
  
              chukwaClientIsNull = false;
  
              // Watchdog is watching for ChukwaAgent only once every 5 minutes,
              // so there's no point in retrying more than once every 5 mins.
              // In practice, if the watchdog is not able to automatically restart
              // the agent, it will take more than 20 minutes to get Ops to
              // restart it.
              // Also its a good to limit the number of communications between
              // Hadoop and Chukwa, that's why 30 minutes.
              long retryInterval = chukwaClientConnectRetryInterval;
              if (retryInterval == 0) {
                retryInterval = 1000 * 60 * 30;
              }
              long numRetries = chukwaClientConnectNumRetry;
              if (numRetries == 0) {
                numRetries = 48;
              }
  
              String name = AdaptorNamingUtils.synthesizeAdaptorID
                (ChukwaAgentController.CharFileTailUTF8NewLineEscaped, recordType, log4jFileName);
              
              String adaptorID = chukwaClient.addByName(name, ChukwaAgentController.CharFileTailUTF8NewLineEscaped,
                  recordType,currentLength + " " + log4jFileName, currentLength,
                  numRetries, retryInterval);
  
              // Setup a shutdownHook for the controller
              clientFinalizer = new ClientFinalizer(chukwaClient);
              Runtime.getRuntime().addShutdownHook(clientFinalizer);
  
              if (adaptorID != null) {
                log.debug("Added file tailing adaptor to chukwa agent for file "
                    + log4jFileName + ", adaptorId:" + adaptorID 
                    + " using this recordType :" + recordType 
                    + ", starting at offset:" + currentLength);
              } else {
                log.debug("Chukwa adaptor not added, addFile(" + log4jFileName
                    + ") returned, current offset: " + currentLength);
              }
  
            }
          }
        }
  
        long n = System.currentTimeMillis();
        if (n >= nextCheck) {
          now.setTime(n);
          nextCheck = rc.getNextCheckMillis(now);
          try {
            rollOver();
          } catch (IOException ioe) {
            LogLog.error("rollOver() failed.", ioe);
          }
        }
  
        boolean written = false;
        if(layout.ignoresThrowable()) {
          String[] s = event.getThrowableStrRep();
          if (s != null) {
            int len = s.length;
            StringBuilder sb = new StringBuilder();
            sb.append(this.layout.format(event));
            for(int i = 0; i < len; i++) {
              sb.append(s[i]).append("\n");
            }
            //escape the newlines from record bodies, exception and then write this record to the log file
            written = true;
            this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",sb.toString()));
          } 
        }
         
        if (!written) {
          //escape the newlines from record bodies and then write this record to the log file
          this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",this.layout.format(event)));
        }
        
        if (this.immediateFlush) {
          this.qw.flush();
        }
      } catch (Throwable e) {
        System.err.println("Exception in ChukwaRollingAppender: "
            + e.getMessage());
        e.printStackTrace();
      }
  
    }
  
    public String getChukwaClientHostname() {
      return chukwaClientHostname;
    }
  
    public void setChukwaClientHostname(String chukwaClientHostname) {
      this.chukwaClientHostname = chukwaClientHostname;
    }
  
    public int getChukwaClientPortNum() {
      return chukwaClientPortNum;
    }
  
    public void setChukwaClientPortNum(int chukwaClientPortNum) {
      this.chukwaClientPortNum = chukwaClientPortNum;
    }
  
    public void setChukwaClientConnectNumRetry(int i) {
      this.chukwaClientConnectNumRetry = i;
    }
  
    public void setChukwaClientConnectRetryInterval(long i) {
      this.chukwaClientConnectRetryInterval = i;
    }
  
  }
  
  
  /**
   * RollingCalendar is a helper class to DailyRollingFileAppender. Given a
   * periodicity type and the current time, it computes the start of the next
   * interval.
   * */
  class RollingCalendar extends GregorianCalendar {
  
    /**
  	 * 
  	 */
    private static final long serialVersionUID = 2153481574198792767L;
    int type = ChukwaDailyRollingFileAppender.TOP_OF_TROUBLE;
  
    RollingCalendar() {
      super();
    }
  
    RollingCalendar(TimeZone tz, Locale locale) {
      super(tz, locale);
    }
  
    void setType(int type) {
      this.type = type;
    }
  
    public long getNextCheckMillis(Date now) {
      return getNextCheckDate(now).getTime();
    }
  
    public Date getNextCheckDate(Date now) {
      this.setTime(now);
  
      switch (type) {
      case ChukwaDailyRollingFileAppender.TOP_OF_MINUTE:
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        this.add(Calendar.MINUTE, 1);
        break;
      case ChukwaDailyRollingFileAppender.TOP_OF_HOUR:
        this.set(Calendar.MINUTE, 0);
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        this.add(Calendar.HOUR_OF_DAY, 1);
        break;
      case ChukwaDailyRollingFileAppender.HALF_DAY:
        this.set(Calendar.MINUTE, 0);
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        int hour = get(Calendar.HOUR_OF_DAY);
        if (hour < 12) {
          this.set(Calendar.HOUR_OF_DAY, 12);
        } else {
          this.set(Calendar.HOUR_OF_DAY, 0);
          this.add(Calendar.DAY_OF_MONTH, 1);
        }
        break;
      case ChukwaDailyRollingFileAppender.TOP_OF_DAY:
        this.set(Calendar.HOUR_OF_DAY, 0);
        this.set(Calendar.MINUTE, 0);
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        this.add(Calendar.DATE, 1);
        break;
      case ChukwaDailyRollingFileAppender.TOP_OF_WEEK:
        this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
        this.set(Calendar.HOUR_OF_DAY, 0);
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        this.add(Calendar.WEEK_OF_YEAR, 1);
        break;
      case ChukwaDailyRollingFileAppender.TOP_OF_MONTH:
        this.set(Calendar.DATE, 1);
        this.set(Calendar.HOUR_OF_DAY, 0);
        this.set(Calendar.SECOND, 0);
        this.set(Calendar.MILLISECOND, 0);
        this.add(Calendar.MONTH, 1);
        break;
      default:
        throw new IllegalStateException("Unknown periodicity type.");
      }
      return getTime();
    }
    
    @Override
    public boolean equals(Object o) {
      return super.equals(o);
    }
    
    @Override
    public int hashCode() {
      return super.hashCode();
    }
  }
  