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}