package water.api;
import com.google.gson.Gson;
import water.*;
import water.api.schemas3.FrameV3;
import water.api.schemas3.JobV3;
import water.api.schemas3.KeyV3;
import water.api.schemas3.ModelSchemaV3;
import water.exceptions.H2OIllegalArgumentException;
import water.exceptions.H2OKeyNotFoundArgumentException;
import water.exceptions.H2ONotFoundArgumentException;
import water.fvec.Frame;
import water.util.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Base Schema class; all REST API Schemas inherit from here.
* <p>
* Schema is a primary interface of the REST APIs: all endpoints consume some schema object as an input, and produce
* another schema object as an output (though some endpoints may return nothing).
* <p>
* Schemas, as an external interface, are required to be stable: fields may not be renamed or removed, their types or
* meaning may not change, etc. It is allowed to add new fields to a schema, provided that they are optional, and
* that their default values correspond to the old behavior of the endpoint. If these requirements cannot be met,
* then a new version of a schema class must be created.
* <p>
* Many schemas are in direct correspondence with H2O objects. For example, JobV3 schema represents the Job object.
* These "representative" Iced objects are called "implementation" or "impl", and are parametrized with type I.
* Such representation is necessary in order to ensure stability of the interface: even as Job class evolves, the
* interface of JobV3 schema must not. In the simplest case, when there is 1-to-1 correspondence between fields in
* the impl class and in the schema, we use reflection magic to copy those fields over. The reflection magic is smart
* enough to perform simple field name translations, and even certain type translations (like Keyed objects into Keys).
* If there is no such correspondence, then special type adapters must be written. Right now this is done by
* overriding the {@link #fillImpl(I) fillImpl} and {@link #fillFromImpl(I) fillFromImpl} methods. Usually they will
* want to call super to get the default behavior, and then modify the results a bit (e.g., to map differently-named
* fields, or to compute field values). Transient and static fields are ignored by the reflection magic.
* <p>
* There are also schemas that do not correspond to any H2O object. These are mostly the input schemas (schemas used
* for inputs of api requests). Such schemas should be "implemented" by Iced.
* <p>
* All schemas are expected to be self-documenting, in the sense that all fields within those schemas should carry
* detailed documentation about their meaning, as well as any additional hints about the field's usage. These should
* be annotated using the {@link API @API} interface. If a schema contains a complicated object, then that object
* itself should derive from Schema, so that its fields can also be properly documented. However if the internal
* object is sufficiently simple (say, a Map), then it may be sufficient to document it as a whole and have it derived
* from Iced, not from Schema.
* <p>
* Schema names (getSimpleName()) must be unique within an application. During Schema discovery and registration
* there are checks to ensure this. Each schema is associated with exactly one implementation object, however some
* Iced objects are mapped into multiple schemas.
* <p>
* For V3 Schemas each field had a "direction" (input / output / both), which allowed us to use the same schema as
* both input and output for an endpoint. This is no longer possible in V4: two separate schema classes for input /
* output should be created.
*
* <h1>Usage</h1>
* <p>
* {@link Handler} creates an input schema from the body/parameters of the HTTP request (using
* {@link #fillFromParms(Properties) fillFromParms()}, and passes it on to the corresponding handler method.
* <p>
* Each handler method may modify the input schema and return it as the output schema (common for V3 endpoints,
* should be avoided in V4).
* <p>
* Alternatively, a handler method may create a new output schema object from scratch, or from an existing
* implementation object.
*
* <h1>Internal details</h1>
* <p>
* Most Java developers need not be concerned with the details that follow, because the
* framework will make these calls as necessary.
* <p>
* To create a schema object and fill it from an existing impl object:
* <pre>{@code S schema = new SomeSchemaClass().fillFromImpl(impl);}</pre>
* <p>
* To create an impl object and fill it from an existing schema object:
* <pre>{@code I impl = schema.createAndFillImpl();}</pre>
* <p>
* Schemas that are used for HTTP requests are filled with the default values of their impl
* class, and then any present HTTP parameters override those default values.
* To create a schema object filled from the default values of its impl class and then
* overridden by HTTP request params:
* <pre>{@code S schema = new SomeSchemaClass().fillFromImpl().fillFromParms(parms);}</pre>
*
* @param <I> "implementation" (Iced) class for this schema
* @param <S> reference to self: this should always be the same class as being declared. For example:
* <pre>public class TimelineV3 extends Schema<Timeline, TimelineV3></pre>
*/
public abstract class Schema<I extends Iced, S extends Schema<I,S>> extends Iced {
// These fields are declared transient so that they do not get included when a schema is serialized into JSON.
private transient Class<I> _impl_class;
private transient int _schema_version;
private transient String _schema_name;
private transient String _schema_type;
private transient static final Gson gson = H2oRestGsonHelper.createH2oCompatibleGson(); // stateless and thread safe
/** Default constructor; triggers lazy schema registration.
* @throws water.exceptions.H2OFailException if there is a name collision or
* there is more than one schema which maps to the same Iced class */
public Schema() {
init_meta();
SchemaServer.checkIfRegistered(this);
}
/**
* Create a new Schema instance from an existing impl object.
*/
public Schema(I impl) {
this();
this.fillFromImpl(impl);
}
protected void init_meta() {
if (_schema_name != null) return;
_schema_name = this.getClass().getSimpleName();
_schema_version = extractVersionFromSchemaName(_schema_name);
_schema_type = getImplClass().getSimpleName();
}
/** Extract the version number from the schema class name. Returns -1 if
* there's no version number at the end of the classname. */
public static int extractVersionFromSchemaName(String clz_name) {
int idx = clz_name.lastIndexOf('V');
if (idx == -1) return -1;
try { return Integer.valueOf(clz_name.substring(idx+1)); }
catch( NumberFormatException ex) { return -1; }
}
/** Get the version number of this schema, for example 3 or 99. Note that 99
* is the "experimental" version, meaning that there are no stability
* guarantees between H2O versions. */
public int getSchemaVersion() { return _schema_version; }
public String getSchemaName() { return _schema_name; }
public String getSchemaType() { return _schema_type; }
/* Temporary hack to allow reassignment of schema_type by KeyV3 class */
public void setSchemaType_doNotCall(String s) { _schema_type = s; }
/**
* Create an appropriate implementation object and any child objects but does not fill them.
* The standard purpose of a createImpl without a fillImpl is to be able to get the default
* values for all the impl's fields.
* <p>
* For objects without children this method does all the required work. For objects
* with children the subclass will need to override, e.g. by calling super.createImpl()
* and then calling createImpl() on its children.
* <p>
* Note that impl objects for schemas which override this method don't need to have
* a default constructor (e.g., a Keyed object constructor can still create and set
* the Key), but they must not fill any fields which can be filled later from the schema.
* <p>
* TODO: We could handle the common case of children with the same field names here
* by finding all of our fields that are themselves Schemas.
*/
public I createImpl() {
try { return getImplClass().newInstance(); }
catch (Exception e) { throw H2O.fail("Exception making a newInstance",e); }
}
protected I fillImpl(I impl, String[] fieldsToSkip) {
PojoUtils.copyProperties(impl, this, PojoUtils.FieldNaming.CONSISTENT, fieldsToSkip); // TODO: make field names in the impl classes consistent and remove
PojoUtils.copyProperties(impl, this, PojoUtils.FieldNaming.DEST_HAS_UNDERSCORES, fieldsToSkip);
return impl;
}
/** Fill an impl object and any children from this schema and its children.
* If a schema doesn't need to adapt any fields if does not need to override
* this method. */
public I fillImpl(I impl) {
return fillImpl(impl, null);
}
/** Convenience helper which creates and fills an impl object from this schema. */
public final I createAndFillImpl() {
return this.fillImpl(this.createImpl());
}
/**
* Fill this schema from the default impl, and then return self.
*/
public final S fillFromImpl() {
return fillFromImpl(createImpl(), null);
}
/**
* Fill this Schema from the given implementation object. If a schema doesn't need to adapt any fields if does not
* need to override this method.
*/
public S fillFromImpl(I impl) {
return fillFromImpl(impl, null);
}
protected S fillFromImpl(I impl, String[] fieldsToSkip) {
PojoUtils.copyProperties(this, impl, PojoUtils.FieldNaming.ORIGIN_HAS_UNDERSCORES, fieldsToSkip);
PojoUtils.copyProperties(this, impl, PojoUtils.FieldNaming.CONSISTENT, fieldsToSkip); // TODO: make field names in the impl classes consistent and remove
//noinspection unchecked (parameter <S> should be the derived class itself)
return (S) this;
}
/** Return the class of the implementation type parameter I for the
* given Schema class. Used by the metadata facilities and the
* reflection-base field-copying magic in PojoUtils. */
public static Class<? extends Iced> getImplClass(Class<? extends Schema> clz) {
Class<? extends Iced> impl_class = ReflectionUtils.findActualClassParameter(clz, 0);
if (null == impl_class)
Log.warn("Failed to find an impl class for Schema: " + clz);
return impl_class;
}
/** Return the class of the implementation type parameter I for this Schema.
* Used by generic code which deals with arbitrary schemas and their backing
* impl classes. Never returns null. */
public Class<I> getImplClass() {
return _impl_class != null ? _impl_class : (_impl_class = ReflectionUtils.findActualClassParameter(this.getClass(), 0));
}
/**
* Fill this Schema object from a set of parameters.
*
* @param parms parameters - set of tuples (parameter name, parameter value)
* @return this schema
*
* @see #fillFromParms(Properties, boolean)
*/
public S fillFromParms(Properties parms) {
return fillFromParms(parms, true);
}
/**
* Fill this Schema from a set of (generally HTTP) parameters.
* <p>
* Using reflection this process determines the type of the target field and
* conforms the types if possible. For example, if the field is a Keyed type
* the name (ID) will be looked up in the DKV and mapped appropriately.
* <p>
* The process ignores parameters which are not fields in the schema, and it
* verifies that all fields marked as required are present in the parameters
* list.
* <p>
* It also does various sanity checks for broken Schemas, for example fields must
* not be private, and since input fields get filled here they must not be final.
* @param parms Properties map of parameter values
* @param checkRequiredFields perform check for missing required fields
* @return this schema
* @throws H2OIllegalArgumentException for bad/missing parameters
*/
public S fillFromParms(Properties parms, boolean checkRequiredFields) {
// Get passed-in fields, assign into Schema
Class thisSchemaClass = this.getClass();
Map<String, Field> fields = new HashMap<>();
Field current = null; // declare here so we can print in catch{}
try {
Class clz = thisSchemaClass;
do {
Field[] some_fields = clz.getDeclaredFields();
for (Field f : some_fields) {
current = f;
if (null == fields.get(f.getName()))
fields.put(f.getName(), f);
}
clz = clz.getSuperclass();
} while (Iced.class.isAssignableFrom(clz.getSuperclass()));
}
catch (SecurityException e) {
throw H2O.fail("Exception accessing field: " + current + " in class: " + this.getClass() + ": " + e);
}
for( String key : parms.stringPropertyNames() ) {
try {
Field f = fields.get(key); // No such field error, if parm is junk
if (null == f) {
throw new H2OIllegalArgumentException("Unknown parameter: " + key, "Unknown parameter in fillFromParms: " + key + " for class: " + this.getClass().toString());
}
int mods = f.getModifiers();
if( Modifier.isTransient(mods) || Modifier.isStatic(mods) ) {
// Attempting to set a transient or static; treat same as junk fieldname
throw new H2OIllegalArgumentException(
"Bad parameter for field: " + key + " for class: " + this.getClass().toString(),
"Bad parameter definition for field: " + key + " in fillFromParms for class: " + this.getClass().toString() + " (field was declared static or transient)");
}
// Only support a single annotation which is an API, and is required
Annotation[] apis = f.getAnnotations();
if( apis.length == 0 ) throw H2O.fail("Broken internal schema; missing API annotation for field: " + key);
API api = (API)apis[0];
// Must have one of these set to be an input field
if( api.direction() == API.Direction.OUTPUT ) {
throw new H2OIllegalArgumentException(
"Attempting to set output field: " + key + " for class: " + this.getClass().toString(),
"Attempting to set output field: " + key + " in fillFromParms for class: " + this.getClass().toString() + " (field was annotated as API.Direction.OUTPUT)");
}
// Parse value and set the field
setField(this, f, key, parms.getProperty(key), api.required(), thisSchemaClass);
} catch( IllegalAccessException iae ) {
// Come here if field is final or private
throw H2O.fail("Broken internal schema; field cannot be private nor final: " + key);
}
}
// Here every thing in 'parms' was set into some field - so we have already
// checked for unknown or extra parms.
// Confirm required fields are set
if (checkRequiredFields) {
for (Field f : fields.values()) {
int mods = f.getModifiers();
if (Modifier.isTransient(mods) || Modifier.isStatic(mods))
continue; // Ignore transient & static
try {
API api = (API) f.getAnnotations()[0]; // TODO: is there a more specific way we can do this?
if (api.required()) {
if (parms.getProperty(f.getName()) == null) {
IcedHashMapGeneric.IcedHashMapStringObject values = new IcedHashMapGeneric.IcedHashMapStringObject();
values.put("schema", this.getClass().getSimpleName());
values.put("argument", f.getName());
throw new H2OIllegalArgumentException(
"Required field " + f.getName() + " not specified",
"Required field " + f.getName() + " not specified for schema class: " + this.getClass(),
values);
}
}
} catch (ArrayIndexOutOfBoundsException e) {
throw H2O.fail("Missing annotation for API field: " + f.getName());
}
}
}
//noinspection unchecked (parameter <S> should be the derived class itself)
return (S) this;
}
/**
* Safe method to set the field on given schema object
* @param o schema object to modify
* @param f field to modify
* @param key name of field to modify
* @param value string-based representation of value to set
* @param required is field required by API
* @param thisSchemaClass class of schema handling this (can be null)
* @throws IllegalAccessException
*/
public static <T extends Schema> void setField(T o, Field f, String key, String value, boolean required, Class thisSchemaClass) throws IllegalAccessException {
// Primitive parse by field type
Object parse_result = parse(key, value, f.getType(), required, thisSchemaClass);
if (parse_result != null && f.getType().isArray() && parse_result.getClass().isArray() && (f.getType().getComponentType() != parse_result.getClass().getComponentType())) {
// We have to conform an array of primitives. There's got to be a better way. . .
if (parse_result.getClass().getComponentType() == int.class && f.getType().getComponentType() == Integer.class) {
int[] from = (int[])parse_result;
Integer[] copy = new Integer[from.length];
for (int i = 0; i < from.length; i++)
copy[i] = from[i];
f.set(o, copy);
} else if (parse_result.getClass().getComponentType() == Integer.class && f.getType().getComponentType() == int.class) {
Integer[] from = (Integer[])parse_result;
int[] copy = new int[from.length];
for (int i = 0; i < from.length; i++)
copy[i] = from[i];
f.set(o, copy);
} else if (parse_result.getClass().getComponentType() == Double.class && f.getType().getComponentType() == double.class) {
Double[] from = (Double[])parse_result;
double[] copy = new double[from.length];
for (int i = 0; i < from.length; i++)
copy[i] = from[i];
f.set(o, copy);
} else if (parse_result.getClass().getComponentType() == Float.class && f.getType().getComponentType() == float.class) {
Float[] from = (Float[])parse_result;
float[] copy = new float[from.length];
for (int i = 0; i < from.length; i++)
copy[i] = from[i];
f.set(o, copy);
} else {
throw H2O.fail("Don't know how to cast an array of: " + parse_result.getClass().getComponentType() + " to an array of: " + f.getType().getComponentType());
}
} else {
f.set(o, parse_result);
}
}
static <E> Object parsePrimitve(String s, Class fclz) {
if (fclz.equals(String.class)) return s; // Strings already the right primitive type
if (fclz.equals(int.class)) return parseInteger(s, int.class);
if (fclz.equals(long.class)) return parseInteger(s, long.class);
if (fclz.equals(short.class)) return parseInteger(s, short.class);
if (fclz.equals(boolean.class)) {
if (s.equals("0")) return Boolean.FALSE;
if (s.equals("1")) return Boolean.TRUE;
return Boolean.valueOf(s);
}
if (fclz.equals(byte.class)) return parseInteger(s, byte.class);
if (fclz.equals(double.class)) return Double.valueOf(s);
if (fclz.equals(float.class)) return Float.valueOf(s);
//FIXME: if (fclz.equals(char.class)) return Character.valueOf(s);
throw H2O.fail("Unknown primitive type to parse: " + fclz.getSimpleName());
}
// URL parameter parse
static <E> Object parse(String field_name, String s, Class fclz, boolean required, Class schemaClass) {
if (fclz.isPrimitive() || String.class.equals(fclz)) {
try {
return parsePrimitve(s, fclz);
} catch (NumberFormatException ne) {
String msg = "Illegal argument for field: " + field_name + " of schema: " + schemaClass.getSimpleName() + ": cannot convert \"" + s + "\" to type " + fclz.getSimpleName();
throw new H2OIllegalArgumentException(msg);
}
}
// An array?
if (fclz.isArray()) {
// Get component type
Class<E> afclz = (Class<E>) fclz.getComponentType();
// Result
E[] a = null;
// Handle simple case with null-array
if (s.equals("null") || s.length() == 0) return null;
// Splitted values
String[] splits; // "".split(",") => {""} so handle the empty case explicitly
if (s.startsWith("[") && s.endsWith("]") ) { // It looks like an array
read(s, 0, '[', fclz);
read(s, s.length() - 1, ']', fclz);
String inside = s.substring(1, s.length() - 1).trim();
if (inside.length() == 0)
splits = new String[]{};
else
splits = splitArgs(inside);
} else { // Lets try to parse single value as an array!
// See PUBDEV-1955
splits = new String[] { s.trim() };
}
// Can't cast an int[] to an Object[]. Sigh.
if (afclz == int.class) { // TODO: other primitive types. . .
a = (E[]) Array.newInstance(Integer.class, splits.length);
} else if (afclz == double.class) {
a = (E[]) Array.newInstance(Double.class, splits.length);
} else if (afclz == float.class) {
a = (E[]) Array.newInstance(Float.class, splits.length);
} else {
// Fails with primitive classes; need the wrapper class. Thanks, Java.
a = (E[]) Array.newInstance(afclz, splits.length);
}
for (int i = 0; i < splits.length; i++) {
if (String.class == afclz || KeyV3.class.isAssignableFrom(afclz)) {
// strip quotes off string values inside array
String stripped = splits[i].trim();
if ("null".equals(stripped.toLowerCase()) || "na".equals(stripped.toLowerCase())) {
a[i] = null;
continue;
}
// Quotes are now optional because standard clients will send arrays of length one as just strings.
if (stripped.startsWith("\"") && stripped.endsWith("\"")) {
stripped = stripped.substring(1, stripped.length() - 1);
}
a[i] = (E) parse(field_name, stripped, afclz, required, schemaClass);
} else {
a[i] = (E) parse(field_name, splits[i].trim(), afclz, required, schemaClass);
}
}
return a;
}
// Are we parsing an object from a string? NOTE: we might want to make this check more restrictive.
if (! fclz.isAssignableFrom(Schema.class) && s != null && s.startsWith("{") && s.endsWith("}")) {
return gson.fromJson(s, fclz);
}
if (fclz.equals(Key.class))
if ((s == null || s.length() == 0) && required) throw new H2OKeyNotFoundArgumentException(field_name, s);
else if (!required && (s == null || s.length() == 0)) return null;
else
return Key.make(s.startsWith("\"") ? s.substring(1, s.length() - 1) : s); // If the key name is in an array we need to trim surrounding quotes.
if (KeyV3.class.isAssignableFrom(fclz)) {
if ((s == null || s.length() == 0) && required) throw new H2OKeyNotFoundArgumentException(field_name, s);
if (!required && (s == null || s.length() == 0)) return null;
return KeyV3.make(fclz, Key.make(s.startsWith("\"") ? s.substring(1, s.length() - 1) : s)); // If the key name is in an array we need to trim surrounding quotes.
}
if (Enum.class.isAssignableFrom(fclz)) {
return EnumUtils.valueOf(fclz, s);
}
// TODO: these can be refactored into a single case using the facilities in Schema:
if (FrameV3.class.isAssignableFrom(fclz)) {
if ((s == null || s.length() == 0) && required) throw new H2OKeyNotFoundArgumentException(field_name, s);
else if (!required && (s == null || s.length() == 0)) return null;
else {
Value v = DKV.get(s);
if (null == v) return null; // not required
if (!v.isFrame()) throw H2OIllegalArgumentException.wrongKeyType(field_name, s, "Frame", v.get().getClass());
return new FrameV3((Frame) v.get()); // TODO: version!
}
}
if (JobV3.class.isAssignableFrom(fclz)) {
if ((s == null || s.length() == 0) && required) throw new H2OKeyNotFoundArgumentException(s);
else if (!required && (s == null || s.length() == 0)) return null;
else {
Value v = DKV.get(s);
if (null == v) return null; // not required
if (!v.isJob()) throw H2OIllegalArgumentException.wrongKeyType(field_name, s, "Job", v.get().getClass());
return new JobV3().fillFromImpl((Job) v.get()); // TODO: version!
}
}
// TODO: for now handle the case where we're only passing the name through; later we need to handle the case
// where the frame name is also specified.
if (FrameV3.ColSpecifierV3.class.isAssignableFrom(fclz)) {
return new FrameV3.ColSpecifierV3(s);
}
if (ModelSchemaV3.class.isAssignableFrom(fclz))
throw H2O.fail("Can't yet take ModelSchemaV3 as input.");
/*
if( (s==null || s.length()==0) && required ) throw new IllegalArgumentException("Missing key");
else if (!required && (s == null || s.length() == 0)) return null;
else {
Value v = DKV.get(s);
if (null == v) return null; // not required
if (! v.isModel()) throw new IllegalArgumentException("Model argument points to a non-model object.");
return v.get();
}
*/
throw H2O.fail("Unimplemented schema fill from " + fclz.getSimpleName());
} // parse()
/**
* Helper functions for parse()
**/
/**
* Parses a string into an integer data type specified by parameter return_type. Accepts any format that
* is accepted by java's BigDecimal class.
* - Throws a NumberFormatException if the evaluated string is not an integer or if the value is too large to
* be stored into return_type without overflow.
* - Throws an IllegalAgumentException if return_type is not an integer data type.
**/
static private <T> T parseInteger(String s, Class<T> return_type) {
try {
java.math.BigDecimal num = new java.math.BigDecimal(s);
T result = (T) num.getClass().getDeclaredMethod(return_type.getSimpleName() + "ValueExact", new Class[0]).invoke(num);
return result;
} catch (InvocationTargetException ite) {
throw new NumberFormatException("The expression's numeric value is out of the range of type " + return_type.getSimpleName());
} catch (NoSuchMethodException nsme) {
throw new IllegalArgumentException(return_type.getSimpleName() + " is not an integer data type");
} catch (IllegalAccessException iae) {
throw H2O.fail("Cannot parse expression as " + return_type.getSimpleName() + " (Illegal Access)");
}
}
static private int read( String s, int x, char c, Class fclz ) {
if( peek(s,x,c) ) return x+1;
throw new IllegalArgumentException("Expected '"+c+"' while reading a "+fclz.getSimpleName()+", but found "+s);
}
static private boolean peek( String s, int x, char c ) { return x < s.length() && s.charAt(x) == c; }
// Splits on commas, but ignores commas in double quotes. Required
// since using a regex blow the stack on long column counts
// TODO: detect and complain about malformed JSON
private static String[] splitArgs(String argStr) {
StringBuilder sb = new StringBuilder(argStr);
StringBuilder arg = new StringBuilder();
List<String> splitArgList = new ArrayList<String> ();
boolean inDoubleQuotes = false;
boolean inSquareBrackets = false; // for arrays of arrays
for (int i=0; i < sb.length(); i++) {
if (sb.charAt(i) == '"' && !inDoubleQuotes && !inSquareBrackets) {
inDoubleQuotes = true;
arg.append(sb.charAt(i));
} else if (sb.charAt(i) == '"' && inDoubleQuotes && !inSquareBrackets) {
inDoubleQuotes = false;
arg.append(sb.charAt(i));
} else if (sb.charAt(i) == ',' && !inDoubleQuotes && !inSquareBrackets) {
splitArgList.add(arg.toString());
// clear the field for next word
arg.setLength(0);
} else if (sb.charAt(i) == '[') {
inSquareBrackets = true;
arg.append(sb.charAt(i));
} else if (sb.charAt(i) == ']') {
inSquareBrackets = false;
arg.append(sb.charAt(i));
} else {
arg.append(sb.charAt(i));
}
}
if (arg.length() > 0)
splitArgList.add(arg.toString());
return splitArgList.toArray(new String[splitArgList.size()]);
}
/**
* Returns a new Schema instance. Does not throw, nor returns null.
* @return New instance of Schema Class 'clz'.
*/
public static <T extends Schema> T newInstance(Class<T> clz) {
try { return clz.newInstance(); }
catch (Exception e) { throw H2O.fail("Failed to instantiate schema of class: " + clz.getCanonicalName(),e); }
}
/**
* For a given schema_name (e.g., "FrameV2") return an appropriate new schema object (e.g., a water.api.Framev2).
*/
protected static Schema newInstance(String schema_name) {
return Schema.newInstance(SchemaServer.getSchema(schema_name));
}
/**
* Generate Markdown documentation for this Schema possibly including only the input or output fields.
* @throws H2ONotFoundArgumentException if reflection on a field fails
*/
public StringBuffer markdown(boolean include_input_fields, boolean include_output_fields) {
return markdown(new SchemaMetadata(this), include_input_fields, include_output_fields);
}
/**
* Generate Markdown documentation for this Schema, given we already have the metadata constructed.
* @throws H2ONotFoundArgumentException if reflection on a field fails
*/
public StringBuffer markdown(SchemaMetadata meta, boolean include_input_fields, boolean include_output_fields) {
MarkdownBuilder builder = new MarkdownBuilder();
builder.comment("Preview with http://jbt.github.io/markdown-editor");
builder.heading1("schema ", this.getClass().getSimpleName());
builder.hline();
// builder.paragraph(metadata.summary);
// TODO: refactor with Route.markdown():
// fields
boolean first; // don't print the table at all if there are no rows
try {
if (include_input_fields) {
first = true;
builder.heading2("input fields");
for (SchemaMetadata.FieldMetadata field_meta : meta.fields) {
if (field_meta.direction == API.Direction.INPUT || field_meta.direction == API.Direction.INOUT) {
if (first) {
builder.tableHeader("name", "required?", "level", "type", "schema?", "schema", "default", "description", "values", "is member of frames", "is mutually exclusive with");
first = false;
}
builder.tableRow(
field_meta.name,
String.valueOf(field_meta.required),
field_meta.level.name(),
field_meta.type,
String.valueOf(field_meta.is_schema),
field_meta.is_schema ? field_meta.schema_name : "", (null == field_meta.value ? "(null)" : field_meta.value.toString()), // Something better for toString()?
field_meta.help,
(field_meta.values == null || field_meta.values.length == 0 ? "" : Arrays.toString(field_meta.values)),
(field_meta.is_member_of_frames == null ? "[]" : Arrays.toString(field_meta.is_member_of_frames)),
(field_meta.is_mutually_exclusive_with == null ? "[]" : Arrays.toString(field_meta.is_mutually_exclusive_with))
);
}
}
if (first)
builder.paragraph("(none)");
}
if (include_output_fields) {
first = true;
builder.heading2("output fields");
for (SchemaMetadata.FieldMetadata field_meta : meta.fields) {
if (field_meta.direction == API.Direction.OUTPUT || field_meta.direction == API.Direction.INOUT) {
if (first) {
builder.tableHeader("name", "type", "schema?", "schema", "default", "description", "values", "is member of frames", "is mutually exclusive with");
first = false;
}
builder.tableRow(
field_meta.name,
field_meta.type,
String.valueOf(field_meta.is_schema),
field_meta.is_schema ? field_meta.schema_name : "",
(null == field_meta.value ? "(null)" : field_meta.value.toString()), // something better than toString()?
field_meta.help,
(field_meta.values == null || field_meta.values.length == 0 ? "" : Arrays.toString(field_meta.values)),
(field_meta.is_member_of_frames == null ? "[]" : Arrays.toString(field_meta.is_member_of_frames)),
(field_meta.is_mutually_exclusive_with == null ? "[]" : Arrays.toString(field_meta.is_mutually_exclusive_with)));
}
}
if (first)
builder.paragraph("(none)");
}
// TODO: render examples and other stuff, if it's passed in
}
catch (Exception e) {
IcedHashMapGeneric.IcedHashMapStringObject values = new IcedHashMapGeneric.IcedHashMapStringObject();
values.put("schema", this);
// TODO: This isn't quite the right exception type:
throw new H2OIllegalArgumentException("Caught exception using reflection on schema: " + this,
"Caught exception using reflection on schema: " + this + ": " + e,
values);
}
return builder.stringBuffer();
}
}