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 UTC time element, which represents a timestamp
042 * with a string representation in the format "YYMMDDhhmmssZ".  Although the
043 * general UTC time format considers the seconds element to be optional, the
044 * ASN.1 specification requires the element to be present.
045 * <BR><BR>
046 * Note that the UTC time format only allows two digits for the year, which is
047 * obviously prone to causing problems when deciding which century is implied
048 * by the timestamp.  The official specification does not indicate which
049 * behavior should be used, so this implementation will use the same logic as
050 * Java's {@code SimpleDateFormat} class, which infers the century using a
051 * sliding window that assumes that the year is somewhere between 80 years
052 * before and 20 years after the current time.  For example, if the current year
053 * is 2017, the following values would be inferred:
054 * <UL>
055 *   <LI>A year of "40" would be interpreted as 1940.</LI>
056 *   <LI>A year of "50" would be interpreted as 1950.</LI>
057 *   <LI>A year of "60" would be interpreted as 1960.</LI>
058 *   <LI>A year of "70" would be interpreted as 1970.</LI>
059 *   <LI>A year of "80" would be interpreted as 1980.</LI>
060 *   <LI>A year of "90" would be interpreted as 1990.</LI>
061 *   <LI>A year of "00" would be interpreted as 2000.</LI>
062 *   <LI>A year of "10" would be interpreted as 2010.</LI>
063 *   <LI>A year of "20" would be interpreted as 2020.</LI>
064 *   <LI>A year of "30" would be interpreted as 2030.</LI>
065 * </UL>
066 * <BR><BR>
067 * UTC time elements should generally only be used for historical purposes in
068 * encodings that require them.  For new cases in which a timestamp may be
069 * required, you should use some other format to represent the timestamp.  The
070 * {@link ASN1GeneralizedTime} element type does use a four-digit year (and also
071 * allows for the possibility of sub-second values), so it may be a good fit.
072 * You may also want to use a general-purpose string format like
073 * {@link ASN1OctetString} that is flexible enough to support whatever encoding
074 * you want.
075 */
076@NotMutable()
077@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
078public final class ASN1UTCTime
079       extends ASN1Element
080{
081  /**
082   * The thread-local date formatter used to encode and decode UTC time values.
083   */
084  private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTERS =
085       new ThreadLocal<>();
086
087
088
089  /**
090   * The serial version UID for this serializable class.
091   */
092  private static final long serialVersionUID = -3107099228691194285L;
093
094
095
096  // The timestamp represented by this UTC time value.
097  private final long time;
098
099  // The string representation of the UTC time value.
100  private final String stringRepresentation;
101
102
103
104  /**
105   * Creates a new UTC time element with the default BER type that represents
106   * the current time.
107   */
108  public ASN1UTCTime()
109  {
110    this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE);
111  }
112
113
114
115  /**
116   * Creates a new UTC time element with the specified BER type that represents
117   * the current time.
118   *
119   * @param  type  The BER type to use for this element.
120   */
121  public ASN1UTCTime(final byte type)
122  {
123    this(type, System.currentTimeMillis());
124  }
125
126
127
128  /**
129   * Creates a new UTC time element with the default BER type that represents
130   * the indicated time.
131   *
132   * @param  date  The date value that specifies the time to represent.  This
133   *               must not be {@code null}.  Note that the time that is
134   *               actually represented by the element will have its
135   *               milliseconds component set to zero.
136   */
137  public ASN1UTCTime(final Date date)
138  {
139    this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, date.getTime());
140  }
141
142
143
144  /**
145   * Creates a new UTC time element with the specified BER type that represents
146   * 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}.  Note that the time that is
151   *               actually represented by the element will have its
152   *               milliseconds component set to zero.
153   */
154  public ASN1UTCTime(final byte type, final Date date)
155  {
156    this(type, date.getTime());
157  }
158
159
160
161  /**
162   * Creates a new UTC time element with the default BER type that represents
163   * the indicated time.
164   *
165   * @param  time  The time to represent.  This must be expressed in
166   *               milliseconds since the epoch (the same format used by
167   *               {@code System.currentTimeMillis()} and
168   *               {@code Date.getTime()}).  Note that the time that is actually
169   *               represented by the element will have its milliseconds
170   *               component set to zero.
171   */
172  public ASN1UTCTime(final long time)
173  {
174    this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, time);
175  }
176
177
178
179  /**
180   * Creates a new UTC time element with the specified BER type that represents
181   * the indicated time.
182   *
183   * @param  type  The BER type to use for this element.
184   * @param  time  The time to represent.  This must be expressed in
185   *               milliseconds since the epoch (the same format used by
186   *               {@code System.currentTimeMillis()} and
187   *               {@code Date.getTime()}).  Note that the time that is actually
188   *               represented by the element will have its milliseconds
189   *               component set to zero.
190   */
191  public ASN1UTCTime(final byte type, final long time)
192  {
193    super(type, StaticUtils.getBytes(encodeTimestamp(time)));
194
195    final GregorianCalendar calendar =
196         new GregorianCalendar(StaticUtils.getUTCTimeZone());
197    calendar.setTimeInMillis(time);
198    calendar.set(GregorianCalendar.MILLISECOND, 0);
199
200    this.time = calendar.getTimeInMillis();
201    stringRepresentation = encodeTimestamp(time);
202  }
203
204
205
206  /**
207   * Creates a new UTC time element with the default BER type and a time decoded
208   * from the provided string representation.
209   *
210   * @param  timestamp  The string representation of the timestamp to represent.
211   *                    This must not be {@code null}.
212   *
213   * @throws  ASN1Exception  If the provided timestamp does not represent a
214   *                         valid ASN.1 UTC time string representation.
215   */
216  public ASN1UTCTime(final String timestamp)
217         throws ASN1Exception
218  {
219    this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, timestamp);
220  }
221
222
223
224  /**
225   * Creates a new UTC time element with the specified BER type and a time
226   * decoded from the provided string representation.
227   *
228   * @param  type       The BER type to use for this element.
229   * @param  timestamp  The string representation of the timestamp to represent.
230   *                    This must not be {@code null}.
231   *
232   * @throws  ASN1Exception  If the provided timestamp does not represent a
233   *                         valid ASN.1 UTC time string representation.
234   */
235  public ASN1UTCTime(final byte type, final String timestamp)
236         throws ASN1Exception
237  {
238    super(type, StaticUtils.getBytes(timestamp));
239
240    time = decodeTimestamp(timestamp);
241    stringRepresentation = timestamp;
242  }
243
244
245
246  /**
247   * Encodes the time represented by the provided date into the appropriate
248   * ASN.1 UTC time format.
249   *
250   * @param  date  The date value that specifies the time to represent.  This
251   *               must not be {@code null}.
252   *
253   * @return  The encoded timestamp.
254   */
255  public static String encodeTimestamp(final Date date)
256  {
257    return getDateFormatter().format(date);
258  }
259
260
261
262  /**
263   * Gets a date formatter instance, using a thread-local instance if one
264   * exists, or creating a new one if not.
265   *
266   * @return  A date formatter instance.
267   */
268  private static SimpleDateFormat getDateFormatter()
269  {
270    final SimpleDateFormat existingFormatter = DATE_FORMATTERS.get();
271    if (existingFormatter != null)
272    {
273      return existingFormatter;
274    }
275
276    final SimpleDateFormat newFormatter
277         = new SimpleDateFormat("yyMMddHHmmss'Z'");
278    newFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
279    newFormatter.setLenient(false);
280    DATE_FORMATTERS.set(newFormatter);
281    return newFormatter;
282  }
283
284
285
286  /**
287   * Encodes the specified time into the appropriate ASN.1 UTC time format.
288   *
289   * @param  time  The time to represent.  This must be expressed in
290   *               milliseconds since the epoch (the same format used by
291   *               {@code System.currentTimeMillis()} and
292   *               {@code Date.getTime()}).
293   *
294   * @return  The encoded timestamp.
295   */
296  public static String encodeTimestamp(final long time)
297  {
298    return encodeTimestamp(new Date(time));
299  }
300
301
302
303  /**
304   * Decodes the provided string as a timestamp in the UTC time format.
305   *
306   * @param  timestamp  The string representation of a UTC time to be parsed as
307   *                    a timestamp.  It must not be {@code null}.
308   *
309   * @return  The decoded time, expressed in milliseconds since the epoch (the
310   *          same format used by {@code System.currentTimeMillis()} and
311   *          {@code Date.getTime()}).
312   *
313   * @throws  ASN1Exception  If the provided timestamp cannot be parsed as a
314   *                         valid string representation of an ASN.1 UTC time
315   *                         value.
316   */
317  public static long decodeTimestamp(final String timestamp)
318         throws ASN1Exception
319  {
320    if (timestamp.length() != 13)
321    {
322      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_LENGTH.get());
323    }
324
325    if (! (timestamp.endsWith("Z") || timestamp.endsWith("z")))
326    {
327      throw new ASN1Exception(ERR_UTC_TIME_STRING_DOES_NOT_END_WITH_Z.get());
328    }
329
330    for (int i=0; i < (timestamp.length() - 1); i++)
331    {
332      final char c = timestamp.charAt(i);
333      if ((c < '0') || (c > '9'))
334      {
335        throw new ASN1Exception(ERR_UTC_TIME_STRING_CHAR_NOT_DIGIT.get(i + 1));
336      }
337    }
338
339    final int month = Integer.parseInt(timestamp.substring(2, 4));
340    if ((month < 1) || (month > 12))
341    {
342      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MONTH.get());
343    }
344
345    final int day = Integer.parseInt(timestamp.substring(4, 6));
346    if ((day < 1) || (day > 31))
347    {
348      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_DAY.get());
349    }
350
351    final int hour = Integer.parseInt(timestamp.substring(6, 8));
352    if (hour > 23)
353    {
354      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_HOUR.get());
355    }
356
357    final int minute = Integer.parseInt(timestamp.substring(8, 10));
358    if (minute > 59)
359    {
360      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MINUTE.get());
361    }
362
363    final int second = Integer.parseInt(timestamp.substring(10, 12));
364    if (second > 60)
365    {
366      // In the case of a leap second, there can be 61 seconds in a minute.
367      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_SECOND.get());
368    }
369
370    try
371    {
372      return getDateFormatter().parse(timestamp).getTime();
373    }
374    catch (final Exception e)
375    {
376      // Even though we've already done a lot of validation, this could still
377      // happen if the timestamp isn't valid as a whole because one of the
378      // components is out of a range implied by another component.  In the case
379      // of UTC time values, this should only happen when trying to use a day
380      // of the month that is not valid for the desired month (for example,
381      // trying to use a date of September 31, when September only has 30 days).
382      Debug.debugException(e);
383      throw new ASN1Exception(
384           ERR_UTC_TIME_STRING_CANNOT_PARSE.get(
385                StaticUtils.getExceptionMessage(e)),
386           e);
387    }
388  }
389
390
391
392  /**
393   * Retrieves the time represented by this UTC time element, expressed as the
394   * number of milliseconds since the epoch (the same format used by
395   * {@code System.currentTimeMillis()} and {@code Date.getTime()}).
396
397   * @return  The time represented by this UTC time element.
398   */
399  public long getTime()
400  {
401    return time;
402  }
403
404
405
406  /**
407   * Retrieves a {@code Date} object that is set to the time represented by this
408   * UTC time element.
409   *
410   * @return  A {@code Date} object that is set ot the time represented by this
411   *          UTC time element.
412   */
413  public Date getDate()
414  {
415    return new Date(time);
416  }
417
418
419
420  /**
421   * Retrieves the string representation of the UTC time value contained in this
422   * element.
423   *
424   * @return  The string representation of the UTC time value contained in this
425   *          element.
426   */
427  public String getStringRepresentation()
428  {
429    return stringRepresentation;
430  }
431
432
433
434  /**
435   * Decodes the contents of the provided byte array as a UTC time element.
436   *
437   * @param  elementBytes  The byte array to decode as an ASN.1 UTC time
438   *                       element.
439   *
440   * @return  The decoded ASN.1 UTC time element.
441   *
442   * @throws  ASN1Exception  If the provided array cannot be decoded as a UTC
443   *                         time element.
444   */
445  public static ASN1UTCTime decodeAsUTCTime(final byte[] elementBytes)
446         throws ASN1Exception
447  {
448    try
449    {
450      int valueStartPos = 2;
451      int length = (elementBytes[1] & 0x7F);
452      if (length != elementBytes[1])
453      {
454        final int numLengthBytes = length;
455
456        length = 0;
457        for (int i=0; i < numLengthBytes; i++)
458        {
459          length <<= 8;
460          length |= (elementBytes[valueStartPos++] & 0xFF);
461        }
462      }
463
464      if ((elementBytes.length - valueStartPos) != length)
465      {
466        throw new ASN1Exception(ERR_ELEMENT_LENGTH_MISMATCH.get(length,
467                                     (elementBytes.length - valueStartPos)));
468      }
469
470      final byte[] elementValue = new byte[length];
471      System.arraycopy(elementBytes, valueStartPos, elementValue, 0, length);
472
473      return new ASN1UTCTime(elementBytes[0],
474           StaticUtils.toUTF8String(elementValue));
475    }
476    catch (final ASN1Exception ae)
477    {
478      Debug.debugException(ae);
479      throw ae;
480    }
481    catch (final Exception e)
482    {
483      Debug.debugException(e);
484      throw new ASN1Exception(ERR_ELEMENT_DECODE_EXCEPTION.get(e), e);
485    }
486  }
487
488
489
490  /**
491   * Decodes the provided ASN.1 element as a UTC time element.
492   *
493   * @param  element  The ASN.1 element to be decoded.
494   *
495   * @return  The decoded ASN.1 UTC time element.
496   *
497   * @throws  ASN1Exception  If the provided element cannot be decoded as a UTC
498   *                         time element.
499   */
500  public static ASN1UTCTime decodeAsUTCTime(final ASN1Element element)
501         throws ASN1Exception
502  {
503    return new ASN1UTCTime(element.getType(),
504         StaticUtils.toUTF8String(element.getValue()));
505  }
506
507
508
509  /**
510   * {@inheritDoc}
511   */
512  @Override()
513  public void toString(final StringBuilder buffer)
514  {
515    buffer.append(stringRepresentation);
516  }
517}