/* * sulky-modules - several general-purpose modules. * Copyright (C) 2007-2015 Joern Huxhorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright 2007-2015 Joern Huxhorn * * 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 de.huxhorn.sulky.stax; import java.text.ParseException; import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DateTimeFormatter { private static final String TIMEZONE_DATE_FORMAT_PATTERN = ".*([+-]\\d{2})(\\d{2})$"; private static final int TIMEZONE_DATE_FORMAT_LENGTH = 5; private static final java.time.format.DateTimeFormatter ISO_DATE_TIME_PARSER = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .optionalStart() .appendFraction(ChronoField.MILLI_OF_SECOND, 3, 3, true) .optionalEnd() .appendOffset("+HH:MM", "Z") .toFormatter() .withZone(ZoneOffset.UTC); private static final java.time.format.DateTimeFormatter ISO_DATE_TIME_FORMATTER_WITH_MILLIS = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .appendFraction(ChronoField.MILLI_OF_SECOND, 3, 3, true) .appendOffset("+HH:MM", "+00:00") .toFormatter() .withZone(ZoneOffset.UTC); private static final java.time.format.DateTimeFormatter ISO_DATE_TIME_FORMATTER_WITHOUT_MILLIS = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(java.time.format.DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendLiteral(':') .appendValue(ChronoField.SECOND_OF_MINUTE, 2) .appendOffset("+HH:MM", "+00:00") .toFormatter() .withZone(ZoneOffset.UTC); private Pattern javaTimezonePattern; public DateTimeFormatter() { javaTimezonePattern = Pattern.compile(TIMEZONE_DATE_FORMAT_PATTERN); } /** * This method parses a given string containing a dateTime in ISO8601 notation into a date. * * It can handle an optional millisecond fraction as well as timezone with either explicit '+/-HH:MM' or 'Z' UTC designator. * * @param dateTime a string containing a dateTime in ISO8601 notation. * @return the parsed date * @throws ParseException If the dateTime string is invalid. */ public Date parse(String dateTime) throws ParseException { Matcher matcher = javaTimezonePattern.matcher(dateTime); if(matcher.matches()) { String hh = matcher.group(1); String mm = matcher.group(2); dateTime = dateTime.substring(0, dateTime.length() - TIMEZONE_DATE_FORMAT_LENGTH) + hh + ":" + mm; } TemporalAccessor temporal = ISO_DATE_TIME_PARSER.parse(dateTime); long seconds = temporal.getLong(ChronoField.INSTANT_SECONDS) + temporal.getLong(ChronoField.OFFSET_SECONDS); long millis = seconds * 1000 + temporal.getLong(ChronoField.MILLI_OF_SECOND); return new Date(millis); } /** * Returns a simplified ISO8601 datetime string in UTC. * * It will always contain a three-number millisecond field regardless if it is "needed" * (i.e. MILLI_OF_SECOND != 0) or not. The timezone of the date is always UTC but isn't using * the UTC designator 'Z'. Instead, it's using an explicit '+00:00'. * That way a date formatted by this method will always have the same number of characters while creating output * that less intelligent date-parsing frameworks (incapable of the 'Z' notation) are still able to process. * * @param date the date to be formatted. * @return a simplified ISO8601 datetime string in UTC. */ public String format(Date date) { return this.format(date, true); } /** * Returns a simplified ISO8601 datetime string in UTC. * * It will always contain a three-number millisecond field regardless if it is "needed" * (i.e. MILLI_OF_SECOND != 0) or not if withMillis is true. The timezone of the date is always UTC but isn't using * the UTC designator 'Z'. Instead, it's using an explicit '+00:00'. * That way a date formatted by this method will always have the same number of characters while creating output * that less intelligent date-parsing frameworks (incapable of the 'Z' notation) are still able to process. * * @param date the date to be formatted. * @param withMillis whether or not milliseconds should be printed. * @return a simplified ISO8601 datetime string in UTC. */ public String format(Date date, boolean withMillis) { Instant instant = Instant.ofEpochMilli(date.getTime()); ZonedDateTime zoned = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC); if(withMillis) { return ISO_DATE_TIME_FORMATTER_WITH_MILLIS.format(zoned); } return ISO_DATE_TIME_FORMATTER_WITHOUT_MILLIS.format(zoned); } }