src.it.unimi.dsi.lang.ObjectParser Maven / Gradle / Ivy
package it.unimi.dsi.lang;
/*
* DSI utilities
*
* Copyright (C) 2006-2017 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 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