commits@javamail.java.net

[javamail~mercurial:769] Add spam filter for use with MailHandler. - bug 6905

From: <shannon_at_java.net>
Date: Fri, 2 Oct 2015 22:58:37 +0000

Project: javamail
Repository: mercurial
Revision: 769
Author: shannon
Date: 2015-10-02 22:57:37 UTC
Link:

Log Message:
------------
whitespace line at beginning confuses InternetHeaders - bug 7009
Add spam filter for use with MailHandler. - bug 6905

(From Jason)


Revisions:
----------
768
769


Modified Paths:
---------------
doc/release/CHANGES.txt
mail/src/main/java/javax/mail/internet/InternetHeaders.java
logging/src/main/java/MailHandlerDemo.java


Added Paths:
------------
mail/src/test/java/javax/mail/internet/InternetHeadersTest.java
mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java
mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java


Diffs:
------
diff -r e6d6f201be1a -r d5c073eac2f6 doc/release/CHANGES.txt
--- a/doc/release/CHANGES.txt Mon Sep 28 17:02:04 2015 -0700
+++ b/doc/release/CHANGES.txt Fri Oct 02 15:30:14 2015 -0700
@@ -30,6 +30,7 @@
 K 6973 capability() command doesn't properly transform errors
 K 6989 MailHandler needs better support for stateful filters.
 K 6997 add support for IMAP login referrals (RFC 2221)
+K 7009 whitespace line at beginning confuses InternetHeaders
 
 
                   CHANGES IN THE 1.5.4 RELEASE

diff -r e6d6f201be1a -r d5c073eac2f6 mail/src/main/java/javax/mail/internet/InternetHeaders.java
--- a/mail/src/main/java/javax/mail/internet/InternetHeaders.java Mon Sep 28 17:02:04 2015 -0700
+++ b/mail/src/main/java/javax/mail/internet/InternetHeaders.java Fri Oct 02 15:30:14 2015 -0700
@@ -1,7 +1,7 @@
 /*
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
  *
- * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997-2015 Oracle and/or its affiliates. All rights reserved.
  *
  * The contents of this file are subject to the terms of either the GNU
  * General Public License Version 2 only ("GPL") or the Common Development
@@ -354,7 +354,10 @@
         StringBuffer lineBuffer = new StringBuffer();
 
         try {
- //while ((line = lis.readLine()) != null) {
+ // if the first line being read is a continuation line,
+ // we ignore it if it's otherwise empty or we treat it as
+ // a non-continuation line if it has non-whitespace content
+ boolean first = true;
             do {
                 line = lis.readLine();
                 if (line != null &&
@@ -364,8 +367,15 @@
                         lineBuffer.append(prevline);
                         prevline = null;
                     }
- lineBuffer.append("\r\n");
- lineBuffer.append(line);
+ if (first) {
+ String lt = line.trim();
+ if (lt.length() > 0)
+ lineBuffer.append(lt);
+ } else {
+ if (lineBuffer.length() > 0)
+ lineBuffer.append("\r\n");
+ lineBuffer.append(line);
+ }
                 } else {
                     // new header
                     if (prevline != null)
@@ -377,6 +387,7 @@
                     }
                     prevline = line;
                 }
+ first = false;
             } while (line != null && !isEmpty(line));
         } catch (IOException ioex) {
             throw new MessagingException("Error in input stream", ioex);

diff -r e6d6f201be1a -r d5c073eac2f6 mail/src/test/java/javax/mail/internet/InternetHeadersTest.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/src/test/java/javax/mail/internet/InternetHeadersTest.java Fri Oct 02 15:30:14 2015 -0700
@@ -0,0 +1,137 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2011-2015 Oracle and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
+ * or packager/legal/LICENSE.txt. See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at packager/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * Oracle designates this particular file as subject to the "Classpath"
+ * exception as provided by Oracle in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+
+package javax.mail.internet;
+
+import java.io.*;
+import java.util.Enumeration;
+
+import javax.mail.*;
+
+import org.junit.*;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test the InternetHeaders class.
+ */
+public class InternetHeadersTest {
+
+ private static final String initialWhitespaceHeader =
+ " \r\nSubject: test\r\n\r\n";
+ private static final String initialContinuationHeader =
+ " Subject: test\r\n\r\n";
+
+ /**
+ * Test that a continuation line is handled properly.
+ */
+ @Test
+ public void testContinuationLine() throws Exception {
+ String header = "Subject: a\r\n b\r\n\r\n";
+ InternetHeaders ih = new InternetHeaders(
+ new StringBufferInputStream(header));
+ assertEquals(1, ih.getHeader("Subject").length);
+ assertEquals("a\r\n b", ih.getHeader("Subject")[0]);
+ }
+
+ /**
+ * Test that a whitespace line at the beginning is ignored.
+ */
+ @Test
+ public void testInitialWhitespaceLineConstructor() throws Exception {
+ InternetHeaders ih = new InternetHeaders(
+ new StringBufferInputStream(initialWhitespaceHeader));
+ testInitialWhitespaceLine(ih);
+ }
+
+ /**
+ * Test that a whitespace line at the beginning is ignored.
+ */
+ @Test
+ public void testInitialWhitespaceLineLoad() throws Exception {
+ InternetHeaders ih = new InternetHeaders();
+ ih.load(new StringBufferInputStream(initialWhitespaceHeader));
+ testInitialWhitespaceLine(ih);
+ }
+
+ private void testInitialWhitespaceLine(InternetHeaders ih)
+ throws Exception {
+ assertEquals(1, ih.getHeader("Subject").length);
+ assertEquals("test", ih.getHeader("Subject")[0]);
+ Enumeration e = ih.getAllHeaders();
+ while (e.hasMoreElements()) {
+ Header h = (Header)e.nextElement();
+ assertEquals("Subject", h.getName());
+ assertEquals("test", h.getValue());
+ }
+ }
+
+ /**
+ * Test that a continuation line at the beginning is handled.
+ */
+ @Test
+ public void testInitialContinuationLineConstructor() throws Exception {
+ InternetHeaders ih = new InternetHeaders(
+ new StringBufferInputStream(initialContinuationHeader));
+ testInitialContinuationLine(ih);
+ }
+
+ /**
+ * Test that a continuation line at the beginning is handled.
+ */
+ @Test
+ public void testInitialContinuationLineLoad() throws Exception {
+ InternetHeaders ih = new InternetHeaders();
+ ih.load(new StringBufferInputStream(initialContinuationHeader));
+ testInitialContinuationLine(ih);
+ }
+
+ private void testInitialContinuationLine(InternetHeaders ih)
+ throws Exception {
+ assertEquals(1, ih.getHeader("Subject").length);
+ assertEquals("test", ih.getHeader("Subject")[0]);
+ Enumeration e = ih.getAllHeaders();
+ while (e.hasMoreElements()) {
+ Header h = (Header)e.nextElement();
+ assertEquals("Subject", h.getName());
+ assertEquals("test", h.getValue());
+ }
+ }
+}


diff -r d5c073eac2f6 -r d22ab7996acc doc/release/CHANGES.txt
--- a/doc/release/CHANGES.txt Fri Oct 02 15:30:14 2015 -0700
+++ b/doc/release/CHANGES.txt Fri Oct 02 15:57:37 2015 -0700
@@ -20,6 +20,7 @@
 The following bugs have been fixed in the 1.5.5 release.
 
 K 6886 add support for setting GMail labels on messages
+K 6905 Add spam filter for use with MailHandler.
 K 6907 Address MailDateFormat issues
 K 6938 Typo in "mail.stmp.sendpartial"
 K 6943 mail.mime.encodefilename property should override RFC 2231 encoding

diff -r d5c073eac2f6 -r d22ab7996acc logging/src/main/java/MailHandlerDemo.java
--- a/logging/src/main/java/MailHandlerDemo.java Fri Oct 02 15:30:14 2015 -0700
+++ b/logging/src/main/java/MailHandlerDemo.java Fri Oct 02 15:57:37 2015 -0700
@@ -1,6 +1,6 @@
 /*
- * Copyright (c) 2009-2014 Oracle and/or its affiliates. All rights reserved.
- * Copyright (c) 2009-2014 Jason Mehrens. All Rights Reserved.
+ * Copyright (c) 2009-2015 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2009-2015 Jason Mehrens. All Rights Reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -31,6 +31,7 @@
  */
 
 import com.sun.mail.util.logging.CollectorFormatter;
+import com.sun.mail.util.logging.DurationFilter;
 import com.sun.mail.util.logging.MailHandler;
 import com.sun.mail.util.logging.SeverityComparator;
 import java.io.File;
@@ -151,10 +152,10 @@
                 err.println(prefix + ": SecurityManager.class=" + sm.getClass().getName());
                 err.println(prefix + ": SecurityManager.toString=" + sm);
             } else {
- err.println(prefix + ": SecurityManager.class=" + null);
- err.println(prefix + ": SecurityManager.toString=" + null);
+ err.println(prefix + ": SecurityManager.class=null");
+ err.println(prefix + ": SecurityManager.toString=null");
             }
-
+
             String policy = System.getProperty("java.security.policy");
             if (policy != null) {
                 File f = new File(policy);
@@ -366,22 +367,24 @@
     }
 
     /**
- * Example for priority messages by custom trigger. If the push filter is
+ * Example for priority messages by generation rate. If the push filter is
      * triggered the message is high priority. Otherwise, on close any remaining
- * messages are sent. <code>
+ * messages are sent. If the capacity is set to the <code>
      * ##logging.properties
      * MailHandlerDemo.handlers=com.sun.mail.util.logging.MailHandler
- * com.sun.mail.util.logging.MailHandler.subject=Push on MessagingException demo
+ * com.sun.mail.util.logging.MailHandler.subject=Push if under two records per minute.
      * com.sun.mail.util.logging.MailHandler.pushLevel=ALL
- * com.sun.mail.util.logging.MailHandler.pushFilter=MailHandlerDemo$MessageErrorsFilter
+ * com.sun.mail.util.logging.MailHandler.pushFilter=com.sun.mail.util.logging.DurationFilter
+ * com.sun.mail.util.logging.DurationFilter.records=2
+ * com.sun.mail.util.logging.DurationFilter.duration=1 * 60 * 1000
      * ##
      * </code>
      */
     private static void initWithPushFilter() {
         MailHandler h = new MailHandler();
- h.setSubject("Push on MessagingException demo");
+ h.setSubject("Push filter demo");
         h.setPushLevel(Level.ALL);
- h.setPushFilter(new MessageErrorsFilter(true));
+ h.setPushFilter(new DurationFilter(2, 1L * 60L * 1000L));
         LOGGER.addHandler(h);
     }
 
@@ -466,7 +469,7 @@
     private static void initCustomAttachments() {
         MailHandler h = new MailHandler();
 
- //Sort records by level keeping the severe messages at the top.
+ //Sort records by severity keeping the severe messages at the top.
         h.setComparator(Collections.reverseOrder(new SeverityComparator()));
 
         //Use subject to provide a hint as to what is in the email.
@@ -480,8 +483,9 @@
                 new XMLFormatter(), new SimpleFormatter());
 
         //Filter each attachment differently.
- h.setAttachmentFilters(null, new MessageErrorsFilter(false),
- new MessageErrorsFilter(true));
+ h.setAttachmentFilters(null,
+ new DurationFilter(3L, 1000L),
+ new DurationFilter(1L, 15L * 60L * 1000L));
 
         //Creating the attachment name formatters.
         h.setAttachmentNames(new CollectorFormatter("all.xml"),
@@ -610,35 +614,4 @@
         }
         return file;
     }
-
- /**
- * A simple message filter example.
- */
- public static final class MessageErrorsFilter implements Filter {
-
- /**
- * Used to negate this filter.
- */
- private final boolean complement;
-
- /**
- * Default constructor.
- */
- public MessageErrorsFilter() {
- this(true);
- }
-
- /**
- * Creates the message filter.
- *
- * @param complement used to allow or deny message exceptions.
- */
- public MessageErrorsFilter(final boolean complement) {
- this.complement = complement;
- }
-
- public boolean isLoggable(LogRecord r) {
- return r.getThrown() instanceof MessagingException == complement;
- }
- }
 }

diff -r d5c073eac2f6 -r d22ab7996acc mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/src/main/java/com/sun/mail/util/logging/DurationFilter.java Fri Oct 02 15:57:37 2015 -0700
@@ -0,0 +1,403 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015 Jason Mehrens. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
+ * or packager/legal/LICENSE.txt. See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at packager/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * Oracle designates this particular file as subject to the "Classpath"
+ * exception as provided by Oracle in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.mail.util.logging;
+
+import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager;
+import java.util.logging.*;
+
+/**
+ * A filter used to limit log records based on a maximum generation rate.
+ *
+ * The duration specified is used to compute the record rate and the amount of
+ * time the filter will reject records once the rate has been exceeded. Once the
+ * rate is exceeded records are not allowed until the duration has elapsed.
+ *
+ * <p>
+ * By default each {_at_code DurationFilter} is initialized using the following
+ * LogManager configuration properties where {_at_code <filter-name>} refers to the
+ * fully qualified class name of the handler. If properties are not defined, or
+ * contain invalid values, then the specified default values are used.
+ *
+ * <ul>
+ * <li>{_at_literal <filter-name>}.records the max number of records per duration.
+ * A numeric long integer or a multiplication expression can be used as the
+ * value. (defaults to {_at_code 1000})
+ *
+ * <li>{_at_literal <filter-name>}.duration the number of milliseconds to suppress
+ * log records from being published. This is also used as duration to determine
+ * the log record rate. A numeric long integer or a multiplication expression
+ * can be used as the value. (defaults to {_at_code 15L * 60L * 1000L})
+ * </ul>
+ *
+ * <p>
+ * For example, the settings to limit {_at_code MailHandler} with a default
+ * capacity to only send a maximum of two email messages every six minutes would
+ * be as follows:
+ * <pre>
+ * {_at_code
+ * com.sun.mail.util.logging.MailHandler.filter = com.sun.mail.util.logging.DurationFilter
+ * com.sun.mail.util.logging.MailHandler.capacity = 1000
+ * com.sun.mail.util.logging.DurationFilter.records = 2L * 1000L
+ * com.sun.mail.util.logging.DurationFilter.duration = 6L * 60L * 1000L
+ * }
+ * </pre>
+ *
+ *
+ * @author Jason Mehrens
+ * @since JavaMail 1.5.5
+ */
+public class DurationFilter implements Filter {
+
+ /**
+ * The number of expected records per duration.
+ */
+ private final long records;
+ /**
+ * The duration in milliseconds used to determine the rate. The duration is
+ * also used as the amount of time that the filter will not allow records
+ * when saturated.
+ */
+ private final long duration;
+ /**
+ * The number of records seen for the current duration. This value negative
+ * if saturated. Zero is considered saturated but is reserved for recording
+ * the first duration.
+ */
+ private long count;
+ /**
+ * The most recent record time seen for the current duration.
+ */
+ private long peek;
+ /**
+ * The start time for the current duration.
+ */
+ private long start;
+
+ /**
+ * Creates the filter using the default properties.
+ */
+ public DurationFilter() {
+ this.records = checkRecords(initLong(".records"));
+ this.duration = checkDuration(initLong(".duration"));
+ }
+
+ /**
+ * Creates the filter using the given properties. Default values are used if
+ * any of the given values are outside the allowed range.
+ *
+ * @param records the number of records per duration.
+ * @param duration the number of milliseconds to suppress log records from
+ * being published.
+ */
+ public DurationFilter(final long records, final long duration) {
+ this.records = checkRecords(records);
+ this.duration = checkDuration(duration);
+ }
+
+ /**
+ * Determines if this filter is equal to another filter.
+ *
+ * @param obj the given object.
+ * @return true if equal otherwise false.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final DurationFilter other = (DurationFilter) obj;
+ if (this.records != other.records) {
+ return false;
+ }
+
+ if (this.duration != other.duration) {
+ return false;
+ }
+
+ final long c;
+ final long p;
+ final long s;
+ synchronized (other) {
+ c = other.count;
+ p = other.peek;
+ s = other.start;
+ }
+
+ synchronized (this) {
+ if (c != this.count || p != this.peek || s != this.start) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns a hash code value for this filter.
+ *
+ * @return hash code for this filter.
+ */
+ @Override
+ public int hashCode() {
+ int hash = 3;
+ hash = 89 * hash + (int) (this.records ^ (this.records >>> 32));
+ hash = 89 * hash + (int) (this.duration ^ (this.duration >>> 32));
+ return hash;
+ }
+
+ /**
+ * Check if the given log record should be published. This method will
+ * modify the internal state of this filter.
+ *
+ * @param record the log record to check.
+ * @return true if allowed; false otherwise.
+ * @throws NullPointerException if given record is null.
+ */
+ public boolean isLoggable(final LogRecord record) {
+ return accept(record.getMillis());
+ }
+
+ /**
+ * Determines if this filter will accept log records for this instant in
+ * time. The result is a best-effort estimate and should be considered out
+ * of date as soon as it is produced. This method is designed for use in
+ * monitoring the state of this filter.
+ *
+ * @return true if the filter is not saturated; false otherwise.
+ */
+ public boolean isLoggable() {
+ final long c;
+ final long s;
+ synchronized (this) {
+ c = count;
+ s = start;
+ }
+
+ final long millis = System.currentTimeMillis();
+ if (c > 0L) { //If not saturated.
+ if (c != records || (millis - s) >= duration) {
+ return true;
+ }
+ } else {
+ if ((millis - s) >= 0L || c == 0L) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of this filter.
+ *
+ * @return a string representation of this filter.
+ */
+ @Override
+ public String toString() {
+ return getClass().getName() + "{records=" + records
+ + ", duration=" + duration
+ + ", loggable=" + isLoggable() + '}';
+ }
+
+ /**
+ * Creates a copy of this filter that retains the filter settings but does
+ * not include the current filter state. The newly create clone acts as if
+ * it has never seen any records.
+ *
+ * @return a copy of this filter.
+ * @throws CloneNotSupportedException if this filter is not allowed to be
+ * cloned.
+ */
+ @Override
+ protected DurationFilter clone() throws CloneNotSupportedException {
+ final DurationFilter clone = (DurationFilter) super.clone();
+ clone.count = 0L; //Reset the filter state.
+ clone.peek = 0L;
+ clone.start = 0L;
+ return clone;
+ }
+
+ /**
+ * Determines if the record is loggable by time.
+ *
+ * @param millis the log record milliseconds.
+ * @return true if accepted false otherwise.
+ */
+ private synchronized boolean accept(final long millis) {
+ boolean allow;
+ if (count > 0L) { //If not saturated.
+ if ((millis - peek) > 0L) {
+ peek = millis; //Record the new peek.
+ }
+
+ //Under the rate if the count has not been reached.
+ if (count != records) {
+ ++count;
+ allow = true;
+ } else {
+ if ((peek - start) >= duration) {
+ count = 1L; //Start a new duration.
+ start = peek;
+ allow = true;
+ } else {
+ count = -1L; //Saturate for the duration.
+ start = peek + duration;
+ allow = false;
+ }
+ }
+ } else {
+ //If the saturation period has expired or this is the first record
+ //then start a new duration and allow records.
+ if ((millis - start) >= 0L || count == 0L) {
+ count = 1L;
+ start = millis;
+ peek = millis;
+ allow = true;
+ } else {
+ allow = false; //Remain in a saturated state.
+ }
+ }
+ return allow;
+ }
+
+ /**
+ * Reads a long value or multiplication expression from the LogManager. If
+ * the value can not be parsed or is not defined then Long.MIN_VALUE is
+ * returned.
+ *
+ * @param suffix a dot character followed by the key name.
+ * @return a long value or Long.MIN_VALUE if unable to parse or undefined.
+ * @throws NullPointerException if suffix is null.
+ */
+ private long initLong(final String suffix) {
+ long result;
+ final String p = getClass().getName();
+ String value = fromLogManager(p.concat(suffix));
+ if (value != null && value.length() != 0) {
+ try {
+ result = 1L;
+ for (String s : tokenizeLongs(value)) {
+ if (s.endsWith("L") || s.endsWith("l")) {
+ s = s.substring(0, s.length() - 1);
+ }
+ result = multiplyExact(result, Long.parseLong(s));
+ }
+ } catch (final RuntimeException ignore) {
+ result = Long.MIN_VALUE;
+ }
+ } else {
+ result = Long.MIN_VALUE;
+ }
+ return result;
+ }
+
+ /**
+ * Parse any long value or multiplication expressions into tokens.
+ *
+ * @param value the expression or value.
+ * @return an array of long tokens, never empty.
+ * @throws NullPointerException if the given value is null.
+ * @throws NumberFormatException if the expression is invalid.
+ */
+ private static String[] tokenizeLongs(String value) {
+ value = value.trim();
+ String[] e;
+ final int i = value.indexOf('*');
+ if (i > -1 && (e = value.split(
+ "[\\s]*[\\x2A]{1}[\\s]*")).length != 0) {
+ if (i == 0 || value.charAt(value.length() - 1) == '*') {
+ throw new NumberFormatException(value);
+ }
+
+ if (e.length == 1) {
+ throw new NumberFormatException(e[0]);
+ }
+ } else {
+ e = new String[]{value};
+ }
+ return e;
+ }
+
+ /**
+ * Multiply and check for overflow. This can be replaced with
+ * {_at_code java.lang.Math.multiplyExact} when JavaMail requires JDK 8.
+ *
+ * @param x the first value.
+ * @param y the second value.
+ * @return x times y.
+ * @throws ArithmeticException if overflow is detected.
+ */
+ private static long multiplyExact(final long x, final long y) {
+ long r = x * y;
+ if (((Math.abs(x) | Math.abs(y)) >>> 31L != 0L)) {
+ if (((y != 0L) && (r / y != x))
+ || (x == Long.MIN_VALUE && y == -1L)) {
+ throw new ArithmeticException();
+ }
+ }
+ return r;
+ }
+
+ /**
+ * Converts record count to a valid record count. If the value is out of
+ * bounds then the default record count is used.
+ *
+ * @param records the record count.
+ * @return a valid number of record count.
+ */
+ private static long checkRecords(final long records) {
+ return records > 0L ? records : 1000L;
+ }
+
+ /**
+ * Converts the duration to a valid duration. If the value is out of bounds
+ * then the default duration is used.
+ *
+ * @param duration the duration to check.
+ * @return a valid duration.
+ */
+ private static long checkDuration(final long duration) {
+ return duration > 0L ? duration : 15L * 60L * 1000L;
+ }
+}

diff -r d5c073eac2f6 -r d22ab7996acc mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/src/test/java/com/sun/mail/util/logging/DurationFilterTest.java Fri Oct 02 15:57:37 2015 -0700
@@ -0,0 +1,672 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015 Jason Mehrens. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
+ * or packager/legal/LICENSE.txt. See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at packager/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * Oracle designates this particular file as subject to the "Classpath"
+ * exception as provided by Oracle in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package com.sun.mail.util.logging;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.util.Properties;
+import java.util.logging.*;
+import org.junit.*;
+import static org.junit.Assert.*;
+
+/**
+ * Test case for the DurationFilter spec.
+ *
+ * @author Jason Mehrens
+ */
+public class DurationFilterTest {
+
+ public DurationFilterTest() {
+ }
+
+ @BeforeClass
+ public static void setUpClass() {
+ checkJVMOptions();
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ checkJVMOptions();
+ }
+
+ private static void checkJVMOptions() {
+ assertTrue(DurationFilterTest.class.desiredAssertionStatus());
+ assertNull(System.getProperty("java.util.logging.manager"));
+ assertNull(System.getProperty("java.util.logging.config.class"));
+ assertNull(System.getProperty("java.util.logging.config.file"));
+ assertEquals(LogManager.class, LogManager.getLogManager().getClass());
+ }
+
+ @Test
+ public void testClone() throws Exception {
+ DurationFilterExt source = new DurationFilterExt();
+ assertTrue(source instanceof Cloneable);
+ final Filter clone = source.clone();
+ assertNotNull(clone);
+ assertFalse(source == clone);
+ assertTrue(source.equals(clone));
+ assertEquals(source.getClass(), clone.getClass());
+
+ LogRecord r = new LogRecord(Level.INFO, "");
+ assertTrue(source.isLoggable(r));
+ assertFalse(source.equals(clone));
+ assertTrue(((DurationFilterExt) clone).clone().equals(clone));
+ }
+
+ @Test
+ public void testCloneState() throws Exception {
+ long millis = 0;
+ final int records = 10;
+ final int duration = 5 * 60 * 1000;
+ Level lvl = Level.INFO;
+ DurationFilterExt sf = new DurationFilterExt(records, duration);
+ String msg = Long.toString(millis);
+ LogRecord r = new LogRecord(lvl, msg);
+
+ //Allow
+ for (int i = 0; i < records; i++) {
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ Filter clone = sf.clone();
+ for (int i = 0; i < records; i++) {
+ r.setMillis(millis);
+ String m = Integer.toString(i);
+ assertFalse(m, sf.isLoggable(r));
+ assertTrue(m, clone.isLoggable(r));
+ }
+
+ assertFalse(sf.isLoggable(r));
+ assertFalse(clone.isLoggable(r));
+ }
+
+ @Test
+ @SuppressWarnings("SleepWhileInLoop")
+ public void testIsLoggableNow() throws Exception {
+ final int records = 10;
+ final int duration = 1000;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ assertTrue(sf.isLoggable());
+
+ LogRecord r = new LogRecord(lvl, "");
+ assertTrue(sf.isLoggable(r));
+ assertTrue(sf.isLoggable());
+
+ //Allow
+ for (int i = 1; i < records; i++) {
+ r = new LogRecord(lvl, "");
+ String msg = Integer.toString(i);
+ assertTrue(msg, sf.isLoggable());
+ assertTrue(msg, sf.isLoggable(r));
+ }
+
+ assertFalse(sf.isLoggable());
+ assertFalse(sf.isLoggable(r));
+
+ //Cool down and allow.
+ final long then = System.currentTimeMillis();
+ do {
+ Thread.sleep(duration + 100);
+ } while ((System.currentTimeMillis() - then) < duration);
+
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, "");
+ String msg = Integer.toString(i);
+ assertTrue(msg, sf.isLoggable());
+ assertTrue(msg, sf.isLoggable(r));
+ }
+
+ assertFalse(sf.isLoggable());
+ assertFalse(sf.isLoggable(r));
+ }
+
+ @Test
+ public void testSaturation() {
+ long millis = 0;
+ final int records = 10;
+ final int duration = 5 * 60 * 1000;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r;
+
+ //Allow
+ for (int i = 0; i < records; i++) {
+ ++millis;
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ //Saturate.
+ for (int i = 0; i < records * 10; i++) {
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertFalse(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ //Cool down and allow.
+ millis += duration;
+ for (int i = 0; i < records; i++) {
+ ++millis;
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ }
+ }
+
+ @Test
+ public void testSaturateIntergral() {
+ long duration = 15L * 60L * 1000L;
+ for (long i = 0; i <= duration * 2; i++) {
+ testSaturateIntergral(i, duration);
+ }
+ }
+
+ private void testSaturateIntergral(long millis, long duration) {
+ final int records = 10;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r = new LogRecord(lvl, "");
+ sf.isLoggable(r); //Init the duration.
+ millis += (2 * duration) - 1;
+ for (int i = 0; i < records - 2; i++) {
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ millis += 100;
+ r.setMillis(millis);
+ assertTrue(sf.isLoggable(r));
+
+ for (int i = 0; i < records - 1; i++) {
+ r.setMillis(millis);
+ assertFalse(Integer.toString(i), sf.isLoggable(r));
+ }
+ }
+
+ @Test
+ public void testSaturatePositiveOutOfOrder() {
+ testSaturateOutOfOrder(Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void testSaturateNegativeOutOfOrder() {
+ testSaturateOutOfOrder(-Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void testSaturateOverFlowOutOfOrder() {
+ testSaturateOutOfOrder(Long.MAX_VALUE);
+ }
+
+ @Test
+ public void testSaturateUnderFlowOutOfOrder() {
+ testSaturateOutOfOrder(-Long.MAX_VALUE);
+ }
+
+ public void testSaturateOutOfOrder(long millis) {
+ final int records = 10;
+ final int duration = 5 * 60 * 1000;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r;
+
+ //Allow
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ --millis;
+ }
+
+ //Still saturated.
+ millis += duration;
+ final long peek = millis;
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertFalse(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ //Cool down and allow.
+ millis = peek + duration;
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ ++millis;
+ }
+ }
+
+ @Test
+ public void testTimeSpringShort() {
+ testClockAdjustment(5 * 60 * 1000, 1);
+ }
+
+ @Test
+ public void testTimeSpringLong() {
+ testClockAdjustment(24 * 60 * 60 * 1000, 1);
+ }
+
+ @Test
+ public void testTimeFallShort() {
+ testClockAdjustment(5 * 60 * 1000, -1);
+ }
+
+ @Test
+ public void testTimeFallLong() {
+ testClockAdjustment(24 * 60 * 60 * 1000, -1);
+ }
+
+ private void testClockAdjustment(int records, int signum) {
+ assertFalse(0 == signum);
+ assertEquals(Integer.signum(signum), signum);
+ long millis = 0L;
+ DurationFilter sf = new DurationFilter(records, records);
+ LogRecord r = new LogRecord(Level.INFO, "");
+ for (int i = 1; i < (records / 2); i++) {
+ r.setMillis(++millis);
+ assertTrue(sf.isLoggable(r));
+ }
+
+ millis += signum * (60L * 60L * 1000L);
+ for (int i = (records / 2); i <= records; i++) {
+ r.setMillis(++millis);
+ assertTrue(sf.isLoggable(r));
+ }
+ }
+
+ @Test
+ public void testPredictedOverflow() {
+ int records = 4;
+ int duration = 4;
+ DurationFilter sf = new DurationFilter(records, duration);
+ for (int i = 0; i < records; i++) {
+ LogRecord r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MAX_VALUE);
+ assertTrue(sf.isLoggable(r));
+ }
+
+ LogRecord r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MAX_VALUE);
+ assertFalse(sf.isLoggable(r));
+
+ r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MAX_VALUE + duration);
+ assertTrue(sf.isLoggable(r));
+ }
+
+ @Test
+ public void testMillisNegativeSaturation() {
+ int records = 4;
+ int duration = 4;
+ DurationFilter sf = new DurationFilter(records, duration);
+ for (int i = 0; i < records; i++) {
+ LogRecord r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MIN_VALUE);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ }
+
+ LogRecord r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MIN_VALUE);
+ assertFalse(sf.isLoggable(r));
+
+ r = new LogRecord(Level.INFO, "");
+ r.setMillis(Long.MIN_VALUE + duration);
+ assertTrue(sf.isLoggable(r));
+ }
+
+ @Test
+ public void testExactRate() throws Exception {
+ long millis = System.currentTimeMillis();
+ final int records = 1000;
+ final int duration = 5 * 60 * 1000;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r;
+
+ int period = duration / records;
+ assertEquals(period, (double) duration / (double) records, 0.0);
+ for (int i = 0; i < records * records; i++) {
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ millis += period;
+ }
+ }
+
+ @Test
+ public void testCeilRate() throws Exception {
+ double millis = 0L;
+ final int records = 3;
+ final int duration = 40;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r;
+
+ double period = duration / (double) records;
+ for (int i = 0; i < (duration * records) * 2; i++) {
+ r = new LogRecord(lvl, Double.toString(millis));
+ r.setMillis((long) millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ millis = millis + Math.ceil(period);
+ }
+ }
+
+ @Test
+ public void testFloorRate() {
+ double millis = 0.0d;
+ final int records = 30;
+ final int duration = 400;
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r;
+ long period = duration / records;
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, Long.toString((long) millis));
+ r.setMillis((long) millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ millis += period;
+ }
+
+ //Saturated for records + one.
+ for (int i = 0; i <= records; i++) {
+ r = new LogRecord(lvl, Long.toString((long) millis));
+ r.setMillis((long) millis);
+ assertFalse(Integer.toString(i), sf.isLoggable(r));
+ millis += period;
+ }
+
+ for (int i = 0; i < records; i++) {
+ r = new LogRecord(lvl, Long.toString((long) millis));
+ r.setMillis((long) millis);
+ assertTrue(Integer.toString(i), sf.isLoggable(r));
+ millis += period;
+ }
+ }
+
+ private void testRate(long millis, long records, long duration) {
+ Level lvl = Level.INFO;
+ DurationFilter sf = new DurationFilter(records, duration);
+ LogRecord r = new LogRecord(lvl, Long.toString(millis));
+
+ for (long i = 0; i < records; i++) {
+ r.setMillis(millis);
+ assertTrue(sf.isLoggable(r));
+ }
+
+ r = new LogRecord(lvl, Long.toString(millis));
+ r.setMillis(millis);
+ assertFalse(sf.isLoggable(r));
+ }
+
+ @Test
+ public void testOneTenthErrorRate() {
+ testRate(0, 10, 1);
+ }
+
+ @Test
+ public void testOneHundredthErrorRate() {
+ testRate(0, 100, 1);
+ }
+
+ @Test
+ public void testOneThousanthErrorRate() {
+ testRate(0, 1000, 1);
+ }
+
+ @Test
+ public void testOneMillionthErrorRate() {
+ testRate(0, 1000000, 1);
+ }
+
+ @Test
+ public void testTwoToThe53rdRate() {
+ testRate(0, 1, 1L << 53L);
+ }
+
+ @Ignore
+ public void testIntegerMaxValueByTenRate() {
+ /**
+ * This can take a few minutes to run.
+ */
+ testRate(0, Integer.MAX_VALUE, 10);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testIsLoggableNull() {
+ new DurationFilter().isLoggable((LogRecord) null);
+ }
+
+ @Test
+ public void testEquals() {
+ DurationFilter one = new DurationFilter();
+ DurationFilter two = new DurationFilter();
+ assertTrue(one.equals(one));
+ assertTrue(two.equals(two));
+ assertTrue(one.equals(two));
+ assertTrue(two.equals(one));
+
+ LogRecord r = new LogRecord(Level.INFO, "");
+ assertTrue(one.isLoggable(r));
+ assertTrue(one.equals(one));
+ assertTrue(two.equals(two));
+ assertFalse(one.equals(two));
+ assertFalse(two.equals(one));
+ }
+
+ @Test
+ public void testHashCode() {
+ DurationFilter one = new DurationFilter(10, 10);
+ DurationFilter two = new DurationFilter(10, 10);
+ DurationFilter three = new DurationFilter(3, 3);
+
+ assertTrue(one.hashCode() == two.hashCode());
+ assertFalse(one.hashCode() == three.hashCode());
+
+ LogRecord r = new LogRecord(Level.INFO, "");
+ assertTrue(one.isLoggable(r));
+ assertTrue(one.hashCode() == two.hashCode());
+ assertFalse(one.hashCode() == three.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ testToString(new DurationFilter());
+ }
+
+ @Test
+ public void testToStringEx() {
+ testToString(new DurationFilterExt());
+ }
+
+ private void testToString(DurationFilter f) {
+ String s = f.toString();
+ assertTrue(s.startsWith(f.getClass().getName()));
+ assertTrue(s.contains("records="));
+ assertTrue(s.contains("duration="));
+ assertTrue(s.contains("loggable="));
+ }
+
+ @Test
+ public void testInitRecords() throws Exception {
+ testInitRecords("210", 210);
+ }
+
+ @Test
+ public void testInitRecordsZero() throws Exception {
+ testInitRecords("0", 1000);
+ }
+
+ @Test
+ public void testInitRecordsNegative() throws Exception {
+ testInitRecords("-1", 1000);
+ }
+
+ @Test
+ public void testInitDuration() throws Exception {
+ testInitDuration("1024", 1024);
+ }
+
+ @Test
+ public void testInitDurationZero() throws Exception {
+ testInitDuration("0", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationNegative() throws Exception {
+ testInitDuration("-1", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExp() throws Exception {
+ testInitDuration("15 * 60 * 1000", 15L * 60L * 1000L);
+ testInitDuration("15*60*1000", 15L * 60L * 1000L);
+ testInitDuration("15L * 60L * 1000L", 15L * 60L * 1000L);
+ testInitDuration("15L*60L*1000L", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpLifetime() throws Exception {
+ testInitDuration("125L * 366 * 24L * 60L * 60L * 1000L",
+ 125L * 366 * 24L * 60L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpOverflow() throws Exception {
+ testInitDuration(Long.MAX_VALUE + " * "
+ + Long.MAX_VALUE, 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpAlpha() throws Exception {
+ testInitDuration("15LL * 60 * 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpTooManyMult() throws Exception {
+ testInitDuration("15L ** 60 ** 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpTrailing() throws Exception {
+ testInitDuration("15 * 60 * 1000*", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpSpace() throws Exception {
+ testInitDuration("15 * 60 * 1000* ", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpLeading() throws Exception {
+ testInitDuration("*15 * 60 * 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpAdd() throws Exception {
+ testInitDuration("15 + 60 + 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpDivide() throws Exception {
+ testInitDuration("15 / 60 / 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitDurationExpSubstract() throws Exception {
+ testInitDuration("15 - 60 - 1000", 15L * 60L * 1000L);
+ }
+
+ @Test
+ public void testInitNegativeDuration() throws Exception {
+ testInitDuration("-1024", 15L * 60L * 1000L);
+ }
+
+ private void testInitDuration(String d, long expect) throws Exception {
+ testInit("duration", d, expect);
+ }
+
+ private void testInitRecords(String r, long expect) throws Exception {
+ testInit("records", r, expect);
+ }
+
+ private void testInit(String field, String value, long expect) throws Exception {
+ String p = DurationFilter.class.getName();
+ Properties props = new Properties();
+ props.put(p + '.' + field, value);
+ LogManager m = LogManager.getLogManager();
+ try {
+ read(m, props);
+ DurationFilter sf = new DurationFilter();
+ Field f = DurationFilter.class.getDeclaredField(field);
+ f.setAccessible(true);
+ assertEquals(expect, f.get(sf));
+ } finally {
+ m.reset();
+ }
+ }
+
+ private void read(LogManager manager, Properties props) throws IOException {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream(512);
+ props.store(out, "No comment");
+ manager.readConfiguration(new ByteArrayInputStream(out.toByteArray()));
+ }
+
+ public final class DurationFilterExt extends DurationFilter
+ implements Cloneable {
+
+ public DurationFilterExt() {
+ super();
+ }
+
+ public DurationFilterExt(long records, long duration) {
+ super(records, duration);
+ }
+
+ @Override
+ public DurationFilterExt clone() throws CloneNotSupportedException {
+ return (DurationFilterExt) super.clone();
+ }
+ }
+}