001package org.kohsuke.args4j;
002
003import java.lang.reflect.Field;
004import java.lang.reflect.Method;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.Comparator;
009import java.util.List;
010
011import org.kohsuke.args4j.spi.MethodSetter;
012import org.kohsuke.args4j.spi.OptionHandler;
013import org.kohsuke.args4j.spi.Parameters;
014import org.kohsuke.args4j.spi.Setter;
015import org.kohsuke.args4j.spi.Setters;
016
017/**
018 * The {@link ProxyOptionHandler} allows options to have associated options.
019 * For example, an enum option might have different options depending
020 * of its value.
021 * 
022 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
023 *
024 */
025public class ProxyOptionHandler extends OptionHandler<Object> {
026        OptionHandler<?> proxy;
027
028        /**
029         * Default constructor.
030         * @param parser the parser
031         * @param option the option definition
032         * @param setter the setter
033         * @throws CmdLineException 
034         */
035        public ProxyOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super Object> setter) throws CmdLineException {
036                super(parser, option, setter);
037
038                OptionDef proxyOption = new OptionDef(option.usage(), option.metaVar(), option.required(), OptionHandler.class, option.isMultiValued()); 
039                proxy = parser.createOptionHandler(proxyOption, setter);
040
041                if (!option.required() && CmdLineOptionsProvider.class.isAssignableFrom(this.setter.getType())) {
042                        handleExtraArgs();
043                }
044        }
045
046        @Override
047        public String getDefaultMetaVariable() {
048                return proxy.getDefaultMetaVariable();
049        }
050
051        @Override
052        public int parseArguments(Parameters params) throws CmdLineException {
053                if (CmdLineOptionsProvider.class.isAssignableFrom(this.setter.getType())) {
054                        removeExtraArgs();
055                }
056
057                int val = proxy.parseArguments(params);
058
059                if (CmdLineOptionsProvider.class.isAssignableFrom(this.setter.getType())) {
060                        handleExtraArgs();
061                }
062
063                return val;
064        }
065
066        private void removeExtraArgs() throws CmdLineException {
067                try {
068                        Setter<?> actualsetter = null;
069
070                        if (setter instanceof SetterWrapper) {
071                                actualsetter = ((SetterWrapper)setter).setter;
072                        } else {
073                                actualsetter = setter;
074                        }
075
076                        Class<?> type = actualsetter.getClass();
077
078                        Field beanField = type.getDeclaredField("bean");
079                        beanField.setAccessible(true);
080                        Object bean = beanField.get(actualsetter);
081
082                        Field field = type.getDeclaredField("f");
083                        field.setAccessible(true);
084
085                        field = (Field) field.get(actualsetter);
086                        field.setAccessible(true);
087
088                        //the actual value being set:
089                        Object object = field.get(bean);
090                        if (object == null) return;
091
092                        if(object instanceof ArrayList) {
093                                //For the time being we'll do nothing if its a list; we'll
094                                //assume that once an option has been added it can't be removed.
095                                //This INCLUDES defaults!!
096//                              @SuppressWarnings("unchecked")
097//                              ArrayList<CmdLineOptionsProvider> list = (ArrayList<CmdLineOptionsProvider>) object;
098//                              
099//                              if (list.size() > 0) {
100//                                      Object obj = list.get(list.size()-1).getOptions();
101//
102//                                      //removeOptions(obj, owner);
103//                              }
104                        }
105                        else
106                        {
107                                Object obj = ((CmdLineOptionsProvider)object).getOptions();
108
109                                if (obj instanceof Enum)
110                                        System.err.println("Warning: Using an enum ("+field+") as an options object with proxied options is not recommended and will be disallowed in the near future!");
111
112                                removeOptions(obj, owner);                              
113                        }
114                } catch (CmdLineException e) {
115                        throw e;
116                } catch (Exception e) {
117                        throw new CmdLineException(owner, "", e);
118                }
119        }
120
121        @SuppressWarnings("unchecked")
122        private void handleExtraArgs() throws CmdLineException {
123                try {
124                        Setter<?> actualsetter = null;
125
126                        if (setter instanceof SetterWrapper) {
127                                actualsetter = ((SetterWrapper)setter).setter;
128                        } else {
129                                actualsetter = setter;
130                        }
131
132                        Class<?> type = actualsetter.getClass();
133
134                        Field beanField = type.getDeclaredField("bean");
135                        beanField.setAccessible(true);
136                        Object bean = beanField.get(actualsetter);
137
138                        Field field = type.getDeclaredField("f");
139                        field.setAccessible(true);
140
141                        field = (Field) field.get(actualsetter);
142                        field.setAccessible(true);
143
144                        //the actual value being set:
145                        Object object = field.get(bean);
146                        if (object == null) return;
147
148                        if(object instanceof ArrayList) {
149                                if(((ArrayList<CmdLineOptionsProvider>) object).size() > 0){
150                                        Object obj = ((ArrayList<CmdLineOptionsProvider>) object).get(((ArrayList<CmdLineOptionsProvider>) object).size()-1).getOptions();
151
152                                        addOptions(obj, owner);
153                                        setObjectField(bean, field, obj);
154                                }
155                        }
156                        else
157                        {
158                                Object obj = ((CmdLineOptionsProvider)object).getOptions();
159
160                                if (obj instanceof Enum)
161                                        System.err.println("Warning: Using an enum ("+field+") as an options object with proxied options is not recommended and will be disallowed in the near future!");
162
163                                addOptions(obj, owner);                         
164                                setObjectField(bean, field, obj);
165                        }
166
167                        // for display purposes, we like the arguments in argument order, but the options in alphabetical order
168                        Field optionsField = owner.getClass().getDeclaredField("options");
169                        optionsField.setAccessible(true);
170
171                        final List<OptionHandler<?>> options = (List<OptionHandler<?>>) optionsField.get(owner);
172                        Collections.sort(options, new Comparator<OptionHandler<?>>() {
173                                @Override
174                                public int compare(OptionHandler<?> o1, OptionHandler<?> o2) {
175                                        return o1.option.toString().compareTo(o2.option.toString());
176                                }
177                        });                     
178                } catch (Exception e) {
179                        throw new CmdLineException(owner, "", e);
180                }
181        }
182
183        @SuppressWarnings({ "unchecked", "rawtypes" })
184        protected void setObjectField(Object bean, Field field, Object obj) throws IllegalArgumentException, IllegalAccessException {
185                //test and deal with new style
186                try {
187                        Field newoptsfield = getDeclaredField(bean.getClass(), field.getName() + "Op");
188                        newoptsfield.setAccessible(true);
189                        Object o = newoptsfield.get(bean);
190
191                        if (Collection.class.isAssignableFrom(newoptsfield.getType())) {
192                                if (o == null) {
193                                        o = new ArrayList();
194                                        newoptsfield.set(bean, o);
195                                }
196                                ((Collection)o).add(obj);
197                        } else {
198                                newoptsfield.set(bean, obj);
199                        }
200                        //newoptsfield.set(bean, value)
201                } catch (NoSuchFieldException nsfe) {
202                        //nsfe.printStackTrace();
203                }
204        }
205
206        protected Field getDeclaredField(Class<?> clz, String name) throws NoSuchFieldException {
207                try {
208                        return clz.getDeclaredField(name);
209                } catch (SecurityException e) {
210                        throw new RuntimeException(e);
211                } catch (NoSuchFieldException e) {
212                        if (clz.getSuperclass() != null)
213                                return getDeclaredField(clz.getSuperclass(), name);
214                        throw e;
215                }
216        }
217
218        @SuppressWarnings("rawtypes")
219        private class SetterWrapper implements Setter {
220                Setter setter;
221                boolean used = false;
222
223                SetterWrapper(Setter setter) {
224                        this.setter = setter;
225                }
226
227                @SuppressWarnings("unchecked")
228                @Override
229                public void addValue(Object value) throws CmdLineException {
230                        used = true;
231                        setter.addValue(value);
232                }
233
234                @Override
235                public Class getType() {
236                        return setter.getType();
237                }
238
239                @Override
240                public boolean isMultiValued() {
241                        return setter.isMultiValued();
242                }
243        }
244
245        private void addOptions(Object bean, CmdLineParser parser) {
246                // recursively process all the methods/fields.
247                for (Class<?> c=bean.getClass(); c!=null; c=c.getSuperclass()) {
248                        for (Method m : c.getDeclaredMethods()) {
249                                Option o = m.getAnnotation(Option.class);
250                                if(o!=null) {
251                                        parser.addOption(new SetterWrapper(new MethodSetter(parser,bean,m)), o);
252                                }
253                                Argument a = m.getAnnotation(Argument.class);
254                                if(a!=null) {
255                                        parser.addArgument(new SetterWrapper(new MethodSetter(parser,bean,m)), a);
256                                }
257                        }
258
259                        for( Field f : c.getDeclaredFields() ) {
260                                Option o = f.getAnnotation(Option.class);
261                                if(o!=null) {
262                                        parser.addOption(new SetterWrapper(Setters.create(f,bean)),o);
263                                }
264                                Argument a = f.getAnnotation(Argument.class);
265                                if(a!=null) {
266                                        parser.addArgument(new SetterWrapper(Setters.create(f,bean)), a);
267                                }
268                        }
269                }
270        }
271
272        private void removeOptions(Object bean, CmdLineParser parser) throws CmdLineException {
273                if (bean == null) return;
274
275                // recursively process all the methods/fields.
276                for (Class<?> c=bean.getClass(); c!=null; c=c.getSuperclass()) {
277                        for (Method m : c.getDeclaredMethods()) {
278                                Option o = m.getAnnotation(Option.class);
279                                if(o!=null) {
280                                        removeOption(parser, o);
281
282                                        //TODO: handle recursive removal
283                                }
284                                Argument a = m.getAnnotation(Argument.class);
285                                if(a!=null) {
286                                        removeArgument(parser, a);
287                                }
288                        }
289
290                        for( Field f : c.getDeclaredFields() ) {
291                                Option o = f.getAnnotation(Option.class);
292                                if(o!=null) {
293                                        removeOption(parser, o);
294
295                                        try {
296                                                f.setAccessible(true);
297                                                Object val = f.get(bean);
298                                                if (val instanceof CmdLineOptionsProvider)
299                                                        removeOptions(((CmdLineOptionsProvider)val).getOptions(), parser);
300                                        } catch (Exception e) {
301                                                e.printStackTrace();
302                                        }
303                                }
304                                Argument a = f.getAnnotation(Argument.class);
305                                if(a!=null) {
306                                        removeArgument(parser, a);
307                                }
308                        }
309                }
310        }
311
312        private void removeArgument(CmdLineParser parser, Argument a) throws CmdLineException {
313                try {
314                        Field argsField = CmdLineParser.class.getDeclaredField("arguments");
315                        argsField.setAccessible(true);
316
317                        List<?> args = (List<?>) argsField.get(parser);
318                        OptionHandler<?> op = (OptionHandler<?>) args.get(a.index());
319
320                        if (op.setter instanceof SetterWrapper && ((SetterWrapper)op.setter).used) {
321                                throw new CmdLineException(parser, "The use of the argument " + op.option.metaVar() + " is shaded by another argument");
322                        }
323
324                        args.set(a.index(), null);
325                } catch (CmdLineException e) {
326                        throw e;
327                } catch (Exception e) {
328                        throw new CmdLineException(parser, "", e);
329                }
330        }
331
332        private void removeOption(CmdLineParser parser, Option o) throws CmdLineException {
333                try {
334                        Method find = CmdLineParser.class.getDeclaredMethod("findOptionHandler", String.class);
335                        find.setAccessible(true);
336
337                        OptionHandler<?> op = (OptionHandler<?>) find.invoke(parser, o.name());
338
339                        if (op.setter instanceof SetterWrapper && ((SetterWrapper)op.setter).used) {
340                                throw new CmdLineException(parser, "The use of the option " + op.option + " is shaded by another option");
341                        }
342
343                        Field optionsField = CmdLineParser.class.getDeclaredField("options");
344                        optionsField.setAccessible(true);
345
346                        List<?> options = (List<?>) optionsField.get(parser);
347                        options.remove(op);
348                } catch (CmdLineException e) {
349                        throw e;
350                } catch (Exception e) {
351                        throw new CmdLineException(parser, "", e);
352                } 
353        }
354}