001/*
002 * Copyright 2010-2015 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2015 UnboundID Corp.
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.util.args;
022
023
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.concurrent.TimeUnit;
028
029import com.unboundid.util.Debug;
030import com.unboundid.util.LDAPSDKUsageException;
031import com.unboundid.util.Mutable;
032import com.unboundid.util.StaticUtils;
033import com.unboundid.util.ThreadSafety;
034import com.unboundid.util.ThreadSafetyLevel;
035
036import static com.unboundid.util.args.ArgsMessages.*;
037
038
039
040/**
041 * Creates a new argument that is intended to represent a duration.  Duration
042 * values contain an integer portion and a unit portion which represents the
043 * time unit.  The unit must be one of the following:
044 * <UL>
045 *   <LI>Nanoseconds -- ns, nano, nanos, nanosecond, nanoseconds</LI>
046 *   <LI>Microseconds -- us, micro, micros, microsecond, microseconds</LI>
047 *   <LI>Milliseconds -- ms, milli, millis, millisecond, milliseconds</LI>
048 *   <LI>Seconds -- s, sec, secs, second, seconds</LI>
049 *   <LI>Minutes -- m, min, mins, minute, minutes</LI>
050 *   <LI>Hours -- h, hr, hrs, hour, hours</LI>
051 *   <LI>Days -- d, day, days</LI>
052 * </UL>
053 *
054 * There may be zero or more spaces between the integer portion and the unit
055 * portion.  However, if spaces are used in the command-line argument, then the
056 * value must be enquoted or the spaces must be escaped so that the duration
057 * is not seen as multiple arguments.
058 */
059@Mutable()
060@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
061public final class DurationArgument
062       extends Argument
063{
064  /**
065   * The serial version UID for this serializable class.
066   */
067  private static final long serialVersionUID = -8824262632728709264L;
068
069
070
071  // The argument value validators that have been registered for this argument.
072  private final List<ArgumentValueValidator> validators;
073
074  // The default value for this argument, in nanoseconds.
075  private final Long defaultValueNanos;
076
077  // The maximum allowed value for this argument, in nanoseconds.
078  private final long maxValueNanos;
079
080  // The minimum allowed value for this argument, in nanoseconds.
081  private final long minValueNanos;
082
083  // The provided value for this argument, in nanoseconds.
084  private Long valueNanos;
085
086  // The string representation of the lower bound, using the user-supplied
087  // value.
088  private final String lowerBoundStr;
089
090  // The string representation of the upper bound, using the user-supplied
091  // value.
092  private final String upperBoundStr;
093
094
095
096  /**
097   * Creates a new duration argument with no default value and no bounds on the
098   * set of allowed values.
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  isRequired        Indicates whether this argument is required to
107   *                           be provided.
108   * @param  valuePlaceholder  A placeholder to display in usage information to
109   *                           indicate that a value must be provided.  It must
110   *                           not be {@code null}.
111   * @param  description       A human-readable description for this argument.
112   *                           It must not be {@code null}.
113   *
114   * @throws  ArgumentException  If there is a problem with the definition of
115   *                             this argument.
116   */
117  public DurationArgument(final Character shortIdentifier,
118                          final String longIdentifier, final boolean isRequired,
119                          final String valuePlaceholder,
120                          final String description)
121         throws ArgumentException
122  {
123    this(shortIdentifier, longIdentifier, isRequired, valuePlaceholder,
124         description, null, null, null, null, null, null);
125  }
126
127
128
129  /**
130   * Creates a new duration argument with the provided information.
131   *
132   * @param  shortIdentifier   The short identifier for this argument.  It may
133   *                           not be {@code null} if the long identifier is
134   *                           {@code null}.
135   * @param  longIdentifier    The long identifier for this argument.  It may
136   *                           not be {@code null} if the short identifier is
137   *                           {@code null}.
138   * @param  isRequired        Indicates whether this argument is required to
139   *                           be provided.
140   * @param  valuePlaceholder  A placeholder to display in usage information to
141   *                           indicate that a value must be provided.  It must
142   *                           not be {@code null}.
143   * @param  description       A human-readable description for this argument.
144   *                           It must not be {@code null}.
145   * @param  defaultValue      The default value that will be used for this
146   *                           argument if none is provided.  It may be
147   *                           {@code null} if there should not be a default
148   *                           value.
149   * @param  defaultValueUnit  The time unit for the default value.  It may be
150   *                           {@code null} only if the default value is also
151   *                           {@code null}.
152   * @param  lowerBound        The value for the minimum duration that may be
153   *                           represented using this argument, in conjunction
154   *                           with the {@code lowerBoundUnit} parameter to
155   *                           specify the unit for this value.  If this is
156   *                           {@code null}, then a lower bound of 0 nanoseconds
157   *                           will be used.
158   * @param  lowerBoundUnit    The time unit for the lower bound value.  It may
159   *                           be {@code null} only if the lower bound is also
160   *                           {@code null}.
161   * @param  upperBound        The value for the maximum duration that may be
162   *                           represented using this argument, in conjunction
163   *                           with the {@code upperBoundUnit} parameter to
164   *                           specify the unit for this value.  If this is
165   *                           {@code null}, then an upper bound of
166   *                           {@code Long.MAX_VALUE} nanoseconds will be used.
167   * @param  upperBoundUnit    The time unit for the upper bound value.  It may
168   *                           be {@code null} only if the upper bound is also
169   *                           {@code null}.
170   *
171   * @throws  ArgumentException  If there is a problem with the definition of
172   *                             this argument.
173   */
174  public DurationArgument(final Character shortIdentifier,
175                          final String longIdentifier, final boolean isRequired,
176                          final String valuePlaceholder,
177                          final String description, final Long defaultValue,
178                          final TimeUnit defaultValueUnit,
179                          final Long lowerBound, final TimeUnit lowerBoundUnit,
180                          final Long upperBound, final TimeUnit upperBoundUnit)
181         throws ArgumentException
182  {
183    super(shortIdentifier, longIdentifier, isRequired, 1, valuePlaceholder,
184         description);
185
186    if (valuePlaceholder == null)
187    {
188      throw new ArgumentException(
189           ERR_ARG_MUST_TAKE_VALUE.get(getIdentifierString()));
190    }
191
192    if (defaultValue == null)
193    {
194      defaultValueNanos = null;
195    }
196    else
197    {
198      if (defaultValueUnit == null)
199      {
200        throw new ArgumentException(ERR_DURATION_DEFAULT_REQUIRES_UNIT.get(
201             getIdentifierString()));
202      }
203
204      defaultValueNanos = defaultValueUnit.toNanos(defaultValue);
205    }
206
207    if (lowerBound == null)
208    {
209      minValueNanos = 0L;
210      lowerBoundStr = "0ns";
211    }
212    else
213    {
214      if (lowerBoundUnit == null)
215      {
216        throw new ArgumentException(ERR_DURATION_LOWER_REQUIRES_UNIT.get(
217             getIdentifierString()));
218      }
219
220      minValueNanos = lowerBoundUnit.toNanos(lowerBound);
221      final String lowerBoundUnitName = lowerBoundUnit.name();
222      if (lowerBoundUnitName.equals("NANOSECONDS"))
223      {
224        lowerBoundStr = minValueNanos + "ns";
225      }
226      else if (lowerBoundUnitName.equals("MICROSECONDS"))
227      {
228        lowerBoundStr = lowerBound + "us";
229      }
230      else if (lowerBoundUnitName.equals("MILLISECONDS"))
231      {
232        lowerBoundStr = lowerBound + "ms";
233      }
234      else if (lowerBoundUnitName.equals("SECONDS"))
235      {
236        lowerBoundStr = lowerBound + "s";
237      }
238      else if (lowerBoundUnitName.equals("MINUTES"))
239      {
240        lowerBoundStr = lowerBound + "m";
241      }
242      else if (lowerBoundUnitName.equals("HOURS"))
243      {
244        lowerBoundStr = lowerBound + "h";
245      }
246      else if (lowerBoundUnitName.equals("DAYS"))
247      {
248        lowerBoundStr = lowerBound + "d";
249      }
250      else
251      {
252        throw new LDAPSDKUsageException(
253             ERR_DURATION_UNSUPPORTED_LOWER_BOUND_UNIT.get(lowerBoundUnitName));
254      }
255    }
256
257    if (upperBound == null)
258    {
259      maxValueNanos = Long.MAX_VALUE;
260      upperBoundStr = Long.MAX_VALUE + "ns";
261    }
262    else
263    {
264      if (upperBoundUnit == null)
265      {
266        throw new ArgumentException(ERR_DURATION_UPPER_REQUIRES_UNIT.get(
267             getIdentifierString()));
268      }
269
270      maxValueNanos = upperBoundUnit.toNanos(upperBound);
271      final String upperBoundUnitName = upperBoundUnit.name();
272      if (upperBoundUnitName.equals("NANOSECONDS"))
273      {
274        upperBoundStr = minValueNanos + "ns";
275      }
276      else if (upperBoundUnitName.equals("MICROSECONDS"))
277      {
278        upperBoundStr = upperBound + "us";
279      }
280      else if (upperBoundUnitName.equals("MILLISECONDS"))
281      {
282        upperBoundStr = upperBound + "ms";
283      }
284      else if (upperBoundUnitName.equals("SECONDS"))
285      {
286        upperBoundStr = upperBound + "s";
287      }
288      else if (upperBoundUnitName.equals("MINUTES"))
289      {
290        upperBoundStr = upperBound + "m";
291      }
292      else if (upperBoundUnitName.equals("HOURS"))
293      {
294        upperBoundStr = upperBound + "h";
295      }
296      else if (upperBoundUnitName.equals("DAYS"))
297      {
298        upperBoundStr = upperBound + "d";
299      }
300      else
301      {
302        throw new LDAPSDKUsageException(
303             ERR_DURATION_UNSUPPORTED_UPPER_BOUND_UNIT.get(upperBoundUnitName));
304      }
305    }
306
307    if (minValueNanos > maxValueNanos)
308    {
309      throw new ArgumentException(ERR_DURATION_LOWER_GT_UPPER.get(
310           getIdentifierString(), lowerBoundStr, upperBoundStr));
311    }
312
313    valueNanos = null;
314    validators = new ArrayList<ArgumentValueValidator>(5);
315  }
316
317
318
319  /**
320   * Creates a new duration argument that is a "clean" copy of the provided
321   * source argument.
322   *
323   * @param  source  The source argument to use for this argument.
324   */
325  private DurationArgument(final DurationArgument source)
326  {
327    super(source);
328
329    defaultValueNanos = source.defaultValueNanos;
330    maxValueNanos     = source.maxValueNanos;
331    minValueNanos     = source.minValueNanos;
332    lowerBoundStr     = source.lowerBoundStr;
333    upperBoundStr     = source.upperBoundStr;
334    validators        =
335         new ArrayList<ArgumentValueValidator>(source.validators);
336    valueNanos        = null;
337  }
338
339
340
341  /**
342   * Retrieves the lower bound for this argument using the specified time unit.
343   *
344   * @param  unit  The time unit in which the lower bound value may be
345   *               expressed.
346   *
347   * @return  The lower bound for this argument using the specified time unit.
348   */
349  public long getLowerBound(final TimeUnit unit)
350  {
351    return unit.convert(minValueNanos, TimeUnit.NANOSECONDS);
352  }
353
354
355
356  /**
357   * Retrieves the upper bound for this argument using the specified time unit.
358   *
359   * @param  unit  The time unit in which the upper bound value may be
360   *               expressed.
361   *
362   * @return  The upper bound for this argument using the specified time unit.
363   */
364  public long getUpperBound(final TimeUnit unit)
365  {
366    return unit.convert(maxValueNanos, TimeUnit.NANOSECONDS);
367  }
368
369
370
371  /**
372   * {@inheritDoc}
373   */
374  @Override()
375  protected boolean hasDefaultValue()
376  {
377    return (defaultValueNanos != null);
378  }
379
380
381
382  /**
383   * Retrieves the default value for this argument using the specified time
384   * unit, if defined.
385   *
386   * @param  unit  The time unit in which the default value should be expressed.
387   *
388   * @return  The default value for this argument using the specified time unit,
389   *          or {@code null} if none is defined.
390   */
391  public Long getDefaultValue(final TimeUnit unit)
392  {
393    if (defaultValueNanos == null)
394    {
395      return null;
396    }
397
398    return unit.convert(defaultValueNanos, TimeUnit.NANOSECONDS);
399  }
400
401
402
403  /**
404   * Retrieves the value for this argument using the specified time unit, if one
405   * was provided.
406   *
407   * @param  unit  The time unit in which to express the value for this
408   *               argument.
409   *
410   * @return  The value for this argument using the specified time unit.  If no
411   *          value was provided but a default value was defined, then the
412   *          default value will be returned.  If no value was provided and no
413   *          default value was defined, then {@code null} will be returned.
414   */
415  public Long getValue(final TimeUnit unit)
416  {
417    if (valueNanos == null)
418    {
419      if (defaultValueNanos == null)
420      {
421        return null;
422      }
423
424      return unit.convert(defaultValueNanos, TimeUnit.NANOSECONDS);
425    }
426    else
427    {
428      return unit.convert(valueNanos, TimeUnit.NANOSECONDS);
429    }
430  }
431
432
433
434  /**
435   * Updates this argument to ensure that the provided validator will be invoked
436   * for any values provided to this argument.  This validator will be invoked
437   * after all other validation has been performed for this argument.
438   *
439   * @param  validator  The argument value validator to be invoked.  It must not
440   *                    be {@code null}.
441   */
442  public void addValueValidator(final ArgumentValueValidator validator)
443  {
444    validators.add(validator);
445  }
446
447
448
449  /**
450   * {@inheritDoc}
451   */
452  @Override()
453  protected void addValue(final String valueString)
454            throws ArgumentException
455  {
456    if (valueNanos != null)
457    {
458      throw new ArgumentException(
459           ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get(getIdentifierString()));
460    }
461
462    final long proposedValueNanos;
463    try
464    {
465      proposedValueNanos = parseDuration(valueString, TimeUnit.NANOSECONDS);
466    }
467    catch (final ArgumentException ae)
468    {
469      Debug.debugException(ae);
470      throw new ArgumentException(
471           ERR_DURATION_MALFORMED_VALUE.get(valueString, getIdentifierString(),
472                ae.getMessage()),
473           ae);
474    }
475
476    if (proposedValueNanos < minValueNanos)
477    {
478      throw new ArgumentException(ERR_DURATION_BELOW_LOWER_BOUND.get(
479           getIdentifierString(), lowerBoundStr));
480    }
481    else if (proposedValueNanos > maxValueNanos)
482    {
483      throw new ArgumentException(ERR_DURATION_ABOVE_UPPER_BOUND.get(
484           getIdentifierString(), upperBoundStr));
485    }
486    else
487    {
488      for (final ArgumentValueValidator v : validators)
489      {
490        v.validateArgumentValue(this, valueString);
491      }
492
493      valueNanos = proposedValueNanos;
494    }
495  }
496
497
498
499  /**
500   * Parses the provided string representation of a duration to a corresponding
501   * numeric representation.
502   *
503   * @param  durationString  The string representation of the duration to be
504   *                         parsed.
505   * @param  timeUnit        The time unit to use for the return value.
506   *
507   * @return  The parsed duration as a count in the specified time unit.
508   *
509   * @throws  ArgumentException  If the provided string cannot be parsed as a
510   *                             valid duration.
511   */
512  public static long parseDuration(final String durationString,
513                                   final TimeUnit timeUnit)
514         throws ArgumentException
515  {
516    // The string must not be empty.
517    final String lowerStr = StaticUtils.toLowerCase(durationString);
518    if (lowerStr.length() == 0)
519    {
520      throw new ArgumentException(ERR_DURATION_EMPTY_VALUE.get());
521    }
522
523    // Find the position of the first non-digit character.
524    boolean digitFound    = false;
525    boolean nonDigitFound = false;
526    int     nonDigitPos   = -1;
527    for (int i=0; i < lowerStr.length(); i++)
528    {
529      final char c = lowerStr.charAt(i);
530      if (Character.isDigit(c))
531      {
532        digitFound = true;
533      }
534      else
535      {
536        nonDigitFound = true;
537        nonDigitPos   = i;
538        if (! digitFound)
539        {
540          throw new ArgumentException(ERR_DURATION_NO_DIGIT.get());
541        }
542        break;
543      }
544    }
545
546    if (! nonDigitFound)
547    {
548      throw new ArgumentException(ERR_DURATION_NO_UNIT.get());
549    }
550
551    // Separate the integer portion from the unit.
552    long integerPortion = Long.parseLong(lowerStr.substring(0, nonDigitPos));
553    final String unitStr = lowerStr.substring(nonDigitPos).trim();
554
555    // Parse the time unit.
556    final TimeUnit unitFromString;
557    if (unitStr.equals("ns") ||
558        unitStr.equals("nano") ||
559        unitStr.equals("nanos") ||
560        unitStr.equals("nanosecond") ||
561        unitStr.equals("nanoseconds"))
562    {
563      unitFromString = TimeUnit.NANOSECONDS;
564    }
565    else if (unitStr.equals("us") ||
566             unitStr.equals("micro") ||
567             unitStr.equals("micros") ||
568             unitStr.equals("microsecond") ||
569             unitStr.equals("microseconds"))
570    {
571      unitFromString = TimeUnit.MICROSECONDS;
572    }
573    else if (unitStr.equals("ms") ||
574             unitStr.equals("milli") ||
575             unitStr.equals("millis") ||
576             unitStr.equals("millisecond") ||
577             unitStr.equals("milliseconds"))
578    {
579      unitFromString = TimeUnit.MILLISECONDS;
580    }
581    else if (unitStr.equals("s") ||
582             unitStr.equals("sec") ||
583             unitStr.equals("secs") ||
584             unitStr.equals("second") ||
585             unitStr.equals("seconds"))
586    {
587      unitFromString = TimeUnit.SECONDS;
588    }
589    else if (unitStr.equals("m") ||
590             unitStr.equals("min") ||
591             unitStr.equals("mins") ||
592             unitStr.equals("minute") ||
593             unitStr.equals("minutes"))
594    {
595      integerPortion *= 60L;
596      unitFromString = TimeUnit.SECONDS;
597    }
598    else if (unitStr.equals("h") ||
599             unitStr.equals("hr") ||
600             unitStr.equals("hrs") ||
601             unitStr.equals("hour") ||
602             unitStr.equals("hours"))
603    {
604      integerPortion *= 3600L;
605      unitFromString = TimeUnit.SECONDS;
606    }
607    else if (unitStr.equals("d") ||
608             unitStr.equals("day") ||
609             unitStr.equals("days"))
610    {
611      integerPortion *= 86400L;
612      unitFromString = TimeUnit.SECONDS;
613    }
614    else
615    {
616      throw new ArgumentException(ERR_DURATION_UNRECOGNIZED_UNIT.get(unitStr));
617    }
618
619    return timeUnit.convert(integerPortion, unitFromString);
620  }
621
622
623
624  /**
625   * {@inheritDoc}
626   */
627  @Override()
628  public String getDataTypeName()
629  {
630    return INFO_DURATION_TYPE_NAME.get();
631  }
632
633
634
635  /**
636   * {@inheritDoc}
637   */
638  @Override()
639  public String getValueConstraints()
640  {
641    final StringBuilder buffer = new StringBuilder();
642    buffer.append(INFO_DURATION_CONSTRAINTS_FORMAT.get());
643
644    if (lowerBoundStr != null)
645    {
646      if (upperBoundStr == null)
647      {
648        buffer.append("  ");
649        buffer.append(INFO_DURATION_CONSTRAINTS_LOWER_BOUND.get(lowerBoundStr));
650      }
651      else
652      {
653        buffer.append("  ");
654        buffer.append(INFO_DURATION_CONSTRAINTS_LOWER_AND_UPPER_BOUND.get(
655             lowerBoundStr, upperBoundStr));
656      }
657    }
658    else
659    {
660      if (upperBoundStr != null)
661      {
662        buffer.append("  ");
663        buffer.append(INFO_DURATION_CONSTRAINTS_UPPER_BOUND.get(upperBoundStr));
664      }
665    }
666
667    return buffer.toString();
668  }
669
670
671
672  /**
673   * {@inheritDoc}
674   */
675  @Override()
676  public DurationArgument getCleanCopy()
677  {
678    return new DurationArgument(this);
679  }
680
681
682
683  /**
684   * {@inheritDoc}
685   */
686  @Override()
687  public void toString(final StringBuilder buffer)
688  {
689    buffer.append("DurationArgument(");
690    appendBasicToStringInfo(buffer);
691
692    if (lowerBoundStr != null)
693    {
694      buffer.append(", lowerBound='");
695      buffer.append(lowerBoundStr);
696      buffer.append('\'');
697    }
698
699    if (upperBoundStr != null)
700    {
701      buffer.append(", upperBound='");
702      buffer.append(upperBoundStr);
703      buffer.append('\'');
704    }
705
706    if (defaultValueNanos != null)
707    {
708      buffer.append(", defaultValueNanos=");
709      buffer.append(defaultValueNanos);
710    }
711
712    buffer.append(')');
713  }
714}