Add a RFC3339 parser

pull/17/merge
Richard Körber 2015-12-26 18:00:07 +01:00
parent 78ccec7d1d
commit 74750a9f88
2 changed files with 249 additions and 0 deletions

View File

@ -0,0 +1,86 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j.util;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses a timestamp as defined in RFC 3339.
*
* @see <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>
* @author Richard "Shred" Körber
*/
public class TimestampParser {
private static final Pattern DATE_PATTERN = Pattern.compile(
"^(\\d{4})-(\\d{2})-(\\d{2})T"
+ "(\\d{2}):(\\d{2}):(\\d{2})"
+ "(?:\\.(\\d{1,3})\\d*)?"
+ "(Z|[+-]\\d{2}:?\\d{2})$", Pattern.CASE_INSENSITIVE);
private static final Pattern TZ_PATTERN = Pattern.compile(
"([+-])(\\d{2}):?(\\d{2})$");
/**
* Parses a RFC 3339 formatted date.
*
* @param str
* Date string
* @return {@link Date} that was parsed
* @throws IllegalArgumentException
* if the date string was not RFC 3339 formatted
*/
public static Date parse(String str) {
Matcher m = DATE_PATTERN.matcher(str);
if (!m.matches()) {
throw new IllegalArgumentException("Illegal date: " + str);
}
int year = Integer.parseInt(m.group(1));
int month = Integer.parseInt(m.group(2));
int dom = Integer.parseInt(m.group(3));
int hour = Integer.parseInt(m.group(4));
int minute = Integer.parseInt(m.group(5));
int second = Integer.parseInt(m.group(6));
String msStr = m.group(7);
if (msStr == null) {
msStr = "000";
} else {
while (msStr.length() < 3) {
msStr += '0';
}
}
int ms = Integer.parseInt(msStr);
String tz = m.group(8);
if ("Z".equalsIgnoreCase(tz)) {
tz = "GMT";
} else {
tz = TZ_PATTERN.matcher(tz).replaceAll("GMT$1$2:$3");
}
Calendar cal = GregorianCalendar.getInstance(TimeZone.getTimeZone(tz));
cal.clear();
cal.set(year, month - 1, dom, hour, minute, second);
cal.set(Calendar.MILLISECOND, ms);
return cal.getTime();
}
}

View File

@ -0,0 +1,163 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2015 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j.util;
import static org.junit.Assert.*;
import static org.shredzone.acme4j.util.TimestampParser.parse;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Test;
/**
* Unit tests for {@link TimestampParser}.
*
* @author Richard "Shred" Körber
*/
public class TimestampParserTest {
/**
* Test valid strings.
*/
@Test
public void testParser() {
assertThat(parse("2015-12-27T22:58:35.006769519Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.00676951Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.0067695Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.006769Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.00676Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.0067Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.006Z"), isDate(2015, 12, 27, 22, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.01Z"), isDate(2015, 12, 27, 22, 58, 35, 10));
assertThat(parse("2015-12-27T22:58:35.2Z"), isDate(2015, 12, 27, 22, 58, 35, 200));
assertThat(parse("2015-12-27T22:58:35Z"), isDate(2015, 12, 27, 22, 58, 35));
assertThat(parse("2015-12-27t22:58:35z"), isDate(2015, 12, 27, 22, 58, 35));
assertThat(parse("2015-12-27T22:58:35.006769519+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.006+02:00"), isDate(2015, 12, 27, 20, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35+02:00"), isDate(2015, 12, 27, 20, 58, 35));
assertThat(parse("2015-12-27T21:58:35.006769519-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6));
assertThat(parse("2015-12-27T21:58:35.006-02:00"), isDate(2015, 12, 27, 23, 58, 35, 6));
assertThat(parse("2015-12-27T21:58:35-02:00"), isDate(2015, 12, 27, 23, 58, 35));
assertThat(parse("2015-12-27T22:58:35.006769519+0200"), isDate(2015, 12, 27, 20, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35.006+0200"), isDate(2015, 12, 27, 20, 58, 35, 6));
assertThat(parse("2015-12-27T22:58:35+0200"), isDate(2015, 12, 27, 20, 58, 35));
assertThat(parse("2015-12-27T21:58:35.006769519-0200"), isDate(2015, 12, 27, 23, 58, 35, 6));
assertThat(parse("2015-12-27T21:58:35.006-0200"), isDate(2015, 12, 27, 23, 58, 35, 6));
assertThat(parse("2015-12-27T21:58:35-0200"), isDate(2015, 12, 27, 23, 58, 35));
}
/**
* Test invalid strings.
*/
@Test
public void testInvalid() {
try {
parse("");
fail("accepted empty string");
} catch (IllegalArgumentException ex) {
// expected
}
try {
parse("abc");
fail("accepted nonsense string");
} catch (IllegalArgumentException ex) {
// expected
}
try {
parse("2015-12-27");
fail("accepted year only string");
} catch (IllegalArgumentException ex) {
// expected
}
try {
parse("2015-12-27T");
fail("accepted year only string");
} catch (IllegalArgumentException ex) {
// expected
}
}
/**
* Matches the given time.
*/
private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second) {
return isDate(year, month, dom, hour, minute, second, 0);
}
/**
* Matches the given time and milliseconds.
*/
private DateMatcher isDate(int year, int month, int dom, int hour, int minute, int second, int ms) {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.clear();
cal.set(year, month - 1, dom, hour, minute, second);
cal.set(Calendar.MILLISECOND, ms);
return new DateMatcher(cal);
}
/**
* Date matcher that gives a readable output on mismatch.
*/
private static class DateMatcher extends BaseMatcher<Date> {
private final Calendar cal;
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH);
public DateMatcher(Calendar cal) {
this.cal = cal;
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
public boolean matches(Object item) {
if (!(item instanceof Date)) {
return false;
}
Date date = (Date) item;
return date.equals(cal.getTime());
}
@Override
public void describeTo(Description description) {
description.appendValue(sdf.format(cal.getTime()));
}
@Override
public void describeMismatch(Object item, Description description) {
if (!(item instanceof Date)) {
description.appendText("is not a Date");
return;
}
Date date = (Date) item;
description.appendText("was ").appendValue(sdf.format(date));
}
}
}