code: def generate_main_class(endpoints):
      yield dedent(
          0,
          """
          /*
           * This file is auto-generated by h2o-3/h2o-bindings/bin/gen_java.py
           * Copyright 2016 H2O.ai;  Apache License Version 2.0 (see LICENSE for details)
           */
          package water.bindings;
  
          import water.bindings.pojos.*;
          import water.bindings.proxies.retrofit.*;
          import retrofit2.*;
          import retrofit2.converter.gson.GsonConverterFactory;
          import com.google.gson.*;
          import com.google.gson.reflect.TypeToken;
          import com.google.gson.stream.JsonReader;
          import com.google.gson.stream.JsonWriter;
          import okhttp3.OkHttpClient;
          import java.io.IOException;
          import java.util.concurrent.TimeUnit;
          import java.lang.reflect.Array;
          import java.lang.reflect.Field;
          import java.lang.reflect.InvocationTargetException;
          import java.lang.reflect.Type;
  
  
          @SuppressWarnings("unused")
          public class H2oApi {
  
            public static String DEFAULT_URL = \"http://localhost:54321/\";
  
            public H2oApi() {
              this(DEFAULT_URL);
            }
            public H2oApi(String url) {
              _url = url;
            }
  
            public H2oApi setUrl(String url) {
              _url = url;
              retrofit = null;
              return this;
            }
  
            public H2oApi setTimeout(int t) {
              timeout_s = t;
              retrofit = null;
              return this;
            }
  
            /**
             * Set time interval for job polling in {@link #waitForJobCompletion(JobKeyV3)}.
             *   @param millis time interval, in milliseconds
             */
            public H2oApi setJobPollInterval(int millis) {
              pollInterval_ms = millis;
              return this;
            }
  
            /**
             * Continuously poll server for the status of the given job, until it completes.
             *   @param jobKey job to query
             *   @return the finished job
             */
            public JobV3 waitForJobCompletion(JobKeyV3 jobKey) {
              return waitForJobCompletion(keyToString(jobKey));
            }
            public JobV3 waitForJobCompletion(String jobId) {
              Jobs jobService = getService(Jobs.class);
              Response<JobsV3> jobsResponse = null;
              int retries = 3;
              JobsV3 jobs = null;
              do {
                try {
                  Thread.sleep(pollInterval_ms);
                  jobsResponse = jobService.fetch(jobId).execute();
                } catch (IOException e) {
                  System.err.println(\"Caught exception: \" + e);
                } catch (InterruptedException e) { /* pass */ }
                if (jobsResponse == null || !jobsResponse.isSuccessful())
                  if (retries-- > 0)
                    continue;
                  else
                    throw new RuntimeException(\"/3/Jobs/\" + jobId + \" failed 3 times.\");
                jobs = jobsResponse.body();
                if (jobs.jobs == null || jobs.jobs.length != 1)
                  throw new RuntimeException(\"Failed to find Job: \" + jobId);
              } while (jobs != null && jobs.jobs[0].status.equals(\"RUNNING\"));
              return jobs == null? null : jobs.jobs[0];
            }
  
          """,
      )
  
      for route in endpoints:
          apiname = route["api_name"]
          class_name = route["class_name"]
          outtype = route["output_schema"]
          input_fields = route["input_params"]
          required_fields = [
              field for field in input_fields if field["required"]
          ]
          input_fields_wo_excluded = [
              field
              for field in input_fields
              if field["name"] != "_exclude_fields"
          ]
  
          if class_name == "SegmentModelsBuilders":
              continue
  
          yield "  /**"
          yield bi.wrap(route["summary"], indent="   * ")
          yield "   */"
          # Make several versions of each API call:
          #  (1) Only the required parameters
          #  (2) All parameters except the _excluded_fields
          #  (3) All parameters
          li = len(input_fields)
          le = len(input_fields_wo_excluded)
          lr = len(required_fields)
          assert lr <= 3, (
              "Too many required fields in method " + apiname
          )
          if lr == li:
              # The set of required fields is the same as the set of input fields. No need for (2) and (3).
              input_fields = None
              input_fields_wo_excluded = None
          elif le == li or le == lr or li >= 4:
              # If set (2) coincides with either (1) or (3), then we will not generate code for it.
              # Additionally, if there are too many input params so that we will put them into a container class,
              # then there will be no need for separate case (2) either.
              input_fields_wo_excluded = None
  
          # Temporary hack, since Parse requires all the fields from ParseSetup: don't generate the required-fields-only
          # method
          if "parse" == apiname:
              required_fields = None
  
          for fields in [
              required_fields,
              input_fields_wo_excluded,
              input_fields,
          ]:
              if fields is None:
                  continue
              use_schema_param = len(fields) >= 4
              value_field_strs = []
              typed_field_strs = []
              for field in fields:
                  ftype = translate_type(
                      field["type"], field["schema_name"]
                  )
                  fname = translate_name(field["name"])
                  typed_field_strs.append("%s %s" % (ftype, fname))
                  if use_schema_param:
                      fname = "params." + fname
                  if ftype.endswith("KeyV3"):
                      s = "keyToString(%s)" % fname
                  elif ftype.endswith("KeyV3[]"):
                      s = "keyArrayToStringArray(%s)" % fname
                  elif ftype == "ColSpecifierV3":
                      s = "colToString(%s)" % fname
                  else:
                      s = fname
                  value_field_strs.append(s)
  
              if use_schema_param:
                  args = route["input_schema"] + " params"
                  values = (
                      "\n      "
                      + ",\n      ".join(value_field_strs)
                      + "\n    "
                  )
              else:
                  args = ", ".join(typed_field_strs)
                  values = ", ".join(value_field_strs)
                  if fields == input_fields_wo_excluded:
                      values += ', ""'
  
              method = (
                  apiname
                  if route["handler_method"] == "exec"
                  else route["handler_method"]
              )
              yield "  public {type} {method}({args}) throws IOException {{".format(
                  type=outtype, method=apiname, args=args
              )
              yield "    {clazz} s = getService({clazz}.class);".format(
                  clazz=class_name
              )
              yield "    return s.{method}({values}).execute().body();".format(
                  method=method, values=values
              )
              yield "  }"
          yield ""
  
      yield dedent(
          2,
          """
  
            //--------- PRIVATE --------------------------------------------------------------------------------------------------
  
            private Retrofit retrofit;
            private String _url = DEFAULT_URL;
            private int timeout_s = 60;
            private int pollInterval_ms = 1000;
  
            private void initializeRetrofit() {
              Gson gson = new GsonBuilder()
                .registerTypeAdapterFactory(new ModelV3TypeAdapter())
                .registerTypeAdapter(KeyV3.class, new KeySerializer())
                .registerTypeAdapter(ColSpecifierV3.class, new ColSerializer())
                .registerTypeAdapter(ModelBuilderSchema.class, new ModelDeserializer())
                .registerTypeAdapter(ModelSchemaBaseV3.class, new ModelSchemaDeserializer())
                .registerTypeAdapter(ModelOutputSchemaV3.class, new ModelOutputDeserializer())
                .registerTypeAdapter(ModelParametersSchemaV3.class, new ModelParametersDeserializer())
                .create();
  
              OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(timeout_s, TimeUnit.SECONDS)
                .writeTimeout(timeout_s, TimeUnit.SECONDS)
                .readTimeout(timeout_s, TimeUnit.SECONDS)
                .build();
  
              this.retrofit = new Retrofit.Builder()
                .client(client)
                .baseUrl(_url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();
            }
  
            private Retrofit getRetrofit() {
              if (retrofit == null) initializeRetrofit();
              return retrofit;
            }
  
            private <T> T getService(Class<T> clazz) {
              return getRetrofit().create(clazz);
            }
  
  
            /**
             * Keys get sent as Strings and returned as objects also containing the type and URL,
             * so they need a custom GSON serializer.
             */
            private static class KeySerializer implements JsonSerializer<KeyV3>, JsonDeserializer<KeyV3> {
              @Override
              public JsonElement serialize(KeyV3 key, Type typeOfKey, JsonSerializationContext context) {
                return new JsonPrimitive(key.name);
              }
              @Override
              public KeyV3 deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
                if (json.isJsonNull()) return null;
                JsonObject jobj = json.getAsJsonObject();
                String type = jobj.get(\"type\").getAsString();
                switch (type) {
                  // TODO: dynamically generate all possible cases
                  case \"Key<Model>\": return context.deserialize(jobj, ModelKeyV3.class);
                  case \"Key<Job>\":   return context.deserialize(jobj, JobKeyV3.class);
                  case \"Key<Grid>\":  return context.deserialize(jobj, GridKeyV3.class);
                  case \"Key<Frame>\": return context.deserialize(jobj, FrameKeyV3.class);
                  default: throw new JsonParseException(\"Unable to deserialize key of type \" + type);
                }
              }
            }
  
            private static class ColSerializer implements JsonSerializer<ColSpecifierV3> {
              @Override
              public JsonElement serialize(ColSpecifierV3 col, Type typeOfCol, JsonSerializationContext context) {
                return new JsonPrimitive(col.columnName);
              }
            }
  
          """,
      )
  
      def inj(middle):
          return (
              lambda schema, algo: schema[: len(algo)]
              + middle
              + schema[len(algo) :]
          )
  
      for clz, base, target_clz_factory in [
          (
              "ModelDeserializer",
              "ModelBuilderSchema",
              lambda schema, algo: schema,
          ),
          (
              "ModelSchemaDeserializer",
              "ModelSchemaBaseV3",
              inj("Model"),
          ),
          (
              "ModelOutputDeserializer",
              "ModelOutputSchemaV3",
              inj("ModelOutput"),
          ),
          (
              "ModelParametersDeserializer",
              "ModelParametersSchemaV3",
              inj("Parameters"),
          ),
      ]:
          yield "  /**"
          yield "   * Factory method for parsing a %s json object into an instance of the model-specific subclass." % base
          yield "   */"
          yield "  private static class %s implements JsonDeserializer<%s> {" % (
              clz,
              base,
          )
          yield "    @Override"
          yield "    public %s deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)" % base
          yield "      throws JsonParseException {"
          yield "      if (json.isJsonNull()) return null;"
          yield "      if (json.isJsonObject()) {"
          yield "        JsonObject jobj = json.getAsJsonObject();"
          yield '        if (jobj.has("algo")) {'
          yield '          String algo = jobj.get("algo").getAsJsonPrimitive().getAsString().toLowerCase();'
          yield "          switch (algo) {"
          for route in endpoints:
              if route["class_name"] == "ModelBuilders" and route[
                  "api_name"
              ].startswith("train"):
                  algo = route["algo"]
                  oschema = route["output_schema"]
                  assert (
                      oschema.lower()[: len(algo)] == algo
                  ), "Wrong output schema for algo %s: %s" % (
                      algo,
                      oschema,
                  )
                  model = target_clz_factory(oschema, algo)
                  yield '            case "{algo}": return context.deserialize(json, {model}.class);'.format(
                      **locals()
                  )
          yield "            default:"
          yield '              throw new JsonParseException("Unable to deserialize model of type " + algo);'
          yield "          }"
          yield "        }"
          yield "      }"
          yield '      throw new JsonParseException("Invalid %s element " + json.toString());' % base
          yield "    }"
          yield "  }"
          yield ""
  
      yield dedent(
          2,
          """
            private static class ModelV3TypeAdapter implements TypeAdapterFactory {
              @Override
              public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
                final Class<? super T> rawType = type.getRawType();
                if (!ModelBuilderSchema.class.isAssignableFrom(rawType) &&
                    !ModelSchemaBaseV3.class.isAssignableFrom(rawType)) return null;
                final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
                return new TypeAdapter<T>() {
                  @Override
                  public void write(JsonWriter out, T value) throws IOException {
                    delegate.write(out, value);
                  }
                  @Override
                  public T read(JsonReader in) throws IOException {
                    JsonObject jobj = new JsonParser().parse(in).getAsJsonObject();
                    if (jobj.has(\"parameters\") && jobj.get(\"parameters\").isJsonArray()) {
                      JsonArray jarr = jobj.get(\"parameters\").getAsJsonArray();
                      JsonObject paramsNew = new JsonObject();
                      for (JsonElement item : jarr) {
                        JsonObject itemObj = item.getAsJsonObject();
                        paramsNew.add(itemObj.get(\"name\").getAsString(), itemObj.get(\"actual_value\"));
                      }
                      jobj.add(\"parameters\", paramsNew);
                    }
                    // noinspection unchecked
                    return (T) new Gson().fromJson(jobj, rawType);
                  }
                };
              }
            }
  
  
            /**
             * Return an array of Strings for an array of keys.
             */
            public static String[] keyArrayToStringArray(KeyV3[] keys) {
              if (keys == null) return null;
              String[] ids = new String[keys.length];
              int i = 0;
              for (KeyV3 key : keys) ids[i++] = key.name;
              return ids;
            }
  
            /**
             * Return an array of keys from an array of Strings.
             * @param ids array of string ids to convert to KeyV3's
             * @param clz class of key objects to create. Since we have JobKeyV3, FrameKeyV3, ModelKeyV3, etc -- this
             *            method needs to know which of these keys you want to create
             */
            public static <T extends KeyV3> T[] stringArrayToKeyArray(String[] ids, Class<T> clz) {
              if (ids == null) return null;
              // noinspection unchecked
              T[] keys = (T[]) Array.newInstance(clz, ids.length);
              String keyType = clz.getSimpleName();
              if (keyType.endsWith(\"KeyV3\")) keyType = keyType.substring(0, keyType.length()-5);
              try {
                int i = 0;
                for (String id: ids) {
                  keys[i] = clz.getConstructor().newInstance();
                  keys[i].name = id;
                  keys[i].type = keyType;
                  i++;
                }
              }
              catch (Exception e) {
                e.printStackTrace();
              }
              return keys;
            }
  
            /**
             *
             */
            public static String keyToString(KeyV3 key) {
              return key == null? null : key.name;
            }
  
            /**
             *
             */
            public static FrameKeyV3 stringToFrameKey(String key) {
              if (key == null) return null;
              FrameKeyV3 k = new FrameKeyV3();
              k.name = key;
              return k;
            }
  
            /**
             *
             */
            private static String colToString(ColSpecifierV3 col) {
              return col == null? null : col.columnName;
            }
  
            /**
             *
             */
            public static String stringToCol(String col) {
              if (col == null) return null;
              ColSpecifierV3 c = new ColSpecifierV3();
              c.columnName = col;
              return col;
            }
  
  
            public static void copyFields(Object to, Object from) {
              Field[] fromFields = from.getClass().getDeclaredFields();
              Field[] toFields   = to.getClass().getDeclaredFields();
  
              for (Field fromField : fromFields){
                Field toField;
                try {
                  toField = to.getClass().getDeclaredField(fromField.getName());
                  fromField.setAccessible(true);
                  toField.setAccessible(true);
                  toField.set(to, fromField.get(from));
                }
                catch (Exception ignored) {
                  // NoSuchField is the normal case
                }
              }
            }
          }
          """,
      )
  
file_path: h2o-3/h2o-bindings/bin/gen_java.py
imports: import re
  import textwrap
  import bindings
label_desc: Use image node_id 8 for calling a global function with example usage: generate_main_class(endpoints) and returns: function