MIME4J-298 Convert DateTimeFieldLenientImpl to DateTimeFormatter (#44)
This allows:
- Specifying all patterns at once, avoiding one parsing pass per pattern
- DateTimeFormatter is thread safe, thus can be initialized once and reused
Special care have been taken to preserve previous behaviour (missing tests were added):
- Accept extra input after the date
- Relax cross-validation
diff --git a/dom/src/main/java/org/apache/james/mime4j/field/DateTimeFieldLenientImpl.java b/dom/src/main/java/org/apache/james/mime4j/field/DateTimeFieldLenientImpl.java
index 06dce84..43e7949 100644
--- a/dom/src/main/java/org/apache/james/mime4j/field/DateTimeFieldLenientImpl.java
+++ b/dom/src/main/java/org/apache/james/mime4j/field/DateTimeFieldLenientImpl.java
@@ -19,14 +19,29 @@
package org.apache.james.mime4j.field;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Collections;
+import static java.time.temporal.ChronoField.DAY_OF_MONTH;
+import static java.time.temporal.ChronoField.DAY_OF_WEEK;
+import static java.time.temporal.ChronoField.HOUR_OF_DAY;
+import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
+import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
+import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
+import static java.time.temporal.ChronoField.OFFSET_SECONDS;
+import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
+import static java.time.temporal.ChronoField.YEAR;
+
+import java.text.ParsePosition;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.ResolverStyle;
+import java.time.format.SignStyle;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalField;
import java.util.Date;
-import java.util.List;
+import java.util.HashMap;
import java.util.Locale;
-import java.util.TimeZone;
+import java.util.Map;
import org.apache.james.mime4j.codec.DecodeMonitor;
import org.apache.james.mime4j.dom.FieldParser;
@@ -37,34 +52,83 @@
* Date-time field such as <code>Date</code> or <code>Resent-Date</code>.
*/
public class DateTimeFieldLenientImpl extends AbstractField implements DateTimeField {
+ private static final int INITIAL_YEAR = 1970;
+ public static final DateTimeFormatter RFC_5322 = new DateTimeFormatterBuilder()
+ .parseCaseInsensitive()
+ .parseLenient()
+ .optionalStart()
+ .appendText(DAY_OF_WEEK, dayOfWeek())
+ .appendLiteral(", ")
+ .optionalEnd()
+ .appendValue(DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
+ .appendLiteral(' ')
+ .appendText(MONTH_OF_YEAR, monthOfYear())
+ .appendLiteral(' ')
+ .appendValueReduced(YEAR, 2, 4, INITIAL_YEAR)
+ .appendLiteral(' ')
+ .appendValue(HOUR_OF_DAY, 2)
+ .appendLiteral(':')
+ .appendValue(MINUTE_OF_HOUR, 2)
+ .optionalStart()
+ .appendLiteral(':')
+ .appendValue(SECOND_OF_MINUTE, 2)
+ .optionalEnd()
+ .optionalStart()
+ .appendLiteral('.')
+ .appendValue(MILLI_OF_SECOND, 3)
+ .optionalEnd()
+ .optionalStart()
+ .appendLiteral(' ')
+ .appendOffset("+HHMM", "GMT")
+ .optionalEnd()
+ .optionalStart()
+ .appendLiteral(' ')
+ .appendOffsetId()
+ .optionalEnd()
+ .optionalStart()
+ .appendLiteral(' ')
+ .appendPattern("0000")
+ .optionalEnd()
+ .toFormatter()
+ .withZone(ZoneId.of("GMT"))
+ .withResolverStyle(ResolverStyle.LENIENT)
+ .withResolverFields(DAY_OF_MONTH, MONTH_OF_YEAR, YEAR, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE, MILLI_OF_SECOND, OFFSET_SECONDS)
+ .withLocale(Locale.US);
- private static final String[] DEFAULT_DATE_FORMATS = {
- "EEE, dd MMM yy HH:mm:ss ZZZZ",
- "dd MMM yy HH:mm:ss ZZZZ",
- "EEE, dd MMM yy HH:mm:ss.SSS 0000",
- "EEE, dd MMM yy HH:mm:ss 0000",
- "EEE, dd MMM yyyy HH:mm:ss ZZZZ",
- "dd MMM yyyy HH:mm:ss ZZZZ",
- "EEE, dd MMM yyyy HH:mm:ss.SSS 0000",
- "EEE, dd MMM yyyy HH:mm:ss 0000",
- "EEE, dd MMM yy HH:mm:ss X",
- "dd MMM yy HH:mm:ss X",
- "EEE, dd MMM yy HH:mm:ss.SSS X",
- "EEE, dd MMM yy HH:mm:ss X",
- "EEE, dd MMM yyyy HH:mm:ss X",
- "dd MMM yyyy HH:mm:ss X",
- "EEE, dd MMM yyyy HH:mm:ss.SSS X",
- "EEE, dd MMM yyyy HH:mm:ss X",
- };
+ private static Map<Long, String> monthOfYear() {
+ HashMap<Long, String> result = new HashMap<>();
+ result.put(1L, "Jan");
+ result.put(2L, "Feb");
+ result.put(3L, "Mar");
+ result.put(4L, "Apr");
+ result.put(5L, "May");
+ result.put(6L, "Jun");
+ result.put(7L, "Jul");
+ result.put(8L, "Aug");
+ result.put(9L, "Sep");
+ result.put(10L, "Oct");
+ result.put(11L, "Nov");
+ result.put(12L, "Dec");
+ return result;
+ }
- private final List<String> datePatterns;
+ private static Map<Long, String> dayOfWeek() {
+ HashMap<Long, String> result = new HashMap<>();
+ result.put(1L, "Mon");
+ result.put(2L, "Tue");
+ result.put(3L, "Wed");
+ result.put(4L, "Thu");
+ result.put(5L, "Fri");
+ result.put(6L, "Sat");
+ result.put(7L, "Sun");
+ return result;
+ }
private boolean parsed = false;
private Date date;
private DateTimeFieldLenientImpl(Field rawField, DecodeMonitor monitor) {
super(rawField, monitor);
- this.datePatterns = Collections.unmodifiableList(Arrays.asList(DEFAULT_DATE_FORMATS));
}
public Date getDate() {
@@ -81,15 +145,10 @@
if (body != null) {
body = body.trim();
}
- for (String datePattern : datePatterns) {
- try {
- SimpleDateFormat parser = new SimpleDateFormat(datePattern, Locale.US);
- parser.setTimeZone(TimeZone.getTimeZone("GMT"));
- parser.setLenient(true);
- date = parser.parse(body);
- break;
- } catch (ParseException ignore) {
- }
+ try {
+ date = Date.from(Instant.from(RFC_5322.parse(body, new ParsePosition(0))));
+ } catch (Exception e) {
+ // Ignore
}
}
diff --git a/dom/src/test/java/org/apache/james/mime4j/field/LenientDateTimeFieldTest.java b/dom/src/test/java/org/apache/james/mime4j/field/LenientDateTimeFieldTest.java
index 55a4738..284faac 100644
--- a/dom/src/test/java/org/apache/james/mime4j/field/LenientDateTimeFieldTest.java
+++ b/dom/src/test/java/org/apache/james/mime4j/field/LenientDateTimeFieldTest.java
@@ -19,6 +19,8 @@
package org.apache.james.mime4j.field;
+import java.util.Date;
+
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.field.DateTimeField;
import org.apache.james.mime4j.stream.RawField;
@@ -28,8 +30,6 @@
import org.junit.Assert;
import org.junit.Test;
-import java.util.Date;
-
public class LenientDateTimeFieldTest {
static DateTimeField parse(final String s) throws MimeException {
@@ -45,6 +45,18 @@
}
@Test
+ public void extraPDTShouldBeTolerated() throws Exception {
+ DateTimeField f = parse("Date: Wed, 16 Jul 2008 17:12:33 +0200 (PDT)");
+ Assert.assertEquals(new Date(1216221153000L), f.getDate());
+ }
+
+ @Test
+ public void extraCharsShouldBeTolerated() throws Exception {
+ DateTimeField f = parse("Date: Thu, 4 Oct 2001 20:12:26 -0700 (PDT),Thu, 4 Oct 2001 20:12:26 -0700");
+ Assert.assertEquals(new Date(1002251546000L), f.getDate());
+ }
+
+ @Test
public void parseShouldSupportPartialYears() throws Exception {
DateTimeField f = parse("Date: Wed, 16 Jul 08 17:12:33 +0200");
Assert.assertEquals(new Date(1216221153000L), f.getDate());
@@ -70,6 +82,13 @@
}
@Test
+ public void parseShouldAcceptWrongDayOfWeek() throws Exception {
+ // Should be Thu
+ DateTimeField f = parse("Date: Fri, 01 Jan 1970 12:00:00 +0000");
+ Assert.assertEquals(43200000L, f.getDate().getTime());
+ }
+
+ @Test
public void testMime4j219() throws Exception {
DateTimeField f = parse("Date: Tue, 17 Jul 2012 22:23:35.882 0000");
Assert.assertEquals(1342563815882L, f.getDate().getTime());