diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 0a09123ea..9bfa4b9ca 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -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) } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 95a20f51b..578819b01 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -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))) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala index 0091bf1ef..dd43c7249 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala @@ -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: diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 4b5ae7051..fe4faccd5 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -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, diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index ab01ff068..fa3ad1a46 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -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, diff --git a/obp-api/src/main/scala/code/bankconnectors/package.scala b/obp-api/src/main/scala/code/bankconnectors/package.scala index ead2c35b8..44120f248 100644 --- a/obp-api/src/main/scala/code/bankconnectors/package.scala +++ b/obp-api/src/main/scala/code/bankconnectors/package.scala @@ -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 diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index c85069cab..e909bc495 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -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) diff --git a/obp-api/src/main/scala/code/util/JsonUtils.scala b/obp-api/src/main/scala/code/util/JsonUtils.scala index a632ba58c..f865bbba8 100644 --- a/obp-api/src/main/scala/code/util/JsonUtils.scala +++ b/obp-api/src/main/scala/code/util/JsonUtils.scala @@ -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) + } }