package water.schemas;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Properties;
import water.*;
import water.api.Handler;
import water.fvec.Frame;
/** Base Schema Class
*
* All Schemas inherit from here. Schemas have a State section (broken into
* Input fields and Output fields) and an Adapter section to fill the State to
* and from URLs and JSON. The base Adapter logic is here, and will by
* default copy same-named fields to and from Schemas to concrete Iced objects.
*
* Schema Fields must have a single API annotation describing in they are an
* input field or not (all fields will be output by default), and any extra
* requirements on the input (prior field dependencies and other validation
* checks). Transient & Static fields are ignored.
*/
public abstract class Schema<H extends Handler<H,S>,S extends Schema<H,S>> extends Iced {
private final transient int _version;
protected final int getVersion() { return _version; }
protected Schema() {
// Check version number
String n = this.getClass().getSimpleName();
assert n.charAt(n.length()-2)=='V' : "Schema classname does not end in a 'V' and a version #";
_version = n.charAt(n.length()-1)-'0';
assert 0 <= _version && _version <= 9 : "Schema classname does not contain version";
}
// Version&Schema-specific filling into the handler
abstract public S fillInto( H h );
// Version&Schema-specific filling from the handler
abstract public S fillFrom( H h );
// This Schema accepts a Frame as it's first & main argument, used by the
// Frame Inspect & Parse pages to give obvious options for Modeling, Summary,
// export-to-CSV etc options. Return a URL or null if not appropriate.
public String acceptsFrame( Frame fr ) { return null; }
// Fill self from parms. Limited to dumb primitive parsing and simple
// reflective field filling. Ignores fields not in the Schema. Throws IAE
// if the primitive parameter cannot be parsed as the primitive field type.
// Dupped args are handled by Nano, as 'parms' can only have a single arg
// mapping for a given name.
// Also does various sanity checks for broken Schemas. Fields must not be
// private. Input fields get filled here, so must not be final.
public S fillFrom( Properties parms ) {
// Get passed-in fields, assign into Schema
Class clz = getClass();
for( String key : parms.stringPropertyNames() ) {
try {
Field f = clz.getDeclaredField(key); // No such field error, if parm is junk
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 IllegalArgumentException("Unknown argument "+key);
// Only support a single annotation which is an API, and is required
API api = (API)f.getAnnotations()[0];
// Must have one of these set to be an input field
if( api.validation().length()==0 &&
api.values ().length()==0 &&
api.dependsOn ().length ==0 )
throw new IllegalArgumentException("Attempting to set output field "+key);
// Primitive parse by field type
f.set(this,parse(parms.getProperty(key),f.getType()));
} catch( NoSuchFieldException nsfe ) { // Convert missing-field to IAE
throw new IllegalArgumentException("Unknown argument "+key);
} catch( ArrayIndexOutOfBoundsException aioobe ) {
// Come here if missing annotation
throw new RuntimeException("Broken internal schema; missing API annotation: "+key);
} catch( IllegalAccessException iae ) {
// Come here if field is final or private
throw new RuntimeException("Broken internal schema; 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
do {
for( Field f : clz.getDeclaredFields() ) {
int mods = f.getModifiers();
if( Modifier.isTransient(mods) || Modifier.isStatic(mods) )
continue; // Ignore transient & static
API api = (API)f.getAnnotations()[0];
if( api.validation().length() > 0 ) {
// TODO: execute "validation language" in the BackEnd, which includes a "required check", if any
if( parms.getProperty(f.getName()) == null )
throw new IllegalArgumentException("Required field "+f.getName()+" not specified");
}
}
clz = clz.getSuperclass();
} while( Iced.class.isAssignableFrom(clz.getSuperclass()) );
return (S)this;
}
// URL parameter parse
private <E> Object parse( String s, Class fclz ) {
if( fclz.equals(String.class) ) return s; // Strings already the right primitive type
if( fclz.isArray() ) { // An array?
read(s, 0 ,'[',fclz);
read(s,s.length()-1,']',fclz);
String[] splits = s.substring(1,s.length()-1).split(",");
Class<E> afclz = (Class<E>)fclz.getComponentType();
E[] a= (E[])Array.newInstance(afclz,splits.length);
for( int i=0; i<splits.length; i++ )
a[i] = (E)parse(splits[i],afclz);
return a;
}
if( fclz.equals(Key.class) ) return Key.make(s);
throw new RuntimeException("Unimplemented schema fill from "+fclz.getSimpleName());
}
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);
}
private boolean peek( String s, int x, char c ) { return x < s.length() && s.charAt(x) == c; }
}