001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 */ 021/* 022 * BioJava development code 023 * 024 * Copyright for this code is held jointly by the individual 025 * authors. These should be listed in @author doc comments. 026 * 027 * For more information on the BioJava project and its aims, 028 * or to join the biojava-l mailing list, visit the home page 029 * at: 030 * 031 * http://www.biojava.org/ 032 * 033 */ 034 035package org.biojava.nbio.structure.align.util; 036 037import java.beans.BeanInfo; 038import java.beans.Introspector; 039import java.beans.PropertyDescriptor; 040import java.io.*; 041import java.lang.reflect.Array; 042import java.lang.reflect.InvocationTargetException; 043import java.util.*; 044 045/** 046 * Utilities for autoconfiguring javabeans based on command line arguments. 047 * 048 * @author Thomas Down 049 */ 050 051public class CliTools { 052 private CliTools() { 053 } 054 055 056 /** 057 * Configure a JavaBean based on a set of command line arguments. 058 * For a command line construct such as "-foo 42", this method will use 059 * available <code>BeanInfo</code> (usually obtained by introspection) 060 * to find a property named "foo". The argument will be interpreted 061 * according to the type of the "foo" property, then the appropriate 062 * mutator method (generally named setFoo) will be called to configure 063 * the property on the bean. 064 * 065 * <p> 066 * Currently supported property types are <code>int, double, 067 * boolean, String, File, Reader, Writer, InputStream, OutputStream, Enum</code>, 068 * plus arrays of all the above types. In the case of arrays, the option 069 * may appear multiple times on the command line, otherwise recurrance of 070 * the same option is an error. 071 * </p> 072 * 073 * <p> 074 * For stream types, the parameter is interpreted as a filename unless it 075 * is equal to "-" in which case standard input or standard output are 076 * used as appropriate. Each of the standard streams may only be used 077 * one. 078 * </p> 079 * 080 * <p> 081 * In the future, this method will probably be extended to handle multiple 082 * parameter occurances, and use Annotations to generate more useful help 083 * messages when something goes wrong. 084 * </p> 085 * 086 * @param bean 087 * @param args 088 * @return A string array which contains any 'anonymous' arguments (may be empty) 089 * @throws ConfigurationException 090 */ 091 092 @SuppressWarnings({ "rawtypes", "unchecked" }) 093 public static String[] configureBean(Object bean, String[] args) 094 throws ConfigurationException 095 { 096 BeanInfo bi; 097 try { 098 bi = Introspector.getBeanInfo(bean.getClass()); 099 } catch (Exception ex) { 100 throw new ConfigurationException("Couldn't get information for target bean " + ex.getMessage()); 101 } 102 103 Map<String,PropertyDescriptor> propertiesByName = new HashMap<String, PropertyDescriptor>(); 104 for (PropertyDescriptor pd : bi.getPropertyDescriptors() ) { 105 propertiesByName.put(pd.getName(), pd); 106 } 107 108 List<String> anonArgs = new ArrayList<String>(); 109 Map<PropertyDescriptor,List<String>> arrayProps = new HashMap<PropertyDescriptor, List<String>>(); 110 Set<PropertyDescriptor> usedProps = new HashSet<PropertyDescriptor>(); 111 112 boolean stdInUsed = false; 113 boolean stdOutUsed = false; 114 115 for (int i = 0; i < args.length; ++i) { 116 String arg = args[i]; 117 118 //System.out.println("checking argument: " + arg); 119 120 if ( arg == null) 121 continue; 122 123 if ((arg.length() > 0 ) && arg.charAt(0) == '-') { 124 PropertyDescriptor pd = propertiesByName.get(arg.substring(1)); 125 126 boolean arrayMode = false; 127 Object propVal = null; 128 Class propType = null; 129 130 if (pd == null) { 131 if (arg.startsWith("-no")) { 132 String altPropName = Introspector.decapitalize(arg.substring(3)); 133 pd = propertiesByName.get(altPropName); 134 if (pd == null) { 135 throw new ConfigurationException("No property named " + arg.substring(1) + " or " + altPropName); 136 } 137 propType = pd.getPropertyType(); 138 if (propType == Boolean.TYPE) { 139 propVal = Boolean.FALSE; 140 } else { 141 throw new ConfigurationException("Negatory option " + arg + " does not refer to a boolean property"); 142 } 143 } else { 144 throw new ConfigurationException("No property named " + arg.substring(1)); 145 } 146 } else { 147 propType = pd.getPropertyType(); 148 149 if (propType.isArray()) { 150 arrayMode = true; 151 propType = propType.getComponentType(); 152 } 153 154 if (propType == Integer.TYPE) { 155 try { 156 propVal = new Integer(args[++i]); 157 } catch (Exception ex) { 158 throw new ConfigurationException("Option " + arg + " requires an integer parameter"); 159 } 160 } else if (propType == Double.TYPE || propType == Double.class ) { 161 try { 162 propVal = new Double(args[++i]); 163 } catch (Exception ex) { 164 throw new ConfigurationException("Option " + arg + " requires a numerical parameter"); 165 } 166 } else if (propType == String.class) { 167 propVal = args[++i]; 168 } else if (propType == Boolean.TYPE) { 169 170 String val = args[++i]; 171 if ( val == null ) 172 propVal = Boolean.TRUE; 173 else { 174 if ( val.equalsIgnoreCase("true") || val.equalsIgnoreCase("t")) 175 propVal = Boolean.TRUE; 176 else if( val.equalsIgnoreCase("false") || val.equalsIgnoreCase("f")) 177 propVal = Boolean.FALSE; 178 else 179 throw new ConfigurationException("Option "+arg+" requires a boolean parameter"); 180 } 181 } else if (File.class.isAssignableFrom(propType)) { 182 // can't distinguish if the file is for reading or writing 183 // at the moment, so accept it without validation. 184 propVal = new File(args[++i]); 185 } else if (Reader.class.isAssignableFrom(propType)) { 186 String name = args[++i]; 187 if ("-".equals(name)) { 188 if (stdInUsed) { 189 throw new ConfigurationException("Can't use standard input more than once"); 190 } 191 propVal = new InputStreamReader(System.in); 192 stdInUsed = true; 193 } else { 194 try { 195 propVal = new FileReader(new File(name)); 196 } catch (Exception ex) { 197 throw new ConfigurationException("Can't open " + name + " for input"); 198 } 199 } 200 } else if (InputStream.class.isAssignableFrom(propType)) { 201 String name = args[++i]; 202 if ("-".equals(name)) { 203 if (stdInUsed) { 204 throw new ConfigurationException("Can't use standard input more than once"); 205 } 206 propVal = System.in; 207 stdInUsed = true; 208 } else { 209 try { 210 propVal = new FileInputStream(new File(name)); 211 } catch (Exception ex) { 212 throw new ConfigurationException("Can't open " + name + " for input"); 213 } 214 } 215 } else if (Writer.class.isAssignableFrom(propType)) { 216 String name = args[++i]; 217 if ("-".equals(name)) { 218 if (stdOutUsed) { 219 throw new ConfigurationException("Can't use standard output more than once"); 220 } 221 propVal = new OutputStreamWriter(System.out); 222 stdOutUsed = true; 223 } else { 224 try { 225 propVal = new FileWriter(new File(name)); 226 } catch (Exception ex) { 227 throw new ConfigurationException("Can't open " + name + " for output"); 228 } 229 } 230 } else if (OutputStream.class.isAssignableFrom(propType)) { 231 String name = args[++i]; 232 if ("-".equals(name)) { 233 if (stdOutUsed) { 234 throw new ConfigurationException("Can't use standard output more than once"); 235 } 236 propVal = System.out; 237 stdOutUsed = true; 238 } else { 239 try { 240 propVal = new FileOutputStream(new File(name)); 241 } catch (Exception ex) { 242 throw new ConfigurationException("Can't open " + name + " for output"); 243 } 244 } 245 } else if( propType.isEnum()) { 246 String name = args[++i]; 247 try { 248 propVal = Enum.valueOf(propType, name); 249 } catch (Exception ex) { 250 try { 251 // Try with uppercase version, as common for enums 252 propVal = Enum.valueOf(propType, name.toUpperCase()); 253 } catch (Exception ex2) { 254 //give up 255 StringBuilder errMsg = new StringBuilder(); 256 errMsg.append("Option ").append(arg); 257 errMsg.append(" requires a ").append(propType.getSimpleName()); 258 errMsg.append(" parameter. One of: "); 259 for(Object val: propType.getEnumConstants() ) { 260 Enum enumVal = (Enum) val; 261 errMsg.append(enumVal.name()); 262 errMsg.append(" "); 263 } 264 throw new ConfigurationException(errMsg.toString()); 265 } 266 } 267 268 } else { 269 System.err.println("Unsupported optionType for " + arg + " propType:" + propType); 270 System.exit(1); 271 } 272 } 273 274 //System.out.println("setting to: " + propVal + " " + propVal.getClass().getName()); 275 276 if (arrayMode) { 277 List valList = arrayProps.get(pd); 278 if (valList == null) { 279 valList = new ArrayList(); 280 arrayProps.put(pd, valList); 281 } 282 valList.add(propVal); 283 } else { 284 if (usedProps.contains(pd)) { 285 throw new ConfigurationException("Multiple values supplied for " + pd.getName()); 286 } 287 try { 288 pd.getWriteMethod().invoke(bean, new Object[] {propVal}); 289 } catch (InvocationTargetException ex) { 290 throw new ConfigurationException("Error configuring '" + pd.getName() + "'"); 291 } catch (Exception ex) { 292 throw new ConfigurationException("Error configuring '" + pd.getName() + "'"); 293 } 294 usedProps.add(pd); 295 } 296 } else { 297 anonArgs.add(arg); 298 } 299 } 300 301 for (Iterator api = arrayProps.entrySet().iterator(); api.hasNext(); ) { 302 Map.Entry me = (Map.Entry) api.next(); 303 PropertyDescriptor pd = (PropertyDescriptor) me.getKey(); 304 List vals = (List) me.getValue(); 305 306 Class compType = pd.getPropertyType().getComponentType(); 307 Object valArray; 308 if (compType.isPrimitive()) { 309 if (compType == Integer.TYPE) { 310 valArray = CollectionTools.toIntArray(vals); 311 } else if (compType == Double.TYPE) { 312 valArray = CollectionTools.toDoubleArray(vals); 313 } else { 314 throw new ConfigurationException("Arrays of type " + compType.getName() + " are currently unsupported"); 315 } 316 } else { 317 valArray = vals.toArray((Object[]) Array.newInstance(compType, vals.size())); 318 } 319 try { 320 pd.getWriteMethod().invoke(bean, new Object[] {valArray}); 321 } catch (InvocationTargetException ex) { 322 throw new ConfigurationException("Error configuring '" + pd.getName() + "'"); 323 } catch (Exception ex) { 324 throw new ConfigurationException("Error configuring '" + pd.getName() + "'"); 325 } 326 } 327 328 return anonArgs.toArray(new String[anonArgs.size()]); 329 } 330 331 /** 332 * Constructs a comma-separated list of values for an enum. 333 * 334 * Example: 335 * > getEnumValues(ScoringStrategy.class) 336 * "CA_SCORING, SIDE_CHAIN_SCORING, SIDE_CHAIN_ANGLE_SCORING, CA_AND_SIDE_CHAIN_ANGLE_SCORING, or SEQUENCE_CONSERVATION" 337 * @param enumClass 338 * @return 339 */ 340 public static <T extends Enum<?>> String getEnumValuesAsString(Class<T> enumClass) { 341 //ScoringStrategy[] vals = ScoringStrategy.values(); 342 T[] vals = enumClass.getEnumConstants(); 343 344 StringBuilder str = new StringBuilder(); 345 if(vals.length == 1) { 346 str.append(vals[0].name()); 347 } else if(vals.length > 1 ) { 348 for(int i=0;i<vals.length-1;i++) { 349 str.append(vals[i].name()); 350 str.append(", "); 351 } 352 str.append("or "); 353 str.append(vals[vals.length-1].name()); 354 } 355 356 return str.toString(); 357 } 358 359}