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