diff --git a/idea-plugin/src/main/kotlin/com/itangcent/ai/AIService.kt b/idea-plugin/src/main/kotlin/com/itangcent/ai/AIService.kt index 0f578703b..a6bf9b307 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/ai/AIService.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/ai/AIService.kt @@ -1,8 +1,13 @@ package com.itangcent.ai +import com.google.inject.ProvidedBy +import com.google.inject.Singleton +import com.itangcent.spi.SpiSingleBeanProvider + /** * Interface for AI service operations */ +@ProvidedBy(AIServiceProvider::class) interface AIService { /** * Sends a prompt to the AI service and returns the response @@ -27,3 +32,5 @@ interface AIService { } } +@Singleton +class AIServiceProvider : SpiSingleBeanProvider() \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt index 1e02c8dfe..03f65effc 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt @@ -29,8 +29,6 @@ class YapiExportAction : ApiExportAction("Export Yapi") { builder.bindInstance(ExportChannel::class, ExportChannel.of("yapi")) builder.bindInstance(ExportDoc::class, ExportDoc.of("request", "methodDoc")) - builder.bind(MethodDocBuilderListener::class) { it.with(CompositeMethodDocBuilderListener::class).singleton() } - builder.bind(MethodFilter::class) { it.with(ConfigurableMethodFilter::class).singleton() } builder.bindInstance("file.save.default", "yapi.json") diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListener.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListener.kt deleted file mode 100644 index 72be64168..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListener.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.itangcent.idea.plugin.api.export.core - -import com.google.inject.Singleton -import com.itangcent.common.model.MethodDoc -import com.itangcent.common.model.Param -import com.itangcent.spi.SpiCompositeLoader - -@Singleton -class CompositeMethodDocBuilderListener : - MethodDocBuilderListener { - - private val delegate: MethodDocBuilderListener by lazy { - SpiCompositeLoader.loadComposite() - } - - override fun setName(exportContext: ExportContext, methodDoc: MethodDoc, name: String) { - delegate.setName(exportContext, methodDoc, name) - } - - override fun appendDesc(exportContext: ExportContext, methodDoc: MethodDoc, desc: String?) { - delegate.appendDesc(exportContext, methodDoc, desc) - } - - override fun addParam(exportContext: ExportContext, methodDoc: MethodDoc, param: Param) { - delegate.addParam(exportContext, methodDoc, param) - } - - override fun setRet(exportContext: ExportContext, methodDoc: MethodDoc, ret: Any?) { - delegate.setRet(exportContext, methodDoc, ret) - } - - override fun appendRetDesc(exportContext: ExportContext, methodDoc: MethodDoc, retDesc: String?) { - delegate.appendRetDesc(exportContext, methodDoc, retDesc) - } - - override fun startProcessMethod(methodExportContext: MethodExportContext, methodDoc: MethodDoc) { - delegate.startProcessMethod(methodExportContext, methodDoc) - } - - override fun processCompleted(methodExportContext: MethodExportContext, methodDoc: MethodDoc) { - delegate.processCompleted(methodExportContext, methodDoc) - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeRequestBuilderListener.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeRequestBuilderListener.kt deleted file mode 100644 index b1f002c04..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeRequestBuilderListener.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.itangcent.idea.plugin.api.export.core - -import com.google.inject.Singleton -import com.itangcent.common.model.* -import com.itangcent.spi.SpiCompositeLoader - -@Singleton -class CompositeRequestBuilderListener : - RequestBuilderListener { - - private val delegate: RequestBuilderListener by lazy { - SpiCompositeLoader.loadComposite() - } - - override fun setName(exportContext: ExportContext, request: Request, name: String) { - delegate.setName(exportContext, request, name) - } - - override fun setMethod(exportContext: ExportContext, request: Request, method: String) { - delegate.setMethod(exportContext, request, method) - } - - override fun setPath(exportContext: ExportContext, request: Request, path: URL) { - delegate.setPath(exportContext, request, path) - } - - override fun setModelAsBody(exportContext: ExportContext, request: Request, model: Any) { - delegate.setModelAsBody(exportContext, request, model) - } - - override fun addModelAsParam(exportContext: ExportContext, request: Request, model: Any) { - delegate.addModelAsParam(exportContext, request, model) - } - - override fun addModelAsFormParam(exportContext: ExportContext, request: Request, model: Any) { - delegate.addModelAsFormParam(exportContext, request, model) - } - - override fun addFormParam(exportContext: ExportContext, request: Request, formParam: FormParam) { - delegate.addFormParam(exportContext, request, formParam) - } - - override fun addParam(exportContext: ExportContext, request: Request, param: Param) { - delegate.addParam(exportContext, request, param) - } - - override fun removeParam(exportContext: ExportContext, request: Request, param: Param) { - delegate.removeParam(exportContext, request, param) - } - - override fun addPathParam(exportContext: ExportContext, request: Request, pathParam: PathParam) { - delegate.addPathParam(exportContext, request, pathParam) - } - - override fun setJsonBody(exportContext: ExportContext, request: Request, body: Any?, bodyAttr: String?) { - delegate.setJsonBody(exportContext, request, body, bodyAttr) - } - - override fun appendDesc(exportContext: ExportContext, request: Request, desc: String?) { - delegate.appendDesc(exportContext, request, desc) - } - - override fun addHeader(exportContext: ExportContext, request: Request, header: Header) { - delegate.addHeader(exportContext, request, header) - } - - override fun addResponse(exportContext: ExportContext, request: Request, response: Response) { - delegate.addResponse(exportContext, request, response) - } - - override fun addResponseHeader(exportContext: ExportContext, response: Response, header: Header) { - delegate.addResponseHeader(exportContext, response, header) - } - - override fun setResponseBody(exportContext: ExportContext, response: Response, bodyType: String, body: Any?) { - delegate.setResponseBody(exportContext, response, bodyType, body) - } - - override fun setResponseCode(exportContext: ExportContext, response: Response, code: Int) { - delegate.setResponseCode(exportContext, response, code) - } - - override fun appendResponseBodyDesc(exportContext: ExportContext, response: Response, bodyDesc: String?) { - delegate.appendResponseBodyDesc(exportContext, response, bodyDesc) - } - - override fun startProcessMethod(methodExportContext: MethodExportContext, request: Request) { - delegate.startProcessMethod(methodExportContext, request) - } - - override fun processCompleted(methodExportContext: MethodExportContext, request: Request) { - delegate.processCompleted(methodExportContext, request) - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/MethodDocBuilderListener.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/MethodDocBuilderListener.kt index bc401971b..c289b19a9 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/MethodDocBuilderListener.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/MethodDocBuilderListener.kt @@ -1,41 +1,59 @@ package com.itangcent.idea.plugin.api.export.core -import com.google.inject.ImplementedBy +import com.google.inject.ProvidedBy +import com.google.inject.Singleton import com.itangcent.common.model.MethodDoc import com.itangcent.common.model.Param -import com.itangcent.common.model.Request +import com.itangcent.spi.SpiCompositeBeanProvider -@ImplementedBy(DefaultMethodDocBuilderListener::class) +@ProvidedBy(MethodDocBuilderListenerCompositeProvider::class) interface MethodDocBuilderListener { - fun setName(exportContext: ExportContext, - methodDoc: MethodDoc, name: String) + fun setName( + exportContext: ExportContext, + methodDoc: MethodDoc, name: String + ) - fun appendDesc(exportContext: ExportContext, - methodDoc: MethodDoc, desc: String?) + fun appendDesc( + exportContext: ExportContext, + methodDoc: MethodDoc, desc: String? + ) - fun addParam(exportContext: ExportContext, - methodDoc: MethodDoc, param: Param) + fun addParam( + exportContext: ExportContext, + methodDoc: MethodDoc, param: Param + ) - fun setRet(exportContext: ExportContext, - methodDoc: MethodDoc, ret: Any?) + fun setRet( + exportContext: ExportContext, + methodDoc: MethodDoc, ret: Any? + ) - fun appendRetDesc(exportContext: ExportContext, - methodDoc: MethodDoc, retDesc: String?) + fun appendRetDesc( + exportContext: ExportContext, + methodDoc: MethodDoc, retDesc: String? + ) fun startProcessMethod(methodExportContext: MethodExportContext, methodDoc: MethodDoc) fun processCompleted(methodExportContext: MethodExportContext, methodDoc: MethodDoc) } +@Singleton +class MethodDocBuilderListenerCompositeProvider : SpiCompositeBeanProvider() + //region utils------------------------------------------------------------------ -fun MethodDocBuilderListener.addParam(exportContext: ExportContext, - methodDoc: MethodDoc, paramName: String, value: Any?, desc: String?, required: Boolean) { +fun MethodDocBuilderListener.addParam( + exportContext: ExportContext, + methodDoc: MethodDoc, paramName: String, value: Any?, desc: String?, required: Boolean +) { addParam(exportContext, methodDoc, paramName, value, required, desc) } -fun MethodDocBuilderListener.addParam(exportContext: ExportContext, - methodDoc: MethodDoc, paramName: String, value: Any?, required: Boolean, desc: String?) { +fun MethodDocBuilderListener.addParam( + exportContext: ExportContext, + methodDoc: MethodDoc, paramName: String, value: Any?, required: Boolean, desc: String? +) { val param = Param() param.name = paramName param.value = value diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/RequestBuilderListener.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/RequestBuilderListener.kt index e8586de15..36940b5d1 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/RequestBuilderListener.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/core/RequestBuilderListener.kt @@ -1,9 +1,11 @@ package com.itangcent.idea.plugin.api.export.core -import com.google.inject.ImplementedBy +import com.google.inject.ProvidedBy +import com.google.inject.Singleton import com.itangcent.common.model.* +import com.itangcent.spi.SpiCompositeBeanProvider -@ImplementedBy(CompositeRequestBuilderListener::class) +@ProvidedBy(RequestBuilderListenerCompositeProvider::class) interface RequestBuilderListener { fun setName( @@ -128,6 +130,9 @@ interface RequestBuilderListener { fun processCompleted(methodExportContext: MethodExportContext, request: Request) } +@Singleton +class RequestBuilderListenerCompositeProvider : SpiCompositeBeanProvider() + //region utils------------------------------------------------------------------ fun RequestBuilderListener.addParam( diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/DefaultSpringControllerAnnotationResolver.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/DefaultSpringControllerAnnotationResolver.kt deleted file mode 100644 index 88ace242e..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/DefaultSpringControllerAnnotationResolver.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.itangcent.idea.plugin.api.export.spring - -import com.google.inject.Singleton -import com.intellij.psi.PsiClass -import com.itangcent.spi.SpiCompositeLoader - -/* - * This class provides a default implementation for resolving whether a given PsiClass - * has a Spring controller annotation. It delegates the resolution to a composite - * loader that can handle multiple strategies. - */ -@Singleton -class DefaultSpringControllerAnnotationResolver : SpringControllerAnnotationResolver { - - private val delegate: SpringControllerAnnotationResolver by lazy { - SpiCompositeLoader.loadComposite() - } - - override fun hasControllerAnnotation(psiClass: PsiClass): Boolean { - return delegate.hasControllerAnnotation(psiClass) - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringControllerAnnotationResolver.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringControllerAnnotationResolver.kt index 49dd3df07..1899f279b 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringControllerAnnotationResolver.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/spring/SpringControllerAnnotationResolver.kt @@ -1,14 +1,20 @@ package com.itangcent.idea.plugin.api.export.spring -import com.google.inject.ImplementedBy +import com.google.inject.ProvidedBy +import com.google.inject.Singleton import com.intellij.psi.PsiClass +import com.itangcent.spi.SpiCompositeBeanProvider /* * This interface defines a contract for resolving whether a given PsiClass * has a Spring controller annotation. It is implemented by various classes * to provide different strategies for determining the presence of controller annotations. */ -@ImplementedBy(DefaultSpringControllerAnnotationResolver::class) +@ProvidedBy(SpringControllerAnnotationResolverCompositeProvider::class) interface SpringControllerAnnotationResolver { fun hasControllerAnnotation(psiClass: PsiClass): Boolean -} \ No newline at end of file +} + +@Singleton +class SpringControllerAnnotationResolverCompositeProvider : + SpiCompositeBeanProvider() \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt index e125a41f3..fec8ff668 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt @@ -381,10 +381,6 @@ open class SuvApiExporter { builder.bindInstance(ExportChannel::class, ExportChannel.of("yapi")) builder.bindInstance(ExportDoc::class, ExportDoc.of("request", "methodDoc")) - builder.bind(MethodDocBuilderListener::class) { - it.with(CompositeMethodDocBuilderListener::class).singleton() - } - builder.bindInstance("file.save.default", "api.json") builder.bindInstance("file.save.last.location.key", "com.itangcent.api.export.path") diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/translation/APITranslationHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/translation/APITranslationHelper.kt index 3de52b7b5..6b4accd33 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/translation/APITranslationHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/translation/APITranslationHelper.kt @@ -12,9 +12,7 @@ import com.itangcent.common.model.Request import com.itangcent.common.utils.GsonUtils import com.itangcent.common.utils.notNullOrEmpty import com.itangcent.idea.plugin.settings.helper.AISettingsHelper -import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.logger.Logger -import com.itangcent.spi.SpiCompositeLoader import java.util.concurrent.ConcurrentHashMap /** @@ -32,11 +30,7 @@ class APITranslationHelper { private lateinit var aiSettingsHelper: AISettingsHelper @Inject - private lateinit var actionContext: ActionContext - - private val aiService: AIService by lazy { - SpiCompositeLoader.load(actionContext).first() - } + private lateinit var aiService: AIService // Cache for translated content to avoid duplicate translations private val translationCache = ConcurrentHashMap() diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/AbstractYapiApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/AbstractYapiApiHelper.kt index 54608fc38..b9c4b5dd2 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/AbstractYapiApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/AbstractYapiApiHelper.kt @@ -10,7 +10,6 @@ import com.itangcent.common.utils.trySet import com.itangcent.idea.plugin.api.export.core.Folder import com.itangcent.idea.plugin.rule.SuvRuleContext import com.itangcent.idea.plugin.settings.helper.YapiSettingsHelper -import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.config.rule.RuleComputer import com.itangcent.intellij.extend.asHashMap import com.itangcent.intellij.extend.asMap @@ -33,13 +32,10 @@ abstract class AbstractYapiApiHelper : YapiApiHelper { protected lateinit var logger: Logger @Inject - private val configReader: ConfigReader? = null + protected lateinit var ruleComputer: RuleComputer @Inject - protected val ruleComputer: RuleComputer? = null - - @Inject - protected val httpClientProvide: HttpClientProvider? = null + protected lateinit var httpClientProvide: HttpClientProvider @Volatile var init: Boolean = false @@ -249,7 +245,7 @@ abstract class AbstractYapiApiHelper : YapiApiHelper { synchronized(this) { if (!init) { - ruleComputer!!.computer( + ruleComputer.computer( YapiClassExportRuleKeys.BEFORE_EXPORT, SuvRuleContext(), null ) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt index 7e5b2b0a7..a371dca79 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt @@ -18,10 +18,8 @@ import com.itangcent.intellij.extend.asJsonElement import com.itangcent.intellij.extend.asList import com.itangcent.intellij.extend.asMutableList import com.itangcent.intellij.extend.sub -import com.itangcent.spi.SpiCompositeLoader import org.apache.commons.lang3.StringUtils import org.apache.http.entity.ContentType -import kotlin.collections.set import kotlin.concurrent.withLock @Singleton @@ -39,6 +37,9 @@ open class DefaultYapiApiHelper : AbstractYapiApiHelper(), YapiApiHelper { @Inject internal lateinit var actionContext: ActionContext + @Inject + private lateinit var saveInterceptor: YapiSaveInterceptor + override fun getApiInfo(token: String, id: String): JsonObject? { val url = "${yapiSettingsHelper.getServer()}$GET_INTERFACE?token=$token&id=$id" return GsonUtils.parseToJsonTree(getByApi(url)) @@ -73,10 +74,6 @@ open class DefaultYapiApiHelper : AbstractYapiApiHelper(), YapiApiHelper { return jsonArray } - private val saveInterceptor: YapiSaveInterceptor by lazy { - SpiCompositeLoader.loadComposite() - } - override fun saveApiInfo(apiInfo: HashMap): Boolean { if (saveInterceptor.beforeSaveApi(this, apiInfo) == false) { return false @@ -88,7 +85,7 @@ open class DefaultYapiApiHelper : AbstractYapiApiHelper(), YapiApiHelper { } try { - val returnValue = httpClientProvide!!.getHttpClient() + val returnValue = httpClientProvide.getHttpClient() .post(yapiSettingsHelper.getServer(false) + SAVE_API) .contentType(ContentType.APPLICATION_JSON) .body(apiInfo) @@ -174,7 +171,7 @@ open class DefaultYapiApiHelper : AbstractYapiApiHelper(), YapiApiHelper { override fun addCart(projectId: String, token: String, name: String, desc: String): Boolean { try { - val returnValue = httpClientProvide!!.getHttpClient() + val returnValue = httpClientProvide.getHttpClient() .post(yapiSettingsHelper.getServer(false) + ADD_CART) .contentType(ContentType.APPLICATION_JSON) .body( diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt index 30056e53f..ec747f629 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt @@ -1,5 +1,7 @@ package com.itangcent.idea.plugin.api.export.yapi +import com.google.inject.ProvidedBy +import com.google.inject.Singleton import com.intellij.openapi.ui.Messages import com.itangcent.common.concurrent.ValueHolder import com.itangcent.common.utils.toBool @@ -9,11 +11,13 @@ import com.itangcent.idea.swing.MessagesHelper import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.sub +import com.itangcent.spi.SpiCompositeBeanProvider /** * Workflow interface that allows for customized yapi save action. */ -internal interface YapiSaveInterceptor { +@ProvidedBy(YapiSaveInterceptorCompositeProvider::class) +interface YapiSaveInterceptor { /** * Called before [YapiApiHelper] save an apiInfo to yapi server. * @@ -23,6 +27,9 @@ internal interface YapiSaveInterceptor { fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean? } +@Singleton +class YapiSaveInterceptorCompositeProvider : SpiCompositeBeanProvider() + /** * Immutable [YapiSaveInterceptor] that always return true. * This indicates that the apis will to be always diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/infer/AIMethodInferHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/infer/AIMethodInferHelper.kt index 4d0c69457..62297cbdb 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/infer/AIMethodInferHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/infer/AIMethodInferHelper.kt @@ -19,7 +19,6 @@ import com.itangcent.intellij.jvm.PsiClassHelper import com.itangcent.intellij.jvm.PsiResolver import com.itangcent.intellij.logger.Logger import com.itangcent.intellij.psi.PsiClassUtils -import com.itangcent.spi.SpiCompositeLoader import java.util.concurrent.ConcurrentHashMap /** @@ -68,9 +67,8 @@ class AIMethodInferHelper : MethodInferHelper { @Inject private lateinit var cacheSwitcher: CacheSwitcher - private val aiService: AIService by lazy { - SpiCompositeLoader.load(actionContext).first() - } + @Inject + private lateinit var aiService: AIService // Cache for storing AI inference results to avoid repeated API calls private val inferCache: ConcurrentHashMap = ConcurrentHashMap() diff --git a/idea-plugin/src/main/kotlin/com/itangcent/spi/AbstractSpiBeanProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/spi/AbstractSpiBeanProvider.kt new file mode 100644 index 000000000..5b54e16ca --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/spi/AbstractSpiBeanProvider.kt @@ -0,0 +1,76 @@ +package com.itangcent.spi + +import com.google.inject.Provider +import com.itangcent.intellij.context.ActionContext +import com.itangcent.utils.findGenericType +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import kotlin.reflect.KClass + +/** + * Abstract base class for SPI bean providers. + * This class provides common functionality for both SpiSingleBeanProvider and SpiCompositeBeanProvider. + */ +abstract class AbstractSpiBeanProvider : Provider { + + // Get the KClass from the generic type parameter + @Suppress("UNCHECKED_CAST") + protected val kClass: KClass by lazy { + this::class.java.findGenericType(AbstractSpiBeanProvider::class.java) + } + + /** + * Load the bean(s) from the SPI registry. + * Subclasses should implement this to load either a single bean or a composite of beans. + */ + protected abstract fun loadBean(actionContext: ActionContext, kClass: KClass): T + + @Suppress("UNCHECKED_CAST") + override fun get(): T { + val context = ActionContext.getContext() + return if (context != null) { + // ActionContext is prepared, load the bean directly + loadBean(context, kClass) + } else { + // ActionContext is not prepared, create a proxy + createLazyLoadingProxy() + } + } + + @Suppress("UNCHECKED_CAST") + private fun createLazyLoadingProxy(): T { + return Proxy.newProxyInstance( + kClass.java.classLoader, + arrayOf(kClass.java), + LazyLoadingInvocationHandler(kClass) + ) as T + } + + /** + * An InvocationHandler that lazily loads the actual bean when a method is invoked. + * It checks for ActionContext availability at method invocation time. + */ + private inner class LazyLoadingInvocationHandler( + private val kClass: KClass + ) : InvocationHandler { + + @Suppress("UNCHECKED_CAST") + private val delegate: T by lazy { + val context = ActionContext.getContext() + if (context == null) { + throw IllegalStateException("ActionContext is not prepared when attempting to use ${kClass.qualifiedName}") + } + loadBean(context, kClass) + } + + override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + // Invoke the method on the actual delegate + return if (args == null) { + method.invoke(delegate) + } else { + method.invoke(delegate, *args) + } + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeBeanProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeBeanProvider.kt new file mode 100644 index 000000000..c4ba69dba --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeBeanProvider.kt @@ -0,0 +1,34 @@ +package com.itangcent.spi + +import com.itangcent.common.spi.ProxyBean +import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.jvm.spi.ContextProxyBean +import java.lang.reflect.Proxy +import kotlin.reflect.KClass + +/** + * A provider that loads all available implementations of a service type and returns a composite proxy. + * The composite proxy delegates method calls to all implementations in order. + */ +abstract class SpiCompositeBeanProvider : AbstractSpiBeanProvider() { + + @Suppress("UNCHECKED_CAST") + override fun loadBean(actionContext: ActionContext, kClass: KClass): T { + val services = SpiCompositeLoader.load(actionContext, kClass) + if (services.isEmpty()) { + throw IllegalStateException("No services found for ${kClass.qualifiedName}") + } + + // If there's only one service, return it directly + if (services.size == 1) { + return services[0] + } + + // Create a composite proxy that delegates to all services + return Proxy.newProxyInstance( + kClass.java.classLoader, + arrayOf(kClass.java), + ProxyBean(arrayOf(*services)) + ) as T + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeLoader.kt b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeLoader.kt index fe93352d6..a78d3e057 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeLoader.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiCompositeLoader.kt @@ -1,18 +1,20 @@ package com.itangcent.spi +import com.itangcent.common.logger.Log import com.itangcent.common.spi.SpiUtils import com.itangcent.condition.ConditionEvaluator import com.itangcent.condition.Exclusion import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.jvm.spi.ContextProxyBean import com.itangcent.order.order +import com.itangcent.utils.ArrayKit import com.itangcent.utils.superClasses import java.lang.reflect.Proxy import java.util.* import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation -object SpiCompositeLoader { +object SpiCompositeLoader : Log() { inline fun loadComposite(): S { val cls = S::class @@ -31,15 +33,24 @@ object SpiCompositeLoader { * All the beans returned are all initialized by [actionContext]. */ inline fun load(actionContext: ActionContext): Array { + return load(actionContext, T::class) + } + + /** + * Load available beans of special type [T]. + * All the beans returned are all initialized by [actionContext]. + */ + @Suppress("UNCHECKED_CAST") + fun load(actionContext: ActionContext, tClass: KClass): Array { val conditionEvaluator = actionContext.instance(ConditionEvaluator::class) - var matchedClasses = SpiUtils.loadServices(T::class)!! + var matchedClasses = SpiUtils.loadServices(tClass)!! .map { it::class } .filter { conditionEvaluator.matches(actionContext, it) } if (matchedClasses.isEmpty()) { - return emptyArray() + return ArrayKit.emptyArray(tClass) } //support @Exclusion @@ -49,14 +60,15 @@ object SpiCompositeLoader { } if (matchedClasses.isEmpty()) { - return emptyArray() + return ArrayKit.emptyArray(tClass) } - LOG.info("matched ${T::class}:${matchedClasses}") - return matchedClasses + LOG.info("matched $tClass:${matchedClasses}") + val instances = matchedClasses .map { actionContext.instance(it) } .sortedBy { it.order() } - .toTypedArray() + + return ArrayKit.toArray(tClass, instances) } /** @@ -72,6 +84,4 @@ object SpiCompositeLoader { } return exclusions } - - val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(SpiCompositeLoader::class.java) } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiSingleBeanProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiSingleBeanProvider.kt new file mode 100644 index 000000000..a946a2521 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/spi/SpiSingleBeanProvider.kt @@ -0,0 +1,18 @@ +package com.itangcent.spi + +import com.itangcent.intellij.context.ActionContext +import kotlin.reflect.KClass + +/** + * A provider that loads the first available implementation of a service type. + */ +abstract class SpiSingleBeanProvider : AbstractSpiBeanProvider() { + + override fun loadBean(actionContext: ActionContext, kClass: KClass): T { + val services = SpiCompositeLoader.load(actionContext, kClass) + if (services.isEmpty()) { + throw IllegalStateException("No services found for ${kClass.qualifiedName}") + } + return services.first() as T + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt deleted file mode 100644 index 58176a378..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.itangcent.suv.http - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.itangcent.http.HttpClient -import com.itangcent.intellij.context.ActionContext -import com.itangcent.spi.SpiCompositeLoader - - -/** - * The default implementation of the [HttpClientProvider] interface - * which automatically loads an implementation of the HttpClientProvider interface using the service provider interface (SPI) mechanism. - */ -@Singleton -open class DefaultHttpClientProvider : AbstractHttpClientProvider() { - - @Inject - private lateinit var actionContext: ActionContext - - private val httpClientProvider: HttpClientProvider by lazy { - SpiCompositeLoader.load(actionContext).firstOrNull() - ?: actionContext.instance(ApacheHttpClientProvider::class) - } - - override fun buildHttpClient(): HttpClient { - return httpClientProvider.getHttpClient() - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt index 4ea6ac7cb..1e5e503e3 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt @@ -1,7 +1,10 @@ package com.itangcent.suv.http import com.google.inject.ImplementedBy +import com.google.inject.ProvidedBy +import com.google.inject.Singleton import com.itangcent.http.HttpClient +import com.itangcent.spi.SpiSingleBeanProvider /** @@ -13,7 +16,7 @@ import com.itangcent.http.HttpClient * @author tangcent * @date 2024/05/08 */ -@ImplementedBy(DefaultHttpClientProvider::class) +@ProvidedBy(HttpClientProviderProvider::class) interface HttpClientProvider { /** @@ -21,3 +24,6 @@ interface HttpClientProvider { */ fun getHttpClient(): HttpClient } + +@Singleton +class HttpClientProviderProvider : SpiSingleBeanProvider() \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/utils/ArrayKit.kt b/idea-plugin/src/main/kotlin/com/itangcent/utils/ArrayKit.kt new file mode 100644 index 000000000..b773e5f5f --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/utils/ArrayKit.kt @@ -0,0 +1,57 @@ +package com.itangcent.utils + +import kotlin.reflect.KClass + +/** + * Utility class for array operations. + */ +object ArrayKit { + + /** + * Creates a typed array of the specified class with the given elements. + * + * @param clazz The class of the array elements + * @param elements The elements to be placed in the array + * @return A typed array containing the elements + */ + @Suppress("UNCHECKED_CAST") + fun toArray(clazz: Class, elements: List): Array { + return java.lang.reflect.Array.newInstance(clazz, elements.size).apply { + elements.forEachIndexed { index, element -> + java.lang.reflect.Array.set(this, index, element) + } + } as Array + } + + /** + * Creates a typed array of the specified KClass with the given elements. + * + * @param kClass The KClass of the array elements + * @param elements The elements to be placed in the array + * @return A typed array containing the elements + */ + fun toArray(kClass: KClass, elements: List): Array { + return toArray(kClass.java, elements) + } + + /** + * Creates an empty typed array of the specified class. + * + * @param clazz The class of the array elements + * @return An empty typed array + */ + @Suppress("UNCHECKED_CAST") + fun emptyArray(clazz: Class): Array { + return java.lang.reflect.Array.newInstance(clazz, 0) as Array + } + + /** + * Creates an empty typed array of the specified KClass. + * + * @param kClass The KClass of the array elements + * @return An empty typed array + */ + fun emptyArray(kClass: KClass): Array { + return emptyArray(kClass.java) + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/utils/ReflectionKit.kt b/idea-plugin/src/main/kotlin/com/itangcent/utils/ReflectionKit.kt new file mode 100644 index 000000000..141b44cf4 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/utils/ReflectionKit.kt @@ -0,0 +1,118 @@ +package com.itangcent.utils + +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +/** + * Utility class for reflection operations. + */ +object ReflectionKit + +/** + * Recursively find the generic type parameter T from the class hierarchy. + * + * @param targetClass The target class to check against (e.g., a base class or interface) + * @param typeIndex The index of the type parameter to retrieve (default is 0) + * @return The KClass representing the generic type parameter T + */ +@Suppress("UNCHECKED_CAST") +fun Class<*>.findGenericType( + targetClass: Class<*>, + typeIndex: Int = 0 +): KClass { + val superclass = this.genericSuperclass + ?: throw IllegalStateException("Cannot determine the generic type for ${this.name}") + + return when { + // If we found a ParameterizedType (like BaseClass) + superclass is ParameterizedType -> { + val rawType = superclass.rawType as Class<*> + + // Check if this is the target class or a subclass of it + if (targetClass.isAssignableFrom(rawType)) { + val typeArguments = superclass.actualTypeArguments + if (typeIndex >= typeArguments.size) { + throw IllegalArgumentException("Type index $typeIndex is out of bounds for class $this with ${typeArguments.size} type parameters") + } + + val typeArgument = typeArguments[typeIndex] + if (typeArgument is Class<*>) { + return typeArgument.kotlin as KClass + } else if (typeArgument is ParameterizedType) { + // Handle nested generic types like BaseClass> + return (typeArgument.rawType as Class<*>).kotlin as KClass + } + } + + // If we're at a different generic class in the hierarchy, continue searching upward + rawType.findGenericType(targetClass, typeIndex) + } + // If we found a regular Class, continue searching upward + else -> (superclass as Class<*>).findGenericType(targetClass, typeIndex) + } +} + +/** + * Find the generic type parameter T from a class that implements an interface. + * + * @param interfaceClass The interface class to check against + * @param typeIndex The index of the type parameter to retrieve (default is 0) + * @return The KClass representing the generic type parameter T, or null if not found + */ +@Suppress("UNCHECKED_CAST") +fun Class<*>.findGenericTypeFromInterface( + interfaceClass: Class<*>, + typeIndex: Int = 0 +): KClass? { + // Check all generic interfaces implemented by this class + for (genericInterface in this.genericInterfaces) { + if (genericInterface is ParameterizedType) { + val rawType = genericInterface.rawType as Class<*> + if (interfaceClass.isAssignableFrom(rawType)) { + val typeArguments = genericInterface.actualTypeArguments + if (typeIndex < typeArguments.size) { + val typeArgument = typeArguments[typeIndex] + if (typeArgument is Class<*>) { + return typeArgument.kotlin as KClass + } else if (typeArgument is ParameterizedType) { + return (typeArgument.rawType as Class<*>).kotlin as KClass + } + } + } + } + } + + // If not found in direct interfaces, check the superclass + val superclass = this.superclass ?: return null + return superclass.findGenericTypeFromInterface(interfaceClass, typeIndex) +} + +/** + * Recursively find the generic type parameter T from the class hierarchy using KClass. + * + * @param targetClass The target class to check against (e.g., a base class or interface) + * @param typeIndex The index of the type parameter to retrieve (default is 0) + * @return The KClass representing the generic type parameter T + */ +@Suppress("UNCHECKED_CAST") +fun KClass<*>.findGenericType( + targetClass: KClass<*>, + typeIndex: Int = 0 +): KClass { + return this.java.findGenericType(targetClass.java, typeIndex) +} + +/** + * Find the generic type parameter T from a class that implements an interface using KClass. + * + * @param interfaceClass The interface class to check against + * @param typeIndex The index of the type parameter to retrieve (default is 0) + * @return The KClass representing the generic type parameter T, or null if not found + */ +@Suppress("UNCHECKED_CAST") +fun KClass<*>.findGenericTypeFromInterface( + interfaceClass: KClass<*>, + typeIndex: Int = 0 +): KClass? { + return this.java.findGenericTypeFromInterface(interfaceClass.java, typeIndex) +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListenerTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListenerTest.kt deleted file mode 100644 index e87f7bbf8..000000000 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/core/CompositeMethodDocBuilderListenerTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.itangcent.idea.plugin.api.export.core - -import com.google.inject.Inject -import com.itangcent.common.model.MethodDoc -import com.itangcent.intellij.context.ActionContextBuilder -import com.itangcent.intellij.extend.guice.singleton -import com.itangcent.intellij.extend.guice.with -import com.itangcent.mock.AdvancedContextTest -import com.itangcent.mock.FakeExportContext -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals - -/** - * Test case of [CompositeMethodDocBuilderListener] - */ -internal class CompositeMethodDocBuilderListenerTest : AdvancedContextTest() { - - @Inject - private lateinit var methodDocBuilderListener: MethodDocBuilderListener - - private lateinit var methodDoc: MethodDoc - - override fun bind(builder: ActionContextBuilder) { - super.bind(builder) - builder.bind(MethodDocBuilderListener::class) { - it.with(CompositeMethodDocBuilderListener::class).singleton() - } - } - - @BeforeEach - fun init() { - methodDoc = MethodDoc() - } - - @Test - fun testSetName() { - methodDocBuilderListener.setName( - FakeExportContext.INSTANCE, - methodDoc, "test" - ) - assertEquals("test", methodDoc.name) - } - - @Test - fun testAppendDesc() { - methodDocBuilderListener.appendDesc( - FakeExportContext.INSTANCE, - methodDoc, "abc" - ) - assertEquals("abc", methodDoc.desc) - methodDocBuilderListener.appendDesc( - FakeExportContext.INSTANCE, - methodDoc, "def" - ) - assertEquals("abcdef", methodDoc.desc) - } - - @Test - fun testAddParam() { - methodDocBuilderListener.addParam( - FakeExportContext.INSTANCE, - methodDoc, "token", "123", "token for auth", true - ) - methodDoc.params!![0].let { - assertEquals("token", it.name) - assertEquals("123", it.value) - assertEquals("token for auth", it.desc) - assertEquals(true, it.required) - } - } - - @Test - fun testSetRet() { - methodDocBuilderListener.setRet( - FakeExportContext.INSTANCE, - methodDoc, "ret" - ) - assertEquals("ret", methodDoc.ret) - } - - @Test - fun testAppendRetDesc() { - methodDocBuilderListener.appendRetDesc( - FakeExportContext.INSTANCE, - methodDoc, "abc" - ) - assertEquals("abc", methodDoc.retDesc) - methodDocBuilderListener.appendRetDesc( - FakeExportContext.INSTANCE, - methodDoc, "def" - ) - assertEquals("abc\ndef", methodDoc.retDesc) - } -} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanSpringClassExporterBaseTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanSpringClassExporterBaseTest.kt index 652851ab3..f971ec581 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanSpringClassExporterBaseTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanSpringClassExporterBaseTest.kt @@ -4,8 +4,6 @@ import com.google.inject.Inject import com.intellij.psi.PsiClass import com.itangcent.idea.plugin.api.export.ExportChannel import com.itangcent.idea.plugin.api.export.core.ClassExporter -import com.itangcent.idea.plugin.api.export.core.CompositeRequestBuilderListener -import com.itangcent.idea.plugin.api.export.core.RequestBuilderListener import com.itangcent.idea.plugin.api.export.spring.SpringRequestClassExporter import com.itangcent.idea.plugin.settings.SettingBinder import com.itangcent.idea.plugin.settings.Settings diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSpringClassExporterBaseTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSpringClassExporterBaseTest.kt index 1bf6049db..5ee08e547 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSpringClassExporterBaseTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSpringClassExporterBaseTest.kt @@ -5,8 +5,6 @@ import com.intellij.psi.PsiClass import com.itangcent.idea.plugin.api.export.ExportChannel import com.itangcent.idea.plugin.api.export.core.AdditionalParseHelper import com.itangcent.idea.plugin.api.export.core.ClassExporter -import com.itangcent.idea.plugin.api.export.core.CompositeRequestBuilderListener -import com.itangcent.idea.plugin.api.export.core.RequestBuilderListener import com.itangcent.idea.plugin.settings.SettingBinder import com.itangcent.idea.plugin.settings.Settings import com.itangcent.idea.utils.RuleComputeListenerRegistry diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/infer/MethodInferHelperFactoryTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/infer/MethodInferHelperFactoryTest.kt index 4c270d712..bc4912ba4 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/infer/MethodInferHelperFactoryTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/infer/MethodInferHelperFactoryTest.kt @@ -38,10 +38,7 @@ class MethodInferHelperFactoryTest : PluginContextLightCodeInsightFixtureTestCas fun testGetAIMethodInferHelper() { settings.aiEnable = true - - // Verify it's the default helper - assertIs(methodInferHelperFactory.getMethodInferHelper()) - + settings.aiProvider = "OpenAI" settings.aiModel = "gpt-4" settings.aiToken = "test-token-123" // Verify it's the AI helper diff --git a/idea-plugin/src/test/kotlin/com/itangcent/spi/SpiCompositeBeanProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/spi/SpiCompositeBeanProviderTest.kt new file mode 100644 index 000000000..c3682ba21 --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/spi/SpiCompositeBeanProviderTest.kt @@ -0,0 +1,117 @@ +package com.itangcent.spi + +import com.itangcent.common.spi.ProxyBean +import com.itangcent.intellij.context.ActionContext +import com.itangcent.mock.AdvancedContextTest +import com.itangcent.order.Order +import com.itangcent.order.Ordered +import org.junit.jupiter.api.Test +import java.lang.reflect.Proxy +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Test case for [SpiCompositeBeanProvider] + */ +internal class SpiCompositeBeanProviderTest : AdvancedContextTest() { + + @Test + fun testCompositeProvider() { + // Create mock implementations + val service1 = TestService1() + val service2 = TestService2() + val services = arrayOf(service1, service2) + + // Create a test provider that directly returns our services + val provider = TestCompositeProviderWithDirectServices(services) + + // Get the composite service from the provider + val compositeService = provider.get() + + // Test the composite service with string processing + assertEquals("TestService1: test data", compositeService.process("test data")) + assertEquals("TestService1: special", compositeService.process("special")) + + // Test the composite service with list processing + val callTracker = mutableListOf() + compositeService.processWithList(callTracker, "test data") + + // Verify both services were called + assertTrue(callTracker.contains("TestService1")) + assertTrue(callTracker.contains("TestService2")) + assertEquals(2, callTracker.size) + + // Test with special data + callTracker.clear() + compositeService.processWithList(callTracker, "special") + assertTrue(callTracker.contains("TestService1-special")) + assertTrue(callTracker.contains("TestService2-special")) + assertEquals(2, callTracker.size) + } + + // Test interfaces and classes + interface TestService { + fun process(data: String): String + + // method that updates a list to track which implementations were called + fun processWithList(callTracker: MutableList, data: String) + } + + @Order(Ordered.HIGHEST_PRECEDENCE) + class TestService1 : TestService { + override fun process(data: String): String { + return if (data == "special") { + "TestService1: special" + } else { + "TestService1: $data" + } + } + + override fun processWithList(callTracker: MutableList, data: String) { + if (data == "special") { + callTracker.add("TestService1-special") + } else { + callTracker.add("TestService1") + } + } + } + + @Order(Ordered.LOWEST_PRECEDENCE) + class TestService2 : TestService { + override fun process(data: String): String { + return if (data == "special") { + "TestService2 doesn't handle special" + } else { + "TestService2: $data" + } + } + + override fun processWithList(callTracker: MutableList, data: String) { + if (data == "special") { + callTracker.add("TestService2-special") + } else { + callTracker.add("TestService2") + } + } + } + + // Custom implementation that directly returns our test services + class TestCompositeProviderWithDirectServices(private val services: Array) : + SpiCompositeBeanProvider() { + @Suppress("UNCHECKED_CAST") + override fun loadBean(actionContext: ActionContext, kClass: KClass): TestService { + // If there's only one service, return it directly + if (services.size == 1) { + return services[0] + } + + // Create a composite proxy that delegates to all services + return Proxy.newProxyInstance( + kClass.java.classLoader, + arrayOf(kClass.java), + ProxyBean(arrayOf(*services)) + ) as TestService + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt index 99f5870e2..f9d4b5143 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt @@ -5,6 +5,7 @@ import com.itangcent.http.ApacheCookie import com.itangcent.http.ApacheHttpClient import com.itangcent.http.asApacheCookie import com.itangcent.idea.plugin.settings.HttpClientType +import com.itangcent.intellij.context.ActionContextBuilder import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import kotlin.test.assertContentEquals @@ -13,12 +14,10 @@ import kotlin.test.assertNull import kotlin.test.assertTrue /** - * Test case of [DefaultHttpClientProvider] + * Test case of [HttpClientProvider] */ internal abstract class DefaultHttpClientProviderTest : HttpClientProviderTest() { - override val httpClientProviderClass get() = DefaultHttpClientProvider::class - override fun customConfig(): String { return "http.call.before=groovy:logger.info(\"call:\"+request.url())\nhttp.call.after=groovy:logger.info(\"response:\"+response.string())\nhttp.timeOut=3" } @@ -158,7 +157,8 @@ internal class UnsafeSslApacheHttpClientProviderTest : DefaultHttpClientProvider } internal class OkHttpClientProviderTest : DefaultHttpClientProviderTest() { - override fun setUp() { + override fun bind(builder: ActionContextBuilder) { + super.bind(builder) settings.httpClient = HttpClientType.OKHTTP.value } @@ -171,7 +171,8 @@ internal class OkHttpClientProviderTest : DefaultHttpClientProviderTest() { } internal class UnsafeSslOkHttpClientProviderTest : DefaultHttpClientProviderTest() { - override fun setUp() { + override fun bind(builder: ActionContextBuilder) { + super.bind(builder) settings.httpClient = HttpClientType.OKHTTP.value settings.unsafeSsl = true } @@ -199,8 +200,6 @@ internal class IllegalHttpClientProviderTest : DefaultHttpClientProviderTest() { internal class NonConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { - override val httpClientProviderClass get() = DefaultHttpClientProvider::class - @Test fun `test buildHttpClient`() { // Build an instance of HttpClient using the provider. @@ -212,9 +211,6 @@ internal class NonConfigConfigurableHttpClientProviderTest : HttpClientProviderT } internal class IllegalConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { - - override val httpClientProviderClass get() = DefaultHttpClientProvider::class - override fun customConfig(): String { return "http.timeOut=illegal" } diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt index e44a0a4ab..6763daf27 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt @@ -30,13 +30,10 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { @Inject protected lateinit var httpClientProvider: HttpClientProvider - abstract val httpClientProviderClass: KClass - protected val settings = Settings() override fun bind(builder: ActionContextBuilder) { super.bind(builder) - builder.bind(HttpClientProvider::class) { it.with(httpClientProviderClass) } builder.bind(SettingBinder::class) { it.toInstance(SettingBinderAdaptor(settings.also { settings -> settings.trustHosts = arrayOf( diff --git a/idea-plugin/src/test/kotlin/com/itangcent/utils/ReflectionKitTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/utils/ReflectionKitTest.kt new file mode 100644 index 000000000..f5814a350 --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/utils/ReflectionKitTest.kt @@ -0,0 +1,93 @@ +package com.itangcent.utils + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +/** + * Test case for [ReflectionKit] + */ +internal class ReflectionKitTest { + + @Test + fun testFindGenericType() { + // Test finding generic type from direct subclass + val directSubclassType = DirectSubclass::class.java + .findGenericType(TestBaseClass::class.java) + assertEquals(TestService::class, directSubclassType) + + // Test finding generic type from indirect subclass + val indirectSubclassType = IndirectSubclass::class.java + .findGenericType(TestBaseClass::class.java) + assertEquals(TestService::class, indirectSubclassType) + + // Test finding generic type from nested generic type + val nestedGenericType = NestedGenericSubclass::class.java + .findGenericType>(TestBaseClass::class.java) + assertEquals(List::class, nestedGenericType) + + // Test finding second type parameter + val secondTypeParam = MultiTypeParamClass::class.java + .findGenericType(MultiTypeParamBaseClass::class.java, 1) + assertEquals(String::class, secondTypeParam) + } + + @Test + fun testFindGenericTypeFromInterface() { + // Test finding generic type from direct interface implementation + val directInterfaceType = InterfaceImpl::class.java + .findGenericTypeFromInterface(TestGenericInterface::class.java) + assertEquals(TestService::class, directInterfaceType) + + // Test finding generic type from indirect interface implementation + val indirectInterfaceType = IndirectInterfaceImpl::class.java + .findGenericTypeFromInterface(TestGenericInterface::class.java) + assertEquals(TestService::class, indirectInterfaceType) + + // Test finding generic type from interface with multiple type parameters + val secondInterfaceTypeParam = MultiTypeParamInterfaceImpl::class.java + .findGenericTypeFromInterface(MultiTypeParamInterface::class.java, 1) + assertEquals(String::class, secondInterfaceTypeParam) + + // Test when interface is not found + val notFoundType = NoInterface::class.java + .findGenericTypeFromInterface(TestGenericInterface::class.java) + assertNull(notFoundType) + } + + // Test classes for class hierarchy + interface TestService + + open class TestBaseClass + + // Direct subclass of TestBaseClass + class DirectSubclass : TestBaseClass() + + // Indirect subclass of TestBaseClass + open class IntermediateClass : TestBaseClass() + class IndirectSubclass : IntermediateClass() + + // Nested generic type + class NestedGenericSubclass : TestBaseClass>() + + // Multiple type parameters + open class MultiTypeParamBaseClass + class MultiTypeParamClass : MultiTypeParamBaseClass() + + // Test classes for interface hierarchy + interface TestGenericInterface + + // Direct interface implementation + class InterfaceImpl : TestGenericInterface + + // Indirect interface implementation + interface IntermediateInterface : TestGenericInterface + class IndirectInterfaceImpl : IntermediateInterface + + // Interface with multiple type parameters + interface MultiTypeParamInterface + class MultiTypeParamInterfaceImpl : MultiTypeParamInterface + + // Class with no relevant interface + class NoInterface +} \ No newline at end of file