package water.api; import water.H2O; import water.Iced; import water.IcedWrapper; import water.Weaver; import water.api.schemas3.*; import water.exceptions.H2OIllegalArgumentException; import water.util.IcedHashMapBase; import water.util.IcedHashMapGeneric; import water.util.Log; import water.util.ReflectionUtils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; /** * The metadata info on all the fields in a Schema. This is used to help Schema be self-documenting, * and to generate language bindings for route handlers and entities. */ public final class SchemaMetadata extends Iced { public int version; public String name; public String superclass; public String type; public List<FieldMetadata> fields; public String markdown; // TODO: combine with ModelParameterSchemaV2. static public final class FieldMetadata extends Iced { /** * Field name in the POJO. Set through reflection. */ public String name; /** * Type for this field. Set through reflection. */ public String type; /** * Type for this field is itself a Schema. Set through reflection. */ public boolean is_schema; /** * Schema name for this field, if it is_schema. Set through reflection. */ public String schema_name; /** * Value for this field. Set through reflection. */ public Iced value; /** * A short help description to appear alongside the field in a UI. Set from the @API annotation. */ String help; /** * The label that should be displayed for the field if the name is insufficient. Set from the @API annotation. */ String label; /** * Is this field required, or is the default value generally sufficient? Set from the @API annotation. */ boolean required; /** * How important is this field? The web UI uses the level to do a slow reveal of the parameters. Set from the @API annotation. */ API.Level level; /** * Is this field an input, output or inout? Set from the @API annotation. */ API.Direction direction; /** * Is this field inherited from a class higher in the hierarchy? */ public boolean is_inherited; /** * If this field is inherited from a class higher in the hierarchy which one? */ public String inherited_from; /** * Is this field gridable? Set from the @API annotation. */ public boolean is_gridable; // The following are markers for *input* fields. /** * For enum-type fields the allowed values are specified using the values annotation. * This is used in UIs to tell the user the allowed values, and for validation. * Set from the @API annotation. */ String[] values; /** * Should this field be rendered in the JSON representation? Set from the @API annotation. */ boolean json; /** * For Vec-type fields this is the set of Frame-type fields which must contain the named column. * For example, for a SupervisedModel the response_column must be in both the training_frame * and (if it's set) the validation_frame. */ String[] is_member_of_frames; /** * For Vec-type fields this is the set of other Vec-type fields which must contain * mutually exclusive values. For example, for a SupervisedModel the response_column * must be mutually exclusive with the weights_column. */ String[] is_mutually_exclusive_with; public FieldMetadata() { } /** * Create a new FieldMetadata object for the given Field of the given Schema. * @param schema water.api.Schema object * @param f java.lang.reflect.Field for the Schema class */ public FieldMetadata(Schema schema, Field f, List<Field>superclassFields) { super(); try { f.setAccessible(true); // handle private and protected fields // Get annotation directly API annotation = f.getAnnotation(API.class); this.name = f.getName(); Object o = f.get(schema); this.value = consValue(o); // Enum is a field of enum type or of String type with defined and fixed set of values! boolean is_enum = isEnum(f.getType(), annotation) || (f.getType().isArray() && isEnum(f.getType().getComponentType(), annotation)); boolean is_fake_enum = isFakeEnum(f.getType(), annotation) || (f.getType().isArray() && isFakeEnum(f.getType().getComponentType(), annotation)); this.is_schema = Schema.class.isAssignableFrom(f.getType()) || (f.getType().isArray() && Schema.class.isAssignableFrom(f.getType().getComponentType())); this.type = consType(schema, ReflectionUtils.findActualFieldClass(schema.getClass(), f), f.getName(), annotation); // Note, this has to work when the field is null. In addition, if the field's type is a base class we want to see if we have a versioned schema for its Iced type and, if so, use it. if (this.is_schema) { // First, get the class of the field: NOTE: this gets the actual type for genericized fields, but not for arrays of genericized fields Class schema_class = f.getType().isArray()? f.getType().getComponentType() : ReflectionUtils.findActualFieldClass(schema.getClass(), f); this.schema_name = schema_class.getSimpleName(); } else if ((is_enum || is_fake_enum) && !f.getType().isArray()) { // We have enums of the same name defined in a few classes (e.g., Loss and Initialization) this.schema_name = getEnumSchemaName(is_enum ? f.getType() : annotation.valuesProvider()); } else if ((is_enum || is_fake_enum) && f.getType().isArray()) { // We have enums of the same name defined in a few classes (e.g., Loss and Initialization) this.schema_name = getEnumSchemaName(is_enum ? f.getType().getComponentType() : annotation.valuesProvider()); } this.is_inherited = (superclassFields.contains(f)); if (this.is_inherited) this.inherited_from = f.getDeclaringClass().getSimpleName(); if (null != annotation) { this.help = annotation.help(); this.label = this.name; this.required = annotation.required(); this.level = annotation.level(); this.direction = annotation.direction(); this.is_gridable = annotation.gridable(); this.values = annotation.valuesProvider() == ValuesProvider.NULL ? annotation.values() : getValues(annotation.valuesProvider()); this.json = annotation.json(); this.is_member_of_frames = annotation.is_member_of_frames(); this.is_mutually_exclusive_with = annotation.is_mutually_exclusive_with(); // TODO: need to form the transitive closure // If the field is an enum then the values annotation field had better be set. . . if (is_enum && (null == this.values || 0 == this.values.length)) { throw H2O.fail("Didn't find values annotation for enum field: " + this.name); } } } catch (Exception e) { throw H2O.fail("Caught exception accessing field: " + f + " for schema object: " + schema + ": " + e.toString()); } } // FieldMetadata(Schema, Field) /** * Factory method to create a new FieldMetadata instance if the Field has an @API annotation. * @param schema water.api.Schema object * @param f java.lang.reflect.Field for the Schema class * @return a new FieldMetadata instance if the Field has an @API annotation, else null */ public static FieldMetadata createIfApiAnnotation(Schema schema, Field f, List<Field> superclassFields) { f.setAccessible(true); // handle private and protected fields if (null != f.getAnnotation(API.class)) return new FieldMetadata(schema, f, superclassFields); Log.warn("Skipping field that lacks an annotation: " + schema.toString() + "." + f); return null; } /** For a given Class generate a client-friendly type name (e.g., int[][] or Frame). */ public static String consType(Schema schema, Class clz, String field_name, API annotation) { boolean is_enum = isEnum(clz, null) || isFakeEnum(clz, annotation); boolean is_array = clz.isArray(); // built-in Java types: if (is_enum) return "enum"; if (String.class.isAssignableFrom(clz)) return "string"; // lower-case, to be less Java-centric if (clz.equals(Boolean.TYPE) || clz.equals(Byte.TYPE) || clz.equals(Short.TYPE) || clz.equals(Integer.TYPE) || clz.equals(Long.TYPE) || clz.equals(Float.TYPE) || clz.equals(Double.TYPE)) return clz.toString(); if (is_array) return consType(schema, clz.getComponentType(), field_name, annotation) + "[]"; if (Map.class.isAssignableFrom(clz)) { if (IcedHashMapGeneric.class.isAssignableFrom(clz) || IcedHashMapBase.class.isAssignableFrom(clz)) { String type0 = ReflectionUtils.findActualClassParameter(clz, 0).getSimpleName(); String type1 = ReflectionUtils.findActualClassParameter(clz, 1).getSimpleName(); if ("String".equals(type0)) type0 = "string"; if ("String".equals(type1)) type1 = "string"; return "Map<" + type0 + "," + type1 + ">"; } else { Log.warn("Schema Map field isn't a subclass of IcedHashMap, so its metadata won't have type parameters: " + schema.getClass().getSimpleName() + "." + field_name); return "Map"; } } if (List.class.isAssignableFrom(clz)) return "List"; // H2O-specific types: // TODO: NOTE, this is a mix of Schema types and Iced types; that's not right. . . // Should ONLY have schema types. // Also, this mapping could/should be moved to Schema. if (water.Key.class.isAssignableFrom(clz)) { Log.warn("Raw Key (not KeySchema) in Schema: " + schema.getClass() + " field: " + field_name); return "Key"; } if (KeyV3.class.isAssignableFrom(clz)) { return "Key<" + KeyV3.getKeyedClassType((Class<? extends KeyV3>) clz) + ">"; } if (Schema.class.isAssignableFrom(clz)) { return Schema.getImplClass((Class<Schema>)clz).getSimpleName(); // same as Schema.schema_type } if (Iced.class.isAssignableFrom(clz)) { if (clz == SchemaV3.Meta.class) { // Special case where we allow an Iced in a Schema so we don't get infinite meta-regress: return "Schema.Meta"; } else { // Special cases: polymorphic metadata fields that can contain scalars, Schemas (any Iced, actually), or arrays of these: if (schema instanceof ModelParameterSchemaV3 && ("default_value".equals(field_name) || "actual_value".equals(field_name))) return "Polymorphic"; if ((schema instanceof FieldMetadataV3) && "value".equals(field_name)) return "Polymorphic"; if (((schema instanceof TwoDimTableV3) && "data".equals(field_name))) // IcedWrapper return "Polymorphic"; Log.warn("WARNING: found non-Schema Iced field: " + clz.toString() + " in Schema: " + schema.getClass() + " field: " + field_name); return clz.getSimpleName(); } } String msg = "Don't know how to generate a client-friendly type name for class: " + clz.toString() + " in Schema: " + schema.getClass() + " field: " + field_name; Log.warn(msg); throw H2O.fail(msg); } public static Iced consValue(Object o) { if (null == o) return null; Class clz = o.getClass(); if (water.Iced.class.isAssignableFrom(clz)) return (Iced)o; if (clz.isArray()) { return new IcedWrapper(o); } /* if (water.Keyed.class.isAssignableFrom(o.getClass())) { Keyed k = (Keyed)o; return k._key.toString(); } if (! o.getClass().isArray()) { if (Schema.class.isAssignableFrom(o.getClass())) { return new String(((Schema)o).writeJSON(new AutoBuffer()).buf()); } else { return o.toString(); } } StringBuilder sb = new StringBuilder(); sb.append("["); for (int i = 0; i < Array.getLength(o); i++) { if (i > 0) sb.append(", "); sb.append(consValue(Array.get(o, i))); } sb.append("]"); return sb.toString(); */ // Primitive type if (clz.isPrimitive()) return new IcedWrapper(o); if (o instanceof Number) return new IcedWrapper(o); if (o instanceof Boolean) return new IcedWrapper(o); if (o instanceof String) return new IcedWrapper(o); if (o instanceof Enum) return new IcedWrapper(o); throw new H2OIllegalArgumentException("o", "consValue", o); } } // FieldMetadata public SchemaMetadata() { fields = new ArrayList<>(); } public SchemaMetadata(Schema schema) { version = schema.getSchemaVersion(); name = schema.getSchemaName(); type = schema.getSchemaType(); superclass = schema.getClass().getSuperclass().getSimpleName(); // Get metadata of all annotated fields fields = getFieldMetadata(schema); // Also generates markdown markdown = schema.markdown(this, true, true).toString(); } /** * Returns metadata of all annotated fields. * * @param schema a schema instance * @return list of field metadata */ public static List<FieldMetadata> getFieldMetadata(Schema schema) { List<Field> superclassFields = Arrays.asList(Weaver.getWovenFields(schema.getClass().getSuperclass())); List<FieldMetadata> fields = new ArrayList<>(); // Fields up to but not including Schema for (Field field : Weaver.getWovenFields(schema.getClass())) { FieldMetadata fmd = FieldMetadata.createIfApiAnnotation(schema, field, superclassFields); if (null != fmd) // skip transient or other non-annotated fields fields.add(fmd); // NOTE: we include non-JSON fields here; remove them later if we don't want them } return fields; } public static SchemaMetadata createSchemaMetadata(String classname) throws IllegalArgumentException { try { Class<? extends Schema> clz = (Class<? extends Schema>) Class.forName(classname); Schema s = clz.newInstance(); s.fillFromImpl(s.createImpl()); // get defaults return new SchemaMetadata(s); } catch (Exception e) { String msg = "Caught exception fetching schema: " + classname + ": " + e; Log.warn(msg); throw new IllegalArgumentException(msg); } } private static String[] getValues(Class<? extends ValuesProvider> valuesProvider) { String[] values; try { ValuesProvider vp = valuesProvider.newInstance(); values = vp.values(); } catch (Throwable e) { values = null; } return values; } // Enum is a field of enum type or of String type with defined and fixed set of values! private static boolean isEnum(Class<?> type, API annotation) { return Enum.class.isAssignableFrom(type); } private static boolean isFakeEnum(Class<?> type, API annotation) { return (annotation != null && annotation.valuesProvider() != ValuesProvider.NULL && String.class.isAssignableFrom(type)); } private static String getEnumSchemaName(Class<?> type) { StringBuffer sb = new StringBuffer(type.getCanonicalName()); sb.delete(0, sb.indexOf(".")+1); sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); return sb.toString().replace(".", "").replace("$", ""); } }