001package org.unix4j.unix.echo;
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.Echo;
017
018/**
019 * Arguments and options for the {@link Echo echo} command.
020 */
021public final class EchoArguments implements Arguments<EchoArguments> {
022        
023        private final EchoOptions options;
024
025        
026        // operand: <string>
027        private String string;
028        private boolean stringIsSet = false;
029        
030        // operand: <strings>
031        private String[] strings;
032        private boolean stringsIsSet = 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 EchoArguments() {
042                this.options = EchoOptions.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 EchoArguments(EchoOptions 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 EchoOptions 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 EchoArguments(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: echo " + 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 (EchoOptions.class.equals(operandType)) {
115                                convertedValue = operandType.cast(EchoOptions.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 echo command");
126        }
127        
128        @Override
129        public EchoArguments 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("strings");
151                final Map<String, List<Object>> map = ArgsUtil.parseArgs("options", defaultOperands, resolvedArgs);
152                final EchoOptions.Default options = new EchoOptions.Default();
153                final EchoArguments argsForContext = new EchoArguments(options);
154                for (final Map.Entry<String, List<Object>> e : map.entrySet()) {
155                        if ("string".equals(e.getKey())) {
156                                        
157                                final String value = convertList(context, "string", String.class, e.getValue());  
158                                argsForContext.setString(value);
159                        } else if ("strings".equals(e.getKey())) {
160                                        
161                                final String[] value = convertList(context, "strings", String[].class, e.getValue());  
162                                argsForContext.setStrings(value);
163                        } else if ("args".equals(e.getKey())) {
164                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in echo command args: " + Arrays.toString(args));
165                        } else if ("options".equals(e.getKey())) {
166                                        
167                                final EchoOptions value = convertList(context, "options", EchoOptions.class, e.getValue());  
168                                options.setAll(value);
169                        } else {
170                                throw new IllegalStateException("invalid operand '" + e.getKey() + "' in echo command args: " + Arrays.toString(args));
171                        }
172                }
173                return argsForContext;
174        }
175        
176        /**
177         * Returns the {@code <string>} operand value (variables are NOT resolved): A string to be written to standard output.
178         * 
179         * @return the {@code <string>} operand value (variables are not resolved)
180         * @throws IllegalStateException if this operand has never been set
181         * @see #getString(ExecutionContext)
182         */
183        public String getString() {
184                if (stringIsSet) {
185                        return string;
186                }
187                throw new IllegalStateException("operand has not been set: " + string);
188        }
189        /**
190         * Returns the {@code <string>} (variables are resolved): A string to be written to standard output.
191         * 
192         * @param context the execution context used to resolve variables
193         * @return the {@code <string>} operand value after resolving variables
194         * @throws IllegalStateException if this operand has never been set
195         * @see #getString()
196         */
197        public String getString(ExecutionContext context) {
198                final String value = getString();
199                if (Arg.isVariable(value)) {
200                        final Object resolved = resolveVariable(context.getVariableContext(), value);
201                        final String converted = convert(context, "string", String.class, resolved);
202                        return converted;
203                }
204                return value;
205        }
206
207        /**
208         * Returns true if the {@code <string>} operand has been set. 
209         * <p>
210         * Note that this method returns true even if {@code null} was passed to the
211         * {@link #setString(String)} method.
212         * 
213         * @return      true if the setter for the {@code <string>} operand has 
214         *                      been called at least once
215         */
216        public boolean isStringSet() {
217                return stringIsSet;
218        }
219        /**
220         * Sets {@code <string>}: A string to be written to standard output.
221         * 
222         * @param string the value for the {@code <string>} operand
223         */
224        public void setString(String string) {
225                this.string = string;
226                this.stringIsSet = true;
227        }
228        /**
229         * Returns the {@code <strings>} operand value: Strings to be written to standard output, separated by single blank 
230                        characters.
231         * 
232         * @return the {@code <strings>} operand value (variables are not resolved)
233         * @throws IllegalStateException if this operand has never been set
234         * 
235         */
236        public String[] getStrings() {
237                if (stringsIsSet) {
238                        return strings;
239                }
240                throw new IllegalStateException("operand has not been set: " + strings);
241        }
242
243        /**
244         * Returns true if the {@code <strings>} operand has been set. 
245         * <p>
246         * Note that this method returns true even if {@code null} was passed to the
247         * {@link #setStrings(String[])} method.
248         * 
249         * @return      true if the setter for the {@code <strings>} operand has 
250         *                      been called at least once
251         */
252        public boolean isStringsSet() {
253                return stringsIsSet;
254        }
255        /**
256         * Sets {@code <strings>}: Strings to be written to standard output, separated by single blank 
257                        characters.
258         * 
259         * @param strings the value for the {@code <strings>} operand
260         */
261        public void setStrings(String... strings) {
262                this.strings = strings;
263                this.stringsIsSet = true;
264        }
265        /**
266         * Returns the {@code <args>} operand value: String arguments defining the options for the command and the 
267                        strings to be written to the output. Options can be specified by 
268                        acronym (with a leading dash "-") or by long name (with two leading 
269                        dashes "--").
270         * 
271         * @return the {@code <args>} operand value (variables are not resolved)
272         * @throws IllegalStateException if this operand has never been set
273         * 
274         */
275        public String[] getArgs() {
276                if (argsIsSet) {
277                        return args;
278                }
279                throw new IllegalStateException("operand has not been set: " + args);
280        }
281
282        /**
283         * Returns true if the {@code <args>} operand has been set. 
284         * 
285         * @return      true if the setter for the {@code <args>} operand has 
286         *                      been called at least once
287         */
288        public boolean isArgsSet() {
289                return argsIsSet;
290        }
291        
292        /**
293         * Returns true if the {@code --}{@link EchoOption#noNewline noNewline} option
294         * is set. The option is also known as {@code -}n option.
295         * <p>
296         * Description: Do not print the trailing newline character(s).
297         * 
298         * @return true if the {@code --noNewline} or {@code -n} option is set
299         */
300        public boolean isNoNewline() {
301                return getOptions().isSet(EchoOption.noNewline);
302        }
303
304        @Override
305        public String toString() {
306                // ok, we have options or arguments or both
307                final StringBuilder sb = new StringBuilder();
308
309                if (argsIsSet) {
310                        for (String arg : args) {
311                                if (sb.length() > 0) sb.append(' ');
312                                sb.append(arg);
313                        }
314                } else {
315                
316                        // first the options
317                        if (options.size() > 0) {
318                                sb.append(DefaultOptionSet.toString(options));
319                        }
320                        // operand: <string>
321                        if (stringIsSet) {
322                                if (sb.length() > 0) sb.append(' ');
323                                sb.append("--").append("string");
324                                sb.append(" ").append(toString(getString()));
325                        }
326                        // operand: <strings>
327                        if (stringsIsSet) {
328                                if (sb.length() > 0) sb.append(' ');
329                                sb.append("--").append("strings");
330                                sb.append(" ").append(toString(getStrings()));
331                        }
332                        // operand: <args>
333                        if (argsIsSet) {
334                                if (sb.length() > 0) sb.append(' ');
335                                sb.append("--").append("args");
336                                sb.append(" ").append(toString(getArgs()));
337                        }
338                }
339                
340                return sb.toString();
341        }
342        private static String toString(Object value) {
343                if (value != null && value.getClass().isArray()) {
344                        return ArrayUtil.toString(value);
345                }
346                return String.valueOf(value);
347        }
348}