001package org.unix4j.unix.uniq;
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.Uniq;
017
018/**
019 * Arguments and options for the {@link Uniq uniq} command.
020 */
021public final class UniqArguments implements Arguments<UniqArguments> {
022        
023        private final UniqOptions options;
024
025        
026        // operand: <file>
027        private java.io.File file;
028        private boolean fileIsSet = false;
029        
030        // operand: <path>
031        private String path;
032        private boolean pathIsSet = 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 UniqArguments() {
042                this.options = UniqOptions.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 UniqArguments(UniqOptions 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 UniqOptions 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 UniqArguments(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: uniq " + 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 (UniqOptions.class.equals(operandType)) {
115                                convertedValue = operandType.cast(UniqOptions.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 uniq command");
126        }
127        
128        @Override
129        public UniqArguments 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("path");
151                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
152                final UniqOptions.Default options = new UniqOptions.Default();
153                final UniqArguments argsForContext = new UniqArguments(options);
154                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
155                        if ("file".equals(e.getKey())) {
156                                        
157                                final java.io.File value = convertList(context, "file", java.io.File.class, e.getValue());  
158                                argsForContext.setFile(value);
159                        } else if ("path".equals(e.getKey())) {
160                                        
161                                final String value = convertList(context, "path", String.class, e.getValue());  
162                                argsForContext.setPath(value);
163                        } else if ("args".equals(e.getKey())) {
164                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in uniq command args: " + Arrays.toString(args));
165                        } else if ("options".equals(e.getKey())) {
166                                        
167                                final UniqOptions value = convertList(context, "options", UniqOptions.class, e.getValue());  
168                                options.setAll(value);
169                        } else {
170                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in uniq command args: " + Arrays.toString(args));
171                        }
172                }
173                return argsForContext;
174        }
175        
176        /**
177         * Returns the {@code <file>} operand value: The files or directories used as starting point for the listing; 
178                        relative paths are not resolved (use the string path argument to 
179                        enable relative path resolving based on the current working 
180                        directory).
181         * 
182         * @return the {@code <file>} operand value (variables are not resolved)
183         * @throws IllegalStateException if this operand has never been set
184         * 
185         */
186        public java.io.File getFile() {
187                if (fileIsSet) {
188                        return file;
189                }
190                throw new IllegalStateException("operand has not been set: " + file);
191        }
192
193        /**
194         * Returns true if the {@code <file>} operand has been set. 
195         * <p>
196         * Note that this method returns true even if {@code null} was passed to the
197         * {@link #setFile(java.io.File)} method.
198         * 
199         * @return      true if the setter for the {@code <file>} operand has 
200         *                      been called at least once
201         */
202        public boolean isFileSet() {
203                return fileIsSet;
204        }
205        /**
206         * Sets {@code <file>}: The files or directories used as starting point for the listing; 
207                        relative paths are not resolved (use the string path argument to 
208                        enable relative path resolving based on the current working 
209                        directory).
210         * 
211         * @param file the value for the {@code <file>} operand
212         */
213        public void setFile(java.io.File file) {
214                this.file = file;
215                this.fileIsSet = true;
216        }
217        /**
218         * Returns the {@code <path>} operand value (variables are NOT resolved): The files or directories used as starting point for the listing; 
219                        wildcards * and ? are supported; relative paths are resolved on the
220            basis of the current working directory.
221         * 
222         * @return the {@code <path>} operand value (variables are not resolved)
223         * @throws IllegalStateException if this operand has never been set
224         * @see #getPath(ExecutionContext)
225         */
226        public String getPath() {
227                if (pathIsSet) {
228                        return path;
229                }
230                throw new IllegalStateException("operand has not been set: " + path);
231        }
232        /**
233         * Returns the {@code <path>} (variables are resolved): The files or directories used as starting point for the listing; 
234                        wildcards * and ? are supported; relative paths are resolved on the
235            basis of the current working directory.
236         * 
237         * @param context the execution context used to resolve variables
238         * @return the {@code <path>} operand value after resolving variables
239         * @throws IllegalStateException if this operand has never been set
240         * @see #getPath()
241         */
242        public String getPath(ExecutionContext context) {
243                final String value = getPath();
244                if (Arg.isVariable(value)) {
245                        final Object resolved = resolveVariable(context.getVariableContext(), value);
246                        final String converted = convert(context, "path", String.class, resolved);
247                        return converted;
248                }
249                return value;
250        }
251
252        /**
253         * Returns true if the {@code <path>} operand has been set. 
254         * <p>
255         * Note that this method returns true even if {@code null} was passed to the
256         * {@link #setPath(String)} method.
257         * 
258         * @return      true if the setter for the {@code <path>} operand has 
259         *                      been called at least once
260         */
261        public boolean isPathSet() {
262                return pathIsSet;
263        }
264        /**
265         * Sets {@code <path>}: The files or directories used as starting point for the listing; 
266                        wildcards * and ? are supported; relative paths are resolved on the
267            basis of the current working directory.
268         * 
269         * @param path the value for the {@code <path>} operand
270         */
271        public void setPath(String path) {
272                this.path = path;
273                this.pathIsSet = true;
274        }
275        /**
276         * Returns the {@code <args>} operand value: String arguments defining the options and operands for the command. 
277                        Options can be specified by acronym (with a leading dash "-") or by 
278                        long name (with two leading dashes "--"). Operands other than the
279                        default "--path" operand have to be prefixed with the operand 
280                        name.
281         * 
282         * @return the {@code <args>} operand value (variables are not resolved)
283         * @throws IllegalStateException if this operand has never been set
284         * 
285         */
286        public String[] getArgs() {
287                if (argsIsSet) {
288                        return args;
289                }
290                throw new IllegalStateException("operand has not been set: " + args);
291        }
292
293        /**
294         * Returns true if the {@code <args>} operand has been set. 
295         * 
296         * @return      true if the setter for the {@code <args>} operand has 
297         *                      been called at least once
298         */
299        public boolean isArgsSet() {
300                return argsIsSet;
301        }
302        
303        /**
304         * Returns true if the {@code --}{@link UniqOption#count count} option
305         * is set. The option is also known as {@code -}c option.
306         * <p>
307         * Description: Precedes each output line with a count of the number of times the
308                        line occurred in the input.
309         * 
310         * @return true if the {@code --count} or {@code -c} option is set
311         */
312        public boolean isCount() {
313                return getOptions().isSet(UniqOption.count);
314        }
315        /**
316         * Returns true if the {@code --}{@link UniqOption#duplicatedOnly duplicatedOnly} option
317         * is set. The option is also known as {@code -}d option.
318         * <p>
319         * Description: Suppresses the writing of lines that are not repeated in the input.
320         * 
321         * @return true if the {@code --duplicatedOnly} or {@code -d} option is set
322         */
323        public boolean isDuplicatedOnly() {
324                return getOptions().isSet(UniqOption.duplicatedOnly);
325        }
326        /**
327         * Returns true if the {@code --}{@link UniqOption#uniqueOnly uniqueOnly} option
328         * is set. The option is also known as {@code -}u option.
329         * <p>
330         * Description: Suppresses the writing of lines that are repeated in the input.
331         * 
332         * @return true if the {@code --uniqueOnly} or {@code -u} option is set
333         */
334        public boolean isUniqueOnly() {
335                return getOptions().isSet(UniqOption.uniqueOnly);
336        }
337        /**
338         * Returns true if the {@code --}{@link UniqOption#global global} option
339         * is set. The option is also known as {@code -}g option.
340         * <p>
341         * Description: Suppresses repeated lines globally, that is, if lines are 
342                        non-adjacent. This option guarantees unique output lines even if the
343                        input lines are not sorted.
344         * 
345         * @return true if the {@code --global} or {@code -g} option is set
346         */
347        public boolean isGlobal() {
348                return getOptions().isSet(UniqOption.global);
349        }
350
351        @Override
352        public String toString() {
353                // ok, we have options or arguments or both
354                final StringBuilder sb = new StringBuilder();
355
356                if (argsIsSet) {
357                        for (String arg : args) {
358                                if (sb.length() > 0) sb.append(' ');
359                                sb.append(arg);
360                        }
361                } else {
362                
363                        // first the options
364                        if (options.size() > 0) {
365                                sb.append(DefaultOptionSet.toString(options));
366                        }
367                        // operand: <file>
368                        if (fileIsSet) {
369                                if (sb.length() > 0) sb.append(' ');
370                                sb.append("--").append("file");
371                                sb.append(" ").append(toString(getFile()));
372                        }
373                        // operand: <path>
374                        if (pathIsSet) {
375                                if (sb.length() > 0) sb.append(' ');
376                                sb.append("--").append("path");
377                                sb.append(" ").append(toString(getPath()));
378                        }
379                        // operand: <args>
380                        if (argsIsSet) {
381                                if (sb.length() > 0) sb.append(' ');
382                                sb.append("--").append("args");
383                                sb.append(" ").append(toString(getArgs()));
384                        }
385                }
386                
387                return sb.toString();
388        }
389        private static String toString(Object value) {
390                if (value != null && value.getClass().isArray()) {
391                        return ArrayUtil.toString(value);
392                }
393                return String.valueOf(value);
394        }
395}