001package org.unix4j.unix.cat;
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.Cat;
017
018/**
019 * Arguments and options for the {@link Cat cat} command.
020 */
021public final class CatArguments implements Arguments<CatArguments> {
022        
023        private final CatOptions options;
024
025        
026        // operand: <files>
027        private java.io.File[] files;
028        private boolean filesIsSet = false;
029        
030        // operand: <paths>
031        private String[] paths;
032        private boolean pathsIsSet = false;
033        
034        // operand: <args>
035        private String[] args;
036        private boolean argsIsSet = false;
037        
038        /**
039         * Constructor to use if no options are specified.
040         */
041        public CatArguments() {
042                this.options = CatOptions.EMPTY;
043        }
044
045        /**
046         * Constructor with option set containing the selected command options.
047         * 
048         * @param options the selected options
049         * @throws NullPointerException if the argument is null
050         */
051        public CatArguments(CatOptions options) {
052                if (options == null) {
053                        throw new NullPointerException("options argument cannot be null");
054                }
055                this.options = options;
056        }
057        
058        /**
059         * Returns the options set containing the selected command options. Returns
060         * an empty options set if no option has been selected.
061         * 
062         * @return set with the selected options
063         */
064        public CatOptions getOptions() {
065                return options;
066        }
067
068        /**
069         * Constructor string arguments encoding options and arguments, possibly
070         * also containing variable expressions. 
071         * 
072         * @param args string arguments for the command
073         * @throws NullPointerException if args is null
074         */
075        public CatArguments(String... args) {
076                this();
077                this.args = args;
078                this.argsIsSet = true;
079        }
080        private Object[] resolveVariables(VariableContext context, String... unresolved) {
081                final Object[] resolved = new Object[unresolved.length];
082                for (int i = 0; i < resolved.length; i++) {
083                        final String expression = unresolved[i];
084                        if (Arg.isVariable(expression)) {
085                                resolved[i] = resolveVariable(context, expression);
086                        } else {
087                                resolved[i] = expression;
088                        }
089                }
090                return resolved;
091        }
092        private <V> V convertList(ExecutionContext context, String operandName, Class<V> operandType, List<Object> values) {
093                if (values.size() == 1) {
094                        final Object value = values.get(0);
095                        return convert(context, operandName, operandType, value);
096                }
097                return convert(context, operandName, operandType, values);
098        }
099
100        private Object resolveVariable(VariableContext context, String variable) {
101                final Object value = context.getValue(variable);
102                if (value != null) {
103                        return value;
104                }
105                throw new IllegalArgumentException("cannot resolve variable " + variable + 
106                                " in command: cat " + this);
107        }
108        private <V> V convert(ExecutionContext context, String operandName, Class<V> operandType, Object value) {
109                final ValueConverter<V> converter = context.getValueConverterFor(operandType);
110                final V convertedValue;
111                if (converter != null) {
112                        convertedValue = converter.convert(value);
113                } else {
114                        if (CatOptions.class.equals(operandType)) {
115                                convertedValue = operandType.cast(CatOptions.CONVERTER.convert(value));
116                        } else {
117                                convertedValue = null;
118                        }
119                }
120                if (convertedValue != null) {
121                        return convertedValue;
122                }
123                throw new IllegalArgumentException("cannot convert --" + operandName + 
124                                " value '" + value + "' into the type " + operandType.getName() + 
125                                " for cat command");
126        }
127        
128        @Override
129        public CatArguments getForContext(ExecutionContext context) {
130                if (context == null) {
131                        throw new NullPointerException("context cannot be null");
132                }
133                if (!argsIsSet || args.length == 0) {
134                        //nothing to resolve
135                        return this;
136                }
137
138                //check if there is at least one variable
139                boolean hasVariable = false;
140                for (final String arg : args) {
141                        if (arg != null && arg.startsWith("$")) {
142                                hasVariable = true;
143                                break;
144                        }
145                }
146                //resolve variables
147                final Object[] resolvedArgs = hasVariable ? resolveVariables(context.getVariableContext(), this.args) : this.args;
148                
149                //convert now
150                final List<String> defaultOperands = Arrays.asList("paths");
151                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
152                final CatOptions.Default options = new CatOptions.Default();
153                final CatArguments argsForContext = new CatArguments(options);
154                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
155                        if ("files".equals(e.getKey())) {
156                                        
157                                final java.io.File[] value = convertList(context, "files", java.io.File[].class, e.getValue());  
158                                argsForContext.setFiles(value);
159                        } else if ("paths".equals(e.getKey())) {
160                                        
161                                final String[] value = convertList(context, "paths", String[].class, e.getValue());  
162                                argsForContext.setPaths(value);
163                        } else if ("args".equals(e.getKey())) {
164                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cat command args: " + Arrays.toString(args));
165                        } else if ("options".equals(e.getKey())) {
166                                        
167                                final CatOptions value = convertList(context, "options", CatOptions.class, e.getValue());  
168                                options.setAll(value);
169                        } else {
170                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cat command args: " + Arrays.toString(args));
171                        }
172                }
173                return argsForContext;
174        }
175        
176        /**
177         * Returns the {@code <files>} operand value: The input files to be printed; relative paths are not resolved (use 
178                        the string path argument to enable relative path resolving based on 
179                        the current working directory).
180         * 
181         * @return the {@code <files>} operand value (variables are not resolved)
182         * @throws IllegalStateException if this operand has never been set
183         * 
184         */
185        public java.io.File[] getFiles() {
186                if (filesIsSet) {
187                        return files;
188                }
189                throw new IllegalStateException("operand has not been set: " + files);
190        }
191
192        /**
193         * Returns true if the {@code <files>} operand has been set. 
194         * <p>
195         * Note that this method returns true even if {@code null} was passed to the
196         * {@link #setFiles(java.io.File[])} method.
197         * 
198         * @return      true if the setter for the {@code <files>} operand has 
199         *                      been called at least once
200         */
201        public boolean isFilesSet() {
202                return filesIsSet;
203        }
204        /**
205         * Sets {@code <files>}: The input files to be printed; relative paths are not resolved (use 
206                        the string path argument to enable relative path resolving based on 
207                        the current working directory).
208         * 
209         * @param files the value for the {@code <files>} operand
210         */
211        public void setFiles(java.io.File... files) {
212                this.files = files;
213                this.filesIsSet = true;
214        }
215        /**
216         * Returns the {@code <paths>} operand value: Pathnames of the input files to be printed; wildcards * and ? are
217                        supported; relative paths are resolved on the basis of the current 
218                        working directory.
219         * 
220         * @return the {@code <paths>} operand value (variables are not resolved)
221         * @throws IllegalStateException if this operand has never been set
222         * 
223         */
224        public String[] getPaths() {
225                if (pathsIsSet) {
226                        return paths;
227                }
228                throw new IllegalStateException("operand has not been set: " + paths);
229        }
230
231        /**
232         * Returns true if the {@code <paths>} operand has been set. 
233         * <p>
234         * Note that this method returns true even if {@code null} was passed to the
235         * {@link #setPaths(String[])} method.
236         * 
237         * @return      true if the setter for the {@code <paths>} operand has 
238         *                      been called at least once
239         */
240        public boolean isPathsSet() {
241                return pathsIsSet;
242        }
243        /**
244         * Sets {@code <paths>}: Pathnames of the input files to be printed; wildcards * and ? are
245                        supported; relative paths are resolved on the basis of the current 
246                        working directory.
247         * 
248         * @param paths the value for the {@code <paths>} operand
249         */
250        public void setPaths(String... paths) {
251                this.paths = paths;
252                this.pathsIsSet = true;
253        }
254        /**
255         * Returns the {@code <args>} operand value: String arguments defining the options and file operands for the 
256                        command. Options can be specified by acronym (with a leading dash 
257                        "-") or by long name (with two leading dashes "--"). File arguments 
258                        are expanded if wildcards are used.
259         * 
260         * @return the {@code <args>} operand value (variables are not resolved)
261         * @throws IllegalStateException if this operand has never been set
262         * 
263         */
264        public String[] getArgs() {
265                if (argsIsSet) {
266                        return args;
267                }
268                throw new IllegalStateException("operand has not been set: " + args);
269        }
270
271        /**
272         * Returns true if the {@code <args>} operand has been set. 
273         * 
274         * @return      true if the setter for the {@code <args>} operand has 
275         *                      been called at least once
276         */
277        public boolean isArgsSet() {
278                return argsIsSet;
279        }
280        
281        /**
282         * Returns true if the {@code --}{@link CatOption#numberNonBlankLines numberNonBlankLines} option
283         * is set. The option is also known as {@code -}b option.
284         * <p>
285         * Description: Number the non-blank output lines, starting at 1.
286         * 
287         * @return true if the {@code --numberNonBlankLines} or {@code -b} option is set
288         */
289        public boolean isNumberNonBlankLines() {
290                return getOptions().isSet(CatOption.numberNonBlankLines);
291        }
292        /**
293         * Returns true if the {@code --}{@link CatOption#numberLines numberLines} option
294         * is set. The option is also known as {@code -}n option.
295         * <p>
296         * Description: Number the output lines, starting at 1.
297         * 
298         * @return true if the {@code --numberLines} or {@code -n} option is set
299         */
300        public boolean isNumberLines() {
301                return getOptions().isSet(CatOption.numberLines);
302        }
303        /**
304         * Returns true if the {@code --}{@link CatOption#squeezeEmptyLines squeezeEmptyLines} option
305         * is set. The option is also known as {@code -}s option.
306         * <p>
307         * Description: Squeeze multiple adjacent empty lines, causing the output to be 
308                        single spaced.
309         * 
310         * @return true if the {@code --squeezeEmptyLines} or {@code -s} option is set
311         */
312        public boolean isSqueezeEmptyLines() {
313                return getOptions().isSet(CatOption.squeezeEmptyLines);
314        }
315
316        @Override
317        public String toString() {
318                // ok, we have options or arguments or both
319                final StringBuilder sb = new StringBuilder();
320
321                if (argsIsSet) {
322                        for (String arg : args) {
323                                if (sb.length() > 0) sb.append(' ');
324                                sb.append(arg);
325                        }
326                } else {
327                
328                        // first the options
329                        if (options.size() > 0) {
330                                sb.append(DefaultOptionSet.toString(options));
331                        }
332                        // operand: <files>
333                        if (filesIsSet) {
334                                if (sb.length() > 0) sb.append(' ');
335                                sb.append("--").append("files");
336                                sb.append(" ").append(toString(getFiles()));
337                        }
338                        // operand: <paths>
339                        if (pathsIsSet) {
340                                if (sb.length() > 0) sb.append(' ');
341                                sb.append("--").append("paths");
342                                sb.append(" ").append(toString(getPaths()));
343                        }
344                        // operand: <args>
345                        if (argsIsSet) {
346                                if (sb.length() > 0) sb.append(' ');
347                                sb.append("--").append("args");
348                                sb.append(" ").append(toString(getArgs()));
349                        }
350                }
351                
352                return sb.toString();
353        }
354        private static String toString(Object value) {
355                if (value != null && value.getClass().isArray()) {
356                        return ArrayUtil.toString(value);
357                }
358                return String.valueOf(value);
359        }
360}