001package org.unix4j.unix.xargs;
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.Xargs;
017
018/**
019 * Arguments and options for the {@link Xargs xargs} command.
020 */
021public final class XargsArguments implements Arguments<XargsArguments> {
022        
023        private final XargsOptions options;
024
025        
026        // operand: <delimiter>
027        private String delimiter;
028        private boolean delimiterIsSet = false;
029        
030        // operand: <eof>
031        private String eof;
032        private boolean eofIsSet = false;
033        
034        // operand: <maxLines>
035        private long maxLines;
036        private boolean maxLinesIsSet = false;
037        
038        // operand: <maxArgs>
039        private int maxArgs;
040        private boolean maxArgsIsSet = 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 XargsArguments() {
050                this.options = XargsOptions.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 XargsArguments(XargsOptions 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 XargsOptions 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 XargsArguments(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: xargs " + 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 (XargsOptions.class.equals(operandType)) {
123                                convertedValue = operandType.cast(XargsOptions.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 xargs command");
134        }
135        
136        @Override
137        public XargsArguments 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("maxArgs");
159                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
160                final XargsOptions.Default options = new XargsOptions.Default();
161                final XargsArguments argsForContext = new XargsArguments(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 ("eof".equals(e.getKey())) {
168                                        
169                                final String value = convertList(context, "eof", String.class, e.getValue());  
170                                argsForContext.setEof(value);
171                        } else if ("maxLines".equals(e.getKey())) {
172                                        
173                                final long value = convertList(context, "maxLines", long.class, e.getValue());  
174                                argsForContext.setMaxLines(value);
175                        } else if ("maxArgs".equals(e.getKey())) {
176                                        
177                                final int value = convertList(context, "maxArgs", int.class, e.getValue());  
178                                argsForContext.setMaxArgs(value);
179                        } else if ("args".equals(e.getKey())) {
180                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in xargs command args: " + Arrays.toString(args));
181                        } else if ("options".equals(e.getKey())) {
182                                        
183                                final XargsOptions value = convertList(context, "options", XargsOptions.class, e.getValue());  
184                                options.setAll(value);
185                        } else {
186                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in xargs command args: " + Arrays.toString(args));
187                        }
188                }
189                return argsForContext;
190        }
191        
192        /**
193         * Returns the {@code <delimiter>} operand value (variables are NOT resolved): Input items are terminated by the specified characters.
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): Input items are terminated by the specified characters.
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>}: Input items are terminated by the specified characters.
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 <eof>} operand value (variables are NOT resolved): If the end of file string occurs as a line of input, the rest of the
246                        input is ignored.
247         * 
248         * @return the {@code <eof>} operand value (variables are not resolved)
249         * @throws IllegalStateException if this operand has never been set
250         * @see #getEof(ExecutionContext)
251         */
252        public String getEof() {
253                if (eofIsSet) {
254                        return eof;
255                }
256                throw new IllegalStateException("operand has not been set: " + eof);
257        }
258        /**
259         * Returns the {@code <eof>} (variables are resolved): If the end of file string occurs as a line of input, the rest of the
260                        input is ignored.
261         * 
262         * @param context the execution context used to resolve variables
263         * @return the {@code <eof>} operand value after resolving variables
264         * @throws IllegalStateException if this operand has never been set
265         * @see #getEof()
266         */
267        public String getEof(ExecutionContext context) {
268                final String value = getEof();
269                if (Arg.isVariable(value)) {
270                        final Object resolved = resolveVariable(context.getVariableContext(), value);
271                        final String converted = convert(context, "eof", String.class, resolved);
272                        return converted;
273                }
274                return value;
275        }
276
277        /**
278         * Returns true if the {@code <eof>} operand has been set. 
279         * <p>
280         * Note that this method returns true even if {@code null} was passed to the
281         * {@link #setEof(String)} method.
282         * 
283         * @return      true if the setter for the {@code <eof>} operand has 
284         *                      been called at least once
285         */
286        public boolean isEofSet() {
287                return eofIsSet;
288        }
289        /**
290         * Sets {@code <eof>}: If the end of file string occurs as a line of input, the rest of the
291                        input is ignored.
292         * 
293         * @param eof the value for the {@code <eof>} operand
294         */
295        public void setEof(String eof) {
296                this.eof = eof;
297                this.eofIsSet = true;
298        }
299        /**
300         * Returns the {@code <maxLines>} operand value: Use at most maxLines nonblank input lines per command invocation.
301         * 
302         * @return the {@code <maxLines>} operand value (variables are not resolved)
303         * @throws IllegalStateException if this operand has never been set
304         * 
305         */
306        public long getMaxLines() {
307                if (maxLinesIsSet) {
308                        return maxLines;
309                }
310                throw new IllegalStateException("operand has not been set: " + maxLines);
311        }
312
313        /**
314         * Returns true if the {@code <maxLines>} operand has been set. 
315         * <p>
316         * Note that this method returns true even if {@code null} was passed to the
317         * {@link #setMaxLines(long)} method.
318         * 
319         * @return      true if the setter for the {@code <maxLines>} operand has 
320         *                      been called at least once
321         */
322        public boolean isMaxLinesSet() {
323                return maxLinesIsSet;
324        }
325        /**
326         * Sets {@code <maxLines>}: Use at most maxLines nonblank input lines per command invocation.
327         * 
328         * @param maxLines the value for the {@code <maxLines>} operand
329         */
330        public void setMaxLines(long maxLines) {
331                this.maxLines = maxLines;
332                this.maxLinesIsSet = true;
333        }
334        /**
335         * Returns the {@code <maxArgs>} operand value: Use at most maxArgs arguments per command invocation.
336         * 
337         * @return the {@code <maxArgs>} operand value (variables are not resolved)
338         * @throws IllegalStateException if this operand has never been set
339         * 
340         */
341        public int getMaxArgs() {
342                if (maxArgsIsSet) {
343                        return maxArgs;
344                }
345                throw new IllegalStateException("operand has not been set: " + maxArgs);
346        }
347
348        /**
349         * Returns true if the {@code <maxArgs>} operand has been set. 
350         * <p>
351         * Note that this method returns true even if {@code null} was passed to the
352         * {@link #setMaxArgs(int)} method.
353         * 
354         * @return      true if the setter for the {@code <maxArgs>} operand has 
355         *                      been called at least once
356         */
357        public boolean isMaxArgsSet() {
358                return maxArgsIsSet;
359        }
360        /**
361         * Sets {@code <maxArgs>}: Use at most maxArgs arguments per command invocation.
362         * 
363         * @param maxArgs the value for the {@code <maxArgs>} operand
364         */
365        public void setMaxArgs(int maxArgs) {
366                this.maxArgs = maxArgs;
367                this.maxArgsIsSet = true;
368        }
369        /**
370         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
371                        Options can be specified by acronym (with a leading dash "-") or by 
372                        long name (with two leading dashes "--"). Operands other than the
373                        default "--maxArgs" operand have to be prefixed with the operand 
374                        name (e.g. "--maxLines" for a subsequent line count operand value).
375         * 
376         * @return the {@code <args>} operand value (variables are not resolved)
377         * @throws IllegalStateException if this operand has never been set
378         * 
379         */
380        public String[] getArgs() {
381                if (argsIsSet) {
382                        return args;
383                }
384                throw new IllegalStateException("operand has not been set: " + args);
385        }
386
387        /**
388         * Returns true if the {@code <args>} operand has been set. 
389         * 
390         * @return      true if the setter for the {@code <args>} operand has 
391         *                      been called at least once
392         */
393        public boolean isArgsSet() {
394                return argsIsSet;
395        }
396        
397        /**
398         * Returns true if the {@code --}{@link XargsOption#delimiter0 delimiter0} option
399         * is set. The option is also known as {@code -}z option.
400         * <p>
401         * Description: Input items are terminated by a null character instead of by 
402                        whitespace, and the quotes and backslash are not special (every
403                        character is taken literally). Disables the end of file string,
404                        which is treated like any other argument. Useful when input items 
405                        might contain white space, quote marks, or backslashes. The find 
406                        --print0 option produces input suitable for this mode.
407                        <p>
408                        (This option is ignored if an explicit delimiter operand is specified).
409         * 
410         * @return true if the {@code --delimiter0} or {@code -z} option is set
411         */
412        public boolean isDelimiter0() {
413                return getOptions().isSet(XargsOption.delimiter0);
414        }
415        /**
416         * Returns true if the {@code --}{@link XargsOption#exactArgs exactArgs} option
417         * is set. The option is also known as {@code -}x option.
418         * <p>
419         * Description: Terminate immediately if {@code maxArgs} is specified but the found
420                        number of variable items is less than {@code maxArgs}.          
421<p>
422                        (This option is ignored if no {@code maxArgs} operand is specified).
423         * 
424         * @return true if the {@code --exactArgs} or {@code -x} option is set
425         */
426        public boolean isExactArgs() {
427                return getOptions().isSet(XargsOption.exactArgs);
428        }
429        /**
430         * Returns true if the {@code --}{@link XargsOption#noRunIfEmpty noRunIfEmpty} option
431         * is set. The option is also known as {@code -}r option.
432         * <p>
433         * Description: If the standard input does not contain any nonblanks, do not run the
434                        command. Normally, the command is run once even if there is no 
435                        input.
436         * 
437         * @return true if the {@code --noRunIfEmpty} or {@code -r} option is set
438         */
439        public boolean isNoRunIfEmpty() {
440                return getOptions().isSet(XargsOption.noRunIfEmpty);
441        }
442        /**
443         * Returns true if the {@code --}{@link XargsOption#verbose verbose} option
444         * is set. The option is also known as {@code -}t option.
445         * <p>
446         * Description: Print the command line on the standard error output before executing
447                        it.
448         * 
449         * @return true if the {@code --verbose} or {@code -t} option is set
450         */
451        public boolean isVerbose() {
452                return getOptions().isSet(XargsOption.verbose);
453        }
454
455        @Override
456        public String toString() {
457                // ok, we have options or arguments or both
458                final StringBuilder sb = new StringBuilder();
459
460                if (argsIsSet) {
461                        for (String arg : args) {
462                                if (sb.length() > 0) sb.append(' ');
463                                sb.append(arg);
464                        }
465                } else {
466                
467                        // first the options
468                        if (options.size() > 0) {
469                                sb.append(DefaultOptionSet.toString(options));
470                        }
471                        // operand: <delimiter>
472                        if (delimiterIsSet) {
473                                if (sb.length() > 0) sb.append(' ');
474                                sb.append("--").append("delimiter");
475                                sb.append(" ").append(toString(getDelimiter()));
476                        }
477                        // operand: <eof>
478                        if (eofIsSet) {
479                                if (sb.length() > 0) sb.append(' ');
480                                sb.append("--").append("eof");
481                                sb.append(" ").append(toString(getEof()));
482                        }
483                        // operand: <maxLines>
484                        if (maxLinesIsSet) {
485                                if (sb.length() > 0) sb.append(' ');
486                                sb.append("--").append("maxLines");
487                                sb.append(" ").append(toString(getMaxLines()));
488                        }
489                        // operand: <maxArgs>
490                        if (maxArgsIsSet) {
491                                if (sb.length() > 0) sb.append(' ');
492                                sb.append("--").append("maxArgs");
493                                sb.append(" ").append(toString(getMaxArgs()));
494                        }
495                        // operand: <args>
496                        if (argsIsSet) {
497                                if (sb.length() > 0) sb.append(' ');
498                                sb.append("--").append("args");
499                                sb.append(" ").append(toString(getArgs()));
500                        }
501                }
502                
503                return sb.toString();
504        }
505        private static String toString(Object value) {
506                if (value != null && value.getClass().isArray()) {
507                        return ArrayUtil.toString(value);
508                }
509                return String.valueOf(value);
510        }
511}