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.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.7677 <p>Is is possible to specify monthly, weekly, half-daily, daily,78 hourly, or minutely rollover schedules.7980 <p><table border="1" cellpadding="2">81 <tr>82 <th>DatePattern</th>83 <th>Rollover schedule</th>84 <th>Example</th>85 </tr>8687 <tr>88 <td><code>'.'yyyy-MM</code></td>89 <td>Rollover at the beginning of each month</td>9091 <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be92 copied to <code>/foo/bar.log.2002-05</code>. Logging for the month93 of June will be output to <code>/foo/bar.log</code> until it is94 also rolled over the next month.</td></tr>9596 <tr>97 <td><code>'.'yyyy-ww</code></td>9899 <td>Rollover at the first day of each week. The first day of the100 week depends on the locale.</td>101102 <td>Assuming the first day of the week is Sunday, on Saturday103 midnight, June 9th 2002, the file <i>/foo/bar.log</i> will be104 copied to <i>/foo/bar.log.2002-23</i>. Logging for the 24th week105 of 2002 will be output to <code>/foo/bar.log</code> until it is106 rolled over the next week.</td></tr>107108 <tr>109 <td><code>'.'yyyy-MM-dd</code></td>110111 <td>Rollover at midnight each day.</td>112113 <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will114 be copied to <code>/foo/bar.log.2002-03-08</code>. Logging for the115 9th day of March will be output to <code>/foo/bar.log</code> until116 it is rolled over the next day.</td></tr>117118 <tr>119 <td><code>'.'yyyy-MM-dd-a</code></td>120121 <td>Rollover at midnight and midday of each day.</td>122123 <td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be124 copied to <code>/foo/bar.log.2002-03-09-AM</code>. Logging for the125 afternoon of the 9th will be output to <code>/foo/bar.log</code>126 until it is rolled over at midnight.</td></tr>127128 <tr>129 <td><code>'.'yyyy-MM-dd-HH</code></td>130131 <td>Rollover at the top of every hour.</td>132133 <td>At approximately 11:00.000 o'clock on March 9th, 2002,134 <code>/foo/bar.log</code> will be copied to135 <code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour136 of the 9th of March will be output to <code>/foo/bar.log</code>137 until it is rolled over at the beginning of the next hour.</td></tr>138139140 <tr>141 <td><code>'.'yyyy-MM-dd-HH-mm</code></td>142143 <td>Rollover at the beginning of every minute.</td>144145 <td>At approximately 11:23,000, on March 9th, 2001,146 <code>/foo/bar.log</code> will be copied to147 <code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute148 of 11:23 (9th of March) will be output to149 <code>/foo/bar.log</code> until it is rolled over the next minute.</td></tr>150151 </table>152153 <p>Do not use the colon ":" character in anywhere in the154 <b>DatePattern</b> option. The text before the colon is interpeted155 as the protocol specificaion of a URL which is probably not what156 you want.</p> */157158publicclassChukwaDailyRollingFileAppenderextends FileAppender {
159160static Logger log = Logger.getLogger(ChukwaDailyRollingFileAppender.class);
161// The code assumes that the following constants are in a increasing162// sequence.163staticfinalint TOP_OF_TROUBLE = -1;
164staticfinalint TOP_OF_MINUTE = 0;
165staticfinalint TOP_OF_HOUR = 1;
166staticfinalint HALF_DAY = 2;
167staticfinalint TOP_OF_DAY = 3;
168staticfinalint TOP_OF_WEEK = 4;
169staticfinalint TOP_OF_MONTH = 5;
170171staticfinal String adaptorType = ChukwaAgentController.CharFileTailUTF8NewLineEscaped;
172173staticfinal Object lock = new Object();
174static String lastRotation = "";
175176/**177 * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning178 * daily rollover.179 */180private String datePattern = "'.'yyyy-MM-dd";
181182/**183 * The log file will be renamed to the value of the scheduledFilename variable184 * when the next interval is entered. For example, if the rollover period is185 * one hour, the log file will be renamed to the value of "scheduledFilename"186 * at the beginning of the next hour.187 * 188 * The precise time when a rollover occurs depends on logging activity.189 */190private String scheduledFilename;
191192/**193 * The next time we estimate a rollover should occur.194 */195privatelong nextCheck = System.currentTimeMillis() - 1;
196197/**198 * Regex to select log files to be deleted199 */200private String cleanUpRegex = null;
201202/**203 * Set the maximum number of backup files to keep around.204 */205privateint maxBackupIndex = 10;
206207privateClientFinalizer clientFinalizer = null;
208209boolean hasBeenActivated = false;
210 Date now = new Date();
211212 SimpleDateFormat sdf;
213214RollingCalendar rc = newRollingCalendar();
215216int checkPeriod = TOP_OF_TROUBLE;
217218ChukwaAgentController chukwaClient;
219boolean chukwaClientIsNull = true;
220staticfinal Object chukwaLock = new Object();
221222 String chukwaClientHostname;
223int chukwaClientPortNum;
224long chukwaClientConnectNumRetry;
225long chukwaClientConnectRetryInterval;
226227 String recordType;
228229// The gmtTimeZone is used only in computeCheckPeriod() method.230staticfinal TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
231232/**233 * The default constructor does nothing.234 * @throws IOException 235 */236publicChukwaDailyRollingFileAppender() throws IOException {
237super();
238 }
239240/**241 Instantiate a <code>DailyRollingFileAppender</code> and open the242 file designated by <code>filename</code>. The opened filename will243 become the output destination for this appender.244 * @param layout 245 * @param filename 246 * @param datePattern 247 * @throws IOException 248249 */250publicChukwaDailyRollingFileAppender(Layout layout, String filename,
251 String datePattern) throws IOException {
252super(layout, filename, true);
253 System.out
254 .println("Daily Rolling File Appender successfully registered file with agent: "255 + filename);
256this.datePattern = datePattern;
257 }
258259/**260 * The <b>DatePattern</b> takes a string in the same format as expected by261 * {@link SimpleDateFormat}. This options determines the rollover schedule.262 * @param pattern 263 */264publicvoid setDatePattern(String pattern) {
265 datePattern = pattern;
266 }
267268/** Returns the value of the <b>DatePattern</b> option. 269 * @return */270public String getDatePattern() {
271return datePattern;
272 }
273274public String getRecordType() {
275if (recordType != null)
276return recordType;
277else278return"unknown";
279 }
280281publicvoid setRecordType(String recordType) {
282this.recordType = recordType;
283 }
284285publicvoid activateOptions() {
286287// Prevent early initialisation288if (!hasBeenActivated)
289 { return;}
290291super.activateOptions();
292if (datePattern != null && fileName != null) {
293 now.setTime(System.currentTimeMillis());
294 sdf = new SimpleDateFormat(datePattern);
295int type = computeCheckPeriod();
296 printPeriodicity(type);
297 rc.setType(type);
298 File file = new File(fileName);
299 scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));
300301 } else {
302 LogLog
303 .error("Either File or DatePattern options are not set for appender ["304 + name + "].");
305 }
306 }
307308void printPeriodicity(int type) {
309switch (type) {
310case TOP_OF_MINUTE:
311 LogLog.debug("Appender [" + name + "] to be rolled every minute.");
312break;
313case TOP_OF_HOUR:
314 LogLog
315 .debug("Appender [" + name + "] to be rolled on top of every hour.");
316break;
317case HALF_DAY:
318 LogLog.debug("Appender [" + name
319 + "] to be rolled at midday and midnight.");
320break;
321case TOP_OF_DAY:
322 LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
323break;
324case TOP_OF_WEEK:
325 LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
326break;
327case TOP_OF_MONTH:
328 LogLog.debug("Appender [" + name
329 + "] to be rolled at start of every month.");
330break;
331default:
332 LogLog.warn("Unknown periodicity for appender [" + name + "].");
333 }
334 }
335336// This method computes the roll over period by looping over the337// periods, starting with the shortest, and stopping when the r0 is338// different from from r1, where r0 is the epoch formatted according339// the datePattern (supplied by the user) and r1 is the340// epoch+nextMillis(i) formatted according to datePattern. All date341// formatting is done in GMT and not local format because the test342// logic is based on comparisons relative to 1970-01-01 00:00:00343// GMT (the epoch).344345int computeCheckPeriod() {
346RollingCalendar rollingCalendar = newRollingCalendar(gmtTimeZone,
347 Locale.ENGLISH);
348// set sate to 1970-01-01 00:00:00 GMT349 Date epoch = new Date(0);
350if (datePattern != null) {
351for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
352 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
353 simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in354// GMT355 String r0 = simpleDateFormat.format(epoch);
356 rollingCalendar.setType(i);
357 Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
358 String r1 = simpleDateFormat.format(next);
359// System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);360if (r0 != null && r1 != null && !r0.equals(r1)) {
361return i;
362 }
363 }
364 }
365return TOP_OF_TROUBLE; // Deliberately head for trouble...366 }
367368/**369 * Rollover the current file to a new file.370 */371void rollOver() throws IOException {
372373/* Compute filename, but only if datePattern is specified */374if (datePattern == null) {
375 errorHandler.error("Missing DatePattern option in rollOver().");
376return;
377 }
378379 String datedFilename = fileName + sdf.format(now);
380// It is too early to roll over because we are still within the381// bounds of the current interval. Rollover will occur once the382// next interval is reached.383if (scheduledFilename.equals(datedFilename)) {
384return;
385 }
386387// close current file, and rename it to datedFilename388this.closeFile();
389390 File target = new File(scheduledFilename);
391if (target.exists()) {
392if(!target.delete()) {
393 LogLog.warn("Unable to remove: "+target.getAbsolutePath());
394 };
395 }
396397 File file = new File(fileName);
398399boolean result = file.renameTo(target);
400if (result) {
401 LogLog.debug(fileName + " -> " + scheduledFilename);
402 } else {
403 LogLog.error("Failed to rename [" + fileName + "] to ["404 + scheduledFilename + "].");
405 }
406407try {
408// This will also close the file. This is OK since multiple409// close operations are safe.410this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
411 } catch (IOException e) {
412 errorHandler.error("setFile(" + fileName + ", false) call failed.");
413 }
414 scheduledFilename = datedFilename;
415 cleanUp();
416 }
417418publicsynchronized String getCleanUpRegex() {
419return cleanUpRegex;
420 }
421422protectedsynchronizedvoid setCleanUpRegex(String cleanUpRegex) {
423this.cleanUpRegex = cleanUpRegex;
424 }
425426publicint getMaxBackupIndex() {
427return maxBackupIndex;
428 }
429430publicvoid setMaxBackupIndex(int maxBackupIndex) {
431this.maxBackupIndex = maxBackupIndex;
432 }
433434protectedsynchronizedvoid cleanUp() {
435 String regex = "";
436try {
437 File actualFile = new File(fileName);
438439 String directoryName = actualFile.getParent();
440 String actualFileName = actualFile.getName();
441 File dirList = new File(directoryName);
442443if (cleanUpRegex == null || !cleanUpRegex.contains("$fileName")) {
444 LogLog
445 .error("cleanUpRegex == null || !cleanUpRegex.contains(\"$fileName\")");
446return;
447 }
448 regex = cleanUpRegex.replace("$fileName", actualFileName);
449 String[] dirFiles = dirList.list(newLogFilter(actualFileName, regex));
450451 List<String> files = new ArrayList<String>();
452if(dirFiles!=null) {
453for (String file : dirFiles) {
454 files.add(file);
455 }
456 }
457 Collections.sort(files);
458459while (files.size() > maxBackupIndex) {
460 String file = files.remove(0);
461 File f = new File(directoryName + "/" + file);
462if(!f.delete()) {
463 LogLog.warn("Cannot remove: " + file);
464 }
465 }
466 } catch (Exception e) {
467 errorHandler
468 .error("cleanUp(" + fileName + "," + regex + ") call failed.");
469 }
470 }
471472privatestaticclassLogFilterimplements FilenameFilter {
473private Pattern p = null;
474private String logFile = null;
475476publicLogFilter(String logFile, String regex) {
477this.logFile = logFile;
478 p = Pattern.compile(regex);
479 }
480481 @Override
482publicboolean accept(File dir, String name) {
483// ignore current log file484if (name.intern() == this.logFile.intern()) {
485return false;
486 }
487// ignore file without the same prefix488if (!name.startsWith(logFile)) {
489return false;
490 }
491return p.matcher(name).find();
492 }
493 }
494495/**496 * Fix for late-initialisation497 */498 @Override
499protectedboolean checkEntryConditions() {
500synchronized(chukwaLock) {
501if (!hasBeenActivated) {
502 hasBeenActivated = true;
503 activateOptions();
504 }
505 }
506returnsuper.checkEntryConditions();
507 }
508509/**510 * This method differentiates DailyRollingFileAppender from its super class.511 * 512 * <p>Before actually logging, this method will check whether it is time to do513 * a rollover. If it is, it will schedule the next rollover time and then514 * rollover.515 * */516protectedvoid subAppend(LoggingEvent event) {
517try {
518// we set up the chukwa adaptor here because this is the first519// point which is called after all setters have been called with520// their values from the log4j.properties file, in particular we521// needed to give setCukwaClientPortNum() and -Hostname() a shot522523// Make sure only one thread can do this524// and use the boolean to avoid the first level locking525if (chukwaClientIsNull) {
526synchronized (chukwaLock) {
527528 String log4jFileName = getFile();
529 String recordType = getRecordType();
530531long currentLength = 0L;
532try {
533 File fooLog = new File(log4jFileName);
534 log4jFileName = fooLog.getAbsolutePath();
535 currentLength = fooLog.length();
536 } catch (Throwable e) {
537 log.warn("Exception while trying to get current file size for " + log4jFileName);
538 currentLength = 0L;
539 }
540541if (chukwaClient == null) {
542if (getChukwaClientHostname() != null543 && getChukwaClientPortNum() != 0) {
544 chukwaClient = newChukwaAgentController(
545 getChukwaClientHostname(), getChukwaClientPortNum());
546 log.debug("setup adaptor with hostname "547 + getChukwaClientHostname() + " and portnum "548 + getChukwaClientPortNum());
549 } else {
550 chukwaClient = newChukwaAgentController();
551 log
552 .debug("setup adaptor with no args, which means it used its defaults");
553 }
554555 chukwaClientIsNull = false;
556557// Watchdog is watching for ChukwaAgent only once every 5 minutes,558// so there's no point in retrying more than once every 5 mins.559// In practice, if the watchdog is not able to automatically restart560// the agent, it will take more than 20 minutes to get Ops to561// restart it.562// Also its a good to limit the number of communications between563// Hadoop and Chukwa, that's why 30 minutes.564long retryInterval = chukwaClientConnectRetryInterval;
565if (retryInterval == 0) {
566 retryInterval = 1000 * 60 * 30;
567 }
568long numRetries = chukwaClientConnectNumRetry;
569if (numRetries == 0) {
570 numRetries = 48;
571 }
572573 String name = AdaptorNamingUtils.synthesizeAdaptorID
574 (ChukwaAgentController.CharFileTailUTF8NewLineEscaped, recordType, log4jFileName);
575576 String adaptorID = chukwaClient.addByName(name, ChukwaAgentController.CharFileTailUTF8NewLineEscaped,
577 recordType,currentLength + " " + log4jFileName, currentLength,
578 numRetries, retryInterval);
579580// Setup a shutdownHook for the controller581 clientFinalizer = newClientFinalizer(chukwaClient);
582 Runtime.getRuntime().addShutdownHook(clientFinalizer);
583584if (adaptorID != null) {
585 log.debug("Added file tailing adaptor to chukwa agent for file "586 + log4jFileName + ", adaptorId:" + adaptorID
587 + " using this recordType :" + recordType
588 + ", starting at offset:" + currentLength);
589 } else {
590 log.debug("Chukwa adaptor not added, addFile(" + log4jFileName
591 + ") returned, current offset: " + currentLength);
592 }
593594 }
595 }
596 }
597598long n = System.currentTimeMillis();
599if (n >= nextCheck) {
600 now.setTime(n);
601 nextCheck = rc.getNextCheckMillis(now);
602try {
603 rollOver();
604 } catch (IOException ioe) {
605 LogLog.error("rollOver() failed.", ioe);
606 }
607 }
608609boolean written = false;
610if(layout.ignoresThrowable()) {
611 String[] s = event.getThrowableStrRep();
612if (s != null) {
613int len = s.length;
614 StringBuilder sb = new StringBuilder();
615 sb.append(this.layout.format(event));
616for(int i = 0; i < len; i++) {
617 sb.append(s[i]).append("\n");
618 }
619//escape the newlines from record bodies, exception and then write this record to the log file620 written = true;
621this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",sb.toString()));
622 }
623 }
624625if (!written) {
626//escape the newlines from record bodies and then write this record to the log file627this.qw.write(RecordConstants.escapeAllButLastRecordSeparator("\n",this.layout.format(event)));
628 }
629630if (this.immediateFlush) {
631this.qw.flush();
632 }
633 } catch (Throwable e) {
634 System.err.println("Exception in ChukwaRollingAppender: "635 + e.getMessage());
636 e.printStackTrace();
637 }
638639 }
640641public String getChukwaClientHostname() {
642return chukwaClientHostname;
643 }
644645publicvoid setChukwaClientHostname(String chukwaClientHostname) {
646this.chukwaClientHostname = chukwaClientHostname;
647 }
648649publicint getChukwaClientPortNum() {
650return chukwaClientPortNum;
651 }
652653publicvoid setChukwaClientPortNum(int chukwaClientPortNum) {
654this.chukwaClientPortNum = chukwaClientPortNum;
655 }
656657publicvoid setChukwaClientConnectNumRetry(int i) {
658this.chukwaClientConnectNumRetry = i;
659 }
660661publicvoid setChukwaClientConnectRetryInterval(long i) {
662this.chukwaClientConnectRetryInterval = i;
663 }
664665 }
666667668/**669 * RollingCalendar is a helper class to DailyRollingFileAppender. Given a670 * periodicity type and the current time, it computes the start of the next671 * interval.672 * */673classRollingCalendarextends GregorianCalendar {
674675/**676 * 677 */678privatestaticfinallong serialVersionUID = 2153481574198792767L;
679int type = ChukwaDailyRollingFileAppender.TOP_OF_TROUBLE;
680681RollingCalendar() {
682super();
683 }
684685RollingCalendar(TimeZone tz, Locale locale) {
686super(tz, locale);
687 }
688689void setType(int type) {
690this.type = type;
691 }
692693publiclong getNextCheckMillis(Date now) {
694return getNextCheckDate(now).getTime();
695 }
696697public Date getNextCheckDate(Date now) {
698this.setTime(now);
699700switch (type) {
701case ChukwaDailyRollingFileAppender.TOP_OF_MINUTE:
702this.set(Calendar.SECOND, 0);
703this.set(Calendar.MILLISECOND, 0);
704this.add(Calendar.MINUTE, 1);
705break;
706case ChukwaDailyRollingFileAppender.TOP_OF_HOUR:
707this.set(Calendar.MINUTE, 0);
708this.set(Calendar.SECOND, 0);
709this.set(Calendar.MILLISECOND, 0);
710this.add(Calendar.HOUR_OF_DAY, 1);
711break;
712case ChukwaDailyRollingFileAppender.HALF_DAY:
713this.set(Calendar.MINUTE, 0);
714this.set(Calendar.SECOND, 0);
715this.set(Calendar.MILLISECOND, 0);
716int hour = get(Calendar.HOUR_OF_DAY);
717if (hour < 12) {
718this.set(Calendar.HOUR_OF_DAY, 12);
719 } else {
720this.set(Calendar.HOUR_OF_DAY, 0);
721this.add(Calendar.DAY_OF_MONTH, 1);
722 }
723break;
724case ChukwaDailyRollingFileAppender.TOP_OF_DAY:
725this.set(Calendar.HOUR_OF_DAY, 0);
726this.set(Calendar.MINUTE, 0);
727this.set(Calendar.SECOND, 0);
728this.set(Calendar.MILLISECOND, 0);
729this.add(Calendar.DATE, 1);
730break;
731case ChukwaDailyRollingFileAppender.TOP_OF_WEEK:
732this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
733this.set(Calendar.HOUR_OF_DAY, 0);
734this.set(Calendar.SECOND, 0);
735this.set(Calendar.MILLISECOND, 0);
736this.add(Calendar.WEEK_OF_YEAR, 1);
737break;
738case ChukwaDailyRollingFileAppender.TOP_OF_MONTH:
739this.set(Calendar.DATE, 1);
740this.set(Calendar.HOUR_OF_DAY, 0);
741this.set(Calendar.SECOND, 0);
742this.set(Calendar.MILLISECOND, 0);
743this.add(Calendar.MONTH, 1);
744break;
745default:
746thrownew IllegalStateException("Unknown periodicity type.");
747 }
748return getTime();
749 }
750751 @Override
752publicboolean equals(Object o) {
753returnsuper.equals(o);
754 }
755756 @Override
757publicint hashCode() {
758returnsuper.hashCode();
759 }
760 }