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