001/*
002 * Copyright 2017-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2017-2019 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}