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}