/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * * 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.apache.streams.data.util; import org.joda.time.DateTime; 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 java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Parses and formats dates to Joda Time {@link org.joda.time.DateTime} and to RFC3339 compatible Strings */ public class RFC3339Utils { private static final RFC3339Utils INSTANCE = new RFC3339Utils(); public static RFC3339Utils getInstance() { return INSTANCE; } private static final String BASE = "^[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}"; private static final String TZ = "[+-][0-9]{2}:?[0-9]{2}$"; private static final String SUB_SECOND = "\\.([0-9]*)"; private static final String UTC = "Z$"; private static final Pattern MILLIS = Pattern.compile("^[0-9]*$"); private static final Pattern UTC_STANDARD = Pattern.compile(BASE + UTC); private static final Pattern UTC_SUB_SECOND = Pattern.compile(BASE + SUB_SECOND + UTC); private static final Pattern LOCAL_STANDARD = Pattern.compile(BASE + TZ); private static final Pattern LOCAL_SUB_SECOND = Pattern.compile(BASE + SUB_SECOND + TZ); private static final String BASE_FMT = "yyyy-MM-dd'T'HH:mm:ss"; public static final DateTimeFormatter UTC_STANDARD_FMT = DateTimeFormat.forPattern(BASE_FMT + "'Z'").withZoneUTC(); public static final DateTimeFormatter UTC_SUB_SECOND_FMT = DateTimeFormat.forPattern(BASE_FMT + ".SSS'Z'").withZoneUTC(); public static final DateTimeFormatter LOCAL_STANDARD_FMT = DateTimeFormat.forPattern(BASE_FMT + "Z").withZoneUTC(); public static final DateTimeFormatter LOCAL_SUB_SECOND_FMT = DateTimeFormat.forPattern(BASE_FMT + ".SSSZ").withZoneUTC(); /** * Contains various formats. All formats should be of international standards when comes to the ordering of the * days and month. */ private static final DateTimeFormatter DEFAULT_FORMATTER; /** * Contains alternative formats that will succeed after failures from the DEFAULT_FORMATTER. * i.e. 4/24/2014 will throw an exception on the default formatter because it will assume international date standards * However, the date will parse in the ALT_FORMATTER because it contains the US format of MM/dd/yyyy. */ private static final DateTimeFormatter ALT_FORMATTER; static { DateTimeParser[] parsers = new DateTimeParser[]{ DateTimeFormat.forPattern("EEE MMM dd HH:mm:ss Z yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss Z").getParser(), DateTimeFormat.forPattern("dd MMMM yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyyMMdd").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd-MM-yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy-MM-dd").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy/MM/dd").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd MMM yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd MMMM yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyyMMddHHmm").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyyMMdd HHmm").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd-MM-yyyy HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy/MM/dd HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd MMM yyyy HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd MMMM yyyy HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyyMMddHHmmss").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyyMMdd HHmmss").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd-MM-yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("yyyy/MM/dd HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd MMM yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("HH:mm:ss yyyy/MM/dd").withZoneUTC().getParser(), DateTimeFormat.forPattern("HH:mm:ss MM/dd/yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("HH:mm:ss yyyy-MM-dd").withZoneUTC().getParser(), DateTimeFormat.forPattern("HH:mm:ss MM-dd-yyyy").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd/MM/yyyy HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("dd/MM/yyyy").withZoneUTC().getParser(), UTC_STANDARD_FMT.getParser(), UTC_SUB_SECOND_FMT.getParser(), LOCAL_STANDARD_FMT.getParser(), LOCAL_SUB_SECOND_FMT.getParser() }; DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); builder.append(null, parsers); DEFAULT_FORMATTER = builder.toFormatter().withZoneUTC(); DateTimeParser[] altParsers = new DateTimeParser[] { DateTimeFormat.forPattern("MM-dd-yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss").withZoneUTC().getParser(), DateTimeFormat.forPattern("MM/dd/yyyy HH:mm").withZoneUTC().getParser(), DateTimeFormat.forPattern("MM/dd/yyyy").withZoneUTC().getParser(), }; builder = new DateTimeFormatterBuilder(); builder.append(null, altParsers); ALT_FORMATTER = builder.toFormatter().withZoneUTC(); } private RFC3339Utils() {} /** * parse String to DateTime * @param toParse DateTime as UTC String * @return DateTime */ public static DateTime parseUTC(String toParse) { if (MILLIS.matcher(toParse).matches()) { return new DateTime(Long.valueOf(toParse), DateTimeZone.UTC); } if (UTC_STANDARD.matcher(toParse).matches()) { return parseUTC(UTC_STANDARD_FMT, toParse); } Matcher utc = UTC_SUB_SECOND.matcher(toParse); if (utc.matches()) { return parseUTC(getSubSecondFormat(utc.group(1), "'Z'"), toParse); } if (LOCAL_STANDARD.matcher(toParse).matches()) { return parseUTC(LOCAL_STANDARD_FMT, toParse); } Matcher local = LOCAL_SUB_SECOND.matcher(toParse); if (local.matches()) { return parseUTC(getSubSecondFormat(local.group(1), "Z"), toParse); } throw new IllegalArgumentException(String.format("Failed to parse date %s. Ensure format is RFC3339 Compliant", toParse)); } private static DateTime parseUTC(DateTimeFormatter formatter, String toParse) { return formatter.parseDateTime(toParse); } /** * Parses arbitrarily formatted Strings representing dates or dates and times to a {@link org.joda.time.DateTime} * objects. It first attempts parse with international standards, assuming the dates are either dd MM yyyy or * yyyy MM dd. If that fails it will try American formats where the month precedes the days of the month. * @param dateString abitrarily formatted date or date and time string * @return {@link org.joda.time.DateTime} representation of the dateString */ public static DateTime parseToUTC(String dateString) { if (MILLIS.matcher(dateString).find()) { return new DateTime(Long.parseLong(dateString)); } try { return DEFAULT_FORMATTER.parseDateTime(dateString); } catch (Exception ex) { return ALT_FORMATTER.parseDateTime(dateString); } } /** * Formats an arbitrarily formatted into RFC3339 Specifications. * @param dateString date string to be formatted * @return RFC3339 compliant date string */ public static String format(String dateString) { return format(parseToUTC(dateString)); } public static String format(DateTime toFormat) { return UTC_SUB_SECOND_FMT.print(toFormat.getMillis()); } public static String format(DateTime toFormat, TimeZone tz) { return LOCAL_SUB_SECOND_FMT.withZone(DateTimeZone.forTimeZone(tz)).print(toFormat.getMillis()); } private static DateTimeFormatter getSubSecondFormat(String sub, String suffix) { DateTimeFormatter result; //Since RFC3339 allows for any number of sub-second notations, we need to flexibly support more or less than 3 //digits; however, if it is exactly 3, just use the standards. if (sub.length() == 3) { result = suffix.equals("Z") ? LOCAL_SUB_SECOND_FMT : UTC_SUB_SECOND_FMT; } else { StringBuilder pattern = new StringBuilder(); pattern.append(BASE_FMT); pattern.append("."); for (int i = 0; i < sub.length(); i++) { pattern.append("S"); } pattern.append(suffix); result = DateTimeFormat.forPattern(pattern.toString()).withZoneUTC(); } return result; } }