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