001package org.unix4j.unix.sort;
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.Sort;
017
018/**
019 * Arguments and options for the {@link Sort sort} command.
020 */
021public final class SortArguments implements Arguments<SortArguments> {
022        
023        private final SortOptions options;
024
025        
026        // operand: <paths>
027        private String[] paths;
028        private boolean pathsIsSet = false;
029        
030        // operand: <files>
031        private java.io.File[] files;
032        private boolean filesIsSet = false;
033        
034        // operand: <comparator>
035        private java.util.Comparator<? super org.unix4j.line.Line> comparator;
036        private boolean comparatorIsSet = false;
037        
038        // operand: <args>
039        private String[] args;
040        private boolean argsIsSet = false;
041        
042        /**
043         * Constructor to use if no options are specified.
044         */
045        public SortArguments() {
046                this.options = SortOptions.EMPTY;
047        }
048
049        /**
050         * Constructor with option set containing the selected command options.
051         * 
052         * @param options the selected options
053         * @throws NullPointerException if the argument is null
054         */
055        public SortArguments(SortOptions options) {
056                if (options == null) {
057                        throw new NullPointerException("options argument cannot be null");
058                }
059                this.options = options;
060        }
061        
062        /**
063         * Returns the options set containing the selected command options. Returns
064         * an empty options set if no option has been selected.
065         * 
066         * @return set with the selected options
067         */
068        public SortOptions getOptions() {
069                return options;
070        }
071
072        /**
073         * Constructor string arguments encoding options and arguments, possibly
074         * also containing variable expressions. 
075         * 
076         * @param args string arguments for the command
077         * @throws NullPointerException if args is null
078         */
079        public SortArguments(String... args) {
080                this();
081                this.args = args;
082                this.argsIsSet = true;
083        }
084        private Object[] resolveVariables(VariableContext context, String... unresolved) {
085                final Object[] resolved = new Object[unresolved.length];
086                for (int i = 0; i < resolved.length; i++) {
087                        final String expression = unresolved[i];
088                        if (Arg.isVariable(expression)) {
089                                resolved[i] = resolveVariable(context, expression);
090                        } else {
091                                resolved[i] = expression;
092                        }
093                }
094                return resolved;
095        }
096        private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) {
097                if (values.size() == 1) {
098                        final Object value = values.get(0);
099                        return convert(context, operandName, operandType, value);
100                }
101                return convert(context, operandName, operandType, values);
102        }
103
104        private Object resolveVariable(VariableContext context, String variable) {
105                final Object value = context.getValue(variable);
106                if (value != null) {
107                        return value;
108                }
109                throw new IllegalArgumentException("cannot resolve variable " + variable + 
110                                " in command: sort " + this);
111        }
112        private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) {
113                final ValueConverter<V> converter = context.getValueConverterFor(operandType);
114                final V convertedValue;
115                if (converter != null) {
116                        convertedValue = converter.convert(value);
117                } else {
118                        if (SortOptions.class.equals(operandType)) {
119                                convertedValue = operandType.cast(SortOptions.CONVERTER.convert(value));
120                        } else {
121                                convertedValue = null;
122                        }
123                }
124                if (convertedValue != null) {
125                        return convertedValue;
126                }
127                throw new IllegalArgumentException("cannot convert --" + operandName + 
128                                " value '" + value + "' into the type " + operandType.getName() + 
129                                " for sort command");
130        }
131        
132        @Override
133        public SortArguments getForContext(ExecutionContext context) {
134                if (context == null) {
135                        throw new NullPointerException("context cannot be null");
136                }
137                if (!argsIsSet || args.length == 0) {
138                        //nothing to resolve
139                        return this;
140                }
141
142                //check if there is at least one variable
143                boolean hasVariable = false;
144                for (final String arg : args) {
145                        if (arg != null && arg.startsWith("$")) {
146                                hasVariable = true;
147                                break;
148                        }
149                }
150                //resolve variables
151                final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args;
152                
153                //convert now
154                final List<String> defaultOperands = Arrays.asList("paths");
155                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
156                final SortOptions.Default options = new SortOptions.Default();
157                final SortArguments argsForContext = new SortArguments(options);
158                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
159                        if ("paths".equals(e.getKey())) {
160                                        
161                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
162                                argsForContext.setPaths(value);
163                        } else if ("files".equals(e.getKey())) {
164                                        
165                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
166                                argsForContext.setFiles(value);
167                        } else if ("comparator".equals(e.getKey())) {
168                                        @SuppressWarnings("unchecked")
169                                final java.util.Comparator<? super org.unix4j.line.Line> value = convertList(context, "comparator", (Class<java.util.Comparator<? super org.unix4j.line.Line>>)(Class<?>)java.util.Comparator.class, e.getValue());  
170                                argsForContext.setComparator(value);
171                        } else if ("args".equals(e.getKey())) {
172                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args));
173                        } else if ("options".equals(e.getKey())) {
174                                        
175                                final SortOptions value = convertList(context, "options", SortOptions.class, e.getValue());  
176                                options.setAll(value);
177                        } else {
178                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args));
179                        }
180                }
181                return argsForContext;
182        }
183        
184        /**
185         * Returns the {@code <paths>} operand value: Pathnames of the files to be sorted, merged, or checked; wildcards * 
186                        and ? are supported; relative paths are resolved on the
187            basis of the current working directory.
188         * 
189         * @return the {@code <paths>} operand value (variables are not resolved)
190         * @throws IllegalStateException if this operand has never been set
191         * 
192         */
193        public String[] getPaths() {
194                if (pathsIsSet) {
195                        return paths;
196                }
197                throw new IllegalStateException("operand has not been set: " + paths);
198        }
199
200        /**
201         * Returns true if the {@code <paths>} operand has been set. 
202         * <p>
203         * Note that this method returns true even if {@code null} was passed to the
204         * {@link #setPaths(String[])} method.
205         * 
206         * @return      true if the setter for the {@code <paths>} operand has 
207         *                      been called at least once
208         */
209        public boolean isPathsSet() {
210                return pathsIsSet;
211        }
212        /**
213         * Sets {@code <paths>}: Pathnames of the files to be sorted, merged, or checked; wildcards * 
214                        and ? are supported; relative paths are resolved on the
215            basis of the current working directory.
216         * 
217         * @param paths the value for the {@code <paths>} operand
218         */
219        public void setPaths(String... paths) {
220                this.paths = paths;
221                this.pathsIsSet = true;
222        }
223        /**
224         * Returns the {@code <files>} operand value: The files to be sorted or merged; relative paths are not resolved 
225                        (use the string paths argument to enable relative path resolving 
226                        based on the current working directory).
227         * 
228         * @return the {@code <files>} operand value (variables are not resolved)
229         * @throws IllegalStateException if this operand has never been set
230         * 
231         */
232        public java.io.File[] getFiles() {
233                if (filesIsSet) {
234                        return files;
235                }
236                throw new IllegalStateException("operand has not been set: " + files);
237        }
238
239        /**
240         * Returns true if the {@code <files>} operand has been set. 
241         * <p>
242         * Note that this method returns true even if {@code null} was passed to the
243         * {@link #setFiles(java.io.File[])} method.
244         * 
245         * @return      true if the setter for the {@code <files>} operand has 
246         *                      been called at least once
247         */
248        public boolean isFilesSet() {
249                return filesIsSet;
250        }
251        /**
252         * Sets {@code <files>}: The files to be sorted or merged; relative paths are not resolved 
253                        (use the string paths argument to enable relative path resolving 
254                        based on the current working directory).
255         * 
256         * @param files the value for the {@code <files>} operand
257         */
258        public void setFiles(java.io.File... files) {
259                this.files = files;
260                this.filesIsSet = true;
261        }
262        /**
263         * Returns the {@code <comparator>} operand value: The comparator to use for the line comparisons.
264         * 
265         * @return the {@code <comparator>} operand value (variables are not resolved)
266         * @throws IllegalStateException if this operand has never been set
267         * 
268         */
269        public java.util.Comparator<? super org.unix4j.line.Line> getComparator() {
270                if (comparatorIsSet) {
271                        return comparator;
272                }
273                throw new IllegalStateException("operand has not been set: " + comparator);
274        }
275
276        /**
277         * Returns true if the {@code <comparator>} operand has been set. 
278         * <p>
279         * Note that this method returns true even if {@code null} was passed to the
280         * {@link #setComparator(java.util.Comparator)} method.
281         * 
282         * @return      true if the setter for the {@code <comparator>} operand has 
283         *                      been called at least once
284         */
285        public boolean isComparatorSet() {
286                return comparatorIsSet;
287        }
288        /**
289         * Sets {@code <comparator>}: The comparator to use for the line comparisons.
290         * 
291         * @param comparator the value for the {@code <comparator>} operand
292         */
293        public void setComparator(java.util.Comparator<? super org.unix4j.line.Line> comparator) {
294                this.comparator = comparator;
295                this.comparatorIsSet = true;
296        }
297        /**
298         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
299                        Options can be specified by acronym (with a leading dash "-") or by 
300                        long name (with two leading dashes "--"). Operands other than the
301                        default "--paths" operand have to be prefixed with the operand 
302                        name (e.g. "--comparator" for a subsequent comparator operand value).
303         * 
304         * @return the {@code <args>} operand value (variables are not resolved)
305         * @throws IllegalStateException if this operand has never been set
306         * 
307         */
308        public String[] getArgs() {
309                if (argsIsSet) {
310                        return args;
311                }
312                throw new IllegalStateException("operand has not been set: " + args);
313        }
314
315        /**
316         * Returns true if the {@code <args>} operand has been set. 
317         * 
318         * @return      true if the setter for the {@code <args>} operand has 
319         *                      been called at least once
320         */
321        public boolean isArgsSet() {
322                return argsIsSet;
323        }
324        
325        /**
326         * Returns true if the {@code --}{@link SortOption#check check} option
327         * is set. The option is also known as {@code -}c option.
328         * <p>
329         * Description: Checks that the single input file is ordered as specified by the
330                        arguments and the collating sequence of the current locale. No 
331                        output is produced; only the exit code is affected.
332         * 
333         * @return true if the {@code --check} or {@code -c} option is set
334         */
335        public boolean isCheck() {
336                return getOptions().isSet(SortOption.check);
337        }
338        /**
339         * Returns true if the {@code --}{@link SortOption#merge merge} option
340         * is set. The option is also known as {@code -}m option.
341         * <p>
342         * Description: Merge only; the input file are assumed to be already sorted.
343         * 
344         * @return true if the {@code --merge} or {@code -m} option is set
345         */
346        public boolean isMerge() {
347                return getOptions().isSet(SortOption.merge);
348        }
349        /**
350         * Returns true if the {@code --}{@link SortOption#unique unique} option
351         * is set. The option is also known as {@code -}u option.
352         * <p>
353         * Description: Unique: suppress all but one in each set of lines having equal keys.
354                        If used with the {@code -c} option, checks that there are no lines 
355                        with duplicate keys, in addition to checking that the input file is 
356                        sorted.
357         * 
358         * @return true if the {@code --unique} or {@code -u} option is set
359         */
360        public boolean isUnique() {
361                return getOptions().isSet(SortOption.unique);
362        }
363        /**
364         * Returns true if the {@code --}{@link SortOption#ignoreLeadingBlanks ignoreLeadingBlanks} option
365         * is set. The option is also known as {@code -}b option.
366         * <p>
367         * Description: Ignore leading blanks. 
368                        (This option is ignored if a comparator operand is present).
369         * 
370         * @return true if the {@code --ignoreLeadingBlanks} or {@code -b} option is set
371         */
372        public boolean isIgnoreLeadingBlanks() {
373                return getOptions().isSet(SortOption.ignoreLeadingBlanks);
374        }
375        /**
376         * Returns true if the {@code --}{@link SortOption#dictionaryOrder dictionaryOrder} option
377         * is set. The option is also known as {@code -}d option.
378         * <p>
379         * Description: Consider only blanks and alphanumeric characters.
380                        (This option is ignored if a comparator operand is present).
381         * 
382         * @return true if the {@code --dictionaryOrder} or {@code -d} option is set
383         */
384        public boolean isDictionaryOrder() {
385                return getOptions().isSet(SortOption.dictionaryOrder);
386        }
387        /**
388         * Returns true if the {@code --}{@link SortOption#ignoreCase ignoreCase} option
389         * is set. The option is also known as {@code -}f option.
390         * <p>
391         * Description: Consider all lowercase characters that have uppercase equivalents to
392                        be the uppercase equivalent for the purposes of comparison.
393                        (This option is ignored if a comparator operand is present).
394         * 
395         * @return true if the {@code --ignoreCase} or {@code -f} option is set
396         */
397        public boolean isIgnoreCase() {
398                return getOptions().isSet(SortOption.ignoreCase);
399        }
400        /**
401         * Returns true if the {@code --}{@link SortOption#numericSort numericSort} option
402         * is set. The option is also known as {@code -}n option.
403         * <p>
404         * Description: Sort numerically; the number begins each line and consists of 
405                        optional blanks, an optional minus sign, and zero or more digits
406                        possibly separated by thousands separators, optionally followed by a
407                        decimal-point character and zero or more digits. An empty number is
408                        treated as '0'. The current local specifies the decimal-point 
409                        character and thousands separator.
410                        <p>
411                        Comparison is exact; there is no rounding error.
412                        <p>
413                        Neither a leading '+' nor exponential notation is recognized. To 
414                        compare such strings numerically, use the
415                        {@code -genericNumericSort (-g)} option. 
416<p>
417                        (This option is ignored if a comparator operand is present).
418         * 
419         * @return true if the {@code --numericSort} or {@code -n} option is set
420         */
421        public boolean isNumericSort() {
422                return getOptions().isSet(SortOption.numericSort);
423        }
424        /**
425         * Returns true if the {@code --}{@link SortOption#generalNumericSort generalNumericSort} option
426         * is set. The option is also known as {@code -}g option.
427         * <p>
428         * Description: Sort numerically, using the standard {@link Double#parseDouble(String)}  
429                        function to convert a trimmed line to a double-precision floating 
430                        point number. This allows floating point numbers to be specified in 
431                        scientific notation, like 1.0e-34 and 10e100. 
432                        <p>
433                        Uses the following collating sequence: Lines that cannot be parsed 
434                        because they do not represent valid double values (in alpha-numeric
435                        order); "-Infinity"; finite numbers in ascending numeric order 
436                        (with -0 < +0); "Infinity"; "NaN".
437<p>
438                        This option is usually slower than {@code -numeric-sort (-n)} and it
439                        can lose information when converting to floating point.         
440                <p>
441                        (This option is ignored if a comparator operand is present).
442         * 
443         * @return true if the {@code --generalNumericSort} or {@code -g} option is set
444         */
445        public boolean isGeneralNumericSort() {
446                return getOptions().isSet(SortOption.generalNumericSort);
447        }
448        /**
449         * Returns true if the {@code --}{@link SortOption#humanNumericSort humanNumericSort} option
450         * is set. The option is also known as {@code -}h option.
451         * <p>
452         * Description: Sort numerically, first by numeric sign (negative, zero, or 
453                        positive); then by SI suffix (either empty, or 'k' or 'K', or one 
454                        of 'MGTPEZY', in that order); and finally by numeric value. For
455                        example, '1023M' sorts before '1G' because 'M' (mega) precedes 'G' 
456                        (giga) as an SI suffix. 
457                        <p>
458                        This option sorts values that are consistently scaled to the nearest
459                        suffix, regardless of whether suffixes denote powers of 1000 or
460                        1024, and it therefore sorts the output of any single invocation of 
461                        the {@code ls} command that are invoked with the --human-readable 
462                        option. 
463                        <p>
464                        The syntax for numbers is the same as for the
465                        {@code --numericSort (-n)} option; the SI suffix must immediately 
466                        follow the number.              
467<p>
468                        (This option is ignored if a comparator operand is present).
469         * 
470         * @return true if the {@code --humanNumericSort} or {@code -h} option is set
471         */
472        public boolean isHumanNumericSort() {
473                return getOptions().isSet(SortOption.humanNumericSort);
474        }
475        /**
476         * Returns true if the {@code --}{@link SortOption#monthSort monthSort} option
477         * is set. The option is also known as {@code -}M option.
478         * <p>
479         * Description: An initial string, consisting of any amount of blanks, followed by a
480                        month name abbreviation, is folded to UPPER case and compared in the
481                        order: (unknown) < 'JAN' < ... < 'DEC'. The current locale
482                        determines the month spellings.
483         * 
484         * @return true if the {@code --monthSort} or {@code -M} option is set
485         */
486        public boolean isMonthSort() {
487                return getOptions().isSet(SortOption.monthSort);
488        }
489        /**
490         * Returns true if the {@code --}{@link SortOption#versionSort versionSort} option
491         * is set. The option is also known as {@code -}V option.
492         * <p>
493         * Description: Sort by version name and number. It behaves like a standard sort, 
494                        except that each sequence of decimal digits is treated numerically 
495                        as an index/version number.
496                        <p>
497                        (This option is ignored if a comparator operand is present).
498         * 
499         * @return true if the {@code --versionSort} or {@code -V} option is set
500         */
501        public boolean isVersionSort() {
502                return getOptions().isSet(SortOption.versionSort);
503        }
504        /**
505         * Returns true if the {@code --}{@link SortOption#reverse reverse} option
506         * is set. The option is also known as {@code -}r option.
507         * <p>
508         * Description: Reverse the sense of comparisons.
509         * 
510         * @return true if the {@code --reverse} or {@code -r} option is set
511         */
512        public boolean isReverse() {
513                return getOptions().isSet(SortOption.reverse);
514        }
515
516        @Override
517        public String toString() {
518                // ok, we have options or arguments or both
519                final StringBuilder sb = new StringBuilder();
520
521                if (argsIsSet) {
522                        for (String arg : args) {
523                                if (sb.length() > 0) sb.append(' ');
524                                sb.append(arg);
525                        }
526                } else {
527                
528                        // first the options
529                        if (options.size() > 0) {
530                                sb.append(DefaultOptionSet.toString(options));
531                        }
532                        // operand: <paths>
533                        if (pathsIsSet) {
534                                if (sb.length() > 0) sb.append(' ');
535                                sb.append("--").append("paths");
536                                sb.append(" ").append(toString(getPaths()));
537                        }
538                        // operand: <files>
539                        if (filesIsSet) {
540                                if (sb.length() > 0) sb.append(' ');
541                                sb.append("--").append("files");
542                                sb.append(" ").append(toString(getFiles()));
543                        }
544                        // operand: <comparator>
545                        if (comparatorIsSet) {
546                                if (sb.length() > 0) sb.append(' ');
547                                sb.append("--").append("comparator");
548                                sb.append(" ").append(toString(getComparator()));
549                        }
550                        // operand: <args>
551                        if (argsIsSet) {
552                                if (sb.length() > 0) sb.append(' ');
553                                sb.append("--").append("args");
554                                sb.append(" ").append(toString(getArgs()));
555                        }
556                }
557                
558                return sb.toString();
559        }
560        private static String toString(Object value) {
561                if (value != null && value.getClass().isArray()) {
562                        return ArrayUtil.toString(value);
563                }
564                return String.valueOf(value);
565        }
566}