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