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}