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 }