001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-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) 2016-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.util.args;
037
038
039
040import java.text.ParseException;
041import java.text.SimpleDateFormat;
042import java.util.ArrayList;
043import java.util.Date;
044import java.util.Collections;
045import java.util.Iterator;
046import java.util.List;
047
048import com.unboundid.util.Debug;
049import com.unboundid.util.Mutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.ObjectPair;
053import com.unboundid.util.StaticUtils;
054import com.unboundid.util.ThreadSafety;
055import com.unboundid.util.ThreadSafetyLevel;
056
057import static com.unboundid.util.args.ArgsMessages.*;
058
059
060
061/**
062 * This class defines an argument that is intended to hold one or more
063 * timestamp values.  Values may be provided in any of the following formats:
064 * <UL>
065 *   <LI>Any valid generalized time format.</LI>
066 *   <LI>Any valid ISO 8601 timestamp in the format described in RFC 3339.</LI>
067 *   <LI>A local time zone timestamp in the format YYYYMMDDhhmmss.uuu</LI>
068 *   <LI>A local time zone timestamp in the format YYYYMMDDhhmmss</LI>
069 *   <LI>A local time zone timestamp in the format YYYYMMDDhhmm</LI>
070 * </UL>
071 */
072@Mutable()
073@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
074public final class TimestampArgument
075       extends Argument
076{
077  /**
078   * The serial version UID for this serializable class.
079   */
080  private static final long serialVersionUID = -4842934851103696096L;
081
082
083
084  // The argument value validators that have been registered for this argument.
085  @NotNull private final List<ArgumentValueValidator> validators;
086
087  // The list of default values for this argument.
088  @Nullable private final List<Date> defaultValues;
089
090  // The set of values assigned to this argument.
091  @NotNull private final List<ObjectPair<Date,String>> values;
092
093
094
095  /**
096   * Creates a new timestamp argument with the provided information.  It will
097   * not be required, will permit at most one occurrence, will use a default
098   * placeholder, and will not have a default value.
099   *
100   * @param  shortIdentifier   The short identifier for this argument.  It may
101   *                           not be {@code null} if the long identifier is
102   *                           {@code null}.
103   * @param  longIdentifier    The long identifier for this argument.  It may
104   *                           not be {@code null} if the short identifier is
105   *                           {@code null}.
106   * @param  description       A human-readable description for this argument.
107   *                           It must not be {@code null}.
108   *
109   * @throws  ArgumentException  If there is a problem with the definition of
110   *                             this argument.
111   */
112  public TimestampArgument(@Nullable final Character shortIdentifier,
113                           @Nullable final String longIdentifier,
114                           @NotNull final String description)
115         throws ArgumentException
116  {
117    this(shortIdentifier, longIdentifier, false, 1, null, description);
118  }
119
120
121
122  /**
123   * Creates a new timestamp argument with the provided information.  It will
124   * not have a default value.
125   *
126   * @param  shortIdentifier   The short identifier for this argument.  It may
127   *                           not be {@code null} if the long identifier is
128   *                           {@code null}.
129   * @param  longIdentifier    The long identifier for this argument.  It may
130   *                           not be {@code null} if the short identifier is
131   *                           {@code null}.
132   * @param  isRequired        Indicates whether this argument is required to
133   *                           be provided.
134   * @param  maxOccurrences    The maximum number of times this argument may be
135   *                           provided on the command line.  A value less than
136   *                           or equal to zero indicates that it may be present
137   *                           any number of times.
138   * @param  valuePlaceholder  A placeholder to display in usage information to
139   *                           indicate that a value must be provided.  It may
140   *                           be {@code null} if a default placeholder should
141   *                           be used.
142   * @param  description       A human-readable description for this argument.
143   *                           It must not be {@code null}.
144   *
145   * @throws  ArgumentException  If there is a problem with the definition of
146   *                             this argument.
147   */
148  public TimestampArgument(@Nullable final Character shortIdentifier,
149                           @Nullable final String longIdentifier,
150                           final boolean isRequired, final int maxOccurrences,
151                           @Nullable final String valuePlaceholder,
152                           @NotNull final String description)
153         throws ArgumentException
154  {
155    this(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
156         valuePlaceholder, description, (List<Date>) null);
157  }
158
159
160
161  /**
162   * Creates a new timestamp argument with the provided information.
163   *
164   * @param  shortIdentifier   The short identifier for this argument.  It may
165   *                           not be {@code null} if the long identifier is
166   *                           {@code null}.
167   * @param  longIdentifier    The long identifier for this argument.  It may
168   *                           not be {@code null} if the short identifier is
169   *                           {@code null}.
170   * @param  isRequired        Indicates whether this argument is required to
171   *                           be provided.
172   * @param  maxOccurrences    The maximum number of times this argument may be
173   *                           provided on the command line.  A value less than
174   *                           or equal to zero indicates that it may be present
175   *                           any number of times.
176   * @param  valuePlaceholder  A placeholder to display in usage information to
177   *                           indicate that a value must be provided.  It may
178   *                           be {@code null} if a default placeholder should
179   *                           be used.
180   * @param  description       A human-readable description for this argument.
181   *                           It must not be {@code null}.
182   * @param  defaultValue      The default value to use for this argument if no
183   *                           values were provided.
184   *
185   * @throws  ArgumentException  If there is a problem with the definition of
186   *                             this argument.
187   */
188  public TimestampArgument(@Nullable final Character shortIdentifier,
189                           @Nullable final String longIdentifier,
190                           final boolean isRequired, final int maxOccurrences,
191                           @Nullable final String valuePlaceholder,
192                           @NotNull final String description,
193                           @Nullable final Date defaultValue)
194         throws ArgumentException
195  {
196    this(shortIdentifier, longIdentifier, isRequired, maxOccurrences,
197         valuePlaceholder, description,
198         ((defaultValue == null)
199              ? null
200              : Collections.singletonList(defaultValue)));
201  }
202
203
204
205  /**
206   * Creates a new timestamp argument with the provided information.
207   *
208   * @param  shortIdentifier   The short identifier for this argument.  It may
209   *                           not be {@code null} if the long identifier is
210   *                           {@code null}.
211   * @param  longIdentifier    The long identifier for this argument.  It may
212   *                           not be {@code null} if the short identifier is
213   *                           {@code null}.
214   * @param  isRequired        Indicates whether this argument is required to
215   *                           be provided.
216   * @param  maxOccurrences    The maximum number of times this argument may be
217   *                           provided on the command line.  A value less than
218   *                           or equal to zero indicates that it may be present
219   *                           any number of times.
220   * @param  valuePlaceholder  A placeholder to display in usage information to
221   *                           indicate that a value must be provided.  It may
222   *                           be {@code null} if a default placeholder should
223   *                           be used.
224   * @param  description       A human-readable description for this argument.
225   *                           It must not be {@code null}.
226   * @param  defaultValues     The set of default values to use for this
227   *                           argument if no values were provided.
228   *
229   * @throws  ArgumentException  If there is a problem with the definition of
230   *                             this argument.
231   */
232  public TimestampArgument(@Nullable final Character shortIdentifier,
233                           @Nullable final String longIdentifier,
234                           final boolean isRequired, final int maxOccurrences,
235                           @Nullable final String valuePlaceholder,
236                           @NotNull final String description,
237                           @Nullable final List<Date> defaultValues)
238         throws ArgumentException
239  {
240    super(shortIdentifier, longIdentifier, isRequired,  maxOccurrences,
241         (valuePlaceholder == null)
242              ? INFO_PLACEHOLDER_TIMESTAMP.get()
243              : valuePlaceholder,
244         description);
245
246    if ((defaultValues == null) || defaultValues.isEmpty())
247    {
248      this.defaultValues = null;
249    }
250    else
251    {
252      this.defaultValues = Collections.unmodifiableList(defaultValues);
253    }
254
255    values = new ArrayList<>(5);
256    validators = new ArrayList<>(5);
257  }
258
259
260
261  /**
262   * Creates a new timestamp argument that is a "clean" copy of the provided
263   * source argument.
264   *
265   * @param  source  The source argument to use for this argument.
266   */
267  private TimestampArgument(@NotNull final TimestampArgument source)
268  {
269    super(source);
270
271    defaultValues = source.defaultValues;
272    values        = new ArrayList<>(5);
273    validators    = new ArrayList<>(source.validators);
274  }
275
276
277
278  /**
279   * Retrieves the list of default values for this argument, which will be used
280   * if no values were provided.
281   *
282   * @return   The list of default values for this argument, or {@code null} if
283   *           there are no default values.
284   */
285  @Nullable()
286  public List<Date> getDefaultValues()
287  {
288    return defaultValues;
289  }
290
291
292
293  /**
294   * Updates this argument to ensure that the provided validator will be invoked
295   * for any values provided to this argument.  This validator will be invoked
296   * after all other validation has been performed for this argument.
297   *
298   * @param  validator  The argument value validator to be invoked.  It must not
299   *                    be {@code null}.
300   */
301  public void addValueValidator(@NotNull final ArgumentValueValidator validator)
302  {
303    validators.add(validator);
304  }
305
306
307
308  /**
309   * {@inheritDoc}
310   */
311  @Override()
312  protected void addValue(@NotNull final String valueString)
313            throws ArgumentException
314  {
315    final Date d;
316    try
317    {
318      d = parseTimestamp(valueString);
319    }
320    catch (final Exception e)
321    {
322      Debug.debugException(e);
323      throw new ArgumentException(
324           ERR_TIMESTAMP_VALUE_NOT_TIMESTAMP.get(valueString,
325                getIdentifierString()),
326           e);
327    }
328
329
330    if (values.size() >= getMaxOccurrences())
331    {
332      throw new ArgumentException(ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get(
333                                       getIdentifierString()));
334    }
335
336    for (final ArgumentValueValidator v : validators)
337    {
338      v.validateArgumentValue(this, valueString);
339    }
340
341    values.add(new ObjectPair<>(d, valueString));
342  }
343
344
345
346  /**
347   * Parses the provided string as a timestamp using one of the supported
348   * formats.
349   *
350   * @param  s  The string to parse as a timestamp.  It must not be
351   *            {@code null}.
352   *
353   * @return  The {@code Date} object parsed from the provided timestamp.
354   *
355   * @throws  ParseException  If the provided string cannot be parsed as a
356   *                          timestamp.
357   */
358  @NotNull()
359  public static Date parseTimestamp(@NotNull final String s)
360         throws ParseException
361  {
362    // First, try to parse the value as a generalized time.
363    try
364    {
365      return StaticUtils.decodeGeneralizedTime(s);
366    }
367    catch (final Exception e)
368    {
369      // This is fine.  It just means the value isn't in the generalized time
370      // format.
371    }
372
373
374    // First, try to parse the value as a timestamp formatted in the ISO 8601
375    // format specified in RFC 3339.
376    try
377    {
378      return StaticUtils.decodeRFC3339Time(s);
379    }
380    catch (final Exception e)
381    {
382      // This is fine.  It just means the value isn't in the RFC 3339 format.
383    }
384
385
386    // See if the length of the string matches one of the supported local
387    // formats.  If so, get a format string that we can use to parse the value.
388    final String dateFormatString;
389    switch (s.length())
390    {
391      case 18:
392        dateFormatString = "yyyyMMddHHmmss.SSS";
393        break;
394      case 14:
395        dateFormatString = "yyyyMMddHHmmss";
396        break;
397      case 12:
398        dateFormatString = "yyyyMMddHHmm";
399        break;
400      default:
401        throw new ParseException(ERR_TIMESTAMP_PARSE_ERROR.get(s), 0);
402    }
403
404
405    // Create a date formatter that will use the selected format string to parse
406    // the timestamp.
407    final SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString);
408    dateFormat.setLenient(false);
409    return dateFormat.parse(s);
410  }
411
412
413
414  /**
415   * Retrieves the value for this argument, or the default value if none was
416   * provided.  If there are multiple values, then the first will be returned.
417   *
418   * @return  The value for this argument, or the default value if none was
419   *          provided, or {@code null} if there is no value and no default
420   *          value.
421   */
422  @Nullable()
423  public Date getValue()
424  {
425    if (values.isEmpty())
426    {
427      if ((defaultValues == null) || defaultValues.isEmpty())
428      {
429        return null;
430      }
431      else
432      {
433        return defaultValues.get(0);
434      }
435    }
436    else
437    {
438      return values.get(0).getFirst();
439    }
440  }
441
442
443
444  /**
445   * Retrieves the set of values for this argument.
446   *
447   * @return  The set of values for this argument.
448   */
449  @NotNull()
450  public List<Date> getValues()
451  {
452    if (values.isEmpty() && (defaultValues != null))
453    {
454      return defaultValues;
455    }
456
457    final ArrayList<Date> dateList = new ArrayList<>(values.size());
458    for (final ObjectPair<Date,String> p : values)
459    {
460      dateList.add(p.getFirst());
461    }
462
463    return Collections.unmodifiableList(dateList);
464  }
465
466
467
468  /**
469   * Retrieves a string representation of the value for this argument, or a
470   * string representation of the default value if none was provided.  If there
471   * are multiple values, then the first will be returned.
472   *
473   * @return  The string representation of the value for this argument, or the
474   *          string representation of the default value if none was provided,
475   *          or {@code null} if there is no value and no default value.
476   */
477  @Nullable()
478  public String getStringValue()
479  {
480    if (! values.isEmpty())
481    {
482      return values.get(0).getSecond();
483    }
484
485    if ((defaultValues != null) && (! defaultValues.isEmpty()))
486    {
487      return StaticUtils.encodeGeneralizedTime(defaultValues.get(0));
488    }
489
490    return null;
491  }
492
493
494
495  /**
496   * {@inheritDoc}
497   */
498  @Override()
499  @NotNull()
500  public List<String> getValueStringRepresentations(final boolean useDefault)
501  {
502    if (! values.isEmpty())
503    {
504      final ArrayList<String> valueStrings = new ArrayList<>(values.size());
505      for (final ObjectPair<Date,String> p : values)
506      {
507        valueStrings.add(p.getSecond());
508      }
509
510      return Collections.unmodifiableList(valueStrings);
511    }
512
513    if (useDefault && (defaultValues != null) && (! defaultValues.isEmpty()))
514    {
515      final ArrayList<String> valueStrings =
516           new ArrayList<>(defaultValues.size());
517      for (final Date d : defaultValues)
518      {
519        valueStrings.add(StaticUtils.encodeGeneralizedTime(d));
520      }
521
522      return Collections.unmodifiableList(valueStrings);
523    }
524
525    return Collections.emptyList();
526  }
527
528
529
530  /**
531   * {@inheritDoc}
532   */
533  @Override()
534  protected boolean hasDefaultValue()
535  {
536    return ((defaultValues != null) && (! defaultValues.isEmpty()));
537  }
538
539
540
541  /**
542   * {@inheritDoc}
543   */
544  @Override()
545  @NotNull()
546  public String getDataTypeName()
547  {
548    return INFO_TIMESTAMP_TYPE_NAME.get();
549  }
550
551
552
553  /**
554   * {@inheritDoc}
555   */
556  @Override()
557  @NotNull()
558  public String getValueConstraints()
559  {
560    return INFO_TIMESTAMP_CONSTRAINTS.get();
561  }
562
563
564
565  /**
566   * {@inheritDoc}
567   */
568  @Override()
569  protected void reset()
570  {
571    super.reset();
572    values.clear();
573  }
574
575
576
577  /**
578   * {@inheritDoc}
579   */
580  @Override()
581  @NotNull()
582  public TimestampArgument getCleanCopy()
583  {
584    return new TimestampArgument(this);
585  }
586
587
588
589  /**
590   * {@inheritDoc}
591   */
592  @Override()
593  protected void addToCommandLine(@NotNull final List<String> argStrings)
594  {
595    for (final ObjectPair<Date,String> p : values)
596    {
597      argStrings.add(getIdentifierString());
598      if (isSensitive())
599      {
600        argStrings.add("***REDACTED***");
601      }
602      else
603      {
604        argStrings.add(p.getSecond());
605      }
606    }
607  }
608
609
610
611  /**
612   * {@inheritDoc}
613   */
614  @Override()
615  public void toString(@NotNull final StringBuilder buffer)
616  {
617    buffer.append("TimestampArgument(");
618    appendBasicToStringInfo(buffer);
619
620    if ((defaultValues != null) && (! defaultValues.isEmpty()))
621    {
622      if (defaultValues.size() == 1)
623      {
624        buffer.append(", defaultValue='");
625        buffer.append(StaticUtils.encodeGeneralizedTime(defaultValues.get(0)));
626      }
627      else
628      {
629        buffer.append(", defaultValues={");
630
631        final Iterator<Date> iterator = defaultValues.iterator();
632        while (iterator.hasNext())
633        {
634          buffer.append('\'');
635          buffer.append(StaticUtils.encodeGeneralizedTime(iterator.next()));
636          buffer.append('\'');
637
638          if (iterator.hasNext())
639          {
640            buffer.append(", ");
641          }
642        }
643
644        buffer.append('}');
645      }
646    }
647
648    buffer.append(')');
649  }
650}