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