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