001package org.unix4j.unix.cut;
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.Cut;
017
018/**
019 * Arguments and options for the {@link Cut cut} command.
020 */
021public final class CutArguments implements Arguments<CutArguments> {
022        
023        private final CutOptions options;
024
025        
026        // operand: <delimiter>
027        private String delimiter;
028        private boolean delimiterIsSet = false;
029        
030        // operand: <outputDelimiter>
031        private char outputDelimiter;
032        private boolean outputDelimiterIsSet = false;
033        
034        // operand: <indexes>
035        private int[] indexes;
036        private boolean indexesIsSet = false;
037        
038        // operand: <range>
039        private org.unix4j.util.Range range;
040        private boolean rangeIsSet = 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 CutArguments() {
050                this.options = CutOptions.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 CutArguments(CutOptions 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 CutOptions 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 CutArguments(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: cut " + 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 (CutOptions.class.equals(operandType)) {
123                                convertedValue = operandType.cast(CutOptions.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 cut command");
134        }
135        
136        @Override
137        public CutArguments 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("range");
159                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
160                final CutOptions.Default options = new CutOptions.Default();
161                final CutArguments argsForContext = new CutArguments(options);
162                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
163                        if ("delimiter".equals(e.getKey())) {
164                                        
165                                final String value = convertList(context, "delimiter", String.class, e.getValue());  
166                                argsForContext.setDelimiter(value);
167                        } else if ("outputDelimiter".equals(e.getKey())) {
168                                        
169                                final char value = convertList(context, "outputDelimiter", char.class, e.getValue());  
170                                argsForContext.setOutputDelimiter(value);
171                        } else if ("indexes".equals(e.getKey())) {
172                                        
173                                final int[] value = convertList(context, "indexes", int[].class, e.getValue());  
174                                argsForContext.setIndexes(value);
175                        } else if ("range".equals(e.getKey())) {
176                                        
177                                final org.unix4j.util.Range value = convertList(context, "range", org.unix4j.util.Range.class, e.getValue());  
178                                argsForContext.setRange(value);
179                        } else if ("args".equals(e.getKey())) {
180                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cut command args: " + Arrays.toString(args));
181                        } else if ("options".equals(e.getKey())) {
182                                        
183                                final CutOptions value = convertList(context, "options", CutOptions.class, e.getValue());  
184                                options.setAll(value);
185                        } else {
186                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in cut command args: " + Arrays.toString(args));
187                        }
188                }
189                return argsForContext;
190        }
191        
192        /**
193         * Returns the {@code <delimiter>} operand value (variables are NOT resolved): use as the output delimiter the default is to use the input delimiter
194         * 
195         * @return the {@code <delimiter>} operand value (variables are not resolved)
196         * @throws IllegalStateException if this operand has never been set
197         * @see #getDelimiter(ExecutionContext)
198         */
199        public String getDelimiter() {
200                if (delimiterIsSet) {
201                        return delimiter;
202                }
203                throw new IllegalStateException("operand has not been set: " + delimiter);
204        }
205        /**
206         * Returns the {@code <delimiter>} (variables are resolved): use as the output delimiter the default is to use the input delimiter
207         * 
208         * @param context the execution context used to resolve variables
209         * @return the {@code <delimiter>} operand value after resolving variables
210         * @throws IllegalStateException if this operand has never been set
211         * @see #getDelimiter()
212         */
213        public String getDelimiter(ExecutionContext context) {
214                final String value = getDelimiter();
215                if (Arg.isVariable(value)) {
216                        final Object resolved = resolveVariable(context.getVariableContext(), value);
217                        final String converted = convert(context, "delimiter", String.class, resolved);
218                        return converted;
219                }
220                return value;
221        }
222
223        /**
224         * Returns true if the {@code <delimiter>} operand has been set. 
225         * <p>
226         * Note that this method returns true even if {@code null} was passed to the
227         * {@link #setDelimiter(String)} method.
228         * 
229         * @return      true if the setter for the {@code <delimiter>} operand has 
230         *                      been called at least once
231         */
232        public boolean isDelimiterSet() {
233                return delimiterIsSet;
234        }
235        /**
236         * Sets {@code <delimiter>}: use as the output delimiter the default is to use the input delimiter
237         * 
238         * @param delimiter the value for the {@code <delimiter>} operand
239         */
240        public void setDelimiter(String delimiter) {
241                this.delimiter = delimiter;
242                this.delimiterIsSet = true;
243        }
244        /**
245         * Returns the {@code <outputDelimiter>} operand value: use as the output delimiter the default is to use the input delimiter
246         * 
247         * @return the {@code <outputDelimiter>} operand value (variables are not resolved)
248         * @throws IllegalStateException if this operand has never been set
249         * 
250         */
251        public char getOutputDelimiter() {
252                if (outputDelimiterIsSet) {
253                        return outputDelimiter;
254                }
255                throw new IllegalStateException("operand has not been set: " + outputDelimiter);
256        }
257
258        /**
259         * Returns true if the {@code <outputDelimiter>} operand has been set. 
260         * <p>
261         * Note that this method returns true even if {@code null} was passed to the
262         * {@link #setOutputDelimiter(char)} method.
263         * 
264         * @return      true if the setter for the {@code <outputDelimiter>} operand has 
265         *                      been called at least once
266         */
267        public boolean isOutputDelimiterSet() {
268                return outputDelimiterIsSet;
269        }
270        /**
271         * Sets {@code <outputDelimiter>}: use as the output delimiter the default is to use the input delimiter
272         * 
273         * @param outputDelimiter the value for the {@code <outputDelimiter>} operand
274         */
275        public void setOutputDelimiter(char outputDelimiter) {
276                this.outputDelimiter = outputDelimiter;
277                this.outputDelimiterIsSet = true;
278        }
279        /**
280         * Returns the {@code <indexes>} operand value: select these chars/field based on the given indexes. Indexes are 1 based.  i.e. the first character/field on a line has an index of 1.
281         * 
282         * @return the {@code <indexes>} operand value (variables are not resolved)
283         * @throws IllegalStateException if this operand has never been set
284         * 
285         */
286        public int[] getIndexes() {
287                if (indexesIsSet) {
288                        return indexes;
289                }
290                throw new IllegalStateException("operand has not been set: " + indexes);
291        }
292
293        /**
294         * Returns true if the {@code <indexes>} operand has been set. 
295         * <p>
296         * Note that this method returns true even if {@code null} was passed to the
297         * {@link #setIndexes(int[])} method.
298         * 
299         * @return      true if the setter for the {@code <indexes>} operand has 
300         *                      been called at least once
301         */
302        public boolean isIndexesSet() {
303                return indexesIsSet;
304        }
305        /**
306         * Sets {@code <indexes>}: select these chars/field based on the given indexes. Indexes are 1 based.  i.e. the first character/field on a line has an index of 1.
307         * 
308         * @param indexes the value for the {@code <indexes>} operand
309         */
310        public void setIndexes(int... indexes) {
311                this.indexes = indexes;
312                this.indexesIsSet = true;
313        }
314        /**
315         * Returns the {@code <range>} operand value: select only these fields
316         * 
317         * @return the {@code <range>} operand value (variables are not resolved)
318         * @throws IllegalStateException if this operand has never been set
319         * 
320         */
321        public org.unix4j.util.Range getRange() {
322                if (rangeIsSet) {
323                        return range;
324                }
325                throw new IllegalStateException("operand has not been set: " + range);
326        }
327
328        /**
329         * Returns true if the {@code <range>} operand has been set. 
330         * <p>
331         * Note that this method returns true even if {@code null} was passed to the
332         * {@link #setRange(org.unix4j.util.Range)} method.
333         * 
334         * @return      true if the setter for the {@code <range>} operand has 
335         *                      been called at least once
336         */
337        public boolean isRangeSet() {
338                return rangeIsSet;
339        }
340        /**
341         * Sets {@code <range>}: select only these fields
342         * 
343         * @param range the value for the {@code <range>} operand
344         */
345        public void setRange(org.unix4j.util.Range range) {
346                this.range = range;
347                this.rangeIsSet = true;
348        }
349        /**
350         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
351                        Options can be specified by acronym (with a leading dash "-") or by 
352                        long name (with two leading dashes "--"). Operands other than the
353                        default "--range" operand have to be prefixed with the operand name
354                        (e.g. "--indexes" for subsequent index operand values).
355         * 
356         * @return the {@code <args>} operand value (variables are not resolved)
357         * @throws IllegalStateException if this operand has never been set
358         * 
359         */
360        public String[] getArgs() {
361                if (argsIsSet) {
362                        return args;
363                }
364                throw new IllegalStateException("operand has not been set: " + args);
365        }
366
367        /**
368         * Returns true if the {@code <args>} operand has been set. 
369         * 
370         * @return      true if the setter for the {@code <args>} operand has 
371         *                      been called at least once
372         */
373        public boolean isArgsSet() {
374                return argsIsSet;
375        }
376        
377        /**
378         * Returns true if the {@code --}{@link CutOption#chars chars} option
379         * is set. The option is also known as {@code -}c option.
380         * <p>
381         * Description: The list specifies character positions.
382         * 
383         * @return true if the {@code --chars} or {@code -c} option is set
384         */
385        public boolean isChars() {
386                return getOptions().isSet(CutOption.chars);
387        }
388        /**
389         * Returns true if the {@code --}{@link CutOption#fields fields} option
390         * is set. The option is also known as {@code -}f option.
391         * <p>
392         * Description: The list specifies fields, separated in the input by the field
393                        delimiter character (see the -d option.)  Output fields are
394                        separated by a single occurrence of the field delimiter character.
395         * 
396         * @return true if the {@code --fields} or {@code -f} option is set
397         */
398        public boolean isFields() {
399                return getOptions().isSet(CutOption.fields);
400        }
401
402        @Override
403        public String toString() {
404                // ok, we have options or arguments or both
405                final StringBuilder sb = new StringBuilder();
406
407                if (argsIsSet) {
408                        for (String arg : args) {
409                                if (sb.length() > 0) sb.append(' ');
410                                sb.append(arg);
411                        }
412                } else {
413                
414                        // first the options
415                        if (options.size() > 0) {
416                                sb.append(DefaultOptionSet.toString(options));
417                        }
418                        // operand: <delimiter>
419                        if (delimiterIsSet) {
420                                if (sb.length() > 0) sb.append(' ');
421                                sb.append("--").append("delimiter");
422                                sb.append(" ").append(toString(getDelimiter()));
423                        }
424                        // operand: <outputDelimiter>
425                        if (outputDelimiterIsSet) {
426                                if (sb.length() > 0) sb.append(' ');
427                                sb.append("--").append("outputDelimiter");
428                                sb.append(" ").append(toString(getOutputDelimiter()));
429                        }
430                        // operand: <indexes>
431                        if (indexesIsSet) {
432                                if (sb.length() > 0) sb.append(' ');
433                                sb.append("--").append("indexes");
434                                sb.append(" ").append(toString(getIndexes()));
435                        }
436                        // operand: <range>
437                        if (rangeIsSet) {
438                                if (sb.length() > 0) sb.append(' ');
439                                sb.append("--").append("range");
440                                sb.append(" ").append(toString(getRange()));
441                        }
442                        // operand: <args>
443                        if (argsIsSet) {
444                                if (sb.length() > 0) sb.append(' ');
445                                sb.append("--").append("args");
446                                sb.append(" ").append(toString(getArgs()));
447                        }
448                }
449                
450                return sb.toString();
451        }
452        private static String toString(Object value) {
453                if (value != null && value.getClass().isArray()) {
454                        return ArrayUtil.toString(value);
455                }
456                return String.valueOf(value);
457        }
458}