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