Skip to content
Permalink
Browse files
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
  • Loading branch information
chibenwa committed Jun 20, 2021
1 parent 566ebcd commit 999f319dcfe7786f15177378f4d7e2b028e74809
Showing 2 changed files with 115 additions and 37 deletions.
@@ -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 @@ private void parse() {
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
}
}

@@ -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 {
@@ -44,6 +44,18 @@ public void testDateDST() throws Exception {
Assert.assertEquals(new Date(1216221153000L), f.getDate());
}

@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");
@@ -69,6 +81,13 @@ public void testdd() throws Exception {
Assert.assertEquals(43200000L, f.getDate().getTime());
}

@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");

0 comments on commit 999f319

Please sign in to comment.