001package org.unix4j.unix.wc;
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.Wc;
017
018/**
019 * Arguments and options for the {@link Wc wc} command.
020 */
021public final class WcArguments implements Arguments<WcArguments> {
022        
023        private final WcOptions 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: <args>
039        private String[] args;
040        private boolean argsIsSet = false;
041        
042        /**
043         * Constructor to use if no options are specified.
044         */
045        public WcArguments() {
046                this.options = WcOptions.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 WcArguments(WcOptions 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 WcOptions 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 WcArguments(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: wc " + 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 (WcOptions.class.equals(operandType)) {
119                                convertedValue = operandType.cast(WcOptions.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 wc command");
130        }
131        
132        @Override
133        public WcArguments 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 WcOptions.Default options = new WcOptions.Default();
157                final WcArguments argsForContext = new WcArguments(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 ("inputs".equals(e.getKey())) {
168                                        
169                                final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue());  
170                                argsForContext.setInputs(value);
171                        } else if ("args".equals(e.getKey())) {
172                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in wc command args: " + Arrays.toString(args));
173                        } else if ("options".equals(e.getKey())) {
174                                        
175                                final WcOptions value = convertList(context, "options", WcOptions.class, e.getValue());  
176                                options.setAll(value);
177                        } else {
178                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in wc command args: " + Arrays.toString(args));
179                        }
180                }
181                return argsForContext;
182        }
183        
184        /**
185         * Returns the {@code <paths>} operand value: Path names of the input files; wildcards * and ? are supported;
186                        relative paths are resolved on the basis of the current working 
187                        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>}: Path names of the input files; wildcards * and ? are supported;
214                        relative paths are resolved on the basis of the current working 
215                        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 input files; relative paths are not resolved (use the string
225                        paths argument to enable relative path resolving based on the
226                        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 input files; relative paths are not resolved (use the string
253                        paths argument to enable relative path resolving based on the
254                        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 <inputs>} operand value: The inputs.
264         * 
265         * @return the {@code <inputs>} operand value (variables are not resolved)
266         * @throws IllegalStateException if this operand has never been set
267         * 
268         */
269        public org.unix4j.io.Input[] getInputs() {
270                if (inputsIsSet) {
271                        return inputs;
272                }
273                throw new IllegalStateException("operand has not been set: " + inputs);
274        }
275
276        /**
277         * Returns true if the {@code <inputs>} operand has been set. 
278         * <p>
279         * Note that this method returns true even if {@code null} was passed to the
280         * {@link #setInputs(org.unix4j.io.Input[])} method.
281         * 
282         * @return      true if the setter for the {@code <inputs>} operand has 
283         *                      been called at least once
284         */
285        public boolean isInputsSet() {
286                return inputsIsSet;
287        }
288        /**
289         * Sets {@code <inputs>}: The inputs.
290         * 
291         * @param inputs the value for the {@code <inputs>} operand
292         */
293        public void setInputs(org.unix4j.io.Input... inputs) {
294                this.inputs = inputs;
295                this.inputsIsSet = 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.
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 WcOption#lines lines} option
327         * is set. The option is also known as {@code -}l option.
328         * <p>
329         * Description: Executes a count of lines and writes this count to the output.
330         * 
331         * @return true if the {@code --lines} or {@code -l} option is set
332         */
333        public boolean isLines() {
334                return getOptions().isSet(WcOption.lines);
335        }
336        /**
337         * Returns true if the {@code --}{@link WcOption#words words} option
338         * is set. The option is also known as {@code -}w option.
339         * <p>
340         * Description: Executes a count of words and writes this count to the output. A
341                        word is a non-zero-length string of characters delimited by white
342                        space as defined by {@link Character#isWhitespace(char)}.
343         * 
344         * @return true if the {@code --words} or {@code -w} option is set
345         */
346        public boolean isWords() {
347                return getOptions().isSet(WcOption.words);
348        }
349        /**
350         * Returns true if the {@code --}{@link WcOption#chars chars} option
351         * is set. The option is also known as {@code -}m option.
352         * <p>
353         * Description: Executes a count of chars and writes this count to the output.
354         * 
355         * @return true if the {@code --chars} or {@code -m} option is set
356         */
357        public boolean isChars() {
358                return getOptions().isSet(WcOption.chars);
359        }
360
361        @Override
362        public String toString() {
363                // ok, we have options or arguments or both
364                final StringBuilder sb = new StringBuilder();
365
366                if (argsIsSet) {
367                        for (String arg : args) {
368                                if (sb.length() > 0) sb.append(' ');
369                                sb.append(arg);
370                        }
371                } else {
372                
373                        // first the options
374                        if (options.size() > 0) {
375                                sb.append(DefaultOptionSet.toString(options));
376                        }
377                        // operand: <paths>
378                        if (pathsIsSet) {
379                                if (sb.length() > 0) sb.append(' ');
380                                sb.append("--").append("paths");
381                                sb.append(" ").append(toString(getPaths()));
382                        }
383                        // operand: <files>
384                        if (filesIsSet) {
385                                if (sb.length() > 0) sb.append(' ');
386                                sb.append("--").append("files");
387                                sb.append(" ").append(toString(getFiles()));
388                        }
389                        // operand: <inputs>
390                        if (inputsIsSet) {
391                                if (sb.length() > 0) sb.append(' ');
392                                sb.append("--").append("inputs");
393                                sb.append(" ").append(toString(getInputs()));
394                        }
395                        // operand: <args>
396                        if (argsIsSet) {
397                                if (sb.length() > 0) sb.append(' ');
398                                sb.append("--").append("args");
399                                sb.append(" ").append(toString(getArgs()));
400                        }
401                }
402                
403                return sb.toString();
404        }
405        private static String toString(Object value) {
406                if (value != null && value.getClass().isArray()) {
407                        return ArrayUtil.toString(value);
408                }
409                return String.valueOf(value);
410        }
411}