001/** 002 * 003 * Copyright the original author or authors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.util; 018 019import java.text.DateFormat; 020import java.text.ParseException; 021import java.text.SimpleDateFormat; 022import java.util.ArrayList; 023import java.util.Calendar; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.Date; 027import java.util.List; 028import java.util.TimeZone; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031 032public class XmppDateTime { 033 034 private static final DateFormatType dateFormatter = DateFormatType.XEP_0082_DATE_PROFILE; 035 private static final Pattern datePattern = Pattern.compile("^\\d+-\\d+-\\d+$"); 036 037 private static final DateFormatType timeFormatter = DateFormatType.XEP_0082_TIME_MILLIS_ZONE_PROFILE; 038 private static final Pattern timePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$"); 039 private static final DateFormatType timeNoZoneFormatter = DateFormatType.XEP_0082_TIME_MILLIS_PROFILE; 040 private static final Pattern timeNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+$"); 041 042 private static final DateFormatType timeNoMillisFormatter = DateFormatType.XEP_0082_TIME_ZONE_PROFILE; 043 private static final Pattern timeNoMillisPattern = Pattern.compile("^(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$"); 044 private static final DateFormatType timeNoMillisNoZoneFormatter = DateFormatType.XEP_0082_TIME_PROFILE; 045 private static final Pattern timeNoMillisNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+$"); 046 047 private static final DateFormatType dateTimeFormatter = DateFormatType.XEP_0082_DATETIME_MILLIS_PROFILE; 048 private static final Pattern dateTimePattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))?$"); 049 private static final DateFormatType dateTimeNoMillisFormatter = DateFormatType.XEP_0082_DATETIME_PROFILE; 050 private static final Pattern dateTimeNoMillisPattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))?$"); 051 052 private static final DateFormat xep0091Formatter = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); 053 private static final DateFormat xep0091Date6DigitFormatter = new SimpleDateFormat( 054 "yyyyMd'T'HH:mm:ss"); 055 private static final DateFormat xep0091Date7Digit1MonthFormatter = new SimpleDateFormat( 056 "yyyyMdd'T'HH:mm:ss"); 057 private static final DateFormat xep0091Date7Digit2MonthFormatter = new SimpleDateFormat( 058 "yyyyMMd'T'HH:mm:ss"); 059 private static final Pattern xep0091Pattern = Pattern.compile("^\\d+T\\d+:\\d+:\\d+$"); 060 061 public static enum DateFormatType { 062 // @formatter:off 063 XEP_0082_DATE_PROFILE("yyyy-MM-dd"), 064 XEP_0082_DATETIME_PROFILE("yyyy-MM-dd'T'HH:mm:ssZ"), 065 XEP_0082_DATETIME_MILLIS_PROFILE("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), 066 XEP_0082_TIME_PROFILE("hh:mm:ss"), 067 XEP_0082_TIME_ZONE_PROFILE("hh:mm:ssZ"), 068 XEP_0082_TIME_MILLIS_PROFILE("hh:mm:ss.SSS"), 069 XEP_0082_TIME_MILLIS_ZONE_PROFILE("hh:mm:ss.SSSZ"), 070 XEP_0091_DATETIME("yyyyMMdd'T'HH:mm:ss"); 071 // @formatter:on 072 073 private final String FORMAT_STRING; 074 private final DateFormat FORMATTER; 075 private final boolean CONVERT_TIMEZONE; 076 077 private DateFormatType(String dateFormat) { 078 FORMAT_STRING = dateFormat; 079 FORMATTER = new SimpleDateFormat(FORMAT_STRING); 080 FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); 081 CONVERT_TIMEZONE = dateFormat.charAt(dateFormat.length() - 1) == 'Z'; 082 } 083 084 public String format(Date date) { 085 String res; 086 synchronized(FORMATTER) { 087 res = FORMATTER.format(date); 088 } 089 if (CONVERT_TIMEZONE) { 090 res = convertRfc822TimezoneToXep82(res); 091 } 092 return res; 093 } 094 095 public Date parse(String dateString) throws ParseException { 096 if (CONVERT_TIMEZONE) { 097 dateString = convertXep82TimezoneToRfc822(dateString); 098 } 099 synchronized(FORMATTER) { 100 return FORMATTER.parse(dateString); 101 } 102 } 103 } 104 105 private static final List<PatternCouplings> couplings = new ArrayList<PatternCouplings>(); 106 107 static { 108 TimeZone utc = TimeZone.getTimeZone("UTC"); 109 110 xep0091Formatter.setTimeZone(utc); 111 xep0091Date6DigitFormatter.setTimeZone(utc); 112 xep0091Date7Digit1MonthFormatter.setTimeZone(utc); 113 xep0091Date7Digit1MonthFormatter.setLenient(false); 114 xep0091Date7Digit2MonthFormatter.setTimeZone(utc); 115 xep0091Date7Digit2MonthFormatter.setLenient(false); 116 117 couplings.add(new PatternCouplings(datePattern, dateFormatter)); 118 couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter)); 119 couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter)); 120 couplings.add(new PatternCouplings(timePattern, timeFormatter)); 121 couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter)); 122 couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter)); 123 couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter)); 124 } 125 126 /** 127 * Parses the given date string in the <a 128 * href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a>. 129 * 130 * @param dateString the date string to parse 131 * @return the parsed Date 132 * @throws ParseException if the specified string cannot be parsed 133 * @deprecated Use {@link #parseDate(String)} instead. 134 */ 135 public static Date parseXEP0082Date(String dateString) throws ParseException { 136 return parseDate(dateString); 137 } 138 139 /** 140 * Parses the given date string in either of the three profiles of <a 141 * href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a> or 142 * <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091 - Legacy Delayed Delivery</a> 143 * format. 144 * <p> 145 * This method uses internal date formatters and is thus threadsafe. 146 * 147 * @param dateString the date string to parse 148 * @return the parsed Date 149 * @throws ParseException if the specified string cannot be parsed 150 */ 151 public static Date parseDate(String dateString) throws ParseException { 152 Matcher matcher = xep0091Pattern.matcher(dateString); 153 154 /* 155 * if date is in XEP-0091 format handle ambiguous dates missing the leading zero in month 156 * and day 157 */ 158 if (matcher.matches()) { 159 int length = dateString.split("T")[0].length(); 160 161 if (length < 8) { 162 Date date = handleDateWithMissingLeadingZeros(dateString, length); 163 164 if (date != null) 165 return date; 166 } 167 else { 168 synchronized (xep0091Formatter) { 169 return xep0091Formatter.parse(dateString); 170 } 171 } 172 } 173 else { 174 for (PatternCouplings coupling : couplings) { 175 matcher = coupling.pattern.matcher(dateString); 176 177 if (matcher.matches()) { 178 return coupling.formatter.parse(dateString); 179 } 180 } 181 } 182 183 /* 184 * We assume it is the XEP-0082 DateTime profile with no milliseconds at this point. If it 185 * isn't, is is just not parseable, then we attempt to parse it regardless and let it throw 186 * the ParseException. 187 */ 188 synchronized (dateTimeNoMillisFormatter) { 189 return dateTimeNoMillisFormatter.parse(dateString); 190 } 191 } 192 193 /** 194 * Formats a Date into a XEP-0082 - XMPP Date and Time Profiles string. 195 * 196 * @param date the time value to be formatted into a time string 197 * @return the formatted time string in XEP-0082 format 198 */ 199 public static String formatXEP0082Date(Date date) { 200 synchronized (dateTimeFormatter) { 201 return dateTimeFormatter.format(date); 202 } 203 } 204 205 /** 206 * Converts a XEP-0082 date String's time zone definition into a RFC822 time zone definition. 207 * The major difference is that XEP-0082 uses a smicolon between hours and minutes and RFC822 208 * does not. 209 * 210 * @param dateString 211 * @return the String with converted timezone 212 */ 213 public static String convertXep82TimezoneToRfc822(String dateString) { 214 if (dateString.charAt(dateString.length() - 1) == 'Z') { 215 return dateString.replace("Z", "+0000"); 216 } 217 else { 218 // If the time zone wasn't specified with 'Z', then it's in 219 // ISO8601 format (i.e. '(+|-)HH:mm') 220 // RFC822 needs a similar format just without the colon (i.e. 221 // '(+|-)HHmm)'), so remove it 222 return dateString.replaceAll("([\\+\\-]\\d\\d):(\\d\\d)", "$1$2"); 223 } 224 } 225 226 public static String convertRfc822TimezoneToXep82(String dateString) { 227 int length = dateString.length(); 228 String res = dateString.substring(0, length -2); 229 res += ':'; 230 res += dateString.substring(length - 2, length); 231 return res; 232 } 233 234 /** 235 * Converts a time zone to the String format as specified in XEP-0082 236 * 237 * @param timeZone 238 * @return the String representation of the TimeZone 239 */ 240 public static String asString(TimeZone timeZone) { 241 int rawOffset = timeZone.getRawOffset(); 242 int hours = rawOffset / (1000*60*60); 243 int minutes = Math.abs((rawOffset / (1000*60)) - (hours * 60)); 244 return String.format("%+d:%02d", hours, minutes); 245 } 246 247 /** 248 * Parses the given date string in different ways and returns the date that lies in the past 249 * and/or is nearest to the current date-time. 250 * 251 * @param stampString date in string representation 252 * @param dateLength 253 * @param noFuture 254 * @return the parsed date 255 * @throws ParseException The date string was of an unknown format 256 */ 257 private static Date handleDateWithMissingLeadingZeros(String stampString, int dateLength) 258 throws ParseException { 259 if (dateLength == 6) { 260 synchronized (xep0091Date6DigitFormatter) { 261 return xep0091Date6DigitFormatter.parse(stampString); 262 } 263 } 264 Calendar now = Calendar.getInstance(); 265 266 Calendar oneDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit1MonthFormatter); 267 Calendar twoDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit2MonthFormatter); 268 269 List<Calendar> dates = filterDatesBefore(now, oneDigitMonth, twoDigitMonth); 270 271 if (!dates.isEmpty()) { 272 return determineNearestDate(now, dates).getTime(); 273 } 274 return null; 275 } 276 277 private static Calendar parseXEP91Date(String stampString, DateFormat dateFormat) { 278 try { 279 synchronized (dateFormat) { 280 dateFormat.parse(stampString); 281 return dateFormat.getCalendar(); 282 } 283 } 284 catch (ParseException e) { 285 return null; 286 } 287 } 288 289 private static List<Calendar> filterDatesBefore(Calendar now, Calendar... dates) { 290 List<Calendar> result = new ArrayList<Calendar>(); 291 292 for (Calendar calendar : dates) { 293 if (calendar != null && calendar.before(now)) { 294 result.add(calendar); 295 } 296 } 297 298 return result; 299 } 300 301 private static Calendar determineNearestDate(final Calendar now, List<Calendar> dates) { 302 303 Collections.sort(dates, new Comparator<Calendar>() { 304 305 public int compare(Calendar o1, Calendar o2) { 306 Long diff1 = new Long(now.getTimeInMillis() - o1.getTimeInMillis()); 307 Long diff2 = new Long(now.getTimeInMillis() - o2.getTimeInMillis()); 308 return diff1.compareTo(diff2); 309 } 310 311 }); 312 313 return dates.get(0); 314 } 315 316 private static class PatternCouplings { 317 final Pattern pattern; 318 final DateFormatType formatter; 319 320 public PatternCouplings(Pattern datePattern, DateFormatType dateFormat) { 321 pattern = datePattern; 322 formatter = dateFormat; 323 } 324 } 325}