001package ezvcard.util; 002 003import java.text.DateFormat; 004import java.text.FieldPosition; 005import java.text.SimpleDateFormat; 006import java.util.Calendar; 007import java.util.Date; 008import java.util.TimeZone; 009import java.util.regex.Matcher; 010import java.util.regex.Pattern; 011 012import ezvcard.Messages; 013 014/* 015 Copyright (c) 2012-2018, Michael Angstadt 016 All rights reserved. 017 018 Redistribution and use in source and binary forms, with or without 019 modification, are permitted provided that the following conditions are met: 020 021 1. Redistributions of source code must retain the above copyright notice, this 022 list of conditions and the following disclaimer. 023 2. Redistributions in binary form must reproduce the above copyright notice, 024 this list of conditions and the following disclaimer in the documentation 025 and/or other materials provided with the distribution. 026 027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 030 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 031 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 032 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 033 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 034 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 035 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 036 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 037 038 The views and conclusions contained in the software and documentation are those 039 of the authors and should not be interpreted as representing official policies, 040 either expressed or implied, of the FreeBSD Project. 041 */ 042 043/** 044 * Defines all of the date formats that are used in vCards, and also 045 * parses/formats vCard dates. These date formats are defined in the ISO8601 046 * specification. 047 * @author Michael Angstadt 048 */ 049public enum VCardDateFormat { 050 //@formatter:off 051 /** 052 * Example: 20120701 053 */ 054 DATE_BASIC( 055 "yyyyMMdd"), 056 057 /** 058 * Example: 2012-07-01 059 */ 060 DATE_EXTENDED( 061 "yyyy-MM-dd"), 062 063 /** 064 * Example: 20120701T142110-0500 065 */ 066 DATE_TIME_BASIC( 067 "yyyyMMdd'T'HHmmssZ"), 068 069 /** 070 * Example: 2012-07-01T14:21:10-05:00 071 */ 072 DATE_TIME_EXTENDED( 073 "yyyy-MM-dd'T'HH:mm:ssZ"){ 074 @SuppressWarnings("serial") 075 @Override 076 public DateFormat getDateFormat(TimeZone timezone) { 077 DateFormat df = new SimpleDateFormat(formatStr){ 078 @Override 079 public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition){ 080 StringBuffer sb = super.format(date, toAppendTo, fieldPosition); 081 082 //add a colon between the hour and minute offsets 083 sb.insert(sb.length()-2, ':'); 084 085 return sb; 086 } 087 }; 088 089 if (timezone != null){ 090 df.setTimeZone(timezone); 091 } 092 093 return df; 094 } 095 }, 096 097 /** 098 * Example: 20120701T192110Z 099 */ 100 UTC_DATE_TIME_BASIC( 101 "yyyyMMdd'T'HHmmss'Z'"){ 102 @Override 103 public DateFormat getDateFormat(TimeZone timezone) { 104 //always use the UTC timezone 105 TimeZone utc = TimeZone.getTimeZone("UTC"); 106 return super.getDateFormat(utc); 107 } 108 }, 109 110 /** 111 * Example: 2012-07-01T19:21:10Z 112 */ 113 UTC_DATE_TIME_EXTENDED( 114 "yyyy-MM-dd'T'HH:mm:ss'Z'"){ 115 @Override 116 public DateFormat getDateFormat(TimeZone timezone) { 117 //always use the UTC timezone 118 TimeZone utc = TimeZone.getTimeZone("UTC"); 119 return super.getDateFormat(utc); 120 } 121 }, 122 123 /** 124 * Example: 2012-07-01T14:21:10-0500 125 */ 126 HCARD_DATE_TIME( 127 "yyyy-MM-dd'T'HH:mm:ssZ") 128 129 ; 130 //@formatter:on 131 132 /** 133 * The {@link SimpleDateFormat} format string used for parsing dates. 134 */ 135 protected final String formatStr; 136 137 /** 138 * @param formatStr the {@link SimpleDateFormat} format string used for 139 * formatting dates. 140 */ 141 private VCardDateFormat(String formatStr) { 142 this.formatStr = formatStr; 143 } 144 145 /** 146 * Builds a {@link DateFormat} object for parsing and formating dates in 147 * this format. 148 * @return the {@link DateFormat} object 149 */ 150 public DateFormat getDateFormat() { 151 return getDateFormat(null); 152 } 153 154 /** 155 * Builds a {@link DateFormat} object for parsing and formating dates in 156 * this format. 157 * @param timezone the timezone the date is in or null for the default 158 * timezone 159 * @return the {@link DateFormat} object 160 */ 161 public DateFormat getDateFormat(TimeZone timezone) { 162 DateFormat df = new SimpleDateFormat(formatStr); 163 if (timezone != null) { 164 df.setTimeZone(timezone); 165 } 166 return df; 167 } 168 169 /** 170 * Formats a date in this vCard date format. 171 * @param date the date to format 172 * @return the date string 173 */ 174 public String format(Date date) { 175 return format(date, null); 176 } 177 178 /** 179 * Formats a date in this vCard date format. 180 * @param date the date to format 181 * @param timezone the timezone to format the date in or null for the 182 * default timezone 183 * @return the date string 184 */ 185 public String format(Date date, TimeZone timezone) { 186 DateFormat df = getDateFormat(timezone); 187 return df.format(date); 188 } 189 190 /** 191 * Parses a date string. 192 * @param dateStr the date string to parse (e.g. "20130609T181023Z") 193 * @return the parsed date 194 * @throws IllegalArgumentException if the date string isn't in one of the 195 * accepted ISO8601 formats 196 */ 197 public static Date parse(String dateStr) { 198 TimestampPattern p = new TimestampPattern(dateStr); 199 if (!p.matches()) { 200 throw Messages.INSTANCE.getIllegalArgumentException(41, dateStr); 201 } 202 203 TimeZone timezone = p.hasOffset() ? TimeZone.getTimeZone("UTC") : TimeZone.getDefault(); 204 Calendar c = Calendar.getInstance(timezone); 205 c.clear(); 206 207 c.set(Calendar.YEAR, p.year()); 208 c.set(Calendar.MONTH, p.month() - 1); 209 c.set(Calendar.DATE, p.date()); 210 211 if (p.hasTime()) { 212 c.set(Calendar.HOUR_OF_DAY, p.hour()); 213 c.set(Calendar.MINUTE, p.minute()); 214 c.set(Calendar.SECOND, p.second()); 215 c.set(Calendar.MILLISECOND, p.millisecond()); 216 217 if (p.hasOffset()) { 218 c.set(Calendar.ZONE_OFFSET, p.offsetMillis()); 219 } 220 } 221 222 return c.getTime(); 223 } 224 225 /** 226 * Wrapper for a complex regular expression that parses multiple date 227 * formats. 228 */ 229 private static class TimestampPattern { 230 //@formatter:off 231 private static final Pattern regex = Pattern.compile( 232 "^(\\d{4})-?(\\d{2})-?(\\d{2})" + 233 "(" + 234 "T(\\d{2}):?(\\d{2}):?(\\d{2})(\\.\\d+)?" + 235 "(" + 236 "Z|([-+])((\\d{2})|((\\d{2}):?(\\d{2})))" + 237 ")?" + 238 ")?$" 239 ); 240 //@formatter:on 241 242 private final Matcher m; 243 private final boolean matches; 244 245 public TimestampPattern(String str) { 246 m = regex.matcher(str); 247 matches = m.find(); 248 } 249 250 public boolean matches() { 251 return matches; 252 } 253 254 public int year() { 255 return parseInt(1); 256 } 257 258 public int month() { 259 return parseInt(2); 260 } 261 262 public int date() { 263 return parseInt(3); 264 } 265 266 public boolean hasTime() { 267 return m.group(5) != null; 268 } 269 270 public int hour() { 271 return parseInt(5); 272 } 273 274 public int minute() { 275 return parseInt(6); 276 } 277 278 public int second() { 279 return parseInt(7); 280 } 281 282 public int millisecond() { 283 if (m.group(8) == null) { 284 return 0; 285 } 286 287 double ms = Double.parseDouble(m.group(8)) * 1000; 288 return (int) Math.round(ms); 289 } 290 291 public boolean hasOffset() { 292 return m.group(9) != null; 293 } 294 295 public int offsetMillis() { 296 if (m.group(9).equals("Z")) { 297 return 0; 298 } 299 300 int positive = m.group(10).equals("+") ? 1 : -1; 301 302 int offsetHour, offsetMinute; 303 if (m.group(12) != null) { 304 offsetHour = parseInt(12); 305 offsetMinute = 0; 306 } else { 307 offsetHour = parseInt(14); 308 offsetMinute = parseInt(15); 309 } 310 311 return (offsetHour * 60 * 60 * 1000 + offsetMinute * 60 * 1000) * positive; 312 } 313 314 private int parseInt(int group) { 315 return Integer.parseInt(m.group(group)); 316 } 317 } 318 319 /** 320 * Determines whether a date string has a time component. 321 * @param dateStr the date string (e.g. "20130601T120000") 322 * @return true if it has a time component, false if not 323 */ 324 public static boolean dateHasTime(String dateStr) { 325 return dateStr.contains("T"); 326 } 327 328 /** 329 * Determines whether a date string is in UTC time or has a timezone offset. 330 * @param dateStr the date string (e.g. "20130601T120000Z", 331 * "20130601T120000-0400") 332 * @return true if it has a timezone, false if not 333 */ 334 public static boolean dateHasTimezone(String dateStr) { 335 return dateStr.endsWith("Z") || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d"); 336 } 337 338 /** 339 * Gets the {@link TimeZone} object that corresponds to the given ID. 340 * @param timezoneId the timezone ID (e.g. "America/New_York") 341 * @return the timezone object or null if not found 342 */ 343 public static TimeZone parseTimeZoneId(String timezoneId) { 344 TimeZone timezone = TimeZone.getTimeZone(timezoneId); 345 return "GMT".equals(timezone.getID()) ? null : timezone; 346 } 347}