package water.api; import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import dontweave.gson.JsonObject; import water.*; import water.Request2.TypeaheadKey; import water.api.Request.Filter; import water.api.Request.Validator; import water.fvec.Frame; import water.fvec.Vec; import water.util.Check; import water.util.RString; import water.util.Utils; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Properties; /** All arguments related classes are defined in this guy. * * Argument is the base class for all arguments, which then branches to * different still abstract subclasses that specify how are any given HTML input * elements being drawn. * * From these a proper Arguments that define particular value types are then * subclassed. * * When an argument is created, its pointer is stored in the _arguments array * list so that the request knows how many arguments and in which order it has. * * Because request objects and therefore also argument objects one per * application, while the codepath can be multithreaded (server decides this), * the argument state is not preserved in the argument itself, but in the * Record static object that is kept thread local and must be properly * initialized at each iteration by calling reset() method on the argument. * * See the respective classes for more details. * * NOTE add more arguments to this class as they are needed and keep them here. * * @author peta */ public class RequestArguments extends RequestStatics { // =========================================================================== // Helper functions // =========================================================================== /** Returns a json object containing all arguments specified to the page. * * Useful for redirects and polling. */ protected JsonObject argumentsToJson() { JsonObject result = new JsonObject(); for (Argument a : _arguments) { if (a.specified()) result.addProperty(a._name,a.originalValue()); } return result; } protected static int frameColumnNameToIndex(Frame fr, String input, boolean namesOnly) { // first check if we have string match for (int i = 0; fr._names != null && i < fr._names.length; ++i) { String colName = fr._names[i]; if (colName == null) colName = String.valueOf(i); if (colName.equals(input)) return i; } try { if(!namesOnly) { int i = Integer.parseInt(input); if ((i<0) || (i>=fr.vecs().length)) return -1; return i; } } catch (NumberFormatException e) { } return -1; } // =========================================================================== // Record // =========================================================================== /** List of arguments for the request. Automatically filled in by the argument * constructors. */ protected transient ArrayList<Argument> _arguments = new ArrayList(); public ArrayList<Argument> arguments() { return _arguments; } // --------------------------------------------------------------------------- /** Argument state record. * * Contains all state required for the argument and a few functions to operate * on the state. */ protected static class Record<T> { /** Determines the original input value of the argument. null if the value * was not supplied, or was empty. Retains the original value even if the * argument value is wrong. */ public String _originalValue = null; /** Parsed value. If the parse was successful, or default value if the * checking failed, or the argument is not required and was missing. Note * that default value may very well be null and thus you cannot check this * for null of determine validity. */ public T _value = null; /** Reason why the argument is disabled, or null if it is enabled. A * disabled argument cannot be edited by the user yet. */ public String _disabledReason = null; /** True if the argument's value stored in _value is valid, that is either * correctly parsed, or not present and default value used. Note that if * checking fails, the defaultValue is stored in _value, but _valid is * false. */ public boolean _valid = false; /** Returns true if the argument is disabled. */ public boolean disabled() { return _disabledReason != null; } /** Returns true if the argument is valid. */ public boolean valid() { return _valid; } /** Returns if the argument is specified by user. returns true only if it is * valid and parsing the argument was successful. * @return */ public boolean specified() { return valid() && _originalValue != null; } } // A string used to display the query element part of the argument protected static final String _queryHtml = "\n<dl class='dl-horizontal'>" + "<dt style='padding-top:3px'><span rel='tooltip' title='%TOOLTIP_DESCRIPTION' data-placement='left'>%ASTERISK %NAME</span></dt>" + "<dd>%ELEMENT %COMMENT</dd>" + "</dl>" ; // =========================================================================== // Argument // =========================================================================== public abstract class Argument<T> extends Iced implements Filter { @Override public boolean run(Object value) { throw new RuntimeException("Should not be called for special case Argument"); } /** As with request's _requestHelp, this provides the extended help that * will be displayed on the help and wiki pages. Specify this in the * particular request constructor. */ public String _requestHelp; /** True if the argument should not appear in the automatically generated * query. */ public boolean _hideInQuery = false; /** * True if the argument should be only read-only. */ public boolean _readOnly = false; /** * Can be a grid search parameter. */ public boolean _gridable; /** * For keys. If specified, the key must exist. */ public boolean _mustExist; /** Value validator. */ public Validator<T> _validator; /** Override this method to provide parsing of the input string to the Java * expected value. The input is guaranteed to be non-empty when this method * is called and all prerequisities are guaranteed to be valid before this * method is called. */ protected abstract T parse(String input) throws IllegalArgumentException; /** Returns the default value of the argument. Note that the method will be * called also on required arguments, in which case it is ok return null. * * It is kept abstract because defining a proper default value might be * tricky and in many case you do not want it to be null. Overriding it * always makes you think:) */ protected abstract T defaultValue(); /** Returns the javascript code that will be executed when the query is * loaded that associates the given callback JS function with the on change * event of the input. This method is only called if the element should * refresh the webpage upon its change. */ protected abstract String jsRefresh(String callbackName); /** Returns the javascript code that will be executed when the value of * the argument is to be determined. It must contain a return statement, * that returns the string that should be sent back to the request for the * given arhument. */ protected abstract String jsValue(); /** If there is any additional javascript that should be dumped to the * query page, it should be defined here. Please follow chaining rules. */ protected String jsAddons() { return ""; } /** Returns the HTML elements of the argument query only. This should return * the elements in HTML that will be used to enter the value. For instance * the input text, selection, etc. */ protected abstract String queryElement(); /* A little bonus extra text out to the right */ protected String queryComment() { return ""; } /** Returns the query description. This is a concise description of a * correct value for the argument. generally used as a placeholder in the * html query elements. */ protected abstract String queryDescription(); /** Returns a list of possible error strings, that could be thrown in an * IllegalArgumentException. */ protected String[] errors() { return null; } /** Any query addons can be specified here. These will be displayed with * the query html code and should be used for instance for default value * calculators, etc. */ protected String queryAddons() { return ""; } public String getName() { return _name; } public String getDisplayName() { return _displayName; } /** Returns the html query for the given argument, including the full * formatting. That means not only the queryElement, but also the argument * name in front of it, etc. * * You may want to override this if you want different form layouts to be * present. */ protected String query() { RString result = new RString(_queryHtml); result.replace("ID",_name); result.replace("NAME", _displayName != null ? _displayName : JSON2HTML(_name)); if (disabled()) result.replace("ELEMENT","<div class='alert alert-info' style='padding-top:4px;padding-bottom:4px;margin-bottom:5px'>"+record()._disabledReason+"</div>"); else result.replace("ELEMENT",queryElement()); result.replace("TOOLTIP_DESCRIPTION", queryDescription()); if (!disabled()) result.replace("COMMENT",queryComment()); if (_required) result.replace("ASTERISK","<span style='color:#ff0000'>* </span>"); return result.toString(); } /** Creates the request help page part for the given argument. Displays its * JSON name, query name (the one in HTML), value type and the request help * provided by the argument. */ public final JsonObject requestHelp() { JsonObject r = new JsonObject(); r.addProperty(NAME, _name); r.addProperty(DESCRIPTION, queryDescription()); r.addProperty(HELP, _requestHelp); return r; } /** Name of the argument. This must correspond to the name of the JSON * request argument. */ public String _name, _displayName; /** True if the argument is required, false if it may be skipped. */ public boolean _required; /** True if change of the value in the query controls should trigger an * automatic refresh of the query form. * * This is set by the setrefreshOnChange() method. It is automatically set * for any controls that are prerequisites for other controls and can be * manually select for other controls by users (do it in the request * constructor). */ private boolean _refreshOnChange; /** List of all prerequisite arguments for the current argument. All the * prerequisite arguments must be created before the current argument. */ public transient ArrayList<Argument<T>> _prerequisites = null; /** The thread local argument state record. Must be initialized at the * beginning of each request before it can be used. */ private transient ThreadLocal<Record> _argumentRecord = new ThreadLocal(); /** * If argument has been created reflectively from a request field. */ public transient Field _field; /** Creates the argument of given name. Also specifies whether the argument * is required or not. This cannot be changed later. */ protected Argument(String name, boolean required) { assert Check.paramName(name); _name = name; _required = required; _refreshOnChange = false; _arguments.add(this); } /** Adds the given argument as a prerequisite. This means that current * argument will not be checked and/or reported in queries as a control form * unless all its prerequisite arguments are in a valid state. (the argument * will be disabled if not all its prerequisites are satisfied). */ protected final void addPrerequisite(Argument arg) { if (_prerequisites == null) _prerequisites = new ArrayList(); _prerequisites.add(arg); arg.setRefreshOnChange(); } /** Returns the thread local argument state record. */ protected final Record<T> record() { return _argumentRecord.get(); } /** Disables the argument with given reason. If the argument is already * disabled, its reason is overwritten by the new one. * * NOTE disable(null) effectively enables the argument, that is why the * assert! */ public final void disable(String reason) { assert (reason != null); record()._disabledReason = reason; } /** Disables the argument and makes its input value empty. This is the * preferred way of disabling arguments. */ public final void disable(String reason, Properties args) { assert (reason != null); disable(reason); args.remove(_name); } /** Returns whether the argument is disabled or not. */ public final boolean disabled() { return record().disabled(); } /** Makes the argument refresh the query page on its change automatically. * If you want this behavior to be disabled for the argument, overwrite this * method to error. */ public void setRefreshOnChange() { _refreshOnChange = true; } /** Returns true if the argument refreshes the query automatically on its * change. */ public boolean refreshOnChange() { return _refreshOnChange; } /** Returns true if the argument is valid. Valid means specified by user * and parsed properly, or not required and not specified. */ public final boolean valid() { // return record().valid(); return record() != null && record().valid(); } /** Returns true if the argument is specified by the user. That is if the * argument value was submitted by the user and parsed correctly. */ public final boolean specified() { return record() != null && record().specified(); } /** Returns the value of the argument. This is either the value parsed, if * specified, or defaultValue. Note that default value is returned also for * invalid arguments. */ public final T value() { return record()._value; } /** Returns the input value submitted by the user, if specified. */ public final String originalValue() { return record()._originalValue; } /** Resets the argument by creating it a new thread local state. Everything * is null and the argument is not valid. */ public final void reset() { _argumentRecord.set(new Record()); } /** Checks that the argument supplied is correct. This method is called for * each argument and is given the HTTP supplied argument value. If the value * was not supplied, input contains an empty string. * * The argument must already be reseted before calling this method. * * If the argument is disabled, the function does not do anything except * setting the original value in the record. * * If the prerequisites of the argument are not all valid, then the argument * is disabled and function returns. * * Then the argument is parsed if provided, or an error thrown if the input * is empty and the argument is required. * * At the end of the function the value is either the result of a successful * parse() call or a defaultValue or null if the argument is disabled. * However if the argument is disabled a defaultValue should not be called. */ public void check(RequestQueries callInstance, String input) throws IllegalArgumentException { // get the record -- we assume we have been reset properly Record record = record(); // check that the input is canonical == value or null and store it to the // record if (input.isEmpty()) input = null; record._originalValue = input; // there is not much to do if we are disabled if (record.disabled()) { record._value = null; return; } // check that we have all prerequisites properly initialized if (_prerequisites != null) { for (Argument dep : _prerequisites) if (!dep.valid()) { record._disabledReason = "Not all prerequisite arguments have been supplied: "+dep._name; record._value = null; return; } } // if input is null, throw if required, otherwise use the default value if (input == null) { if (_required) throw new IllegalArgumentException("Argument '"+_name+"' is required, but not specified"); record._value = defaultValue(); record._valid = true; // parse the argument, if parse throws we will still be invalid correctly } else { try { record._value = parse(input); record._valid = true; if(callInstance instanceof Request2) ((Request2) callInstance).set(this, input, record._value); } catch( IllegalArgumentException e) { //record._value = defaultValue(); throw e; } } } } // =========================================================================== // InputText // =========================================================================== /** Argument that uses simple text input to define its value. * * This is the simplest argument. Uses the classic input element. All * functionality is supported. * * @param <T> */ public abstract class InputText<T> extends Argument<T> { public InputText(String name, boolean required) { super(name, required); } /** A query element is the default HTML form input. * * The id of the element is the name of the argument. Placeholder is the * query description and the value is filled in either as the value * submitted, or as the toString() method on defaultValue. */ @Override protected String queryElement() { // first determine the value to put in the field Record record = record(); String value = record._originalValue; // if no original value was supplied, try the one provided by the // default value if (value == null) { T v = defaultValue(); value = (v == null) ? "" : v.toString(); } if (_name == "path" || _name == "key" || _name == "source" || _name == "data_key" || _name == "source_key" || _name == "model_key" || _name == "thresholds" || _name == "model") { return "<input autocomplete=\"off\"" + (_readOnly ? " disabled" : "")+ " class='span5' type='text' name='"+_name+"' id='"+_name+"' placeholder='"+queryDescription()+"' "+ (!value.isEmpty() ? (" value='"+value+"' />") : "/>"); } else { return "<input " + (_readOnly ? " disabled" : "")+ " class='span5' type='text' name='"+_name+"' id='"+_name+"' placeholder='"+queryDescription()+"' "+ (!value.isEmpty() ? (" value='"+value+"' />") : "/>"); } } /** JS refresh is a default jQuery hook to the change() method. */ @Override protected String jsRefresh(String callbackName) { return "$('#"+_name+"').change('"+_name+"',"+callbackName+");"; } /** JS value is the simple jQuery val() method. */ @Override protected String jsValue() { return "return $('#"+_name+"').val();"; } } // =========================================================================== // TypeaheadInputText // =========================================================================== /** Typeahead enabled text input. * * Typeahead is enabled using the jQuery typeahead plugin. You must specify * the JSON request which provides the typeahead, and the data name in the * response that contains the array of strings corresponding to the typeahead * options. Optionally you can specify the typeahead limit (how many options * will be displayed), which is 1024 by default. * * The typeahead json request must take Str argument filter and Int optional * argument limit. */ public abstract class TypeaheadInputText<T> extends InputText<T> { /** href of the json request supplying the typeahead values. */ protected final String _typeaheadHref; /** Typeahead limit. If more than this limit options will be available, the * typeahead will be disabled. */ protected final int _typeaheadLimit; /** Creates the typeahead. */ protected TypeaheadInputText(Class<? extends TypeaheadRequest> href, String name, boolean required) { super(name, required); _typeaheadHref = href.getSimpleName(); _typeaheadLimit = 1024; } /** Adds the json to hook initialize the typeahead functionality. It is * jQuery typeahead plugin standard initialization with async filler. */ @Override protected String jsAddons() { RString s = new RString("" + "$('#%ID').typeahead({\n" + " source:\n" + " function(query,process) {\n" + " return $.get('%HREF', { filter: query, limit: %LIMIT }, function (data) {\n" + " return process(data.%DATA_NAME);\n" + " });\n" + " },\n" + "});\n" + "\n"); s.replace("ID", _name); s.replace("HREF", _typeaheadHref); s.replace("LIMIT", _typeaheadLimit); s.replace("DATA_NAME", ITEMS); return super.jsAddons()+s.toString(); } } // =========================================================================== // InputCheckBox // =========================================================================== /** A boolean argument that is represented as the checkbox. * * The only allowed values for a boolean checkbox are "0" for false, "1" for * true. If the argument is not required, then default value will be used. * * Please note that due to the nature of a checkbox, the html query will * always specify this argument to its default value, or to false if the user * did not specify it explicitly. */ public abstract class InputCheckBox extends Argument<Boolean> { /** Default value. */ public final transient Boolean _defaultValue; /** Creates the argument with specified default value. */ public InputCheckBox(String name, boolean defaultValue) { super(name, false); // checkbox is never required _defaultValue = defaultValue; } /** Creates the argument as required one. This has only effect on JSON, for * HTML it means the default value is false effectively. */ public InputCheckBox(String name) { super(name, true); _defaultValue = null; } /** Parses the value. 1 to true and 0 to false. Anything else is an error. */ @Override public Boolean parse(String input) { if (input.equals("1")) return true; if (input.equals("0")) return false; if (input.equals("true")) return true; if (input.equals("false")) return false; throw new H2OIllegalArgumentException(this, input+" is not valid boolean value. Only 1 and 0 are allowed."); } /** Displays the query element. This is just the checkbox followed by the * description. */ @Override protected String queryElement() { // first determine the value to put in the field Record record = record(); String value = record._originalValue; // if no original value was supplied, use the provided one if (value == null) { Boolean v = defaultValue(); value = ((v == null) || (v == false)) ? "" : "1" ; } return "<input value='1' class='span5' type='checkbox' name='"+_name+"' id='"+_name+"' "+ (value.equals("1") ? (" checked />") : "/>")+"  "+queryDescription(); } /** Refresh only taps to jQuery change event. */ @Override protected String jsRefresh(String callbackName) { return "$('#"+_name+"').change('"+_name+"',"+callbackName+");"; } /** Returns 1 if the checkbox is checked and 0 otherwise. */ @Override protected String jsValue() { return "return $('#"+_name+"').is(':checked') ? '1' : '0';"; } /** Returns the default value. */ @Override protected Boolean defaultValue() { return _defaultValue; } } // =========================================================================== // InputSelect // =========================================================================== /** Select element from the list of options. * * Array of values and arrays of names can be specified together with the * selected element's value. */ public abstract class InputSelect<T> extends Argument<T> { /** Override this method to provide the values for the options. These will * be the possible values returned by the form's input and should be the * possible values for the JSON argument. */ protected abstract String[] selectValues(); /** Returns which value should be selected. This is *not* the default value * itself, as the default values may be of any type, but the input value * that should be selected in the browser. */ protected abstract String selectedItemValue(); /** Override this method to determine the value names, that is the names * displayed in the browser. Return null, if the value strings should be * used (this is default behavior). */ protected String[] selectNames() { return null; } /** Constructor just calls super. */ public InputSelect(String name, boolean required) { super(name, required); } /** Displays the query element. It is a select tag with option tags inside. * If the argument is required then additional empty value is added with * name "Please select..." that ensures that the user selects actual value. */ @Override protected String queryElement() { StringBuilder sb = new StringBuilder(); sb.append("<select id='"+_name+"' name='"+_name+"'>"); String selected = selectedItemValue(); String[] values = selectValues(); String[] names = selectNames(); if (names == null) names = values; assert (values.length == names.length); if (_required) sb.append("<option value=''>Please select...</option>"); for (int i = 0 ; i < values.length; ++i) { if (values[i].equals(selected)) sb.append("<option value='"+values[i]+"' selected>"+names[i]+"</option>"); else sb.append("<option value='"+values[i]+"'>"+names[i]+"</option>"); } sb.append("</select>"); return sb.toString(); } /** Refresh is supported using standard jQuery change event. */ @Override protected String jsRefresh(String callbackName) { return "$('#"+_name+"').change('"+_name+"',"+callbackName+");"; } /** Get value is supported by the standard val() jQuery function. */ @Override protected String jsValue() { return "return $('#"+_name+"').val();"; } } // =========================================================================== // MultipleCheckbox // =========================================================================== /** Displays multiple checkboxes for different values. Returns a list of the * checked values separated by commas. */ public abstract class MultipleSelect<T> extends Argument<T> { /** Override this method to provide the values for the options. These will * be the possible values returned by the form's input and should be the * possible values for the JSON argument. */ protected abstract String[] selectValues(); /** Returns true if the given option (by its value) is selected. False * otherwise. */ protected abstract boolean isSelected(String value); /** Override this method to determine the value names, that is the names * displayed in the browser. Return null, if the value strings should be * used (this is default behavior). */ protected String[] selectNames() { return null; } /** Constructor just calls super. Is never required, translates to the * default value. */ public MultipleSelect(String name) { super(name, false); } /** Displays the query element. It is a tabled list of all possibilities * with an optional scrollbar on the right. */ @Override protected String queryElement() { String[] values = selectValues(); String[] names = selectNames(); if (names == null) names = values; assert (values.length == names.length); if (values.length == 0) return "<div class='alert alert-error'>No editable controls under current setup</div>"; StringBuilder sb = new StringBuilder(); sb.append("<select multiple"); sb.append(" size='").append(Math.min(20, values.length)).append("'"); sb.append(" id='").append(_name).append("' >"); for (int i = 0 ; i < values.length; ++i) { sb.append("<option value='").append(values[i]).append("' "); if( isSelected(values[i]) ) sb.append("selected='true' "); sb.append(">").append(names[i]).append("</option>"); } sb.append("</select>"); return sb.toString(); } /** Refresh is supported using standard jQuery change event. Each * possibility's checkbox is instrumented. */ @Override protected String jsRefresh(String callbackName) { return "$('#"+_name+"').change('"+_name+"',"+callbackName+");"; } /** Get value is supported by a JS function that enumerates over the * possibilities. If checked, the value of the possibility is appended to * a comma separated list. */ @Override protected String jsValue() { return "var tmp = $('#"+_name+"').val(); return tmp == null ? \"\" : tmp.join(',');"; } } // =========================================================================== // MultipleText // =========================================================================== private static final char JS_SEP = '='; private static final String _multipleTextValueJS = " var str = ''\n" + " for (var i = 0; i < %NUMITEMS; ++i) {\n" + " var element = $('#%NAME'+i);\n" + " if (element.val() != '') {\n" + " if (str == '')\n" + " str = element.attr('name') + '" + JS_SEP + "' +element.val();\n" + " else\n" + " str = str + ',' + element.attr('name') + '" + JS_SEP + "' + element.val();\n" + " }\n" + " }\n" + " return str;\n" ; public abstract class MultipleText<T> extends Argument<T> { protected abstract String[] textValues(); protected abstract String[] textNames(); protected String[] textPrefixes() { return null; } protected String[] textSuffixes() { return null; } protected String textSuffix() { return null; } public MultipleText(String name, boolean required) { super(name, required); } /** Displays the query element. It is a tabled list of all possibilities * with an optional scrollbar on the right. */ @Override protected String queryElement() { StringBuilder sb = new StringBuilder(); sb.append("<div style='max-height:300px;overflow:auto'>"); String[] prefixes = textPrefixes(); String[] values = textValues(); String[] names = textNames(); String[] suffixes = textSuffixes(); if (prefixes == null) prefixes = names; if (suffixes == null && textSuffix() != null) { suffixes = new String[names.length]; String suffix = textSuffix(); for(int i = 0; i<names.length; i++) suffixes[i] = suffix; } if (values == null) { values = new String[prefixes.length]; for (int i = 0; i < values.length; ++i) values[i] = ""; } assert (prefixes.length == values.length); if (values.length == 0) sb.append("<div class='alert alert-error'>No editable controls under current setup</div>"); for (int i = 0 ; i < values.length; ++i) { sb.append("<div class='input-prepend" + (suffixes!=null?" input-append":"") + "'>"); sb.append("<span class='add-on'>" + prefixes[i]+"</span>"); sb.append("<input autocomplete=\"off\" class='span3' name='"+names[i]+"' id='"+_name+String.valueOf(i)+"' type='text' value='"+values[i]+"' placeholder='"+queryDescription()+"'>"); if (suffixes!=null) sb.append("<span class='add-on'>" + suffixes[i]+"</span>"); sb.append("</div>"); } sb.append("</div>"); return sb.toString(); } /** Refresh is supported using standard jQuery change event. Each text * input is instrumented. */ @Override protected String jsRefresh(String callbackName) { int size = textNames().length; StringBuilder sb = new StringBuilder(); for (int i = 0; i < size; ++i) sb.append("$('#"+_name+String.valueOf(i)+"').change('"+_name+"',"+callbackName+");\n"); return sb.toString(); } /** Get value is supported by a JS function that enumerates over the * possibilities. If checked, the value of the possibility is appended to * a comma separated list. */ @Override protected String jsValue() { int size = textNames().length; RString result = new RString(_multipleTextValueJS); result.replace("NUMITEMS",size); result.replace("NAME",_name); return result.toString(); } } // =========================================================================== // UserDefinedArguments // // Place your used defined arguments here. // // =========================================================================== // --------------------------------------------------------------------------- // Str // --------------------------------------------------------------------------- /** A string value. * * Any string can be a proper value. If required, empty string is not allowed. */ public class Str extends InputText<String> { public final String _defaultValue; public Str(String name) { super(name,true); _defaultValue = null; } public Str(String name, String defaultValue) { super(name, false); _defaultValue = defaultValue; } @Override protected String parse(String input) throws IllegalArgumentException { return input; } @Override protected String defaultValue() { return _defaultValue; } @Override protected String queryDescription() { return _required ? "Any non-empty string" : "Any string"; } } public static class NumberSequence { public final double [] _arr; final String _str; final boolean _ints; public NumberSequence(double [] val, String str, boolean ints) { _arr = val; _str = str; _ints = ints; } public NumberSequence(String str, boolean mul, double defaultStep) { this(parseArray(str,mul,defaultStep),str, false); } static double [] parseArray(String input, boolean mul, double defaultStep) { String str = input.trim().toLowerCase(); if(str.startsWith("c(") && str.endsWith(")")) str = str.substring(2,str.length()-1); if( str.startsWith("seq") ) { throw new RuntimeException("unimplemented"); } if( str.contains(":") ) return parseGenerator(input, mul, defaultStep); else if( str.contains(",") ) { String [] parts = str.split(","); double [] res = new double[parts.length]; for(int i = 0; i < parts.length; ++i) res[i] = Double.parseDouble(parts[i]); return res; } else { return new double [] {Double.parseDouble(str)}; } } public static double[] parseGenerator(String input, boolean mul, double defaultStep) { String str = input.trim().toLowerCase(); String [] parts = str.split(":"); if(parts.length != 2 && parts.length != 3 )throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); double step = defaultStep; if( parts.length == 3 ){ step = Double.parseDouble(parts[2]); } double from = Double.parseDouble(parts[0]); double to = Double.parseDouble(parts[1]); if(to == from) return new double[]{from}; if(to < from)throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); if(mul?(step <= 1):(step<=0))throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); // make sure we have format from < to double [] res = new double[1024]; int i = 0; while(from <= to){ res[i++] = from; if(i == res.length)res = Arrays.copyOf(res, res.length + Math.max(1, res.length >> 1)); if( mul) from *= step; else from += step; } return Arrays.copyOf(res,i); } static NumberSequence parse(String input, boolean mul, double defaultStep) { return new NumberSequence(parseArray(input, mul, defaultStep),null, false); } @Override public String toString() { if(_str != null)return _str; if(_arr == null || _arr.length == 0)return""; StringBuilder res = new StringBuilder(); for(int i = 0; i < _arr.length; ++i) { if(i > 0) res.append(","); res.append(_ints ? "" + (int) _arr[i] : _arr[i]); } return res.toString(); } } public static class NumberSequenceFloat { public final float [] _arr; final String _str; final boolean _ints; public NumberSequenceFloat(float [] val, String str, boolean ints) { _arr = val; _str = str; _ints = ints; } public NumberSequenceFloat(String str, boolean mul, float defaultStep) { this(parseArray(str,mul,defaultStep),str, false); } static float [] parseArray(String input, boolean mul, float defaultStep) { String str = input.trim().toLowerCase(); if(str.startsWith("c(") && str.endsWith(")")) str = str.substring(2,str.length()-1); if( str.startsWith("seq") ) { throw new RuntimeException("unimplemented"); } if( str.contains(":") ) return parseGenerator(input, mul, defaultStep); else if( str.contains(",") ) { String [] parts = str.split(","); float [] res = new float[parts.length]; for(int i = 0; i < parts.length; ++i) res[i] = Float.parseFloat(parts[i]); return res; } else { return new float [] {Float.parseFloat(str)}; } } public static float[] parseGenerator(String input, boolean mul, float defaultStep) { String str = input.trim().toLowerCase(); String [] parts = str.split(":"); if(parts.length != 2 && parts.length != 3 )throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); float step = defaultStep; if( parts.length == 3 ){ step = Float.parseFloat(parts[2]); } float from = Float.parseFloat(parts[0]); float to = Float.parseFloat(parts[1]); if(to == from) return new float[]{from}; if(to < from)throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); if(mul?(step <= 1):(step<=0))throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); // make sure we have format from < to float [] res = new float[1024]; int i = 0; while(from <= to){ res[i++] = from; if(i == res.length)res = Arrays.copyOf(res, res.length + Math.max(1, res.length >> 1)); if( mul) from *= step; else from += step; } return Arrays.copyOf(res,i); } static NumberSequenceFloat parse(String input, boolean mul, float defaultStep) { return new NumberSequenceFloat(parseArray(input, mul, defaultStep),null, false); } @Override public String toString() { if(_str != null)return _str; if(_arr == null || _arr.length == 0)return""; StringBuilder res = new StringBuilder(); for(int i = 0; i < _arr.length; ++i) { if(i > 0) res.append(","); res.append(_ints ? "" + (int) _arr[i] : _arr[i]); } return res.toString(); } } public class RSeq extends InputText<NumberSequence> { boolean _multiplicative; transient NumberSequence _dVal; double _defaultStep; String _comment; @Override public String queryComment() { if( disabled() ) return ""; if( _comment != null ) return _comment; return "Comma separated list of values. Or range specified as from:to:step" + (_multiplicative?"(*).":"(+)."); } public RSeq(String name, boolean req, boolean mul){ this(name,req,null,mul); } public RSeq(String seq, boolean mul){ this("", false, new NumberSequence(seq, mul, 0), mul); } public RSeq(String name, boolean req, NumberSequence dVal, boolean mul){ this(name, req, dVal, mul, null); } public RSeq(String name, boolean req, NumberSequence dVal, boolean mul, String comment){ super(name,req); _dVal = dVal; _multiplicative = mul; _defaultStep = mul?10:1; _comment = comment; } @Override protected NumberSequence parse(String input) throws IllegalArgumentException { try { return NumberSequence.parse(input, _multiplicative, _defaultStep); } catch( NumberFormatException e) { // allow grid search number sequences to pass without an exception (if all numbers except for [(),] are valid) if (input.contains("(") && input.contains(")")) { try { String[] s = input.replaceAll("[()]", "").split(","); //remove ( and ) and split on , for (String num : s) Double.parseDouble(num); //try to parse every number as Double return NumberSequence.parse(s[0], _multiplicative, _defaultStep); //HACK: report back the first number (to satisfy the UI) } catch (NumberFormatException e2) { throw new IllegalArgumentException("Value " + input + " is not a valid number sequence."); } } else throw new IllegalArgumentException("Value " + input + " is not a valid number sequence."); } } @Override protected NumberSequence defaultValue() { return _dVal; } @Override protected String queryDescription() { return "Number sequence. Comma separated list of values. Or range specified as from:to:step."; } } public class RSeqFloat extends InputText<NumberSequenceFloat> { boolean _multiplicative; transient NumberSequenceFloat _fVal; float _defaultStep; String _comment; @Override public String queryComment() { if( disabled() ) return ""; if( _comment != null ) return _comment; return "Comma separated list of values. Or range specified as from:to:step" + (_multiplicative?"(*).":"(+)."); } public RSeqFloat(String name, boolean req, boolean mul){ this(name,req,null,mul); } public RSeqFloat(String seq, boolean mul){ this("", false, new NumberSequenceFloat(seq, mul, 0), mul); } public RSeqFloat(String name, boolean req, NumberSequenceFloat fVal, boolean mul){ this(name, req, fVal, mul, null); } public RSeqFloat(String name, boolean req, NumberSequenceFloat fVal, boolean mul, String comment){ super(name,req); _fVal = fVal; _multiplicative = mul; _defaultStep = mul?10:1; _comment = comment; } @Override protected NumberSequenceFloat parse(String input) throws IllegalArgumentException { try { return NumberSequenceFloat.parse(input, _multiplicative, _defaultStep); } catch( NumberFormatException e) { throw new IllegalArgumentException("Value "+input+" is not a valid number sequence."); } } @Override protected NumberSequenceFloat defaultValue() { return _fVal; } @Override protected String queryDescription() { return "Number sequence. Comma separated list of values. Or range specified as from:to:step."; } } // --------------------------------------------------------------------------- // Int // --------------------------------------------------------------------------- public class Int extends InputText<Integer> { public final transient Integer _defaultValue; public final int _min; public final int _max; public Int(String name) { this(name, Integer.MIN_VALUE, Integer.MAX_VALUE); } public Int(String name, int min, int max) { super(name,true); _defaultValue = null; _min = min; _max = max; } public Int(String name, Integer defaultValue) { this(name, defaultValue, Integer.MIN_VALUE, Integer.MAX_VALUE); } public Int(String name, Integer defaultValue, int min, int max) { super(name,false); _defaultValue = defaultValue; _min = min; _max = max; } @Override protected Integer parse(String input) throws IllegalArgumentException { try { int i = Integer.parseInt(input); if ((i< _min) || (i > _max)) throw new H2OIllegalArgumentException(this, "Value "+i+" is not between "+_min+" and "+_max+" (inclusive)"); return i; } catch( NumberFormatException e ) { throw new H2OIllegalArgumentException(this, "Value "+input+" is not a valid integer."); } } @Override protected Integer defaultValue() { return _defaultValue; } @Override protected String queryDescription() { return ((_min == Integer.MIN_VALUE) && (_max == Integer.MAX_VALUE)) ? "Integer value" : "Integer from "+_min+" to "+_max; } } // --------------------------------------------------------------------------- // LongInt // --------------------------------------------------------------------------- public class LongInt extends InputText<Long> { public final transient long _defaultValue; public final long _min; public final long _max; public final String _comment; public LongInt(String name, long min, long max) { this(name,false,0,min,max,""); } public LongInt(String name, long defaultValue, String comment) { this(name, false, defaultValue, Long.MIN_VALUE, Long.MAX_VALUE, comment); } public LongInt(String name, boolean req, long defaultValue, long min, long max, String comment) { super(name, req); _defaultValue = defaultValue; _min = min; _max = max; _comment = comment; } @Override protected Long parse(String input) throws IllegalArgumentException { long i = 0; try { i = Long.parseLong(input); } catch( NumberFormatException e ) { double d = Double.NaN; try { d = Double.parseDouble(input); i = (long)d; } catch ( NumberFormatException xe ) { d = i - 1; } // make d different from i if( i!=d ) throw new H2OIllegalArgumentException(this, "Value '"+input+"' is not a valid long integer."); } if ((i< _min) || (i > _max)) throw new H2OIllegalArgumentException(this, "Value "+i+" is not between "+_min+" and "+_max+" (inclusive)"); return i; } @Override protected Long defaultValue() { return _defaultValue; } @Override protected String queryComment() { return _comment; } @Override protected String queryDescription() { return ((_min == Long.MIN_VALUE) && (_max == Long.MAX_VALUE)) ? "Integer value" : "Integer from "+_min+" to "+_max; } } // --------------------------------------------------------------------------- // Real // --------------------------------------------------------------------------- public class Real extends InputText<Double> { public transient final Double _defaultValue; public double _min; public double _max; public final String _comment; public Real(String name) { this(name, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } public Real(String name, double min, double max) { super(name,true); _defaultValue = null; _min = min; _max = max; _comment = ""; } public Real(String name, Double defaultValue) { this(name, false, defaultValue, null, null, ""); } public Real(String name, Double defaultValue, String comment) { this(name, false, defaultValue, null, null, comment); } public Real(String name, Double defaultValue, double min, double max, String comment) { this(name, false, defaultValue, min, max, comment); } public Real(String name, boolean req, Double defaultValue, Double min, Double max, String comment) { super(name,req); _defaultValue = defaultValue; _min = min != null ? min : Double.NEGATIVE_INFINITY; _max = max != null ? max : Double.POSITIVE_INFINITY; _comment = comment; } @Override protected Double parse(String input) throws IllegalArgumentException { try { double i = Double.parseDouble(input); if ((i< _min) || (i > _max)) throw new H2OIllegalArgumentException(this, "Value "+i+" is not between "+_min+" and "+_max+" (inclusive)"); return i; } catch( NumberFormatException e ) { throw new H2OIllegalArgumentException(this, "Value "+input+" is not a valid real number."); } } @Override protected Double defaultValue() { return _defaultValue; } @Override protected String queryComment() { return _comment; } @Override protected String queryDescription() { return ((_min == Double.NEGATIVE_INFINITY) && (_max == Double.POSITIVE_INFINITY)) ? "Real value" : "Real from "+_min+" to "+_max; } } // --------------------------------------------------------------------------- // Bool // --------------------------------------------------------------------------- public class Bool extends InputCheckBox { public final String _description; public Bool(String name, boolean defaultValue, String description) { super(name, defaultValue); _description = description; } public void setValue(boolean b){ record()._value = b; record()._valid = true; record()._originalValue = b?"1":"0"; } @Override protected String queryDescription() { return _description; } } public class ClassifyBool extends Bool { private FrameClassVec _fcv; public ClassifyBool(String name, FrameClassVec fcv) { super(name,false,"Classification or Regression"); addPrerequisite(_fcv=fcv); setRefreshOnChange(); } @Override public Boolean parse(String input) { boolean b=false; if( false ) ; else if (input.equals("1")) b= true; else if (input.equals("0")) b= false; else if (input.equals("true")) b= true; else if (input.equals("false")) b= false; else throw new H2OIllegalArgumentException(this, input+" is not valid boolean value. Only 1 and 0 are allowed."); Vec vec = _fcv.value(); if( !vec.isInt() && b ) throw new H2OIllegalArgumentException(this, "Float response allows only regression!"); if( vec.isEnum() && !b ) throw new H2OIllegalArgumentException(this, "Categorical response allows only classification!"); return b; } @Override protected Boolean defaultValue() { return _fcv.value().isInt(); // Allows only float columns for regression } } public class DRFCopyDataBool extends Bool { private TypeaheadKey _frkey; public DRFCopyDataBool(String name, TypeaheadKey frkey) { super(name,false,"Run on one node only; no network overhead but fewer cpus used. Suitable for small datasets."); addPrerequisite(_frkey=frkey); setRefreshOnChange(); } protected Frame fr() { return DKV.get(_frkey.value()).get(); } @Override public Boolean parse(String input) { boolean b=false; if( false ) ; else if (input.equals("1")) b= true; else if (input.equals("0")) b= false; else if (input.equals("true")) b= true; else if (input.equals("false")) b= false; else throw new H2OIllegalArgumentException(this, input+" is not valid boolean value. Only 1 and 0 are allowed."); return b; } @Override protected Boolean defaultValue() { // Can we allocate ALL of the dataset locally? long bs = fr().byteSize(); if( !MemoryManager.tryReserveTaskMem(bs) ) return false; // Also, do we have enough chunks to run it well globally? if( fr().anyVec().nChunks() >= 2*H2O.CLOUD.size() ) return false; // Less than 2 chunks per node, and fits locally... default to local-only return true; } } // --------------------------------------------------------------------------- // EnumClass // --------------------------------------------------------------------------- public class EnumArgument<T extends Enum<T>> extends InputSelect<T> { protected transient final Class<T> _enumClass; private transient final T _defaultValue; public EnumArgument(T defaultValue) { this("", defaultValue, false); } public EnumArgument(String name, T defaultValue, boolean refreshOnChange) { this(name,defaultValue); if(refreshOnChange)setRefreshOnChange(); } public EnumArgument(String name, T defaultValue) { super(name, false); _defaultValue = defaultValue; _enumClass = (Class<T>) defaultValue.getClass(); } public EnumArgument(String name, Class enumClass) { super(name, true); _defaultValue = null; _enumClass = enumClass; } @Override protected String[] selectValues() { T[] _enums = _enumClass.getEnumConstants(); String[] result = new String[_enums.length]; for (int i = 0; i < _enums.length; ++i) result[i] = _enums[i].toString(); return result; } @Override protected String selectedItemValue() { T v = value(); if (v == null) return ""; return v.toString(); } @Override protected T parse(String input) throws IllegalArgumentException { for (T v : _enumClass.getEnumConstants()) if (v.toString().equals(input)) return v; throw new H2OIllegalArgumentException(this, "Only "+Arrays.toString(selectValues())+" accepted for argument "+_name); } @Override protected T defaultValue() { return _defaultValue; } @Override protected String queryDescription() { return "Any of "+Arrays.toString(selectValues()); } } // --------------------------------------------------------------------------- // ExistingFile // --------------------------------------------------------------------------- public class ExistingFile extends TypeaheadInputText<File> { public ExistingFile(String name) { super(TypeaheadFileRequest.class, name, true); } @Override protected File parse(String input) throws IllegalArgumentException { File f = new File(input); if( !f.exists() ) throw new H2OIllegalArgumentException(this, "File "+input+" not found"); return f; } @Override protected String queryDescription() { return "Existing file or directory"; } @Override protected File defaultValue() { return null; } @Override protected String[] errors() { return new String[] { "File not found" }; } } public class GeneralFile extends TypeaheadInputText<String> { public GeneralFile() {this("");} public GeneralFile(String name) { super(TypeaheadFileRequest.class, name, true); } @Override protected String parse(String input) throws IllegalArgumentException { return input; } @Override protected String queryDescription() { return "File or directory, can be on NFS, HDFS or S3"; } @Override protected String defaultValue() { return ""; } @Override protected String[] errors() { return new String[] { "File not found" }; } } // --------------------------------------------------------------------------- // H2OKey // --------------------------------------------------------------------------- // key with autocompletion and autoconversion to frame public class H2OKey2 extends TypeaheadInputText<Key> { public final Key _defaultValue; public H2OKey2(String name, boolean required) { this(name,null,required); } public H2OKey2(String name, Key key) { this(name,key,false); } public H2OKey2(String name, Key key, boolean req) { super(TypeaheadKeysRequest.class,name, req); _defaultValue = key; } @Override protected Key parse(String input) { return Key.make(input); } @Override protected Key defaultValue() { return _defaultValue; } @Override protected String queryDescription() { return "Valid H2O key"; } } public class H2OKey extends InputText<Key> { public final Key _defaultValue; private final boolean _checkLegal; public H2OKey(String name, boolean required) { this(name,null,required); } public H2OKey(String name, boolean required, boolean checkLegal) { this(name,null,required,checkLegal); } public H2OKey(String name, Key key) { this(name,key,false); } public H2OKey(String name, Key key, boolean req) { super(name, req); _defaultValue = key; _checkLegal = false; } public H2OKey(String name, Key key, boolean req, boolean checkLegal) { super(name, req); _defaultValue = key; _checkLegal = checkLegal; } @Override protected Key parse(String input) { if (_checkLegal && Utils.contains(input, Key.ILLEGAL_USER_KEY_CHARS)) throw new IllegalArgumentException("Key '" + input + "' contains illegal character! Please avoid these characters: " + Key.ILLEGAL_USER_KEY_CHARS); return Key.make(input); } @Override protected Key defaultValue() { return _defaultValue; } @Override protected String queryDescription() { return "Valid H2O key"; } } // --------------------------------------------------------------------------- // H2OExistingKey // --------------------------------------------------------------------------- public class H2OExistingKey extends TypeaheadInputText<Value> { public final Key _defaultValue; public H2OExistingKey(String name) { this(name,true); } public H2OExistingKey(String name,boolean required) { super(TypeaheadKeysRequest.class, name, required); setRefreshOnChange(); _defaultValue = null; } public H2OExistingKey(String name, String keyName) { this(name, Key.make(keyName)); } public void setValue(Value v){ record()._value = v; record()._originalValue = v._key.toString(); } public H2OExistingKey(String name, Key key) { super(TypeaheadKeysRequest.class, name, false); _defaultValue = key; } @Override protected Value parse(String input) throws IllegalArgumentException { Key k = Key.make(input); Value v = DKV.get(k); if (v == null) throw new H2OIllegalArgumentException(this, "Key "+input+" not found!"); return v; } @Override protected Value defaultValue() { if (_defaultValue == null) return null; return DKV.get(_defaultValue); } @Override protected String queryDescription() { return "An existing H2O key"; } } // --------------------------------------------------------------------------- // StringListArgument // --------------------------------------------------------------------------- // NO EMPTY string in values public class StringList extends InputSelect<String> { public final String[] _values; public final int _defaultIndex; public StringList(String name, String[] values) { super(name, true); _values = values; _defaultIndex = -1; } public StringList(String name, String[] values, int defaultIndex) { super(name, false); _values = values; _defaultIndex = defaultIndex; } @Override protected String[] selectValues() { return _values; } @Override protected String selectedItemValue() { if (_required && (!valid())) return ""; return value(); } @Override protected String parse(String input) throws IllegalArgumentException { for (String s : _values) if (s.equals(input)) return input; throw new H2OIllegalArgumentException(this, "Invalid value "+input+", only "+Arrays.toString(_values)+" allowed"); } @Override protected String defaultValue() { if (_defaultIndex == -1) return null; return _values[_defaultIndex]; } @Override protected String queryDescription() { return "Any of "+Arrays.toString(_values); } } // --------------------------------------------------------------------------- // Fluid Vec Arguments // --------------------------------------------------------------------------- /** A Fluid Vec, via a column name in a Frame */ public class FrameKeyVec extends InputSelect<Vec> { final TypeaheadKey _key; boolean _optional = false; final String _desc; protected transient ThreadLocal<Integer> _colIdx= new ThreadLocal(); public FrameKeyVec(String name, TypeaheadKey key, String desc,boolean required) { super(name, required); addPrerequisite(_key=key); setRefreshOnChange(); _desc = desc; } protected Frame fr() { return DKV.get(_key.value()).get(); } @Override protected String[] selectValues() { String [] vals = fr()._names; if(!_required) vals = Utils.append(new String[]{""},vals); return vals; } @Override protected String selectedItemValue() { Frame fr = fr(); if( value() == null || fr == null ) { if(!refreshOnChange()) { // Not if has dependencies, or page doesn't refresh Vec defaultVec = defaultValue(); if( defaultVec != null && fr != null ) for( int i = 0; i < fr.vecs().length; i++ ) if( fr.vecs()[i] == defaultVec ) return fr._names[i]; } return ""; } if(_colIdx.get() == null) return ""; return fr._names[_colIdx.get()]; } @Override protected Vec parse(String input) throws IllegalArgumentException { int cidx = fr().find(input); if (cidx == -1) { try { cidx = Integer.parseInt(input); } catch( NumberFormatException e ) { cidx = -1; } if (cidx < 0 || cidx >= fr().numCols() ) throw new H2OIllegalArgumentException(this, input+" not a name of column, or a column index"); } _colIdx.set(cidx); return fr().vecs()[cidx]; } @Override protected Vec defaultValue() { return null; } @Override protected String queryDescription() { return _desc; } @Override protected String[] errors() { return new String[] { "Not a name of column, or a column index" }; } } /** A Class Vec/Column within a Frame. Limited to 1000 classes, just to prevent madness. */ public class FrameClassVec extends FrameKeyVec { public FrameClassVec(String name, TypeaheadKey key ) { super(name, key,"response column name",true); } @Override protected String[] selectValues() { final Vec [] vecs = fr().vecs(); String[] names = new String[vecs.length]; int j = 0; for( int i = 0; i < vecs.length; ++i) { if( !vecs[i].isUUID() ) // No math on strings or UUIDs names[j++] = fr()._names[i]; } return Arrays.copyOf(names, j); } @Override protected Vec defaultValue() { Frame fr = fr(); return fr != null ? fr.vecs()[fr.vecs().length - 1] : null; } } public class FrameKeyMultiVec extends MultipleSelect<int[]> { final TypeaheadKey _key; final String _description; final boolean _namesOnly; final boolean _filterNAs; transient ArrayList<FrameKeyVec> _ignoredVecs = new ArrayList<FrameKeyVec>(); protected transient ThreadLocal<Integer> _colIdx= new ThreadLocal(); protected Frame fr() { Value v = DKV.get(_key.value()); if(v == null) throw new H2OIllegalArgumentException(this, "Frame not found"); return v.get(); } public FrameKeyMultiVec(String name, TypeaheadKey key, FrameClassVec vec, String description, boolean namesOnly, boolean filterNAs) { super(name); addPrerequisite(_key = key); _description = description; _namesOnly = namesOnly; _filterNAs = filterNAs; if(vec != null) ignoreVec(vec); } public void ignoreVec(FrameKeyVec v) { for(FrameKeyVec vv:_ignoredVecs) if(vv == v)return; addPrerequisite(v); _ignoredVecs.add(v); } public boolean shouldIgnore(int i, Frame fr ) { if(fr.vecs()[i].isUUID()) return true; for(FrameKeyVec v:_ignoredVecs) if(v.value() == fr.vecs()[i]) return true; return false; } public void checkLegality(Vec v) throws IllegalArgumentException { } transient ArrayList<Integer> _selectedCols; // All the columns I'm willing to show the user @Override protected String queryElement() { Frame fr = fr(); ArrayList<Integer> cols = Lists.newArrayList(); for (int i = 0; i < fr.numCols(); ++i) if( !shouldIgnore(i, fr) ) cols.add(i); _selectedCols = cols; return super.queryElement(); } // "values" to send back and for in URLs. Use numbers for density (shorter URLs). @Override protected final String[] selectValues() { String [] res = new String[_selectedCols.size()]; int idx = 0; for(int i : _selectedCols) res[idx++] = String.valueOf(i); return res; } // "names" to select in the boxes. @Override protected String[] selectNames() { Frame fr = fr(); String [] res = new String[_selectedCols.size()]; int idx = 0; for(int i:_selectedCols) { final Vec v = fr.vec(i); int naRatio = (int)(((double)v.naCnt())/v.length()*100); res[idx++] = fr._names[i] + (naRatio > 0?"(" + naRatio + "% NAs)":""); } return res; } @Override protected boolean isSelected(String value) { int[] val = value(); return val != null && Ints.contains(val, frameColumnNameToIndex(fr(), value, _namesOnly)); } @Override protected int[] parse(String input) throws IllegalArgumentException { Frame fr = fr(); ArrayList<Integer> al = new ArrayList(); for (String col : input.split(",")) { col = col.trim(); int idx = frameColumnNameToIndex(fr(), col, _namesOnly); if (0 > idx || idx > fr.numCols()) throw new H2OIllegalArgumentException(this, "Column "+col+" not part of key "+_key.value()); if (al.contains(idx)) throw new H2OIllegalArgumentException(this, "Column "+col+" is specified twice."); checkLegality(fr.vecs()[idx]); al.add(idx); } return Ints.toArray(al); } @Override protected int[] defaultValue() { final Vec [] vecs = fr().vecs(); int [] res = new int[vecs.length]; int j = 0; for( int i = 0; i < vecs.length; ++i) { if(!(vecs[i].min() < vecs[i].max()) || (_filterNAs && ((double)vecs[i].naCnt())/vecs[i].length() > 0.1) || vecs[i].isUUID() ) // No math on strings or UUIDs res[j++] = i; // ignore constant columns and columns with too many NAs } return Arrays.copyOf(res, j); } @Override protected String queryDescription() { return _description; } } public static class H2OIllegalArgumentException extends IllegalArgumentException { public H2OIllegalArgumentException(Argument a, String msg) { super("Field '" + (a!=null ? a.getName() : "<unknown>") + "' : " + msg); } } }