Skip to content

Commit

Permalink
feat: support @RequestLine、@headers@param@Body (#676)
Browse files Browse the repository at this point in the history
  • Loading branch information
tangcent authored Dec 26, 2021
1 parent fb14b21 commit 1fb5577
Show file tree
Hide file tree
Showing 21 changed files with 773 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ class OnClassCondition : AnnotatedCondition<ConditionOnClass>() {

override fun matches(actionContext: ActionContext, annotation: ConditionOnClass): Boolean {
val project = actionContext.instance(Project::class)
return annotation.value.all {
PsiClassFinder.findClass(it, project) != null
}
return actionContext.callInReadUI {
return@callInReadUI annotation.value.all {
PsiClassFinder.findClass(it, project) != null
}
} ?: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ class OnMissingClassCondition : AnnotatedCondition<ConditionOnMissingClass>() {

override fun matches(actionContext: ActionContext, annotation: ConditionOnMissingClass): Boolean {
val project = actionContext.instance(Project::class)
return annotation.value.all {
PsiClassFinder.findClass(it, project) == null
}
return actionContext.callInReadUI {
return@callInReadUI annotation.value.all {
PsiClassFinder.findClass(it, project) == null
}
} ?: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ object ClassExportRuleKeys {
BooleanRuleMode.ANY
)

val IS_FEIGN_CTRL: RuleKey<Boolean> = SimpleRuleKey(
"class.is.feign.ctrl",
BooleanRuleMode.ANY
)

val IS_SPRING_CTRL: RuleKey<Boolean> = SimpleRuleKey(
"class.is.spring.ctrl",
arrayOf("class.is.ctrl"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,10 @@ fun ExportContext.methodContext(): MethodExportContext? {

fun ExportContext.paramContext(): ParameterExportContext? {
return this.findContext(ParameterExportContext::class)
}

fun <T> ExportContext.searchExt(attr: String): T? {
this.getExt<T>(attr)?.let { return it }
this.parent()?.searchExt<T>(attr)?.let { return it }
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ package com.itangcent.idea.plugin.api.export.feign

import com.google.inject.Singleton
import com.intellij.psi.PsiClass
import com.itangcent.common.kit.equalIgnoreCase
import com.itangcent.common.model.Header
import com.itangcent.common.model.PathParam
import com.itangcent.common.model.Request
import com.itangcent.common.utils.Extensible
import com.itangcent.common.utils.notNullOrEmpty
import com.itangcent.idea.condition.annotation.ConditionOnClass
import com.itangcent.idea.plugin.api.export.condition.ConditionOnSimple
import com.itangcent.idea.plugin.api.export.core.ClassExportContext
import com.itangcent.idea.plugin.api.export.core.*
import com.itangcent.idea.plugin.api.export.spring.SpringRequestClassExporter
import com.itangcent.idea.plugin.condition.ConditionOnSetting
import org.apache.commons.lang.StringUtils.lowerCase

/**
* Support export apis from client that annotated with @FeignClient
Expand All @@ -15,14 +22,156 @@ import com.itangcent.idea.plugin.condition.ConditionOnSetting
*/
@Singleton
@ConditionOnSimple(false)
@ConditionOnClass(SpringFeignClassName.FEIGN_CLIENT_ANNOTATION)
@ConditionOnClass(SpringFeignClassName.REQUEST_LINE_ANNOTATION)
@ConditionOnSetting("feignEnable")
open class FeignRequestClassExporter : SpringRequestClassExporter() {
override fun processClass(cls: PsiClass, classExportContext: ClassExportContext) {
//NOP
}

override fun hasApi(psiClass: PsiClass): Boolean {
return annotationHelper!!.hasAnn(psiClass, SpringFeignClassName.FEIGN_CLIENT_ANNOTATION)
return annotationHelper.hasAnn(psiClass, SpringFeignClassName.FEIGN_CLIENT_ANNOTATION)
|| (ruleComputer.computer(ClassExportRuleKeys.IS_FEIGN_CTRL, psiClass) ?: false)
}

override fun processMethod(methodExportContext: MethodExportContext, request: Request) {
super.processMethod(methodExportContext, request)

val methodPsiElement = methodExportContext.psi()

//resolve path variable
request.path?.urls()?.forEach { url ->
resolveTemplate(url) { variables ->
methodExportContext.addVariables(variables)
for (variable in variables) {
val pathParam = PathParam()
pathParam.name = variable
requestBuilderListener.addPathParam(methodExportContext, request, pathParam)
methodExportContext.setExt("$variable-ref", pathParam)
}
}
}

//resolve @Headers
val headers = annotationHelper.findAttr(methodPsiElement, SpringFeignClassName.HEADERS_ANNOTATION)
if (headers != null) {
if (headers is Array<*>) {
headers.forEach { header ->
resolveHeader(header, methodExportContext, request)
}
} else if (headers is String) {
resolveHeader(headers, methodExportContext, request)
}
}

//resolve @Body
val body = annotationHelper.findAttrAsString(methodPsiElement, SpringFeignClassName.BODY_ANNOTATION)
if (body != null) {
resolveTemplate(methodExportContext, body)
}
}

private fun resolveHeader(
header: Any?,
methodExportContext: MethodExportContext,
request: Request
) {
(header as? String)?.let { resolveHeader(it) }
?.let {
val name = it.first
val value = it.second.trim()
if (name.equalIgnoreCase("content-type")) {
if (lowerCase(value).contains("application/json")) {
methodExportContext.setExt("paramType", "body")
}
}
val headerInRequest = requestBuilderListener.addHeader(methodExportContext, request, name, value)
resolveTemplate(value) { variables ->
methodExportContext.addVariables(variables)
for (variable in variables) {
methodExportContext.setExt("$variable-ref", headerInRequest)
}
}
}
}

private fun resolveTemplate(
methodExportContext: MethodExportContext,
value: String
) {
resolveTemplate(value) {
methodExportContext.addVariables(it)
}
}

private fun resolveTemplate(
value: String,
handle: (List<String>) -> Unit
) {
FeignTemplate.parseVariables(value)
.takeIf { it.notNullOrEmpty() }
?.let { handle(it) }
}

override fun processMethodParameter(
request: Request,
parameterExportContext: ParameterExportContext,
paramDesc: String?
) {
val parameter = parameterExportContext.psi()

//resolve @Param
val paramAnn = annotationHelper.findAttrAsString(parameter, SpringFeignClassName.PARAM_ANNOTATION)
if (paramAnn != null) {

val readParamDefaultValue = readParamDefaultValue(parameterExportContext.element())

var ultimateComment = (paramDesc ?: "")
parameterExportContext.type()?.let { duckType ->
commentResolver!!.resolveCommentForType(duckType, parameterExportContext.psi())?.let {
ultimateComment = "$ultimateComment $it"
}
}

val ref = parameterExportContext.methodContext()?.getExt<Any>("$paramAnn-ref")
if (ref != null) {
when (ref) {
is Header -> {
ref.desc = ultimateComment
}
is PathParam -> {
ref.desc = ultimateComment
}
}
return
}

addParamAsQuery(
parameterExportContext, request, readParamDefaultValue
?: parameterExportContext.unbox(), ultimateComment
)
return
}

super.processMethodParameter(request, parameterExportContext, paramDesc)
}

private fun resolveHeader(header: String): Pair<String, String>? {
val index = header.indexOf(':')
if (index == -1) {
logger.info("illegal header:$header")
return null
}
return header.substring(0, index) to header.substring(index + 1)
}

private fun Extensible.addVariables(variables: List<String>) {
var cacheVariables = this.getExt<List<String>>("VARIABLES")
if (cacheVariables == null) {
cacheVariables = ArrayList<String>().also { it.addAll(variables) }
this.setExt("VARIABLES", cacheVariables)
} else {
(cacheVariables as MutableList<String>).addAll(variables)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.itangcent.idea.plugin.api.export.feign

object FeignTemplate {

/**
* Parse variables from a template fragment.
*
* @param fragment to parse
* @return variables
*/
fun parseVariables(fragment: String): List<String> {
val variables = ArrayList<String>()
val tokenizer = ChunkTokenizer(fragment)
while (tokenizer.hasNext()) {
val chunk = tokenizer.next()
if (chunk.startsWith("{")) {
variables.add(chunk.removePrefix("{").removeSuffix("}"))
}
}
return variables
}
}

/**
* Splits a Uri into Chunks that exists inside and outside of an expression, delimited by curly
* braces "{}". Nested expressions are treated as literals, for example "foo{bar{baz}}" will be
* treated as "foo, {bar{baz}}". Inspired by Apache CXF Jax-RS.
*/
private class ChunkTokenizer(template: String) {
private val tokens: MutableList<String> = ArrayList()
private var index = 0
operator fun hasNext(): Boolean {
return tokens.size > index
}

operator fun next(): String {
if (hasNext()) {
return tokens[index++]
}
throw IllegalStateException("No More Elements")
}

init {
var outside = true
var level = 0
var lastIndex = 0

/* loop through the template, character by character */
var idx = 0
while (idx < template.length) {
if (template[idx] == '{') {
/* start of an expression */
if (outside) {
/* outside of an expression */
if (lastIndex < idx) {
/* this is the start of a new token */
tokens.add(template.substring(lastIndex, idx))
}
lastIndex = idx

/*
* no longer outside of an expression, additional characters will be treated as in an
* expression
*/
outside = false
} else {
/* nested braces, increase our nesting level */
level++
}
} else if (template[idx] == '}' && !outside) {
/* the end of an expression */
if (level > 0) {
/*
* sometimes we see nested expressions, we only want the outer most expression
* boundaries.
*/
level--
} else {
/* outermost boundary */
if (lastIndex < idx) {
/* this is the end of an expression token */
tokens.add(template.substring(lastIndex, idx + 1))
}
lastIndex = idx + 1

/* outside an expression */outside = true
}
}
idx++
}
if (lastIndex < idx) {
/* grab the remaining chunk */
tokens.add(template.substring(lastIndex, idx))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.itangcent.idea.plugin.api.export.feign

import com.google.inject.Inject
import com.google.inject.Singleton
import com.intellij.psi.PsiElement
import com.itangcent.idea.condition.annotation.ConditionOnClass
import com.itangcent.idea.plugin.api.export.spring.SpringRequestMappingResolver
import com.itangcent.idea.plugin.condition.ConditionOnSetting
import com.itangcent.intellij.jvm.AnnotationHelper
import com.itangcent.intellij.logger.Logger
import com.itangcent.intellij.psi.PsiClassUtils
import com.itangcent.order.Order
import com.itangcent.order.Ordered
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
* Support @RequestLine
*/
@Singleton
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionOnClass(SpringFeignClassName.REQUEST_LINE_ANNOTATION)
@ConditionOnSetting("feignEnable")
class RequestLineRequestMappingResolver : SpringRequestMappingResolver {

@Inject
private lateinit var logger: Logger

@Inject
private lateinit var annotationHelper: AnnotationHelper

/**
* see [https://github.com/OpenFeign/feign/blob/f685f76002370413965f8aafea0ea96843b3c806/core/src/main/java/feign/Contract.java#L253-L270]
*
* @param psiElement annotated element(PsiMethod/PsiClass)
* @return annotation attributes
*/
override fun resolveRequestMapping(psiElement: PsiElement): Map<String, Any?>? {
val requestLineValue =
annotationHelper.findAttrAsString(psiElement, SpringFeignClassName.REQUEST_LINE_ANNOTATION) ?: return null

if (requestLineValue.isEmpty()) {
logger.error("RequestLine annotation was empty on method ${PsiClassUtils.fullNameOfMember(psiElement)}.")
return null
}
val requestLineMatcher: Matcher = REQUEST_LINE_PATTERN.matcher(requestLineValue)
if (!requestLineMatcher.find()) {
logger.error(
"RequestLine annotation didn't start with an HTTP verb on method ${
PsiClassUtils.fullNameOfMember(
psiElement
)
}"
)
return null
}
return mapOf("method" to requestLineMatcher.group(1),
"value" to requestLineMatcher.group(2))
}

companion object {
val REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$")!!
}
}
Loading

0 comments on commit 1fb5577

Please sign in to comment.