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: <inputs>
035        private org.unix4j.io.Input[] inputs;
036        private boolean inputsIsSet = false;
037        
038        // operand: <comparator>
039        private java.util.Comparator<? super org.unix4j.line.Line> comparator;
040        private boolean comparatorIsSet = 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 SortArguments() {
050                this.options = SortOptions.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 SortArguments(SortOptions 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 SortOptions 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 SortArguments(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: sort " + 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 (SortOptions.class.equals(operandType)) {
123                                convertedValue = operandType.cast(SortOptions.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 sort command");
134        }
135        
136        @Override
137        public SortArguments 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("paths");
159                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
160                final SortOptions.Default options = new SortOptions.Default();
161                final SortArguments argsForContext = new SortArguments(options);
162                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
163                        if ("paths".equals(e.getKey())) {
164                                        
165                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
166                                argsForContext.setPaths(value);
167                        } else if ("files".equals(e.getKey())) {
168                                        
169                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
170                                argsForContext.setFiles(value);
171                        } else if ("inputs".equals(e.getKey())) {
172                                        
173                                final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue());  
174                                argsForContext.setInputs(value);
175                        } else if ("comparator".equals(e.getKey())) {
176                                        @SuppressWarnings("unchecked")
177                                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());  
178                                argsForContext.setComparator(value);
179                        } else if ("args".equals(e.getKey())) {
180                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args));
181                        } else if ("options".equals(e.getKey())) {
182                                        
183                                final SortOptions value = convertList(context, "options", SortOptions.class, e.getValue());  
184                                options.setAll(value);
185                        } else {
186                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in sort command args: " + Arrays.toString(args));
187                        }
188                }
189                return argsForContext;
190        }
191        
192        /**
193         * Returns the {@code <paths>} operand value: Path names of the files to be sorted, merged, or checked; wildcards *
194                        and ? are supported; relative paths are resolved on the
195            basis of the current working directory.
196         * 
197         * @return the {@code <paths>} operand value (variables are not resolved)
198         * @throws IllegalStateException if this operand has never been set
199         * 
200         */
201        public String[] getPaths() {
202                if (pathsIsSet) {
203                        return paths;
204                }
205                throw new IllegalStateException("operand has not been set: " + paths);
206        }
207
208        /**
209         * Returns true if the {@code <paths>} operand has been set. 
210         * <p>
211         * Note that this method returns true even if {@code null} was passed to the
212         * {@link #setPaths(String[])} method.
213         * 
214         * @return      true if the setter for the {@code <paths>} operand has 
215         *                      been called at least once
216         */
217        public boolean isPathsSet() {
218                return pathsIsSet;
219        }
220        /**
221         * Sets {@code <paths>}: Path names of the files to be sorted, merged, or checked; wildcards *
222                        and ? are supported; relative paths are resolved on the
223            basis of the current working directory.
224         * 
225         * @param paths the value for the {@code <paths>} operand
226         */
227        public void setPaths(String... paths) {
228                this.paths = paths;
229                this.pathsIsSet = true;
230        }
231        /**
232         * Returns the {@code <files>} operand value: The files to be sorted or merged; relative paths are not resolved
233                        (use the string paths argument to enable relative path resolving
234                        based on the current working directory).
235         * 
236         * @return the {@code <files>} operand value (variables are not resolved)
237         * @throws IllegalStateException if this operand has never been set
238         * 
239         */
240        public java.io.File[] getFiles() {
241                if (filesIsSet) {
242                        return files;
243                }
244                throw new IllegalStateException("operand has not been set: " + files);
245        }
246
247        /**
248         * Returns true if the {@code <files>} operand has been set. 
249         * <p>
250         * Note that this method returns true even if {@code null} was passed to the
251         * {@link #setFiles(java.io.File[])} method.
252         * 
253         * @return      true if the setter for the {@code <files>} operand has 
254         *                      been called at least once
255         */
256        public boolean isFilesSet() {
257                return filesIsSet;
258        }
259        /**
260         * Sets {@code <files>}: The files to be sorted or merged; relative paths are not resolved
261                        (use the string paths argument to enable relative path resolving
262                        based on the current working directory).
263         * 
264         * @param files the value for the {@code <files>} operand
265         */
266        public void setFiles(java.io.File... files) {
267                this.files = files;
268                this.filesIsSet = true;
269        }
270        /**
271         * Returns the {@code <inputs>} operand value: The inputs to be sorted or merged.
272         * 
273         * @return the {@code <inputs>} operand value (variables are not resolved)
274         * @throws IllegalStateException if this operand has never been set
275         * 
276         */
277        public org.unix4j.io.Input[] getInputs() {
278                if (inputsIsSet) {
279                        return inputs;
280                }
281                throw new IllegalStateException("operand has not been set: " + inputs);
282        }
283
284        /**
285         * Returns true if the {@code <inputs>} operand has been set. 
286         * <p>
287         * Note that this method returns true even if {@code null} was passed to the
288         * {@link #setInputs(org.unix4j.io.Input[])} method.
289         * 
290         * @return      true if the setter for the {@code <inputs>} operand has 
291         *                      been called at least once
292         */
293        public boolean isInputsSet() {
294                return inputsIsSet;
295        }
296        /**
297         * Sets {@code <inputs>}: The inputs to be sorted or merged.
298         * 
299         * @param inputs the value for the {@code <inputs>} operand
300         */
301        public void setInputs(org.unix4j.io.Input... inputs) {
302                this.inputs = inputs;
303                this.inputsIsSet = true;
304        }
305        /**
306         * Returns the {@code <comparator>} operand value: The comparator to use for the line comparisons.
307         * 
308         * @return the {@code <comparator>} operand value (variables are not resolved)
309         * @throws IllegalStateException if this operand has never been set
310         * 
311         */
312        public java.util.Comparator<? super org.unix4j.line.Line> getComparator() {
313                if (comparatorIsSet) {
314                        return comparator;
315                }
316                throw new IllegalStateException("operand has not been set: " + comparator);
317        }
318
319        /**
320         * Returns true if the {@code <comparator>} operand has been set. 
321         * <p>
322         * Note that this method returns true even if {@code null} was passed to the
323         * {@link #setComparator(java.util.Comparator)} method.
324         * 
325         * @return      true if the setter for the {@code <comparator>} operand has 
326         *                      been called at least once
327         */
328        public boolean isComparatorSet() {
329                return comparatorIsSet;
330        }
331        /**
332         * Sets {@code <comparator>}: The comparator to use for the line comparisons.
333         * 
334         * @param comparator the value for the {@code <comparator>} operand
335         */
336        public void setComparator(java.util.Comparator<? super org.unix4j.line.Line> comparator) {
337                this.comparator = comparator;
338                this.comparatorIsSet = true;
339        }
340        /**
341         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
342                        Options can be specified by acronym (with a leading dash "-") or by 
343                        long name (with two leading dashes "--"). Operands other than the
344                        default "--paths" operand have to be prefixed with the operand 
345                        name (e.g. "--comparator" for a subsequent comparator operand value).
346         * 
347         * @return the {@code <args>} operand value (variables are not resolved)
348         * @throws IllegalStateException if this operand has never been set
349         * 
350         */
351        public String[] getArgs() {
352                if (argsIsSet) {
353                        return args;
354                }
355                throw new IllegalStateException("operand has not been set: " + args);
356        }
357
358        /**
359         * Returns true if the {@code <args>} operand has been set. 
360         * 
361         * @return      true if the setter for the {@code <args>} operand has 
362         *                      been called at least once
363         */
364        public boolean isArgsSet() {
365                return argsIsSet;
366        }
367        
368        /**
369         * Returns true if the {@code --}{@link SortOption#check check} option
370         * is set. The option is also known as {@code -}c option.
371         * <p>
372         * Description: Checks that the single input file is ordered as specified by the
373                        arguments and the collating sequence of the current locale. No 
374                        output is produced; only the exit code is affected.
375         * 
376         * @return true if the {@code --check} or {@code -c} option is set
377         */
378        public boolean isCheck() {
379                return getOptions().isSet(SortOption.check);
380        }
381        /**
382         * Returns true if the {@code --}{@link SortOption#merge merge} option
383         * is set. The option is also known as {@code -}m option.
384         * <p>
385         * Description: Merge only; the input file are assumed to be already sorted.
386         * 
387         * @return true if the {@code --merge} or {@code -m} option is set
388         */
389        public boolean isMerge() {
390                return getOptions().isSet(SortOption.merge);
391        }
392        /**
393         * Returns true if the {@code --}{@link SortOption#unique unique} option
394         * is set. The option is also known as {@code -}u option.
395         * <p>
396         * Description: Unique: suppress all but one in each set of lines having equal keys.
397                        If used with the {@code -c} option, checks that there are no lines 
398                        with duplicate keys, in addition to checking that the input file is 
399                        sorted.
400         * 
401         * @return true if the {@code --unique} or {@code -u} option is set
402         */
403        public boolean isUnique() {
404                return getOptions().isSet(SortOption.unique);
405        }
406        /**
407         * Returns true if the {@code --}{@link SortOption#ignoreLeadingBlanks ignoreLeadingBlanks} option
408         * is set. The option is also known as {@code -}b option.
409         * <p>
410         * Description: Ignore leading blanks. 
411                        (This option is ignored if a comparator operand is present).
412         * 
413         * @return true if the {@code --ignoreLeadingBlanks} or {@code -b} option is set
414         */
415        public boolean isIgnoreLeadingBlanks() {
416                return getOptions().isSet(SortOption.ignoreLeadingBlanks);
417        }
418        /**
419         * Returns true if the {@code --}{@link SortOption#dictionaryOrder dictionaryOrder} option
420         * is set. The option is also known as {@code -}d option.
421         * <p>
422         * Description: Consider only blanks and alphanumeric characters.
423                        (This option is ignored if a comparator operand is present).
424         * 
425         * @return true if the {@code --dictionaryOrder} or {@code -d} option is set
426         */
427        public boolean isDictionaryOrder() {
428                return getOptions().isSet(SortOption.dictionaryOrder);
429        }
430        /**
431         * Returns true if the {@code --}{@link SortOption#ignoreCase ignoreCase} option
432         * is set. The option is also known as {@code -}f option.
433         * <p>
434         * Description: Consider all lowercase characters that have uppercase equivalents to
435                        be the uppercase equivalent for the purposes of comparison.
436                        (This option is ignored if a comparator operand is present).
437         * 
438         * @return true if the {@code --ignoreCase} or {@code -f} option is set
439         */
440        public boolean isIgnoreCase() {
441                return getOptions().isSet(SortOption.ignoreCase);
442        }
443        /**
444         * Returns true if the {@code --}{@link SortOption#numericSort numericSort} option
445         * is set. The option is also known as {@code -}n option.
446         * <p>
447         * Description: Sort numerically; the number begins each line and consists of 
448                        optional blanks, an optional minus sign, and zero or more digits
449                        possibly separated by thousands separators, optionally followed by a
450                        decimal-point character and zero or more digits. An empty number is
451                        treated as '0'. The current local specifies the decimal-point 
452                        character and thousands separator.
453                        <p>
454                        Comparison is exact; there is no rounding error.
455                        <p>
456                        Neither a leading '+' nor exponential notation is recognized. To 
457                        compare such strings numerically, use the
458                        {@code -genericNumericSort (-g)} option. 
459<p>
460                        (This option is ignored if a comparator operand is present).
461         * 
462         * @return true if the {@code --numericSort} or {@code -n} option is set
463         */
464        public boolean isNumericSort() {
465                return getOptions().isSet(SortOption.numericSort);
466        }
467        /**
468         * Returns true if the {@code --}{@link SortOption#generalNumericSort generalNumericSort} option
469         * is set. The option is also known as {@code -}g option.
470         * <p>
471         * Description: Sort numerically, using the standard {@link Double#parseDouble(String)}  
472                        function to convert a trimmed line to a double-precision floating 
473                        point number. This allows floating point numbers to be specified in 
474                        scientific notation, like 1.0e-34 and 10e100. 
475                        <p>
476                        Uses the following collating sequence: Lines that cannot be parsed 
477                        because they do not represent valid double values (in alpha-numeric
478                        order); "-Infinity"; finite numbers in ascending numeric order 
479                        (with -0 < +0); "Infinity"; "NaN".
480<p>
481                        This option is usually slower than {@code -numeric-sort (-n)} and it
482                        can lose information when converting to floating point.         
483                <p>
484                        (This option is ignored if a comparator operand is present).
485         * 
486         * @return true if the {@code --generalNumericSort} or {@code -g} option is set
487         */
488        public boolean isGeneralNumericSort() {
489                return getOptions().isSet(SortOption.generalNumericSort);
490        }
491        /**
492         * Returns true if the {@code --}{@link SortOption#humanNumericSort humanNumericSort} option
493         * is set. The option is also known as {@code -}h option.
494         * <p>
495         * Description: Sort numerically, first by numeric sign (negative, zero, or 
496                        positive); then by SI suffix (either empty, or 'k' or 'K', or one 
497                        of 'MGTPEZY', in that order); and finally by numeric value. For
498                        example, '1023M' sorts before '1G' because 'M' (mega) precedes 'G' 
499                        (giga) as an SI suffix. 
500                        <p>
501                        This option sorts values that are consistently scaled to the nearest
502                        suffix, regardless of whether suffixes denote powers of 1000 or
503                        1024, and it therefore sorts the output of any single invocation of 
504                        the {@code ls} command that are invoked with the --human-readable 
505                        option. 
506                        <p>
507                        The syntax for numbers is the same as for the
508                        {@code --numericSort (-n)} option; the SI suffix must immediately 
509                        follow the number.              
510<p>
511                        (This option is ignored if a comparator operand is present).
512         * 
513         * @return true if the {@code --humanNumericSort} or {@code -h} option is set
514         */
515        public boolean isHumanNumericSort() {
516                return getOptions().isSet(SortOption.humanNumericSort);
517        }
518        /**
519         * Returns true if the {@code --}{@link SortOption#monthSort monthSort} option
520         * is set. The option is also known as {@code -}M option.
521         * <p>
522         * Description: An initial string, consisting of any amount of blanks, followed by a
523                        month name abbreviation, is folded to UPPER case and compared in the
524                        order: (unknown) < 'JAN' < ... < 'DEC'. The current locale
525                        determines the month spellings.
526         * 
527         * @return true if the {@code --monthSort} or {@code -M} option is set
528         */
529        public boolean isMonthSort() {
530                return getOptions().isSet(SortOption.monthSort);
531        }
532        /**
533         * Returns true if the {@code --}{@link SortOption#versionSort versionSort} option
534         * is set. The option is also known as {@code -}V option.
535         * <p>
536         * Description: Sort by version name and number. It behaves like a standard sort, 
537                        except that each sequence of decimal digits is treated numerically 
538                        as an index/version number.
539                        <p>
540                        (This option is ignored if a comparator operand is present).
541         * 
542         * @return true if the {@code --versionSort} or {@code -V} option is set
543         */
544        public boolean isVersionSort() {
545                return getOptions().isSet(SortOption.versionSort);
546        }
547        /**
548         * Returns true if the {@code --}{@link SortOption#reverse reverse} option
549         * is set. The option is also known as {@code -}r option.
550         * <p>
551         * Description: Reverse the sense of comparisons.
552         * 
553         * @return true if the {@code --reverse} or {@code -r} option is set
554         */
555        public boolean isReverse() {
556                return getOptions().isSet(SortOption.reverse);
557        }
558
559        @Override
560        public String toString() {
561                // ok, we have options or arguments or both
562                final StringBuilder sb = new StringBuilder();
563
564                if (argsIsSet) {
565                        for (String arg : args) {
566                                if (sb.length() > 0) sb.append(' ');
567                                sb.append(arg);
568                        }
569                } else {
570                
571                        // first the options
572                        if (options.size() > 0) {
573                                sb.append(DefaultOptionSet.toString(options));
574                        }
575                        // operand: <paths>
576                        if (pathsIsSet) {
577                                if (sb.length() > 0) sb.append(' ');
578                                sb.append("--").append("paths");
579                                sb.append(" ").append(toString(getPaths()));
580                        }
581                        // operand: <files>
582                        if (filesIsSet) {
583                                if (sb.length() > 0) sb.append(' ');
584                                sb.append("--").append("files");
585                                sb.append(" ").append(toString(getFiles()));
586                        }
587                        // operand: <inputs>
588                        if (inputsIsSet) {
589                                if (sb.length() > 0) sb.append(' ');
590                                sb.append("--").append("inputs");
591                                sb.append(" ").append(toString(getInputs()));
592                        }
593                        // operand: <comparator>
594                        if (comparatorIsSet) {
595                                if (sb.length() > 0) sb.append(' ');
596                                sb.append("--").append("comparator");
597                                sb.append(" ").append(toString(getComparator()));
598                        }
599                        // operand: <args>
600                        if (argsIsSet) {
601                                if (sb.length() > 0) sb.append(' ');
602                                sb.append("--").append("args");
603                                sb.append(" ").append(toString(getArgs()));
604                        }
605                }
606                
607                return sb.toString();
608        }
609        private static String toString(Object value) {
610                if (value != null && value.getClass().isArray()) {
611                        return ArrayUtil.toString(value);
612                }
613                return String.valueOf(value);
614        }
615}