All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.it.unimi.dsi.lang.ObjectParser Maven / Gradle / Ivy

Go to download

The DSI utilities are a mish mash of classes accumulated during the last ten years in projects developed at the DSI (Dipartimento di Scienze dell'Informazione, i.e., Information Sciences Department), now DI (Dipartimento di Informatica, i.e., Informatics Department), of the Universita` degli Studi di Milano.

There is a newer version: 2.7.3
Show newest version
package it.unimi.dsi.lang;

/*		 
 * DSI utilities
 *
 * Copyright (C) 2006-2016 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 3 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, see .
 *
 */


import it.unimi.dsi.fastutil.io.BinIO;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;

import java.io.IOException;
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.
 * 
 * 

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 *

 * class(arg,…)
 * 
* *

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, *

 * java.lang.Object
 * java.lang.Object()
 * java.lang.String(foo)
 * java.lang.String("foo")
 * 
* *

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. * *

Alternatively, a specification starting with file: will be interpreted as the filename of a * serialized object, which will be deserialized and returned. This approach makes it possible to have a single * string-based constructor for both serialized objects and textually-described objects, which is often convenient. * *

Additionally, it is possible to specify a {@linkplain #fromSpec(Object, String, Class, String[], String[]) context object} * 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. * *

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. * *

This class is a JSAP * {@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}. */ private final String[] packages; /** A list of factory methods that will be used before trying constructors, or {@code null}. */ 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}. */ 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}. * @param factoryMethod a list of factory methods that will be used before trying constructors, or {@code null}. */ 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}. */ 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}. * @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}. * @param factoryMethod a list of factory methods that will be used before trying constructors, or {@code null}. */ 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}. * @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}. */ 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}. * @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}. */ 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}). * @return an instance generated using the given specification and no ancillary data. */ public static Object fromSpec( String spec ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { 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. * @return an instance generated using the given specification. */ public static S fromSpec( String spec, final Class type ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { 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}. * @return an instance generated using the given specification and ancillary data. */ public static S fromSpec( String spec, final Class type, final String[] packages ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec( NO_CONTEXT, spec, type, packages, null ); } /** Creates a new instance from a context and a specification. * * @param context a context object, or {@code null}. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @return an instance generated using the given specification and ancillary data. */ public static Object fromSpec( Object context, String spec ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { 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}. * @param spec the object specification (see the {@linkplain ObjectParser class documentation}). * @param type a type that will be used to check instantiated objects. * @return an instance generated using the given specification and ancillary data. */ public static S fromSpec( Object context, String spec, final Class type ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { 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}. * @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}. * @return an instance generated using the given specification and ancillary data. */ public static S fromSpec( Object context, String spec, final Class type, final String[] packages ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec( 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}. * @param factoryMethod a list of factory methods that will be used before trying constructors, or {@code null}. * @return an instance generated using the given specification and ancillary data. */ public static S fromSpec( String spec, final Class type, final String[] packages, final String[] factoryMethod ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec( NO_CONTEXT, spec, type, packages, factoryMethod ); } /** 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}. * @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}. * @param factoryMethod a list of factory methods that will be used before trying constructors, or {@code null}. * @return an instance generated using the given specification and ancillary data. */ @SuppressWarnings("unchecked") public static S fromSpec( final Object context, String spec, final Class type, final String[] packages, final String[] factoryMethod ) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { spec = spec.trim(); final boolean contextualised = context != NO_CONTEXT; if ( spec.startsWith( "file:" ) ) { // Easy case--we load an object return (S)BinIO.loadObject( spec.substring( 5 ) ); } int endOfName = spec.indexOf( '(' ); final int length = spec.length(); if ( endOfName < 0 ) endOfName = length; Class klass = null; final String className = spec.substring( 0, endOfName ).trim(); try { klass = (Class)Class.forName( className ); } catch( ClassNotFoundException e ) { // We try by prefixing with the given packages if ( packages != null ) for ( String p : packages ) { try { klass = (Class)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 args = new ObjectArrayList(); 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 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 toSpec() method, or using the class name. * *

The standard approach to generate a parseable representation would be to have some interface specifying a no-arg toSpec() * 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(); } }