diff --git a/.gitignore b/.gitignore index 6c24a9ef0..28d7d7e73 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ gradle/wrapper/ \.DS_Store -plugin/ +common-api/build/ + +/plugin/ \ No newline at end of file diff --git a/common/build.gradle b/common-api/build.gradle similarity index 93% rename from common/build.gradle rename to common-api/build.gradle index 3d8c58afa..419904a99 100644 --- a/common/build.gradle +++ b/common-api/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.11' + ext.kotlin_version = '1.3.21' repositories { mavenCentral() diff --git a/common/src/main/kotlin/com/itangcent/common/constant/Attrs.kt b/common-api/src/main/kotlin/com/itangcent/common/constant/Attrs.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/constant/Attrs.kt rename to common-api/src/main/kotlin/com/itangcent/common/constant/Attrs.kt diff --git a/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt b/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt new file mode 100644 index 000000000..d19b4609c --- /dev/null +++ b/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt @@ -0,0 +1,10 @@ +package com.itangcent.common.constant + +object HttpMethod { + val NO_METHOD = "ALL" + val GET = "GET" + val POST = "POST" + val DELETE = "DELETE" + val PUT = "PUT" + val PATCH = "PATCH" +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/itangcent/common/exporter/DefaultParseHandle.kt b/common-api/src/main/kotlin/com/itangcent/common/exporter/AbstractParseHandle.kt similarity index 68% rename from common/src/main/kotlin/com/itangcent/common/exporter/DefaultParseHandle.kt rename to common-api/src/main/kotlin/com/itangcent/common/exporter/AbstractParseHandle.kt index 0439fb6d6..76fa8cdfe 100644 --- a/common/src/main/kotlin/com/itangcent/common/exporter/DefaultParseHandle.kt +++ b/common-api/src/main/kotlin/com/itangcent/common/exporter/AbstractParseHandle.kt @@ -3,7 +3,7 @@ package com.itangcent.common.exporter import com.itangcent.common.constant.Attrs import com.itangcent.common.model.* -class DefaultParseHandle : ParseHandle { +abstract class AbstractParseHandle : ParseHandle { override fun setName(request: Request, name: String) { request.name = name } @@ -51,7 +51,7 @@ class DefaultParseHandle : ParseHandle { request.paths!!.add(pathParam) } - override fun setJsonBody(request: Request, body: Any?) { + override fun setJsonBody(request: Request, body: Any?, bodyAttr: String?) { request.body = body } @@ -67,6 +67,31 @@ class DefaultParseHandle : ParseHandle { if (request.headers == null) { request.headers = ArrayList() } + request.headers!!.removeIf { it.name == header.name } request.headers!!.add(header) } + + override fun addResponse(request: Request, response: Response) { + if (request.response == null) { + request.response = ArrayList() + } + request.response!!.add(response) + } + + override fun addResponseHeader(response: Response, header: Header) { + + if (response.headers == null) { + response.headers = ArrayList() + } + response.headers!!.add(header) + } + + override fun setResponseBody(response: Response, bodyType: String, body: Any?) { + response.bodyType = bodyType + response.body = body + } + + override fun setResponseCode(response: Response, code: Int) { + response.code = code + } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/itangcent/common/exporter/ClassExporter.kt b/common-api/src/main/kotlin/com/itangcent/common/exporter/ClassExporter.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/exporter/ClassExporter.kt rename to common-api/src/main/kotlin/com/itangcent/common/exporter/ClassExporter.kt diff --git a/common/src/main/kotlin/com/itangcent/common/exporter/FileExporter.kt b/common-api/src/main/kotlin/com/itangcent/common/exporter/FileExporter.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/exporter/FileExporter.kt rename to common-api/src/main/kotlin/com/itangcent/common/exporter/FileExporter.kt diff --git a/common/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt b/common-api/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt similarity index 67% rename from common/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt rename to common-api/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt index 8729bb220..8eb7c3adb 100644 --- a/common/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt +++ b/common-api/src/main/kotlin/com/itangcent/common/exporter/ParseHandle.kt @@ -6,7 +6,7 @@ interface ParseHandle { fun setName(request: Request, name: String) - fun setMethod(request: Request, httpMethod: String) + fun setMethod(request: Request, method: String) fun setPath(request: Request, path: String) @@ -24,11 +24,27 @@ interface ParseHandle { fun addPathParam(request: Request, pathParam: PathParam) - fun setJsonBody(request: Request, body: Any?) + fun setJsonBody(request: Request, body: Any?, bodyAttr: String?) fun appendDesc(request: Request, desc: String?) fun addHeader(request: Request, header: Header) + + //region response + fun addResponse(request: Request, response: Response) + + fun addResponseHeader(response: Response, header: Header) + + fun setResponseBody(response: Response, bodyType: String, body: Any?) + + fun setResponseCode(response: Response, code: Int) + //endregion + + fun linkToClass(linkClass: Any): String? + + fun linkToMethod(linkMethod: Any): String? + + fun linkToProperty(linkField: Any): String? } //region utils------------------------------------------------------------------ @@ -55,6 +71,16 @@ fun ParseHandle.addFormParam(request: Request, paramName: String, defaultVal: St param.value = defaultVal param.required = required param.desc = desc + param.type = "text" + this.addFormParam(request, param) +} + +fun ParseHandle.addFormFileParam(request: Request, paramName: String, required: Boolean, desc: String?) { + val param = FormParam() + param.name = paramName + param.required = required + param.desc = desc + param.type = "file" this.addFormParam(request, param) } @@ -72,4 +98,11 @@ fun ParseHandle.addPathParam(request: Request, name: String, desc: String) { pathParam.desc = desc this.addPathParam(request, pathParam) } + +fun ParseHandle.addResponseHeader(response: Response, name: String, value: String) { + val header = Header() + header.name = name + header.value = value + addResponseHeader(response, header) +} //endregion utils------------------------------------------------------------------ \ No newline at end of file diff --git a/common/src/main/kotlin/com/itangcent/common/model/FormParam.kt b/common-api/src/main/kotlin/com/itangcent/common/model/FormParam.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/model/FormParam.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/FormParam.kt diff --git a/common/src/main/kotlin/com/itangcent/common/model/Header.kt b/common-api/src/main/kotlin/com/itangcent/common/model/Header.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/model/Header.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/Header.kt diff --git a/common/src/main/kotlin/com/itangcent/common/model/Param.kt b/common-api/src/main/kotlin/com/itangcent/common/model/Param.kt similarity index 83% rename from common/src/main/kotlin/com/itangcent/common/model/Param.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/Param.kt index 5f651477b..f255cd949 100644 --- a/common/src/main/kotlin/com/itangcent/common/model/Param.kt +++ b/common-api/src/main/kotlin/com/itangcent/common/model/Param.kt @@ -3,7 +3,7 @@ package com.itangcent.common.model class Param { var name: String? = null - var value: String? = null + var value: Any? = null var desc: String? = null diff --git a/common/src/main/kotlin/com/itangcent/common/model/PathParam.kt b/common-api/src/main/kotlin/com/itangcent/common/model/PathParam.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/model/PathParam.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/PathParam.kt diff --git a/common/src/main/kotlin/com/itangcent/common/model/Request.kt b/common-api/src/main/kotlin/com/itangcent/common/model/Request.kt similarity index 100% rename from common/src/main/kotlin/com/itangcent/common/model/Request.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/Request.kt diff --git a/common/src/main/kotlin/com/itangcent/common/model/Response.kt b/common-api/src/main/kotlin/com/itangcent/common/model/Response.kt similarity index 68% rename from common/src/main/kotlin/com/itangcent/common/model/Response.kt rename to common-api/src/main/kotlin/com/itangcent/common/model/Response.kt index 0a25c737f..446923cc4 100644 --- a/common/src/main/kotlin/com/itangcent/common/model/Response.kt +++ b/common-api/src/main/kotlin/com/itangcent/common/model/Response.kt @@ -2,7 +2,7 @@ package com.itangcent.common.model class Response { - var headers: Array
? = null + var headers: ArrayList
? = null /** * raw/json/xml @@ -10,4 +10,6 @@ class Response { var bodyType: String? = null var body: Any? = null + + var code: Int? = null } \ No newline at end of file diff --git a/idea-plugin/build.gradle b/idea-plugin/build.gradle index e08ad8308..5931407ab 100644 --- a/idea-plugin/build.gradle +++ b/idea-plugin/build.gradle @@ -16,7 +16,6 @@ plugins { group 'com.itangcent' version '1.0-SNAPSHOT' -apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'org.jetbrains.intellij' apply plugin: 'idea' @@ -25,7 +24,6 @@ intellij { // version idea_version // version '2018.2.1' version '2017.3.5' -// plugins 'git4idea' pluginName plugin_name updateSinceUntilBuild false downloadSources true @@ -44,6 +42,11 @@ task sourcesJar(type: Jar) { from sourceSets.main.allSource } + +jar { + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} + artifacts { archives jar archives javadocJar @@ -54,11 +57,9 @@ repositories { mavenCentral() } -jar { - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } -} - dependencies { + compile project(path: ':common-api', configuration: 'default') + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile fileTree(dir: 'libs', include: ['*.jar']) @@ -67,9 +68,9 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testCompile group: 'junit', name: 'junit', version: '4.12' - compile group: 'commons-io', name: 'commons-io', version: '2.6' + + testImplementation group: 'junit', name: 'junit', version: '4.12' } compileKotlin { @@ -77,4 +78,4 @@ compileKotlin { } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" -} \ No newline at end of file +} diff --git a/idea-plugin/idea-sandbox/config/options/updates.xml b/idea-plugin/idea-sandbox/config/options/updates.xml deleted file mode 100644 index c8f407beb..000000000 --- a/idea-plugin/idea-sandbox/config/options/updates.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/idea-plugin/libs/intellij-kotlin.jar b/idea-plugin/libs/intellij-kotlin.jar index e8bc9cd0b..5ee9f480c 100644 Binary files a/idea-plugin/libs/intellij-kotlin.jar and b/idea-plugin/libs/intellij-kotlin.jar differ diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/constant/SpringAttrs.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/constant/SpringAttrs.kt new file mode 100644 index 000000000..2ebe6951f --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/constant/SpringAttrs.kt @@ -0,0 +1,10 @@ +package com.itangcent.idea.constant + +object SpringAttrs { + + val SPRING_REQUEST_RESPONSE: Array = arrayOf("HttpServletRequest", "HttpServletResponse") + + var SPRING_CONTROLLER_ANNOTATION: Set = + mutableSetOf("org.springframework.stereotype.Controller", + "org.springframework.web.bind.annotation.RestController") +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiExportAction.kt index 602f24462..819665db9 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiExportAction.kt @@ -26,77 +26,4 @@ abstract class ApiExportAction(text: String) : KotlinAnAction(text) { builder.bind(PsiClassHelper::class) { it.singleton() } builder.bind(TmTypeHelper::class) { it.singleton() } } - - protected fun saveOrCopy(project: Project?, info: String?, - onCopy: () -> Unit, - onSaveSuccess: () -> Unit, - onSaveFailed: () -> Unit) { - - if (info == null) return - val actionContext = ActionContext.getContext()!! - - actionContext.runInSwingUI { - val descriptor = FileChooserDescriptorFactory - .createSingleFileOrFolderDescriptor() - .withTitle("Export location") - .withDescription("Choose directory to export api to") - .withHideIgnored(false) - val chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, project, null) - var toSelect: VirtualFile? = null - val lastLocation = PropertiesComponent.getInstance().getValue(lastImportedLocation()) - if (lastLocation != null) { - toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(lastLocation) - } - val files = chooser.choose(project, toSelect) - if (files.isNotEmpty()) { - actionContext.runInWriteUI { - val file = files[0] - PropertiesComponent.getInstance().setValue(lastImportedLocation(), file.path) - if (file.isDirectory) { - try { - val defaultFile = defaultExportedFile() - FileUtils.forceSave("${file.path}/$defaultFile", info.toByteArray(Charset.defaultCharset())) - onSaveSuccess() - } catch (e: Exception) { - onSaveFailed() - actionContext.runAsync { - copyAndLog(info, onCopy) - } - } - } else { - try { - FileUtils.forceSave(file, info.toByteArray(Charset.defaultCharset())) - onSaveSuccess() - } catch (e: Exception) { - onSaveFailed() - actionContext.runAsync { - copyAndLog(info, onCopy) - } - } - } - } - } else { - copyAndLog(info, onCopy) - } - } - } - - private fun copyAndLog(info: String, onCopy: () -> Unit) { - val logger: Logger = ActionContext.getContext()!!.instance(Logger::class) - ToolUtils.copy2Clipboard(info) - onCopy() - if (info.length > 10000) { - logger.info("Api data is too lager to show in console!") - } else { - logger.log(info) - } - } - - open fun defaultExportedFile(): String { - return "api.json" - } - - open fun lastImportedLocation(): String { - return "com.itangcent.api.export.path" - } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt index cec4c1331..4df25d5af 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt @@ -2,23 +2,24 @@ package com.itangcent.idea.plugin.actions import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.Project -import com.itangcent.common.utils.GsonUtils +import com.itangcent.common.exporter.ClassExporter +import com.itangcent.common.exporter.ParseHandle +import com.itangcent.idea.plugin.api.export.DocParseHelper +import com.itangcent.idea.plugin.api.export.IdeaParseHandle +import com.itangcent.idea.plugin.api.export.SpringClassExporter +import com.itangcent.idea.plugin.api.export.postman.PostmanApiExporter import com.itangcent.idea.plugin.api.export.postman.PostmanApiHelper import com.itangcent.idea.plugin.api.export.postman.PostmanConfigReader -import com.itangcent.idea.plugin.api.export.postman.PostmanExporter import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository -import com.itangcent.intellij.logger.Logger import com.itangcent.intellij.psi.ClassRuleConfig import com.itangcent.intellij.psi.DefaultClassRuleConfig import com.itangcent.intellij.setting.ReadOnlySettingManager import com.itangcent.intellij.setting.SettingManager -import com.itangcent.intellij.util.ToolUtils -import org.apache.commons.lang3.exception.ExceptionUtils class PostmanExportAction : ApiExportAction("Export Postman") { @@ -28,72 +29,21 @@ class PostmanExportAction : ApiExportAction("Export Postman") { builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } builder.bind(SettingManager::class) { it.with(ReadOnlySettingManager::class).singleton() } builder.bind(PostmanApiHelper::class) { it.singleton() } - builder.bind(PostmanExporter::class) { it.singleton() } + builder.bind(PostmanApiExporter::class) { it.singleton() } + builder.bind(DocParseHelper::class) { it.singleton() } builder.bind(ClassRuleConfig::class) { it.with(DefaultClassRuleConfig::class).singleton() } builder.bind(ConfigReader::class) { it.with(PostmanConfigReader::class).singleton() } + builder.bind(ParseHandle::class) { it.with(IdeaParseHandle::class).singleton() } + builder.bind(ClassExporter::class) { it.with(SpringClassExporter::class).singleton() } + + builder.bindInstance("file.save.default", "postman.json") + builder.bindInstance("file.save.last.location.key", "com.itangcent.postman.export.path") } override fun actionPerformed(actionContext: ActionContext, project: Project?, anActionEvent: AnActionEvent) { - val logger: Logger? = actionContext.instance(Logger::class) - - actionContext.runInReadUI { - try { - logger!!.info("Start find apis...") - val exportedPostman = actionContext.instance(PostmanExporter::class).export() - if (exportedPostman == null) { - logger.info("No api be found to export!") - } else { - val exportedPostmanStr = GsonUtils.prettyJson(exportedPostman) - ToolUtils.copy2Clipboard(exportedPostmanStr) - val postManApiHelper: PostmanApiHelper = actionContext.instance(PostmanApiHelper::class) - val hasPrivateToken: Boolean = postManApiHelper.hasPrivateToken() - if (hasPrivateToken) { - logger.info("PrivateToken of postman be found") - if (postManApiHelper.importApiInfo(exportedPostman)) { - logger.info("Export to postman success") - logger.info("To disable automatically import to postman you could remove privateToken" + - " of host [https://api.getpostman.com] in \"File -> Other Setting -> EasyApiSetting \"") - } else { - logger.error("Export to postman failed,You could check below:" + - "1.the network " + - "2.the privateToken") - - saveOrCopy(project, exportedPostmanStr, { - logger.info("Exported data are copied to clipboard,you can paste to postman now") - }, { - logger.info("Apis save success") - }, { - logger.info("Apis save failed") - }) - } - } else { - logger.info("PrivateToken of postman not be setting") - saveOrCopy(project, exportedPostmanStr, { - logger.info("Exported data are copied to clipboard,you can paste to postman now") - }, { - logger.info("Apis save success") - }, { - logger.info("Apis save failed") - }) - logger.info("To enable automatically import to postman you could set privateToken" + - " of host [https://api.getpostman.com] in \"File -> Other Setting -> EasyApiSetting \"") - logger.info("If you do not have a privateToken of postman, you can easily generate one by heading over to the" + - " Postman Integrations Dashboard [https://go.postman.co/integrations/services/pm_pro_api].") - } - } - } catch (e: Exception) { - logger!!.error("Error export api to postman:" + ExceptionUtils.getStackTrace(e)) - } - } - } - - override fun defaultExportedFile(): String { - return "postman.json" - } - override fun lastImportedLocation(): String { - return "com.itangcent.postman.export.path" + actionContext.instance(PostmanApiExporter::class).export() } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/DocParseHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/DocParseHelper.kt new file mode 100644 index 000000000..9b3c2e20f --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/DocParseHelper.kt @@ -0,0 +1,48 @@ +package com.itangcent.idea.plugin.api.export + +import com.google.inject.Inject +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMember +import com.intellij.psi.PsiMethod +import com.itangcent.common.exporter.ParseHandle +import com.itangcent.intellij.psi.PsiClassHelper +import java.util.regex.Pattern + +class DocParseHelper { + + @Inject + private val psiClassHelper: PsiClassHelper? = null + + fun resolveLinkInAttr(attr: String?, psiMember: PsiMember, parseHandle: ParseHandle): String? { + if (attr.isNullOrBlank()) return attr + + if (attr.contains("@link")) { + val pattern = Pattern.compile("\\{@link (.*?)\\}") + val matcher = pattern.matcher(attr) + + val sb = StringBuffer() + while (matcher.find()) { + matcher.appendReplacement(sb, "") + val linkClassAndMethod = matcher.group(1) + val linkClassName = linkClassAndMethod.substringBefore("#") + val linkMethodOrProperty = linkClassAndMethod.substringAfter("#", "").trim() + val linkClass = psiClassHelper!!.resolveClass(linkClassName, psiMember) ?: continue + if (linkMethodOrProperty.isBlank()) { + sb.append(parseHandle.linkToClass(linkClass)) + } else { + val methodOrProperty = psiClassHelper.resolvePropertyOrMethodOfClass(linkClass, linkMethodOrProperty) + ?: continue + when (methodOrProperty) { + is PsiMethod -> sb.append(parseHandle.linkToMethod(methodOrProperty)) + is PsiField -> sb.append(parseHandle.linkToProperty(methodOrProperty)) + else -> sb.append("[$linkClassAndMethod]") + } + } + } + matcher.appendTail(sb) + return sb.toString() + } + + return attr + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/IdeaParseHandle.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/IdeaParseHandle.kt new file mode 100644 index 000000000..c883b68da --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/IdeaParseHandle.kt @@ -0,0 +1,44 @@ +package com.itangcent.idea.plugin.api.export + +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.itangcent.common.exporter.AbstractParseHandle +import com.itangcent.intellij.psi.PsiClassUtils +import com.itangcent.intellij.util.DocCommentUtils + +open class IdeaParseHandle : AbstractParseHandle() { + override fun linkToClass(linkClass: Any): String? { + if (linkClass !is PsiClass) { + return "[$linkClass]" + } + val attrOfClass = DocCommentUtils.getAttrOfDocComment(linkClass.docComment) + return when { + attrOfClass.isNullOrBlank() -> "[${linkClass.name}]" + else -> "[$attrOfClass]" + } + } + + override fun linkToMethod(linkMethod: Any): String? { + if (linkMethod !is PsiMethod) { + return "[$linkMethod]" + } + val attrOfMethod = DocCommentUtils.getAttrOfDocComment(linkMethod.docComment) + return when { + attrOfMethod.isNullOrBlank() -> "[${PsiClassUtils.fullNameOfMethod(linkMethod)}]" + else -> "[$attrOfMethod]" + } + } + + override fun linkToProperty(linkField: Any): String? { + if (linkField !is PsiField) { + return "[$linkField]" + } + val attrOfProperty = DocCommentUtils.getAttrOfDocComment(linkField.docComment) + return when { + attrOfProperty.isNullOrBlank() -> "[${PsiClassUtils.fullNameOfField(linkField)}]" + else -> "[$attrOfProperty]" + } + } + +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/ApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/SpringClassExporter.kt similarity index 57% rename from idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/ApiExporter.kt rename to idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/SpringClassExporter.kt index 5829a9bd1..b068de217 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/ApiExporter.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/SpringClassExporter.kt @@ -4,22 +4,31 @@ import com.google.inject.Inject import com.intellij.lang.jvm.JvmModifier import com.intellij.psi.* import com.intellij.psi.util.PsiTypesUtil +import com.intellij.psi.util.PsiUtil import com.intellij.util.containers.isNullOrEmpty +import com.itangcent.common.constant.Attrs +import com.itangcent.common.constant.HttpMethod +import com.itangcent.common.exporter.* +import com.itangcent.common.model.Request +import com.itangcent.common.model.RequestHandle +import com.itangcent.common.model.Response +import com.itangcent.idea.constant.SpringAttrs import com.itangcent.intellij.logger.Logger import com.itangcent.intellij.psi.JsonOption import com.itangcent.intellij.psi.PsiAnnotationUtils import com.itangcent.intellij.psi.PsiClassHelper import com.itangcent.intellij.psi.PsiClassUtils +import com.itangcent.intellij.spring.MultipartFile import com.itangcent.intellij.spring.SpringClassName -import com.itangcent.intellij.util.ActionUtils import com.itangcent.intellij.util.DocCommentUtils import com.itangcent.intellij.util.KV import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.exception.ExceptionUtils import java.util.* -import java.util.function.Consumer import java.util.regex.Pattern -open class ApiExporter { +open class SpringClassExporter : ClassExporter { + @Inject protected val logger: Logger? = null @@ -29,78 +38,100 @@ open class ApiExporter { @Inject protected val commonRules: CommonRules? = null - protected fun isJavaFile(psiFile: PsiFile): Boolean { - if (psiFile !is PsiJavaFile) return false - if (!(psiFile.name.endsWith(".java"))) return false - return true + @Inject + private val docParseHelper: DocParseHelper? = null + + override fun export(cls: Any, parseHandle: ParseHandle, requestHandle: RequestHandle) { + when { + cls !is PsiClass -> return + !isCtrl(cls) -> return + shouldIgnore(cls) -> { + logger!!.info("ignore class:" + cls.qualifiedName) + return + } + else -> { + val ctrlRequestMappingAnn = findRequestMapping(cls) + val basePath: String = findHttpPath(ctrlRequestMappingAnn) ?: "" + + val ctrlHttpMethod = findHttpMethod(ctrlRequestMappingAnn) + + foreachMethod(cls) { method -> + exportMethodApi(method, basePath, ctrlHttpMethod, parseHandle, requestHandle) + } + } + } } protected fun isCtrl(psiClass: PsiClass): Boolean { - return psiClass.annotations.any { it.qualifiedName?.contains("Controller") ?: false } + return psiClass.annotations.any { + SpringAttrs.SPRING_CONTROLLER_ANNOTATION.contains(it.qualifiedName) + } } protected fun shouldIgnore(psiClass: PsiClass): Boolean { val ignoreRules = commonRules!!.readIgnoreRules() - return when { - ignoreRules.any { it(psiClass, psiClass, psiClass) } -> { - logger!!.info("ignore class:" + psiClass.qualifiedName) - true - } - else -> false - } + return ignoreRules.any { it(psiClass, psiClass, psiClass) } } - protected fun findModule(psiDirectory: PsiDirectory): String? { - val currentPath = ActionUtils.findCurrentPath(psiDirectory) - return findModuleByPath(currentPath) - } + private fun exportMethodApi(method: PsiMethod, basePath: String, ctrlHttpMethod: String + , parseHandle: ParseHandle, requestHandle: RequestHandle) { - protected fun findModule(psiFile: PsiFile): String? { - val currentPath = ActionUtils.findCurrentPath(psiFile) - return findModuleByPath(currentPath) - } + val requestMappingAnn = findRequestMapping(method) ?: return + val request = Request() + request.resource = method - private fun findModuleByPath(path: String?): String? { - if (path == null) return null - var module: String? = null - try { - var currentPath = path - when { - currentPath.contains("/src/") -> currentPath = StringUtils.substringBefore(currentPath, "/src/") - currentPath.contains("/main/") -> currentPath = StringUtils.substringBefore(currentPath, "/main/") - currentPath.contains("/java/") -> currentPath = StringUtils.substringBefore(currentPath, "/java/") - } - module = StringUtils.substringAfterLast(currentPath, "/") - } catch (e: Exception) { - logger!!.error("error in findCurrentPath:" + e.toString()) + var httpMethod = findHttpMethod(requestMappingAnn) + if (httpMethod == HttpMethod.NO_METHOD && ctrlHttpMethod != HttpMethod.NO_METHOD) { + httpMethod = ctrlHttpMethod } - return module + parseHandle.setMethod(request, httpMethod) + val httpPath = contractPath(basePath, findHttpPath(requestMappingAnn))!! + parseHandle.setPath(request, httpPath) + + var attr: String? = null + val attrOfMethod = findAttrOfMethod(method, parseHandle)!! + val lines = attrOfMethod.lines() + attr = if (lines.size > 1) {//multi line + lines.firstOrNull { it.isNotBlank() } + } else { + attrOfMethod + } + + parseHandle.appendDesc(request, attrOfMethod) + + parseHandle.setName(request, attr ?: method.name) + + parseHandle.setMethod(request, httpMethod) + + processMethodParameters(method, request, parseHandle) + + processResponse(method, request, parseHandle) + + requestHandle(request) } - protected fun traversal(psiDirectory: PsiDirectory, - fileFilter: (PsiFile) -> Boolean, - fileHandle: Consumer) { + protected fun processResponse(method: PsiMethod, request: Request, parseHandle: ParseHandle) { - val dirStack: Stack = Stack() - var dir: PsiDirectory? = psiDirectory - while (dir != null) { - dir.files.filter { fileFilter(it) } - .forEach { fileHandle.accept(it) } + val returnType = method.returnType + if (returnType != null) { + try { + val response = Response() + parseHandle.setResponseCode(response, 200) + val typedResponse = psiClassHelper!!.getTypeObject(returnType, method) - for (subdirectory in dir.subdirectories) { - dirStack.push(subdirectory) + parseHandle.setResponseBody(response, "raw", typedResponse) + + parseHandle.addResponseHeader(response, "content-type", "application/json;charset=UTF-8") + + parseHandle.addResponse(request, response) + + } catch (e: Throwable) { + logger!!.error("error to parse body:" + ExceptionUtils.getStackTrace(e)) } - if (dirStack.isEmpty()) break - dir = dirStack.pop() } } - protected fun findModule(): String? { - val currentPath = ActionUtils.findCurrentPath() - return findModuleByPath(currentPath) - } - /** * queryParam中的数组元素需要拆开 */ @@ -134,13 +165,13 @@ open class ApiExporter { protected fun findHttpMethod(requestMappingAnn: PsiAnnotation?): String { if (requestMappingAnn != null) { - var method = PsiAnnotationUtils.findAttr(requestMappingAnn, "method") ?: return NO_METHOD + var method = PsiAnnotationUtils.findAttr(requestMappingAnn, "method") ?: return HttpMethod.NO_METHOD if (method.contains(",")) { method = method.substringBefore(",") } return when { StringUtils.isBlank(method) -> { - NO_METHOD + HttpMethod.NO_METHOD } method.startsWith("RequestMethod.") -> { method.removePrefix("RequestMethod.") @@ -148,7 +179,7 @@ open class ApiExporter { else -> method } } - return NO_METHOD + return HttpMethod.NO_METHOD } protected fun findRequestMapping(psiClass: PsiClass): PsiAnnotation? { @@ -195,22 +226,13 @@ open class ApiExporter { } } - protected fun findAttrOfClass(cls: PsiClass): String? { - val docComment = cls.docComment - val docText = DocCommentUtils.getAttrOfDocComment(docComment) - return when { - StringUtils.isBlank(docText) -> cls.name - else -> resolveLinkInAttr(docText, cls) - } - } - - protected fun findAttrOfMethod(method: PsiMethod): String? { + protected fun findAttrOfMethod(method: PsiMethod, parseHandle: ParseHandle): String? { val docComment = method.docComment val docText = DocCommentUtils.getAttrOfDocComment(docComment) return when { StringUtils.isBlank(docText) -> method.name - else -> resolveLinkInAttr(docText, method) + else -> docParseHelper!!.resolveLinkInAttr(docText, method, parseHandle) } } @@ -306,31 +328,11 @@ open class ApiExporter { } } - protected fun findModuleOfClass(cls: PsiClass): String? { - val moduleRules = commonRules!!.readModuleRules() - if (moduleRules.isEmpty()) return null - - return moduleRules - .map { it(cls, cls, cls) } - .firstOrNull { it != null } - } - - protected fun findReturnOfMethod(method: PsiMethod): String? { - return DocCommentUtils.findDocsByTag(method.docComment, "return") - } - - protected fun findDeprecatedOfMethod(method: PsiMethod): String? { - return DocCommentUtils.findDocsByTag(method.docComment, "deprecated")?.let { resolveLinkInAttr(it, method) } - } - - protected fun findDeprecatedOfClass(psiClass: PsiClass): String? { - return DocCommentUtils.findDocsByTag(psiClass.docComment, "deprecated")?.let { resolveLinkInAttr(it, psiClass) } - } - - protected fun processMethodParameters(method: PsiMethod, itemInfo: ItemInfo) { + protected fun processMethodParameters(method: PsiMethod, request: Request, + parseHandle: ParseHandle) { val params = method.parameterList.parameters - var httpMethod = itemInfo.getHttpMethod() + var httpMethod = request.method ?: HttpMethod.NO_METHOD if (params.isNotEmpty()) { val paramDocComment = extractParamComment(method) @@ -339,11 +341,12 @@ open class ApiExporter { val requestBodyAnn = findRequestBody(param) if (requestBodyAnn != null) { - if (httpMethod == ApiExporter.NO_METHOD) { - httpMethod = "POST" + if (httpMethod == HttpMethod.NO_METHOD) { + httpMethod = HttpMethod.POST } - itemInfo.setHeader("Content-Type", "application/json") - itemInfo.setJsonBody( + parseHandle.addHeader(request, "Content-Type", "application/json") + parseHandle.setJsonBody( + request, parseRequestBody(param.type, method), findAttrForParam(param.name, paramDocComment) ) @@ -352,13 +355,14 @@ open class ApiExporter { val modelAttrAnn = findModelAttr(param) if (modelAttrAnn != null) { - if (httpMethod == "GET") { - itemInfo.addModelAttrAsQuery(param) + if (httpMethod == HttpMethod.GET) { + addModelAttrAsQuery(param, request, parseHandle) } else { - if (httpMethod == ApiExporter.NO_METHOD) { - httpMethod = "POST" + if (httpMethod == HttpMethod.NO_METHOD) { + httpMethod = HttpMethod.POST } - itemInfo.addModelAttr(param) + + addModelAttr(param, request, parseHandle) } continue } @@ -374,7 +378,7 @@ open class ApiExporter { pathName = param.name } - pathName?.let { itemInfo.addPathVal(it, attr) } + parseHandle.addPathParam(request, pathName!!, attr) continue } @@ -398,161 +402,102 @@ open class ApiExporter { } else if (psiClassHelper.isNormalType(unboxType.canonicalText)) {//normal type defaultVal = psiClassHelper.getDefaultValue(unboxType.canonicalText) } else if (paramType.canonicalText.contains(SpringClassName.MULTIPARTFILE)) { - defaultVal = PsiClassHelper.multipartFileInstance - if (httpMethod == "GET") { + if (httpMethod == HttpMethod.GET) { //can not upload file in a GET method - logger!!.error("Couldn't upload file in 'GET':[$httpMethod:${itemInfo.getHttpPath()}],param:${param.name} type:{${paramType.canonicalText}}") + logger!!.error("Couldn't upload file in 'GET':[$httpMethod:${request.path}],param:${param.name} type:{${paramType.canonicalText}}") continue } - if (httpMethod == ApiExporter.NO_METHOD) { - httpMethod = "POST" + if (httpMethod == HttpMethod.NO_METHOD) { + httpMethod = HttpMethod.POST } - itemInfo.addModelAttr(paramName!!, defaultVal, findAttrForParam(param.name, paramDocComment)) + parseHandle.addFormFileParam(request, paramName!!, required, findAttrForParam(param.name, paramDocComment)) continue - } else if (SPRING_REQUEST_RESPONSE.contains(unboxType.presentableText)) { + } else if (SpringAttrs.SPRING_REQUEST_RESPONSE.contains(unboxType.presentableText)) { continue } if (defaultVal != null) { - itemInfo.addQuery2Url(paramName!! - , defaultVal + parseHandle.addParam(request, + paramName!! + , defaultVal.toString() , required , findAttrForParam(param.name, paramDocComment)) } else { - if (httpMethod == "GET") { - itemInfo.addModelAttrAsQuery(param) + if (httpMethod == HttpMethod.GET) { + addModelAttrAsQuery(param, request, parseHandle) } else { - if (httpMethod == ApiExporter.NO_METHOD) { - httpMethod = "POST" + if (httpMethod == HttpMethod.NO_METHOD) { + httpMethod = HttpMethod.POST } - itemInfo.addModelAttr(param) + addModelAttr(param, request, parseHandle) } } } } - if (httpMethod == ApiExporter.NO_METHOD) { - httpMethod = "GET" + if (httpMethod == HttpMethod.NO_METHOD) { + httpMethod = HttpMethod.GET } - itemInfo.setHttpMethod(httpMethod) + parseHandle.setMethod(request, httpMethod) } - protected fun deepComponent(obj: Any?): Any? { - if (obj == null) { - return null - } - if (obj is Array<*>) { - if (obj.isEmpty()) return obj - return deepComponent(obj[0]) - } - if (obj is Collection<*>) { - if (obj.isEmpty()) return obj - return deepComponent(obj.first()) + fun addModelAttrAsQuery(parameter: PsiParameter, request: Request, parseHandle: ParseHandle) { + val paramType = parameter.type + try { + val paramCls = PsiUtil.resolveClassInClassTypeOnly(psiClassHelper!!.unboxArrayOrList(paramType)) + val fields = psiClassHelper.getFields(paramCls, JsonOption.READ_COMMENT) + val comment: KV? = fields.getAs(Attrs.COMMENT_ATTR) + fields.forEach { filedName, fieldVal -> + if (filedName != Attrs.COMMENT_ATTR) { + parseHandle.addParam(request, filedName, tinyQueryParam(fieldVal?.toString()), false, comment?.get("filedName") as String?) + } + } + } catch (e: Exception) { + logger!!.error("error to parse[" + paramType.canonicalText + "] as ModelAttribute") } - return obj } - protected fun resolveLinkInAttr(attr: String?, psiMember: PsiMember): String? { - if (attr.isNullOrBlank()) return attr - - if (attr.contains("@link")) { - val pattern = Pattern.compile("\\{@link (.*?)\\}") - val matcher = pattern.matcher(attr) - - val sb = StringBuffer() - while (matcher.find()) { - matcher.appendReplacement(sb, "") - val linkClassAndMethod = matcher.group(1) - val linkClassName = linkClassAndMethod.substringBefore("#") - val linkMethodOrProperty = linkClassAndMethod.substringAfter("#", "").trim() - val linkClass = psiClassHelper!!.resolveClass(linkClassName, psiMember) ?: continue - if (linkMethodOrProperty.isBlank()) { - sb.append(linkToClass(linkClass)) - } else { - val methodOrProperty = psiClassHelper.resolvePropertyOrMethodOfClass(linkClass, linkMethodOrProperty) - ?: continue - when (methodOrProperty) { - is PsiMethod -> sb.append(linkToMethod(methodOrProperty)) - is PsiField -> sb.append(linkToProperty(methodOrProperty)) - else -> sb.append("[$linkClassAndMethod]") + fun addModelAttr(parameter: PsiParameter, request: Request, parseHandle: ParseHandle) { + + val paramType = parameter.type + try { + val paramCls = PsiUtil.resolveClassInClassTypeOnly(psiClassHelper!!.unboxArrayOrList(paramType)) + val fields = psiClassHelper.getFields(paramCls, JsonOption.READ_COMMENT) + val comment: KV? = fields.getAs(Attrs.COMMENT_ATTR) + parseHandle.addHeader(request, "Content-Type", "application/x-www-form-urlencoded") + fields.forEach { filedName, fieldVal -> + if (filedName != Attrs.COMMENT_ATTR) { + val fv = deepComponent(fieldVal) + if (fv is MultipartFile) { + parseHandle.addFormFileParam(request, filedName, false, comment?.getAs(filedName)) + } else { + parseHandle.addFormParam(request, filedName, null, comment?.getAs(filedName)) } } } - matcher.appendTail(sb) - return sb.toString() + } catch (e: Exception) { + logger!!.error("error to parse[" + paramType.canonicalText + "] as ModelAttribute") } - - return attr } - open protected fun linkToClass(linkClass: PsiClass): String? { - val attrOfClass = DocCommentUtils.getAttrOfDocComment(linkClass.docComment) - return when { - attrOfClass.isNullOrBlank() -> "[${linkClass.name}]" - else -> "[$attrOfClass]" - } + protected open fun parseRequestBody(psiType: PsiType?, context: PsiElement): Any? { + return psiClassHelper!!.getTypeObject(psiType, context, JsonOption.READ_COMMENT) } - open protected fun linkToMethod(linkMethod: PsiMethod): String? { - val attrOfMethod = DocCommentUtils.getAttrOfDocComment(linkMethod.docComment) - return when { - attrOfMethod.isNullOrBlank() -> "[${PsiClassUtils.fullNameOfMethod(linkMethod)}]" - else -> "[$attrOfMethod]" + private fun deepComponent(obj: Any?): Any? { + if (obj == null) { + return null } - } - - open protected fun linkToProperty(linkField: PsiField): String? { - val attrOfProperty = DocCommentUtils.getAttrOfDocComment(linkField.docComment) - return when { - attrOfProperty.isNullOrBlank() -> "[${PsiClassUtils.fullNameOfField(linkField)}]" - else -> "[$attrOfProperty]" + if (obj is Array<*>) { + if (obj.isEmpty()) return obj + return deepComponent(obj[0]) } - } - - open protected fun parseRequestBody(psiType: PsiType?, context: PsiElement): Any? { - return psiClassHelper!!.getTypeObject(psiType, context, JsonOption.READ_COMMENT) - } - - protected interface ItemInfo { - - fun setHttpMethod(httpMethod: String) - - fun getHttpMethod(): String - - fun getHttpPath(): String - - fun addModelAttr(parameter: PsiParameter) - - fun addModelAttrAsQuery(parameter: PsiParameter) - - fun addModelAttr(name: String, value: Any?, attr: String?) - - fun addQuery2Url(paramName: String, defaultVal: Any?, attr: String?) { - addQuery2Url(paramName, defaultVal, false, attr) + if (obj is Collection<*>) { + if (obj.isEmpty()) return obj + return deepComponent(obj.first()) } - - fun addQuery2Url(paramName: String, defaultVal: Any?, required: Boolean, attr: String?) - - fun addPathVal(name: String, desc: String) - - fun setJsonBody(body: Any?, desc: String?) - - fun appendDesc(desc: String?) - - fun setHeader(name: String, value: String) - - fun getData(): Any - } - - companion object { - val NO_METHOD = "ALL" - val COMMENT_ATTR = "@comment" - val SPRING_REQUEST_RESPONSE: Array = arrayOf("HttpServletRequest", "HttpServletResponse") - - val EMPTY_ARR: List = Collections.emptyList()!! - val EMPTY_PARAMS: List = Collections.emptyList() - val EMPTY_TAGS: List = Collections.emptyList() + return obj } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiExporter.kt new file mode 100644 index 000000000..68aac7c41 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiExporter.kt @@ -0,0 +1,431 @@ +package com.itangcent.idea.plugin.api.export.postman + +import com.google.inject.Inject +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.itangcent.common.constant.Attrs +import com.itangcent.common.exporter.ClassExporter +import com.itangcent.common.exporter.ParseHandle +import com.itangcent.common.model.Request +import com.itangcent.common.utils.DateUtils +import com.itangcent.common.utils.GsonUtils +import com.itangcent.idea.plugin.api.export.CommonRules +import com.itangcent.idea.plugin.api.export.DocParseHelper +import com.itangcent.idea.plugin.utils.FileSaveHelper +import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.logger.Logger +import com.itangcent.intellij.psi.SelectedHelper +import com.itangcent.intellij.util.ActionUtils +import com.itangcent.intellij.util.DocCommentUtils +import com.itangcent.intellij.util.KV +import org.apache.commons.lang3.RandomUtils +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.exception.ExceptionUtils +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +class PostmanApiExporter { + + @Inject + private val logger: Logger? = null + + @Inject + private val postmanApiHelper: PostmanApiHelper? = null + + @Inject + private val actionContext: ActionContext? = null + + @Inject + private val classExporter: ClassExporter? = null + + @Inject + private val parseHandle: ParseHandle? = null + + @Inject + private val commonRules: CommonRules? = null + + @Inject + private val docParseHelper: DocParseHelper? = null + + @Inject + private val fileSaveHelper: FileSaveHelper? = null + + fun export() { + + logger!!.info("Start find apis...") + val requests: MutableList = Collections.synchronizedList(ArrayList()) + + SelectedHelper.Builder() + .dirHandle { dir, callBack -> + actionContext!!.runInSwingUI { + try { + val project = actionContext.instance(Project::class) + val yes = Messages.showYesNoDialog(project, + "Export the model in directory [${ActionUtils.findCurrentPath(dir)}]?", + "Are you sure", + Messages.getQuestionIcon()) + if (yes == Messages.YES) { + callBack(true) + } else { + logger.info("Cancel the operation export api from [${ActionUtils.findCurrentPath(dir)}]!") + callBack(false) + } + } catch (e: Exception) { + callBack(false) + } + } + } + .classHandle { + classExporter!!.export(it, parseHandle!!) { request -> requests.add(request) } + } + .onCompleted { + try { + if (requests.isEmpty()) { + logger.info("No api be found to export!") + return@onCompleted + } + val postman = parseRequests(requests) + requests.clear() + actionContext!!.runAsync { + try { + if (postmanApiHelper!!.hasPrivateToken()) { + if (postmanApiHelper.importApiInfo(postman)) { + return@runAsync + } + } + fileSaveHelper!!.saveOrCopy(GsonUtils.prettyJson(postman), { + logger.info("Exported data are copied to clipboard,you can paste to postman now") + }, { + logger.info("Apis save success") + }) { + logger.info("Apis save failed") + } + } catch (e: Exception) { + logger.info("Apis save failed" + ExceptionUtils.getStackTrace(e)) + } + } + } catch (e: Exception) { + logger.info("Apis save failed" + ExceptionUtils.getStackTrace(e)) + } + } + .traversal() + } + + private fun parseRequests(requests: MutableList): HashMap { + + //group by class into: {class:requests} + val clsGroupedMap: HashMap>> = HashMap() + requests.forEach { request -> + val resource = request.resource?.let { findResourceClass(it) } ?: NULL_RESOURCE + clsGroupedMap.computeIfAbsent(resource) { ArrayList() } + .add(request2Item(request)) + } + + //only one class + if (clsGroupedMap.size == 1) { + clsGroupedMap.entries.first() + .let { + val module = findModule(it.key) ?: "easy-api" + return wrapRootInfo(module, arrayListOf(wrapInfo(it.key, it.value))) + } + } + + //group by module + val moduleGroupedMap: HashMap>> = HashMap() + clsGroupedMap.forEach { cls, items -> + val module = findModule(cls) ?: "easy-api" + moduleGroupedMap.computeIfAbsent(module) { ArrayList() } + .add(wrapInfo(cls, items)) + } + + + //only one class + if (moduleGroupedMap.size == 1) { + moduleGroupedMap.entries.first() + .let { + return wrapRootInfo(it.key, arrayListOf(wrapInfo(it.key, it.value))) + } + } + + val modules: ArrayList> = ArrayList() + moduleGroupedMap.entries + .map { wrapInfo(it.key, arrayListOf(wrapRootInfo(it.key, it.value))) } + .forEach { modules.add(it) } + + val rootModule = findModuleByPath(ActionUtils.findCurrentPath()) ?: "easy-api" + return wrapRootInfo("$rootModule-${DateUtils.format(DateUtils.now(), "yyyyMMddHHmmss")}", modules) + } + + private fun wrapRootInfo(resource: Any, items: ArrayList>): HashMap { + + val postman: HashMap = HashMap() + val info: HashMap = HashMap() + postman["info"] = info + parseNameAndDesc(resource, info) + info["schema"] = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + postman["item"] = items + return postman + } + + private fun wrapInfo(resource: Any, items: ArrayList>): HashMap { + val postman: HashMap = HashMap() + parseNameAndDesc(resource, postman) + postman["item"] = items + return postman + } + + private fun parseNameAndDesc(resource: Any, info: HashMap) { + if (resource is PsiClass) { + val attr = findAttrOfClass(resource, parseHandle!!) + if (attr.isNullOrBlank()) { + info["name"] = resource.name!! + info["description"] = "exported from module:${resource.qualifiedName}" + } else { + val lines = attr.lines() + if (lines.size == 1) { + info["name"] = attr + info["description"] = "exported from module:${actionContext!!.callInReadUI { resource.qualifiedName }}" + } else { + info["name"] = lines[0] + info["description"] = attr + } + } + } else { + info["name"] = "$resource-${DateUtils.format(DateUtils.now(), "yyyyMMddHHmmss")}" + info["description"] = "exported at ${DateUtils.formatYMD_HMS(DateUtils.now())}" + } + } + + private fun request2Item(request: Request): HashMap { + + val module = request.resource?.let { findModule(it) } + var host = "{{host}}" + if (module != null) { + host = "{{$module}}" + } + + val item: HashMap = HashMap() + + item["name"] = request.name + + val requestInfo: HashMap = HashMap() + item["request"] = requestInfo + + requestInfo["method"] = request.method + requestInfo["description"] = request.desc + + val url: HashMap = HashMap() + requestInfo["url"] = url + + url["host"] = host + url["path"] = request.path!!.trim().trim('/').split("/") + url["raw"] = contractPath(host, request.path) + + + val headers: ArrayList> = ArrayList() + requestInfo["header"] = headers + request.headers?.forEach { + headers.add(KV.create() + .set("key", it.name) + .set("value", it.value) + ) + } + + val queryList: ArrayList> = ArrayList() + url["query"] = queryList + request.querys?.forEach { + queryList.add(KV.create() + .set("key", it.name) + .set("value", it.value) + .set("equals", true) + .set("description", it.desc) + ) + } + + val body: HashMap = HashMap() + if (request.formParams != null) { + body["mode"] = "urlencoded" + val urlencodeds: ArrayList> = ArrayList() + request.formParams!!.forEach { + urlencodeds.add(KV.create() + .set("key", it.name) + .set("value", it.value) + .set("type", it.type) + .set("description", it.desc) + ) + } + body["urlencoded"] = urlencodeds + } + + if (request.body != null) { + body["mode"] = "raw" + body["raw"] = parseRawBody(request.body!!) + } + + if (body.isNotEmpty()) { + requestInfo["body"] = body + } + + + if (!request.response.isNullOrEmpty()) { + + val responses: ArrayList> = ArrayList() + val exampleName = request.name + "-Example" + request.response!!.forEachIndexed { index, response -> + val responseInfo: HashMap = HashMap() + if (index > 0) { + responseInfo["name"] = exampleName + (index + 1) + } else { + responseInfo["name"] = exampleName + } + responseInfo["originalRequest"] = requestInfo//need clone?request.clone()? + responseInfo["status"] = "OK" + responseInfo["code"] = 200 + responseInfo["_postman_previewlanguage"] = "json" + responseInfo["_postman_previewtype"] = "text" + val responseHeader = ArrayList>() + responseInfo["header"] = responseHeader + responseHeader.add(KV.create() + .set("name", "content-type") + .set("key", "content-type") + .set("value", "application/json;charset=UTF-8") + .set("description", "The mime type of this content") + ) + responseHeader.add(KV.create() + .set("name", "date") + .set("key", "date") + .set("value", DateUtils.format(Date(), "EEE, dd MMM yyyyHH:mm:ss 'GMT'")) + .set("description", "The date and time that the message was sent") + ) + responseHeader.add(KV.create() + .set("name", "server") + .set("key", "server") + .set("value", "Apache-Coyote/1.1") + .set("description", "A name for the server") + ) + responseHeader.add(KV.create() + .set("name", "transfer-encoding") + .set("key", "transfer-encoding") + .set("value", "chunked") + .set("description", "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity.") + ) + + response.headers?.forEach { + responseHeader.add(KV.create() + .set("name", it.name) + .set("key", it.name) + .set("value", it.value) + .set("description", it.desc) + ) + } + + responseInfo["responseTime"] = RandomUtils.nextInt(10, 100) + + responseInfo["body"] = response.body?.let { parseRawBody(it) } + + responses.add(responseInfo) + } + item["response"] = responses + } + + return item + } + + private fun parseRawBody(body: Any): String { + if (body is String) { + return body + } + if (body is Map<*, *>) { + if (body.containsKey(Attrs.COMMENT_ATTR)) { + return GsonUtils.prettyJson(body.filterKeys { it != Attrs.COMMENT_ATTR }) + } + } + return GsonUtils.prettyJson(body) + } + + private fun contractPath(pathPre: String?, pathAfter: String?): String? { + if (pathPre == null) return pathAfter + if (pathAfter == null) return pathPre + return pathPre.removeSuffix("/") + "/" + pathAfter.removePrefix("/") + } + + private fun findResourceClass(resource: Any): PsiClass? { + return when (resource) { + is PsiMethod -> resource.containingClass + is PsiClass -> resource + else -> null + } + } + + private fun findAttrOfClass(cls: PsiClass, parseHandle: ParseHandle): String? { + val docComment = cls.docComment + val docText = DocCommentUtils.getAttrOfDocComment(docComment) + return when { + StringUtils.isBlank(docText) -> cls.name + else -> docParseHelper!!.resolveLinkInAttr(docText, cls, parseHandle) + } + } + + //region find module + private fun findModule(resource: Any): String? { + return when (resource) { + is PsiMethod -> findModule(resource) + is PsiClass -> findModule(resource) + is PsiFile -> findModule(resource) + else -> null + } + } + + private fun findModule(psiMethod: PsiMethod): String? { + val containingClass = psiMethod.containingClass + if (containingClass != null) { + return findModule(containingClass) + } + return null + } + + private fun findModule(cls: PsiClass): String? { + val moduleRules = commonRules!!.readModuleRules() + if (!moduleRules.isEmpty()) { + val module = moduleRules + .map { it(cls, cls, cls) } + .firstOrNull { it != null } + if (!module.isNullOrBlank()) { + return module + } + } + return findModule(cls.containingFile) + } + + private fun findModule(psiFile: PsiFile): String? { + val currentPath = ActionUtils.findCurrentPath(psiFile) + return findModuleByPath(currentPath) + } + + private fun findModuleByPath(path: String?): String? { + if (path == null) return null + var module: String? = null + try { + var currentPath = path + when { + currentPath.contains("/src/") -> currentPath = StringUtils.substringBefore(currentPath, "/src/") + currentPath.contains("/main/") -> currentPath = StringUtils.substringBefore(currentPath, "/main/") + currentPath.contains("/java/") -> currentPath = StringUtils.substringBefore(currentPath, "/java/") + } + module = StringUtils.substringAfterLast(currentPath, "/") + } catch (e: Exception) { + logger!!.error("error in findCurrentPath:" + e.toString()) + } + return module + + } + //endregion + + companion object { + val NULL_RESOURCE = Object() + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiHelper.kt index e3cd6efb6..e6ce0245c 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanApiHelper.kt @@ -38,30 +38,17 @@ class PostmanApiHelper { val httpClient = HttpClients.createDefault() -// val httpGet = HttpGet(CREATECOLLECTION) -// val responseHandler = TMResponseHandler() -// httpGet.setHeader("x-api-key", getPrivateToken()) -// try { -// val returnValue = httpClient.execute(httpGet, responseHandler) //调接口获取返回值时,必须用此方法 -// logger!!.info("response:$returnValue") -// } catch (e: Throwable) { -// logger!!.info("post failed:" + ExceptionUtils.getStackTrace(e)) -// return false -// } -// return false - val httpPost = HttpPost(CREATECOLLECTION) val collection: HashMap = HashMap() collection["collection"] = apiInfo - //第三步:给httpPost设置JSON格式的参数 + val requestEntity = StringEntity(GsonUtils.toJson(collection), ContentType.APPLICATION_JSON) -// requestEntity.setContentEncoding("UTF-8") -// httpPost.setHeader("Content-Type", "application/json") + httpPost.setHeader("x-api-key", getPrivateToken()) httpPost.entity = requestEntity val responseHandler = TMResponseHandler() - //第四步:发送HttpPost请求,获取返回值 + try { val returnValue = httpClient.execute(httpPost, responseHandler) //调接口获取返回值时,必须用此方法 if (StringUtils.isNotBlank(returnValue) && returnValue.contains("collection")) { @@ -89,61 +76,6 @@ class PostmanApiHelper { } } -// -// fun importApiInfo(apiInfo: HashMap): Boolean { -// -// val httpClient = HttpClients.createDefault() -// -// val httpPost = HttpPost(CREATECOLLECTION) -// -// //自己生一个boundary -// val boundary = "--------------------" + RandomStringUtils.randomAlphanumeric(24) -// -// logger!!.info("boundary:[$boundary]") -// -// val multipartEntity = MultipartEntityBuilder.create() -// .setMode(HttpMultipartMode.BROWSER_COMPATIBLE) -// .setBoundary(boundary) -// .setCharset(Charset.defaultCharset()) -// .addTextBody("type", "file") -// .addBinaryBody("input", apiInfo.toByteArray(Charset.defaultCharset())) -// .build() -// -// httpPost.setHeader("Content-Type", "multipart/form-data; boundary=$boundary") -// httpPost.setHeader("x-api-key", getPrivateToken()) -// httpPost.entity = multipartEntity -// val responseHandler = BasicResponseHandler() -// //第四步:发送HttpPost请求,获取返回值 -// -// logger!!.info("start post") -// try { -// val returnValue = httpClient.execute(httpPost, responseHandler) -// logger!!.info("response:$returnValue") -// } catch (e: Throwable) { -// logger!!.info("post failed:" + ExceptionUtils.getStackTrace(e)) -// return false -// } -// return false -// } - -// fun importApiInfo(apiInfo: String): Boolean { -// val httpClient = HttpClients.createDefault() -// -// val httpPost = HttpPost(imporedApi) -// -// //第三步:给httpPost设置JSON格式的参数 -// val requestEntity = StringEntity(apiInfo, "utf-8") -// requestEntity.setContentEncoding("UTF-8") -// httpPost.setHeader("Content-type", "application/json") -// httpPost.setHeader("x-api-key", getPrivateToken()) -// httpPost.entity = requestEntity -// val responseHandler = BasicResponseHandler() -// //第四步:发送HttpPost请求,获取返回值 -// val returnValue = httpClient.execute(httpPost, responseHandler) //调接口获取返回值时,必须用此方法 -// logger!!.info("response:$returnValue") -// return false -// } - companion object { val POSTMANHOST = "https://api.getpostman.com" val IMPOREDAPI = "${POSTMANHOST}/import/exported" diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanExporter.kt deleted file mode 100644 index 7a8456a32..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/postman/PostmanExporter.kt +++ /dev/null @@ -1,452 +0,0 @@ -package com.itangcent.idea.plugin.api.export.postman - -import com.google.inject.Inject -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.psi.* -import com.intellij.psi.util.PsiUtil -import com.itangcent.common.utils.DateUtils -import com.itangcent.common.utils.GsonUtils -import com.itangcent.idea.plugin.api.export.ApiExporter -import com.itangcent.intellij.psi.JsonOption -import com.itangcent.intellij.spring.MultipartFile -import com.itangcent.intellij.util.ActionUtils -import com.itangcent.intellij.util.KV -import org.apache.commons.lang3.RandomUtils -import org.apache.commons.lang3.StringUtils -import org.apache.commons.lang3.exception.ExceptionUtils -import java.util.* -import java.util.function.Consumer - -class PostmanExporter : ApiExporter() { - - @Inject - var anActionEvent: AnActionEvent? = null - - fun export(): HashMap? { - - val navigatable = anActionEvent!!.getData(CommonDataKeys.NAVIGATABLE) - if (navigatable is PsiDirectory) {//select dir - val itemsMap: HashMap>?> = HashMap() - traversal(navigatable, { - isJavaFile(it) - }, Consumer { file -> - export(file)?.forEach { module, item -> - if (!itemsMap.containsKey(module)) { - itemsMap[module] = ArrayList() - } - itemsMap[module]!!.add(item) - } - }) - - if (itemsMap.isEmpty()) return null - - var rootModule = findModule(navigatable) - if (rootModule == null) rootModule = "" - return warpItemsWithInfo(itemsMap, rootModule) - } else { - val currentClass = ActionUtils.findCurrentClass() ?: return null - val module = findModuleOfClass(currentClass) ?: findModule() - val item = export(currentClass, module) - return item?.let { warpWithInfo(it, module) } - } - } - - private fun export(psiFile: PsiFile): HashMap>? { - if (psiFile !is PsiJavaFile) return null - var module: String? = null - var items: HashMap>? = null - for (psiCls in psiFile.classes) { - if (isCtrl(psiCls)) { - if (module == null) { - module = findModuleOfClass(psiCls) ?: findModule(psiFile) - } - val item = export(psiCls, module) - if (item != null) { - if (items == null) { - items = HashMap() - } - items[module!!] = item - } - } - } - - if (items == null || items.isEmpty()) return null - - return items - } - - private fun warpItemsWithInfo(itemsMap: Map>?>, rootModule: String): HashMap? { - val postman: HashMap = HashMap() - - val info: HashMap = HashMap() - postman["info"] = info - info["name"] = "$rootModule-${DateUtils.format(DateUtils.now(), "yyyyMMddHHmmss")}" - info["description"] = "exported from module:$rootModule at ${DateUtils.formatYMD_HMS(DateUtils.now())}" - info["schema"] = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" - - val dirItems: ArrayList> = ArrayList() - postman["item"] = dirItems - - if (itemsMap.size == 1) { - val singleModule = itemsMap.keys.first() - if (singleModule == rootModule) {//todo:required equal? - val items = itemsMap[singleModule] - if (items == null) return null - else { - dirItems.addAll(items) - return postman - } - } - - } - - itemsMap.forEach { module, items -> - val dirItem: HashMap = HashMap() - dirItem["name"] = module - dirItem["description"] = "exported from:" + module - dirItem["item"] = items - dirItems.add(dirItem) - } - return postman - } - - private fun warpWithInfo(item: HashMap, module: String?): HashMap { - val postman: HashMap = HashMap() - - val info: HashMap = HashMap() - postman["info"] = info - info["name"] = "$module-${DateUtils.format(DateUtils.now(), "yyyyMMddHHmmss")}" - info["description"] = "exported from module:$module at ${DateUtils.formatYMD_HMS(DateUtils.now())}" - info["schema"] = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" - - val dirItems: ArrayList> = ArrayList() - postman["item"] = dirItems - - dirItems.add(item) - - return postman - } - - private fun export(currentClass: PsiClass, module: String?): HashMap? { - - if (shouldIgnore(currentClass)) { - return null - } - - //region use direct module name as host----------------------------------------------------- - var host = "{{host}}" - if (module != null) { - host = "{{$module}}" - } - //endregion use direct module name as host----------------------------------------------------- - - val ctrlRequestMappingAnn = findRequestMapping(currentClass) - val basePath: String = findHttpPath(ctrlRequestMappingAnn) ?: "" - - val ctrlHttpMethod = findHttpMethod(ctrlRequestMappingAnn) - - val dirItem: HashMap = HashMap() - dirItem["name"] = StringUtils.left(findAttrOfClass(currentClass), 30) - dirItem["description"] = "exported from:" + currentClass.name - - val items: ArrayList> = ArrayList() - dirItem["item"] = items - - foreachMethod(currentClass, { method -> - exportMethodApi(method, basePath, ctrlHttpMethod, host)?.let { items.add(it) } - }) - - if (items.isEmpty()) { - return null - } - - return dirItem - } - - private fun exportMethodApi(method: PsiMethod, basePath: String, ctrlHttpMethod: String, host: String): HashMap? { - - val requestMappingAnn = findRequestMapping(method) ?: return null - var httpMethod = findHttpMethod(requestMappingAnn) - if (httpMethod == NO_METHOD && ctrlHttpMethod != NO_METHOD) { - httpMethod = ctrlHttpMethod - } - - val httpPath = contractPath(basePath, findHttpPath(requestMappingAnn))!! - - val item: HashMap = HashMap() - - var attr: String? = null - val attrOfMethod = findAttrOfMethod(method)!! - if (attrOfMethod.contains("\n")) {//multi line - val lines = attrOfMethod.lines() - for (line in lines) { - if (line.isNotBlank()) { - attr = line - break - } - } - item["description"] = attrOfMethod - } else { - attr = attrOfMethod - } - - item["name"] = attr - - val request: HashMap = HashMap() - item["request"] = request - - val url: HashMap = HashMap() - request["url"] = url - - url["host"] = host - url["path"] = httpPath.trim().trim('/').split("/") - url["raw"] = contractPath(host, httpPath) - - val itemInfo = PostmanItemInfo(item) - itemInfo.setHttpMethod(httpMethod) - processMethodParameters(method, itemInfo) - - val returnType = method.returnType - if (returnType != null) { - try { - val response: HashMap = HashMap() - response["name"] = attr + "-Example" - response["originalRequest"] = request//need clone?request.clone()? - response["status"] = "OK" - response["code"] = 200 - response["_postman_previewlanguage"] = "json" - response["_postman_previewtype"] = "text" - val responseHeader = ArrayList>() - response["header"] = responseHeader - responseHeader.add(KV.create() - .set("name", "content-type") - .set("key", "content-type") - .set("value", "application/json;charset=UTF-8") - .set("description", "The mime type of this content") - ) - responseHeader.add(KV.create() - .set("name", "date") - .set("key", "date") - .set("value", DateUtils.format(Date(), "EEE, dd MMM yyyyHH:mm:ss 'GMT'")) - .set("description", "The date and time that the message was sent") - ) - responseHeader.add(KV.create() - .set("name", "server") - .set("key", "server") - .set("value", "Apache-Coyote/1.1") - .set("description", "A name for the server") - ) - responseHeader.add(KV.create() - .set("name", "transfer-encoding") - .set("key", "transfer-encoding") - .set("value", "chunked") - .set("description", "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity.") - ) - response["responseTime"] = RandomUtils.nextInt(1, 10 * (1 + method.parameters.size)) - - val typedResponse = psiClassHelper!!.getTypeObject(returnType, method) - - response["body"] = GsonUtils.prettyJson(typedResponse) - - //add response to item - val responses: ArrayList> = ArrayList() - responses.add(response) - item["response"] = responses - - } catch (e: Throwable) { - logger!!.error("error to parse body:" + ExceptionUtils.getStackTrace(e)) - } - - } - return item - - } - - override fun parseRequestBody(psiType: PsiType?, context: PsiElement): Any? { - return psiClassHelper!!.getTypeObject(psiType, context, JsonOption.NONE) - } - - @Suppress("UNCHECKED_CAST") - private inner class PostmanItemInfo(private val item: HashMap) : ItemInfo { - override fun getHttpPath(): String { - - val request = getRequest() - val url = request["url"] as HashMap - - val host = url["host"] as String? - val raw = url["raw"] as String? ?: return "" - - return if (host != null) { - raw.removePrefix(host) - } else { - raw - } - } - - override fun addPathVal(name: String, desc: String) { - //ignore - } - - override fun setJsonBody(body: Any?, desc: String?) { - val request = getRequest() - val bodyOfRequest: HashMap = HashMap() - request["body"] = bodyOfRequest - - bodyOfRequest["mode"] = "raw" - - bodyOfRequest["raw"] = GsonUtils.prettyJson(body) - } - - override fun setHttpMethod(httpMethod: String) { - getRequest()["method"] = httpMethod - } - - override fun getHttpMethod(): String { - val method = getRequest()["method"] - if (method != null) return method as String - setHttpMethod(NO_METHOD) - return NO_METHOD - } - - override fun getData(): Any { - return item - } - - @Suppress("UNCHECKED_CAST") - fun getRequest(): HashMap { - var request = item["request"] - if (request == null) { - request = HashMap() - item["request"] = request - } - return request as HashMap - } - - @Suppress("UNCHECKED_CAST") - fun getUrl(): HashMap { - val request = getRequest() - var url = request["url"] - if (url == null) { - url = HashMap() - request["url"] = url - } - return url as HashMap - } - - @Suppress("UNCHECKED_CAST") - fun getUrlencodeds(): ArrayList> { - - setHeader("Content-Type", "application/x-www-form-urlencoded") - val request = getRequest() - val body: HashMap = request.computeIfAbsent("body", - { _ -> HashMap() }) as HashMap - - body["mode"] = "urlencoded" - val urlencodeds: ArrayList> = body.computeIfAbsent("urlencoded", - { _ -> ArrayList>() }) as ArrayList> - return urlencodeds - } - - override fun addModelAttr(parameter: PsiParameter) { - - val urlencodeds: ArrayList> = getUrlencodeds() - - val paramType = parameter.type - try { - val paramCls = PsiUtil.resolveClassInClassTypeOnly(psiClassHelper!!.unboxArrayOrList(paramType)) - val fields = psiClassHelper.getFields(paramCls, JsonOption.READ_COMMENT) - val comment: KV? = fields.getAs(COMMENT_ATTR) - fields.forEach { filedName, fieldVal -> - if (filedName != COMMENT_ATTR) { - val urlencodedParam: HashMap = HashMap() - urlencodeds.add(buildFormParam(filedName, fieldVal)) - if (comment != null) { - val fieldComment = comment.getAs(filedName) - urlencodedParam["description"] = fieldComment - } - } - } - } catch (e: Exception) { - logger!!.error("error to parse[" + paramType.canonicalText + "] as ModelAttribute") - } - } - - @Suppress("UNCHECKED_CAST") - override fun addQuery2Url(paramName: String, defaultVal: Any?, required: Boolean, attr: String?) { - val url = getUrl() - val queryInUrl = url["query"] - val queryList: ArrayList> - if (queryInUrl == null) { - queryList = ArrayList() - url["query"] = queryList - } else { - queryList = queryInUrl as ArrayList> - } - - val query: HashMap = HashMap() - queryList.add(query) - - query["key"] = paramName - query["value"] = tinyQueryParam(defaultVal?.toString()) - query["equals"] = true - query["description"] = attr - } - - override fun addModelAttr(name: String, value: Any?, attr: String?) { - val urlencodeds: ArrayList> = getUrlencodeds() - urlencodeds.add(buildFormParam(name, value)) - } - - override fun addModelAttrAsQuery(parameter: PsiParameter) { - val paramType = parameter.type - try { - val paramCls = PsiUtil.resolveClassInClassTypeOnly(psiClassHelper!!.unboxArrayOrList(paramType)) - val fields = psiClassHelper.getFields(paramCls, JsonOption.READ_COMMENT) - val comment: KV? = fields.getAs(COMMENT_ATTR) - fields.forEach { filedName, fieldVal -> - if (filedName != COMMENT_ATTR) { - addQuery2Url(filedName, fieldVal?.toString(), comment?.get("filedName") as String?) - } - } - } catch (e: Exception) { - logger!!.error("error to parse[" + paramType.canonicalText + "] as ModelAttribute") - } - } - - override fun appendDesc(desc: String?) { - //ignore - } - - @Suppress("UNCHECKED_CAST") - override fun setHeader(name: String, value: String) { - - val request = getRequest() - - val headers: ArrayList> = request.computeIfAbsent("header", - { _ -> ArrayList>() }) as ArrayList> - - var contentHeader: java.util.HashMap? = headers.firstOrNull { it["key"] == name } - if (contentHeader == null) { - contentHeader = HashMap() - headers.add(contentHeader) - contentHeader["key"] = name - } - contentHeader["value"] = value - } - - private fun buildFormParam(name: String, value: Any?): HashMap { - val urlencodedParam: HashMap = HashMap() - urlencodedParam["key"] = name - val dv = deepComponent(value) - if (dv != null && dv is MultipartFile) { - urlencodedParam["value"] = null - urlencodedParam["type"] = "file" - } else { - urlencodedParam["value"] = dv?.toString() - urlencodedParam["type"] = "text" - } - return urlencodedParam - } - } -} diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/MappedJDialog.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/MappedJDialog.kt deleted file mode 100644 index 8cfe5d920..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/MappedJDialog.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.itangcent.idea.plugin.dialog - -import java.util.function.Function -import javax.swing.JDialog - -abstract class MappedJDialog : JDialog() { - - protected var resultHandle: Function?, Boolean>? = null - - protected fun onOK() { - if (this.resultHandle != null) { - if (!this.resultHandle!!.apply(collectInfo())) { - return - } - } - - // add your code here - dispose() - } - - protected fun onCancel() { - - if (this.resultHandle != null) { - this.resultHandle!!.apply(null) - } - - // add your code here if necessary - dispose() - } - - open fun onResult(resultHandle: Function?, Boolean>) { - this.resultHandle = resultHandle - } - - internal abstract fun collectInfo(): Map - -} diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/utils/FileSaveHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/utils/FileSaveHelper.kt new file mode 100644 index 000000000..02c1a5208 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/utils/FileSaveHelper.kt @@ -0,0 +1,103 @@ +package com.itangcent.idea.plugin.utils + +import com.google.inject.Inject +import com.google.inject.name.Named +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.logger.Logger +import com.itangcent.intellij.util.FileUtils +import com.itangcent.intellij.util.ToolUtils +import java.nio.charset.Charset + +class FileSaveHelper { + + + @Inject(optional = true) + @Named("file.save.default") + private val defaultExportedFile: String? = null + + @Inject(optional = true) + @Named("file.save.last.location.key") + private val lastImportedLocation: String? = null + + @Inject + private val project: Project? = null + + fun saveOrCopy(info: String?, + onCopy: () -> Unit, + onSaveSuccess: () -> Unit, + onSaveFailed: () -> Unit) { + + if (info == null) return + val actionContext = ActionContext.getContext()!! + + actionContext.runInSwingUI { + val descriptor = FileChooserDescriptorFactory + .createSingleFileOrFolderDescriptor() + .withTitle("Export location") + .withDescription("Choose directory to export api to") + .withHideIgnored(false) + val chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, project, null) + var toSelect: VirtualFile? = null + val lastLocation = PropertiesComponent.getInstance().getValue(getLastImportedLocation()) + if (lastLocation != null) { + toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(lastLocation) + } + val files = chooser.choose(project, toSelect) + if (files.isNotEmpty()) { + actionContext.runInWriteUI { + val file = files[0] + PropertiesComponent.getInstance().setValue(getLastImportedLocation(), file.path) + if (file.isDirectory) { + try { + val defaultFile = getDefaultExportedFile() + FileUtils.forceSave("${file.path}/$defaultFile", info.toByteArray(Charset.defaultCharset())) + onSaveSuccess() + } catch (e: Exception) { + onSaveFailed() + actionContext.runAsync { + copyAndLog(info, onCopy) + } + } + } else { + try { + FileUtils.forceSave(file, info.toByteArray(Charset.defaultCharset())) + onSaveSuccess() + } catch (e: Exception) { + onSaveFailed() + actionContext.runAsync { + copyAndLog(info, onCopy) + } + } + } + } + } else { + copyAndLog(info, onCopy) + } + } + } + + private fun copyAndLog(info: String, onCopy: () -> Unit) { + val logger: Logger = ActionContext.getContext()!!.instance(Logger::class) + ToolUtils.copy2Clipboard(info) + onCopy() + if (info.length > 10000) { + logger.info("Api data is too lager to show in console!") + } else { + logger.log(info) + } + } + + private fun getDefaultExportedFile(): String { + return defaultExportedFile ?: "api.json" + } + + private fun getLastImportedLocation(): String { + return lastImportedLocation ?: "com.itangcent.api.export.path" + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/intellij/extend/guice/Injectors.kt b/idea-plugin/src/main/kotlin/com/itangcent/intellij/extend/guice/Injectors.kt index 6957dac4a..6c5ef8e51 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/intellij/extend/guice/Injectors.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/intellij/extend/guice/Injectors.kt @@ -46,6 +46,7 @@ fun LinkedBindingBuilder.with(implementation: KClass): Scope /** * #todo:provider暂时无法完成PostConstruct */ +@Suppress("NOTHING_TO_INLINE") inline fun LinkedBindingBuilder.withProvider(noinline provider: () -> TE): ScopedBindingBuilder { return this.toProvider(com.google.inject.Provider { provider() }) } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index b0fceb94a..419888844 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ rootProject.name = 'easy-api' include 'idea-plugin' -include 'common' +include 'common-api'