All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jxmpp.util.XmppDateTime Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
/**
 *
 * Copyright © 2014-2021 Florian Schmaus
 *
 * 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.jxmpp.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class for date and time handling in XMPP.
 *
 * @see XEP-82: XMPP Date and Time Profiles
 */
public class XmppDateTime {

	private static final DateFormatType dateFormatter = DateFormatType.XEP_0082_DATE_PROFILE;
	private static final Pattern datePattern = Pattern.compile("^\\d+-\\d+-\\d+$");

	private static final DateFormatType timeFormatter = DateFormatType.XEP_0082_TIME_MILLIS_ZONE_PROFILE;
	private static final Pattern timePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$");
	private static final DateFormatType timeNoZoneFormatter = DateFormatType.XEP_0082_TIME_MILLIS_PROFILE;
	private static final Pattern timeNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+$");

	private static final DateFormatType timeNoMillisFormatter = DateFormatType.XEP_0082_TIME_ZONE_PROFILE;
	private static final Pattern timeNoMillisPattern = Pattern.compile("^(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$");
	private static final DateFormatType timeNoMillisNoZoneFormatter = DateFormatType.XEP_0082_TIME_PROFILE;
	private static final Pattern timeNoMillisNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+$");

	private static final DateFormatType dateTimeFormatter = DateFormatType.XEP_0082_DATETIME_MILLIS_PROFILE;
	private static final Pattern dateTimePattern = Pattern
			.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$");
	private static final DateFormatType dateTimeNoMillisFormatter = DateFormatType.XEP_0082_DATETIME_PROFILE;
	private static final Pattern dateTimeNoMillisPattern = Pattern
			.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$");

	private static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("UTC");

	private static DateFormat constructUtcDateFormat(String format) {
		DateFormat dateFormat = new SimpleDateFormat(format, Locale.ENGLISH);
		dateFormat.setTimeZone(TIME_ZONE_UTC);
		return dateFormat;
	}

	private static final ThreadLocal xep0091Formatter = new ThreadLocal() {
		@Override
		protected DateFormat initialValue() {
			DateFormat dateFormat = constructUtcDateFormat("yyyyMMdd'T'HH:mm:ss");
			return dateFormat;
		}
	};
	private static final ThreadLocal xep0091Date6DigitFormatter = new ThreadLocal() {
		@Override
		protected DateFormat initialValue() {
			DateFormat dateFormat = constructUtcDateFormat("yyyyMd'T'HH:mm:ss");
			return dateFormat;
		}
	};
	private static final ThreadLocal xep0091Date7Digit1MonthFormatter = new ThreadLocal() {
		@Override
		protected DateFormat initialValue() {
			DateFormat dateFormat = constructUtcDateFormat("yyyyMdd'T'HH:mm:ss");
			dateFormat.setLenient(false);
			return dateFormat;
		}
	};
	private static final ThreadLocal xep0091Date7Digit2MonthFormatter = new ThreadLocal() {
		@Override
		protected DateFormat initialValue() {
			DateFormat dateFormat = constructUtcDateFormat("yyyyMMd'T'HH:mm:ss");
			dateFormat.setLenient(false);
			return dateFormat;
		}
	};
	private static final Pattern xep0091Pattern = Pattern.compile("^\\d+T\\d+:\\d+:\\d+$");

	@SuppressWarnings("ImmutableEnumChecker")
	private enum DateFormatType {
		// @formatter:off
		XEP_0082_DATE_PROFILE("yyyy-MM-dd"),
		XEP_0082_DATETIME_PROFILE("yyyy-MM-dd'T'HH:mm:ssZ"),
		XEP_0082_DATETIME_MILLIS_PROFILE("yyyy-MM-dd'T'HH:mm:ss.SSSZ"),
		XEP_0082_TIME_PROFILE("hh:mm:ss"),
		XEP_0082_TIME_ZONE_PROFILE("hh:mm:ssZ"),
		XEP_0082_TIME_MILLIS_PROFILE("hh:mm:ss.SSS"),
		XEP_0082_TIME_MILLIS_ZONE_PROFILE("hh:mm:ss.SSSZ"),
		XEP_0091_DATETIME("yyyyMMdd'T'HH:mm:ss");
		// @formatter:on

		private final String FORMAT_STRING;
		private final ThreadLocal FORMATTER;
		private final boolean CONVERT_TIMEZONE;

		/**
		 * XEP-0082 allows the fractional second addendum to contain ANY number
		 * of digits. Implementations are therefore free to send as much digits
		 * after the dot as they want, therefore we need to truncate or fill up
		 * milliseconds. Certain platforms are only able to parse up to milliseconds,
		 * so truncate to 3 digits after the dot or fill zeros until 3 digits.
		 */
		private final boolean HANDLE_MILLIS;

		DateFormatType(String dateFormat) {
			FORMAT_STRING = dateFormat;
			FORMATTER = new ThreadLocal() {
				@Override
				protected DateFormat initialValue() {
					DateFormat dateFormat = constructUtcDateFormat(FORMAT_STRING);
					return dateFormat;
				}
			};
			CONVERT_TIMEZONE = dateFormat.charAt(dateFormat.length() - 1) == 'Z';
			HANDLE_MILLIS = dateFormat.contains("SSS");
		}

		private String format(Date date) {
			String res = FORMATTER.get().format(date);
			if (CONVERT_TIMEZONE) {
				res = convertRfc822TimezoneToXep82(res);
			}
			return res;
		}

		private Date parse(String dateString) throws ParseException {
			if (CONVERT_TIMEZONE) {
				dateString = convertXep82TimezoneToRfc822(dateString);
			}
			if (HANDLE_MILLIS) {
				dateString = handleMilliseconds(dateString);
			}
			return FORMATTER.get().parse(dateString);
		}
	}

	private static final List couplings = new ArrayList();

	static {
		couplings.add(new PatternCouplings(datePattern, dateFormatter));
		couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter));
		couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter));
		couplings.add(new PatternCouplings(timePattern, timeFormatter));
		couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter));
		couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter));
		couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter));
	}

	/**
	 * Parses the given date string in the XEP-0082 - XMPP Date and
	 * Time Profiles.
	 * 
	 * @param dateString
	 *            the date string to parse
	 * @return the parsed Date
	 * @throws ParseException
	 *             if the specified string cannot be parsed
	 */
	public static Date parseXEP0082Date(String dateString) throws ParseException {
		for (PatternCouplings coupling : couplings) {
			Matcher matcher = coupling.pattern.matcher(dateString);

			if (matcher.matches()) {
				return coupling.formatter.parse(dateString);
			}
		}
		/*
		 * We assume it is the XEP-0082 DateTime profile with no milliseconds at
		 * this point. If it isn't, is is just not parsable, then we attempt to
		 * parse it regardless and let it throw the ParseException.
		 */
		return dateTimeNoMillisFormatter.parse(dateString);
	}

	/**
	 * Parses the given date string in either of the three profiles of XEP-0082 - XMPP Date and
	 * Time Profiles or XEP-0091 - Legacy Delayed
	 * Delivery format.
	 * 

* This method uses internal date formatters and is thus threadsafe. * * @param dateString * the date string to parse * @return the parsed Date * @throws ParseException * if the specified string cannot be parsed */ public static Date parseDate(String dateString) throws ParseException { Matcher matcher = xep0091Pattern.matcher(dateString); /* * if date is in XEP-0091 format handle ambiguous dates missing the * leading zero in month and day */ if (matcher.matches()) { int length = dateString.split("T")[0].length(); if (length < 8) { Date date = handleDateWithMissingLeadingZeros(dateString, length); if (date != null) return date; } else { return xep0091Formatter.get().parse(dateString); } } // Assume XEP-82 date if Matcher does not match return parseXEP0082Date(dateString); } /** * Formats a Date into a XEP-0082 - XMPP Date and Time Profiles string. * * @param date * the time value to be formatted into a time string * @return the formatted time string in XEP-0082 format */ public static String formatXEP0082Date(Date date) { return dateTimeFormatter.format(date); } /** * Converts a XEP-0082 date String's time zone definition into a RFC822 time * zone definition. The major difference is that XEP-0082 uses a semicolon * between hours and minutes and RFC822 does not. * * @param dateString the date String. * @return the String with converted timezone */ public static String convertXep82TimezoneToRfc822(String dateString) { if (dateString.charAt(dateString.length() - 1) == 'Z') { return dateString.replace("Z", "+0000"); } else { // If the time zone wasn't specified with 'Z', then it's in // ISO8601 format (i.e. '(+|-)HH:mm') // RFC822 needs a similar format just without the colon (i.e. // '(+|-)HHmm)'), so remove it return dateString.replaceAll("([\\+\\-]\\d\\d):(\\d\\d)", "$1$2"); } } /** * Convert a RFC 822 Timezone to the Timezone format used in XEP-82. * * @param dateString the input date String. * @return the input String with the timezone converted to XEP-82. */ public static String convertRfc822TimezoneToXep82(String dateString) { int length = dateString.length(); String res = dateString.substring(0, length - 2); res += ':'; res += dateString.substring(length - 2, length); return res; } /** * Converts a time zone to the String format as specified in XEP-0082. * * @param timeZone the time zone to convert. * @return the String representation of the TimeZone */ public static String asString(TimeZone timeZone) { int rawOffset = timeZone.getRawOffset(); int hours = rawOffset / (1000 * 60 * 60); int minutes = Math.abs((rawOffset / (1000 * 60)) - (hours * 60)); return String.format("%+d:%02d", hours, minutes); } /** * Parses the given date string in different ways and returns the date that * lies in the past and/or is nearest to the current date-time. * * @param stampString * date in string representation * @param dateLength the length of the date prefix of stampString * @return the parsed date * @throws ParseException * The date string was of an unknown format */ private static Date handleDateWithMissingLeadingZeros(String stampString, int dateLength) throws ParseException { if (dateLength == 6) { return xep0091Date6DigitFormatter.get().parse(stampString); } Calendar now = Calendar.getInstance(); Calendar oneDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit1MonthFormatter.get()); Calendar twoDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit2MonthFormatter.get()); List dates = filterDatesBefore(now, oneDigitMonth, twoDigitMonth); if (!dates.isEmpty()) { return determineNearestDate(now, dates).getTime(); } return null; } private static Calendar parseXEP91Date(String stampString, DateFormat dateFormat) { try { dateFormat.parse(stampString); return dateFormat.getCalendar(); } catch (ParseException e) { return null; } } private static List filterDatesBefore(Calendar now, Calendar... dates) { List result = new ArrayList(); for (Calendar calendar : dates) { if (calendar != null && calendar.before(now)) { result.add(calendar); } } return result; } /** * A pattern with 3 capturing groups, the second one are at least 1 digits * after the 'dot'. The last one is the timezone definition, either 'Z', * '+1234' or '-1234'. */ private static final Pattern SECOND_FRACTION = Pattern.compile(".*\\.(\\d{1,})(Z|((\\+|-)\\d{4}))"); /** * Handle the milliseconds. This means either fill up with zeros or * truncate the date String so that the fractional second addendum only * contains 3 digits. Returns the given string unmodified if it doesn't * match {@link #SECOND_FRACTION}. * * @param dateString the date string * @return the date String where the fractional second addendum is a most 3 * digits */ private static String handleMilliseconds(String dateString) { Matcher matcher = SECOND_FRACTION.matcher(dateString); if (!matcher.matches()) { // The date string does not contain any milliseconds return dateString; } int fractionalSecondsDigitCount = matcher.group(1).length(); if (fractionalSecondsDigitCount == 3) { // The date string has exactly 3 fractional second digits return dateString; } // Gather information about the date string int posDecimal = dateString.indexOf("."); StringBuilder sb = new StringBuilder(dateString.length() - fractionalSecondsDigitCount + 3); if (fractionalSecondsDigitCount > 3) { // Append only 3 fractional digits after posDecimal sb.append(dateString.substring(0, posDecimal + 4)); } else { // The date string has less then 3 fractional second digits sb.append(dateString.substring(0, posDecimal + fractionalSecondsDigitCount + 1)); // Fill up the "missing" fractional second digits with zeros for (int i = fractionalSecondsDigitCount; i < 3; i++) { sb.append('0'); } } // Append the timezone definition sb.append(dateString.substring(posDecimal + fractionalSecondsDigitCount + 1)); return sb.toString(); } private static Calendar determineNearestDate(final Calendar now, List dates) { Collections.sort(dates, new Comparator() { @Override public int compare(Calendar o1, Calendar o2) { Long diff1 = now.getTimeInMillis() - o1.getTimeInMillis(); Long diff2 = now.getTimeInMillis() - o2.getTimeInMillis(); return diff1.compareTo(diff2); } }); return dates.get(0); } private static class PatternCouplings { final Pattern pattern; final DateFormatType formatter; PatternCouplings(Pattern datePattern, DateFormatType dateFormat) { pattern = datePattern; formatter = dateFormat; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy