001package org.unix4j.unix.head;
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.Head;
017
018/**
019 * Arguments and options for the {@link Head head} command.
020 */
021public final class HeadArguments implements Arguments<HeadArguments> {
022        
023        private final HeadOptions options;
024
025        
026        // operand: <count>
027        private long count;
028        private boolean countIsSet = false;
029        
030        // operand: <paths>
031        private String[] paths;
032        private boolean pathsIsSet = false;
033        
034        // operand: <files>
035        private java.io.File[] files;
036        private boolean filesIsSet = false;
037        
038        // operand: <inputs>
039        private org.unix4j.io.Input[] inputs;
040        private boolean inputsIsSet = 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 HeadArguments() {
050                this.options = HeadOptions.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 HeadArguments(HeadOptions 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 HeadOptions 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 HeadArguments(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: head " + 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 (HeadOptions.class.equals(operandType)) {
123                                convertedValue = operandType.cast(HeadOptions.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 head command");
134        }
135        
136        @Override
137        public HeadArguments 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 HeadOptions.Default options = new HeadOptions.Default();
161                final HeadArguments argsForContext = new HeadArguments(options);
162                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
163                        if ("count".equals(e.getKey())) {
164                                        
165                                final long value = convertList(context, "count", long.class, e.getValue());  
166                                argsForContext.setCount(value);
167                        } else if ("paths".equals(e.getKey())) {
168                                        
169                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
170                                argsForContext.setPaths(value);
171                        } else if ("files".equals(e.getKey())) {
172                                        
173                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
174                                argsForContext.setFiles(value);
175                        } else if ("inputs".equals(e.getKey())) {
176                                        
177                                final org.unix4j.io.Input[] value = convertList(context, "inputs", org.unix4j.io.Input[].class, e.getValue());  
178                                argsForContext.setInputs(value);
179                        } else if ("args".equals(e.getKey())) {
180                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in head command args: " + Arrays.toString(args));
181                        } else if ("options".equals(e.getKey())) {
182                                        
183                                final HeadOptions value = convertList(context, "options", HeadOptions.class, e.getValue());  
184                                options.setAll(value);
185                        } else {
186                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in head command args: " + Arrays.toString(args));
187                        }
188                }
189                return argsForContext;
190        }
191        
192        /**
193         * Returns the {@code <count>} operand value: The first {@code count} lines of each input file are
194                        copied to standard output, starting from 1 (characters instead of 
195                        lines if the {@code -c} option is specified). Must be a non-negative 
196                        integer or an exception is thrown. If {@code count} is greater than 
197                        the number number of lines (characters) in the input, the
198                        application will not error and send the whole file to the output.
199         * 
200         * @return the {@code <count>} operand value (variables are not resolved)
201         * @throws IllegalStateException if this operand has never been set
202         * 
203         */
204        public long getCount() {
205                if (countIsSet) {
206                        return count;
207                }
208                throw new IllegalStateException("operand has not been set: " + count);
209        }
210
211        /**
212         * Returns true if the {@code <count>} operand has been set. 
213         * <p>
214         * Note that this method returns true even if {@code null} was passed to the
215         * {@link #setCount(long)} method.
216         * 
217         * @return      true if the setter for the {@code <count>} operand has 
218         *                      been called at least once
219         */
220        public boolean isCountSet() {
221                return countIsSet;
222        }
223        /**
224         * Sets {@code <count>}: The first {@code count} lines of each input file are
225                        copied to standard output, starting from 1 (characters instead of 
226                        lines if the {@code -c} option is specified). Must be a non-negative 
227                        integer or an exception is thrown. If {@code count} is greater than 
228                        the number number of lines (characters) in the input, the
229                        application will not error and send the whole file to the output.
230         * 
231         * @param count the value for the {@code <count>} operand
232         */
233        public void setCount(long count) {
234                this.count = count;
235                this.countIsSet = true;
236        }
237        /**
238         * Returns the {@code <paths>} operand value: Path names of the input files to be filtered; wildcards * and ? are
239                        supported; relative paths are resolved on the basis of the current 
240                        working directory.
241         * 
242         * @return the {@code <paths>} operand value (variables are not resolved)
243         * @throws IllegalStateException if this operand has never been set
244         * 
245         */
246        public String[] getPaths() {
247                if (pathsIsSet) {
248                        return paths;
249                }
250                throw new IllegalStateException("operand has not been set: " + paths);
251        }
252
253        /**
254         * Returns true if the {@code <paths>} operand has been set. 
255         * <p>
256         * Note that this method returns true even if {@code null} was passed to the
257         * {@link #setPaths(String[])} method.
258         * 
259         * @return      true if the setter for the {@code <paths>} operand has 
260         *                      been called at least once
261         */
262        public boolean isPathsSet() {
263                return pathsIsSet;
264        }
265        /**
266         * Sets {@code <paths>}: Path names of the input files to be filtered; wildcards * and ? are
267                        supported; relative paths are resolved on the basis of the current 
268                        working directory.
269         * 
270         * @param paths the value for the {@code <paths>} operand
271         */
272        public void setPaths(String... paths) {
273                this.paths = paths;
274                this.pathsIsSet = true;
275        }
276        /**
277         * Returns the {@code <files>} operand value: The input files to be filtered; relative paths are not resolved (use 
278                        the string paths argument to enable relative path resolving based on 
279                        the current working directory).
280         * 
281         * @return the {@code <files>} operand value (variables are not resolved)
282         * @throws IllegalStateException if this operand has never been set
283         * 
284         */
285        public java.io.File[] getFiles() {
286                if (filesIsSet) {
287                        return files;
288                }
289                throw new IllegalStateException("operand has not been set: " + files);
290        }
291
292        /**
293         * Returns true if the {@code <files>} operand has been set. 
294         * <p>
295         * Note that this method returns true even if {@code null} was passed to the
296         * {@link #setFiles(java.io.File[])} method.
297         * 
298         * @return      true if the setter for the {@code <files>} operand has 
299         *                      been called at least once
300         */
301        public boolean isFilesSet() {
302                return filesIsSet;
303        }
304        /**
305         * Sets {@code <files>}: The input files to be filtered; relative paths are not resolved (use 
306                        the string paths argument to enable relative path resolving based on 
307                        the current working directory).
308         * 
309         * @param files the value for the {@code <files>} operand
310         */
311        public void setFiles(java.io.File... files) {
312                this.files = files;
313                this.filesIsSet = true;
314        }
315        /**
316         * Returns the {@code <inputs>} operand value: The inputs to be filtered.
317         * 
318         * @return the {@code <inputs>} operand value (variables are not resolved)
319         * @throws IllegalStateException if this operand has never been set
320         * 
321         */
322        public org.unix4j.io.Input[] getInputs() {
323                if (inputsIsSet) {
324                        return inputs;
325                }
326                throw new IllegalStateException("operand has not been set: " + inputs);
327        }
328
329        /**
330         * Returns true if the {@code <inputs>} operand has been set. 
331         * <p>
332         * Note that this method returns true even if {@code null} was passed to the
333         * {@link #setInputs(org.unix4j.io.Input[])} method.
334         * 
335         * @return      true if the setter for the {@code <inputs>} operand has 
336         *                      been called at least once
337         */
338        public boolean isInputsSet() {
339                return inputsIsSet;
340        }
341        /**
342         * Sets {@code <inputs>}: The inputs to be filtered.
343         * 
344         * @param inputs the value for the {@code <inputs>} operand
345         */
346        public void setInputs(org.unix4j.io.Input... inputs) {
347                this.inputs = inputs;
348                this.inputsIsSet = true;
349        }
350        /**
351         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
352                        Options can be specified by acronym (with a leading dash "-") or by 
353                        long name (with two leading dashes "--"). Operands other than the
354                        default "--paths" operand have to be prefixed with the operand 
355                        name (e.g. "--count" for a subsequent count operand value).
356         * 
357         * @return the {@code <args>} operand value (variables are not resolved)
358         * @throws IllegalStateException if this operand has never been set
359         * 
360         */
361        public String[] getArgs() {
362                if (argsIsSet) {
363                        return args;
364                }
365                throw new IllegalStateException("operand has not been set: " + args);
366        }
367
368        /**
369         * Returns true if the {@code <args>} operand has been set. 
370         * 
371         * @return      true if the setter for the {@code <args>} operand has 
372         *                      been called at least once
373         */
374        public boolean isArgsSet() {
375                return argsIsSet;
376        }
377        
378        /**
379         * Returns true if the {@code --}{@link HeadOption#chars chars} option
380         * is set. The option is also known as {@code -}c option.
381         * <p>
382         * Description: The {@code count} argument is in units of characters instead of 
383                        lines. Starts from 1 and includes line ending characters.
384         * 
385         * @return true if the {@code --chars} or {@code -c} option is set
386         */
387        public boolean isChars() {
388                return getOptions().isSet(HeadOption.chars);
389        }
390        /**
391         * Returns true if the {@code --}{@link HeadOption#suppressHeaders suppressHeaders} option
392         * is set. The option is also known as {@code -}q option.
393         * <p>
394         * Description: Suppresses printing of headers when multiple files are being
395                        examined.
396         * 
397         * @return true if the {@code --suppressHeaders} or {@code -q} option is set
398         */
399        public boolean isSuppressHeaders() {
400                return getOptions().isSet(HeadOption.suppressHeaders);
401        }
402
403        @Override
404        public String toString() {
405                // ok, we have options or arguments or both
406                final StringBuilder sb = new StringBuilder();
407
408                if (argsIsSet) {
409                        for (String arg : args) {
410                                if (sb.length() > 0) sb.append(' ');
411                                sb.append(arg);
412                        }
413                } else {
414                
415                        // first the options
416                        if (options.size() > 0) {
417                                sb.append(DefaultOptionSet.toString(options));
418                        }
419                        // operand: <count>
420                        if (countIsSet) {
421                                if (sb.length() > 0) sb.append(' ');
422                                sb.append("--").append("count");
423                                sb.append(" ").append(toString(getCount()));
424                        }
425                        // operand: <paths>
426                        if (pathsIsSet) {
427                                if (sb.length() > 0) sb.append(' ');
428                                sb.append("--").append("paths");
429                                sb.append(" ").append(toString(getPaths()));
430                        }
431                        // operand: <files>
432                        if (filesIsSet) {
433                                if (sb.length() > 0) sb.append(' ');
434                                sb.append("--").append("files");
435                                sb.append(" ").append(toString(getFiles()));
436                        }
437                        // operand: <inputs>
438                        if (inputsIsSet) {
439                                if (sb.length() > 0) sb.append(' ');
440                                sb.append("--").append("inputs");
441                                sb.append(" ").append(toString(getInputs()));
442                        }
443                        // operand: <args>
444                        if (argsIsSet) {
445                                if (sb.length() > 0) sb.append(' ');
446                                sb.append("--").append("args");
447                                sb.append(" ").append(toString(getArgs()));
448                        }
449                }
450                
451                return sb.toString();
452        }
453        private static String toString(Object value) {
454                if (value != null && value.getClass().isArray()) {
455                        return ArrayUtil.toString(value);
456                }
457                return String.valueOf(value);
458        }
459}