diff --git a/.github/workflows/develocity.yml b/.github/workflows/develocity.yml deleted file mode 100644 index 141957f6db5..00000000000 --- a/.github/workflows/develocity.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Develocity testbed - -on: - push: - workflow_dispatch: - -env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - -jobs: - develocity: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Java - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: 'zulu' - server-id: central - - - name: Quick One - run: ./mvnw -N install diff --git a/.mvn/develocity.xml b/.mvn/develocity.xml index 385a8ab0abe..4db52b4dfe8 100644 --- a/.mvn/develocity.xml +++ b/.mvn/develocity.xml @@ -26,7 +26,7 @@ #{isFalse(env['CI'])} - + #{{'0.0.0.0'}} diff --git a/build-plugins/pom.xml b/build-plugins/pom.xml index 94d5ca1251d..1f906735d4f 100644 --- a/build-plugins/pom.xml +++ b/build-plugins/pom.xml @@ -18,6 +18,11 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + org.apache.maven.plugins maven-compiler-plugin @@ -126,6 +131,11 @@ com.google.devtools.ksp symbol-processing-aa-embeddable + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + com.squareup kotlinpoet-jvm diff --git a/build-plugins/src/main/java/util/AsmBuilders.java b/build-plugins/src/main/java/util/AsmBuilders.java index 60748cf0eb0..140e64bc1e5 100644 --- a/build-plugins/src/main/java/util/AsmBuilders.java +++ b/build-plugins/src/main/java/util/AsmBuilders.java @@ -54,7 +54,7 @@ public class AsmBuilders extends AbstractMojo { @SuppressWarnings("ConstantConditions") public void execute() throws MojoExecutionException { List files = new ArrayList<>(); - generated = new File(project.getBasedir() + "/target/generated-sources/morphia-annotations-asm/"); + generated = new File(project.getBasedir() + "/target/generated-sources/morphia-annotations/"); String path = core() + "/src/main/java/dev/morphia/annotations"; files.addAll(find(path)); diff --git a/build-plugins/src/main/java/util/KotlinAnnotationExtensions.java b/build-plugins/src/main/java/util/KotlinAnnotationExtensions.java index ffc1f9f06dc..95d7b8c4b96 100644 --- a/build-plugins/src/main/java/util/KotlinAnnotationExtensions.java +++ b/build-plugins/src/main/java/util/KotlinAnnotationExtensions.java @@ -67,7 +67,7 @@ public class KotlinAnnotationExtensions extends AbstractMojo { @SuppressWarnings("ConstantConditions") public void execute() throws MojoExecutionException { List files = new ArrayList<>(); - generated = new File(project.getBasedir() + "/target/generated-sources/morphia-annotations-kotlin/"); + generated = new File(project.getBasedir() + "/target/generated-sources/morphia-annotations/"); String path = core() + "/src/main/java/dev/morphia/annotations"; files.addAll(find(path)); diff --git a/build-plugins/src/main/kotlin/util/AnnotationNodeExtensions.kt b/build-plugins/src/main/kotlin/util/AnnotationNodeExtensions.kt new file mode 100644 index 00000000000..fdbc45b8fd4 --- /dev/null +++ b/build-plugins/src/main/kotlin/util/AnnotationNodeExtensions.kt @@ -0,0 +1,376 @@ +package util + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ClassName.Companion.bestGuess +import com.squareup.kotlinpoet.DelicateKotlinPoetApi +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.TypeSpec.Companion.objectBuilder +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.asClassName +import java.io.File +import java.io.FileFilter +import java.io.FileWriter +import java.io.IOException +import java.text.MessageFormat +import java.util.Arrays +import java.util.TreeMap +import org.apache.maven.plugin.AbstractMojo +import org.apache.maven.plugin.MojoExecutionException +import org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES +import org.apache.maven.plugins.annotations.Mojo +import org.apache.maven.plugins.annotations.Parameter +import org.apache.maven.project.MavenProject +import org.jboss.forge.roaster.ParserException +import org.jboss.forge.roaster.Roaster +import org.jboss.forge.roaster.model.Type +import org.jboss.forge.roaster.model.source.Import +import org.jboss.forge.roaster.model.source.JavaAnnotationSource +import org.jboss.forge.roaster.model.source.JavaClassSource +import org.jboss.forge.roaster.model.source.JavaSource +import org.jboss.forge.roaster.model.util.Types +import org.objectweb.asm.Type as AsmType +import org.objectweb.asm.tree.AnnotationNode + +@OptIn(DelicateKotlinPoetApi::class) +@Mojo(name = "morphia-annotation-node", defaultPhase = GENERATE_SOURCES) +class AnnotationNodeExtensions : AbstractMojo() { + private val builders: MutableMap = TreeMap() + lateinit var fileBuilder: FileSpec.Builder + + @Parameter(defaultValue = "\${project}", required = true, readonly = true) + private val project: MavenProject? = null + lateinit var factory: TypeSpec.Builder + private var generated: File? = null + private val filter = FileFilter { pathname: File -> + (pathname.name.endsWith(".java") || pathname.name.endsWith(".kt")) && + !pathname.name.endsWith("Handler.java") && + !pathname.name.endsWith("Helper.java") && + pathname.name != "package-info.java" + } + + @Throws(MojoExecutionException::class) + override fun execute() { + val files: MutableList = ArrayList() + generated = + File(project!!.basedir.toString() + "/target/generated-sources/morphia-annotations/") + val path = core().toString() + "/src/main/java/dev/morphia/annotations" + files.addAll(find(path)) + project.addCompileSourceRoot(generated!!.absolutePath) + + try { + for (file in files) { + try { + val source = Roaster.parse(JavaAnnotationSource::class.java, file) + if (source.isPublic) { + builders[source.name] = source + } + } catch (e: ParserException) { + throw MojoExecutionException("Could not parse $file", e) + } + } + fileBuilder = + FileSpec.builder( + builders.values.iterator().next().getPackage() + ".internal", + "AnnotationNodeExtensions" + ) + fileBuilder.addAnnotation( + AnnotationSpec.builder(Suppress::class.java) + .addMember("%S", "UNCHECKED_CAST") + .useSiteTarget(FILE) + .build() + ) + + fileBuilder.addImport("dev.morphia.mapping", "MappingException") + fileBuilder.addImport("java.util", "Objects") + emitFactory() + } catch (e: Exception) { + throw MojoExecutionException(e.message, e) + } + } + + private fun core(): File { + var dir = project!!.basedir + while (!File(dir, ".git").exists()) { + dir = dir.parentFile + } + return File(dir, "core") + } + + @Throws(Exception::class) + private fun emitFactory() { + factory = createFactory() + genericConverter() + for (source in builders.values) { + annotationConverters(source) + // annotationExtractors(source) + // annotationCodeBuilders(source) + } + + fileBuilder.addType(factory.build()) + val fileSpec: FileSpec = fileBuilder.build() + val outputFile = File(generated, fileSpec.relativePath) + if (!outputFile.parentFile.mkdirs() && !outputFile.parentFile.exists()) { + throw IOException( + String.format("Could not create directory: %s", outputFile.parentFile) + ) + } + FileWriter(outputFile).use { out -> fileSpec.writeTo(out) } + } + + private fun genericConverter() { + val typeVariable = TypeVariableName("T", Annotation::class.asClassName()) + val method = + FunSpec.builder("toMorphiaAnnotation") + .addTypeVariable(typeVariable) + .receiver(AnnotationNode::class.asClassName()) + .returns(typeVariable) + + method.beginControlFlow("return when (desc)") + for (source in builders.values) { + val type = bestGuess(source.qualifiedName).toType() + method.addStatement(""""${type.descriptor}" -> to${source.name}()""") + } + method.addStatement( + """else -> throw %T("Unknown annotation type: ${"$"}{desc}")""", + IllegalArgumentException::class + ) + + method.endControlFlow() + method.addStatement("as T") + + factory.addFunction(method.build()) + } + + private fun annotationConverters(source: JavaAnnotationSource) { + val builderName = "${source.name}Builder" + fileBuilder.addImport( + fileBuilder.packageName, + "$builderName.${AnnotationBuilders.methodCase(builderName)}" + ) + val method = + FunSpec.builder("to" + source.name) + .receiver(AnnotationNode::class.asClassName()) + .returns(bestGuess(source.qualifiedName)) + var code = "" + if (source.annotationElements.isNotEmpty()) { + code += + """ + val map = (values?.windowed(2, 2) ?: emptyList()) + .map { it -> (it[0] ?: "value") to it[1] } + .toMap() + + """ + .trimIndent() + } + code += + """ + return ${AnnotationBuilders.methodCase(source.name)}Builder().apply { + + """ + .trimIndent() + + for (element in source.annotationElements) { + val name = element.name + var cast = processType(element.type) + /* + if (element.type.isArray) { + cast = "*(${cast})" + } + */ + code += ("map[\"${name}\"]?.let { ${name}(${cast}) }\n") + } + + code += "}\n.build()" + method.addCode(code) + + factory.addFunction(method.build()) + } + + private fun annotationCodeBuilders(source: JavaAnnotationSource) { + val method = + FunSpec.builder(AnnotationBuilders.methodCase(source.name) + "CodeGen") + .receiver(KSAnnotation::class.java) + .returns(bestGuess("kotlin.String")) + val className: ClassName = bestGuess(builderName(source)) + fileBuilder.addImport(className.packageName, className.simpleName) + method.addCode( + MessageFormat.format( + """ + val map = arguments + .map '{' it -> (it.name?.asString() ?: "value") to it.value } + .toMap() + var code = "{0}Builder.{1}Builder()" + + """ + .trimIndent(), + source.name, + AnnotationBuilders.methodCase(source.name) + ) + ) + + for (element in source.annotationElements) { + val name = element.name + var value: String? + + method.addCode( + """ + map["$name"]?.let { + + """ + .trimIndent() + ) + val type = element.type + if (type.qualifiedName.startsWith("dev.morphia.annotations.")) { + val typeName = type.simpleName + method.addCode( + """ + if (!Objects.equals(${source.name}Builder.defaults.${name}, (it as AnnotationNode).to${typeName}())) { + + """ + .trimIndent() + ) + value = "\${it.${AnnotationBuilders.methodCase(typeName)}CodeGen()}" + } else { + method.addCode( + """ + if (!Objects.equals(${source.name}Builder.defaults.${name}, it)) { + + """ + .trimIndent() + ) + + value = getValue(type) + } + + method.addCode( + """ + code += ".${name}(${value})" + } + } + + """ + .trimIndent() + ) + } + + method.addCode( + """ + code += ".build()" + + """ + .trimIndent() + ) + method.addCode("return code") + + factory.addFunction(method.build()) + } + + private fun getValue(type: Type): String { + val typeName = type.name + var code = "\$it" + + if (typeName == "String") { + code = """\"${code}\"""" + } else if (typeName == "Class") { + code += ".class" + } + return code + } + + private fun processType(type: Type): String { + val typeName = type.name + var code: String? = "NOT SET" + + if (typeName == "boolean") { + code = "it as Boolean" + } else if (typeName == "String") { + code = "it as String" + } else if (typeName == "int") { + code = "it as Int" + } else if (typeName == "long") { + code = "(it as Number).toLong()" + } else if (typeName == "Class") { + code = "it as Class<*>" + } else if (type.isArray) { + code = "${processArrayType(type)}" + } else if ( + type.qualifiedName.startsWith("com.mongodb.client.model.") || + type.qualifiedName.startsWith("dev.morphia.mapping.") + ) { + code = "it as ${type.qualifiedName}" + } else if (type.qualifiedName.startsWith("dev.morphia.annotations.")) { + code = "(it as AnnotationNode).to${type.simpleName}()" + } else { + System.out.printf( + "unknown type: %n\t%s %n\t%s %n\t%s %n", + typeName, + type.qualifiedName, + type.origin.isEnum + ) + code = "" + } + + return code + } + + private fun createFactory(): TypeSpec.Builder { + val extensionsFactory = objectBuilder("AnnotationNodeExtensions") + val classBuilder = + Roaster.create(JavaClassSource::class.java) + .setName("AnnotationNodeExtensions") + .setPackage(builders.values.iterator().next().getPackage() + ".internal") + .setFinal(true) + classBuilder.addAnnotation("dev.morphia.annotations.internal.MorphiaInternal") + + return extensionsFactory + } + + private fun find(path: String): List { + val files = File(path).listFiles(filter) + return if (files != null) Arrays.asList(*files) else listOf() + } + + companion object { + private fun builderName(builder: JavaSource<*>): String { + val pkg = builder.getPackage() + ".internal." + val name = builder.name + "Builder" + return pkg + name + } + + private fun processArrayType(type: Type): String { + var code: String = type.simpleName + val parameterized = type.isParameterized + val params = if (parameterized) type.typeArguments else emptyList() + if (!params.isEmpty()) { + val param = params[0] + var parameterName = Types.toSimpleName(param.qualifiedName) + if (param.isWildcard) { + parameterName = parameterName.substring(parameterName.lastIndexOf(' ') + 1) + } + if (!Types.isQualified(parameterName)) { + val target = parameterName + val imp = + type.origin.imports + .stream() + .filter { i: Import -> i.simpleName == target } + .findFirst() + .orElseThrow() + parameterName = imp.qualifiedName + } + if (param.isWildcard) { + parameterName += "<*>" + } + + code = "${type.simpleName}<${parameterName}>" + } + return "*(it as List<${code}>).toTypedArray()" + } + } +} + +private fun ClassName.toType() = AsmType.getType("L${canonicalName};".replace('.', '/')) diff --git a/critter/core/pom.xml b/critter/core/pom.xml index db37de84789..1c6fbaf9cf0 100644 --- a/critter/core/pom.xml +++ b/critter/core/pom.xml @@ -95,6 +95,12 @@ build-plugins ${project.version} + + morphia-annotations-asm + + morphia-annotation-node + + morphia-annotations-kotlin diff --git a/critter/core/src/main/kotlin/dev/morphia/critter/conventions/PropertyConvention.java b/critter/core/src/main/kotlin/dev/morphia/critter/conventions/PropertyConvention.java new file mode 100644 index 00000000000..81c3b0ef176 --- /dev/null +++ b/critter/core/src/main/kotlin/dev/morphia/critter/conventions/PropertyConvention.java @@ -0,0 +1,47 @@ +package dev.morphia.critter.conventions; + +import dev.morphia.annotations.Id; +import dev.morphia.annotations.Property; +import dev.morphia.annotations.Reference; +import dev.morphia.annotations.Version; +import dev.morphia.annotations.internal.MorphiaInternal; +import dev.morphia.config.MorphiaConfig; +import dev.morphia.mapping.Mapper; +import dev.morphia.mapping.codec.pojo.PropertyModel; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.tree.AnnotationNode; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; + +/** + * @hidden + * @morphia.internal + * @since 3.0 + */ +@MorphiaInternal +public class PropertyConvention { + @MorphiaInternal + public static String mappedName(MorphiaConfig config, Map annotations, String modelName) { + Property property = (Property) annotations.get(Property.class.getName()); + Reference reference = (Reference) annotations.get(Reference.class.getName()); + Version version = (Version) annotations.get(Version.class.getName()); + Id id = (Id) annotations.get(Id.class.getName()); + + String mappedName; + + if (id != null) { + mappedName = "_id"; + } else if (property != null && !property.value().equals(Mapper.IGNORED_FIELDNAME)) { + mappedName = property.value(); + } else if (reference != null && !reference.value().equals(Mapper.IGNORED_FIELDNAME)) { + mappedName = reference.value(); + } else if (version != null && !version.value().equals(Mapper.IGNORED_FIELDNAME)) { + mappedName = version.value(); + } else { + mappedName = config.propertyNaming().apply(modelName); + } + return mappedName; + } +} diff --git a/critter/core/src/main/kotlin/dev/morphia/critter/parser/PropertyFinder.kt b/critter/core/src/main/kotlin/dev/morphia/critter/parser/PropertyFinder.kt index 9fe0f4f3e1a..2616e9f4b44 100644 --- a/critter/core/src/main/kotlin/dev/morphia/critter/parser/PropertyFinder.kt +++ b/critter/core/src/main/kotlin/dev/morphia/critter/parser/PropertyFinder.kt @@ -2,6 +2,7 @@ package dev.morphia.critter.parser import dev.morphia.config.PropertyAnnotationProvider import dev.morphia.critter.parser.asm.AddFieldAccessorMethods +import dev.morphia.critter.parser.asm.Generators.config import dev.morphia.critter.parser.gizmo.GizmoPropertyAccessorGenerator import dev.morphia.critter.parser.gizmo.GizmoPropertyModelGenerator import dev.morphia.critter.parser.java.CritterClassLoader @@ -30,7 +31,7 @@ class PropertyFinder(mapper: Mapper, val classLoader: CritterClassLoader) { fields.forEach { field -> val accessorGenerator = GizmoPropertyAccessorGenerator(entityType, field) accessorGenerator.emit() - val propertyModelGenerator = GizmoPropertyModelGenerator(entityType, field) + val propertyModelGenerator = GizmoPropertyModelGenerator(config, entityType, field) propertyModelGenerator.emit() models += propertyModelGenerator.generatedType } diff --git a/critter/core/src/main/kotlin/dev/morphia/critter/parser/gizmo/GizmoPropertyModelGenerator.kt b/critter/core/src/main/kotlin/dev/morphia/critter/parser/gizmo/GizmoPropertyModelGenerator.kt index 674bf0ba86d..f2ebae3ecab 100644 --- a/critter/core/src/main/kotlin/dev/morphia/critter/parser/gizmo/GizmoPropertyModelGenerator.kt +++ b/critter/core/src/main/kotlin/dev/morphia/critter/parser/gizmo/GizmoPropertyModelGenerator.kt @@ -1,5 +1,8 @@ package dev.morphia.critter.parser.gizmo +import dev.morphia.annotations.internal.AnnotationNodeExtensions.toMorphiaAnnotation +import dev.morphia.config.MorphiaConfig +import dev.morphia.critter.conventions.PropertyConvention import dev.morphia.critter.parser.java.CritterParser.critterClassLoader import dev.morphia.critter.parser.ksp.extensions.methodCase import dev.morphia.critter.titleCase @@ -9,26 +12,44 @@ import dev.morphia.mapping.codec.pojo.critter.CritterPropertyModel import io.quarkus.gizmo.ClassCreator import io.quarkus.gizmo.MethodCreator import io.quarkus.gizmo.MethodDescriptor +import io.quarkus.gizmo.MethodDescriptor.ofMethod import io.quarkus.gizmo.ResultHandle import org.bson.codecs.pojo.PropertyAccessor -import org.objectweb.asm.Type import org.objectweb.asm.Type.getType import org.objectweb.asm.tree.AnnotationNode import org.objectweb.asm.tree.FieldNode +import org.objectweb.asm.tree.MethodNode -class GizmoPropertyModelGenerator : BaseGizmoGenerator { +class GizmoPropertyModelGenerator private constructor(val config: MorphiaConfig, entity: Class<*>) : + BaseGizmoGenerator(entity) { - constructor(entity: Class<*>, field: FieldNode) : super(entity) { - propertyName = field.name.titleCase() - generatedType = "${baseName}.${propertyName}Model" - accessorType = "${baseName}.${propertyName}Accessor" - annotations = field.visibleAnnotations ?: listOf() + constructor(config: MorphiaConfig, entity: Class<*>, field: FieldNode) : this(config, entity) { + propertyName = field.name.methodCase() + generatedType = "${baseName}.${propertyName.titleCase()}Model" + accessorType = "${baseName}.${propertyName.titleCase()}Accessor" + annotations = field.visibleAnnotations } - val propertyName: String - val accessorType: String + constructor( + config: MorphiaConfig, + entity: Class<*>, + method: MethodNode + ) : this(config, entity) { + propertyName = method.name.methodCase() + generatedType = "${baseName}.${propertyName.titleCase()}Model" + accessorType = "${baseName}.${propertyName.titleCase()}Accessor" + annotations = method.visibleAnnotations + } + + lateinit var propertyName: String + lateinit var accessorType: String lateinit var creator: ClassCreator lateinit var annotations: List + val annotationMap: Map by lazy { + annotations + .map { it.toMorphiaAnnotation() as Annotation } + .associateBy { it.annotationClass.qualifiedName!! } + } fun emit() { creator = @@ -42,30 +63,47 @@ class GizmoPropertyModelGenerator : BaseGizmoGenerator { ctor() getAccessor() + getName() + getMappedName() creator.close() } - private fun ctor() { - val constructor = creator.getConstructorCreator(EntityModel::class.java) - constructor.invokeSpecialMethod( - MethodDescriptor.ofConstructor( - CritterPropertyModel::class.java, - EntityModel::class.java - ), - constructor.getThis(), - constructor.getMethodParam(0) - ) - constructor.setParameterNames(arrayOf("model")) + private fun getName() { + creator.getMethodCreator("getName", String::class.java).use { methodCreator -> + methodCreator.returnValue(methodCreator.load(propertyName)) + } + } - // registerAnnotations(constructor) + private fun getMappedName() { + creator.getMethodCreator("getMappedName", String::class.java).use { methodCreator -> + methodCreator.returnValue( + methodCreator.load( + PropertyConvention.mappedName(config, annotationMap, propertyName) + ) + ) + } + } - constructor.close() + private fun ctor() { + creator.getConstructorCreator(EntityModel::class.java).use { constructor -> + constructor.invokeSpecialMethod( + MethodDescriptor.ofConstructor( + CritterPropertyModel::class.java, + EntityModel::class.java + ), + constructor.getThis(), + constructor.getMethodParam(0) + ) + constructor.setParameterNames(arrayOf("model")) + registerAnnotations(constructor) + constructor.returnVoid() + } } private fun registerAnnotations(constructor: MethodCreator) { val annotationMethod = - MethodDescriptor.ofMethod( + ofMethod( PropertyModel::class.java.name, "annotation", PropertyModel::class.java.name, @@ -85,29 +123,37 @@ class GizmoPropertyModelGenerator : BaseGizmoGenerator { annotation: AnnotationNode ): ResultHandle { val type = getType(annotation.desc) - val classType = type.className.substringAfterLast('.') - val builderType = Type.getType("L${type.className}Builder;") + val classPackage = type.className.substringBeforeLast('.') + val className = type.className.substringAfterLast('.') + val builderType = + getType("L${classPackage}.internal.${className}Builder;".replace('.', '/')) val builder = - MethodDescriptor.ofMethod( + ofMethod( builderType.className, - "${classType.methodCase()}Builder", + "${className.methodCase()}Builder", builderType.className ) - var local = constructor.invokeStaticMethod(builder) + val local = constructor.invokeStaticMethod(builder) val values = annotation.values?.windowed(2, 2) ?: emptyList() values.forEach { value -> val method = - MethodDescriptor.ofMethod( + ofMethod( builderType.className, value[0] as String, builderType.className, - value[1].javaClass + when (value[1]) { + is List<*> -> Array::class.java + else -> value[1].javaClass + } ) constructor.invokeVirtualMethod(method, local, load(constructor, value[1])) } - return local + return constructor.invokeVirtualMethod( + ofMethod(builderType.className, "build", type.className), + local + ) } private fun load(constructor: MethodCreator, value: Any): ResultHandle { @@ -128,11 +174,7 @@ class GizmoPropertyModelGenerator : BaseGizmoGenerator { val field = creator.getFieldCreator("accessor", accessorType) val method = creator.getMethodCreator( - MethodDescriptor.ofMethod( - creator.className, - "getAccessor", - PropertyAccessor::class.java.name - ) + ofMethod(creator.className, "getAccessor", PropertyAccessor::class.java.name) ) method.returnValue(method.readInstanceField(field.fieldDescriptor, method.`this`)) diff --git a/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterClassLoader.kt b/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterClassLoader.kt index e526a9f24b3..f5d2efa0633 100644 --- a/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterClassLoader.kt +++ b/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterClassLoader.kt @@ -9,7 +9,9 @@ import org.jboss.forge.roaster.Roaster import org.jboss.forge.roaster.model.source.JavaClassSource import org.objectweb.asm.Type -class CritterClassLoader(parent: ClassLoader?) : ChildFirst(parent, mapOf()) { +class CritterClassLoader(parent: ClassLoader?) : + // URLClassLoader(arrayOf(File("target/critter").toURI().toURL()), parent) + ChildFirst(parent, mapOf()) { companion object { var output = "target/critter" } @@ -20,11 +22,7 @@ class CritterClassLoader(parent: ClassLoader?) : ChildFirst(parent, mapOf()) { } override fun loadClass(name: String?): Class<*> { - try { - return super.loadClass(name) - } catch (e: ClassNotFoundException) { - throw ClassNotFoundException("Known class names: ${typeDefinitions.keys}", e) - } + return super.loadClass(name) } fun dump(name: String, mappings: Map = mapOf()) { @@ -32,13 +30,11 @@ class CritterClassLoader(parent: ClassLoader?) : ChildFirst(parent, mapOf()) { val outputFolder = File(output, File(name.replace('.', '/')).parent) val fileName = name.substringAfterLast('.') outputFolder.mkdirs() - var bytes = typeDefinitions[name] - if (bytes == null) { - bytes = getResourceAsStream("${name.replace('.', '/')}.class")?.readBytes() - } + val bytes = + typeDefinitions[name] + ?: getResourceAsStream("${name.replace('.', '/')}.class")?.readBytes() if (bytes != null) { - FileOutputStream(File(outputFolder, "$fileName.class")).use { it.write(bytes) } var asm = asmify(bytes) mappings.forEach { (type, mapping) -> diff --git a/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterParser.kt b/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterParser.kt index afb251975ff..fff4148eaf7 100644 --- a/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterParser.kt +++ b/critter/core/src/main/kotlin/dev/morphia/critter/parser/java/CritterParser.kt @@ -1,6 +1,7 @@ package dev.morphia.critter.parser.java import dev.morphia.annotations.Property +import dev.morphia.mapping.codec.pojo.PropertyModel import java.io.File import java.io.PrintWriter import java.io.StringWriter @@ -11,7 +12,7 @@ import org.objectweb.asm.util.TraceClassVisitor object CritterParser { var outputGenerated: File? = null - val critterClassLoader = CritterClassLoader(Thread.currentThread().contextClassLoader) + val critterClassLoader = CritterClassLoader(PropertyModel::class.java.classLoader) val propertyAnnotations = mutableListOf(Type.getType(Property::class.java)) val transientAnnotations = mutableListOf(Type.getType(Transient::class.java)) diff --git a/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt b/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt index 4be04b66473..4b338ac6d6f 100644 --- a/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt +++ b/critter/core/src/test/kotlin/dev/morphia/critter/parser/gizmo/TestGizmoGeneration.kt @@ -4,8 +4,12 @@ import dev.morphia.critter.parser.CritterGizmoGenerator import dev.morphia.critter.parser.GeneratorTest import dev.morphia.critter.parser.java.CritterParser.critterClassLoader import dev.morphia.critter.sources.Example +import dev.morphia.mapping.codec.pojo.PropertyModel +import io.quarkus.gizmo.ClassCreator import io.quarkus.gizmo.ClassOutput -import org.bson.codecs.pojo.PropertyAccessor +import io.quarkus.gizmo.MethodDescriptor +import java.lang.reflect.Modifier +import org.testng.Assert.assertNotNull import org.testng.annotations.Test class TestGizmoGeneration { @@ -20,7 +24,7 @@ class TestGizmoGeneration { critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.AgeModel") val nameModel = critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.NameModel") - invokeAll(nameModel) + invokeAll(PropertyModel::class.java, nameModel) critterClassLoader.loadClass("dev.morphia.critter.sources.__morphia.example.SalaryModel") critterClassLoader .loadClass("dev.morphia.critter.sources.__morphia.example.AgeAccessor") @@ -36,10 +40,75 @@ class TestGizmoGeneration { .newInstance() } - private fun invokeAll(nameModel: Class<*>) { + private fun invokeAll(type: Class<*>, nameModel: Class<*>) { val instance = nameModel.constructors[0].newInstance(null) - PropertyAccessor::class.java.declaredMethods.forEach { method -> - nameModel.getMethod(method.name).invoke(instance) - } + type.declaredMethods + .filter { it.parameterCount == 0 } + .forEach { method -> nameModel.getMethod(method.name).invoke(instance) } + } + + @Test + fun testConstructors() { + val className = "dev.morphia.critter.GizmoSubclass" + val constructorCall = + ClassCreator.builder() + .classOutput { name, data -> + critterClassLoader.register(name.replace('/', '.'), data) + } + .className("dev.morphia.critter.ConstructorCall") + .build() + val fieldCreator = + constructorCall + .getFieldCreator("name", String::class.java) + .setModifiers(Modifier.PUBLIC) + val constructorCreator = constructorCall.getConstructorCreator(String::class.java) + constructorCreator.invokeSpecialMethod( + MethodDescriptor.ofConstructor(Object::class.java), + constructorCreator.`this` + ) + constructorCreator.setParameterNames(arrayOf("name")) + constructorCreator.writeInstanceField( + fieldCreator.fieldDescriptor, + constructorCreator.`this`, + constructorCreator.getMethodParam(0) + ) + + constructorCreator.returnVoid() + constructorCall.close() + val newInstance = + critterClassLoader + .loadClass("dev.morphia.critter.ConstructorCall") + .getConstructor(String::class.java) + .newInstance("here i am") + + println("**************** newInstance = ${newInstance}") + val creator = + ClassCreator.builder() + .classOutput { name, data -> + critterClassLoader.register(name.replace('/', '.'), data) + } + .className(className) + .superClass("dev.morphia.critter.ConstructorCall") + .build() + val constructor = creator.getConstructorCreator(String::class.java) + constructor.invokeSpecialMethod( + MethodDescriptor.ofConstructor( + "dev.morphia.critter.ConstructorCall", + String::class.java + ), + constructor.getThis(), + constructor.getMethodParam(0) + ) + constructor.setParameterNames(arrayOf("subName")) + constructor.returnVoid() + constructor.close() + creator.close() + val instance = + critterClassLoader + .loadClass(className) + .getConstructor(String::class.java) + .newInstance("This is my name") + + assertNotNull(instance) } }