THRIFT-253. java: Enhance FieldMetaData

The code generator new creates a static map of field id to metadata for each field, including information like the field TType, class of embedded structs, required/optional/default, etc. Additionally, on loading, generated classes statically register their class and metadata map with the global FieldMetaData map, so you can get the metadata for any TBase-implementing class easily.

git-svn-id: https://svn.apache.org/repos/asf/incubator/thrift/trunk@738708 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/compiler/cpp/src/generate/t_java_generator.cc b/compiler/cpp/src/generate/t_java_generator.cc
index 439aeba..3e718fb 100644
--- a/compiler/cpp/src/generate/t_java_generator.cc
+++ b/compiler/cpp/src/generate/t_java_generator.cc
@@ -11,6 +11,7 @@
 #include <vector>
 
 #include <sys/stat.h>
+#include <stdexcept>
 
 #include "platform.h"
 #include "t_oop_generator.h"
@@ -80,6 +81,8 @@
   void generate_java_struct_writer(std::ofstream& out, t_struct* tstruct);
   void generate_java_struct_tostring(std::ofstream& out, t_struct* tstruct);
   void generate_java_meta_data_map(std::ofstream& out, t_struct* tstruct);
+  void generate_field_value_meta_data(std::ofstream& out, t_type* type);
+  std::string get_java_type_string(t_type* type);
   void generate_reflection_setters(std::ostringstream& out, t_type* type, std::string field_name, std::string cap_name);
   void generate_reflection_getters(std::ostringstream& out, t_type* type, std::string field_name, std::string cap_name);
   void generate_generic_field_getters_setters(std::ofstream& out, t_struct* tstruct);
@@ -259,7 +262,8 @@
     "import java.util.HashSet;\n" +
     "import java.util.Collections;\n" +
     hash_builder +
-    "import org.apache.thrift.*;\n\n";
+    "import org.apache.thrift.*;\n" +
+    "import org.apache.thrift.meta_data.*;\n\n";
 }
 
 /**
@@ -635,6 +639,13 @@
   }
 
   generate_java_meta_data_map(out, tstruct);
+  
+  // Static initializer to populate global class to struct metadata map
+  indent(out) << "static {" << endl;
+  indent_up();
+  indent(out) << "FieldMetaData.addStructMetaDataMap(" << tstruct->get_name() << ".class, metaDataMap);" << endl;
+  indent_down();
+  indent(out) << "}" << endl << endl;
 
   // Default constructor
   indent(out) <<
@@ -1509,12 +1520,90 @@
   for (f_iter = fields.begin(); f_iter != fields.end(); ++f_iter) {
     t_field* field = *f_iter;
     std::string field_name = field->get_name();
-    indent(out) << "put(" << upcase_string(field_name) << ", new FieldMetaData(\"" << field_name << "\"));" << endl;
+    indent(out) << "put(" << upcase_string(field_name) << ", new FieldMetaData(\"" << field_name << "\", ";
+
+    // Set field requirement type (required, optional, etc.)
+    if (field->get_req() == t_field::T_REQUIRED) {
+      out << "TFieldRequirementType.REQUIRED, ";
+    } else if (field->get_req() == t_field::T_OPTIONAL) {
+      out << "TFieldRequirementType.OPTIONAL, ";
+    } else {
+      out << "TFieldRequirementType.DEFAULT, ";
+    }
+
+    // Create value meta data    
+    generate_field_value_meta_data(out, field->get_type());
+    out  << "));" << endl;
   }
   indent_down();
   indent(out) << "}});" << endl << endl;
 }
 
+/** 
+ * Returns a string with the java representation of the given thrift type
+ * (e.g. for the type struct it returns "TType.STRUCT")
+ */
+std::string t_java_generator::get_java_type_string(t_type* type) {
+  if (type->is_list()){
+    return "TType.LIST";
+  } else if (type->is_map()) {
+    return "TType.MAP";
+  } else if (type->is_set()) {
+    return "TType.SET";
+  } else if (type->is_struct() || type->is_xception()) {
+    return "TType.STRUCT";
+  } else if (type->is_enum()) {
+    return "TType.I32";
+  } else if (type->is_typedef()) {
+    return get_java_type_string(((t_typedef*)type)->get_type());
+  } else if (type->is_base_type()) {
+    switch (((t_base_type*)type)->get_base()) {
+      case t_base_type::TYPE_VOID   : return      "TType.VOID"; break;
+      case t_base_type::TYPE_STRING : return    "TType.STRING"; break;
+      case t_base_type::TYPE_BOOL   : return      "TType.BOOL"; break;
+      case t_base_type::TYPE_BYTE   : return      "TType.BYTE"; break;
+      case t_base_type::TYPE_I16    : return       "TType.I16"; break;
+      case t_base_type::TYPE_I32    : return       "TType.I32"; break;
+      case t_base_type::TYPE_I64    : return       "TType.I64"; break;
+      case t_base_type::TYPE_DOUBLE : return    "TType.DOUBLE"; break;
+      default : throw std::runtime_error("Unknown thrift type \"" + type->get_name() + "\" passed to t_java_generator::get_java_type_string!"); break; // This should never happen!
+    }
+  } else {
+    throw std::runtime_error("Unknown thrift type \"" + type->get_name() + "\" passed to t_java_generator::get_java_type_string!"); // This should never happen!
+  }
+}
+
+void t_java_generator::generate_field_value_meta_data(std::ofstream& out, t_type* type){
+  out << endl;
+  indent_up();
+  indent_up();
+  if (type->is_struct()){
+    indent(out) << "new StructMetaData(TType.STRUCT, " << type->get_name() << ".class";
+  } else if (type->is_container()){
+    if (type->is_list()){
+      indent(out) << "new ListMetaData(TType.LIST, ";
+      t_type* elem_type = ((t_list*)type)->get_elem_type();    
+      generate_field_value_meta_data(out, elem_type);   
+    } else if (type->is_set()){
+      indent(out) << "new SetMetaData(TType.SET, ";
+      t_type* elem_type = ((t_list*)type)->get_elem_type();    
+      generate_field_value_meta_data(out, elem_type); 
+    } else{ // map
+      indent(out) << "new MapMetaData(TType.MAP, ";
+      t_type* key_type = ((t_map*)type)->get_key_type();
+      t_type* val_type = ((t_map*)type)->get_val_type();
+      generate_field_value_meta_data(out, key_type);
+      out << ", ";
+      generate_field_value_meta_data(out, val_type);
+    }
+  } else {
+    indent(out) << "new FieldValueMetaData(" << get_java_type_string(type);
+  }
+  out << ")";
+  indent_down();
+  indent_down();
+}
+
 
 /**
  * Generates a thrift service. In C++, this comprises an entirely separate
diff --git a/lib/java/build.xml b/lib/java/build.xml
index 1215de1..c686c7d 100644
--- a/lib/java/build.xml
+++ b/lib/java/build.xml
@@ -61,6 +61,8 @@
       classpath="${cpath}:${build.test}" failonerror="true" />
     <java classname="org.apache.thrift.test.DeepCopyTest"
       classpath="${cpath}:${build.test}" failonerror="true" />
+    <java classname="org.apache.thrift.test.MetaDataTest"
+      classpath="${cpath}:${build.test}" failonerror="true" />
     <java classname="org.apache.thrift.test.JavaBeansTest"
       classpath="${cpath}:${build.test}" failonerror="true" />
   </target>
diff --git a/lib/java/src/org/apache/thrift/FieldMetaData.java b/lib/java/src/org/apache/thrift/FieldMetaData.java
index 2469892..e69de29 100644
--- a/lib/java/src/org/apache/thrift/FieldMetaData.java
+++ b/lib/java/src/org/apache/thrift/FieldMetaData.java
@@ -1,8 +0,0 @@
-package org.apache.thrift;
-
-public class FieldMetaData implements java.io.Serializable {
-  public final String fieldName;
-  public FieldMetaData(String fieldName){
-    this.fieldName = fieldName;
-  }
-}
diff --git a/lib/java/src/org/apache/thrift/TFieldRequirementType.java b/lib/java/src/org/apache/thrift/TFieldRequirementType.java
new file mode 100644
index 0000000..d816a78
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/TFieldRequirementType.java
@@ -0,0 +1,11 @@
+package org.apache.thrift;
+
+/**
+ * Requirement type constants.
+ *
+ */
+public final class TFieldRequirementType {
+  public static final byte REQUIRED  = 1;
+  public static final byte OPTIONAL = 2;
+  public static final byte DEFAULT = 3;
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/FieldMetaData.java b/lib/java/src/org/apache/thrift/meta_data/FieldMetaData.java
new file mode 100644
index 0000000..efb7630
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/FieldMetaData.java
@@ -0,0 +1,50 @@
+package org.apache.thrift.meta_data;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.thrift.TBase;
+
+/**
+ * This class is used to store meta data about thrift fields. Every field in a
+ * a struct should have a corresponding instance of this class describing it.
+ *
+ */
+public class FieldMetaData implements java.io.Serializable {
+  public final String fieldName;
+  public final byte requirementType;
+  public final FieldValueMetaData valueMetaData;
+  private static Map<Class<? extends TBase>, Map> structMap;
+  
+  static {
+    structMap = new HashMap<Class<? extends TBase>, Map>();
+  }
+  
+  public FieldMetaData(String name, byte req, FieldValueMetaData vMetaData){
+    this.fieldName = name;
+    this.requirementType = req;
+    this.valueMetaData = vMetaData;
+  }
+  
+  public static void addStructMetaDataMap(Class<? extends TBase> sClass, Map map){
+    structMap.put(sClass, map);
+  }
+
+  /**
+   * Returns a map with metadata (i.e. instances of FieldMetaData) that
+   * describe the fields of the given class.
+   *
+   * @param sClass The TBase class for which the metadata map is requested
+   */
+  public static Map<Integer, FieldMetaData> getStructMetaDataMap(Class<? extends TBase> sClass){
+    if (!structMap.containsKey(sClass)){ // Load class if it hasn't been loaded
+      try{
+        sClass.newInstance();
+      } catch (InstantiationException e){
+        throw new RuntimeException("InstantiationException for TBase class: " + sClass.getName() + ", message: " + e.getMessage());
+      } catch (IllegalAccessException e){
+        throw new RuntimeException("IllegalAccessException for TBase class: " + sClass.getName() + ", message: " + e.getMessage());
+      }
+    }
+    return structMap.get(sClass);
+  }
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/FieldValueMetaData.java b/lib/java/src/org/apache/thrift/meta_data/FieldValueMetaData.java
new file mode 100644
index 0000000..83607d8
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/FieldValueMetaData.java
@@ -0,0 +1,23 @@
+package org.apache.thrift.meta_data;
+
+import org.apache.thrift.protocol.TType;
+
+/**
+ * FieldValueMetaData and collection of subclasses to store metadata about
+ * the value(s) of a field
+ */
+public class FieldValueMetaData implements java.io.Serializable {
+  public final byte type;  
+ 
+  public FieldValueMetaData(byte type){
+    this.type = type;
+  }
+  
+  public boolean isStruct() {
+    return type == TType.STRUCT; 
+  }
+  
+  public boolean isContainer() {
+    return type == TType.LIST || type == TType.MAP || type == TType.SET;
+  }
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/ListMetaData.java b/lib/java/src/org/apache/thrift/meta_data/ListMetaData.java
new file mode 100644
index 0000000..6dcbb34
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/ListMetaData.java
@@ -0,0 +1,10 @@
+package org.apache.thrift.meta_data;
+
+public class ListMetaData extends FieldValueMetaData {
+  public final FieldValueMetaData elemMetaData;
+  
+  public ListMetaData(byte type, FieldValueMetaData eMetaData){
+    super(type);
+    this.elemMetaData = eMetaData;
+  }    
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/MapMetaData.java b/lib/java/src/org/apache/thrift/meta_data/MapMetaData.java
new file mode 100644
index 0000000..96aecbc
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/MapMetaData.java
@@ -0,0 +1,12 @@
+package org.apache.thrift.meta_data;
+
+public class MapMetaData extends FieldValueMetaData {
+  public final FieldValueMetaData keyMetaData;
+  public final FieldValueMetaData valueMetaData;
+  
+  public MapMetaData(byte type, FieldValueMetaData kMetaData, FieldValueMetaData vMetaData){
+    super(type);
+    this.keyMetaData = kMetaData;
+    this.valueMetaData = vMetaData;
+  }    
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/SetMetaData.java b/lib/java/src/org/apache/thrift/meta_data/SetMetaData.java
new file mode 100644
index 0000000..dcc9f07
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/SetMetaData.java
@@ -0,0 +1,10 @@
+package org.apache.thrift.meta_data;
+
+public class SetMetaData extends FieldValueMetaData {
+  public final FieldValueMetaData elemMetaData;
+  
+  public SetMetaData(byte type, FieldValueMetaData eMetaData){
+    super(type);
+    this.elemMetaData = eMetaData;
+  }    
+}
diff --git a/lib/java/src/org/apache/thrift/meta_data/StructMetaData.java b/lib/java/src/org/apache/thrift/meta_data/StructMetaData.java
new file mode 100644
index 0000000..8fd6ce0
--- /dev/null
+++ b/lib/java/src/org/apache/thrift/meta_data/StructMetaData.java
@@ -0,0 +1,10 @@
+package org.apache.thrift.meta_data;
+
+public class StructMetaData extends FieldValueMetaData {
+  public final Class structClass;
+  
+  public StructMetaData(byte type, Class sClass){
+    super(type);
+    this.structClass = sClass;
+  }    
+}
diff --git a/lib/java/test/org/apache/thrift/test/MetaDataTest.java b/lib/java/test/org/apache/thrift/test/MetaDataTest.java
new file mode 100644
index 0000000..daf9b44
--- /dev/null
+++ b/lib/java/test/org/apache/thrift/test/MetaDataTest.java
@@ -0,0 +1,60 @@
+
+package org.apache.thrift.test;
+
+import java.util.Map;
+import org.apache.thrift.TFieldRequirementType;
+import org.apache.thrift.meta_data.FieldMetaData;
+import org.apache.thrift.meta_data.ListMetaData;
+import org.apache.thrift.meta_data.MapMetaData;
+import org.apache.thrift.meta_data.SetMetaData;
+import org.apache.thrift.meta_data.StructMetaData;
+import org.apache.thrift.protocol.TType;
+import thrift.test.*;
+
+public class MetaDataTest {
+  
+  public static void main(String[] args) throws Exception {
+    CrazyNesting cn = new CrazyNesting();
+    Insanity in = new Insanity();
+    Map<Integer, FieldMetaData> mdMap = cn.metaDataMap;
+    
+    // Check for struct fields existence
+    if (mdMap.size() != 3)
+      throw new RuntimeException("metadata map contains wrong number of entries!");
+    if (!mdMap.containsKey(CrazyNesting.SET_FIELD) || !mdMap.containsKey(CrazyNesting.LIST_FIELD) || !mdMap.containsKey(CrazyNesting.STRING_FIELD))
+      throw new RuntimeException("metadata map doesn't contain entry for a struct field!");
+    
+    // Check for struct fields contents
+    if (!mdMap.get(CrazyNesting.STRING_FIELD).fieldName.equals("string_field") ||
+            !mdMap.get(CrazyNesting.LIST_FIELD).fieldName.equals("list_field") ||
+            !mdMap.get(CrazyNesting.SET_FIELD).fieldName.equals("set_field"))
+      throw new RuntimeException("metadata map contains a wrong fieldname");
+    if (mdMap.get(CrazyNesting.STRING_FIELD).requirementType != TFieldRequirementType.DEFAULT ||
+            mdMap.get(CrazyNesting.LIST_FIELD).requirementType != TFieldRequirementType.REQUIRED ||
+            mdMap.get(CrazyNesting.SET_FIELD).requirementType != TFieldRequirementType.OPTIONAL)
+      throw new RuntimeException("metadata map contains the wrong requirement type for a field");
+    if (mdMap.get(CrazyNesting.STRING_FIELD).valueMetaData.type != TType.STRING ||
+            mdMap.get(CrazyNesting.LIST_FIELD).valueMetaData.type != TType.LIST ||
+            mdMap.get(CrazyNesting.SET_FIELD).valueMetaData.type != TType.SET)
+      throw new RuntimeException("metadata map contains the wrong requirement type for a field");
+    
+    // Check nested structures
+    if (!mdMap.get(CrazyNesting.LIST_FIELD).valueMetaData.isContainer())
+      throw new RuntimeException("value metadata for a list is stored as non-container!");
+    if (mdMap.get(CrazyNesting.LIST_FIELD).valueMetaData.isStruct())
+      throw new RuntimeException("value metadata for a list is stored as a struct!");
+    if (((MapMetaData)((ListMetaData)((SetMetaData)((MapMetaData)((MapMetaData)((ListMetaData)mdMap.get(CrazyNesting.LIST_FIELD).valueMetaData).elemMetaData).valueMetaData).valueMetaData).elemMetaData).elemMetaData).keyMetaData.type != TType.STRUCT)
+      throw new RuntimeException("metadata map contains wrong type for a value in a deeply nested structure");
+    if (((StructMetaData)((MapMetaData)((ListMetaData)((SetMetaData)((MapMetaData)((MapMetaData)((ListMetaData)mdMap.get(CrazyNesting.LIST_FIELD).valueMetaData).elemMetaData).valueMetaData).valueMetaData).elemMetaData).elemMetaData).keyMetaData).structClass != Insanity.class)
+      throw new RuntimeException("metadata map contains wrong class for a struct in a deeply nested structure");
+    
+    // Check that FieldMetaData contains a map with metadata for all generated struct classes
+    if (FieldMetaData.getStructMetaDataMap(CrazyNesting.class) == null ||
+            FieldMetaData.getStructMetaDataMap(Insanity.class) == null ||
+            FieldMetaData.getStructMetaDataMap(Xtruct.class) == null)
+      throw new RuntimeException("global metadata map doesn't contain an entry for a known struct");
+    if (FieldMetaData.getStructMetaDataMap(CrazyNesting.class) != cn.metaDataMap ||
+            FieldMetaData.getStructMetaDataMap(Insanity.class) != in.metaDataMap)
+      throw new RuntimeException("global metadata map contains wrong entry for a loaded struct");    
+  }
+}
diff --git a/test/ThriftTest.thrift b/test/ThriftTest.thrift
index 275b574..a042d7e 100644
--- a/test/ThriftTest.thrift
+++ b/test/ThriftTest.thrift
@@ -43,6 +43,12 @@
   2: list<Xtruct> xtructs
 }
 
+struct CrazyNesting {
+  1: string string_field,
+  2: optional set<Insanity> set_field,
+  3: required list< map<set<i32>,map<i32,set<list<map<Insanity,string>>>>>> list_field
+}
+
 exception Xception {
   1: i32 errorCode,
   2: string message
@@ -129,4 +135,4 @@
 struct ListTypeVersioningV2 {
        1: list<string> strings;
        2: string hello;
-}
\ No newline at end of file
+}