package it.unimi.dsi.lang; /* * DSI utilities * * Copyright (C) 2006-2009 Paolo Boldi and Sebastiano Vigna * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import com.martiansoftware.jsap.ParseException; import com.martiansoftware.jsap.StringParser; /** A parser for simple object specifications based on strings. * * <p>Whenever a particular instance of a class (not a singleton) has to be specified in textual format, * one faces the difficulty of having {@link Class#forName(String)} but no analogous method for instances. This * class provides a method {@link #fromSpec(String, Class, String[], String[])} that will generate object instances * starting from a specification of the form * <pre style="text-align: center; padding: .5em"> * <var>class</var>(<var>arg</var>,…) * </pre> * <p>The format of the specification is rather loose, to ease use on the command line: each argument may or may not * be quote-delimited, with the the proviso that inside quotes you have the usual escape rules, whereas without quotes the * end of the parameter is marked by the next comma or closed parenthesis, and surrounding space is trimmed. For empty constructors, * parentheses can be omitted. Valid examples are, for instance, * <pre style="text-align: center; padding: .5em"> * java.lang.Object * java.lang.Object() * java.lang.String(foo) * java.lang.String("foo") * </pre> * * <p>After parsing, we search for a constructor accepting as many strings as specified arguments, or possibly * a string varargs constructor. The second optional argument will be used to check * that the generated object is of the correct type, and the last argument is a list of packages that * will be prepended in turn to the specified class name. Finally, the last argument is an optional list of static factory method * names that will be tried before resorting to constructors. Several polymorphic versions make it possible to specify * just a subset of the arguments. * * <p>Additionally, it is possible to specify a {@linkplain #fromSpec(Object, String, Class, String[], String[]) <em>context object</em>} * that will be passed to the construction or factory method used to generate the new instance. The context is class dependent, and must * be correctly understood by the target class. In this case, the resolution process described above proceed similarly, but * the signatures searched for contain an additional {@link Object} argument before the string arguments. * * <p>Note that this arrangement requires some collaboration from the specified class, which must provide string-based constructors. * If additionally you plan on saving parseable representations which require more than just the class name, you are invited * to follow the {@link #toSpec(Object)} conventions. * * <p>This class is a <a href="http://www.martiansoftware.com/jsap/"><acronym title="Java-based Simple Argument Parser">JSAP</acronym></a> * {@link StringParser}, and can be used in a JSAP parameter * specifications to build easily objects on the command line. Several constructors make it possible * to generate parsers that will check for type compliance, and possibly attempt to prepend package names. * */ public class ObjectParser extends StringParser { /** A marker object used to denote lack of a context. */ private final static Object NO_CONTEXT = new Object(); /** A list of package names that will be prepended to specifications, or <code>null</code>. */ private final String[] packages; /** A list of factory methods that will be used before trying constructors, or <code>null</code>. */ private final String[] factoryMethod; /** A type that will be used to check instantiated objects. */ private final Class<?> type; /** The context for this parser, or <code>null</code>. */ private final Object context; /** Creates a new object parser with given control type, list of packages and factory methods. * * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. * @param factoryMethod a list of factory methods that will be used before trying constructors, or <code>null</code>. */ public ObjectParser( final Class<?> type, final String[] packages, final String[] factoryMethod ) { this( NO_CONTEXT, type, packages, factoryMethod ); } /** Creates a new object parser with given control type and list of packages. * * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. */ public ObjectParser( final Class<?> type, final String[] packages ) { this( type, packages, null ); } /** Creates a new object parser with given control type. * * @param type a type that will be used to check instantiated objects. */ public ObjectParser( final Class<?> type ) { this( type, (String[])null ); } /** Creates a new object parser. */ public ObjectParser() { this( Object.class ); } /** Creates a new object parser with given context, control type, list of packages and factory methods. * * @param context the context for this parser (will be passed on to instantiated objects)—possibly <code>null</code>. * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. * @param factoryMethod a list of factory methods that will be used before trying constructors, or <code>null</code>. */ public ObjectParser( final Object context, final Class<?> type, final String[] packages, final String[] factoryMethod ) { this.context = context; this.type = type; this.packages = packages; this.factoryMethod = factoryMethod; } /** Creates a new object parser with given context, control type and list of packages. * * @param context the context for this parser (will be passed on to instantiated objects)—possibly <code>null</code>. * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. */ public ObjectParser( final Object context, final Class<?> type, final String[] packages ) { this( context, type, packages, null ); } /** Creates a new object parser with given context and control type. * * @param context the context for this parser (will be passed on to instantiated objects)—possibly <code>null</code>. * @param type a type that will be used to check instantiated objects. */ public ObjectParser( final Object context, final Class<?> type ) { this( context, type, null ); } /** Creates a new object parser with given context. * @param context the context for this parser (will be passed on to instantiated objects)—possibly <code>null</code>. */ public ObjectParser( final Object context ) { this( context, Object.class ); } @Override public Object parse( String spec ) throws ParseException { try { return fromSpec( context, spec, type, packages, factoryMethod ); } catch ( Exception e ) { throw new ParseException( e ); } } /** Creates a new instance from a specification. * * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). */ public static Object fromSpec( String spec ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( NO_CONTEXT, spec, Object.class, null, null ); } /** Creates a new instance from a specification using a given control type. * * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. */ public static <S> S fromSpec( String spec, final Class<S> type ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( NO_CONTEXT, spec, type, null, null ); } /** Creates a new instance from a specification using a given control type, list of packages and factory methods. * * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. */ public static <S> S fromSpec( String spec, final Class<S> type, final String[] packages ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( NO_CONTEXT, spec, type, packages, null ); } /** Creates a new instance from a specification using a given control type and list of packages. * * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. * @param factoryMethod a list of factory methods that will be used before trying constructors, or <code>null</code>. */ public static <S> S fromSpec( String spec, final Class<S> type, final String[] packages, final String[] factoryMethod ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( NO_CONTEXT, spec, type, packages, factoryMethod ); } /** Creates a new instance from a context and a specification. * * @param context a context object, or <code>null</code>. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). */ public static Object fromSpec( Object context, String spec ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( context, spec, Object.class, null, null ); } /** Creates a new instance from a context and a specification using a given control type. * * @param context a context object, or <code>null</code>. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. */ public static <S> S fromSpec( Object context, String spec, final Class<S> type ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( context, spec, type, null, null ); } /** Creates a new instance from a context and a specification using a given control type, list of packages and factory methods. * * @param context a context object, or <code>null</code>. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. */ public static <S> S fromSpec( Object context, String spec, final Class<S> type, final String[] packages ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { return fromSpec( context, spec, type, packages, null ); } /** Creates a new instance from a context and a specification using a given control type and list of packages. * * @param context a context object, or <code>null</code>. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. * @param packages a list of package names that will be prepended to the specification, or <code>null</code>. * @param factoryMethod a list of factory methods that will be used before trying constructors, or <code>null</code>. */ @SuppressWarnings("unchecked") public static <S> S fromSpec( final Object context, String spec, final Class<S> type, final String[] packages, final String[] factoryMethod ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { spec = spec.trim(); final boolean contextualised = context != NO_CONTEXT; int endOfName = spec.indexOf( '(' ); final int length = spec.length(); if ( endOfName < 0 ) endOfName = length; Class<? extends S> klass = null; final String className = spec.substring( 0, endOfName ).trim(); try { klass = (Class<? extends S>)Class.forName( className ); } catch( ClassNotFoundException e ) { // We try by prefixing with the given packages if ( packages != null ) for ( String p : packages ) { try { klass = (Class<? extends S>)Class.forName( p + "." + className ); } catch ( ClassNotFoundException niceTry ) {} if ( klass != null ) break; } } if ( klass == null ) throw new ClassNotFoundException( className ); if ( ! type.isAssignableFrom( klass ) ) throw new ClassCastException( "Class " + klass.getSimpleName() + " is not assignable to " + type ); final ObjectArrayList<Object> args = new ObjectArrayList<Object>(); if ( contextualised ) args.add( context ); if ( endOfName < length ) { boolean inQuotes, escaped; MutableString arg = new MutableString(); if ( spec.charAt( length - 1 ) != ')' ) throw new IllegalArgumentException( "\")\" missing at the end of argument list" ); int pos = endOfName; while( pos < length ) { // Skip the current delimiter ('(', ',' or ')'). pos++; // Skip whitespace before next argument while( pos < length && Character.isWhitespace( spec.charAt( pos ) ) ) pos++; // We are at the end of the specification. if ( pos == length || args.size() == 0 && pos == length - 1 && spec.charAt( pos ) == ')' ) break; arg.setLength( 0 ); // If we find quotes, we skip then and go into quote mode. if ( inQuotes = spec.charAt( pos ) == '"' ) pos++; escaped = false; char c; for(;;) { c = spec.charAt( pos ); if ( ! inQuotes ) { if ( c == ',' || pos == length - 1 && c == ')' ) break; arg.append( c ); } else { if ( c == '"' && ! escaped ) { do pos++; while( pos < length && Character.isWhitespace( spec.charAt( pos ) ) ); if ( pos == length || ( spec.charAt( pos ) != ')' && spec.charAt( pos ) != ',' ) ) throw new IllegalArgumentException(); break; } if ( c == '\\' && ! escaped ) escaped = true; else { arg.append( c ); escaped = false; } } pos++; } args.add( inQuotes ? arg.toString() : arg.trim().toString() ); } } final Object[] argArray = args.toArray(); final String[] stringArgArray; final Class<?>[] argTypes; if ( contextualised ) { argTypes = new Class[ args.size() ]; stringArgArray = new String[ args.size() - 1 ]; argTypes[ 0 ] = Object.class; for ( int i = 1; i < argTypes.length; i++ ) { argTypes[ i ] = String.class; stringArgArray[ i - 1 ] = (String)args.get( i ); } } else { argTypes = new Class[ args.size() ]; stringArgArray = new String[ args.size() ]; for ( int i = 0; i < argTypes.length; i++ ) { argTypes[ i ] = String.class; stringArgArray[ i ] = (String)args.get( i ); } } Method method = null; S instance = null; if ( factoryMethod != null ) for( String f: factoryMethod ) { // Exact match try { method = klass.getMethod( f, argTypes ); if ( Modifier.isStatic( method.getModifiers() ) ) instance = (S)method.invoke( null, argArray ); } catch ( NoSuchMethodException niceTry ) {} if ( instance != null ) return instance; // Varargs try { if ( contextualised ) { method = klass.getMethod( f, Object.class, String[].class ); if ( Modifier.isStatic( method.getModifiers() ) ) instance = (S)method.invoke( null, context, stringArgArray ); } else { method = klass.getMethod( f, String[].class ); if ( Modifier.isStatic( method.getModifiers() ) ) instance = (S)method.invoke( null, (Object)stringArgArray ); } } catch ( NoSuchMethodException niceTry ) {} if ( instance != null ) return instance; } Constructor<? extends S> constr; // Exact match try { constr = klass.getConstructor( argTypes ); instance = constr.newInstance( argArray ); } catch ( NoSuchMethodException niceTry ) {} if ( instance != null ) return instance; // Varargs try { if ( contextualised ) { constr = klass.getConstructor( Object.class, String[].class ); return constr.newInstance( context, stringArgArray ); } else { constr = klass.getConstructor( String[].class ); return constr.newInstance( (Object)stringArgArray ); } } catch ( NoSuchMethodException e ) { throw new NoSuchMethodException( contextualised ? "No contextual constructor with " + stringArgArray.length + " strings as argument for class " + klass.getName() : "No constructor with " + stringArgArray.length + " strings as argument for class " + klass.getName() ); } } /** Generates a parseable representation of an object fetching by reflection a <code>toSpec()</code> method, or using the class name. * * <p>The standard approach to generate a parseable representation would be to have some interface specifying a no-arg <code>toSpec()</code> * method returning a {@link String}. Since most of the typically parsed objects are singletons, and often one does not need to save a parseable * representation, we rather fetch such a method if available, but we will otherwise return just the class name. * * @param o an object. * @return hopefully, a parseable representation of the object. * @see #fromSpec(String, Class, String[], String[]) */ public static String toSpec( final Object o ) { Method toSpec = null; try { toSpec = o.getClass().getMethod( "toSpec" ); } catch ( Exception e ) {} if ( toSpec != null ) try { return (String)toSpec.invoke( o ); } catch ( Exception e ) { throw new RuntimeException( e ); } return o.getClass().getName(); } }