All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jcustenborder.kafka.connect.xml.KafkaConnectPlugin Maven / Gradle / Ivy

/**
 * Copyright © 2017 Jeremy Custenborder ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.jcustenborder.kafka.connect.xml;

import com.google.common.base.CaseFormat;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JConditional;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldRef;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JForEach;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import com.sun.codemodel.JPrimitiveType;
import com.sun.codemodel.JType;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.Outline;
import org.jvnet.jaxb2_commons.plugin.AbstractParameterizablePlugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlMixed;
import javax.xml.datatype.XMLGregorianCalendar;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
//import org.jvnet.jaxb2_commons.plugin.Customizations;


public class KafkaConnectPlugin extends AbstractParameterizablePlugin {
  private static final Logger log = LoggerFactory.getLogger(KafkaConnectPlugin.class);

  @Override
  public String getOptionName() {
    return "Xconnect";
  }

  @Override
  public String getUsage() {
    return "TBD";
  }

  private static final String CONNECT_SCHEMA_FIELD = "CONNECT_SCHEMA";

  JFieldVar processSchema(JCodeModel codeModel, ClassOutline classOutline, List fields) {
    final JFieldVar schemaVariable = classOutline.implClass.field(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, connectSchemaJClass, CONNECT_SCHEMA_FIELD);
//    final JMethod staticConstructor = classOutline.implClass.constructor(JMod.STATIC);

    final JBlock constructorBlock = classOutline.implClass.init();
    final JVar builderVar = constructorBlock.decl(connectSchemaBuilderJClass, "builder", connectSchemaBuilderJClass.staticInvoke("struct"));
    final String schemaName = String.format("%s.%s", classOutline._package()._package().name(), classOutline.implClass.name());
    constructorBlock.invoke(builderVar, "name").arg(schemaName);
    constructorBlock.invoke(builderVar, "optional");

    final JVar fieldBuilderVar = constructorBlock.decl(connectSchemaBuilderJClass, "fieldBuilder");

    for (Field field : fields) {
      if (field.type != Type.STRUCT) {
        constructorBlock.assign(fieldBuilderVar, field.schemaBuilder);
        if (!field.required) {
          constructorBlock.invoke(fieldBuilderVar, "optional");
        }

        constructorBlock.invoke(builderVar, "field").arg(field.name).arg(fieldBuilderVar.invoke("build"));
      } else {
        constructorBlock.invoke(builderVar, "field").arg(field.name).arg(field.schemaBuilder);
      }
    }


    //Build the schema
    constructorBlock.assign(schemaVariable, builderVar.invoke("build"));
    return schemaVariable;
  }


  JClass connectStructJClass;
  JClass connectTimestampJClass;
  JClass connectTimeJClass;
  JClass connectDateJClass;
  JClass connectDecimalJClass;
  JClass connectSchemaBuilderJClass;
  JClass connectSchemaJClass;
  JClass connectListOfStructJClass;
  JClass connectableJClass;
  JClass timezoneJClass;

  JClass typeList;
  JClass typeArrayList;
  JClass typeBigDecimal;
  JClass typeBigInteger;
  JClass typeXMLGregorianCalendar;
  Map typeLookup;

  void setupImportedClasses(JCodeModel codeModel) {
    typeList = codeModel.ref(List.class);
    typeArrayList = codeModel.ref(ArrayList.class);

    connectStructJClass = codeModel.ref("org.apache.kafka.connect.data.Struct");
    connectListOfStructJClass = typeList.narrow(connectStructJClass);
    connectDateJClass = codeModel.ref("org.apache.kafka.connect.data.Date");
    connectTimeJClass = codeModel.ref("org.apache.kafka.connect.data.Time");
    connectDecimalJClass = codeModel.ref("org.apache.kafka.connect.data.Decimal");
    connectTimestampJClass = codeModel.ref("org.apache.kafka.connect.data.Timestamp");
    connectSchemaBuilderJClass = codeModel.ref("org.apache.kafka.connect.data.SchemaBuilder");
    connectSchemaJClass = codeModel.ref("org.apache.kafka.connect.data.Schema");
    connectableJClass = codeModel.ref("com.github.jcustenborder.kafka.connect.xml.Connectable");
    timezoneJClass = codeModel.ref(TimeZone.class);
    typeBigInteger = codeModel.ref(BigInteger.class);

    Map typeLookup = new HashMap<>();
    typeLookup.put(JPrimitiveType.parse(codeModel, boolean.class.getName()), connectSchemaBuilderJClass.staticInvoke("bool"));
    typeLookup.put(codeModel.ref(Boolean.class), connectSchemaBuilderJClass.staticInvoke("bool"));
    typeLookup.put(JPrimitiveType.parse(codeModel, float.class.getName()), connectSchemaBuilderJClass.staticInvoke("float32"));
    typeLookup.put(codeModel.ref(Float.class), connectSchemaBuilderJClass.staticInvoke("float32"));
    typeLookup.put(JPrimitiveType.parse(codeModel, double.class.getName()), connectSchemaBuilderJClass.staticInvoke("float64"));
    typeLookup.put(codeModel.ref(Double.class), connectSchemaBuilderJClass.staticInvoke("float64"));
    typeLookup.put(JPrimitiveType.parse(codeModel, byte.class.getName()), connectSchemaBuilderJClass.staticInvoke("int8"));
    typeLookup.put(codeModel.ref(Byte.class), connectSchemaBuilderJClass.staticInvoke("int8"));
    typeLookup.put(JPrimitiveType.parse(codeModel, short.class.getName()), connectSchemaBuilderJClass.staticInvoke("int16"));
    typeLookup.put(codeModel.ref(Short.class), connectSchemaBuilderJClass.staticInvoke("int16"));
    typeLookup.put(JPrimitiveType.parse(codeModel, int.class.getName()), connectSchemaBuilderJClass.staticInvoke("int32"));
    typeLookup.put(codeModel.ref(Integer.class), connectSchemaBuilderJClass.staticInvoke("int32"));
    typeLookup.put(JPrimitiveType.parse(codeModel, long.class.getName()), connectSchemaBuilderJClass.staticInvoke("int64"));
    typeLookup.put(codeModel.ref(Long.class), connectSchemaBuilderJClass.staticInvoke("int64"));

    typeLookup.put(codeModel.ref(byte[].class), connectSchemaBuilderJClass.staticInvoke("bytes"));
    //TODO: This needs to be configurable some how.
    typeLookup.put(codeModel.ref(BigDecimal.class), connectDecimalJClass.staticInvoke("builder").arg(JExpr.lit(12)));

    typeLookup.put(codeModel.ref(String.class), connectSchemaBuilderJClass.staticInvoke("string"));
    this.typeLookup = typeLookup;

    typeXMLGregorianCalendar = codeModel.ref(XMLGregorianCalendar.class);
  }

  JMethod findMethod(JCodeModel codeModel, ClassOutline classOutline, Field field) {
    final String methodName;

    if (field.fieldVar.type().equals(codeModel.ref(Boolean.class)) || field.fieldVar.type().equals(JPrimitiveType.parse(codeModel, boolean.class.getName()))) {
      methodName = "is" + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, field.fieldVar.name());
    } else {
      methodName = "get" + CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, field.fieldVar.name());
    }
    JMethod result = classOutline.implClass.getMethod(methodName, new JType[0]);

    if (null == result) {
      for (JMethod method : classOutline.implClass.methods()) {
        if (methodName.equalsIgnoreCase(method.name())) {
          result = method;
          break;
        }
      }
    }

    return result;
  }

  static final String STRUCT_METHOD_NAME = "toConnectStruct";

  void processToStruct(JFieldVar schemaField, JCodeModel codeModel, ClassOutline classOutline, List fields) {

    final JMethod method = classOutline.implClass.method(JMod.PUBLIC, connectStructJClass, STRUCT_METHOD_NAME);
    method.annotate(Override.class);
    final JBlock methodBody = method.body();
    final JVar structVar = methodBody.decl(connectStructJClass, "struct", JExpr._new(connectStructJClass).arg(schemaField));

    for (Field field : fields) {
      final JMethod getterMethod = findMethod(codeModel, classOutline, field);

      if (null == getterMethod) {
        Preconditions.checkNotNull(getterMethod,
            "Could not find getter method for %s.%s",
            classOutline.implClass.fullName(),
            field.fieldVar.name()
        );
      }
      JInvocation invokeGetter = JExpr._this().invoke(getterMethod);


      if (Type.ARRAY == field.type) {
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        final JVar structs = nullCheck._then().decl(connectListOfStructJClass, "structs", JExpr._new(typeArrayList));
        final JFieldRef oField = JExpr.ref("o");

        JForEach forLoop = nullCheck._then().forEach(field.arrayType, "o", invokeGetter);
        forLoop.body().add(structs.invoke("add").arg(oField.invoke(STRUCT_METHOD_NAME)));

        nullCheck._then().add(
            structVar.invoke("put")
                .arg(field.name)
                .arg(structs)
        );

      } else if (Type.STRUCT == field.type) {
        final JInvocation invokeStruct = invokeGetter.invoke(STRUCT_METHOD_NAME);
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck.
            _then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeStruct));
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));

      } else if (Type.XML_INTEGER == field.type) {
        final JInvocation invokeStruct = invokeGetter.invoke("longValue");
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck.
            _then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeStruct));
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));
      } else if (Type.XML_CALENDER == field.type) {
        final JInvocation invokeGetTime = invokeGetter.invoke("toGregorianCalendar")
            .arg(codeModel.ref(TimeZone.class).staticInvoke("getTimeZone").arg("UTC"))
            .arg(JExpr._null())
            .arg(JExpr._null())
            .invoke("getTime");
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck._then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeGetTime)
            );
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));
      } else if (Type.VALUE == field.type) {
        methodBody.add(
            structVar.invoke("put")
                .arg(field.name)
                .arg(invokeGetter)
        );
      } else if (Type.XML_GDAY == field.type) {
        final JInvocation invokeGetTime = invokeGetter.invoke("getDay");
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck._then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeGetTime)
            );
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));
      } else if (Type.XML_GMONTH == field.type) {
        final JInvocation invokeGetTime = invokeGetter.invoke("getMonth");
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck._then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeGetTime)
            );
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));
      } else if (Type.XML_GYEAR == field.type) {
        final JInvocation invokeGetTime = invokeGetter.invoke("getYear");
        final JConditional nullCheck = methodBody._if(JExpr._null().ne(invokeGetter));
        nullCheck._then()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(invokeGetTime)
            );
        nullCheck._else()
            .add(structVar.invoke("put")
                .arg(field.name)
                .arg(JExpr._null()));
      } else if (Type.XML_ENUM == field.type) {
        final JInvocation invokeValue = invokeGetter.invoke("value");
        methodBody.add(
            structVar.invoke("put")
                .arg(field.name)
                .arg(invokeValue)
        );
      }
    }

    methodBody._return(structVar);
  }

  void processFromStruct(JCodeModel codeModel, ClassOutline classOutline) {

  }

  enum Type {
    VALUE,
    ARRAY,
    STRUCT,
    XML_ENUM,
    XML_CALENDER,
    XML_GDAY,
    XML_GMONTH,
    XML_GMONTHDAY,
    XML_GYEAR,
    XML_GYEARMONTH,
    XML_INTEGER;
  }

  static class Field {
    public String name;
    public JFieldVar fieldVar;
    public boolean required;
    public Type type;
    public JExpression schemaBuilder;
    public JType arrayType;

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .omitNullValues()
          .add("name", this.name)
          .add("required", this.required)
          .add("type", this.type)
          .toString();
    }
  }

  static final Class CLASS_JNARROWED;
  static final Class CLASS_JREFERENCEDCLASS;

  static {
    try {
      CLASS_JNARROWED = Class.forName("com.sun.codemodel.JNarrowedClass");
      CLASS_JREFERENCEDCLASS = Class.forName("com.sun.codemodel.JCodeModel$JReferencedClass");
    } catch (ClassNotFoundException e) {
      throw new IllegalStateException(e);
    }
  }


  List fields(JCodeModel codeModel, ClassOutline classOutline) {
    List result = new ArrayList<>();
    final Map fields = classOutline.implClass.fields();

    for (final Map.Entry kvp : fields.entrySet()) {
      final String fieldName = kvp.getKey();
      final JFieldVar jFieldVar = kvp.getValue();
      log.trace("processSchema() - processing name = '{}' type = '{}'", fieldName, jFieldVar.type().name());

      final Field field = new Field();
      result.add(field);
      field.fieldVar = jFieldVar;

      final Map xmlElementValues = AnnotationUtils.xmlElement(codeModel, jFieldVar);
      final Map xmlAttributeValues = AnnotationUtils.xmlAttribute(codeModel, jFieldVar);
      final Map xmlSchemaTypeValues = AnnotationUtils.xmlSchemaType(codeModel, jFieldVar);
      final Map xmlMixed = AnnotationUtils.annotationAttributes(codeModel, jFieldVar, XmlMixed.class);

      if (null != xmlMixed) {
        throw new UnsupportedOperationException(
            String.format("%s.%s is marked with XmlMixed.", classOutline.implClass.fullName(), jFieldVar.name())
        );
      }


      if (null != xmlElementValues && !xmlElementValues.isEmpty()) {
        field.required = (boolean) xmlElementValues.getOrDefault("required", false);
        field.name = (String) xmlElementValues.getOrDefault("name", fieldName);
      } else if (null != xmlAttributeValues && !xmlAttributeValues.isEmpty()) {
        field.required = (boolean) xmlAttributeValues.getOrDefault("required", false);
        field.name = (String) xmlAttributeValues.getOrDefault("name", fieldName);
      } else {
        field.required = false;
        field.name = fieldName;
      }

      Preconditions.checkNotNull(field.name, "fieldName cannot be null. %s", classOutline.implClass.fullName());

      if (null != xmlSchemaTypeValues && !xmlSchemaTypeValues.isEmpty()) {
        final String name = (String) xmlSchemaTypeValues.get("name");
        switch (name) {
          case "date":
            field.schemaBuilder = connectDateJClass.staticInvoke("builder");
            field.type = Type.XML_CALENDER;
            break;
          case "time":
            field.schemaBuilder = connectTimeJClass.staticInvoke("builder");
            field.type = Type.XML_CALENDER;
            break;
          case "dateTime":
            field.schemaBuilder = connectTimestampJClass.staticInvoke("builder");
            field.type = Type.XML_CALENDER;
            break;
          case "unsignedShort":
          case "int":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.VALUE;
            break;
          case "nonNegativeInteger":
          case "nonPositiveInteger":
          case "negativeInteger":
          case "unsignedLong":
          case "positiveInteger":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int64");
            field.type = Type.XML_INTEGER;
            break;

          case "unsignedInt":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int64");
            field.type = Type.VALUE;
            break;
          case "ENTITIES":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("array")
                .arg(connectSchemaJClass.staticRef("STRING_SCHEMA"));
            field.type = Type.VALUE;
            break;
          case "anySimpleType":
          case "normalizedString":
          case "anyURI":
          case "ENTITY":
          case "Name":
          case "NCName":
          case "token":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("string");
            field.type = Type.VALUE;
            break;
          case "gDay":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.XML_GDAY;
            break;
          case "gMonth":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.XML_GMONTH;
            break;
          case "gMonthDay":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.XML_GMONTHDAY;
            break;
          case "gYear":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.XML_GYEAR;
            break;
          case "gYearMonth":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int32");
            field.type = Type.XML_GYEARMONTH;
            break;
          case "base64Binary":
          case "hexBinary":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("bytes");
            field.type = Type.VALUE;
            break;
          case "unsignedByte":
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int16");
            field.type = Type.VALUE;
            break;
          default:
            throw new IllegalStateException(
                String.format("Unknown type %s", name)
            );
        }
      } else {

        if (typeLookup.containsKey(jFieldVar.type())) {
          field.schemaBuilder = typeLookup.get(jFieldVar.type());
          field.type = Type.VALUE;
        } else if (jFieldVar.type() instanceof JDefinedClass) {
          JDefinedClass jDefinedClass = (JDefinedClass) jFieldVar.type();

          if (null != AnnotationUtils.annotationAttributes(codeModel, jFieldVar, XmlEnum.class)) {
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("string");
            field.type = Type.XML_ENUM;
          } else {
            field.schemaBuilder = jDefinedClass.staticRef(CONNECT_SCHEMA_FIELD);
            field.type = Type.STRUCT;
          }

        } else if (CLASS_JNARROWED.equals(jFieldVar.type().getClass())) {
          final JClass jClass = (JClass) jFieldVar.type();
          final JClass basis;
          final List args;
          try {
            Class jnarrowedCls = Class.forName("com.sun.codemodel.JNarrowedClass");
            java.lang.reflect.Field basisField = jnarrowedCls.getDeclaredField("basis");
            basisField.setAccessible(true);
            java.lang.reflect.Field argsField = jnarrowedCls.getDeclaredField("args");
            argsField.setAccessible(true);
            basis = (JClass) basisField.get(jClass);
            args = (List) argsField.get(jClass);
          } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalStateException(e);
          }

          if (typeList.equals(basis)) {
            JClass listType = args.get(0);
            field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("array")
                .arg(listType.staticRef(CONNECT_SCHEMA_FIELD));
            field.type = Type.ARRAY;
            field.arrayType = listType;
          } else {
            throw new IllegalStateException(
                String.format("%s is not supported.", basis.fullName())
            );
          }
        } else if (typeBigInteger.equals(jFieldVar.type())) {
          field.schemaBuilder = connectSchemaBuilderJClass.staticInvoke("int64");
          field.type = Type.XML_INTEGER;
        } else if (CLASS_JREFERENCEDCLASS.equals(jFieldVar.type().getClass())) {
          log.warn("Nothing for {}", jFieldVar.type().fullName());
        } else {
//          continue;
          throw new UnsupportedOperationException(
              String.format(
                  "Field %s - %s is not supported.",
                  kvp.getKey(),
                  jFieldVar.type().getClass().getName()
              )
          );
        }
      }

      Preconditions.checkNotNull(field.schemaBuilder,
          "%s.%s: %s was not handled",
          classOutline.implClass.fullName(),
          jFieldVar.name(),
          jFieldVar.type().fullName()
      );
    }

    return result;
  }


  @Override
  public boolean run(Outline model, Options options, ErrorHandler errorHandler) throws SAXException {
    try {
      JCodeModel codeModel = model.getCodeModel();
      setupImportedClasses(codeModel);
      for (ClassOutline classOutline : model.getClasses()) {
        classOutline.implClass._implements(this.connectableJClass);
        log.trace("run - {}", classOutline.implClass.name());

        List fields = fields(codeModel, classOutline);
        log.trace("Found {} field(s). {}", fields.size(), fields);

        JFieldVar schemaField = processSchema(codeModel, classOutline, fields);
        processToStruct(schemaField, codeModel, classOutline, fields);
//      processFromStruct(codeModel, classOutline);
      }
      return true;
    } catch (Exception e) {
      errorHandler.error(new SAXParseException("Exception thrown while processing: " + e.getMessage(), null, e));
      return false;
    }
  }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy