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