001/* 002 * Copyright 2016-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2016-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.util.args; 037 038 039 040import java.text.ParseException; 041import java.text.SimpleDateFormat; 042import java.util.ArrayList; 043import java.util.Date; 044import java.util.Collections; 045import java.util.Iterator; 046import java.util.List; 047 048import com.unboundid.util.Debug; 049import com.unboundid.util.Mutable; 050import com.unboundid.util.NotNull; 051import com.unboundid.util.Nullable; 052import com.unboundid.util.ObjectPair; 053import com.unboundid.util.StaticUtils; 054import com.unboundid.util.ThreadSafety; 055import com.unboundid.util.ThreadSafetyLevel; 056 057import static com.unboundid.util.args.ArgsMessages.*; 058 059 060 061/** 062 * This class defines an argument that is intended to hold one or more 063 * timestamp values. Values may be provided in any of the following formats: 064 * <UL> 065 * <LI>Any valid generalized time format.</LI> 066 * <LI>Any valid ISO 8601 timestamp in the format described in RFC 3339.</LI> 067 * <LI>A local time zone timestamp in the format YYYYMMDDhhmmss.uuu</LI> 068 * <LI>A local time zone timestamp in the format YYYYMMDDhhmmss</LI> 069 * <LI>A local time zone timestamp in the format YYYYMMDDhhmm</LI> 070 * </UL> 071 */ 072@Mutable() 073@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 074public final class TimestampArgument 075 extends Argument 076{ 077 /** 078 * The serial version UID for this serializable class. 079 */ 080 private static final long serialVersionUID = -4842934851103696096L; 081 082 083 084 // The argument value validators that have been registered for this argument. 085 @NotNull private final List<ArgumentValueValidator> validators; 086 087 // The list of default values for this argument. 088 @Nullable private final List<Date> defaultValues; 089 090 // The set of values assigned to this argument. 091 @NotNull private final List<ObjectPair<Date,String>> values; 092 093 094 095 /** 096 * Creates a new timestamp argument with the provided information. It will 097 * not be required, will permit at most one occurrence, will use a default 098 * placeholder, and will not have a default value. 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 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 TimestampArgument(@Nullable final Character shortIdentifier, 113 @Nullable final String longIdentifier, 114 @NotNull final String description) 115 throws ArgumentException 116 { 117 this(shortIdentifier, longIdentifier, false, 1, null, description); 118 } 119 120 121 122 /** 123 * Creates a new timestamp argument with the provided information. It will 124 * not have a default value. 125 * 126 * @param shortIdentifier The short identifier for this argument. It may 127 * not be {@code null} if the long identifier is 128 * {@code null}. 129 * @param longIdentifier The long identifier for this argument. It may 130 * not be {@code null} if the short identifier is 131 * {@code null}. 132 * @param isRequired Indicates whether this argument is required to 133 * be provided. 134 * @param maxOccurrences The maximum number of times this argument may be 135 * provided on the command line. A value less than 136 * or equal to zero indicates that it may be present 137 * any number of times. 138 * @param valuePlaceholder A placeholder to display in usage information to 139 * indicate that a value must be provided. It may 140 * be {@code null} if a default placeholder should 141 * be used. 142 * @param description A human-readable description for this argument. 143 * It must not be {@code null}. 144 * 145 * @throws ArgumentException If there is a problem with the definition of 146 * this argument. 147 */ 148 public TimestampArgument(@Nullable final Character shortIdentifier, 149 @Nullable final String longIdentifier, 150 final boolean isRequired, final int maxOccurrences, 151 @Nullable final String valuePlaceholder, 152 @NotNull final String description) 153 throws ArgumentException 154 { 155 this(shortIdentifier, longIdentifier, isRequired, maxOccurrences, 156 valuePlaceholder, description, (List<Date>) null); 157 } 158 159 160 161 /** 162 * Creates a new timestamp argument with the provided information. 163 * 164 * @param shortIdentifier The short identifier for this argument. It may 165 * not be {@code null} if the long identifier is 166 * {@code null}. 167 * @param longIdentifier The long identifier for this argument. It may 168 * not be {@code null} if the short identifier is 169 * {@code null}. 170 * @param isRequired Indicates whether this argument is required to 171 * be provided. 172 * @param maxOccurrences The maximum number of times this argument may be 173 * provided on the command line. A value less than 174 * or equal to zero indicates that it may be present 175 * any number of times. 176 * @param valuePlaceholder A placeholder to display in usage information to 177 * indicate that a value must be provided. It may 178 * be {@code null} if a default placeholder should 179 * be used. 180 * @param description A human-readable description for this argument. 181 * It must not be {@code null}. 182 * @param defaultValue The default value to use for this argument if no 183 * values were provided. 184 * 185 * @throws ArgumentException If there is a problem with the definition of 186 * this argument. 187 */ 188 public TimestampArgument(@Nullable final Character shortIdentifier, 189 @Nullable final String longIdentifier, 190 final boolean isRequired, final int maxOccurrences, 191 @Nullable final String valuePlaceholder, 192 @NotNull final String description, 193 @Nullable final Date defaultValue) 194 throws ArgumentException 195 { 196 this(shortIdentifier, longIdentifier, isRequired, maxOccurrences, 197 valuePlaceholder, description, 198 ((defaultValue == null) 199 ? null 200 : Collections.singletonList(defaultValue))); 201 } 202 203 204 205 /** 206 * Creates a new timestamp argument with the provided information. 207 * 208 * @param shortIdentifier The short identifier for this argument. It may 209 * not be {@code null} if the long identifier is 210 * {@code null}. 211 * @param longIdentifier The long identifier for this argument. It may 212 * not be {@code null} if the short identifier is 213 * {@code null}. 214 * @param isRequired Indicates whether this argument is required to 215 * be provided. 216 * @param maxOccurrences The maximum number of times this argument may be 217 * provided on the command line. A value less than 218 * or equal to zero indicates that it may be present 219 * any number of times. 220 * @param valuePlaceholder A placeholder to display in usage information to 221 * indicate that a value must be provided. It may 222 * be {@code null} if a default placeholder should 223 * be used. 224 * @param description A human-readable description for this argument. 225 * It must not be {@code null}. 226 * @param defaultValues The set of default values to use for this 227 * argument if no values were provided. 228 * 229 * @throws ArgumentException If there is a problem with the definition of 230 * this argument. 231 */ 232 public TimestampArgument(@Nullable final Character shortIdentifier, 233 @Nullable final String longIdentifier, 234 final boolean isRequired, final int maxOccurrences, 235 @Nullable final String valuePlaceholder, 236 @NotNull final String description, 237 @Nullable final List<Date> defaultValues) 238 throws ArgumentException 239 { 240 super(shortIdentifier, longIdentifier, isRequired, maxOccurrences, 241 (valuePlaceholder == null) 242 ? INFO_PLACEHOLDER_TIMESTAMP.get() 243 : valuePlaceholder, 244 description); 245 246 if ((defaultValues == null) || defaultValues.isEmpty()) 247 { 248 this.defaultValues = null; 249 } 250 else 251 { 252 this.defaultValues = Collections.unmodifiableList(defaultValues); 253 } 254 255 values = new ArrayList<>(5); 256 validators = new ArrayList<>(5); 257 } 258 259 260 261 /** 262 * Creates a new timestamp argument that is a "clean" copy of the provided 263 * source argument. 264 * 265 * @param source The source argument to use for this argument. 266 */ 267 private TimestampArgument(@NotNull final TimestampArgument source) 268 { 269 super(source); 270 271 defaultValues = source.defaultValues; 272 values = new ArrayList<>(5); 273 validators = new ArrayList<>(source.validators); 274 } 275 276 277 278 /** 279 * Retrieves the list of default values for this argument, which will be used 280 * if no values were provided. 281 * 282 * @return The list of default values for this argument, or {@code null} if 283 * there are no default values. 284 */ 285 @Nullable() 286 public List<Date> getDefaultValues() 287 { 288 return defaultValues; 289 } 290 291 292 293 /** 294 * Updates this argument to ensure that the provided validator will be invoked 295 * for any values provided to this argument. This validator will be invoked 296 * after all other validation has been performed for this argument. 297 * 298 * @param validator The argument value validator to be invoked. It must not 299 * be {@code null}. 300 */ 301 public void addValueValidator(@NotNull final ArgumentValueValidator validator) 302 { 303 validators.add(validator); 304 } 305 306 307 308 /** 309 * {@inheritDoc} 310 */ 311 @Override() 312 protected void addValue(@NotNull final String valueString) 313 throws ArgumentException 314 { 315 final Date d; 316 try 317 { 318 d = parseTimestamp(valueString); 319 } 320 catch (final Exception e) 321 { 322 Debug.debugException(e); 323 throw new ArgumentException( 324 ERR_TIMESTAMP_VALUE_NOT_TIMESTAMP.get(valueString, 325 getIdentifierString()), 326 e); 327 } 328 329 330 if (values.size() >= getMaxOccurrences()) 331 { 332 throw new ArgumentException(ERR_ARG_MAX_OCCURRENCES_EXCEEDED.get( 333 getIdentifierString())); 334 } 335 336 for (final ArgumentValueValidator v : validators) 337 { 338 v.validateArgumentValue(this, valueString); 339 } 340 341 values.add(new ObjectPair<>(d, valueString)); 342 } 343 344 345 346 /** 347 * Parses the provided string as a timestamp using one of the supported 348 * formats. 349 * 350 * @param s The string to parse as a timestamp. It must not be 351 * {@code null}. 352 * 353 * @return The {@code Date} object parsed from the provided timestamp. 354 * 355 * @throws ParseException If the provided string cannot be parsed as a 356 * timestamp. 357 */ 358 @NotNull() 359 public static Date parseTimestamp(@NotNull final String s) 360 throws ParseException 361 { 362 // First, try to parse the value as a generalized time. 363 try 364 { 365 return StaticUtils.decodeGeneralizedTime(s); 366 } 367 catch (final Exception e) 368 { 369 // This is fine. It just means the value isn't in the generalized time 370 // format. 371 } 372 373 374 // First, try to parse the value as a timestamp formatted in the ISO 8601 375 // format specified in RFC 3339. 376 try 377 { 378 return StaticUtils.decodeRFC3339Time(s); 379 } 380 catch (final Exception e) 381 { 382 // This is fine. It just means the value isn't in the RFC 3339 format. 383 } 384 385 386 // See if the length of the string matches one of the supported local 387 // formats. If so, get a format string that we can use to parse the value. 388 final String dateFormatString; 389 switch (s.length()) 390 { 391 case 18: 392 dateFormatString = "yyyyMMddHHmmss.SSS"; 393 break; 394 case 14: 395 dateFormatString = "yyyyMMddHHmmss"; 396 break; 397 case 12: 398 dateFormatString = "yyyyMMddHHmm"; 399 break; 400 default: 401 throw new ParseException(ERR_TIMESTAMP_PARSE_ERROR.get(s), 0); 402 } 403 404 405 // Create a date formatter that will use the selected format string to parse 406 // the timestamp. 407 final SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatString); 408 dateFormat.setLenient(false); 409 return dateFormat.parse(s); 410 } 411 412 413 414 /** 415 * Retrieves the value for this argument, or the default value if none was 416 * provided. If there are multiple values, then the first will be returned. 417 * 418 * @return The value for this argument, or the default value if none was 419 * provided, or {@code null} if there is no value and no default 420 * value. 421 */ 422 @Nullable() 423 public Date getValue() 424 { 425 if (values.isEmpty()) 426 { 427 if ((defaultValues == null) || defaultValues.isEmpty()) 428 { 429 return null; 430 } 431 else 432 { 433 return defaultValues.get(0); 434 } 435 } 436 else 437 { 438 return values.get(0).getFirst(); 439 } 440 } 441 442 443 444 /** 445 * Retrieves the set of values for this argument. 446 * 447 * @return The set of values for this argument. 448 */ 449 @NotNull() 450 public List<Date> getValues() 451 { 452 if (values.isEmpty() && (defaultValues != null)) 453 { 454 return defaultValues; 455 } 456 457 final ArrayList<Date> dateList = new ArrayList<>(values.size()); 458 for (final ObjectPair<Date,String> p : values) 459 { 460 dateList.add(p.getFirst()); 461 } 462 463 return Collections.unmodifiableList(dateList); 464 } 465 466 467 468 /** 469 * Retrieves a string representation of the value for this argument, or a 470 * string representation of the default value if none was provided. If there 471 * are multiple values, then the first will be returned. 472 * 473 * @return The string representation of the value for this argument, or the 474 * string representation of the default value if none was provided, 475 * or {@code null} if there is no value and no default value. 476 */ 477 @Nullable() 478 public String getStringValue() 479 { 480 if (! values.isEmpty()) 481 { 482 return values.get(0).getSecond(); 483 } 484 485 if ((defaultValues != null) && (! defaultValues.isEmpty())) 486 { 487 return StaticUtils.encodeGeneralizedTime(defaultValues.get(0)); 488 } 489 490 return null; 491 } 492 493 494 495 /** 496 * {@inheritDoc} 497 */ 498 @Override() 499 @NotNull() 500 public List<String> getValueStringRepresentations(final boolean useDefault) 501 { 502 if (! values.isEmpty()) 503 { 504 final ArrayList<String> valueStrings = new ArrayList<>(values.size()); 505 for (final ObjectPair<Date,String> p : values) 506 { 507 valueStrings.add(p.getSecond()); 508 } 509 510 return Collections.unmodifiableList(valueStrings); 511 } 512 513 if (useDefault && (defaultValues != null) && (! defaultValues.isEmpty())) 514 { 515 final ArrayList<String> valueStrings = 516 new ArrayList<>(defaultValues.size()); 517 for (final Date d : defaultValues) 518 { 519 valueStrings.add(StaticUtils.encodeGeneralizedTime(d)); 520 } 521 522 return Collections.unmodifiableList(valueStrings); 523 } 524 525 return Collections.emptyList(); 526 } 527 528 529 530 /** 531 * {@inheritDoc} 532 */ 533 @Override() 534 protected boolean hasDefaultValue() 535 { 536 return ((defaultValues != null) && (! defaultValues.isEmpty())); 537 } 538 539 540 541 /** 542 * {@inheritDoc} 543 */ 544 @Override() 545 @NotNull() 546 public String getDataTypeName() 547 { 548 return INFO_TIMESTAMP_TYPE_NAME.get(); 549 } 550 551 552 553 /** 554 * {@inheritDoc} 555 */ 556 @Override() 557 @NotNull() 558 public String getValueConstraints() 559 { 560 return INFO_TIMESTAMP_CONSTRAINTS.get(); 561 } 562 563 564 565 /** 566 * {@inheritDoc} 567 */ 568 @Override() 569 protected void reset() 570 { 571 super.reset(); 572 values.clear(); 573 } 574 575 576 577 /** 578 * {@inheritDoc} 579 */ 580 @Override() 581 @NotNull() 582 public TimestampArgument getCleanCopy() 583 { 584 return new TimestampArgument(this); 585 } 586 587 588 589 /** 590 * {@inheritDoc} 591 */ 592 @Override() 593 protected void addToCommandLine(@NotNull final List<String> argStrings) 594 { 595 for (final ObjectPair<Date,String> p : values) 596 { 597 argStrings.add(getIdentifierString()); 598 if (isSensitive()) 599 { 600 argStrings.add("***REDACTED***"); 601 } 602 else 603 { 604 argStrings.add(p.getSecond()); 605 } 606 } 607 } 608 609 610 611 /** 612 * {@inheritDoc} 613 */ 614 @Override() 615 public void toString(@NotNull final StringBuilder buffer) 616 { 617 buffer.append("TimestampArgument("); 618 appendBasicToStringInfo(buffer); 619 620 if ((defaultValues != null) && (! defaultValues.isEmpty())) 621 { 622 if (defaultValues.size() == 1) 623 { 624 buffer.append(", defaultValue='"); 625 buffer.append(StaticUtils.encodeGeneralizedTime(defaultValues.get(0))); 626 } 627 else 628 { 629 buffer.append(", defaultValues={"); 630 631 final Iterator<Date> iterator = defaultValues.iterator(); 632 while (iterator.hasNext()) 633 { 634 buffer.append('\''); 635 buffer.append(StaticUtils.encodeGeneralizedTime(iterator.next())); 636 buffer.append('\''); 637 638 if (iterator.hasNext()) 639 { 640 buffer.append(", "); 641 } 642 } 643 644 buffer.append('}'); 645 } 646 } 647 648 buffer.append(')'); 649 } 650}