feature/add_dynamic_endpoints_with_swagger: methodRouting can change request url, and url can have expression

This commit is contained in:
shuang 2020-03-30 12:46:55 +08:00
parent 9ac32017c0
commit fd089a9396
8 changed files with 150 additions and 33 deletions

View File

@ -1922,9 +1922,9 @@ object NewStyle {
}
}
}
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]],
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
RestConnector_vMar2019.dynamicEndpointProcess(url, jValue, method, params, callContext)
Connector.connector.vend.dynamicEndpointProcess(url, jValue, method, params, pathParams, callContext)
}

View File

@ -2866,9 +2866,9 @@ trait APIMethods400 {
"GET",
"/management/dynamic-endpoints",
" Get DynamicEndpoints",
s"""Get DynamicEndpoints.
s"""
|
|Get DynamicEndpoints,
|Get DynamicEndpoints.
|
|""",
emptyObjectJson,
@ -2936,12 +2936,12 @@ trait APIMethods400 {
lazy val dynamicEndpoint: OBPEndpoint = {
case DynamicReq(url, json, method, params, role) => { cc =>
case DynamicReq(url, json, method, params, pathParams, role) => { cc =>
for {
(Full(u), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, role, callContext)
(box, _) <- NewStyle.function.dynamicEndpointProcess(url, json, method, params, callContext)
(box, _) <- NewStyle.function.dynamicEndpointProcess(url, json, method, params, pathParams, callContext)
} yield {
box match {
case Full(v) => (v, HttpCode.`200`(Some(cc)))

View File

@ -70,12 +70,20 @@ object DynamicEndpointHelper extends RestHelper {
* extract request body, no matter GET, POST, PUT or DELETE method
*/
object DynamicReq extends JsonTest with JsonBody {
private val ExpressionRegx = """\{(.+?)\}""".r
/**
* unapply Request to (swagger url, json, http method, request parameters, role)
* unapply Request to (request url, json, http method, request parameters, path parameters, role)
* request url is current request url
* json is request body
* http method is request http method
* request parameters is http request parameters
* path parameters: /banks/{bankId}/users/{userId} bankId and userId corresponding key to value
* role is current endpoint required entitlement
* @param r HttpRequest
* @return
*/
def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], ApiRole)] = {
def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] = {
val partPath = r.path.partPath
if (!testResponse_?(r) || partPath.headOption != Option(urlPrefix))
None
@ -84,19 +92,29 @@ object DynamicEndpointHelper extends RestHelper {
val httpMethod = HttpMethod.valueOf(r.requestType.method)
// url that match original swagger endpoint.
val url = partPath.tail.mkString("/", "/", "")
val foundDynamicEndpoint: Optional[(DynamicEndpointInfo, ResourceDoc)] = dynamicEndpointInfos.stream()
.map[Option[(DynamicEndpointInfo, ResourceDoc)]](_.findDynamicEndpoint(httpMethod, url))
val foundDynamicEndpoint: Optional[(DynamicEndpointInfo, ResourceDoc, String)] = dynamicEndpointInfos.stream()
.map[Option[(DynamicEndpointInfo, ResourceDoc, String)]](_.findDynamicEndpoint(httpMethod, url))
.filter(_.isDefined)
.findFirst()
.map(_.get)
foundDynamicEndpoint.asScala
.flatMap[(String, JValue, AkkaHttpMethod, Map[String, List[String]], ApiRole)] { it =>
val (dynamicEndpointInfo, doc) = it
.flatMap[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] { it =>
val (dynamicEndpointInfo, doc, originalUrl) = it
val pathParams: Map[String, String] = if(originalUrl == url) {
Map.empty[String, String]
} else {
val tuples: Array[(String, String)] = StringUtils.split(originalUrl, "/").zip(partPath.tail)
tuples.collect {
case (ExpressionRegx(name), value) => name->value
}.toMap
}
val Some(role::_) = doc.roles
body(r).toOption
.orElse(Some(JNothing))
.map(t => (dynamicEndpointInfo.targetUrl(url), t, akkaHttpMethod, r.params, role))
.map(t => (dynamicEndpointInfo.targetUrl(url), t, akkaHttpMethod, r.params, pathParams, role))
}
}
@ -132,6 +150,12 @@ object DynamicEndpointHelper extends RestHelper {
private def swaggerToResourceDocs(openAPI: OpenAPI, id: String): DynamicEndpointInfo = {
val tags: List[ResourceDocTag] = List(ApiTag.apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle)
val serverUrl = {
val servers = openAPI.getServers
assert(!servers.isEmpty, s"swagger host is mandatory, but current swagger host is empty, id=$id")
servers.get(0).getUrl
}
val paths: mutable.Map[String, PathItem] = openAPI.getPaths.asScala
def entitlementSuffix(path: String) = Math.abs(path.hashCode).toString.substring(0, 3) // to avoid different swagger have same entitlement
val docs: mutable.Iterable[(ResourceDoc, String)] = for {
@ -151,7 +175,34 @@ object DynamicEndpointHelper extends RestHelper {
.orElse(Option(op.getDescription))
.filter(StringUtils.isNotBlank)
.map(_.capitalize)
.getOrElse(summary)
.getOrElse(summary) +
s"""
|
|MethodRouting settings example:
|```
|{
| "is_bank_id_exact_match":false,
| "method_name":"dynamicEndpointProcess",
| "connector_name":"rest_vMar2019",
| "bank_id_pattern":".*",
| "parameters":[
| {
| "key":"url_pattern",
| "value":"$serverUrl$path"
| },
| {
| "key":"http_method",
| "value":"$requestVerb"
| }
| {
| "key":"url",
| "value":"http://mydomain.com/xxx"
| }
| ]
|}
|```
|
|""".stripMargin
val exampleRequestBody: Product = getRequestExample(openAPI, op.getRequestBody)
val successResponseBody: Product = getResponseExample(openAPI, op.getResponses)
val errorResponseBodies: List[String] = List(
@ -169,7 +220,7 @@ object DynamicEndpointHelper extends RestHelper {
ApiRole.getOrCreateDynamicApiRole(roleName)
))
}
val connectorMethods = Some(List(s"""dynamicEntityProcess: parameters contains {"key": "entityName", "value": "$summary"}""")) //TODO temp
val connectorMethods = Some(List("dynamicEndpointProcess"))
val doc = ResourceDoc(
partialFunction,
implementedInApiVersion,
@ -189,18 +240,10 @@ object DynamicEndpointHelper extends RestHelper {
(doc, path)
}
val serverUrl = {
val servers = openAPI.getServers
if(servers.isEmpty) {
None
} else {
Some(servers.get(0).getUrl)
}
}
DynamicEndpointInfo(id, docs, serverUrl)
}
private val PathParamRegx = """\{(.+)\}""".r
private val PathParamRegx = """\{(.+?)\}""".r
private val WordBoundPattern = Pattern.compile("(?<=[a-z])(?=[A-Z])|-")
private def buildRequestUrl(path: String): String = {
@ -363,7 +406,7 @@ object DynamicEndpointHelper extends RestHelper {
* @param docsToUrl ResourceDoc to url that defined in swagger content
* @param serverUrl base url that defined in swagger content
*/
case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(ResourceDoc, String)], serverUrl: Option[String]) {
case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(ResourceDoc, String)], serverUrl: String) {
val resourceDocs: mutable.Iterable[ResourceDoc] = docsToUrl.map(_._1)
private val existsUrlToMethod: mutable.Iterable[(HttpMethod, String, ResourceDoc)] =
@ -373,14 +416,14 @@ case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(Resource
(HttpMethod.valueOf(doc.requestVerb), path, doc)
})
def findDynamicEndpoint(newMethod: HttpMethod, newUrl: String): Option[(DynamicEndpointInfo, ResourceDoc)] = existsUrlToMethod.find(it => {
def findDynamicEndpoint(newMethod: HttpMethod, newUrl: String): Option[(DynamicEndpointInfo, ResourceDoc, String)] = existsUrlToMethod.find(it => {
val (method, url, _) = it
isSameUrl(newUrl, url) && newMethod == method
}).map(this -> _._3)
}).map(it => (this, it._3, it._2))
def existsEndpoint(newMethod: HttpMethod, newUrl: String): Boolean = findDynamicEndpoint(newMethod, newUrl).isDefined
def targetUrl(url: String): String = s"""${serverUrl.get}$url"""
def targetUrl(url: String): String = s"""$serverUrl$url"""
/**
* check whether two url is the same:

View File

@ -59,6 +59,7 @@ import scala.concurrent.duration._
import scala.math.{BigDecimal, BigInt}
import scala.util.Random
import scala.reflect.runtime.universe.{MethodSymbol, typeOf}
import _root_.akka.http.scaladsl.model.HttpMethod
/*
So we can switch between different sources of resources e.g.
@ -2152,6 +2153,9 @@ trait Connector extends MdcLoggable {
requestBody: Option[JObject],
entityId: Option[String],
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)}
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)}
def createDirectDebit(bankId: String,
accountId: String,

View File

@ -2,6 +2,7 @@ package code.bankconnectors
import java.util.Date
import java.util.UUID.randomUUID
import scala.concurrent.duration._
import code.DynamicData.DynamicDataProvider
import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT}
@ -80,6 +81,7 @@ import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To}
import org.mindrot.jbcrypt.BCrypt
import scalacache.ScalaCache
import scalacache.guava.GuavaCache
import scala.collection.immutable.{List, Nil}
import com.openbankproject.commons.ExecutionContext.Implicits.global
@ -88,6 +90,7 @@ import scala.language.postfixOps
import scala.math.{BigDecimal, BigInt}
import scala.util.Random
import _root_.akka.http.scaladsl.model.HttpMethod
object LocalMappedConnector extends Connector with MdcLoggable {
@ -3041,6 +3044,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
}
}
/* delegate to rest connector
*/
override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
Connector.getConnectorInstance("rest_vMar2019").dynamicEndpointProcess(url,jValue, method, params, pathParams, callContext)
}
override def createDirectDebit(bankId: String,
accountId: String,
customerId: String,

View File

@ -1,7 +1,9 @@
package code
import java.lang.reflect.Method
import java.util.regex.Pattern
import akka.http.scaladsl.model.HttpMethod
import code.api.{APIFailureNewStyle, ApiVersionHolder}
import code.api.util.{CallContext, NewStyle}
import code.methodrouting.{MethodRouting, MethodRoutingT}
@ -97,6 +99,18 @@ package object bankconnectors extends MdcLoggable {
NewStyle.function.getMethodRoutings(Some(methodName))
.find(_.parameters.exists(it => it.key == "entityName" && it.value == entityName))
}
case _ if methodName == "dynamicEndpointProcess" => {
val Array(url: String, _, method: HttpMethod, _*) = args
NewStyle.function.getMethodRoutings(Some(methodName))
.find(routing => {
routing.parameters.exists(it => it.key == "http_method" && it.value.equalsIgnoreCase(method.value)) &&
routing.parameters.exists(it => it.key == "url")&&
routing.parameters.exists(
it => it.key == "url_pattern" &&
(it.value == url || Pattern.compile(it.value).matcher(url).matches())
)
})
}
case None => NewStyle.function.getMethodRoutings(Some(methodName), Some(false))
.find {routing =>
val bankIdPattern = routing.bankIdPattern

View File

@ -9327,16 +9327,52 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable
result
}
//TODO params process
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]],
override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
val urlInMethodRouting: Option[String] = MethodRoutingHolder.methodRouting match {
case _: EmptyBox => None
case Full(routing) => routing.parameters.find(_.key == "url").map(_.value)
}
val targetUrl = urlInMethodRouting.getOrElse(url)
val pathVariableRex = """\{:(.+?)\}""".r
val targetUrl = urlInMethodRouting.map { urlInRouting =>
val tuples: Iterator[(String, String)] = pathVariableRex.findAllMatchIn(urlInRouting).map{ regex =>
val expression = regex.group(0)
val paramName = regex.group(1)
val paramValue =
if(paramName.startsWith("body.")) {
val path = StringUtils.substringAfter(paramName, "body.")
val value = JsonUtils.getValueByPath(jValue, path)
JsonUtils.toString(value)
} else {
pathParams.get(paramName)
.orElse(params.get(paramName).flatMap(_.headOption)).getOrElse(throw new RuntimeException(s"param $paramName not exists."))
}
expression -> paramValue
}
(urlInRouting /: tuples) {(pre, kv)=>
pre.replace(kv._1, kv._2)
}
}.getOrElse(url)
val paramNameToValue = for {
(name, values) <- params
value <- values
param = s"$name=$value"
} yield param
val paramUrl: String =
if(params.isEmpty){
targetUrl
} else if(targetUrl.contains("?")) {
targetUrl + "&" + paramNameToValue.mkString("&")
} else {
targetUrl + "?" + paramNameToValue.mkString("&")
}
val jsonToSend = if(jValue == JNothing) "" else compactRender(jValue)
val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext)
val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext)
logger.debug(s"RestConnector_vMar2019 request is : $request")
val responseFuture = makeHttpRequest(request)

View File

@ -421,7 +421,7 @@ object JsonUtils {
* @param pathExpress path, can be prefix by - or !, e.g: "-result.count" "!value.isDeleted"
* @return given nested field value
*/
private def getValueByPath(jValue: JValue, pathExpress: String): JValue = {
def getValueByPath(jValue: JValue, pathExpress: String): JValue = {
pathExpress match {
case str if str.trim == "$root" || str.trim.isEmpty => jValue // if path is "$root" or "", return whole original json
case RegexBoolean(b) => JBool(b.toBoolean)
@ -480,4 +480,14 @@ object JsonUtils {
case v => v.values.toString == expectValue
}
}
def toString(jValue: JValue) = jValue match{
case JString(s) => s
case JInt(num) => num.toString()
case JDouble(num) => num.toString()
case JBool(b) => b.toString()
case JNothing => ""
case JNull => "null"
case v => json.compactRender(v)
}
}