1/*2 * Licensed to the Apache Software Foundation (ASF) under one3 * or more contributor license agreements. See the NOTICE file4 * distributed with this work for additional information5 * regarding copyright ownership. The ASF licenses this file6 * to you under the Apache License, Version 2.0 (the7 * "License"); you may not use this file except in compliance8 * with the License. You may obtain a copy of the License at9 *10 * http://www.apache.org/licenses/LICENSE-2.011 *12 * Unless required by applicable law or agreed to in writing, software13 * distributed under the License is distributed on an "AS IS" BASIS,14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.15 * See the License for the specific language governing permissions and16 * limitations under the License.17 */1819/* Portions copyright (C) The Apache Software Foundation. All rights reserved.20 *21 * This software is published under the terms of the Apache Software22 * License version 1.1, a copy of which has been included with this23 * distribution in the LICENSE.txt file. */2425package org.apache.hadoop.chukwa.inputtools.log4j;
262728import java.io.File;
29import java.io.FilenameFilter;
30import java.io.IOException;
31import java.text.SimpleDateFormat;
32import java.util.ArrayList;
33import java.util.Calendar;
34import java.util.Collections;
35import java.util.Date;
36import java.util.GregorianCalendar;
37import java.util.List;
38import java.util.Locale;
39import java.util.TimeZone;
40import java.util.regex.Pattern;
4142import org.apache.hadoop.chukwa.datacollection.controller.ChukwaAgentController;
43import org.apache.hadoop.chukwa.datacollection.controller.ClientFinalizer;
44import org.apache.hadoop.chukwa.util.AdaptorNamingUtils;
45import org.apache.hadoop.chukwa.util.RecordConstants;
46import org.apache.log4j.FileAppender;
47import org.apache.log4j.Layout;
48import org.apache.log4j.Logger;
49import org.apache.log4j.helpers.LogLog;
50import org.apache.log4j.spi.LoggingEvent;
5152/**53 ChukwaDailyRollingFileAppender is a slightly modified version of54 DailyRollingFileAppender, with modified versions of its55 <code>subAppend()</code> and <code>rollOver()</code> functions. 56 We would have preferred to sub-class DailyRollingFileAppender but57 its authors clearly did not intend that to be a viable option since58 they made too much of the class private or package-private5960 DailyRollingFileAppender extends {@link FileAppender} so that the61 underlying file is rolled over at a user chosen frequency.6263 <p>The rolling schedule is specified by the <b>DatePattern</b>64 option. This pattern should follow the {@link SimpleDateFormat}65 conventions. In particular, you <em>must</em> escape literal text66 within a pair of single quotes. A formatted version of the date67 pattern is used as the suffix for the rolled file name.</p>6869 <p>For example, if the <b>File</b> option is set to70 <code>/foo/bar.log</code> and the <b>DatePattern</b> set to71 <code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging72 file <code>/foo/bar.log</code> will be copied to73 <code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-1774 will continue in <code>/foo/bar.log</code> until it rolls over75 the next day.</p>7677 <p>Is is possible to specify monthly, weekly, half-daily, daily,78 hourly, or minutely rollover schedules.</p>7980 <table border="1" cellpadding="2" summary="">81 <tr>82 <th>DatePattern</th>83 <th>Rollover schedule</th>84 <th>Example</th></tr>8586 <tr>87 <td><code>'.'yyyy-MM</code>88 <td>Rollover at the beginning of each month</td>8990 <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be91 copied to <code>/foo/bar.log.2002-05</code>. Logging for the month92 of June will be output to <code>/foo/bar.log</code> until it is93 also rolled over the next month.</td></tr>9495 <tr>96 <td><code>'.'yyyy-ww</code>9798 <td>Rollover at the first day of each week. The first day of the99 week depends on the locale.</td>100101 <td>Assuming the first day of the week is Sunday, on Saturday102 midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be103 copied to <i>/foo/bar.log.2002-23</i>. Logging for the 24th week104 of 2002 will be output to <code>/foo/bar.log</code> until it is105 rolled over the next week.</td></tr>106107 <tr>108 <td><code>'.'yyyy-MM-dd</code>109110 <td>Rollover at midnight each day.</td>111112 <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will113 be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the114 9th day of March will be output to <code>/foo/bar.log</code> until115 it is rolled over the next day.</td></tr>116117 <tr>118 <td><code>'.'yyyy-MM-dd-a</code>119120 <td>Rollover at midnight and midday of each day.</td>121122 <td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be123 copied to <code>/foo/bar.log.2002-03-09-AM</code>. Logging for the124 afternoon of the 9th will be output to <code>/foo/bar.log</code>125 until it is rolled over at midnight.</td></tr>126127 <tr>128 <td><code>'.'yyyy-MM-dd-HH</code>129130 <td>Rollover at the top of every hour.</td>131132 <td>At approximately 11:00.000 o'clock on March 9th, 2002,133 <code>/foo/bar.log</code> will be copied to134 <code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour135 of the 9th of March will be output to <code>/foo/bar.log</code>136 until it is rolled over at the beginning of the next hour.</td></tr>137138139 <tr>140 <td><code>'.'yyyy-MM-dd-HH-mm</code></td>141142 <td>Rollover at the beginning of every minute.</td>143144 <td>At approximately 11:23,000, on March 9th, 2001,145 <code>/foo/bar.log</code> will be copied to146 <code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute147 of 11:23 (9th of March) will be output to148 <code>/foo/bar.log</code> until it is rolled over the next minute.</td></tr>149 </table>150151 <p>Do not use the colon ":" character in anywhere in the152 <b>DatePattern</b> option. The text before the colon is interpeted153 as the protocol specificaion of a URL which is probably not what154 you want.</p> */155156publicclassChukwaDailyRollingFileAppenderextends FileAppender {
157158static Logger log = Logger.getLogger(ChukwaDailyRollingFileAppender.class);
159// The code assumes that the following constants are in a increasing160// sequence.161staticfinalint TOP_OF_TROUBLE = -1;
162staticfinalint TOP_OF_MINUTE = 0;
163staticfinalint TOP_OF_HOUR = 1;
164staticfinalint HALF_DAY = 2;
165staticfinalint TOP_OF_DAY = 3;
166staticfinalint TOP_OF_WEEK = 4;
167staticfinalint TOP_OF_MONTH = 5;
168169staticfinal String adaptorType = ChukwaAgentController.CharFileTailUTF8NewLineEscaped;
170171staticfinal Object lock = new Object();
172static String lastRotation = "";
173174/**175 * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning176 * daily rollover.177 */178private String datePattern = "'.'yyyy-MM-dd";
179180/**181 * The log file will be renamed to the value of the scheduledFilename variable182 * when the next interval is entered. For example, if the rollover period is183 * one hour, the log file will be renamed to the value of "scheduledFilename"184 * at the beginning of the next hour.185 * 186 * The precise time when a rollover occurs depends on logging activity.187 */188private String scheduledFilename;
189190/**191 * The next time we estimate a rollover should occur.192 */193privatelong nextCheck = System.currentTimeMillis() - 1;
194195/**196 * Regex to select log files to be deleted197 */198private String cleanUpRegex = null;
199200/**201 * Set the maximum number of backup files to keep around.202 */203privateint maxBackupIndex = 10;
204205privateClientFinalizer clientFinalizer = null;
206207boolean hasBeenActivated = false;
208 Date now = new Date();
209210 SimpleDateFormat sdf;
211212RollingCalendar rc = newRollingCalendar();
213214int checkPeriod = TOP_OF_TROUBLE;
215216ChukwaAgentController chukwaClient;
217boolean chukwaClientIsNull = true;
218staticfinal Object chukwaLock = new Object();
219220 String chukwaClientHostname;
221int chukwaClientPortNum;
222long chukwaClientConnectNumRetry;
223long chukwaClientConnectRetryInterval;
224225 String recordType;
226227// The gmtTimeZone is used only in computeCheckPeriod() method.228staticfinal TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
229230/**231 * The default constructor does nothing.232 * @throws IOException if constructor initialization error233 */234publicChukwaDailyRollingFileAppender() throws IOException {
235super();
236 }
237238/**239 Instantiate a <code>DailyRollingFileAppender</code> and open the240 file designated by <code>filename</code>. The opened filename will241 become the output destination for this appender.242 * @param layout is logging layout243 * @param filename is the filename to write logs244 * @param datePattern is the date pattern of log suffix245 * @throws IOException if constructor initialization error246247 */248publicChukwaDailyRollingFileAppender(Layout layout, String filename,
249 String datePattern) throws IOException {
250super(layout, filename, true);
251 System.out
252 .println("Daily Rolling File Appender successfully registered file with agent: "253 + filename);
254this.datePattern = datePattern;
255 }
256257/**258 * The <b>DatePattern</b> takes a string in the same format as expected by259 * {@link SimpleDateFormat}. This options determines the rollover schedule.260 * @param pattern is date formatting pattern261 */262publicvoid setDatePattern(String pattern) {
263 datePattern = pattern;
264 }
265266/** Returns the value of the <b>DatePattern</b> option. 267 * @return date pattern268 */269public String getDatePattern() {
270return datePattern;
271 }
272273public String getRecordType() {
274if (recordType != null)
275return recordType;
276else277return"unknown";
278 }
279280publicvoid setRecordType(String recordType) {
281this.recordType = recordType;
282 }
283284publicvoid activateOptions() {
285286// Prevent early initialisation287if (!hasBeenActivated)
288 { return;}
289290super.activateOptions();
291if (datePattern != null && fileName != null) {
292 now.setTime(System.currentTimeMillis());
293 sdf = new SimpleDateFormat(datePattern);
294int type = computeCheckPeriod();
295 printPeriodicity(type);
296 rc.setType(type);
297 File file = new File(fileName);
298 scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));
299300 } else {
301 LogLog
302 .error("Either File or DatePattern options are not set for appender ["303 + name + "].");
304 }
305 }
306307void printPeriodicity(int type) {
308switch (type) {
309case TOP_OF_MINUTE:
310 LogLog.debug("Appender [" + name + "] to be rolled every minute.");
311break;
312case TOP_OF_HOUR:
313 LogLog
314 .debug("Appender [" + name + "] to be rolled on top of every hour.");
315break;
316case HALF_DAY:
317 LogLog.debug("Appender [" + name
318 + "] to be rolled at midday and midnight.");
319break;
320case TOP_OF_DAY:
321 LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
322break;
323case TOP_OF_WEEK:
324 LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
325break;
326case TOP_OF_MONTH:
327 LogLog.debug("Appender [" + name
328 + "] to be rolled at start of every month.");
329break;
330default:
331 LogLog.warn("Unknown periodicity for appender [" + name + "].");
332 }
333 }
334335// This method computes the roll over period by looping over the336// periods, starting with the shortest, and stopping when the r0 is337// different from from r1, where r0 is the epoch formatted according338// the datePattern (supplied by the user) and r1 is the339// epoch+nextMillis(i) formatted according to datePattern. All date340// formatting is done in GMT and not local format because the test341// logic is based on comparisons relative to 1970-01-01 00:00:00342// GMT (the epoch).343344int computeCheckPeriod() {
345RollingCalendar rollingCalendar = newRollingCalendar(gmtTimeZone,
346 Locale.ENGLISH);
347// set sate to 1970-01-01 00:00:00 GMT348 Date epoch = new Date(0);
349if (datePattern != null) {
350for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
351 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
352 simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in353// GMT354 String r0 = simpleDateFormat.format(epoch);
355 rollingCalendar.setType(i);
356 Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
357 String r1 = simpleDateFormat.format(next);
358// System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);359if (r0 != null && r1 != null && !r0.equals(r1)) {
360return i;
361 }
362 }
363 }
364return TOP_OF_TROUBLE; // Deliberately head for trouble...365 }
366367/**368 * Rollover the current file to a new file.369 */370void rollOver() throws IOException {
371372/* Compute filename, but only if datePattern is specified */373if (datePattern == null) {
374 errorHandler.error("Missing DatePattern option in rollOver().");
375return;
376 }
377378 String datedFilename = fileName + sdf.format(now);
379// It is too early to roll over because we are still within the380// bounds of the current interval. Rollover will occur once the381// next interval is reached.382if (scheduledFilename.equals(datedFilename)) {
383return;
384 }
385386// close current file, and rename it to datedFilename387this.closeFile();
388389 File target = new File(scheduledFilename);
390if (target.exists()) {
391if(!target.delete()) {
392 LogLog.warn("Unable to remove: "+target.getAbsolutePath());
393 };
394 }
395396 File file = new File(fileName);
397398boolean result = file.renameTo(target);
399if (result) {
400 LogLog.debug(fileName + " -> " + scheduledFilename);
401 } else {
402 LogLog.error("Failed to rename [" + fileName + "] to ["403 + scheduledFilename + "].");
404 }
405406try {
407// This will also close the file. This is OK since multiple408// close operations are safe.409this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
410 } catch (IOException e) {
411 errorHandler.error("setFile(" + fileName + ", false) call failed.");
412 }
413 scheduledFilename = datedFilename;
414 cleanUp();
415 }
416417publicsynchronized String getCleanUpRegex() {
418return cleanUpRegex;
419 }
420421protectedsynchronizedvoid setCleanUpRegex(String cleanUpRegex) {
422this.cleanUpRegex = cleanUpRegex;
423 }
424425publicint getMaxBackupIndex() {
426return maxBackupIndex;
427 }
428429publicvoid setMaxBackupIndex(int maxBackupIndex) {
430this.maxBackupIndex = maxBackupIndex;
431 }
432433protectedsynchronizedvoid cleanUp() {
434 String regex = "";
435try {
436 File actualFile = new File(fileName);
437438 String directoryName = actualFile.getParent();
439 String actualFileName = actualFile.getName();
440 File dirList = new File(directoryName);
441442if (cleanUpRegex == null || !cleanUpRegex.contains("$fileName")) {
443 LogLog
444 .error("cleanUpRegex == null || !cleanUpRegex.contains(\"$fileName\")");
445return;
446 }
447 regex = cleanUpRegex.replace("$fileName", actualFileName);
448 String[] dirFiles = dirList.list(newLogFilter(actualFileName, regex));
449450 List<String> files = new ArrayList<String>();
451if(dirFiles!=null) {
452for (String file : dirFiles) {
453 files.add(file);
454 }
455 }
456 Collections.sort(files);
457458while (files.size() > maxBackupIndex) {
459 String file = files.remove(0);
460 File f = new File(directoryName + "/" + file);
461if(!f.delete()) {
462 LogLog.warn("Cannot remove: " + file);
463 }
464 }
465 } catch (Exception e) {
466 errorHandler
467 .error("cleanUp(" + fileName + "," + regex + ") call failed.");
468 }
469 }
470471privatestaticclassLogFilterimplements FilenameFilter {
472private Pattern p = null;
473private String logFile = null;
474475publicLogFilter(String logFile, String regex) {
476this.logFile = logFile;
477 p = Pattern.compile(regex);
478 }
479480 @Override
481publicboolean accept(File dir, String name) {
482// ignore current log file483if (name.intern() == this.logFile.intern()) {
484return false;
485 }
486// ignore file without the same prefix487if (!name.startsWith(logFile)) {
488return false;
489 }
490return p.matcher(name).find();
491 }
492 }
493494/**495 * Fix for late-initialisation496 */497 @Override
498protectedboolean checkEntryConditions() {
499synchronized(chukwaLock) {
500if (!hasBeenActivated) {
501 hasBeenActivated = true;
502 activateOptions();
503 }
504 }
505returnsuper.checkEntryConditions();
506 }
507508/**509 * This method differentiates DailyRollingFileAppender from its super class.510 * 511 * <p>Before actually logging, this method will check whether it is time to do512 * a rollover. If it is, it will schedule the next rollover time and then513 * rollover.514 * */515protectedvoid subAppend(LoggingEvent event) {
516try {
517// we set up the chukwa adaptor here because this is the first518// point which is called after all setters have been called with519// their values from the log4j.properties file, in particular we520// needed to give setCukwaClientPortNum() and -Hostname() a shot521522// Make sure only one thread can do this523// and use the boolean to avoid the first level locking524if (chukwaClientIsNull) {
525synchronized (chukwaLock) {
526527 String log4jFileName = getFile();
528 String recordType = getRecordType();
529530long currentLength = 0L;
531try {
532 File fooLog = new File(log4jFileName);
533 log4jFileName = fooLog.getAbsolutePath();
534 currentLength = fooLog.length();
535 } catch (Throwable e) {
536 log.warn("Exception while trying to get current file size for " + log4jFileName);
537 currentLength = 0L;
538 }
539540if (chukwaClient == null) {
541if (getChukwaClientHostname() != null542 && getChukwaClientPortNum() != 0) {
543 chukwaClient = newChukwaAgentController(
544 getChukwaClientHostname(), getChukwaClientPortNum());
545 log.debug("setup adaptor with hostname "546 + getChukwaClientHostname() + " and portnum "547 + getChukwaClientPortNum());
548 } else {
549 chukwaClient = newChukwaAgentController();
550 log
551 .debug("setup adaptor with no args, which means it used its defaults");
552 }
553554 chukwaClientIsNull = false;
555556// Watchdog is watching for ChukwaAgent only once every 5 minutes,557// so there's no point in retrying more than once every 5 mins.558// In practice, if the watchdog is not able to automatically restart559// the agent, it will take more than 20 minutes to get Ops to560// restart it.561// Also its a good to limit the number of communications between562// Hadoop and Chukwa, that's why 30 minutes.563long retryInterval = chukwaClientConnectRetryInterval;
564if (retryInterval == 0) {
565 retryInterval = 1000 * 60 * 30;
566 }
567long numRetries = chukwaClientConnectNumRetry;
568if (numRetries == 0) {
569 numRetries = 48;
570 }
571572 String name = AdaptorNamingUtils.synthesizeAdaptorID
573 (ChukwaAgentController.CharFileTailUTF8NewLineEscaped, recordType, log4jFileName);
574575 String adaptorID = chukwaClient.addByName(name, ChukwaAgentController.CharFileTailUTF8NewLineEscaped,
576 recordType,currentLength + " " + log4jFileName, currentLength,
577 numRetries, retryInterval);
578579// Setup a shutdownHook for the controller580 clientFinalizer = newClientFinalizer(chukwaClient);
581 Runtime.getRuntime().addShutdownHook(clientFinalizer);
582583if (adaptorID != null) {
584 log.debug("Added file tailing adaptor to chukwa agent for file "585 + log4jFileName + ", adaptorId:" + adaptorID
586 + " using this recordType :" + recordType
587 + ", starting at offset:" + currentLength);
588 } else {
589 log.debug("Chukwa adaptor not added, addFile(" + log4jFileName
590 + ") returned, current offset: " + currentLength);
591 }
592593 }
594 }
595 }
596597long n = System.currentTimeMillis();
598if (n >= nextCheck) {
599 now.setTime(n);
600 nextCheck = rc.getNextCheckMillis(now);
601try {
602 rollOver();
603 } catch (IOException ioe) {
604 LogLog.error("rollOver() failed.", ioe);
605 }
606 }
607608boolean written = false;
609if(layout.ignoresThrowable()) {
610 String[] s = event.getThrowableStrRep();
611if (s != null) {
612int len = s.length;
613 StringBuilder sb = new StringBuilder();
614 sb.append(this.layout.format(event));
615for(int i = 0; i < len; i++) {
616 sb.append(s[i]).append("\n");
617 }
618//escape the newlines from record bodies, exception and then write this record to the log file619 written = true;
620this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",sb.toString()));
621 }
622 }
623624if (!written) {
625//escape the newlines from record bodies and then write this record to the log file626this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",this.layout.format(event)));
627 }
628629if (this.immediateFlush) {
630this.qw.flush();
631 }
632 } catch (Throwable e) {
633 System.err.println("Exception in ChukwaRollingAppender: "634 + e.getMessage());
635 e.printStackTrace();
636 }
637638 }
639640public String getChukwaClientHostname() {
641return chukwaClientHostname;
642 }
643644publicvoid setChukwaClientHostname(String chukwaClientHostname) {
645this.chukwaClientHostname = chukwaClientHostname;
646 }
647648publicint getChukwaClientPortNum() {
649return chukwaClientPortNum;
650 }
651652publicvoid setChukwaClientPortNum(int chukwaClientPortNum) {
653this.chukwaClientPortNum = chukwaClientPortNum;
654 }
655656publicvoid setChukwaClientConnectNumRetry(int i) {
657this.chukwaClientConnectNumRetry = i;
658 }
659660publicvoid setChukwaClientConnectRetryInterval(long i) {
661this.chukwaClientConnectRetryInterval = i;
662 }
663664 }
665666667/**668 * RollingCalendar is a helper class to DailyRollingFileAppender. Given a669 * periodicity type and the current time, it computes the start of the next670 * interval.671 * */672classRollingCalendarextends GregorianCalendar {
673674/**675 * 676 */677privatestaticfinallong serialVersionUID = 2153481574198792767L;
678int type = ChukwaDailyRollingFileAppender.TOP_OF_TROUBLE;
679680RollingCalendar() {
681super();
682 }
683684RollingCalendar(TimeZone tz, Locale locale) {
685super(tz, locale);
686 }
687688void setType(int type) {
689this.type = type;
690 }
691692publiclong getNextCheckMillis(Date now) {
693return getNextCheckDate(now).getTime();
694 }
695696public Date getNextCheckDate(Date now) {
697this.setTime(now);
698699switch (type) {
700case ChukwaDailyRollingFileAppender.TOP_OF_MINUTE:
701this.set(Calendar.SECOND, 0);
702this.set(Calendar.MILLISECOND, 0);
703this.add(Calendar.MINUTE, 1);
704break;
705case ChukwaDailyRollingFileAppender.TOP_OF_HOUR:
706this.set(Calendar.MINUTE, 0);
707this.set(Calendar.SECOND, 0);
708this.set(Calendar.MILLISECOND, 0);
709this.add(Calendar.HOUR_OF_DAY, 1);
710break;
711case ChukwaDailyRollingFileAppender.HALF_DAY:
712this.set(Calendar.MINUTE, 0);
713this.set(Calendar.SECOND, 0);
714this.set(Calendar.MILLISECOND, 0);
715int hour = get(Calendar.HOUR_OF_DAY);
716if (hour < 12) {
717this.set(Calendar.HOUR_OF_DAY, 12);
718 } else {
719this.set(Calendar.HOUR_OF_DAY, 0);
720this.add(Calendar.DAY_OF_MONTH, 1);
721 }
722break;
723case ChukwaDailyRollingFileAppender.TOP_OF_DAY:
724this.set(Calendar.HOUR_OF_DAY, 0);
725this.set(Calendar.MINUTE, 0);
726this.set(Calendar.SECOND, 0);
727this.set(Calendar.MILLISECOND, 0);
728this.add(Calendar.DATE, 1);
729break;
730case ChukwaDailyRollingFileAppender.TOP_OF_WEEK:
731this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
732this.set(Calendar.HOUR_OF_DAY, 0);
733this.set(Calendar.SECOND, 0);
734this.set(Calendar.MILLISECOND, 0);
735this.add(Calendar.WEEK_OF_YEAR, 1);
736break;
737case ChukwaDailyRollingFileAppender.TOP_OF_MONTH:
738this.set(Calendar.DATE, 1);
739this.set(Calendar.HOUR_OF_DAY, 0);
740this.set(Calendar.SECOND, 0);
741this.set(Calendar.MILLISECOND, 0);
742this.add(Calendar.MONTH, 1);
743break;
744default:
745thrownew IllegalStateException("Unknown periodicity type.");
746 }
747return getTime();
748 }
749750 @Override
751publicboolean equals(Object o) {
752returnsuper.equals(o);
753 }
754755 @Override
756publicint hashCode() {
757returnsuper.hashCode();
758 }
759 }