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