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

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

The newest version!
/**
 * 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.Strings;
import com.google.common.collect.ImmutableMap;
import com.sun.codemodel.ClassType;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JInvocation;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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.datatype.Duration;
import javax.xml.namespace.QName;
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 static com.sun.codemodel.JType.parse;

@SuppressFBWarnings
public class KafkaConnectPlugin extends AbstractParameterizablePlugin {
  static final String TO_CONNECT_STRUCT = "toStruct";
  static final String FROM_CONNECT_STRUCT = "fromStruct";
  static final Class CLASS_JNARROWED;
  static final Class CLASS_JREFERENCEDCLASS;
  private static final Logger log = LoggerFactory.getLogger(KafkaConnectPlugin.class);
  private static final String CONNECT_SCHEMA_FIELD = "CONNECT_SCHEMA";

  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);
    }
  }

  Types types;
  Map jTypeLookup;
  Map xmlTypeLookup;
  Map definedTypeStateLookup = new HashMap<>();

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

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

  JFieldVar processSchema(JCodeModel codeModel, ClassOutline classOutline, List fieldStates) {
    final JFieldVar schemaVariable = classOutline.implClass.field(JMod.PUBLIC | JMod.STATIC | JMod.FINAL, this.types.schema(), CONNECT_SCHEMA_FIELD);

    final JBlock constructorBlock = classOutline.implClass.init();
    final JVar builderVar = constructorBlock.decl(this.types.schemaBuilder(), "builder", this.types.schemaBuilder().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(this.types.schemaBuilder(), "fieldBuilder");

    for (FieldState fieldState : fieldStates) {
      if (fieldState.schemaBuilder() instanceof JInvocation) {
        constructorBlock.assign(fieldBuilderVar, fieldState.schemaBuilder());
        if (!fieldState.required()) {
          constructorBlock.invoke(fieldBuilderVar, "optional");
        }

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


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

  void add(Map result, JExpression schemaBuilder, JExpression schema, String readMethod, String writeMethod, JCodeModel codeModel, Class... classes) {
    ImmutableStaticTypeState.Builder builder = ImmutableStaticTypeState.builder();

    for (Class cls : classes) {
      final JType type;
      if (cls.isPrimitive()) {
        type = parse(codeModel, cls.getName());
      } else {
        type = codeModel.ref(cls);
      }
      builder.addTypes(type);
    }
    builder.readMethod(readMethod);
    builder.writeMethod(writeMethod);
    builder.schema(schema);
    builder.schemaBuilder(schemaBuilder);
    StaticTypeState typeState = builder.build();
    for (JType type : typeState.types()) {
      result.put(type, typeState);
    }
  }

  Map buildTypeLookup(JCodeModel codeModel) {
    Map result = new HashMap<>();

    add(result, this.types.schemaBuilder().staticInvoke("bool"), this.types.schema().staticRef("BOOLEAN_SCHEMA"), "toBoolean", "fromBoolean", codeModel, boolean.class, Boolean.class);
    add(result, this.types.schemaBuilder().staticInvoke("float32"), this.types.schema().staticRef("FLOAT32_SCHEMA"), "toFloat32", "fromFloat32", codeModel, float.class, Float.class);
    add(result, this.types.schemaBuilder().staticInvoke("float64"), this.types.schema().staticRef("FLOAT64_SCHEMA"), "toFloat64", "fromFloat64", codeModel, double.class, Double.class);
    add(result, this.types.schemaBuilder().staticInvoke("int8"), this.types.schema().staticRef("INT8_SCHEMA"), "toInt8", "fromInt8", codeModel, byte.class, Byte.class);
    add(result, this.types.schemaBuilder().staticInvoke("int16"), this.types.schema().staticRef("INT16_SCHEMA"), "toInt16", "fromInt16", codeModel, short.class, Short.class);
    add(result, this.types.schemaBuilder().staticInvoke("int32"), this.types.schema().staticRef("INT32_SCHEMA"), "toInt32", "fromInt32", codeModel, int.class, Integer.class);
    add(result, this.types.schemaBuilder().staticInvoke("int64"), this.types.schema().staticRef("INT64_SCHEMA"), "toInt64", "fromInt64", codeModel, long.class, Long.class);
    add(result, this.types.schemaBuilder().staticInvoke("int64"), this.types.schema().staticRef("INT64_SCHEMA"), "toInt64", "fromInt64BigInteger", codeModel, BigInteger.class);
    add(result, this.types.schemaBuilder().staticInvoke("bytes"), this.types.schema().staticRef("BYTES_SCHEMA"), "toBytes", "fromBytes", codeModel, byte[].class);
    add(result, this.types.schemaBuilder().staticInvoke("string"), this.types.schema().staticRef("STRING_SCHEMA"), "toString", "fromString", codeModel, String.class);
    add(result, this.types.decimal().staticInvoke("builder").arg(JExpr.lit(12)), null, "toDecimal", "fromDecimal", codeModel, BigDecimal.class);

    add(result, this.types.connectableHelper().staticInvoke("qnameBuilder"), null, "toQname", "fromQname", codeModel, QName.class);
    add(result, this.types.schemaBuilder().staticInvoke("int64"), this.types.schema().staticRef("IN64_SCHEMA"), "toDuration", "fromDuration", codeModel, Duration.class);

    return ImmutableMap.copyOf(result);
  }

  void add(Map result, JExpression schemaBuilder, JExpression schema, String readMethod, String writeMethod, String... xmlTypes) {
    com.github.jcustenborder.kafka.connect.xml.ImmutableXmlTypeState.Builder builder = com.github.jcustenborder.kafka.connect.xml.ImmutableXmlTypeState.builder();
    builder.addXmlTypes(xmlTypes);
    builder.schema(schema);
    builder.readMethod(readMethod);
    builder.writeMethod(writeMethod);
    builder.schemaBuilder(schemaBuilder);
    XmlTypeState typeState = builder.build();
    for (String type : typeState.xmlTypes()) {
      result.put(type, typeState);
    }
  }

  Map buildXmlTypeLookup() {
    Map result = new HashMap<>();
    add(result, this.types.date().staticInvoke("builder"), this.types.date().staticRef("SCHEMA"), "toDate", "fromDate", "date");
    add(result, this.types.time().staticInvoke("builder"), this.types.time().staticRef("SCHEMA"), "toTime", "fromTime", "time");
    add(result, this.types.timestamp().staticInvoke("builder"), this.types.timestamp().staticRef("BOOLEAN_SCHEMA"), "toDateTime", "fromDateTime", "dateTime");
    add(result, this.types.schemaBuilder().staticInvoke("int32"), this.types.schema().staticRef("INT32_SCHEMA"), "toInt32", "fromInt32", "unsignedShort", "int");
    add(result, this.types.schemaBuilder().staticInvoke("int64"), this.types.schema().staticRef("INT64_SCHEMA"), "toInt64", "fromInt64BigInteger",
        "negativeInteger", "nonNegativeInteger", "nonPositiveInteger", "negativeInteger",
        "unsignedLong", "positiveInteger"
    );
    add(result, this.types.schemaBuilder().staticInvoke("int64"), this.types.schema().staticRef("INT64_SCHEMA"), "toInt64", "fromInt64",
        "unsignedInt"
    );
    add(result, this.types.schemaBuilder().staticInvoke("string"), this.types.schema().staticRef("STRING_SCHEMA"), "toString", "fromString",
        "anySimpleType", "normalizedString", "anyURI", "ENTITY", "Name", "NCName", "token",
        "ID", "IDREF", "language", "NMTOKEN"
    );
    add(result, this.types.schemaBuilder().staticInvoke("int32"), this.types.schema().staticRef("INT32_SCHEMA"), "toXmlgDay", "fromXmlgDay", "gDay");
    add(result, this.types.schemaBuilder().staticInvoke("int32"), this.types.schema().staticRef("INT32_SCHEMA"), "toXmlgMonth", "fromXmlgMonth", "gMonth");
    add(result, this.types.date().staticInvoke("builder"), this.types.schema().staticRef("INT32_SCHEMA"), "toXmlgMonthDay", "fromXmlgMonthDay", "gMonthDay");
    add(result, this.types.date().staticInvoke("builder"), this.types.schema().staticRef("INT32_SCHEMA"), "toXmlgYearMonth", "fromXmlgYearMonth", "gYearMonth");
    add(result, this.types.schemaBuilder().staticInvoke("int32"), this.types.schema().staticRef("INT32_SCHEMA"), "toXmlgYear", "fromXmlgYear", "gYear");
    add(result, this.types.schemaBuilder().staticInvoke("bytes"), this.types.schema().staticRef("BYTES_SCHEMA"), "toBytes", "fromBytes", "base64Binary", "hexBinary");
    add(result, this.types.schemaBuilder().staticInvoke("int16"), this.types.schema().staticRef("INT16_SCHEMA"), "toInt16", "fromInt16", "unsignedByte");
    add(result, this.types.schemaBuilder().staticInvoke("array").arg(this.types.schema().staticRef("STRING")), null, "toArray", "fromArray", "IDREFS");


    return ImmutableMap.copyOf(result);
  }

  void setupImportedClasses(JCodeModel codeModel) {
    this.types = Types.build(codeModel);
    this.jTypeLookup = buildTypeLookup(codeModel);
    this.xmlTypeLookup = buildXmlTypeLookup();
  }

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

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

    for (FieldState fieldState : fieldStates) {

      JInvocation callAddMethod = this.types.connectableHelper().staticInvoke(fieldState.readMethod())
          .arg(structVar)
          .arg(JExpr.lit(fieldState.name()))
          .arg(JExpr._this().ref(fieldState.fieldVar()));

      for (JExpression arg : fieldState.readMethodArgs()) {
        callAddMethod.arg(arg);
      }

      methodBody.add(callAddMethod);
    }

    methodBody._return(structVar);
  }

  void processFromStruct(JCodeModel codeModel, ClassOutline classOutline, List fieldStates) {
    final JMethod method = classOutline.implClass.method(JMod.PUBLIC, void.class, FROM_CONNECT_STRUCT);
    final JVar structVar = method.param(this.types.struct(), "struct");
    method.annotate(Override.class);
    final JBlock methodBody = method.body();

    for (FieldState fieldState : fieldStates) {
      JInvocation callAddMethod = this.types.connectableHelper().staticInvoke(fieldState.writeMethod())
          .arg(structVar)
          .arg(JExpr.lit(fieldState.name()));

      for (JExpression arg : fieldState.writeMethodArgs()) {
        callAddMethod.arg(arg);
      }

      methodBody.assign(
          JExpr._this().ref(fieldState.fieldVar()),
          callAddMethod
      );
    }
  }


  @SuppressFBWarnings
  State type(JCodeModel codeModel, ClassOutline classOutline, JFieldVar field, JType type) {
    log.trace("type() - type = '{}'", type);
    if (this.types.blackListTypes().contains(type)) {
      throw new TypeTooGenericException(classOutline, field, null, type);
    }

    State result = this.jTypeLookup.get(type);

    if (null != result) {
      return result;
    }

    log.trace("type() - Generating state for {}", type);

    if (type instanceof JDefinedClass) {
      DefinedTypeState dtResult = this.definedTypeStateLookup.get(type);
      if (null != dtResult) {
        return dtResult;
      }
      JDefinedClass jDefinedClass = (JDefinedClass) type;
      ImmutableDefinedTypeState.Builder builder = ImmutableDefinedTypeState.builder();
      if (targetsEnum(jDefinedClass)) {
        builder.schemaBuilder(this.types.schemaBuilder().staticInvoke("string"));
        builder.readMethod("fromEnum");
        builder.writeMethod("toEnum");
        builder.addWriteMethodArgs(jDefinedClass.dotclass());
      } else {
        builder.schemaBuilder(jDefinedClass.staticRef(CONNECT_SCHEMA_FIELD));
        builder.readMethod("toStruct");
        builder.writeMethod("fromStruct");
        builder.addWriteMethodArgs(jDefinedClass.dotclass());
        builder.schema(jDefinedClass.staticRef(CONNECT_SCHEMA_FIELD));
      }
      builder.type(type);
      dtResult = builder.build();
      this.definedTypeStateLookup.put(type, dtResult);
      return dtResult;
    }

    if (CLASS_JNARROWED.equals(type.getClass())) {
      DefinedTypeState dtResult = this.definedTypeStateLookup.get(type);
      if (null != dtResult) {
        return dtResult;
      }
      log.trace("type() - type is CLASS_JNARROWED. type = '{}'", type);

      final JClass jClass = (JClass) field.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);
      }

      ImmutableDefinedTypeState.Builder builder = ImmutableDefinedTypeState.builder()
          .type(type);

      if (this.types.qNameMap().equals(jClass)) {
        // This case pops up when anyattribute is in use.
        JInvocation schemaBuilder = this.types.schemaBuilder().staticInvoke("map")
            .arg(this.types.connectableHelper().staticRef("QNAME_SCHEMA"))
            .arg(this.types.schema().staticRef("OPTIONAL_STRING_SCHEMA"));
        builder.schemaBuilder(schemaBuilder);
        builder.readMethod("toQNameMap");
        builder.writeMethod("fromQNameMap");
        dtResult = builder.build();
        this.definedTypeStateLookup.put(type, dtResult);
      } else if (this.types.list().equals(basis)) {
        JClass valueType = args.get(0);
        State valueState = type(codeModel, classOutline, field, valueType);

        JExpression valueSchema = valueState.schema();
        if (valueSchema == null) {
          valueSchema = valueState.schemaBuilder().invoke("build");
        }
        JInvocation schemaBuilder = this.types.schemaBuilder().staticInvoke("array")
            .arg(valueSchema);
        builder.schemaBuilder(schemaBuilder);
        builder.readMethod("toArray");
        builder.writeMethod("fromArray");
        builder.addWriteMethodArgs(valueType.dotclass());


        dtResult = builder.build();
        this.definedTypeStateLookup.put(type, dtResult);
      } else if (this.types.map().equals(basis)) {
        JClass keyType = args.get(0);
        State keyState = type(codeModel, classOutline, field, keyType);
        JExpression keySchema = keyState.schema();
        if (keySchema == null) {
          keySchema = keyState.schemaBuilder().invoke("build");
        }
        JClass valueType = args.get(1);
        State valueState = type(codeModel, classOutline, field, valueType);
        JExpression valueSchema = valueState.schema();
        if (valueSchema == null) {
          valueSchema = valueState.schemaBuilder().invoke("build");
        }
        JInvocation schemaBuilder = this.types.schemaBuilder().staticInvoke("map")
            .arg(keySchema)
            .arg(valueSchema);
        builder.schemaBuilder(schemaBuilder);
        builder.readMethod("toMap");
        builder.writeMethod("fromMap");
        builder.addWriteMethodArgs(keyType.dotclass());
        builder.addWriteMethodArgs(valueType.dotclass());
        dtResult = builder.build();
        this.definedTypeStateLookup.put(type, dtResult);
      } else {
        throw new UnsupportedTypeException(
            classOutline,
            field,
            field.type(),
            basis
        );
      }
      return dtResult;
    }


    throw new UnsupportedTypeException(
        classOutline,
        field,
        null,
        field.type()
    );
  }


  FieldState field(JCodeModel codeModel, ClassOutline classOutline, final String fieldName, final JFieldVar jFieldVar) {
    try {
      log.trace("field() - processing name = '{}' type = '{}'", fieldName, jFieldVar.type().name());
      final com.github.jcustenborder.kafka.connect.xml.ImmutableFieldState.Builder fieldState = com.github.jcustenborder.kafka.connect.xml.ImmutableFieldState.builder();
      final String name = AnnotationUtils.name(codeModel, jFieldVar, fieldName);
      final boolean required = AnnotationUtils.required(codeModel, jFieldVar);
      final String xmlType = AnnotationUtils.xmlType(codeModel, jFieldVar);

      fieldState.required(required);
      fieldState.name(name);
      fieldState.fieldVar(jFieldVar);

      if (!Strings.isNullOrEmpty(xmlType) && !targetsEnum(jFieldVar)) {
        log.trace("field() - xmlType = '{}'", xmlType);
        XmlTypeState xmlTypeState = this.xmlTypeLookup.get(xmlType);
        if (null == xmlTypeState) {
          throw new UnsupportedOperationException(
              String.format("%s is not a supported xml type.", xmlType)
          );
        }
        fieldState.from(xmlTypeState);
      } else {
        State jTypeState = type(codeModel, classOutline, jFieldVar, jFieldVar.type());

        if (null != jTypeState) {
          fieldState.from(jTypeState);
        } else if (CLASS_JREFERENCEDCLASS.equals(jFieldVar.type().getClass())) {
          log.warn("Nothing for {}", jFieldVar.type().fullName());
        } else {
          throw new UnsupportedTypeException(
              classOutline,
              jFieldVar,
              null,
              jFieldVar.type()
          );
        }
      }
      return fieldState.build();
    } catch (Exception ex) {
      throw new IllegalStateException(

          String.format(
              "Exception thrown while building field '%s'. %s",
              fieldName,
              classOutline.implClass.name()
          ),
          ex);
    }
  }

  private boolean targetsEnum(JDefinedClass cls) {
    return ClassType.ENUM == cls.getClassType();
  }

  private boolean targetsEnum(JFieldVar field) {
    if (field.type() instanceof JDefinedClass) {
      return targetsEnum((JDefinedClass) field.type());
    }

    return false;
  }


  void fields(JCodeModel codeModel, ClassOutline classOutline, List fieldStates) {
    final Map fields = classOutline.implClass.fields();

    for (final Map.Entry kvp : fields.entrySet()) {
      final String fieldName = kvp.getKey();
      if (CONNECT_SCHEMA_FIELD.equals(fieldName)) {
        continue;
      }

      final JFieldVar jFieldVar = kvp.getValue();
      final FieldState fieldState = field(codeModel, classOutline, fieldName, jFieldVar);
      fieldStates.add(fieldState);
    }

    if (classOutline.getSuperClass() != null) {
      fields(codeModel, classOutline.getSuperClass(), fieldStates);
    }
  }

  List fields(JCodeModel codeModel, ClassOutline classOutline) {
    List result = new ArrayList<>();
    fields(codeModel, classOutline, result);
    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.types.connectable());
        log.trace("run - {}", classOutline.implClass.name());

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

        JFieldVar schemaField = processSchema(codeModel, classOutline, fieldStates);
        processToStruct(schemaField, codeModel, classOutline, fieldStates);
        processFromStruct(codeModel, classOutline, fieldStates);
      }
      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