1 /** 2 * Copyright (C) 2010 the original author or authors. 3 * See the notice.md file distributed with this work for additional 4 * information regarding copyright ownership. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package com.beust.jcommander; 20 21 import com.beust.jcommander.validators.NoValidator; 22 import com.beust.jcommander.validators.NoValueValidator; 23 24 import java.lang.annotation.Annotation; 25 import java.lang.reflect.Field; 26 import java.util.*; 27 import java.util.ResourceBundle; 28 29 public class ParameterDescription { 30 private Object object; 31 32 private WrappedParameter wrappedParameter; 33 private Parameter parameterAnnotation; 34 private DynamicParameter dynamicParameterAnnotation; 35 36 /** The field/method */ 37 private Parameterized parameterized; 38 /** Keep track of whether a value was added to flag an error */ 39 private boolean assigned = false; 40 private ResourceBundle bundle; 41 private String description; 42 private JCommander jCommander; 43 private Object defaultObject; 44 /** Longest of the names(), used to present usage() alphabetically */ 45 private String longestName = ""; 46 ParameterDescription(Object object, DynamicParameter annotation, Parameterized parameterized, ResourceBundle bundle, JCommander jc)47 public ParameterDescription(Object object, DynamicParameter annotation, 48 Parameterized parameterized, 49 ResourceBundle bundle, JCommander jc) { 50 if (! Map.class.isAssignableFrom(parameterized.getType())) { 51 throw new ParameterException("@DynamicParameter " + parameterized.getName() 52 + " should be of type " 53 + "Map but is " + parameterized.getType().getName()); 54 } 55 56 dynamicParameterAnnotation = annotation; 57 wrappedParameter = new WrappedParameter(dynamicParameterAnnotation); 58 init(object, parameterized, bundle, jc); 59 } 60 ParameterDescription(Object object, Parameter annotation, Parameterized parameterized, ResourceBundle bundle, JCommander jc)61 public ParameterDescription(Object object, Parameter annotation, Parameterized parameterized, 62 ResourceBundle bundle, JCommander jc) { 63 parameterAnnotation = annotation; 64 wrappedParameter = new WrappedParameter(parameterAnnotation); 65 init(object, parameterized, bundle, jc); 66 } 67 68 /** 69 * Find the resource bundle in the annotations. 70 * @return 71 */ 72 @SuppressWarnings("deprecation") findResourceBundle(Object o)73 private ResourceBundle findResourceBundle(Object o) { 74 ResourceBundle result = null; 75 76 Parameters p = o.getClass().getAnnotation(Parameters.class); 77 if (p != null && ! isEmpty(p.resourceBundle())) { 78 result = ResourceBundle.getBundle(p.resourceBundle(), Locale.getDefault()); 79 } else { 80 com.beust.jcommander.ResourceBundle a = o.getClass().getAnnotation( 81 com.beust.jcommander.ResourceBundle.class); 82 if (a != null && ! isEmpty(a.value())) { 83 result = ResourceBundle.getBundle(a.value(), Locale.getDefault()); 84 } 85 } 86 87 return result; 88 } 89 isEmpty(String s)90 private boolean isEmpty(String s) { 91 return s == null || "".equals(s); 92 } 93 initDescription(String description, String descriptionKey, String[] names)94 private void initDescription(String description, String descriptionKey, String[] names) { 95 this.description = description; 96 if (! "".equals(descriptionKey)) { 97 if (bundle != null) { 98 this.description = bundle.getString(descriptionKey); 99 } 100 } 101 102 for (String name : names) { 103 if (name.length() > longestName.length()) longestName = name; 104 } 105 } 106 107 @SuppressWarnings("unchecked") init(Object object, Parameterized parameterized, ResourceBundle bundle, JCommander jCommander)108 private void init(Object object, Parameterized parameterized, ResourceBundle bundle, 109 JCommander jCommander) { 110 this.object = object; 111 this.parameterized = parameterized; 112 this.bundle = bundle; 113 if (this.bundle == null) { 114 this.bundle = findResourceBundle(object); 115 } 116 this.jCommander = jCommander; 117 118 if (parameterAnnotation != null) { 119 String description; 120 if (Enum.class.isAssignableFrom(parameterized.getType()) 121 && parameterAnnotation.description().isEmpty()) { 122 description = "Options: " + EnumSet.allOf((Class<? extends Enum>) parameterized.getType()); 123 }else { 124 description = parameterAnnotation.description(); 125 } 126 initDescription(description, parameterAnnotation.descriptionKey(), 127 parameterAnnotation.names()); 128 } else if (dynamicParameterAnnotation != null) { 129 initDescription(dynamicParameterAnnotation.description(), 130 dynamicParameterAnnotation.descriptionKey(), 131 dynamicParameterAnnotation.names()); 132 } else { 133 throw new AssertionError("Shound never happen"); 134 } 135 136 try { 137 defaultObject = parameterized.get(object); 138 } catch (Exception e) { 139 } 140 141 // 142 // Validate default values, if any and if applicable 143 // 144 if (defaultObject != null) { 145 if (parameterAnnotation != null) { 146 validateDefaultValues(parameterAnnotation.names()); 147 } 148 } 149 } 150 validateDefaultValues(String[] names)151 private void validateDefaultValues(String[] names) { 152 String name = names.length > 0 ? names[0] : ""; 153 validateValueParameter(name, defaultObject); 154 } 155 getLongestName()156 public String getLongestName() { 157 return longestName; 158 } 159 getDefault()160 public Object getDefault() { 161 return defaultObject; 162 } 163 getDescription()164 public String getDescription() { 165 return description; 166 } 167 getObject()168 public Object getObject() { 169 return object; 170 } 171 getNames()172 public String getNames() { 173 StringBuilder sb = new StringBuilder(); 174 String[] names = wrappedParameter.names(); 175 for (int i = 0; i < names.length; i++) { 176 if (i > 0) sb.append(", "); 177 sb.append(names[i]); 178 } 179 return sb.toString(); 180 } 181 getParameter()182 public WrappedParameter getParameter() { 183 return wrappedParameter; 184 } 185 getParameterized()186 public Parameterized getParameterized() { 187 return parameterized; 188 } 189 isMultiOption()190 private boolean isMultiOption() { 191 Class<?> fieldType = parameterized.getType(); 192 return fieldType.equals(List.class) || fieldType.equals(Set.class) 193 || parameterized.isDynamicParameter(); 194 } 195 addValue(String value)196 public void addValue(String value) { 197 addValue(value, false /* not default */); 198 } 199 200 /** 201 * @return true if this parameter received a value during the parsing phase. 202 */ isAssigned()203 public boolean isAssigned() { 204 return assigned; 205 } 206 207 setAssigned(boolean b)208 public void setAssigned(boolean b) { 209 assigned = b; 210 } 211 212 /** 213 * Add the specified value to the field. First, validate the value if a 214 * validator was specified. Then look up any field converter, then any type 215 * converter, and if we can't find any, throw an exception. 216 */ addValue(String value, boolean isDefault)217 public void addValue(String value, boolean isDefault) { 218 addValue(null, value, isDefault, true, -1); 219 } 220 addValue(String name, String value, boolean isDefault, boolean validate, int currentIndex)221 Object addValue(String name, String value, boolean isDefault, boolean validate, int currentIndex) { 222 p("Adding " + (isDefault ? "default " : "") + "value:" + value 223 + " to parameter:" + parameterized.getName()); 224 if(name == null) { 225 name = wrappedParameter.names()[0]; 226 } 227 if (currentIndex == 00 && assigned && ! isMultiOption() && !jCommander.isParameterOverwritingAllowed() 228 || isNonOverwritableForced()) { 229 throw new ParameterException("Can only specify option " + name + " once."); 230 } 231 232 if (validate) { 233 validateParameter(name, value); 234 } 235 236 Class<?> type = parameterized.getType(); 237 238 Object convertedValue = jCommander.convertValue(getParameterized(), getParameterized().getType(), name, value); 239 if (validate) { 240 validateValueParameter(name, convertedValue); 241 } 242 boolean isCollection = Collection.class.isAssignableFrom(type); 243 244 Object finalValue; 245 if (isCollection) { 246 @SuppressWarnings("unchecked") 247 Collection<Object> l = (Collection<Object>) parameterized.get(object); 248 if (l == null || fieldIsSetForTheFirstTime(isDefault)) { 249 l = newCollection(type); 250 parameterized.set(object, l); 251 } 252 if (convertedValue instanceof Collection) { 253 l.addAll((Collection) convertedValue); 254 } else { 255 l.add(convertedValue); 256 } 257 finalValue = l; 258 } else { 259 // If the field type is not a collection, see if it's a type that contains @SubParameters annotations 260 List<SubParameterIndex> subParameters = findSubParameters(type); 261 if (! subParameters.isEmpty()) { 262 // @SubParameters found 263 finalValue = handleSubParameters(value, currentIndex, type, subParameters); 264 } else { 265 // No, regular parameter 266 wrappedParameter.addValue(parameterized, object, convertedValue); 267 finalValue = convertedValue; 268 } 269 } 270 if (! isDefault) assigned = true; 271 272 return finalValue; 273 } 274 handleSubParameters(String value, int currentIndex, Class<?> type, List<SubParameterIndex> subParameters)275 private Object handleSubParameters(String value, int currentIndex, Class<?> type, 276 List<SubParameterIndex> subParameters) { 277 Object finalValue;// Yes, assign each following argument to the corresponding field of that object 278 SubParameterIndex sai = null; 279 for (SubParameterIndex si: subParameters) { 280 if (si.order == currentIndex) { 281 sai = si; 282 break; 283 } 284 } 285 if (sai != null) { 286 Object objectValue = parameterized.get(object); 287 try { 288 if (objectValue == null) { 289 objectValue = type.newInstance(); 290 parameterized.set(object, objectValue); 291 } 292 wrappedParameter.addValue(parameterized, objectValue, value, sai.field); 293 finalValue = objectValue; 294 } catch (InstantiationException | IllegalAccessException e) { 295 throw new ParameterException("Couldn't instantiate " + type, e); 296 } 297 } else { 298 throw new ParameterException("Couldn't find where to assign parameter " + value + " in " + type); 299 } 300 return finalValue; 301 } 302 getParameterAnnotation()303 public Parameter getParameterAnnotation() { 304 return parameterAnnotation; 305 } 306 307 class SubParameterIndex { 308 int order = -1; 309 Field field; 310 SubParameterIndex(int order, Field field)311 public SubParameterIndex(int order, Field field) { 312 this.order = order; 313 this.field = field; 314 } 315 } 316 findSubParameters(Class<?> type)317 private List<SubParameterIndex> findSubParameters(Class<?> type) { 318 List<SubParameterIndex> result = new ArrayList<>(); 319 for (Field field: type.getDeclaredFields()) { 320 Annotation subParameter = field.getAnnotation(SubParameter.class); 321 if (subParameter != null) { 322 SubParameter sa = (SubParameter) subParameter; 323 result.add(new SubParameterIndex(sa.order(), field)); 324 } 325 } 326 return result; 327 } 328 validateParameter(String name, String value)329 private void validateParameter(String name, String value) { 330 final Class<? extends IParameterValidator> validators[] = wrappedParameter.validateWith(); 331 if (validators != null && validators.length > 0) { 332 for(final Class<? extends IParameterValidator> validator: validators) { 333 validateParameter(this, validator, name, value); 334 } 335 } 336 } 337 validateValueParameter(String name, Object value)338 void validateValueParameter(String name, Object value) { 339 final Class<? extends IValueValidator> validators[] = wrappedParameter.validateValueWith(); 340 if (validators != null && validators.length > 0) { 341 for(final Class<? extends IValueValidator> validator: validators) { 342 validateValueParameter(validator, name, value); 343 } 344 } 345 } 346 validateValueParameter(Class<? extends IValueValidator> validator, String name, Object value)347 public static void validateValueParameter(Class<? extends IValueValidator> validator, 348 String name, Object value) { 349 try { 350 if (validator != NoValueValidator.class) { 351 p("Validating value parameter:" + name + " value:" + value + " validator:" + validator); 352 } 353 validator.newInstance().validate(name, value); 354 } catch (InstantiationException | IllegalAccessException e) { 355 throw new ParameterException("Can't instantiate validator:" + e); 356 } 357 } 358 validateParameter(ParameterDescription pd, Class<? extends IParameterValidator> validator, String name, String value)359 public static void validateParameter(ParameterDescription pd, 360 Class<? extends IParameterValidator> validator, 361 String name, String value) { 362 try { 363 364 if (validator != NoValidator.class) { 365 p("Validating parameter:" + name + " value:" + value + " validator:" + validator); 366 } 367 validator.newInstance().validate(name, value); 368 if (IParameterValidator2.class.isAssignableFrom(validator)) { 369 IParameterValidator2 instance = (IParameterValidator2) validator.newInstance(); 370 instance.validate(name, value, pd); 371 } 372 } catch (InstantiationException | IllegalAccessException e) { 373 throw new ParameterException("Can't instantiate validator:" + e); 374 } catch(ParameterException ex) { 375 throw ex; 376 } catch(Exception ex) { 377 throw new ParameterException(ex); 378 } 379 } 380 381 /* 382 * Creates a new collection for the field's type. 383 * 384 * Currently only List and Set are supported. Support for 385 * Queues and Stacks could be useful. 386 */ 387 @SuppressWarnings("unchecked") newCollection(Class<?> type)388 private Collection<Object> newCollection(Class<?> type) { 389 if (SortedSet.class.isAssignableFrom(type)) return new TreeSet(); 390 else if (LinkedHashSet.class.isAssignableFrom(type)) return new LinkedHashSet(); 391 else if (Set.class.isAssignableFrom(type)) return new HashSet(); 392 else if (List.class.isAssignableFrom(type)) return new ArrayList(); 393 else { 394 throw new ParameterException("Parameters of Collection type '" + type.getSimpleName() 395 + "' are not supported. Please use List or Set instead."); 396 } 397 } 398 399 /* 400 * Tests if its the first time a non-default value is 401 * being added to the field. 402 */ fieldIsSetForTheFirstTime(boolean isDefault)403 private boolean fieldIsSetForTheFirstTime(boolean isDefault) { 404 return (!isDefault && !assigned); 405 } 406 p(String string)407 private static void p(String string) { 408 if (System.getProperty(JCommander.DEBUG_PROPERTY) != null) { 409 JCommander.getConsole().println("[ParameterDescription] " + string); 410 } 411 } 412 413 @Override toString()414 public String toString() { 415 return "[ParameterDescription " + parameterized.getName() + "]"; 416 } 417 isDynamicParameter()418 public boolean isDynamicParameter() { 419 return dynamicParameterAnnotation != null; 420 } 421 isHelp()422 public boolean isHelp() { 423 return wrappedParameter.isHelp(); 424 } 425 isNonOverwritableForced()426 public boolean isNonOverwritableForced() { 427 return wrappedParameter.isNonOverwritableForced(); 428 } 429 } 430