001/* 002 * Copyright 2017-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2017-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.asn1; 022 023 024 025import java.text.SimpleDateFormat; 026import java.util.Date; 027import java.util.GregorianCalendar; 028import java.util.TimeZone; 029 030import com.unboundid.util.Debug; 031import com.unboundid.util.NotMutable; 032import com.unboundid.util.ThreadSafety; 033import com.unboundid.util.ThreadSafetyLevel; 034import com.unboundid.util.StaticUtils; 035 036import static com.unboundid.asn1.ASN1Messages.*; 037 038 039 040/** 041 * This class provides an ASN.1 generalized time element, which represents a 042 * timestamp in the generalized time format. The value is encoded as a string, 043 * although the ASN.1 specification imposes a number of restrictions on that 044 * string representation, including: 045 * <UL> 046 * <LI> 047 * The generic generalized time specification allows you to specify the time 048 * zone either by ending the value with "Z" to indicate that the value is in 049 * the UTC time zone, or by ending it with a positive or negative offset 050 * (expressed in hours and minutes) from UTC time. The ASN.1 specification 051 * only allows the "Z" option. 052 * </LI> 053 * <LI> 054 * The generic generalized time specification only requires generalized time 055 * values to include the year, month, day, and hour components of the 056 * timestamp, while the minute, second, and sub-second components are 057 * optional. The ASN.1 specification requires that generalized time values 058 * always include the minute and second components. Sub-second components 059 * are permitted, but with the restriction noted below. 060 * </LI> 061 * <LI> 062 * The ASN.1 specification for generalized time values does not allow the 063 * sub-second component to include any trailing zeroes. If the sub-second 064 * component is all zeroes, then it will be omitted, along with the decimal 065 * point that would have separated the second and sub-second components. 066 * </LI> 067 * </UL> 068 * Note that this implementation only supports up to millisecond-level 069 * precision. It will never generate a value with a sub-second component that 070 * contains more than three digits, and any value decoded from a string 071 * representation that contains a sub-second component with more than three 072 * digits will return a timestamp rounded to the nearest millisecond from the 073 * {@link #getDate()} and {@link #getTime()} methods, although the original 074 * string representation will be retained and will be used in the encoded 075 * representation. 076 */ 077@NotMutable() 078@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 079public final class ASN1GeneralizedTime 080 extends ASN1Element 081{ 082 /** 083 * The thread-local date formatter used to encode generalized time values that 084 * do not include milliseconds. 085 */ 086 private static final ThreadLocal<SimpleDateFormat> 087 DATE_FORMATTERS_WITHOUT_MILLIS = new ThreadLocal<>(); 088 089 090 091 /** 092 * The serial version UID for this serializable class. 093 */ 094 private static final long serialVersionUID = -7215431927354583052L; 095 096 097 098 // The timestamp represented by this generalized time value. 099 private final long time; 100 101 // The string representation of the generalized time value. 102 private final String stringRepresentation; 103 104 105 106 /** 107 * Creates a new generalized time element with the default BER type that 108 * represents the current time. 109 */ 110 public ASN1GeneralizedTime() 111 { 112 this(ASN1Constants.UNIVERSAL_GENERALIZED_TIME_TYPE); 113 } 114 115 116 117 /** 118 * Creates a new generalized time element with the specified BER type that 119 * represents the current time. 120 * 121 * @param type The BER type to use for this element. 122 */ 123 public ASN1GeneralizedTime(final byte type) 124 { 125 this(type, System.currentTimeMillis()); 126 } 127 128 129 130 /** 131 * Creates a new generalized time element with the default BER type that 132 * represents the indicated time. 133 * 134 * @param date The date value that specifies the time to represent. This 135 * must not be {@code null}. 136 */ 137 public ASN1GeneralizedTime(final Date date) 138 { 139 this(ASN1Constants.UNIVERSAL_GENERALIZED_TIME_TYPE, date); 140 } 141 142 143 144 /** 145 * Creates a new generalized time element with the specified BER type that 146 * represents the indicated time. 147 * 148 * @param type The BER type to use for this element. 149 * @param date The date value that specifies the time to represent. This 150 * must not be {@code null}. 151 */ 152 public ASN1GeneralizedTime(final byte type, final Date date) 153 { 154 this(type, date.getTime()); 155 } 156 157 158 159 /** 160 * Creates a new generalized time element with the default BER type that 161 * represents the indicated time. 162 * 163 * @param time The time to represent. This must be expressed in 164 * milliseconds since the epoch (the same format used by 165 * {@code System.currentTimeMillis()} and 166 * {@code Date.getTime()}). 167 */ 168 public ASN1GeneralizedTime(final long time) 169 { 170 this(ASN1Constants.UNIVERSAL_GENERALIZED_TIME_TYPE, time); 171 } 172 173 174 175 /** 176 * Creates a new generalized time element with the specified BER type that 177 * represents the indicated time. 178 * 179 * @param type The BER type to use for this element. 180 * @param time The time to represent. This must be expressed in 181 * milliseconds since the epoch (the same format used by 182 * {@code System.currentTimeMillis()} and 183 * {@code Date.getTime()}). 184 */ 185 public ASN1GeneralizedTime(final byte type, final long time) 186 { 187 this(type, time, encodeTimestamp(time, true)); 188 } 189 190 191 192 /** 193 * Creates a new generalized time element with the default BER type and a 194 * time decoded from the provided string representation. 195 * 196 * @param timestamp The string representation of the timestamp to represent. 197 * This must not be {@code null}. 198 * 199 * @throws ASN1Exception If the provided timestamp does not represent a 200 * valid ASN.1 generalized time string representation. 201 */ 202 public ASN1GeneralizedTime(final String timestamp) 203 throws ASN1Exception 204 { 205 this(ASN1Constants.UNIVERSAL_GENERALIZED_TIME_TYPE, timestamp); 206 } 207 208 209 210 /** 211 * Creates a new generalized time element with the specified BER type and a 212 * time decoded from the provided string representation. 213 * 214 * @param type The BER type to use for this element. 215 * @param timestamp The string representation of the timestamp to represent. 216 * This must not be {@code null}. 217 * 218 * @throws ASN1Exception If the provided timestamp does not represent a 219 * valid ASN.1 generalized time string representation. 220 */ 221 public ASN1GeneralizedTime(final byte type, final String timestamp) 222 throws ASN1Exception 223 { 224 this(type, decodeTimestamp(timestamp), timestamp); 225 } 226 227 228 229 /** 230 * Creates a new generalized time element with the provided information. 231 * 232 * @param type The BER type to use for this element. 233 * @param time The time to represent. This must be 234 * expressed in milliseconds since the epoch 235 * (the same format used by 236 * {@code System.currentTimeMillis()} and 237 * {@code Date.getTime()}). 238 * @param stringRepresentation The string representation of the timestamp to 239 * represent. This must not be {@code null}. 240 */ 241 private ASN1GeneralizedTime(final byte type, final long time, 242 final String stringRepresentation) 243 { 244 super(type, StaticUtils.getBytes(stringRepresentation)); 245 246 this.time = time; 247 this.stringRepresentation = stringRepresentation; 248 } 249 250 251 252 /** 253 * Encodes the time represented by the provided date into the appropriate 254 * ASN.1 generalized time format. 255 * 256 * @param date The date value that specifies the time to 257 * represent. This must not be {@code null}. 258 * @param includeMilliseconds Indicate whether the timestamp should include 259 * a sub-second component representing a 260 * precision of up to milliseconds. Note that 261 * even if this is {@code true}, the sub-second 262 * component will only be included if it is not 263 * all zeroes. If this is {@code false}, then 264 * the resulting timestamp will only use a 265 * precision indicated in seconds, and the 266 * sub-second portion will be truncated rather 267 * than rounded to the nearest second (which is 268 * the behavior that {@code SimpleDateFormat} 269 * exhibits for formatting timestamps without a 270 * sub-second component). 271 * 272 * @return The encoded timestamp. 273 */ 274 public static String encodeTimestamp(final Date date, 275 final boolean includeMilliseconds) 276 { 277 if (includeMilliseconds) 278 { 279 final String timestamp = StaticUtils.encodeGeneralizedTime(date); 280 if (! timestamp.endsWith("0Z")) 281 { 282 return timestamp; 283 } 284 285 final StringBuilder buffer = new StringBuilder(timestamp); 286 287 while (true) 288 { 289 final char c = buffer.charAt(buffer.length() - 2); 290 291 if ((c == '0') || (c == '.')) 292 { 293 buffer.deleteCharAt(buffer.length() - 2); 294 } 295 296 if (c != '0') 297 { 298 break; 299 } 300 } 301 302 return buffer.toString(); 303 } 304 else 305 { 306 SimpleDateFormat dateFormat = DATE_FORMATTERS_WITHOUT_MILLIS.get(); 307 if (dateFormat == null) 308 { 309 dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); 310 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 311 DATE_FORMATTERS_WITHOUT_MILLIS.set(dateFormat); 312 } 313 314 return dateFormat.format(date); 315 } 316 } 317 318 319 320 /** 321 * Encodes the specified time into the appropriate ASN.1 generalized time 322 * format. 323 * 324 * @param time The time to represent. This must be expressed 325 * in milliseconds since the epoch (the same 326 * format used by 327 * {@code System.currentTimeMillis()} and 328 * {@code Date.getTime()}). 329 * @param includeMilliseconds Indicate whether the timestamp should include 330 * a sub-second component representing a 331 * precision of up to milliseconds. Note that 332 * even if this is {@code true}, the sub-second 333 * component will only be included if it is not 334 * all zeroes. 335 * 336 * @return The encoded timestamp. 337 */ 338 public static String encodeTimestamp(final long time, 339 final boolean includeMilliseconds) 340 { 341 return encodeTimestamp(new Date(time), includeMilliseconds); 342 } 343 344 345 346 /** 347 * Decodes the provided string as a timestamp in the generalized time format. 348 * 349 * @param timestamp The string representation of a generalized time to be 350 * parsed as a timestamp. It must not be {@code null}. 351 * 352 * @return The decoded time, expressed in milliseconds since the epoch (the 353 * same format used by {@code System.currentTimeMillis()} and 354 * {@code Date.getTime()}). 355 * 356 * @throws ASN1Exception If the provided timestamp cannot be parsed as a 357 * valid string representation of an ASN.1 generalized 358 * time value. 359 */ 360 public static long decodeTimestamp(final String timestamp) 361 throws ASN1Exception 362 { 363 if (timestamp.length() < 15) 364 { 365 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_TOO_SHORT.get()); 366 } 367 368 if (! (timestamp.endsWith("Z") || timestamp.endsWith("z"))) 369 { 370 throw new ASN1Exception( 371 ERR_GENERALIZED_TIME_STRING_DOES_NOT_END_WITH_Z.get()); 372 } 373 374 boolean hasSubSecond = false; 375 for (int i=0; i < (timestamp.length() - 1); i++) 376 { 377 final char c = timestamp.charAt(i); 378 if (i == 14) 379 { 380 if (c != '.') 381 { 382 throw new ASN1Exception( 383 ERR_GENERALIZED_TIME_STRING_CHAR_NOT_PERIOD.get(i + 1)); 384 } 385 else 386 { 387 hasSubSecond = true; 388 } 389 } 390 else 391 { 392 if ((c < '0') || (c > '9')) 393 { 394 throw new ASN1Exception( 395 ERR_GENERALIZED_TIME_STRING_CHAR_NOT_DIGIT.get(i + 1)); 396 } 397 } 398 } 399 400 final GregorianCalendar calendar = 401 new GregorianCalendar(StaticUtils.getUTCTimeZone()); 402 403 final int year = Integer.parseInt(timestamp.substring(0, 4)); 404 calendar.set(GregorianCalendar.YEAR, year); 405 406 final int month = Integer.parseInt(timestamp.substring(4, 6)); 407 if ((month < 1) || (month > 12)) 408 { 409 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_INVALID_MONTH.get()); 410 } 411 else 412 { 413 calendar.set(GregorianCalendar.MONTH, (month - 1)); 414 } 415 416 final int day = Integer.parseInt(timestamp.substring(6, 8)); 417 if ((day < 1) || (day > 31)) 418 { 419 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_INVALID_DAY.get()); 420 } 421 else 422 { 423 calendar.set(GregorianCalendar.DAY_OF_MONTH, day); 424 } 425 426 final int hour = Integer.parseInt(timestamp.substring(8, 10)); 427 if (hour > 23) 428 { 429 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_INVALID_HOUR.get()); 430 } 431 else 432 { 433 calendar.set(GregorianCalendar.HOUR_OF_DAY, hour); 434 } 435 436 final int minute = Integer.parseInt(timestamp.substring(10, 12)); 437 if (minute > 59) 438 { 439 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_INVALID_MINUTE.get()); 440 } 441 else 442 { 443 calendar.set(GregorianCalendar.MINUTE, minute); 444 } 445 446 final int second = Integer.parseInt(timestamp.substring(12, 14)); 447 if (second > 60) 448 { 449 // In the case of a leap second, there can be 61 seconds in a minute. 450 throw new ASN1Exception(ERR_GENERALIZED_TIME_STRING_INVALID_SECOND.get()); 451 } 452 else 453 { 454 calendar.set(GregorianCalendar.SECOND, second); 455 } 456 457 if (hasSubSecond) 458 { 459 final StringBuilder subSecondString = 460 new StringBuilder(timestamp.substring(15, timestamp.length() - 1)); 461 while (subSecondString.length() < 3) 462 { 463 subSecondString.append('0'); 464 } 465 466 final boolean addOne; 467 if (subSecondString.length() > 3) 468 { 469 final char charFour = subSecondString.charAt(3); 470 addOne = ((charFour >= '5') && (charFour <= '9')); 471 subSecondString.setLength(3); 472 } 473 else 474 { 475 addOne = false; 476 } 477 478 while (subSecondString.charAt(0) == '0') 479 { 480 subSecondString.deleteCharAt(0); 481 } 482 483 final int millisecond = Integer.parseInt(subSecondString.toString()); 484 if (addOne) 485 { 486 calendar.set(GregorianCalendar.MILLISECOND, (millisecond + 1)); 487 } 488 else 489 { 490 calendar.set(GregorianCalendar.MILLISECOND, millisecond); 491 } 492 } 493 else 494 { 495 calendar.set(GregorianCalendar.MILLISECOND, 0); 496 } 497 498 return calendar.getTimeInMillis(); 499 } 500 501 502 503 /** 504 * Retrieves the time represented by this generalized time element, expressed 505 * as the number of milliseconds since the epoch (the same format used by 506 * {@code System.currentTimeMillis()} and {@code Date.getTime()}). 507 508 * @return The time represented by this generalized time element. 509 */ 510 public long getTime() 511 { 512 return time; 513 } 514 515 516 517 /** 518 * Retrieves a {@code Date} object that is set to the time represented by this 519 * generalized time element. 520 * 521 * @return A {@code Date} object that is set ot the time represented by this 522 * generalized time element. 523 */ 524 public Date getDate() 525 { 526 return new Date(time); 527 } 528 529 530 531 /** 532 * Retrieves the string representation of the generalized time value contained 533 * in this element. 534 * 535 * @return The string representation of the generalized time value contained 536 * in this element. 537 */ 538 public String getStringRepresentation() 539 { 540 return stringRepresentation; 541 } 542 543 544 545 /** 546 * Decodes the contents of the provided byte array as a generalized time 547 * element. 548 * 549 * @param elementBytes The byte array to decode as an ASN.1 generalized time 550 * element. 551 * 552 * @return The decoded ASN.1 generalized time element. 553 * 554 * @throws ASN1Exception If the provided array cannot be decoded as a 555 * generalized time element. 556 */ 557 public static ASN1GeneralizedTime decodeAsGeneralizedTime( 558 final byte[] elementBytes) 559 throws ASN1Exception 560 { 561 try 562 { 563 int valueStartPos = 2; 564 int length = (elementBytes[1] & 0x7F); 565 if (length != elementBytes[1]) 566 { 567 final int numLengthBytes = length; 568 569 length = 0; 570 for (int i=0; i < numLengthBytes; i++) 571 { 572 length <<= 8; 573 length |= (elementBytes[valueStartPos++] & 0xFF); 574 } 575 } 576 577 if ((elementBytes.length - valueStartPos) != length) 578 { 579 throw new ASN1Exception(ERR_ELEMENT_LENGTH_MISMATCH.get(length, 580 (elementBytes.length - valueStartPos))); 581 } 582 583 final byte[] elementValue = new byte[length]; 584 System.arraycopy(elementBytes, valueStartPos, elementValue, 0, length); 585 586 return new ASN1GeneralizedTime(elementBytes[0], 587 StaticUtils.toUTF8String(elementValue)); 588 } 589 catch (final ASN1Exception ae) 590 { 591 Debug.debugException(ae); 592 throw ae; 593 } 594 catch (final Exception e) 595 { 596 Debug.debugException(e); 597 throw new ASN1Exception(ERR_ELEMENT_DECODE_EXCEPTION.get(e), e); 598 } 599 } 600 601 602 603 /** 604 * Decodes the provided ASN.1 element as a generalized time element. 605 * 606 * @param element The ASN.1 element to be decoded. 607 * 608 * @return The decoded ASN.1 generalized time element. 609 * 610 * @throws ASN1Exception If the provided element cannot be decoded as a 611 * generalized time element. 612 */ 613 public static ASN1GeneralizedTime decodeAsGeneralizedTime( 614 final ASN1Element element) 615 throws ASN1Exception 616 { 617 return new ASN1GeneralizedTime(element.getType(), 618 StaticUtils.toUTF8String(element.getValue())); 619 } 620 621 622 623 /** 624 * {@inheritDoc} 625 */ 626 @Override() 627 public void toString(final StringBuilder buffer) 628 { 629 buffer.append(stringRepresentation); 630 } 631}