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