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