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