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}