001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */
019: package org.apache.openjpa.lib.util;
020:
021: import java.lang.reflect.Constructor;
022: import java.lang.reflect.Field;
023: import java.lang.reflect.Member;
024: import java.lang.reflect.Method;
025: import java.security.AccessController;
026: import java.security.PrivilegedActionException;
027: import java.util.Collection;
028: import java.util.Iterator;
029: import java.util.LinkedList;
030: import java.util.List;
031: import java.util.Map;
032: import java.util.Properties;
033: import java.util.TreeSet;
034:
035: import org.apache.commons.lang.StringUtils;
036:
037: import serp.util.Strings;
038:
039: /**
040: * A specialization of the {@link Properties} map type with the added
041: * abilities to read application options from the command line and to
042: * use bean patterns to set an object's properties via command-line the
043: * stored mappings.
044: * A typical use pattern for this class is to construct a new instance
045: * in the <code>main</code> method, then call {@link #setFromCmdLine} with the
046: * given args. Next, an instanceof the class being invoked is created, and
047: * {@link #setInto} is called with that instance as a parameter. With this
048: * pattern, the user can configure any bean properties of the class, or even
049: * properties of classes reachable from the class, through the command line.
050: *
051: * @author Abe White
052: * @nojavadoc
053: */
054: public class Options extends TypedProperties {
055:
056: /**
057: * Immutable empty instance.
058: */
059: public static Options EMPTY = new EmptyOptions();
060:
061: // maps primitive types to the appropriate wrapper class and default value
062: private static Object[][] _primWrappers = new Object[][] {
063: { boolean.class, Boolean.class, Boolean.FALSE },
064: { byte.class, Byte.class, new Byte((byte) 0) },
065: { char.class, Character.class, new Character((char) 0) },
066: { double.class, Double.class, new Double(0D) },
067: { float.class, Float.class, new Float(0F) },
068: { int.class, Integer.class, new Integer(0) },
069: { long.class, Long.class, new Long(0L) },
070: { short.class, Short.class, new Short((short) 0) }, };
071:
072: /**
073: * Default constructor.
074: */
075: public Options() {
076: super ();
077: }
078:
079: /**
080: * Construct the options instance with the given set of defaults.
081: *
082: * @see Properties#Properties(Properties)
083: */
084: public Options(Properties defaults) {
085: super (defaults);
086: }
087:
088: /**
089: * Parses the given argument list into flag/value pairs, which are stored
090: * as properties. Flags that are present without values are given
091: * the value "true". If any flag is found for which there is already
092: * a mapping present, the existing mapping will be overwritten.
093: * Flags should be of the form:<br />
094: * <code>java Foo -flag1 value1 -flag2 value2 ... arg1 arg2 ...</code>
095: *
096: * @param args the command-line arguments
097: * @return all arguments in the original array beyond the
098: * flag/value pair list
099: * @author Patrick Linskey
100: */
101: public String[] setFromCmdLine(String[] args) {
102: if (args == null || args.length == 0)
103: return args;
104:
105: String key = null;
106: String value = null;
107: List remainder = new LinkedList();
108: for (int i = 0; i < args.length + 1; i++) {
109: if (i == args.length || args[i].startsWith("-")) {
110: key = trimQuote(key);
111: if (key != null) {
112: if (!StringUtils.isEmpty(value))
113: setProperty(key, trimQuote(value));
114: else
115: setProperty(key, "true");
116: }
117:
118: if (i == args.length)
119: break;
120: else {
121: key = args[i].substring(1);
122: value = null;
123: }
124: } else if (key != null) {
125: setProperty(key, trimQuote(args[i]));
126: key = null;
127: } else
128: remainder.add(args[i]);
129: }
130:
131: return (String[]) remainder
132: .toArray(new String[remainder.size()]);
133: }
134:
135: /**
136: * This method uses reflection to set all the properties in the given
137: * object that are named by the keys in this map. For a given key 'foo',
138: * the algorithm will look for a 'setFoo' method in the given instance.
139: * For a given key 'foo.bar', the algorithm will first look for a
140: * 'getFoo' method in the given instance, then will recurse on the return
141: * value of that method, now looking for the 'bar' property. This allows
142: * the setting of nested object properties. If in the above example the
143: * 'getFoo' method is not present or returns null, the algorithm will
144: * look for a 'setFoo' method; if found it will constrct a new instance
145: * of the correct type, set it using the 'setFoo' method, then recurse on
146: * it as above. Property names can be nested in this way to an arbitrary
147: * depth. For setter methods that take multiple parameters, the value
148: * mapped to the key can use the ',' as an argument separator character.
149: * If not enough values are present for a given method after splitting
150: * the string on ',', the remaining arguments will receive default
151: * values. All arguments are converted from string form to the
152: * correct type if possible(i.e. if the type is primitive,
153: * java.lang.Clas, or has a constructor that takes a single string
154: * argument). Examples:
155: * <ul>
156: * <li>Map Entry: <code>"age"->"12"</code><br />
157: * Resultant method call: <code>obj.setAge(12)</code></li>
158: * <li>Map Entry: <code>"range"->"1,20"</code><br />
159: * Resultant method call: <code>obj.setRange(1, 20)</code></li>
160: * <li>Map Entry: <code>"range"->"10"</code><br />
161: * Resultant method call: <code>obj.setRange(10, 10)</code></li>
162: * <li>Map Entry: <code>"brother.name"->"Bob"</code><br />
163: * Resultant method call: <code>obj.getBrother().setName("Bob")
164: * <code></li>
165: * </ul>
166: * Any keys present in the map for which there is no
167: * corresponding property in the given object will be ignored,
168: * and will be returned in the {@link Map} returned by this method.
169: *
170: * @return an {@link Options} of key-value pairs in this object
171: * for which no setters could be found.
172: * @throws RuntimeException on parse error
173: */
174: public Options setInto(Object obj) {
175: // set all defaults that have no explicit value
176: Map.Entry entry = null;
177: if (defaults != null) {
178: for (Iterator itr = defaults.entrySet().iterator(); itr
179: .hasNext();) {
180: entry = (Map.Entry) itr.next();
181: if (!containsKey(entry.getKey()))
182: setInto(obj, entry);
183: }
184: }
185:
186: // set from main map
187: Options invalidEntries = null;
188: Map.Entry e;
189: for (Iterator itr = entrySet().iterator(); itr.hasNext();) {
190: e = (Map.Entry) itr.next();
191: if (!setInto(obj, e)) {
192: if (invalidEntries == null)
193: invalidEntries = new Options();
194: invalidEntries.put(e.getKey(), e.getValue());
195: }
196: }
197: return (invalidEntries == null) ? EMPTY : invalidEntries;
198: }
199:
200: /**
201: * Sets the property named by the key of the given entry in the
202: * given object.
203: *
204: * @return <code>true</code> if the set succeeded, or
205: * <code>false</code> if no method could be found for this property.
206: */
207: private boolean setInto(Object obj, Map.Entry entry) {
208: if (entry.getKey() == null)
209: return false;
210:
211: try {
212: // look for matching parameter of object
213: Object[] match = new Object[] { obj, null };
214: if (!matchOptionToMember(entry.getKey().toString(), match))
215: return false;
216:
217: Class[] type = getType(match[1]);
218: Object[] values = new Object[type.length];
219: String[] strValues;
220: if (entry.getValue() == null)
221: strValues = new String[1];
222: else if (values.length == 1)
223: strValues = new String[] { entry.getValue().toString() };
224: else
225: strValues = Strings.split(entry.getValue().toString(),
226: ",", 0);
227:
228: // convert the string values into parameter values, if not
229: // enough string values repeat last one for rest
230: for (int i = 0; i < strValues.length; i++)
231: values[i] = stringToObject(strValues[i].trim(), type[i]);
232: for (int i = strValues.length; i < values.length; i++)
233: values[i] = getDefaultValue(type[i]);
234:
235: // invoke the setter / set the field
236: invoke(match[0], match[1], values);
237: return true;
238: } catch (Throwable t) {
239: throw new ParseException(obj + "." + entry.getKey() + " = "
240: + entry.getValue(), t);
241: }
242: }
243:
244: /**
245: * Removes leading and trailing single quotes from the given String, if any.
246: */
247: private static String trimQuote(String val) {
248: if (val != null && val.startsWith("'") && val.endsWith("'"))
249: return val.substring(1, val.length() - 1);
250: return val;
251: }
252:
253: /**
254: * Finds all the options that can be set on the provided class. This does
255: * not look for path-traversal expressions.
256: *
257: * @param type The class for which available options should be listed.
258: * @return The available option names in <code>type</code>. The
259: * names will have initial caps. They will be ordered alphabetically.
260: */
261: public static Collection findOptionsFor(Class type) {
262: Collection names = new TreeSet();
263: // look for a setter method matching the key
264: Method[] meths = type.getMethods();
265: Class[] params;
266: for (int i = 0; i < meths.length; i++) {
267: if (meths[i].getName().startsWith("set")) {
268: params = meths[i].getParameterTypes();
269: if (params.length == 0)
270: continue;
271: if (params[0].isArray())
272: continue;
273:
274: names.add(StringUtils.capitalize(meths[i].getName()
275: .substring(3)));
276: }
277: }
278:
279: // check for public fields
280: Field[] fields = type.getFields();
281: for (int i = 0; i < fields.length; i++)
282: names.add(StringUtils.capitalize(fields[i].getName()));
283:
284: return names;
285: }
286:
287: /**
288: * Matches a key to an object/setter pair.
289: *
290: * @param key the key given at the command line; may be of the form
291: * 'foo.bar' to signify the 'bar' property of the 'foo' owned object
292: * @param match an array of length 2, where the first index is set
293: * to the object to retrieve the setter for
294: * @return true if a match was made, false otherwise; additionally,
295: * the first index of the match array will be set to
296: * the matching object and the second index will be
297: * set to the setter method or public field for the
298: * property named by the key
299: */
300: private static boolean matchOptionToMember(String key,
301: Object[] match) throws Exception {
302: if (StringUtils.isEmpty(key))
303: return false;
304:
305: // unfortunately we can't use bean properties for setters; any
306: // setter with more than 1 arg is ignored; calc setter and getter
307: // name to look for
308: String[] find = Strings.split(key, ".", 2);
309: String base = StringUtils.capitalize(find[0]);
310: String set = "set" + base;
311: String get = "get" + base;
312:
313: // look for a setter/getter matching the key; look for methods first
314: Class type = match[0].getClass();
315: Method[] meths = type.getMethods();
316: Method setMeth = null;
317: Method getMeth = null;
318: Class[] params;
319: for (int i = 0; i < meths.length; i++) {
320: if (meths[i].getName().equals(set)) {
321: params = meths[i].getParameterTypes();
322: if (params.length == 0)
323: continue;
324: if (params[0].isArray())
325: continue;
326:
327: // use this method if we haven't found any other setter, if
328: // it has less parameters than any other setter, or if it uses
329: // string parameters
330: if (setMeth == null)
331: setMeth = meths[i];
332: else if (params.length < setMeth.getParameterTypes().length)
333: setMeth = meths[i];
334: else if (params.length == setMeth.getParameterTypes().length
335: && params[0] == String.class)
336: setMeth = meths[i];
337: } else if (meths[i].getName().equals(get))
338: getMeth = meths[i];
339: }
340:
341: // if no methods found, check for public field
342: Member setter = setMeth;
343: Member getter = getMeth;
344: if (setter == null) {
345: Field[] fields = type.getFields();
346: String uncapBase = StringUtils.uncapitalize(find[0]);
347: for (int i = 0; i < fields.length; i++) {
348: if (fields[i].getName().equals(base)
349: || fields[i].getName().equals(uncapBase)) {
350: setter = fields[i];
351: getter = fields[i];
352: break;
353: }
354: }
355: }
356:
357: // if no way to access property, give up
358: if (setter == null && getter == null)
359: return false;
360:
361: // recurse on inner object with remainder of key?
362: if (find.length > 1) {
363: Object inner = null;
364: if (getter != null)
365: inner = invoke(match[0], getter, null);
366:
367: // if no getter or current inner is null, try to create a new
368: // inner instance and set it in object
369: if (inner == null && setter != null) {
370: Class innerType = getType(setter)[0];
371: try {
372: inner = AccessController
373: .doPrivileged(J2DoPrivHelper
374: .newInstanceAction(innerType));
375: } catch (PrivilegedActionException pae) {
376: throw pae.getException();
377: }
378: invoke(match[0], setter, new Object[] { inner });
379: }
380: match[0] = inner;
381: return matchOptionToMember(find[1], match);
382: }
383:
384: // got match; find setter for property
385: match[1] = setter;
386: return match[1] != null;
387: }
388:
389: /**
390: * Return the types of the parameters needed to set the given member.
391: */
392: private static Class[] getType(Object member) {
393: if (member instanceof Method)
394: return ((Method) member).getParameterTypes();
395: return new Class[] { ((Field) member).getType() };
396: }
397:
398: /**
399: * Set the given member to the given value(s).
400: */
401: private static Object invoke(Object target, Object member,
402: Object[] values) throws Exception {
403: if (member instanceof Method)
404: return ((Method) member).invoke(target, values);
405: if (values == null || values.length == 0)
406: return ((Field) member).get(target);
407: ((Field) member).set(target, values[0]);
408: return null;
409: }
410:
411: /**
412: * Converts the given string into an object of the given type, or its
413: * wrapper type if it is primitive.
414: */
415: private Object stringToObject(String str, Class type)
416: throws Exception {
417: // special case for null and for strings
418: if (str == null || type == String.class)
419: return str;
420:
421: // special case for creating Class instances
422: if (type == Class.class)
423: return Class.forName(str, false, getClass()
424: .getClassLoader());
425:
426: // special case for numeric types that end in .0; strip the decimal
427: // places because it can kill int, short, long parsing
428: if (type.isPrimitive() || Number.class.isAssignableFrom(type))
429: if (str.length() > 2 && str.endsWith(".0"))
430: str = str.substring(0, str.length() - 2);
431:
432: // for primitives, recurse on wrapper type
433: if (type.isPrimitive())
434: for (int i = 0; i < _primWrappers.length; i++)
435: if (type == _primWrappers[i][0])
436: return stringToObject(str,
437: (Class) _primWrappers[i][1]);
438:
439: // look for a string constructor
440: Exception err = null;
441: try {
442: Constructor cons = type
443: .getConstructor(new Class[] { String.class });
444: if (type == Boolean.class && "t".equalsIgnoreCase(str))
445: str = "true";
446: return cons.newInstance(new Object[] { str });
447: } catch (Exception e) {
448: err = e;
449: }
450:
451: // special case: the arg value is a subtype name and a new instance
452: // of that type should be set as the object
453: Class subType = null;
454: try {
455: subType = Class.forName(str);
456: } catch (Exception e) {
457: throw err;
458: }
459: if (!type.isAssignableFrom(subType))
460: throw err;
461: try {
462: return AccessController.doPrivileged(J2DoPrivHelper
463: .newInstanceAction(subType));
464: } catch (PrivilegedActionException pae) {
465: throw pae.getException();
466: }
467: }
468:
469: /**
470: * Returns the default value for the given parameter type.
471: */
472: private Object getDefaultValue(Class type) {
473: for (int i = 0; i < _primWrappers.length; i++)
474: if (_primWrappers[i][0] == type)
475: return _primWrappers[i][2];
476:
477: return null;
478: }
479:
480: /**
481: * Specialization of {@link #getBooleanProperty} to allow
482: * a value to appear under either of two keys; useful for short and
483: * long versions of command-line flags.
484: */
485: public boolean getBooleanProperty(String key, String key2,
486: boolean def) {
487: String val = getProperty(key);
488: if (val == null)
489: val = getProperty(key2);
490: if (val == null)
491: return def;
492: return "t".equalsIgnoreCase(val)
493: || "true".equalsIgnoreCase(val);
494: }
495:
496: /**
497: * Specialization of {@link TypedProperties#getFloatProperty} to allow
498: * a value to appear under either of two keys; useful for short and
499: * long versions of command-line flags.
500: */
501: public float getFloatProperty(String key, String key2, float def) {
502: String val = getProperty(key);
503: if (val == null)
504: val = getProperty(key2);
505: return (val == null) ? def : Float.parseFloat(val);
506: }
507:
508: /**
509: * Specialization of {@link TypedProperties#getDoubleProperty} to allow
510: * a value to appear under either of two keys; useful for short and
511: * long versions of command-line flags.
512: */
513: public double getDoubleProperty(String key, String key2, double def) {
514: String val = getProperty(key);
515: if (val == null)
516: val = getProperty(key2);
517: return (val == null) ? def : Double.parseDouble(val);
518: }
519:
520: /**
521: * Specialization of {@link TypedProperties#getLongProperty} to allow
522: * a value to appear under either of two keys; useful for short and
523: * long versions of command-line flags.
524: */
525: public long getLongProperty(String key, String key2, long def) {
526: String val = getProperty(key);
527: if (val == null)
528: val = getProperty(key2);
529: return (val == null) ? def : Long.parseLong(val);
530: }
531:
532: /**
533: * Specialization of {@link TypedProperties#getIntProperty} to allow
534: * a value to appear under either of two keys; useful for short and
535: * long versions of command-line flags.
536: */
537: public int getIntProperty(String key, String key2, int def) {
538: String val = getProperty(key);
539: if (val == null)
540: val = getProperty(key2);
541: return (val == null) ? def : Integer.parseInt(val);
542: }
543:
544: /**
545: * Specialization of {@link Properties#getProperty} to allow
546: * a value to appear under either of two keys; useful for short and
547: * long versions of command-line flags.
548: */
549: public String getProperty(String key, String key2, String def) {
550: String val = getProperty(key);
551: return (val == null) ? getProperty(key2, def) : val;
552: }
553:
554: /**
555: * Specialization of {@link TypedProperties#removeBooleanProperty} to allow
556: * a value to appear under either of two keys; useful for short and
557: * long versions of command-line flags.
558: */
559: public boolean removeBooleanProperty(String key, String key2,
560: boolean def) {
561: String val = removeProperty(key);
562: if (val == null)
563: val = removeProperty(key2);
564: else
565: removeProperty(key2);
566: if (val == null)
567: return def;
568: return "t".equalsIgnoreCase(val)
569: || "true".equalsIgnoreCase(val);
570: }
571:
572: /**
573: * Specialization of {@link TypedProperties#removeFloatProperty} to allow
574: * a value to appear under either of two keys; useful for short and
575: * long versions of command-line flags.
576: */
577: public float removeFloatProperty(String key, String key2, float def) {
578: String val = removeProperty(key);
579: if (val == null)
580: val = removeProperty(key2);
581: else
582: removeProperty(key2);
583: return (val == null) ? def : Float.parseFloat(val);
584: }
585:
586: /**
587: * Specialization of {@link TypedProperties#removeDoubleProperty} to allow
588: * a value to appear under either of two keys; useful for short and
589: * long versions of command-line flags.
590: */
591: public double removeDoubleProperty(String key, String key2,
592: double def) {
593: String val = removeProperty(key);
594: if (val == null)
595: val = removeProperty(key2);
596: else
597: removeProperty(key2);
598: return (val == null) ? def : Double.parseDouble(val);
599: }
600:
601: /**
602: * Specialization of {@link TypedProperties#removeLongProperty} to allow
603: * a value to appear under either of two keys; useful for short and
604: * long versions of command-line flags.
605: */
606: public long removeLongProperty(String key, String key2, long def) {
607: String val = removeProperty(key);
608: if (val == null)
609: val = removeProperty(key2);
610: else
611: removeProperty(key2);
612: return (val == null) ? def : Long.parseLong(val);
613: }
614:
615: /**
616: * Specialization of {@link TypedProperties#removeIntProperty} to allow
617: * a value to appear under either of two keys; useful for short and
618: * long versions of command-line flags.
619: */
620: public int removeIntProperty(String key, String key2, int def) {
621: String val = removeProperty(key);
622: if (val == null)
623: val = removeProperty(key2);
624: else
625: removeProperty(key2);
626: return (val == null) ? def : Integer.parseInt(val);
627: }
628:
629: /**
630: * Specialization of {@link Properties#removeProperty} to allow
631: * a value to appear under either of two keys; useful for short and
632: * long versions of command-line flags.
633: */
634: public String removeProperty(String key, String key2, String def) {
635: String val = removeProperty(key);
636: return (val == null) ? removeProperty(key2, def) : val;
637: }
638:
639: /**
640: * Immutable empty options.
641: */
642: private static class EmptyOptions extends Options {
643:
644: public Object setProperty(String key, String value) {
645: throw new UnsupportedOperationException();
646: }
647:
648: public Object put(Object key, Object value) {
649: throw new UnsupportedOperationException();
650: }
651: }
652: }
|