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

it.unibo.alchemist.boundary.loader.util.JVMConstructor.kt Maven / Gradle / Ivy

There is a newer version: 35.0.2
Show newest version
/*
 * Copyright (C) 2010-2023, Danilo Pianini and contributors
 * listed, for each module, in the respective subproject's build.gradle.kts file.
 *
 * This file is part of Alchemist, and is distributed under the terms of the
 * GNU General Public License, with a linking exception,
 * as described in the file LICENSE in the Alchemist distribution's top directory.
 */

package it.unibo.alchemist.boundary.loader.util

import it.unibo.alchemist.util.BugReporting
import it.unibo.alchemist.util.ClassPathScanner
import net.pearx.kasechange.splitToWords
import org.danilopianini.jirf.CreationResult
import org.danilopianini.jirf.Factory
import org.danilopianini.jirf.InstancingImpossibleException
import org.slf4j.LoggerFactory
import java.lang.reflect.Constructor
import java.lang.reflect.Modifier
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.valueParameters
import kotlin.reflect.jvm.jvmErasure

/**
 * A [JVMConstructor] whose [parameters] are an ordered list (common case for any JVM language).
 */
class OrderedParametersConstructor(
    type: String,
    val parameters: List<*> = emptyList(),
) : JVMConstructor(type) {

    override fun  parametersFor(target: KClass, factory: Factory): List<*> = parameters

    override fun toString(): String = "$typeName${parameters.joinToString(prefix = "(", postfix = ")")}"
}

private typealias OrderedParameters = List

/**
 * A [JVMConstructor] whose parameters are named
 * and hence stored in a [parametersMap]
 * (no pure Java class works with named parameters now, Kotlin-only).
 */
class NamedParametersConstructor(
    type: String,
    val parametersMap: Map<*, *> = emptyMap(),
) : JVMConstructor(type) {

    private fun List.description() = joinToString(prefix = "\n- ", separator = "\n- ") {
        it.namedParametersDescriptor()
    }

    private inline infix fun Boolean.and(then: () -> Boolean): Boolean = if (this) then() else false

    private fun List.allLowerCase() = map { it.lowercase() }

    private fun String?.couldBeInterpretedAs(name: String?): Boolean =
        equals(name, ignoreCase = true) || this?.splitToWords()?.allLowerCase() == name?.splitToWords()?.allLowerCase()

    override fun  parametersFor(target: KClass, factory: Factory): List<*> {
        val providedNames = parametersMap.map { it.key.toString() }
        val singletons = factory.singletonObjects.keys
        val constructorsWithOrderedParameters = target.constructors.map { constructor ->
            constructor.valueParameters.filterNot { it.type.jvmErasure.java in singletons }.sortedBy { it.index }
        }
        val usableConstructors: Map> =
            constructorsWithOrderedParameters.mapNotNull { parameters ->
                if (providedNames.size <= parameters.size) {
                    // Parameter count must be compatible (as many or less parameters provided)
                    val (optional, mandatory) = parameters.partition { it.isOptional }
                    val mandatoryNames = mandatory.map { it.name }
                    val requiredOptionals by lazy { optional.take(providedNames.size - mandatory.size).map { it.name } }
                    fun verifyParameterMatch(matchMethod: (List).(List) -> Boolean) =
                        providedNames.matchMethod(mandatoryNames) and { providedNames.matchMethod(requiredOptionals) }
                    // Check for exact name match
                    val exactMatch = verifyParameterMatch(List::containsAll)
                    if (exactMatch) {
                        parameters to emptyMap()
                    } else {
                        // Check for similar-enough non-ambiguous matches: kebab-case, snake_case, etc.
                        // convertedNames is a map between the actual parameter name and the provided name
                        val convertedNames = providedNames.mapNotNull { providedName ->
                            parameters.filter { it.name.couldBeInterpretedAs(providedName) }
                                .takeIf { it.size == 1 }
                                ?.first()
                                ?.name
                                ?.let { it to providedName }
                        }.toMap()
                        val worksIfNamesAreReplaced = convertedNames.keys.containsAll(mandatoryNames) and {
                            convertedNames.keys.containsAll(requiredOptionals)
                        }
                        if (worksIfNamesAreReplaced) {
                            parameters to convertedNames.filter { it.key != it.value }
                        } else {
                            null
                        }
                    }
                } else {
                    null
                }
            }.toMap()
        // If at least one constructor is a perfect match, discard the ones requiring name replacement.
        val preferredMatch = usableConstructors
            .filterValues { replacements -> replacements.isEmpty() }
            .takeIf { it.isNotEmpty() }
            ?: usableConstructors
        require(preferredMatch.isNotEmpty()) {
            """
            No constructor available for ${target.simpleName} with named parameters $providedNames.
            Note: Due to the way Kotlin's @JvmOverloads works, all the optional parameters that precede the ones
            §of interest must be provided.
            Available constructors have the following *named* parameters:
            """.trimIndent().replace(Regex("\\R§"), " ") +
                constructorsWithOrderedParameters.description()
        }
        require(preferredMatch.size == 1) {
            """
            |Ambiguous constructors resolution for ${target.simpleName} with named parameters $providedNames.
            |${ usableConstructors.keys.joinToString("\n|") { "Match: ${it.namedParametersDescriptor()}" } }
            |Available constructors have the following *named* parameters:
            """.trimMargin() + constructorsWithOrderedParameters.description()
        }
        val (selectedConstructor, replacements) = preferredMatch.toList().first()
        if (replacements.isNotEmpty()) {
            logger.warn(
                "Alchemist had to replace some parameter names to match the constructor signature or {}: {}",
                target.simpleName,
                replacements,
            )
        }
        return selectedConstructor
            .filter { parametersMap.containsKey(replacements.getOrDefault(it.name, it.name)) }
            .map { parametersMap[replacements.getOrDefault(it.name, it.name)] }
    }

    private fun Collection.namedParametersDescriptor() = "$size-ary constructor: " +
        filter { it.name != null }.joinToString {
            "${it.name}:${it.type.jvmErasure.simpleName}${if (it.isOptional) "" else "" }"
        }

    override fun toString(): String = "$typeName($parametersMap)"

    private companion object {
        @JvmStatic
        private val logger = LoggerFactory.getLogger(NamedParametersConstructor::class.java)
    }
}

internal data class TypeSearch(val typeName: String, val targetType: Class) {

    private val packageName: String? = typeName.substringBeforeLast('.', "").takeIf { it.isNotEmpty() }
    private val isQualified get() = packageName != null

    val subTypes: Collection> by lazy {
        val compatibleTypes: List> = when (packageName) {
            null ->
                when {
                    targetType.packageName.startsWith("it.unibo.alchemist") ->
                        ClassPathScanner.subTypesOf(targetType, "it.unibo.alchemist")
                    else -> ClassPathScanner.subTypesOf(targetType)
                }
            else -> ClassPathScanner.subTypesOf(targetType, packageName)
        }
        when {
            // The target type cannot be instanced, just return its concrete subclasses
            Modifier.isAbstract(targetType.modifiers) -> compatibleTypes
            // The target type can be instanced, return it and all its concrete subclasses
            else -> mutableSetOf(targetType).apply { addAll(compatibleTypes) }
        }
    }

    val perfectMatches: List> by lazy {
        subtypes(ignoreCase = false)
    }

    val subOptimalMatches: List> by lazy {
        subtypes(ignoreCase = true)
    }

    private fun subtypes(ignoreCase: Boolean) =
        subTypes.filter { typeName.equals(if (isQualified) it.name else it.simpleName, ignoreCase = ignoreCase) }

    companion object {
        inline fun  typeNamed(name: String) = TypeSearch(name, T::class.java)
    }
}

/**
 * A constructor for a JVM class of type [typeName].
 */
sealed class JVMConstructor(val typeName: String) {

    /**
     * provided a [target] class, extracts the parameters as an ordered list.
     */
    protected abstract fun  parametersFor(target: KClass, factory: Factory): List<*>

    /**
     * Provided a JIRF [factory], builds an instance of the requested type [T] or fails gracefully,
     * returning a [Result].
     */
    inline fun  buildAny(factory: Factory): Result = buildAny(T::class.java, factory)

    /**
     * Provided a JIRF [factory], builds an instance of the requested [type] T or fails gracefully,
     * returning a [Result].
     */
    fun  buildAny(type: Class, factory: Factory): Result {
        val typeSearch = TypeSearch(typeName, type)
        val perfectMatches = typeSearch.perfectMatches
        return when (perfectMatches.size) {
            0 -> {
                val subOptimalMatches = typeSearch.subOptimalMatches
                when (subOptimalMatches.size) {
                    0 -> Result.failure(
                        IllegalStateException(
                            """
                            |No valid match for type $typeName among subtypes of ${type.simpleName}.
                            |Valid subtypes are: ${typeSearch.subTypes.map { it.simpleName }}
                            """.trimMargin(),
                        ),
                    )
                    1 -> {
                        logger.warn(
                            "{} has been selected even though it is not a perfect match for {}",
                            subOptimalMatches.first().name,
                            typeName,
                        )
                        Result.success(newInstance(subOptimalMatches.first().kotlin, factory))
                    }
                    else -> Result.failure(
                        IllegalStateException(
                            "Multiple matches for $typeName as subtype of ${type.simpleName}: " +
                                "${perfectMatches.map { it.name }}. Disambiguation is required.",
                        ),
                    )
                }
            }
            1 -> runCatching { newInstance(perfectMatches.first().kotlin, factory) }
            else -> Result.failure(
                IllegalStateException("Multiple perfect matches for $typeName: ${perfectMatches.map { it.name }}"),
            )
        }
    }

    private fun CreationResult<*>.logErrors(logger: (String, Array) -> Unit) {
        for ((constructor, exception) in exceptions) {
            val errorMessages = generateSequence>(
                exception to exception.message,
            ) { (outer, _) -> outer?.cause to outer?.cause?.message }
                .takeWhile { it.first != null }
                .filter { !it.second.isNullOrBlank() }
                .map { (first, second) ->
                    checkNotNull(first) {
                        BugReporting.reportBug(
                            "Bug in ${JVMConstructor::class.qualifiedName}",
                            mapOf(
                                "first" to first,
                                "second" to second,
                                "constructor" to constructor,
                                "creation result" to this,
                            ),
                        )
                    }
                    "${first::class.simpleName}: $second"
                }
                .toList()
            logger(
                "Constructor {} failed for {} ",
                arrayOf(
                    constructor.shorterToString(),
                    if (errorMessages.isEmpty()) "unknown reasons" else "the following reasons:",
                ),
            )
            errorMessages.reversed().forEach { logger("  - $it", emptyArray()) }
        }
    }

    private fun  newInstance(target: KClass, jirf: Factory): T {
        /*
         * preprocess parameters:
         *
         * 1. take all constructors with at least the number of parameters passed
         * 2. align end positions (the former parameters are usually implicit)
         * 3. find the KClass of such parameter
         * 4. find the subclassess of that class, and see if any matches the provided type
         * 5. if so, build the parameter
         */
        val originalParameters = parametersFor(target, jirf)
        logger.debug("Building a {} with {}", target.simpleName, originalParameters)
        val compatibleConstructors by lazy {
            target.constructors.filter { it.valueParameters.size >= originalParameters.size }
        }
        val parameters = originalParameters.mapIndexed { index, parameter ->
            if (parameter is JVMConstructor) {
                val possibleMappings = compatibleConstructors.flatMap { constructor ->
                    val mappedIndex = constructor.valueParameters.lastIndex - originalParameters.lastIndex + index
                    val potentialType = constructor.valueParameters[mappedIndex]
                    val potentialJavaType = potentialType.type.jvmErasure.java
                    val subtypes = ClassPathScanner.subTypesOf(potentialJavaType) +
                        if (Modifier.isAbstract(potentialJavaType.modifiers)) emptyList() else listOf(potentialJavaType)
                    val compatibleSubtypes = subtypes.filter { subtype ->
                        val subtypeName = if (parameter.typeName.contains('.')) subtype.name else subtype.simpleName
                        parameter.typeName == subtypeName
                    }
                    when {
                        compatibleSubtypes.isEmpty() -> {
                            logger.warn(
                                "Constructor {} discarded as {} is incompatible with parameter #{}:{}",
                                constructor,
                                parameter,
                                mappedIndex,
                                potentialType.type,
                            )
                            emptyList()
                        }
                        compatibleSubtypes.size > 1 -> {
                            error(
                                "Ambiguous mapping: $compatibleSubtypes all match the requested type $typeName for " +
                                    "parameter #$mappedIndex:$potentialType of $constructor",
                            )
                        }
                        else -> {
                            val maybeParameter = parameter.buildAny(compatibleSubtypes.first(), jirf)
                            listOf(maybeParameter.getOrThrow())
                        }
                    }
                }
                when (possibleMappings.size) {
                    0 -> error("Could not build parameter #$index defined as $parameter")
                    1 -> possibleMappings.first()
                    else -> error("Ambiguous parameter #$index $parameter, multiple options match: $possibleMappings")
                }
            } else {
                parameter
            }
        }
        val creationResult = jirf.build(target.java, parameters)
        return creationResult.createdObject
            .orElseThrow { explainedFailure(jirf, target, originalParameters, creationResult) }
            .also { creationResult.logErrors { message, arguments -> logger.warn(message, *arguments) } }
    }

    private companion object {
        @JvmStatic
        private val logger = LoggerFactory.getLogger(JVMConstructor::class.java)

        private fun Constructor<*>.shorterToString() =
            declaringClass.simpleName + parameterTypes.joinToString(prefix = "(", postfix = ")") { it.simpleName }

        private fun  explainedFailure(
            jirf: Factory,
            target: KClass,
            parameters: List<*>,
            creationResult: CreationResult,
        ): Nothing {
            val implicits = when {
                jirf.singletonObjects.isEmpty() -> "|No singleton objects available in the context"
                else ->
                    """
                    |Implicitly available singleton objects in the context: ${
                        jirf.singletonObjects
                            .map { (type, it) -> "|  * $it # associated to type ${type.simpleName}" }
                            .joinToString(prefix = "\n", separator = ";\n", postfix = ".")
                    }
                    """.trim()
            }
            val exceptions = creationResult.exceptions.asSequence()
            val exceptionsSummary = exceptions.mapIndexed { consIndex, (constructor, exception) ->
                val causalChain = generateSequence(exception) { it.cause }
                    .mapIndexed { index, cause ->
                        val message = cause.takeIf { it is InstancingImpossibleException }?.message
                            ?.replace("it.unibo.alchemist.model.interfaces.", "")
                            ?.replace("it.unibo.alchemist.boundary.", "i.u.a.b.")
                            ?.replace("it.unibo.alchemist.model.", "i.u.a.m.")
                            ?.replace("it.unibo.alchemist.", "i.u.a.")
                            ?.replace("java.lang.", "")
                            ?.replace("kotlin.", "")
                            ?: cause.message ?: "No message"
                        "|    failure message ${index + 1} of ${cause::class.simpleName}: $message"
                    }
                val constructorName = constructor.shorterToString()
                val intro = "${consIndex + 1}. Constructor $constructorName failed with the following exception(s):"
                val causalChainString = causalChain.joinToString(separator = "\n")
                "| $intro\n$causalChainString".trim()
            }.joinToString(separator = "\n")
            val errorMessage =
                """
                |Could not create $this, requested as instance of ${target.simpleName}.
                |Actual parameters: $parameters
                $implicits
                |Failure list analysis:
                $exceptionsSummary
                """.trimMargin().trim()
            val masterException = IllegalArgumentException("Illegal Alchemist specification: $errorMessage")
            creationResult.exceptions.forEach { (_, exception) -> masterException.addSuppressed(exception) }
            throw masterException
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy