001package org.unix4j.unix.find; 002 003import java.util.List; 004import java.util.Map; 005import java.util.Arrays; 006 007import org.unix4j.command.Arguments; 008import org.unix4j.context.ExecutionContext; 009import org.unix4j.convert.ValueConverter; 010import org.unix4j.option.DefaultOptionSet; 011import org.unix4j.util.ArgsUtil; 012import org.unix4j.util.ArrayUtil; 013import org.unix4j.variable.Arg; 014import org.unix4j.variable.VariableContext; 015 016import org.unix4j.unix.Find; 017 018/** 019 * Arguments and options for the {@link Find find} command. 020 */ 021public final class FindArguments implements Arguments<FindArguments> { 022 023 private final FindOptions options; 024 025 026 // operand: <path> 027 private String path; 028 private boolean pathIsSet = false; 029 030 // operand: <name> 031 private String name; 032 private boolean nameIsSet = false; 033 034 // operand: <size> 035 private long size; 036 private boolean sizeIsSet = false; 037 038 // operand: <time> 039 private java.util.Date time; 040 private boolean timeIsSet = false; 041 042 // operand: <args> 043 private String[] args; 044 private boolean argsIsSet = false; 045 046 /** 047 * Constructor to use if no options are specified. 048 */ 049 public FindArguments() { 050 this.options = FindOptions.EMPTY; 051 } 052 053 /** 054 * Constructor with option set containing the selected command options. 055 * 056 * @param options the selected options 057 * @throws NullPointerException if the argument is null 058 */ 059 public FindArguments(FindOptions options) { 060 if (options == null) { 061 throw new NullPointerException("options argument cannot be null"); 062 } 063 this.options = options; 064 } 065 066 /** 067 * Returns the options set containing the selected command options. Returns 068 * an empty options set if no option has been selected. 069 * 070 * @return set with the selected options 071 */ 072 public FindOptions getOptions() { 073 return options; 074 } 075 076 /** 077 * Constructor string arguments encoding options and arguments, possibly 078 * also containing variable expressions. 079 * 080 * @param args string arguments for the command 081 * @throws NullPointerException if args is null 082 */ 083 public FindArguments(String... args) { 084 this(); 085 this.args = args; 086 this.argsIsSet = true; 087 } 088 private Object[] resolveVariables(VariableContext context, String... unresolved) { 089 final Object[] resolved = new Object[unresolved.length]; 090 for (int i = 0; i < resolved.length; i++) { 091 final String expression = unresolved[i]; 092 if (Arg.isVariable(expression)) { 093 resolved[i] = resolveVariable(context, expression); 094 } else { 095 resolved[i] = expression; 096 } 097 } 098 return resolved; 099 } 100 private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) { 101 if (values.size() == 1) { 102 final Object value = values.get(0); 103 return convert(context, operandName, operandType, value); 104 } 105 return convert(context, operandName, operandType, values); 106 } 107 108 private Object resolveVariable(VariableContext context, String variable) { 109 final Object value = context.getValue(variable); 110 if (value != null) { 111 return value; 112 } 113 throw new IllegalArgumentException("cannot resolve variable " + variable + 114 " in command: find " + this); 115 } 116 private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) { 117 final ValueConverter<V> converter = context.getValueConverterFor(operandType); 118 final V convertedValue; 119 if (converter != null) { 120 convertedValue = converter.convert(value); 121 } else { 122 if (FindOptions.class.equals(operandType)) { 123 convertedValue = operandType.cast(FindOptions.CONVERTER.convert(value)); 124 } else { 125 convertedValue = null; 126 } 127 } 128 if (convertedValue != null) { 129 return convertedValue; 130 } 131 throw new IllegalArgumentException("cannot convert --" + operandName + 132 " value '" + value + "' into the type " + operandType.getName() + 133 " for find command"); 134 } 135 136 @Override 137 public FindArguments getForContext(ExecutionContext context) { 138 if (context == null) { 139 throw new NullPointerException("context cannot be null"); 140 } 141 if (!argsIsSet || args.length == 0) { 142 //nothing to resolve 143 return this; 144 } 145 146 //check if there is at least one variable 147 boolean hasVariable = false; 148 for (final String arg : args) { 149 if (arg != null && arg.startsWith("$")) { 150 hasVariable = true; 151 break; 152 } 153 } 154 //resolve variables 155 final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args; 156 157 //convert now 158 final List<String> defaultOperands = Arrays.asList("path"); 159 final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs); 160 final FindOptions.Default options = new FindOptions.Default(); 161 final FindArguments argsForContext = new FindArguments(options); 162 for (final Map.Entry<String, List<Object>> e : map.entrySet()) { 163 if ("path".equals(e.getKey())) { 164 165 final String value = convertList(context, "path", String.class, e.getValue()); 166 argsForContext.setPath(value); 167 } else if ("name".equals(e.getKey())) { 168 169 final String value = convertList(context, "name", String.class, e.getValue()); 170 argsForContext.setName(value); 171 } else if ("size".equals(e.getKey())) { 172 173 final long value = convertList(context, "size", long.class, e.getValue()); 174 argsForContext.setSize(value); 175 } else if ("time".equals(e.getKey())) { 176 177 final java.util.Date value = convertList(context, "time", java.util.Date.class, e.getValue()); 178 argsForContext.setTime(value); 179 } else if ("args".equals(e.getKey())) { 180 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in find command args: " + Arrays.toString(args)); 181 } else if ("options".equals(e.getKey())) { 182 183 final FindOptions value = convertList(context, "options", FindOptions.class, e.getValue()); 184 options.setAll(value); 185 } else { 186 throw new IllegalStateException("invalid operand '" + e.getKey() + "' in find command args: " + Arrays.toString(args)); 187 } 188 } 189 return argsForContext; 190 } 191 192 /** 193 * Returns the {@code <path>} operand value (variables are NOT resolved): Starting point for the search in the directory hierarchy; 194 wildcards * and ? are supported; relative paths are resolved on the 195 basis of the current working directory. 196 * 197 * @return the {@code <path>} operand value (variables are not resolved) 198 * @throws IllegalStateException if this operand has never been set 199 * @see #getPath(ExecutionContext) 200 */ 201 public String getPath() { 202 if (pathIsSet) { 203 return path; 204 } 205 throw new IllegalStateException("operand has not been set: " + path); 206 } 207 /** 208 * Returns the {@code <path>} (variables are resolved): Starting point for the search in the directory hierarchy; 209 wildcards * and ? are supported; relative paths are resolved on the 210 basis of the current working directory. 211 * 212 * @param context the execution context used to resolve variables 213 * @return the {@code <path>} operand value after resolving variables 214 * @throws IllegalStateException if this operand has never been set 215 * @see #getPath() 216 */ 217 public String getPath(ExecutionContext context) { 218 final String value = getPath(); 219 if (Arg.isVariable(value)) { 220 final Object resolved = resolveVariable(context.getVariableContext(), value); 221 final String converted = convert(context, "path", String.class, resolved); 222 return converted; 223 } 224 return value; 225 } 226 227 /** 228 * Returns true if the {@code <path>} operand has been set. 229 * <p> 230 * Note that this method returns true even if {@code null} was passed to the 231 * {@link #setPath(String)} method. 232 * 233 * @return true if the setter for the {@code <path>} operand has 234 * been called at least once 235 */ 236 public boolean isPathSet() { 237 return pathIsSet; 238 } 239 /** 240 * Sets {@code <path>}: Starting point for the search in the directory hierarchy; 241 wildcards * and ? are supported; relative paths are resolved on the 242 basis of the current working directory. 243 * 244 * @param path the value for the {@code <path>} operand 245 */ 246 public void setPath(String path) { 247 this.path = path; 248 this.pathIsSet = true; 249 } 250 /** 251 * Returns the {@code <name>} operand value (variables are NOT resolved): Name pattern to match the file name after removing the path with the 252 leading directories; wildcards * and ? are supported, or full 253 regular expressions if either of the options {@code -regex (-r)} or 254 {@code -iregex (-i)} is specified. 255 * 256 * @return the {@code <name>} operand value (variables are not resolved) 257 * @throws IllegalStateException if this operand has never been set 258 * @see #getName(ExecutionContext) 259 */ 260 public String getName() { 261 if (nameIsSet) { 262 return name; 263 } 264 throw new IllegalStateException("operand has not been set: " + name); 265 } 266 /** 267 * Returns the {@code <name>} (variables are resolved): Name pattern to match the file name after removing the path with the 268 leading directories; wildcards * and ? are supported, or full 269 regular expressions if either of the options {@code -regex (-r)} or 270 {@code -iregex (-i)} is specified. 271 * 272 * @param context the execution context used to resolve variables 273 * @return the {@code <name>} operand value after resolving variables 274 * @throws IllegalStateException if this operand has never been set 275 * @see #getName() 276 */ 277 public String getName(ExecutionContext context) { 278 final String value = getName(); 279 if (Arg.isVariable(value)) { 280 final Object resolved = resolveVariable(context.getVariableContext(), value); 281 final String converted = convert(context, "name", String.class, resolved); 282 return converted; 283 } 284 return value; 285 } 286 287 /** 288 * Returns true if the {@code <name>} operand has been set. 289 * <p> 290 * Note that this method returns true even if {@code null} was passed to the 291 * {@link #setName(String)} method. 292 * 293 * @return true if the setter for the {@code <name>} operand has 294 * been called at least once 295 */ 296 public boolean isNameSet() { 297 return nameIsSet; 298 } 299 /** 300 * Sets {@code <name>}: Name pattern to match the file name after removing the path with the 301 leading directories; wildcards * and ? are supported, or full 302 regular expressions if either of the options {@code -regex (-r)} or 303 {@code -iregex (-i)} is specified. 304 * 305 * @param name the value for the {@code <name>} operand 306 */ 307 public void setName(String name) { 308 this.name = name; 309 this.nameIsSet = true; 310 } 311 /** 312 * Returns the {@code <size>} operand value: Consider only files using at least {@code size} bytes if {@code size} 313 is positive, or at most {@code abs(size)} bytes if {@code size} is zero 314 or negative. 315 * 316 * @return the {@code <size>} operand value (variables are not resolved) 317 * @throws IllegalStateException if this operand has never been set 318 * 319 */ 320 public long getSize() { 321 if (sizeIsSet) { 322 return size; 323 } 324 throw new IllegalStateException("operand has not been set: " + size); 325 } 326 327 /** 328 * Returns true if the {@code <size>} operand has been set. 329 * <p> 330 * Note that this method returns true even if {@code null} was passed to the 331 * {@link #setSize(long)} method. 332 * 333 * @return true if the setter for the {@code <size>} operand has 334 * been called at least once 335 */ 336 public boolean isSizeSet() { 337 return sizeIsSet; 338 } 339 /** 340 * Sets {@code <size>}: Consider only files using at least {@code size} bytes if {@code size} 341 is positive, or at most {@code abs(size)} bytes if {@code size} is zero 342 or negative. 343 * 344 * @param size the value for the {@code <size>} operand 345 */ 346 public void setSize(long size) { 347 this.size = size; 348 this.sizeIsSet = true; 349 } 350 /** 351 * Returns the {@code <time>} operand value: Consider only files that have been created, modified or accessed 352 before or after the specified {@code time} operand; consider the 353 {@code -time...} options for details of the comparison. 354 * 355 * @return the {@code <time>} operand value (variables are not resolved) 356 * @throws IllegalStateException if this operand has never been set 357 * 358 */ 359 public java.util.Date getTime() { 360 if (timeIsSet) { 361 return time; 362 } 363 throw new IllegalStateException("operand has not been set: " + time); 364 } 365 366 /** 367 * Returns true if the {@code <time>} operand has been set. 368 * <p> 369 * Note that this method returns true even if {@code null} was passed to the 370 * {@link #setTime(java.util.Date)} method. 371 * 372 * @return true if the setter for the {@code <time>} operand has 373 * been called at least once 374 */ 375 public boolean isTimeSet() { 376 return timeIsSet; 377 } 378 /** 379 * Sets {@code <time>}: Consider only files that have been created, modified or accessed 380 before or after the specified {@code time} operand; consider the 381 {@code -time...} options for details of the comparison. 382 * 383 * @param time the value for the {@code <time>} operand 384 */ 385 public void setTime(java.util.Date time) { 386 this.time = time; 387 this.timeIsSet = true; 388 } 389 /** 390 * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 391 Options can be specified by acronym (with a leading dash "-") or by 392 long name (with two leading dashes "--"). Operands other than the 393 default "--path" operand have to be prefixed with the operand name 394 (e.g. "--name" for subsequent path operand values). 395 * 396 * @return the {@code <args>} operand value (variables are not resolved) 397 * @throws IllegalStateException if this operand has never been set 398 * 399 */ 400 public String[] getArgs() { 401 if (argsIsSet) { 402 return args; 403 } 404 throw new IllegalStateException("operand has not been set: " + args); 405 } 406 407 /** 408 * Returns true if the {@code <args>} operand has been set. 409 * 410 * @return true if the setter for the {@code <args>} operand has 411 * been called at least once 412 */ 413 public boolean isArgsSet() { 414 return argsIsSet; 415 } 416 417 /** 418 * Returns true if the {@code --}{@link FindOption#typeDirectory typeDirectory} option 419 * is set. The option is also known as {@code -}d option. 420 * <p> 421 * Description: Consider only directories 422 * 423 * @return true if the {@code --typeDirectory} or {@code -d} option is set 424 */ 425 public boolean isTypeDirectory() { 426 return getOptions().isSet(FindOption.typeDirectory); 427 } 428 /** 429 * Returns true if the {@code --}{@link FindOption#typeFile typeFile} option 430 * is set. The option is also known as {@code -}f option. 431 * <p> 432 * Description: Consider only regular files 433 * 434 * @return true if the {@code --typeFile} or {@code -f} option is set 435 */ 436 public boolean isTypeFile() { 437 return getOptions().isSet(FindOption.typeFile); 438 } 439 /** 440 * Returns true if the {@code --}{@link FindOption#typeSymlink typeSymlink} option 441 * is set. The option is also known as {@code -}l option. 442 * <p> 443 * Description: Consider only symbolic links 444 * 445 * @return true if the {@code --typeSymlink} or {@code -l} option is set 446 */ 447 public boolean isTypeSymlink() { 448 return getOptions().isSet(FindOption.typeSymlink); 449 } 450 /** 451 * Returns true if the {@code --}{@link FindOption#typeOther typeOther} option 452 * is set. The option is also known as {@code -}x option. 453 * <p> 454 * Description: Consider only files that are neither of directory (d), 455 regular file (f) or symlink (l). 456 * 457 * @return true if the {@code --typeOther} or {@code -x} option is set 458 */ 459 public boolean isTypeOther() { 460 return getOptions().isSet(FindOption.typeOther); 461 } 462 /** 463 * Returns true if the {@code --}{@link FindOption#regex regex} option 464 * is set. The option is also known as {@code -}r option. 465 * <p> 466 * Description: Use full regular expression syntax for the patterns specified by the 467 name operand 468<p> 469 (This option is ignored if no name operand is specified). 470 * 471 * @return true if the {@code --regex} or {@code -r} option is set 472 */ 473 public boolean isRegex() { 474 return getOptions().isSet(FindOption.regex); 475 } 476 /** 477 * Returns true if the {@code --}{@link FindOption#ignoreCase ignoreCase} option 478 * is set. The option is also known as {@code -}i option. 479 * <p> 480 * Description: Use case insensitive matching when applying the file name pattern 481 specified by the name operand 482<p> 483 (This option is ignored if no name operand is specified). 484 * 485 * @return true if the {@code --ignoreCase} or {@code -i} option is set 486 */ 487 public boolean isIgnoreCase() { 488 return getOptions().isSet(FindOption.ignoreCase); 489 } 490 /** 491 * Returns true if the {@code --}{@link FindOption#timeNewer timeNewer} option 492 * is set. The option is also known as {@code -}n option. 493 * <p> 494 * Description: Consider only files that have been created, modified or accessed 495 after or at the time specified by the time operand (the default) 496 <p> 497 (This option is ignored if no time operand is specified). 498 * 499 * @return true if the {@code --timeNewer} or {@code -n} option is set 500 */ 501 public boolean isTimeNewer() { 502 return getOptions().isSet(FindOption.timeNewer); 503 } 504 /** 505 * Returns true if the {@code --}{@link FindOption#timeOlder timeOlder} option 506 * is set. The option is also known as {@code -}o option. 507 * <p> 508 * Description: Consider only files that have been created, modified or accessed 509 before or at the time specified by the time operand 510 <p> 511 (This option is ignored if no time operand is specified). 512 * 513 * @return true if the {@code --timeOlder} or {@code -o} option is set 514 */ 515 public boolean isTimeOlder() { 516 return getOptions().isSet(FindOption.timeOlder); 517 } 518 /** 519 * Returns true if the {@code --}{@link FindOption#timeCreate timeCreate} option 520 * is set. The option is also known as {@code -}c option. 521 * <p> 522 * Description: The time operand refers to the creation time of the file 523 <p> 524 (This option is ignored if no time operand is specified). 525 * 526 * @return true if the {@code --timeCreate} or {@code -c} option is set 527 */ 528 public boolean isTimeCreate() { 529 return getOptions().isSet(FindOption.timeCreate); 530 } 531 /** 532 * Returns true if the {@code --}{@link FindOption#timeAccess timeAccess} option 533 * is set. The option is also known as {@code -}a option. 534 * <p> 535 * Description: The time operand refers to the last access time of the file 536 <p> 537 (This option is ignored if no time operand is specified). 538 * 539 * @return true if the {@code --timeAccess} or {@code -a} option is set 540 */ 541 public boolean isTimeAccess() { 542 return getOptions().isSet(FindOption.timeAccess); 543 } 544 /** 545 * Returns true if the {@code --}{@link FindOption#timeModified timeModified} option 546 * is set. The option is also known as {@code -}m option. 547 * <p> 548 * Description: The time operand refers to the last modification time of the file 549 (the default) 550 <p> 551 (This option is ignored if no time operand is specified). 552 * 553 * @return true if the {@code --timeModified} or {@code -m} option is set 554 */ 555 public boolean isTimeModified() { 556 return getOptions().isSet(FindOption.timeModified); 557 } 558 /** 559 * Returns true if the {@code --}{@link FindOption#print0 print0} option 560 * is set. The option is also known as {@code -}z option. 561 * <p> 562 * Description: Print the full file name on the standard output, followed by a null 563 character (instead of the newline character used by default). This 564 allows file names that contain newlines or other types of white 565 space to be correctly interpreted by programs that process the find 566 output. This option corresponds to the --delimiter0 option of xargs. 567 * 568 * @return true if the {@code --print0} or {@code -z} option is set 569 */ 570 public boolean isPrint0() { 571 return getOptions().isSet(FindOption.print0); 572 } 573 574 @Override 575 public String toString() { 576 // ok, we have options or arguments or both 577 final StringBuilder sb = new StringBuilder(); 578 579 if (argsIsSet) { 580 for (String arg : args) { 581 if (sb.length() > 0) sb.append(' '); 582 sb.append(arg); 583 } 584 } else { 585 586 // first the options 587 if (options.size() > 0) { 588 sb.append(DefaultOptionSet.toString(options)); 589 } 590 // operand: <path> 591 if (pathIsSet) { 592 if (sb.length() > 0) sb.append(' '); 593 sb.append("--").append("path"); 594 sb.append(" ").append(toString(getPath())); 595 } 596 // operand: <name> 597 if (nameIsSet) { 598 if (sb.length() > 0) sb.append(' '); 599 sb.append("--").append("name"); 600 sb.append(" ").append(toString(getName())); 601 } 602 // operand: <size> 603 if (sizeIsSet) { 604 if (sb.length() > 0) sb.append(' '); 605 sb.append("--").append("size"); 606 sb.append(" ").append(toString(getSize())); 607 } 608 // operand: <time> 609 if (timeIsSet) { 610 if (sb.length() > 0) sb.append(' '); 611 sb.append("--").append("time"); 612 sb.append(" ").append(toString(getTime())); 613 } 614 // operand: <args> 615 if (argsIsSet) { 616 if (sb.length() > 0) sb.append(' '); 617 sb.append("--").append("args"); 618 sb.append(" ").append(toString(getArgs())); 619 } 620 } 621 622 return sb.toString(); 623 } 624 private static String toString(Object value) { 625 if (value != null && value.getClass().isArray()) { 626 return ArrayUtil.toString(value); 627 } 628 return String.valueOf(value); 629 } 630}