package water.api;
import hex.nb.NBModel;
import hex.pca.PCAModel;
import java.io.InputStream;
import java.lang.annotation.*;
import java.util.*;
import water.*;
import water.api.Request.Validator.NOPValidator;
import water.api.RequestServer.API_VERSION;
import water.fvec.Frame;
import water.util.*;
import com.google.common.io.ByteStreams;
import dontweave.gson.JsonObject;
public abstract class Request extends RequestBuilders {
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface API {
String help();
/** Must be specified. */
boolean required() default false;
/** For keys. If specified, the key must exist. */
boolean mustExist() default false;
int since() default 1;
int until() default Integer.MAX_VALUE;
Class<? extends Filter> filter() default Filter.class;
Class<? extends Filter>[] filters() default {};
/** Forces an input field to also appear in JSON. */
boolean json() default false;
long lmin() default Long .MIN_VALUE;
long lmax() default Long .MAX_VALUE;
double dmin() default Double.NEGATIVE_INFINITY;
double dmax() default Double.POSITIVE_INFINITY;
boolean hide() default false;
String displayName() default "";
boolean gridable() default true;
Class<? extends Validator> validator() default NOPValidator.class;
ParamImportance importance() default ParamImportance.EXPERT; // Show show up in UI by default.
// ============
// NEW API
String[] dependsOn() default {}; // Should be field automatically depending on values, valid fields
String[] helpFiles() default {};
Direction direction() default Direction.OUT;
/** REST path to reference of this field */
String path() default "";
/** Validation String for annotated field - make sense only for input annotation!
* It should express a predicate, e.g.:
* <code>"/frames/${/parameters/source}/cols/${/parameters/response}/type != 'Float' && ${/parameters/learn_rate} > 1000</code> */
String valid() default "";
/** Is the field is enabled. */
String enabled() default "";
/** Is the field visibled. */
String visible() default "";
/** Predefined values for the field - can be a list of values, or query to
* obtain values.
*
* <code>/frames/${source}/cols?names</code>, <code>1,2,10,15</code>
*/
String values() default "";
/**
* Type of parameter
*/
Class type() default Void.class;
// =========
}
public interface Validator<V> extends Freezable {
void validateRaw(String value) throws IllegalArgumentException;
void validateValue(V value) throws IllegalArgumentException;
/** Dummy helper class for NOP validator. */
public static class NOPValidator<V> extends Iced implements Validator<V> {
@Override public void validateRaw(String value) { }
@Override public void validateValue(V value) { }
}
}
public static interface Filter {
boolean run(Object value);
}
/** NOP filter, use to define a field as input. */
public class Default implements Filter {
@Override public boolean run(Object value) { return true; }
}
//
public String _requestHelp;
protected Request(String help) {
_requestHelp = help;
}
protected Request() {
}
public String href() { return href(supportedVersions()[0]); }
protected String href(API_VERSION v) {
return v.prefix() + getClass().getSimpleName();
}
protected RequestType hrefType() {
return RequestType.www;
}
protected boolean log() {
return true;
}
protected void registered(API_VERSION version) {
}
protected Request create(Properties parms) {
return this;
}
/** Implements UI call.
*
* <p>This should be call only from
* UI layer - i.e., RequestServer.</p>
*
* @see RequestServer
*/
protected abstract Response serve();
protected String serveJava() { throw new UnsupportedOperationException("This request does not provide Java code!"); }
public NanoHTTPD.Response serve(NanoHTTPD server, Properties parms, RequestType type) {
switch( type ) {
case help:
return wrap(server, HTMLHelp());
case xml:
case json:
case www:
return serveGrid(server, parms, type);
case query: {
for (Argument arg: _arguments)
arg.reset();
String query = buildQuery(parms,type);
return wrap(server, query);
}
case java:
checkArguments(parms, type); // Do not check returned query but let it fail in serveJava
String javacode = serveJava();
return wrap(server, javacode, RequestType.java);
default:
throw new RuntimeException("Invalid request type " + type.toString());
}
}
protected NanoHTTPD.Response serveGrid(NanoHTTPD server, Properties parms, RequestType type) {
String query = checkArguments(parms, type);
if( query != null )
return wrap(server, query, type);
long time = System.currentTimeMillis();
Response response = null;
try {
response = serve();
} catch (IllegalArgumentException iae) { // handle illegal arguments
response = Response.error(iae);
}
response.setTimeStart(time);
return serveResponse(server, parms, type, response);
}
public NanoHTTPD.Response serveResponse(NanoHTTPD server, Properties parms, RequestType type, Response response) {
// Argh - referencing subclass, sorry for that, but it is temporary hack
// for transition between v1 and v2 API
if (this instanceof Request2) ((Request2) this).fillResponseInfo(response);
if (this instanceof Parse2) ((Parse2) this).fillResponseInfo(response); // FIXME: Parser2 should inherit from Request2
if( type == RequestType.json ) {
return response._req == null ? //
wrap(server, response.toJson()) : //
wrap(server, new String(response._req.writeJSON(new AutoBuffer()).buf()), RequestType.json);
}
else if (type == RequestType.xml) {
if (response._req == null) {
String xmlString = response.toXml();
NanoHTTPD.Response r = wrap(server, xmlString, RequestType.xml);
return r;
}
else {
String jsonString = new String(response._req.writeJSON(new AutoBuffer()).buf());
org.json.JSONObject jo2 = new org.json.JSONObject(jsonString);
String xmlString = org.json.XML.toString(jo2);
NanoHTTPD.Response r = wrap(server, xmlString, RequestType.xml);
return r;
}
}
return wrap(server, build(response));
}
protected NanoHTTPD.Response wrap(NanoHTTPD server, String response) {
RString html = new RString(htmlTemplate());
html.replace("CONTENTS", response);
return server.new Response(NanoHTTPD.HTTP_OK, NanoHTTPD.MIME_HTML, html.toString());
}
protected NanoHTTPD.Response wrap(NanoHTTPD server, JsonObject response) {
return server.new Response(NanoHTTPD.HTTP_OK, NanoHTTPD.MIME_JSON, response.toString());
}
public NanoHTTPD.Response wrap(NanoHTTPD server, String value, RequestType type) {
if( type == RequestType.xml )
return server.new Response(NanoHTTPD.HTTP_OK, NanoHTTPD.MIME_XML, value);
if( type == RequestType.json )
return server.new Response(NanoHTTPD.HTTP_OK, NanoHTTPD.MIME_JSON, value);
if (type == RequestType.java)
return server.new Response(NanoHTTPD.HTTP_OK, NanoHTTPD.MIME_PLAINTEXT, value);
return wrap(server, value);
}
// html template and navbar handling -----------------------------------------
/**
* Read from file once.
*/
private static final String _htmlTemplateFromFile;
/**
* Written by initializeNavBar().
*/
private static volatile String _htmlTemplate;
protected String htmlTemplate() { return _htmlTemplate; }
static {
_htmlTemplateFromFile = loadTemplate("/page.html");
_htmlTemplate = "";
}
static final String loadTemplate(String name) {
InputStream resource = Boot._init.getResource2(name);
try {
if( H2O.NAME != null )
return new String(ByteStreams.toByteArray(resource)).replace("%cloud_name", H2O.NAME);
} catch( NullPointerException e ) {
if( !Log._dontDie ) {
Log.err(e);
Log.die(name+" not found in resources.");
}
} catch( Exception e ) {
Log.err(e);
Log.die(e.getMessage());
} finally {
Utils.close(resource);
}
return null;
}
private static class MenuItem {
public final Request _request;
public final String _name;
public final boolean _useNewTab;
public MenuItem(Request request, String name, boolean useNewTab) {
_request = request;
_name = name;
_useNewTab = useNewTab;
}
public void toHTML(StringBuilder sb) {
sb.append("<li><a href='");
sb.append(_request.href() + _request.hrefType()._suffix);
sb.append("'");
if (_useNewTab) {
sb.append(" target='_blank'");
}
sb.append(">");
sb.append(_name);
sb.append("</a></li>");
}
}
private static HashMap<String, ArrayList<MenuItem>> _navbar = new HashMap();
private static ArrayList<String> _navbarOrdering = new ArrayList();
/**
* Call this after the last call addToNavbar().
* This is called automatically for navbar entries from inside H2O.
* If user app level code calls addToNavbar, then call this again to make those changes visible.
*/
public static void initializeNavBar() { _htmlTemplate = initializeNavBar(_htmlTemplateFromFile); }
private static String initializeNavBar(String template) {
StringBuilder sb = new StringBuilder();
for( String s : _navbarOrdering ) {
ArrayList<MenuItem> arl = _navbar.get(s);
if( (arl.size() == 1) && arl.get(0)._name.equals(s) ) {
arl.get(0).toHTML(sb);
} else {
sb.append("<li class='dropdown'>");
sb.append("<a href='#' class='dropdown-toggle' data-toggle='dropdown'>");
sb.append(s);
sb.append("<b class='caret'></b>");
sb.append("</a>");
sb.append("<ul class='dropdown-menu'>");
for( MenuItem i : arl )
i.toHTML(sb);
sb.append("</ul></li>");
}
}
RString str = new RString(template);
str.replace("NAVBAR", sb.toString());
str.replace("CONTENTS", "%CONTENTS");
return str.toString();
}
public static Request addToNavbar(Request r, String name) {
assert (!_navbar.containsKey(name));
ArrayList<MenuItem> arl = new ArrayList();
boolean useNewTab = false;
arl.add(new MenuItem(r, name, useNewTab));
_navbar.put(name, arl);
_navbarOrdering.add(name);
return r;
}
public static Request addToNavbar(Request r, String name, String category) {
boolean useNewTab = false;
return addToNavbar(r, name, category, useNewTab);
}
public static Request addToNavbar(Request r, String name, String category, boolean useNewTab) {
ArrayList<MenuItem> arl = _navbar.get(category);
if( arl == null ) {
arl = new ArrayList();
_navbar.put(category, arl);
_navbarOrdering.add(category);
}
arl.add(new MenuItem(r, name, useNewTab));
return r;
}
// TODO clean this stuff, typeahead should take type name
protected static Class mapTypeahead(Class c) {
if(c != null) {
if( PCAModel.class.isAssignableFrom(c) )
return TypeaheadPCAModelKeyRequest.class;
if( NBModel.class.isAssignableFrom(c) )
return TypeaheadNBModelKeyRequest.class;
if( Model.class.isAssignableFrom(c))
return TypeaheadModelKeyRequest.class;
if( Frame.class.isAssignableFrom(c) )
return TypeaheadHexKeyRequest.class;
}
return TypeaheadKeysRequest.class;
}
// ==========================================================================
public boolean toHTML(StringBuilder sb) {
return false;
}
public void toJava(StringBuilder sb) {}
public String toDocGET() {
return null;
}
/**
* Example of passing and failing request. Will be prepended with
* "curl -s localhost:54321/Request.json". Return param/value pairs that will be used to build up
* a URL, and the result from serving the URL will show up as an example.
*/
public String[] DocExampleSucc() {
return null;
}
public String[] DocExampleFail() {
return null;
}
public String HTMLHelp() {
return DocGen.HTML.genHelp(this);
}
public String ReSTHelp() {
return DocGen.ReST.genHelp(this);
}
// Dummy write of a leading field, so the auto-gen JSON can just add commas
// before each succeeding field.
@Override public AutoBuffer writeJSONFields(AutoBuffer bb) { return bb.putJSON4("Request2",0); }
/**
* Request API versioning.
* TODO: better solution would be to have an explicit annotation for each request
* - something like <code>@API-VERSION(2) @API-VERSION(1)</code>
* Annotation will be processed during start of RequestServer and default version will be registered
* under /, else /version/name_of_request.
*/
protected static final API_VERSION[] SUPPORTS_ONLY_V1 = new API_VERSION[] { API_VERSION.V_1 };
protected static final API_VERSION[] SUPPORTS_ONLY_V2 = new API_VERSION[] { API_VERSION.V_2 };
protected static final API_VERSION[] SUPPORTS_V1_V2 = new API_VERSION[] { API_VERSION.V_1, API_VERSION.V_2 };
public API_VERSION[] supportedVersions() { return SUPPORTS_ONLY_V1; }
}