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

io.javalin.plugin.openapi.dsl.extractDocumentation.kt Maven / Gradle / Ivy

The newest version!
package io.javalin.plugin.openapi.dsl

import io.javalin.apibuilder.CrudFunctionHandler
import io.javalin.core.event.HandlerMetaInfo
import io.javalin.core.util.OptionalDependency
import io.javalin.core.util.Reflection.Companion.rfl
import io.javalin.core.util.Util
import io.javalin.core.util.methodReferenceReflectionMethodName
import io.javalin.http.Handler
import io.javalin.http.HandlerType
import io.javalin.plugin.openapi.CreateSchemaOptions
import io.javalin.plugin.openapi.annotations.HttpMethod
import io.javalin.plugin.openapi.annotations.NULL_STRING
import io.javalin.plugin.openapi.annotations.OpenApi
import io.javalin.plugin.openapi.annotations.asOpenApiDocumentation
import io.javalin.plugin.openapi.annotations.pathInfo
import io.javalin.plugin.openapi.annotations.scanForAnnotations
import io.javalin.plugin.openapi.annotations.warnUserAboutPotentialBugs
import org.slf4j.LoggerFactory
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.logging.Logger
import kotlin.reflect.KFunction
import kotlin.reflect.jvm.javaMethod

class ExtractDocumentation {
    // needed for logging purposes only, since most of the Kotlin functions below are defined globally and
    // we want to have source code/file localised messages to ease debugging
}
private val localLogger = LoggerFactory.getLogger(ExtractDocumentation::class.java)

fun HandlerMetaInfo.extractDocumentation(options: CreateSchemaOptions): OpenApiDocumentation {
    val documentation = document()

    options.default?.apply(documentation)

    val userDocumentation: OpenApiDocumentation = when (handler) {
        is DocumentedHandler -> (handler as DocumentedHandler).documentation
        is CrudFunctionHandler -> extractDocumentationFromCrudHandler(handler as CrudFunctionHandler)
        else -> {
            val openApiAnnotation: OpenApi? = getOpenApiAnnotationFromReference()
                    ?: methodReferenceOfHandler?.getOpenApiAnnotation()
                    ?: fieldReferenceOfHandler?.getOpenApiAnnotation()
                    ?: getOpenApiAnnotationFromHandler()
            openApiAnnotation?.asOpenApiDocumentation()
                    ?: extractDocumentationWithPathScanning(options)
                    ?: document()
        }
    }

    documentation.apply(userDocumentation)

    return documentation
}

private fun extractDocumentationFromCrudHandler(handler: CrudFunctionHandler): OpenApiDocumentation {
    val crudHandler = handler.crudHandler
    return if (crudHandler is DocumentedCrudHandler) {
        crudHandler.crudHandlerDocumentation.asMap()[handler.function]
                ?: throw IllegalStateException("No documentation for this function")
    } else {
        val method = rfl(handler.crudHandler).getMethodByName(handler.function.value) ?: throw NoSuchMethodException(handler.function.value)
        val openApi: OpenApi? = method.getDeclaredAnnotation(OpenApi::class.java)
        openApi?.asOpenApiDocumentation()
                ?: document()
    }
}

private fun HandlerMetaInfo.extractDocumentationWithPathScanning(options: CreateSchemaOptions): OpenApiDocumentation? {
    if (options.packagePrefixesToScan.isEmpty()) {
        return null
    }
    Util.ensureDependencyPresent(OptionalDependency.CLASS_GRAPH)
    return pathInfo?.let { pathInfo ->
        val documentationFromScan = scanForAnnotations(options.packagePrefixesToScan)
        documentationFromScan[pathInfo]
    }
}

private fun HandlerMetaInfo.getOpenApiAnnotationFromReference(): OpenApi? {
    return try {
        methodReferenceOfHandler?.getOpenApiAnnotation()
                ?: rfl(handler).lambdaField?.getOpenApiAnnotation()
    } catch (e: NoSuchFieldException) {
        null
    } catch (e: Error) {
        return if (e::class.qualifiedName == "kotlin.reflect.jvm.internal.KotlinReflectionInternalError") {
            Logger.getGlobal()
                    .warning("Local functions, lambdas, anonymous functions and local variables with @OpenApi annotations are currently not supported. " +
                            "The annotation of the handler for \"$httpMethod $path\" will be ignored. To fix this, move the handler into a global function.")
            null
        } else {
            throw e
        }
    }
}

private fun HandlerMetaInfo.getOpenApiAnnotationFromHandler(): OpenApi? {
    return try {
        val method = rfl(handler).getMethodByName("handle")!!
        method.getOpenApiAnnotation()
    } catch (e: NullPointerException) {
        return null
    }
}

private val HandlerMetaInfo.methodReferenceOfHandler: Method?
    get() = rfl(handler).let { handlerReflection ->
            when {
                handlerReflection.isClass -> (handler as Class<*>).methods[0]
                handlerReflection.isKotlinMethodReference -> {
                    val field = handlerReflection.obj.javaClass.declaredFields.single()
                    field.isAccessible = true
                    (field.get(handlerReflection.obj) as KFunction<*>).javaMethod
                }
                handlerReflection.isKotlinAnonymousLambda -> null // Cannot be parsed
                handlerReflection.isJavaNonStaticMethodReference -> methodReferenceOfNonStaticJavaHandler
                handlerReflection.isJavaAnonymousLambda -> null // taken care of by field-type handler
                else -> {
                    localLogger.debug("methodReferenceOfHandler else branch called for handler: "
                            +"$httpMethod path '$path' and parent class = " + handlerReflection.parentClass.name)
                    null
                }
            }
    }

private val HandlerMetaInfo.fieldReferenceOfHandler: Field?
    get() {
        if (handler !is Handler) {
            localLogger.error("fieldReferenceOfHandler $httpMethod path: $path -- incompatible type: "
                    + handler.javaClass.canonicalName)
            return null // early return since handler does not implement Handler interface
        }

        return rfl(handler).let { handlerReflection ->
            when {
                // handlerReflection.isClass -> (handler as Class<*>).methods[0]
                handlerReflection.isClass -> null // case handled in @see methodReferenceOfHandler
                handlerReflection.isKotlinMethodReference -> null // case handled in @see methodReferenceOfHandler
                handlerReflection.isKotlinAnonymousLambda -> null // Cannot be parsed // N.B. possibly add similar treatment as for Java
                handlerReflection.isJavaNonStaticMethodReference -> null // case handled in @see methodReferenceOfHandler
                handlerReflection.isJavaAnonymousLambda -> findFieldByOpenApiAnnotation(handlerReflection.parentClass.declaredFields)
                handlerReflection.isJavaAnonymousClass -> findFieldByOpenApiAnnotation(handlerReflection.parentClass.declaredFields)
                handlerReflection.isJavaMemberClass -> findFieldByOpenApiAnnotation(handlerReflection.parentClass.declaredFields)
                else -> {
                    localLogger.debug("fieldReferenceOfHandler else branch called for handler: "
                            +"$httpMethod path '$path' and parent class = " + handlerReflection.parentClass.name)
                    null
                }
            }
        }
    }

private val HandlerMetaInfo.methodReferenceOfNonStaticJavaHandler: Method?
    get() {
        val handlerParentClass = rfl(handler)
                .getMethodByName(methodReferenceReflectionMethodName)
                ?.parameters?.get(0)
                ?.parameterizedType as Class<*>?

        val methods = handlerParentClass
                ?.let { rfl(it) }
                ?.methodsNotDeclaredByObject
                ?: arrayOf()

        return when {
            methods.isEmpty() -> null
            methods.size == 1 -> methods[0]
            else -> {
                val methodThatMatchesHandler = findMethodByOpenApiAnnotation(methods)
                if (methodThatMatchesHandler != null) {
                    return methodThatMatchesHandler
                }

                val hasAnyMethodTheOpenApiAnnotation = methods.any { it.getOpenApiAnnotation() != null }
                if (hasAnyMethodTheOpenApiAnnotation && handlerParentClass != null) {
                    localLogger.warn("Unfortunately it is not possible to match the @OpenApi annotations to the handler in ${handlerParentClass.canonicalName}. " +
                            "Please add the `path` and the `method` information to the annotation, so the handler can be matched.")
                }
                null
            }
        }
    }

private fun HandlerMetaInfo.findMethodByOpenApiAnnotation(methods: Array): Method? {
    val methodsThatMatchesPath = methods.filter {
        val annotation = it.getOpenApiAnnotation() ?: return@filter false
        annotation.path == this.path
    }
    return when {
        methodsThatMatchesPath.size == 1 -> methodsThatMatchesPath[0]
        methodsThatMatchesPath.size > 1 -> methodsThatMatchesPath
                .find {
                    it.getOpenApiAnnotation()
                            ?.let { annotation -> annotation.pathInfo == pathInfo }
                            ?: false
                }
        else -> null
    }
}

private fun HandlerMetaInfo.findFieldByOpenApiAnnotation(allFields: Array): Field? {
    if (allFields.isEmpty()) {
        // no fields declared -> handler cannot be a field-type declaration
        return null
    }
    // filter only implementations that are derived from the Handler interface
    val handlerFields = allFields.filter { Handler::class.java.isAssignableFrom(it.type) }
    if (handlerFields.isEmpty()) {
        localLogger.warn("cannot match handler: $httpMethod path: $path -- no Handler-type fields declared in class "
                + allFields[0].declaringClass.canonicalName)
        return null
    }

    // case 1: exact match -- nominal case of OpenAPI path and HttpMethod declaration
    val fieldsExactMatch = handlerFields.filter {
        val annotation = it.getOpenApiAnnotation() ?: return@filter false
        path.contentEquals(annotation.path) && httpMethod.compatible(annotation.method)
    }
    when (fieldsExactMatch.size) {
        0 -> { /* empty */ } // no exact or partial-only match, continue with code below
        1 -> return fieldsExactMatch[0] // exact path and HttpMethod match
        else -> { // more than one exact match
            localLogger.error("cannot match handler: $httpMethod path: $path -- multiple equal OpenAPI fields declarations")
            for ((index, value) in fieldsExactMatch.withIndex()) {
                localLogger.error("  - candidate $index: $value")
            }
            return null
        }
    }

    // case 2: only one Handler-type field declared
    if (handlerFields.size == 1) {
        val annotation = handlerFields[0].getOpenApiAnnotation()
        if (annotation == null) {
            localLogger.warn("cannot match handler: $httpMethod path: $path -- missing OpenAPI field annotation for field: "
                    + handlerFields[0].declaringLocation())
            return null
        }

        if (annotation.path == NULL_STRING && httpMethod.compatible(annotation.method)) {
            // trivial match: only one field with OpenAPI
        } else if (annotation.path == NULL_STRING || path.contentEquals(annotation.path)) {
            val annotationMethod = annotation.method
            localLogger.warn("partial match for path $path and single Handler-type field -- handler type $httpMethod defined"
                    +" but only available OpenAPI annotation declared for '$annotationMethod' for field: "
                    + handlerFields[0].declaringLocation())
        } else {
            localLogger.warn("next best match for path $path and handler type $httpMethod defined -" +
                    " imperfect match between annotation '$annotation' and field: "
                    + handlerFields[0].declaringLocation())
        }
        return handlerFields[0]
    }

    // case 3: matching Path
    val fieldsThatMatchPath = handlerFields.filter {
        val annotation = it.getOpenApiAnnotation() ?: return@filter false
        annotation.path == this.path
    }
    when (fieldsThatMatchPath.size) {
        0 -> { /* empty */ } // no exact or partial-only match, continue with code below
        1 -> {
            val annotation = fieldsThatMatchPath[0].getOpenApiAnnotation() ?: return null
            if (!httpMethod.compatible(annotation.method)) {
                val annotationMethod = annotation.method
                localLogger.warn("partial match for path '$path' -- handler method '$httpMethod' mismatch "
                        +"with OpenAPI method '$annotationMethod' for field: "
                        + fieldsThatMatchPath[0].declaringLocation())
            }
            return fieldsThatMatchPath[0]
        }
        else -> {
            // fieldsThatMatchesPath.size > 1 ->
            return fieldsThatMatchPath
            .find {
                it.getOpenApiAnnotation()
                        ?.let { annotation -> annotation.pathInfo == pathInfo }
                        ?: false
            }
        }
    }

    // case 4: matching HttpMethod
    val fieldsMatchingHttpMethod = handlerFields.filter {
        val annotation = it.getOpenApiAnnotation() ?: return@filter false
        httpMethod.compatible(annotation.method)
    }
    when (fieldsMatchingHttpMethod.size) {
        0 -> { /* empty */ } // no exact or partial-only match, continue with code below
        1 -> {
            val annotation = fieldsMatchingHttpMethod[0].getOpenApiAnnotation() ?: return null
            if (annotation.path != NULL_STRING && !path.contentEquals(annotation.path)) {
                val annotationPath = annotation.path
                localLogger.warn("partial match for method $httpMethod -- handler path '$path' mismatch "
                        +"with OpenAPI path '$annotationPath' for field: "
                        + fieldsMatchingHttpMethod[0].declaringLocation())
            }
            return fieldsMatchingHttpMethod[0] // exact HttpMethod match
        }
        else -> { // more than one exact match
            localLogger.error("cannot match handler: $httpMethod path: $path -- multiple equal OpenAPI fields method declarations")
            for ((index, value) in fieldsMatchingHttpMethod.withIndex()) {
                localLogger.error("  - candidate $index: $value")
            }
            return null
        }
    }

    localLogger.warn("cannot match handler: $httpMethod path: $path -- no matching OpenAPI field annotation for eligible fields: ")
    for ((index, value) in handlerFields.withIndex()) {
        localLogger.error("  - candidate $index: $value")
    }
    return null
}

fun Method.getOpenApiAnnotation(): OpenApi? {
    val result = getAnnotation(OpenApi::class.java) ?: return null
    result.warnUserAboutPotentialBugs(this.declaringClass)
    return result
}

fun Field.getOpenApiAnnotation(): OpenApi? {
    val result = getAnnotation(OpenApi::class.java) ?: return null
    result.warnUserAboutPotentialBugs(this.declaringClass)
    return result
}

fun HandlerType.compatible(other: Any?) : Boolean {
    if (other == null) {
        return false
    }
    if (equals(other)) {
        return true
    }
    if (other !is HttpMethod) {
        return false
    }
    val httpMethod : HttpMethod = other
    return when (this) { // N.B. this strange comparison is needed because HttpMethod and HandlerType enums differ
        HandlerType.GET -> httpMethod == HttpMethod.GET
        HandlerType.POST -> httpMethod == HttpMethod.POST
        HandlerType.PUT -> httpMethod == HttpMethod.PUT
        HandlerType.PATCH -> httpMethod == HttpMethod.PATCH
        HandlerType.DELETE -> httpMethod == HttpMethod.DELETE
        HandlerType.HEAD -> httpMethod == HttpMethod.HEAD
        HandlerType.OPTIONS -> httpMethod == HttpMethod.OPTIONS
        HandlerType.TRACE -> httpMethod == HttpMethod.TRACE
        HandlerType.CONNECT -> false // not mapped/no equivalent in HttpMethod
        HandlerType.BEFORE -> false // not mapped/no equivalent in HttpMethod
        HandlerType.AFTER -> false // not mapped/no equivalent in HttpMethod
        HandlerType.INVALID -> false // not mapped/no equivalent in HttpMethod
        else -> false
    }
}

private fun Method.declaringLocation() : String = returnType.toGenericString() + " " + declaringClass.name+"::"+ name

private fun Field.declaringLocation() : String = declaringClass.canonicalName+"::"+ type +" "+ name




© 2015 - 2025 Weber Informatics LLC | Privacy Policy