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     */
021    package com.unboundid.util.args;
022    
023    
024    
025    import java.util.concurrent.TimeUnit;
026    
027    import com.unboundid.util.Debug;
028    import com.unboundid.util.LDAPSDKUsageException;
029    import com.unboundid.util.Mutable;
030    import com.unboundid.util.StaticUtils;
031    import com.unboundid.util.ThreadSafety;
032    import com.unboundid.util.ThreadSafetyLevel;
033    
034    import 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)
059    public 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    }