001/*
002 * Copyright 2017-2025 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2017-2025 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-2025 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;
044
045import com.unboundid.util.Debug;
046import com.unboundid.util.NotMutable;
047import com.unboundid.util.NotNull;
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  @NotNull 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  @NotNull 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(@NotNull 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, @NotNull 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(@NotNull 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, @NotNull 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  @NotNull()
272  public static String encodeTimestamp(@NotNull final Date date)
273  {
274    return getDateFormatter().format(date);
275  }
276
277
278
279  /**
280   * Gets a date formatter instance, using a thread-local instance if one
281   * exists, or creating a new one if not.
282   *
283   * @return  A date formatter instance.
284   */
285  @NotNull()
286  private static SimpleDateFormat getDateFormatter()
287  {
288    final SimpleDateFormat existingFormatter = DATE_FORMATTERS.get();
289    if (existingFormatter != null)
290    {
291      return existingFormatter;
292    }
293
294    final SimpleDateFormat newFormatter
295         = new SimpleDateFormat("yyMMddHHmmss'Z'");
296    newFormatter.setTimeZone(StaticUtils.getUTCTimeZone());
297    newFormatter.setLenient(false);
298    DATE_FORMATTERS.set(newFormatter);
299    return newFormatter;
300  }
301
302
303
304  /**
305   * Encodes the specified time into the appropriate ASN.1 UTC time format.
306   *
307   * @param  time  The time to represent.  This must be expressed in
308   *               milliseconds since the epoch (the same format used by
309   *               {@code System.currentTimeMillis()} and
310   *               {@code Date.getTime()}).
311   *
312   * @return  The encoded timestamp.
313   */
314  @NotNull()
315  public static String encodeTimestamp(final long time)
316  {
317    return encodeTimestamp(new Date(time));
318  }
319
320
321
322  /**
323   * Decodes the provided string as a timestamp in the UTC time format.
324   *
325   * @param  timestamp  The string representation of a UTC time to be parsed as
326   *                    a timestamp.  It must not be {@code null}.
327   *
328   * @return  The decoded time, expressed in milliseconds since the epoch (the
329   *          same format used by {@code System.currentTimeMillis()} and
330   *          {@code Date.getTime()}).
331   *
332   * @throws  ASN1Exception  If the provided timestamp cannot be parsed as a
333   *                         valid string representation of an ASN.1 UTC time
334   *                         value.
335   */
336  public static long decodeTimestamp(@NotNull final String timestamp)
337         throws ASN1Exception
338  {
339    if (timestamp.length() != 13)
340    {
341      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_LENGTH.get());
342    }
343
344    if (! (timestamp.endsWith("Z") || timestamp.endsWith("z")))
345    {
346      throw new ASN1Exception(ERR_UTC_TIME_STRING_DOES_NOT_END_WITH_Z.get());
347    }
348
349    for (int i=0; i < (timestamp.length() - 1); i++)
350    {
351      final char c = timestamp.charAt(i);
352      if ((c < '0') || (c > '9'))
353      {
354        throw new ASN1Exception(ERR_UTC_TIME_STRING_CHAR_NOT_DIGIT.get(i + 1));
355      }
356    }
357
358    final int month = Integer.parseInt(timestamp.substring(2, 4));
359    if ((month < 1) || (month > 12))
360    {
361      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MONTH.get());
362    }
363
364    final int day = Integer.parseInt(timestamp.substring(4, 6));
365    if ((day < 1) || (day > 31))
366    {
367      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_DAY.get());
368    }
369
370    final int hour = Integer.parseInt(timestamp.substring(6, 8));
371    if (hour > 23)
372    {
373      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_HOUR.get());
374    }
375
376    final int minute = Integer.parseInt(timestamp.substring(8, 10));
377    if (minute > 59)
378    {
379      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MINUTE.get());
380    }
381
382    final int second = Integer.parseInt(timestamp.substring(10, 12));
383    if (second > 60)
384    {
385      // In the case of a leap second, there can be 61 seconds in a minute.
386      throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_SECOND.get());
387    }
388
389    try
390    {
391      return getDateFormatter().parse(timestamp).getTime();
392    }
393    catch (final Exception e)
394    {
395      // Even though we've already done a lot of validation, this could still
396      // happen if the timestamp isn't valid as a whole because one of the
397      // components is out of a range implied by another component.  In the case
398      // of UTC time values, this should only happen when trying to use a day
399      // of the month that is not valid for the desired month (for example,
400      // trying to use a date of September 31, when September only has 30 days).
401      Debug.debugException(e);
402      throw new ASN1Exception(
403           ERR_UTC_TIME_STRING_CANNOT_PARSE.get(
404                StaticUtils.getExceptionMessage(e)),
405           e);
406    }
407  }
408
409
410
411  /**
412   * Retrieves the time represented by this UTC time element, expressed as the
413   * number of milliseconds since the epoch (the same format used by
414   * {@code System.currentTimeMillis()} and {@code Date.getTime()}).
415
416   * @return  The time represented by this UTC time element.
417   */
418  public long getTime()
419  {
420    return time;
421  }
422
423
424
425  /**
426   * Retrieves a {@code Date} object that is set to the time represented by this
427   * UTC time element.
428   *
429   * @return  A {@code Date} object that is set ot the time represented by this
430   *          UTC time element.
431   */
432  @NotNull()
433  public Date getDate()
434  {
435    return new Date(time);
436  }
437
438
439
440  /**
441   * Retrieves the string representation of the UTC time value contained in this
442   * element.
443   *
444   * @return  The string representation of the UTC time value contained in this
445   *          element.
446   */
447  @NotNull()
448  public String getStringRepresentation()
449  {
450    return stringRepresentation;
451  }
452
453
454
455  /**
456   * Decodes the contents of the provided byte array as a UTC time element.
457   *
458   * @param  elementBytes  The byte array to decode as an ASN.1 UTC time
459   *                       element.
460   *
461   * @return  The decoded ASN.1 UTC time element.
462   *
463   * @throws  ASN1Exception  If the provided array cannot be decoded as a UTC
464   *                         time element.
465   */
466  @NotNull()
467  public static ASN1UTCTime decodeAsUTCTime(@NotNull final byte[] elementBytes)
468         throws ASN1Exception
469  {
470    try
471    {
472      int valueStartPos = 2;
473      int length = (elementBytes[1] & 0x7F);
474      if (length != elementBytes[1])
475      {
476        final int numLengthBytes = length;
477
478        length = 0;
479        for (int i=0; i < numLengthBytes; i++)
480        {
481          length <<= 8;
482          length |= (elementBytes[valueStartPos++] & 0xFF);
483        }
484      }
485
486      if ((elementBytes.length - valueStartPos) != length)
487      {
488        throw new ASN1Exception(ERR_ELEMENT_LENGTH_MISMATCH.get(length,
489                                     (elementBytes.length - valueStartPos)));
490      }
491
492      final byte[] elementValue = new byte[length];
493      System.arraycopy(elementBytes, valueStartPos, elementValue, 0, length);
494
495      return new ASN1UTCTime(elementBytes[0],
496           StaticUtils.toUTF8String(elementValue));
497    }
498    catch (final ASN1Exception ae)
499    {
500      Debug.debugException(ae);
501      throw ae;
502    }
503    catch (final Exception e)
504    {
505      Debug.debugException(e);
506      throw new ASN1Exception(ERR_ELEMENT_DECODE_EXCEPTION.get(e), e);
507    }
508  }
509
510
511
512  /**
513   * Decodes the provided ASN.1 element as a UTC time element.
514   *
515   * @param  element  The ASN.1 element to be decoded.
516   *
517   * @return  The decoded ASN.1 UTC time element.
518   *
519   * @throws  ASN1Exception  If the provided element cannot be decoded as a UTC
520   *                         time element.
521   */
522  @NotNull()
523  public static ASN1UTCTime decodeAsUTCTime(@NotNull final ASN1Element element)
524         throws ASN1Exception
525  {
526    return new ASN1UTCTime(element.getType(),
527         StaticUtils.toUTF8String(element.getValue()));
528  }
529
530
531
532  /**
533   * {@inheritDoc}
534   */
535  @Override()
536  public void toString(@NotNull final StringBuilder buffer)
537  {
538    buffer.append(stringRepresentation);
539  }
540}