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}