/*******************************************************************************
* Copyright 2013 Open mHealth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
/*
* Copyright 2012 John Jenkins
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openmhealth.reference.util;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.DateTimeParser;
import org.joda.time.format.DateTimeParserBucket;
import org.joda.time.format.ISODateTimeFormat;
/**
* <p>
* Factory that creates instances of DateTimeFormatter for the W3C's profile of
* the ISO 8601 standard.
* </p>
*
* <p>
* ISO8601 is the international standard for data interchange and defines many
* formats for representing date and time information. W3C has created a
* shorter, more specific list of formats.
* </p>
*
* <p>
* One major difference between this and the ISODateTimeFormatter is that this
* will always parse non-zone'd values, e.g. year, year-month, and
* year-month-day, with a default zone of UTC as opposed to the
* ISODateTimeFormatter which will give them JodaTime's configured default time
* zone.
* </p>
*
* @author John Jenkins
*/
public class ISOW3CDateTimeFormat {
/**
* <p>
* A DateTimeParser that implements the W3C profile of the ISO 8601
* representation of dates and times
* (<a href="http://www.w3.org/TR/NOTE-datetime">
* http://www.w3.org/TR/NOTE-datetime
* </a>).
* </p>
*
* <p>
* The most common use-case of this library is through the
* ISOW3CDateTimeFormat, which will create an instance of this parser.
*
* <pre>ISOW3CDateTimeFormat.dateTime();</pre>
*
* For date-only formats, the zone is set to UTC. Like all DateTimeParsers,
* the zone information is overridden with JodaTime's default zone. If you
* are creating this parser yourself and would like to preserve the zone,
* be sure to call 'withOffsetParsed()' on the resulting parser.
*
* <pre>(new ISOW3CDateTimeParser()).withOffsetParsed();</pre>
*
* </p>
*
* @author John Jenkins
*/
public static class ISOW3CDateTimeParser implements DateTimeParser {
/**
* The date-time formatter for the W3C's specifications for date-only
* values.
*/
private static final DateTimeParser ISO_W3C_DATE_PARSER;
static {
// The W3C defines 3 formats as valid ISO date values.
DateTimeParser[] parsers = new DateTimeParser[3];
// Just the year.
parsers[0] = ISOW3CDateTimeFormat.year().getParser();
// The year and month.
parsers[1] = ISOW3CDateTimeFormat.yearMonth().getParser();
// The year, month, and day.
parsers[2] =
ISOW3CDateTimeFormat.yearMonthDay().getParser();
// Build the parser with the 3 sub-parsers.
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.append(null, parsers);
ISO_W3C_DATE_PARSER = builder.toParser();
}
/**
* The date-time formatter for the W3C's specifications for date-time
* values.
*/
private static final DateTimeParser ISO_W3C_DATE_TIME_PARSER;
static {
// The W3C defines 3 formats as valid ISO date-time values.
DateTimeParser[] parsers = new DateTimeParser[3];
// The year, month, day, hour, minute, and time zone.
// Build the parser from the existing ISODateTimeParser for the
// year, month, day, hour, and minute and add the time zone.
parsers[0] = ISOW3CDateTimeFormat.dateHourMinuteZone().getParser();
// The year, month, day, hour, minute, second, and time zone.
parsers[1] =
ISOW3CDateTimeFormat.dateHourMinuteSecondZone().getParser();
// The year, month, day, hour, minute, and time zone.
// Build the parser from the existing ISODateTimeParser for the
// year, month, day, hour, and minute and add the time zone.
parsers[2] = ISOW3CDateTimeFormat.dateTime().getParser();
// Build the parser with the 3 sub-parsers.
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.append(null, parsers);
ISO_W3C_DATE_TIME_PARSER = builder.toParser();
}
/*
* (non-Javadoc)
* @see org.joda.time.format.DateTimeParser#estimateParsedLength()
*/
@Override
public int estimateParsedLength() {
return
Math.max(
ISO_W3C_DATE_PARSER.estimateParsedLength(),
ISO_W3C_DATE_TIME_PARSER.estimateParsedLength());
}
/*
* (non-Javadoc)
* @see org.joda.time.format.DateTimeParser#parseInto(org.joda.time.format.DateTimeParserBucket, java.lang.String, int)
*/
@Override
public int parseInto(
final DateTimeParserBucket bucket,
final String text,
final int position) {
// Save the current state of the bucket.
Object bucketState = bucket.saveState();
// Attempt to parse the text with the date-only parser.
int newPosition =
ISO_W3C_DATE_PARSER.parseInto(bucket, text, position);
// If the parser returned a negative number, that represents an
// error and should be propagated.
if(newPosition < 0) {
return newPosition;
}
// If the new position is the length of the text, then the entire
// string was parsed. We must set the time zone to UTC for these
// values.
else if(newPosition == text.length()) {
bucket.setZone(DateTimeZone.UTC);
return newPosition;
}
// Otherwise, this parser is inadequate and we must reset the
// bucket and use the other parser.
else {
// FIXME: This shouldn't be necessary, and we should be able to
// just continue parsing where we left off. If that were the
// case, then this Parser could be removed as could the any()
// function in the ISOW3CDateTimeFormat function, and users
// could create any combination of the ISOW3CDateTimeFormats.
bucket.restoreState(bucketState);
return
ISO_W3C_DATE_TIME_PARSER.parseInto(bucket, text, position);
}
}
}
// Lazily instantiate the internal formatters.
private static DateTimeFormatter
year,
yearMonth,
yearMonthDay,
yearMonthDayHourMinuteZone,
yearMonthDayHourMinuteSecondZone,
yearMonthDayHourMinuteSecondMillisZone,
any;
/**
* Default constructor. Does nothing.
*/
protected ISOW3CDateTimeFormat() {
super();
}
/**
* Returns a formatter for the four digit year with a time zone of UTC.
*
* @return A formatter for the year with a time zone of UTC.
*/
public static DateTimeFormatter year() {
if(year == null) {
year = ISODateTimeFormat.year().withZoneUTC();
}
return year;
}
/**
* Returns a formatter for the four digit year and two digit month of
* the year with a time zone of UTC.
*
* @return A formatter for the year and month with a time zone of UTC.
*/
public static DateTimeFormatter yearMonth() {
if(yearMonth == null) {
yearMonth = ISODateTimeFormat.yearMonth().withZoneUTC();
}
return yearMonth;
}
/**
* Returns a formatter for the four digit year, two digit month of the
* year, and two digit day of the month with a time zone of UTC.
*
* @return A formatter for the year, month, and day with a time zone of
* UTC.
*/
public static DateTimeFormatter yearMonthDay() {
if(yearMonthDay == null) {
yearMonthDay = ISODateTimeFormat.yearMonthDay().withZoneUTC();
}
return yearMonthDay;
}
/**
* Returns a formatter for the four digit year, two digit month of the
* year, and two digit day of the month with a time zone of UTC.
*
* @return A formatter for the year, month, and day with a time zone of
* UTC.
*/
public static DateTimeFormatter date() {
return yearMonthDay();
}
/**
* Returns a formatter that combines a full date, two digit hour of the
* day, two digit minute of the hour and a time zone.
*
* @return A formatter for the date, hour, minute, and time zone.
*/
public static DateTimeFormatter dateHourMinuteZone() {
if(yearMonthDayHourMinuteZone == null) {
DateTimeFormatterBuilder dateHourMinuteTimezone =
new DateTimeFormatterBuilder();
dateHourMinuteTimezone
.append(ISODateTimeFormat.dateHourMinute());
dateHourMinuteTimezone.append(DateTimeFormat.forPattern("ZZ"));
yearMonthDayHourMinuteZone =
dateHourMinuteTimezone.toFormatter().withOffsetParsed();
}
return yearMonthDayHourMinuteZone;
}
/**
* Returns a formatter that combines a full date, two digit hour of the
* day, two digit minute of the hour, two digit second of the minute
* and a time zone.
*
* @return A formatter for the date, hour, minute, second, and time
* zone.
*/
public static DateTimeFormatter dateHourMinuteSecondZone() {
if(yearMonthDayHourMinuteSecondZone == null) {
yearMonthDayHourMinuteSecondZone =
ISODateTimeFormat.dateTimeNoMillis().withOffsetParsed();
}
return yearMonthDayHourMinuteSecondZone;
}
/**
* Returns a formatter that combines a full date, two digit hour of the
* day, two digit minute of the hour, two digit second of the minute
* and a time zone.
*
* @return A formatter for the date, hour, minute, second, and time
* zone.
*/
public static DateTimeFormatter dateTimeNoMillis() {
return dateHourMinuteSecondZone();
}
/**
* Returns a formatter that combines a full date, two digit hour of the
* day, two digit minute of the hour, two digit second of the minute,
* three digit milliseconds of the second and a time zone.
*
* @return A formatter for the date, hour, minute, second, millisecond,
* and time zone.
*/
public static DateTimeFormatter dateHourMinuteSecondMillisZone() {
if(yearMonthDayHourMinuteSecondMillisZone == null) {
yearMonthDayHourMinuteSecondMillisZone =
ISODateTimeFormat.dateTime().withOffsetParsed();
}
return yearMonthDayHourMinuteSecondMillisZone;
}
/**
* Returns a formatter that combines a full date, two digit hour of the
* day, two digit minute of the hour, two digit second of the minute,
* three digit milliseconds of the second and a time zone.
*
* @return A formatter for the date, hour, minute, second, millisecond,
* and time zone.
*/
public static DateTimeFormatter dateTime() {
return dateHourMinuteSecondMillisZone();
}
/**
* Returns a formatter that combines all ISOW3CDateTimeFormats. This
* formatter will correctly parse any of the other formatters and will only
* fail if the value doesn't match any of them.
*
* @return A universal DateTimeFormatter for all ISO W3C date-time formats.
*
* @see #year()
* @see #yearMonth()
* @see #yearMonthDay()
* @see #dateHourMinuteZone()
* @see #dateHourMinuteSecondZone()
* @see #dateHourMinuteSecondMillisZone()
*/
public static DateTimeFormatter any() {
if(any == null) {
any =
new DateTimeFormatter(
ISODateTimeFormat.dateTime().getPrinter(),
new ISOW3CDateTimeParser())
.withOffsetParsed();
}
return any;
}
}