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