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 mishmash of classes accumulated during the last twenty 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-2020 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 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;

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

/** 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 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 (lacking such a list, * {@link #DEFAULT_FACTORY_METHODS} will be used). Several polymorphic versions make it possible to specify * just a subset of the arguments. Note that if you provide a specific list of factory methods they * will be tried before constructors, whereas {@linkplain #DEFAULT_FACTORY_METHODS default} factory methods will be tried * after constructors. * *

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(); /** Standard names for factory methods. */ public final static String[] DEFAULT_FACTORY_METHODS = { "getInstance", "newInstance", "valueOf" }; /** 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, DEFAULT_FACTORY_METHODS); } /** 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, DEFAULT_FACTORY_METHODS); } /** 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(final String spec) throws ParseException { try { return fromSpec(context, spec, type, packages, factoryMethod); } catch (final 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(final String spec) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(NO_CONTEXT, spec, Object.class, null, DEFAULT_FACTORY_METHODS); } /** 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(final String spec, final Class type) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(NO_CONTEXT, spec, type, null, DEFAULT_FACTORY_METHODS); } /** 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(final String spec, final Class type, final String[] packages) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(NO_CONTEXT, spec, type, packages, DEFAULT_FACTORY_METHODS); } /** 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(final Object context, final String spec) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(context, spec, Object.class, null, DEFAULT_FACTORY_METHODS); } /** 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(final Object context, final String spec, final Class type) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(context, spec, type, null, DEFAULT_FACTORY_METHODS); } /** 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(final Object context, final String spec, final Class type, final String[] packages) throws IllegalArgumentException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, IOException { return fromSpec(context, spec, type, packages, DEFAULT_FACTORY_METHODS); } /** 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(final 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(final ClassNotFoundException e) { // We try by prefixing with the given packages if (packages != null) for (final String p : packages) { try { klass = (Class)Class.forName(p + "." + className); } catch (final 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; final 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++; } if (inQuotes) args.add(arg.toString()); else { final String s = arg.trim().toString(); if ("null".equals(s)) args.add(null); else args.add(s); } } } 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; @SuppressWarnings("null") S instance = null; // First try with provided, non-default factory methods if (factoryMethod != null && factoryMethod != DEFAULT_FACTORY_METHODS) for(final String f: factoryMethod) { // Exact match try { method = klass.getMethod(f, argTypes); if (Modifier.isStatic(method.getModifiers())) instance = (S)method.invoke(null, argArray); } catch (final 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 (final NoSuchMethodException niceTry) {} if (instance != null) return instance; } Constructor constr; // Exact match try { constr = klass.getConstructor(argTypes); instance = constr.newInstance(argArray); } catch (final 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 (final NoSuchMethodException e) { // Second try with default factory methods (copy-and-paste code) assert factoryMethod == null || factoryMethod == DEFAULT_FACTORY_METHODS; if (factoryMethod == DEFAULT_FACTORY_METHODS) for(final String f: factoryMethod) { // Exact match try { method = klass.getMethod(f, argTypes); if (Modifier.isStatic(method.getModifiers())) instance = (S)method.invoke(null, argArray); } catch (final 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 (final NoSuchMethodException niceTry) {} if (instance != null) return instance; } throw new NoSuchMethodException(contextualised ? "No contextual constructor " + (factoryMethod != null ? "or factory method " : "") + "with " + stringArgArray.length + " strings as argument for class " + klass.getName() : "No constructor " + (factoryMethod != null ? "or factory method " : "") + " 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 (final Exception e) {} if (toSpec != null) try { return (String)toSpec.invoke(o); } catch (final Exception e) { throw new RuntimeException(e); } return o.getClass().getName(); } }