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());