diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 40d044754..b3861b82e 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -8,7 +8,7 @@ com.tesobe obp-parent ../pom.xml - 1.8.2 + 1.9.0 obp-api war diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 7e5792baf..26858e265 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -99,6 +99,8 @@ import code.scope.{MappedScope, MappedUserScope} import code.apicollectionendpoint.ApiCollectionEndpoint import code.apicollection.ApiCollection import code.connectormethod.ConnectorMethod +import code.dynamicMessageDoc.DynamicMessageDoc +import code.dynamicResourceDoc.DynamicResourceDoc import code.snippet.{OAuthAuthorisation, OAuthWorkedThanks} import code.socialmedia.MappedSocialMedia import code.standingorders.StandingOrder @@ -123,7 +125,6 @@ import code.webuiprops.WebUiProps import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.Functions.Implicits._ import com.openbankproject.commons.util.{ApiVersion, Functions} - import javax.mail.internet.MimeMessage import net.liftweb.common._ import net.liftweb.db.DBLogEntry @@ -902,7 +903,9 @@ object ToSchemify { ApiCollectionEndpoint, JsonSchemaValidation, AuthenticationTypeValidation, - ConnectorMethod + ConnectorMethod, + DynamicResourceDoc, + DynamicMessageDoc )++ APIBuilder_Connector.allAPIBuilderModels // start grpc server diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 893aa4b9e..e7d51e5da 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -12,7 +12,8 @@ import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} import code.api.v2_2_0.{APIMethods220, OBPAPI2_2_0} import code.api.v3_0_0.OBPAPI3_0_0 import code.api.v3_1_0.OBPAPI3_1_0 -import code.api.v4_0_0.{APIMethods400, DynamicEndpointHelper, DynamicEntityHelper, OBPAPI4_0_0} +import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEndpoints, DynamicEntityHelper} +import code.api.v4_0_0.{APIMethods400, OBPAPI4_0_0} import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider import code.util.Helper.MdcLoggable import com.github.dwickern.macros.NameOf.nameOf @@ -237,7 +238,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth resourceDocTags: Option[List[ResourceDocTag]], partialFunctionNames: Option[List[String]] ): Option[JValue] = { - val dynamicDocs = (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc) + val dynamicDocs = (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs) .filter(rd => rd.implementedInApiVersion == requestedApiVersion) .map(it => it.specifiedUrl match { case Some(_) => it @@ -613,7 +614,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth resourceDocs match { case docs @Some(_) => resourceDocsToJValue(docs) case _ => - val dynamicDocs = (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc).toList + val dynamicDocs = (DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs).toList resourceDocsToJValue(Some(dynamicDocs)) } } diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 116ab55da..7752ae1fd 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -15,11 +15,13 @@ import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_0_0.{LobbyJsonV330, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, CustomerWithAttributesJsonV310, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} -import code.api.v4_0_0.{APIInfoJson400, AccountBalanceJsonV400, AccountTagJSON, AccountTagsJSON, AccountsBalancesJsonV400, ApiCollectionEndpointJson400, ApiCollectionEndpointsJson400, ApiCollectionJson400, ApiCollectionsJson400, AttributeDefinitionJsonV400, AttributeDefinitionResponseJsonV400, AttributeDefinitionsResponseJsonV400, AttributeJsonV400, BalanceJsonV400, BankAccountRoutingJson, BankJson400, BanksJson400, CallLimitPostJsonV400, ChallengeAnswerJson400, ChallengeJsonV400, CounterpartiesJson400, CounterpartyJson400, CounterpartyWithMetadataJson400, CustomerAttributeJsonV400, CustomerAttributesResponseJson, DirectDebitJsonV400, DoubleEntryTransactionJson, EnergySource400, HostedAt400, HostedBy400, IbanCheckerJsonV400, IbanDetailsJsonV400, JsonSchemaV400, JsonValidationV400, LogoutLinkJson, ModeratedAccountJSON400, ModeratedAccountsJSON400, ModeratedCoreAccountJsonV400, ModeratedFirehoseAccountJsonV400, ModeratedFirehoseAccountsJsonV400, PostAccountAccessJsonV400, PostAccountTagJSON, PostApiCollectionEndpointJson400, PostApiCollectionJson400, PostCounterpartyJson400, PostCustomerPhoneNumberJsonV400, PostDirectDebitJsonV400, PostRevokeGrantAccountAccessJsonV400, PostStandingOrderJsonV400, PostViewJsonV400, Properties, RefundJson, RevokedJsonV400, SettlementAccountJson, SettlementAccountRequestJson, SettlementAccountResponseJson, SettlementAccountsJson, StandingOrderJsonV400, TransactionAttributeJsonV400, TransactionAttributeResponseJson, TransactionAttributesResponseJson, TransactionBankAccountJson, TransactionRequestAttributeJsonV400, TransactionRequestAttributeResponseJson, TransactionRequestAttributesResponseJson, TransactionRequestBankAccountJson, TransactionRequestBodyRefundJsonV400, TransactionRequestBodySEPAJsonV400, TransactionRequestReasonJsonV400, TransactionRequestRefundFrom, TransactionRequestRefundTo, TransactionRequestWithChargeJSON400, UpdateAccountJsonV400, UserLockStatusJson, When, XxxId} +import code.api.v4_0_0.{APIInfoJson400, AccountBalanceJsonV400, AccountTagJSON, AccountTagsJSON, AccountsBalancesJsonV400, ApiCollectionEndpointJson400, ApiCollectionEndpointsJson400, ApiCollectionJson400, ApiCollectionsJson400, AttributeDefinitionJsonV400, AttributeDefinitionResponseJsonV400, AttributeDefinitionsResponseJsonV400, AttributeJsonV400, BalanceJsonV400, BankAccountRoutingJson, BankJson400, BanksJson400, CallLimitPostJsonV400, ChallengeAnswerJson400, ChallengeJsonV400, CounterpartiesJson400, CounterpartyJson400, CounterpartyWithMetadataJson400, CustomerAttributeJsonV400, CustomerAttributesResponseJson, DirectDebitJsonV400, DoubleEntryTransactionJson, EnergySource400, HostedAt400, HostedBy400, IbanCheckerJsonV400, IbanDetailsJsonV400, JsonSchemaV400, JsonValidationV400, LogoutLinkJson, ModeratedAccountJSON400, ModeratedAccountsJSON400, ModeratedCoreAccountJsonV400, ModeratedFirehoseAccountJsonV400, ModeratedFirehoseAccountsJsonV400, PostAccountAccessJsonV400, PostAccountTagJSON, PostApiCollectionEndpointJson400, PostApiCollectionJson400, PostCounterpartyJson400, PostCustomerPhoneNumberJsonV400, PostDirectDebitJsonV400, PostRevokeGrantAccountAccessJsonV400, PostStandingOrderJsonV400, PostViewJsonV400, Properties, RefundJson, ResourceDocFragment, RevokedJsonV400, SettlementAccountJson, SettlementAccountRequestJson, SettlementAccountResponseJson, SettlementAccountsJson, StandingOrderJsonV400, TransactionAttributeJsonV400, TransactionAttributeResponseJson, TransactionAttributesResponseJson, TransactionBankAccountJson, TransactionRequestAttributeJsonV400, TransactionRequestAttributeResponseJson, TransactionRequestAttributesResponseJson, TransactionRequestBankAccountJson, TransactionRequestBodyRefundJsonV400, TransactionRequestBodySEPAJsonV400, TransactionRequestReasonJsonV400, TransactionRequestRefundFrom, TransactionRequestRefundTo, TransactionRequestWithChargeJSON400, UpdateAccountJsonV400, UserLockStatusJson, When, XxxId} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} import code.branches.Branches.{Branch, DriveUpString, LobbyString} import code.consent.ConsentStatus import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody} +import code.dynamicMessageDoc.JsonDynamicMessageDoc +import code.dynamicResourceDoc.JsonDynamicResourceDoc import code.sandbox.SandboxData import code.transactionrequests.TransactionRequests.TransactionChallengeTypes import code.transactionrequests.TransactionRequests.TransactionRequestTypes._ @@ -29,7 +31,9 @@ import com.openbankproject.commons.model.PinResetReason.{FORGOT, GOOD_SECURITY_P import com.openbankproject.commons.model.enums.{AttributeCategory, CardAttributeType} import com.openbankproject.commons.model.{UserAuthContextUpdateStatus, ViewBasic, _} import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, ReflectUtils, RequiredArgs, RequiredInfo} +import net.liftweb.json +import java.net.URLEncoder import scala.collection.immutable.List /** @@ -4063,11 +4067,48 @@ object SwaggerDefinitionsJSON { val apiCollectionEndpointJson400 = ApiCollectionEndpointJson400(apiCollectionEndpointIdExample.value, apiCollectionIdExample.value, operationIdExample.value) val apiCollectionEndpointsJson400 = ApiCollectionEndpointsJson400(List(apiCollectionEndpointJson400)) - // the reason of declared as def instead of val: avoid be scanned by allFields field - private def getBankMethodBody = "%20%20%20%20%20%20Future.successful%28%0A%20%20%20%20%20%20%20%20Full%28%28BankCommons%28%0A%20%20%20%20%20%20%20%20%20%20BankId%28%22Hello%20bank%20id%22%29%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%228%22%0A%20%20%20%20%20%20%20%20%29%2C%20None%29%29%0A%20%20%20%20%20%20%29" - val jsonConnectorMethod = JsonConnectorMethod(Some(""),"getBank", getBankMethodBody) - val jsonConnectorMethodMethodBody = JsonConnectorMethodMethodBody(getBankMethodBody) + val jsonConnectorMethod = JsonConnectorMethod(Some(connectorMethodIdExample.value),"getBank", connectorMethodBodyExample.value) + val jsonConnectorMethodMethodBody = JsonConnectorMethodMethodBody(connectorMethodBodyExample.value) + val jsonDynamicResourceDoc = JsonDynamicResourceDoc( + dynamicResourceDocId = Some(dynamicResourceDocIdExample.value), + methodBody = dynamicResourceDocMethodBodyExample.value, + partialFunctionName = dynamicResourceDocPartialFunctionNameExample.value, + requestVerb = requestVerbExample.value, + requestUrl = requestUrlExample.value, + summary = dynamicResourceDocSummaryExample.value, + description = dynamicResourceDocdescriptionExample.value, + exampleRequestBody = Option(json.parse(exampleRequestBodyExample.value)), + successResponseBody = Option(json.parse(successResponseBodyExample.value)), + errorResponseBodies = errorResponseBodiesExample.value, + tags = tagsExample.value, + roles = rolesExample.value + ) + + val jsonDynamicMessageDoc = JsonDynamicMessageDoc( + dynamicMessageDocId = Some(dynamicMessageDocIdExample.value), + process = processExample.value, + messageFormat = messageFormatExample.value, + description = descriptionExample.value, + outboundTopic = outboundTopicExample.value, + inboundTopic = inboundTopicExample.value, + exampleOutboundMessage = json.parse(exampleOutboundMessageExample.value), + exampleInboundMessage = json.parse(exampleInboundMessageExample.value), + outboundAvroSchema = outboundAvroSchemaExample.value, + inboundAvroSchema = inboundAvroSchemaExample.value, + adapterImplementation = adapterImplementationExample.value, + methodBody = connectorMethodBodyExample.value + ) + + val jsonResourceDocFragment = ResourceDocFragment( + requestVerbExample.value, + requestUrlExample.value, + exampleRequestBody = Option(json.parse(exampleRequestBodyExample.value)), + successResponseBody = Option(json.parse(successResponseBodyExample.value)) + ) + + val jsonCodeTemplate = "code" -> URLEncoder.encode("""println("hello")""", "UTF-8") + //The common error or success format. //Just some helper format to use in Json case class NotSupportedYet() diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 97ae92a60..f4d680580 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -41,12 +41,13 @@ import code.api.builder.OBP_APIBuilder import code.api.oauth1a.Arithmetics import code.api.oauth1a.OauthParams._ import code.api.sandbox.SandboxApiCalls +import code.api.util.APIUtil.ResourceDoc.{findPathVariableNames, isPathVariable} import code.api.util.ApiTag.{ResourceDocTag, apiTagBank, apiTagNewStyle} import code.api.util.Glossary.GlossaryItem import code.api.util.RateLimitingJson.CallLimit import code.api.v1_2.ErrorMessage import code.api.{DirectLogin, _} -import code.api.v4_0_0.{DynamicEndpointHelper, DynamicEntityHelper} +import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEndpoints, DynamicEntityHelper} import code.authtypevalidation.AuthenticationTypeValidationProvider import code.bankconnectors.Connector import code.consumer.Consumers @@ -1145,6 +1146,32 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ // So create the EmptyClassJson to set the empty JValue "{}" case class EmptyClassJson(jsonString: String ="{}") + /** + * PrimaryDataBody is used to make the following ResourceDoc.exampleRequestBody and ResourceDoc.successResponseBody to + * support the primitive types(String, Int, Boolean....) + * also see@ case class ResourceDoc -> exampleRequestBody: scala.Product and successResponseBody: scala.Product + * It is `product` type, not support the primitive scala types. + * + * following are some usages eg: + * 1st: we support empty string for `Create Dynamic Resource Doc` endpoint, json body.example_request_body = "", + * 2rd: Swagger file use Boolean as response body. + * ..... + * + * + * Here are the sub-classes of PrimaryDataBody, they can be used for scala.Product type. + * JArrayBody in APIUtil$ (code.api.util) + * FloatBody in APIUtil$ (code.api.util) + * IntBody in APIUtil$ (code.api.util) + * DoubleBody in APIUtil$ (code.api.util) + * BigDecimalBody in APIUtil$ (code.api.util) + * BooleanBody in APIUtil$ (code.api.util) + * StringBody in APIUtil$ (code.api.util) + * LongBody in APIUtil$ (code.api.util) + * BigIntBody in APIUtil$ (code.api.util) + * EmptyBody$ in APIUtil$ (code.api.util) + * + * @tparam T + */ sealed abstract class PrimaryDataBody[T] extends JsonAble { def value: T @@ -1194,7 +1221,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ case class JArrayBody(value: JArray) extends PrimaryDataBody[JArray] /** - * Any dynamic endpoint'ResourceDoc, it's partialFunction should set this stub endpoint. + * Any dynamic endpoint's ResourceDoc, it's partialFunction should set this stub endpoint. */ val dynamicEndpointStub: OBPEndpoint = Functions.doNothing @@ -1202,7 +1229,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ private val operationIdToResourceDoc: ConcurrentHashMap[String, ResourceDoc] = new ConcurrentHashMap[String, ResourceDoc] def getResourceDocs(operationIds: List[String]): List[ResourceDoc] = { - val dynamicDocs = DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc + val dynamicDocs = DynamicEntityHelper.doc ++ DynamicEndpointHelper.doc ++ DynamicEndpoints.dynamicResourceDocs operationIds.collect { case operationId if operationIdToResourceDoc.containsKey(operationId) => operationIdToResourceDoc.get(operationId) @@ -1210,6 +1237,16 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ dynamicDocs.find(_.operationId == operationId).orNull } } + + /** + * check whether url part is path variable + * @param urlFragment + * @return e.g: the url is /abc/ABC_ID/hello, ABC_ID is path variable + */ + def isPathVariable(urlFragment: String) = urlFragment == urlFragment.toUpperCase() + + def findPathVariableNames(url: String) = StringUtils.split(url, '/') + .filter(isPathVariable(_)) } // Used to document the API calls @@ -1342,17 +1379,19 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ private val requestUrlPartPath: Array[String] = StringUtils.split(requestUrl, '/') - private val requestUrlBankId = requestUrlPartPath.indexOf("BANK_ID") - private val requestUrlAccountId = requestUrlPartPath.indexOf("ACCOUNT_ID") - private val requestUrlViewId = requestUrlPartPath.indexOf("VIEW_ID") - private val isNeedCheckAuth = errorResponseBodies.contains($UserNotLoggedIn) private val isNeedCheckRoles = _autoValidateRoles && rolesForCheck.nonEmpty - private val isNeedCheckBank = errorResponseBodies.contains($BankNotFound) && requestUrlBankId != -1 + private val isNeedCheckBank = errorResponseBodies.contains($BankNotFound) && requestUrlPartPath.contains("BANK_ID") private val isNeedCheckAccount = errorResponseBodies.contains($BankAccountNotFound) && - requestUrlBankId != -1 && requestUrlAccountId != -1 + requestUrlPartPath.contains("BANK_ID") && requestUrlPartPath.contains("ACCOUNT_ID") private val isNeedCheckView = errorResponseBodies.contains($UserNoPermissionAccessView) && - requestUrlBankId != -1 && requestUrlAccountId != -1 && requestUrlViewId != -1 + requestUrlPartPath.contains("BANK_ID") && requestUrlPartPath.contains("ACCOUNT_ID") && requestUrlPartPath.contains("VIEW_ID") + + private val reversedRequestUrl = requestUrlPartPath.reverse + def getPathParams(url: List[String]): Map[String, String] = + reversedRequestUrl.zip(url.reverse) collect { + case pair @(k, _) if isPathVariable(k) => pair + } toMap /** * According errorResponseBodies whether contains UserNotLoggedIn and UserHasMissingRoles do validation. @@ -1370,29 +1409,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def wrappedWithAuthCheck(obpEndpoint: OBPEndpoint): OBPEndpoint = { _isEndpointAuthCheck = true - def getIds(url: List[String]): (Option[BankId], Option[AccountId], Option[ViewId]) = { - val apiPrefixLength = url.size - requestUrlPartPath.size - - val bankId = if (requestUrlBankId != -1) { - url.lift(apiPrefixLength + requestUrlBankId).map(BankId(_)) - } else { - None - } - val accountId = if (bankId.isDefined && requestUrlAccountId != -1) { - url.lift(apiPrefixLength + requestUrlAccountId).map(AccountId(_)) - } else { - None - } - - val viewId = if (accountId.isDefined && requestUrlViewId != -1) { - url.lift(apiPrefixLength + requestUrlViewId).map(ViewId(_)) - } else { - None - } - - (bankId, accountId, viewId) - } - def checkAuth(cc: CallContext): Future[(Box[User], Option[CallContext])] = { if (isNeedCheckAuth) authenticatedAccessFun(cc) else anonymousAccessFun(cc) } @@ -1469,39 +1485,32 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val isUrlMatchesResourceDocUrl: List[String] => Boolean = { val urlInDoc = StringUtils.split(this.requestUrl, '/') - val indices = urlInDoc.indices - // all path parameter indices - // whether url part is path parameter, e.g: BAR_ID in the path /obp/v4.0.0/foo/bar/BAR_ID - val pathParamIndices = { - for { - index <- indices - urlPart = urlInDoc(index) - if urlPart.toUpperCase == urlPart - } yield index - }.toSet + val pathVariableNames = findPathVariableNames(this.requestUrl) (requestUrl: List[String]) => { if (requestUrl == urlInDoc) { true } else { (requestUrl.size == urlInDoc.size) && - indices.forall { index => - val requestUrlPart = requestUrl(index) - val docUrlPart = urlInDoc(index) - requestUrlPart == docUrlPart || pathParamIndices.contains(index) + urlInDoc.zip(requestUrl).forall { + case (k, v) => + k == v || pathVariableNames.contains(k) } } } } new OBPEndpoint { - override def isDefinedAt(x: Req): Boolean = obpEndpoint.isDefinedAt(x) && isUrlMatchesResourceDocUrl(x.path.partPath) + override def isDefinedAt(x: Req): Boolean = + obpEndpoint.isDefinedAt(x) && isUrlMatchesResourceDocUrl(x.path.partPath) override def apply(req: Req): CallContext => Box[JsonResponse] = { val originFn: CallContext => Box[JsonResponse] = obpEndpoint.apply(req) - - val (bankId, accountId, viewId) = getIds(req.path.partPath) + val pathParams = getPathParams(req.path.partPath) + val bankId = pathParams.get("BANK_ID").map(BankId(_)) + val accountId = pathParams.get("ACCOUNT_ID").map(AccountId(_)) + val viewId = pathParams.get("VIEW_ID").map(ViewId(_)) val request: Box[Req] = S.request val session: Box[LiftSession] = S.session diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 2b23510c7..76d6217ab 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -1,7 +1,7 @@ package code.api.util import java.util.concurrent.ConcurrentHashMap -import code.api.v4_0_0.{DynamicEndpointHelper, DynamicEntityHelper} +import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEntityHelper} import com.openbankproject.commons.util.{JsonAble, ReflectUtils} import net.liftweb.json.{Formats, JsonAST} import net.liftweb.json.JsonDSL._ @@ -662,6 +662,36 @@ object ApiRole { case class CanGetAllConnectorMethods(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAllConnectorMethods = CanGetAllConnectorMethods() + + case class CanCreateDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateDynamicResourceDoc = CanCreateDynamicResourceDoc() + + case class CanUpdateDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateDynamicResourceDoc = CanUpdateDynamicResourceDoc() + + case class CanGetDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDynamicResourceDoc = CanGetDynamicResourceDoc() + + case class CanGetAllDynamicResourceDocs(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAllDynamicResourceDocs = CanGetAllDynamicResourceDocs() + + case class CanDeleteDynamicResourceDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canDeleteDynamicResourceDoc = CanDeleteDynamicResourceDoc() + + case class CanCreateDynamicMessageDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateDynamicMessageDoc = CanCreateDynamicMessageDoc() + + case class CanUpdateDynamicMessageDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canUpdateDynamicMessageDoc = CanUpdateDynamicMessageDoc() + + case class CanGetDynamicMessageDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetDynamicMessageDoc = CanGetDynamicMessageDoc() + + case class CanGetAllDynamicMessageDocs(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetAllDynamicMessageDocs = CanGetAllDynamicMessageDocs() + + case class CanDeleteDynamicMessageDoc(requiresBankId: Boolean = false) extends ApiRole + lazy val canDeleteDynamicMessageDoc = CanDeleteDynamicMessageDoc() private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole] diff --git a/obp-api/src/main/scala/code/api/util/ApiTag.scala b/obp-api/src/main/scala/code/api/util/ApiTag.scala index 44bc3df5c..f5d1077c3 100644 --- a/obp-api/src/main/scala/code/api/util/ApiTag.scala +++ b/obp-api/src/main/scala/code/api/util/ApiTag.scala @@ -69,7 +69,9 @@ object ApiTag { val apiTagMethodRouting = ResourceDocTag("Method-Routing") val apiTagWebUiProps = ResourceDocTag("WebUi-Props") val apiTagManageDynamicEntity = ResourceDocTag("Dynamic-Entity-(Manage)") - val apiTagManageDynamicEndpoint = ResourceDocTag("Dynamic-Endpoint-(Manage)") + val apiTagDynamicSwaggerDoc = ResourceDocTag("Dynamic-Swagger-Doc-(Manage)") + val apiTagDynamicResourceDoc = ResourceDocTag("Dynamic-Resource-Doc-(Manage)") + val apiTagDynamicMessageDoc = ResourceDocTag("Dynamic-Message-Doc-(Manage)") val apiTagApiCollection = ResourceDocTag("Api-Collection") val apiTagDynamic = ResourceDocTag("Dynamic") diff --git a/obp-api/src/main/scala/code/api/util/CustomJsonFormats.scala b/obp-api/src/main/scala/code/api/util/CustomJsonFormats.scala index 4211004dc..1bf30a24f 100644 --- a/obp-api/src/main/scala/code/api/util/CustomJsonFormats.scala +++ b/obp-api/src/main/scala/code/api/util/CustomJsonFormats.scala @@ -21,7 +21,7 @@ trait CustomJsonFormats { object CustomJsonFormats { - val formats: Formats = JsonSerializers.commonFormats + JsonAbleSerializer + val formats: Formats = JsonSerializers.commonFormats + PrimaryDataBodySerializer + ToStringDeSerializer + TupleSerializer val losslessFormats: Formats = net.liftweb.json.DefaultFormats.lossless ++ JsonSerializers.serializers @@ -37,10 +37,9 @@ object CustomJsonFormats { } -object OptionalFieldSerializer extends Serializer[AnyRef] { +object OptionalFieldSerializer extends ObpSerializer[AnyRef] { private val typedOptionalPathRegx = "(.+?):(.+)".r private val memo = new Memo[universe.Type, List[String]]() - override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, json.JValue), AnyRef] = Functions.doNothing private lazy val propsOutboundOptionalFields = APIUtil.getPropsValue("outbound.optional.fields", "") .split("""\s*,\s*""").filterNot(StringUtils.isBlank).toList @@ -125,11 +124,9 @@ object OptionalFieldSerializer extends Serializer[AnyRef] { } -object JsonAbleSerializer extends Serializer[PrimaryDataBody[_]] { +object PrimaryDataBodySerializer extends ObpDeSerializer[PrimaryDataBody[_]] { private val IntervalClass = classOf[Product] - override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = Functions.doNothing - override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, json.JValue), PrimaryDataBody[_]] = { case (TypeInfo(IntervalClass, _), json) if !json.isInstanceOf[JObject] => json match { case JNothing => EmptyBody @@ -141,4 +138,27 @@ object JsonAbleSerializer extends Serializer[PrimaryDataBody[_]] { case x => throw new MappingException("Can't convert " + x + " to PrimaryDataBody") } } +} + +/** + * deserialize jvalue to string, even if jvalue is not JString type, it can deserialize successfully. + */ +object ToStringDeSerializer extends ObpDeSerializer[String] { + private val IntervalClass = classOf[String] + + override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, json.JValue), String] = { + case (TypeInfo(IntervalClass, _), json) if !json.isInstanceOf[JString] && json != JNothing => + compactRender(json) + } +} + +/** + * deserialize jvalue to string, even if jvalue is not JString type, it can deserialize successfully. + */ +object TupleSerializer extends ObpSerializer[(String, Any)] { + import net.liftweb.json.JsonDSL._ + override def serialize(implicit format: Formats): PartialFunction[Any, json.JValue] = { + case (name: String, value: Any) => + name -> json.Extraction.decompose(value) + } } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/util/DynamicUtil.scala b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala new file mode 100644 index 000000000..a8888a2ad --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/DynamicUtil.scala @@ -0,0 +1,167 @@ +package code.api.util +import com.openbankproject.commons.util.JsonUtils +import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.json.{JObject, JValue, prettyRender} + +import java.util.concurrent.ConcurrentHashMap +import scala.reflect.runtime.universe +import scala.reflect.runtime.universe.runtimeMirror +import scala.tools.reflect.{ToolBox, ToolBoxError} + +object DynamicUtil { + + val toolBox: ToolBox[universe.type] = runtimeMirror(getClass.getClassLoader).mkToolBox() + + // code -> dynamic method function + // the same code should always be compiled once, so here cache them + private val dynamicCompileResult = new ConcurrentHashMap[String, Box[Any]]() + /** + * Compile scala code + * toolBox have bug that first compile fail, second or later compile success. + * @param code + * @return compiled Full[function|object|class] or Failure + */ + def compileScalaCode[T](code: String): Box[T] = { + val compiledResult: Box[Any] = dynamicCompileResult.computeIfAbsent(code, _ => { + val tree = try { + toolBox.parse(code) + } catch { + case e: ToolBoxError => + return Failure(e.message) + } + + try { + val func: () => Any = toolBox.compile(tree) + Box.tryo(func()) + } catch { + case _: ToolBoxError => + // try compile again + try { + val func: () => Any = toolBox.compile(tree) + Box.tryo(func()) + } catch { + case e: ToolBoxError => + Failure(e.message) + } + } + }) + + compiledResult.map(_.asInstanceOf[T]) + } + + /** + * + * @param methodName the method name + * @param function the method body, if it is empty, then throw exception. if it is existing, then call this function. + * @param args the method parameters + * @return the result of the execution of the function. + */ + def executeFunction(methodName: String, function: Box[Any], args: Array[AnyRef]) = { + val result = function.orNull match { + case func: Function0[AnyRef] => func() + case func: Function[AnyRef, AnyRef] => func(args.head) + case func: Function2[AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1)) + case func: Function3[AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2)) + case func: Function4[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3)) + case func: Function5[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4)) + case func: Function6[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5)) + case func: Function7[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6)) + case func: Function8[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7)) + case func: Function9[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8)) + case func: Function10[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9)) + case func: Function11[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10)) + case func: Function12[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11)) + case func: Function13[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12)) + case func: Function14[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13)) + case func: Function15[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14)) + case func: Function16[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15)) + case func: Function17[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16)) + case func: Function18[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17)) + case func: Function19[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17), args.apply(18)) + case func: Function20[AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17), args.apply(18), args.apply(19)) + case null => throw new IllegalStateException(s"There is no method $methodName, it should not be called here") + case _ => throw new IllegalStateException(s"$methodName can not be called here.") + } + result.asInstanceOf[AnyRef] + } + + /** + * this method will create a object from the JValue. + * from JValue --> Case Class String --> DynamicUtil.compileScalaCode(code) --> object + * @param jValue + * @return + */ + def toCaseObject(jValue: JValue): Product = { + val caseClasses = JsonUtils.toCaseClasses(jValue) + val code = + s""" + | $caseClasses + | + | // throws exception: net.liftweb.json.MappingException: + | //No usable value for name + | //Did not find value which can be converted into java.lang.String + | + |implicit val formats = code.api.util.CustomJsonFormats.formats + |(jValue: net.liftweb.json.JsonAST.JValue) => { + | jValue.extract[RootJsonClass] + |} + |""".stripMargin + val fun: Box[JValue => Product] = DynamicUtil.compileScalaCode(code) + fun match { + case Full(func) => func.apply(jValue) + case Failure(msg: String, exception: Box[Throwable], _) => + throw exception.getOrElse(new RuntimeException(msg)) + case _ => throw new RuntimeException(s"Json extract to case object fail, json: \n ${prettyRender(jValue)}") + } + } + + /** + * common import statements those are used by compiler + */ + val importStatements = + """ + |import java.net.{ConnectException, URLEncoder, UnknownHostException} + |import java.util.Date + |import java.util.UUID.randomUUID + | + |import _root_.akka.stream.StreamTcpException + |import akka.http.scaladsl.model.headers.RawHeader + |import akka.http.scaladsl.model.{HttpProtocol, _} + |import akka.util.ByteString + |import code.api.APIFailureNewStyle + |import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions + |import code.api.cache.Caching + |import code.api.util.APIUtil.{AdapterImplementation, MessageDoc, OBPReturnType, saveConnectorMetric, _} + |import code.api.util.ErrorMessages._ + |import code.api.util.ExampleValue._ + |import code.api.util.{APIUtil, CallContext, OBPQueryParam} + |import code.api.v4_0_0.dynamic.MockResponseHolder + |import code.bankconnectors._ + |import code.bankconnectors.vJune2017.AuthInfo + |import code.customer.internalMapping.MappedCustomerIdMappingProvider + |import code.kafka.KafkaHelper + |import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider + |import code.util.AkkaHttpClient._ + |import code.util.Helper.MdcLoggable + |import com.openbankproject.commons.dto.{InBoundTrait, _} + |import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA + |import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} + |import com.openbankproject.commons.model.{ErrorMessage, TopicTrait, _} + |import com.openbankproject.commons.util.{JsonUtils, ReflectUtils} + |// import com.tesobe.{CacheKeyFromArguments, CacheKeyOmit} + |import net.liftweb.common.{Box, Empty, _} + |import net.liftweb.json + |import net.liftweb.json.Extraction.decompose + |import net.liftweb.json.JsonDSL._ + |import net.liftweb.json.JsonParser.ParseException + |import net.liftweb.json.{JValue, _} + |import net.liftweb.util.Helpers.tryo + |import org.apache.commons.lang3.StringUtils + | + |import scala.collection.immutable.List + |import scala.collection.mutable.ArrayBuffer + |import scala.concurrent.duration._ + |import scala.concurrent.{Await, Future} + |import com.openbankproject.commons.dto._ + |""".stripMargin +} diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index bdd33317e..c4f1fd3f7 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -470,9 +470,15 @@ object ErrorMessages { val ConnectorMethodNotFound = "OBP-40036: ConnectorMethod not found, please specify valid CONNECTOR_METHOD_ID. " val ConnectorMethodAlreadyExists = "OBP-40037: ConnectorMethod already exists. " - val ConnectorMethodBodyCompileFail = "OBP-40038: ConnectorMethod methodBody is illegal scala code, compile fail. " - + val ConnectorMethodBodyCompileFail = "OBP-40038: ConnectorMethod methodBody is illegal scala code, compilation failed. " + val DynamicResourceDocAlreadyExists = "OBP-40039: DynamicResourceDoc already exists." + val DynamicResourceDocNotFound = "OBP-40040: DynamicResourceDoc not found, please specify valid DYNAMIC_RESOURCE_DOC_ID. " + val DynamicResourceDocDeleteError = "OBP-40041: DynamicResourceDoc can not be deleted. " + val DynamicMessageDocAlreadyExists = "OBP-40042: DynamicMessageDoc already exists." + val DynamicMessageDocNotFound = "OBP-40043: DynamicMessageDoc not found, please specify valid DYNAMIC_MESSAGE_DOC_ID. " + val DynamicMessageDocDeleteError = "OBP-40044: DynamicMessageDoc can not be deleted. " + val DynamicCodeCompileFail = "OBP-40045: The code to do compile is illegal scala code, compilation failed. " // Exceptions (OBP-50XXX) val UnknownError = "OBP-50000: Unknown Error." val FutureTimeoutException = "OBP-50001: Future Timeout Exception." diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 98ad20a95..a28c0e761 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -2,6 +2,7 @@ package code.api.util import code.api.util.APIUtil.parseDate +import code.api.util.ErrorMessages.{InvalidJsonFormat, UserHasMissingRoles, UserNotLoggedIn, UnknownError} import net.liftweb.json.JsonDSL._ import code.api.util.Glossary.{glossaryItems, makeGlossaryItem} import code.dynamicEntity.{DynamicEntityDefinition, DynamicEntityFooBar, DynamicEntityFullBarFields, DynamicEntityIntTypeExample, DynamicEntityStringTypeExample} @@ -370,6 +371,85 @@ object ExampleValue { lazy val htmlExample = ConnectorField("html format content","the content is displayed in HTML format") glossaryItems += makeGlossaryItem("html", htmlExample) + + lazy val connectorMethodIdExample = ConnectorField("ace0352a-9a0f-4bfa-b30b-9003aa467f51", "A string that MUST uniquely identify the connector method on this OBP instance, can be used in all cache. ") + glossaryItems += makeGlossaryItem("ConnectorMethod.connectorMethodId", connectorMethodIdExample) + + lazy val dynamicResourceDocMethodBodyExample = ConnectorField("%20%20%20%20val%20Some(resourceDoc)%20%3D%20callContext.resourceDocument%0A%20%20%20%20" + + "val%20hasRequestBody%20%3D%20request.body.isDefined%0A%0A%20%20%20%20%2F%2F%20get%20Path%20Parameters%2C%20example%3A%0A%20%20%20" + + "%20%2F%2F%20if%20the%20requestUrl%20of%20resourceDoc%20is%20%2Fhello%2Fbanks%2FBANK_ID%2Fworld%0A%20%20%20%20%2F%2F%20the%20reque" + + "st%20path%20is%20%2Fhello%2Fbanks%2Fbank_x%2Fworld%0A%20%20%20%20%2F%2FpathParams.get(%22BANK_ID%22)%20will%20get%20Option(%22bank" + + "_x%22)%20value%0A%20%20%20%20val%20pathParams%20%3D%20getPathParams(callContext%2C%20request)%0A%20%20%20%20val%20myUserId%20%3D%2" + + "0pathParams(%22MY_USER_ID%22)%0A%0A%0A%20%20%20%20val%20requestEntity%20%3D%20request.json%20match%20%7B%0A%20%20%20%20%20%20case%" + + "20Full(zson)%20%3D%3E%0A%20%20%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20%20%20zson.extract%5BRequestRootJsonClass%5D%0" + + "A%20%20%20%20%20%20%20%20%7D%20catch%20%7B%0A%20%20%20%20%20%20%20%20%20%20case%20e%3A%20MappingException%20%3D%3E%0A%20%20%20%20%" + + "20%20%20%20%20%20%20%20return%20Full(errorJsonResponse(s%22%24InvalidJsonFormat%20%24%7Be.msg%7D%22))%0A%20%20%20%20%20%20%20%20%7" + + "D%0A%20%20%20%20%20%20case%20_%3A%20EmptyBox%20%3D%3E%0A%20%20%20%20%20%20%20%20return%20Full(errorJsonResponse(s%22%24InvalidRequ" + + "estPayload%20Current%20request%20has%20no%20payload%22))%0A%20%20%20%20%7D%0A%0A%0A%20%20%20%20%2F%2F%20please%20add%20business%20" + + "logic%20here%0A%20%20%20%20val%20responseBody%3AResponseRootJsonClass%20%3D%20ResponseRootJsonClass(s%22%24%7BmyUserId%7D_from_pat" + + "h%22%2C%20requestEntity.name%2C%20requestEntity.age%2C%20requestEntity.hobby)%0A%20%20%20%20Future.successful%20%7B%0A%20%20%20%20" + + "%20%20(responseBody%2C%20HttpCode.%60200%60(callContext.callContext))%0A%20%20%20%20%7D", + "the URL-encoded format String, the original code is the OBP connector method body.") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.methodBody", dynamicResourceDocMethodBodyExample) + + lazy val connectorMethodBodyExample = ConnectorField("%20%20%20%20%20%20Future.successful%28%0A%20%20%20%20%20%20%20%20Full%28%" + + "28BankCommons%28%0A%20%20%20%20%20%20%20%20%20%20BankId%28%22Hello%20bank%20id%22%29%2C%0A%20%20%20%20%20%20%20%20%20" + + "%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%2" + + "0%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%221%22%2C%0A%20%20%2" + + "0%20%20%20%20%20%20%20%221%22%2C%0A%20%20%20%20%20%20%20%20%20%20%228%22%0A%20%20%20%20%20%20%20%20%29%2C%20None%29%29%0A%" + + "20%20%20%20%20%20%29", + "the URL-encoded format String, the original code is the OBP connector method body.") + glossaryItems += makeGlossaryItem("DynamicConnectorMethod.methodBody", connectorMethodBodyExample) + + lazy val dynamicResourceDocIdExample = ConnectorField("vce035ca-9a0f-4bfa-b30b-9003aa467f51", "A string that MUST uniquely identify the dynamic Resource Doc on this OBP instance, can be used in all cache. ") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.dynamicResourceDocId", dynamicResourceDocIdExample) + + lazy val partialFunctionExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicResourceDoc.partialFunction", partialFunctionExample) + + lazy val implementedInApiVersionExample = ConnectorField(NoExampleProvided, NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicResourceDoc.implementedInApiVersion", implementedInApiVersionExample) + + lazy val partialFunctionNameExample = ConnectorField("getBanks", "partial function name") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.partialFunctionName", partialFunctionNameExample) + + lazy val dynamicResourceDocPartialFunctionNameExample = ConnectorField("createUser", "partial function name") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.partialFunctionName", dynamicResourceDocPartialFunctionNameExample) + + lazy val requestVerbExample = ConnectorField("POST", "This is the HTTP methods, eg: GET, POST, PUT, DELETE ") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.requestVerb", requestVerbExample) + + lazy val requestUrlExample = ConnectorField("/my_user/MY_USER_ID", "The URL of the endpoint.") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.requestUrl", requestUrlExample) + + lazy val exampleRequestBodyExample = ConnectorField("""{"name": "Jhon", "age": 12, "hobby": ["coding"],"_optional_fields_": ["hobby"]}""", "the json string of the request body.") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.exampleRequestBody", exampleRequestBodyExample) + + lazy val successResponseBodyExample = ConnectorField("""{"my_user_id": "some_id_value", "name": "Jhon", "age": 12, "hobby": ["coding"],"_optional_fields_": ["hobby"]}""".stripMargin, "the json string of the success response body.") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.successResponseBody", successResponseBodyExample) + + lazy val errorResponseBodiesExample = ConnectorField(s"$UnknownError,$UserNotLoggedIn,$UserHasMissingRoles,$InvalidJsonFormat", "The possible error messages of the endpoint. ") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.errorResponseBodies", errorResponseBodiesExample) + + + lazy val isFeaturedExample = ConnectorField("false", "if this is featured or not ") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.isFeatured", isFeaturedExample) + + lazy val specialInstructionsExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicResourceDoc.specialInstructions", specialInstructionsExample) + + lazy val specifiedUrlExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicResourceDoc.specifiedUrl", specifiedUrlExample) + + lazy val dynamicMessageDocIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicMessageDoc.dynamicMessageDocId", dynamicMessageDocIdExample) + + lazy val outboundAvroSchemaExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicMessageDoc.outboundAvroSchema", outboundAvroSchemaExample) + + lazy val inboundAvroSchemaExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + glossaryItems += makeGlossaryItem("DynamicMessageDoc.inboundAvroSchema", inboundAvroSchemaExample) + lazy val canSeeImagesExample = ConnectorField("true",NoDescriptionProvided) glossaryItems += makeGlossaryItem("can_see_images", canSeeImagesExample) @@ -475,7 +555,7 @@ object ExampleValue { lazy val ownersExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("owners", ownersExample) - lazy val exampleInboundMessageExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val exampleInboundMessageExample = ConnectorField("{}", "This is the json object.") glossaryItems += makeGlossaryItem("example_inbound_message", exampleInboundMessageExample) lazy val nationalIdentifierExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -844,7 +924,7 @@ object ExampleValue { lazy val perMonthCallLimitExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("per_month_call_limit", perMonthCallLimitExample) - lazy val rolesExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val rolesExample = ConnectorField("CanCreateMyUser","Entitlements are used to grant System or Bank level roles to Users ") glossaryItems += makeGlossaryItem("roles", rolesExample) lazy val categoryExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1120,7 +1200,7 @@ object ExampleValue { lazy val counterpartyExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("counterparty", counterpartyExample) - lazy val tagsExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val tagsExample = ConnectorField("Create-My-User","OBP uses the tags to group the endpoints, the relevant endpoints can share the same tag. ") glossaryItems += makeGlossaryItem("tags", tagsExample) lazy val perHourExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1417,7 +1497,7 @@ object ExampleValue { lazy val otherAccountSecondaryRoutingSchemeExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("other_account_secondary_routing_scheme", otherAccountSecondaryRoutingSchemeExample) - lazy val processExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val processExample = ConnectorField("obp.getBank","The format must be obp.xxxx, 'obp.' is the prefix, xxx will be the connector method name") glossaryItems += makeGlossaryItem("process", processExample) lazy val otherBranchRoutingSchemeExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1624,7 +1704,7 @@ object ExampleValue { lazy val actualDateExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("actual_date", actualDateExample) - lazy val exampleOutboundMessageExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) + lazy val exampleOutboundMessageExample = ConnectorField("{}","this will the json object") glossaryItems += makeGlossaryItem("example_outbound_message", exampleOutboundMessageExample) lazy val canDeleteWhereTagExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) @@ -1918,6 +1998,9 @@ object ExampleValue { lazy val descriptionExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("description", descriptionExample) + lazy val dynamicResourceDocdescriptionExample = ConnectorField("Create one User", "the description for this endpoint") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.description", dynamicResourceDocdescriptionExample) + lazy val canDeleteCommentExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("can_delete_comment", canDeleteCommentExample) @@ -1948,6 +2031,8 @@ object ExampleValue { lazy val summaryExample = ConnectorField(NoExampleProvided,NoDescriptionProvided) glossaryItems += makeGlossaryItem("summary", summaryExample) + lazy val dynamicResourceDocSummaryExample = ConnectorField("Create My User","The summary of this endpoint") + glossaryItems += makeGlossaryItem("DynamicResourceDoc.summary", dynamicResourceDocSummaryExample) //------------------------------------------------------------ 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 d71b426c9..9c546927d 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -14,7 +14,6 @@ import code.api.v1_4_0.OBPAPI1_4_0.Implementations1_4_0 import code.api.v2_0_0.OBPAPI2_0_0.Implementations2_0_0 import code.api.v2_1_0.OBPAPI2_1_0.Implementations2_1_0 import code.api.v2_2_0.OBPAPI2_2_0.Implementations2_2_0 -import code.api.v4_0_0.{DynamicEndpointHelper, DynamicEntityInfo} import code.authtypevalidation.{AuthenticationTypeValidationProvider, JsonAuthTypeValidation} import code.bankconnectors.Connector import code.branches.Branches.{Branch, DriveUpString, LobbyString} @@ -60,7 +59,10 @@ import code.validation.{JsonSchemaValidationProvider, JsonValidation} import net.liftweb.http.JsonResponse import net.liftweb.util.Props import code.api.JsonResponseException +import code.api.v4_0_0.dynamic.{DynamicEndpointHelper, DynamicEntityInfo} import code.connectormethod.{ConnectorMethodProvider, JsonConnectorMethod} +import code.dynamicMessageDoc.{DynamicMessageDocProvider, JsonDynamicMessageDoc} +import code.dynamicResourceDoc.{DynamicResourceDocProvider, JsonDynamicResourceDoc} object NewStyle { lazy val endpoints: List[(String, String)] = List( @@ -225,7 +227,7 @@ object NewStyle { unboxFullOrFail(_, callContext, s"$BankNotFound Current BankId is $bankId", 404) } } - def getBanks(callContext: Option[CallContext]) : OBPReturnType[List[Bank]] = { + def getBanks(callContext: Option[CallContext]) : Future[(List[Bank], Option[CallContext])] = { Connector.connector.vend.getBanks(callContext: Option[CallContext]) map { connectorEmptyResponse(_, callContext) } @@ -2351,7 +2353,7 @@ object NewStyle { } private[this] val dynamicEntityTTL = { - if(Props.testMode) 0 + if(Props.testMode || Props.devMode) 0 else APIUtil.getPropsValue(s"dynamicEntity.cache.ttl.seconds", "30").toInt } @@ -2846,13 +2848,13 @@ object NewStyle { (unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext) } - def isJsonConnectorMethodExists(connectorMethodId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + def connectorMethodExists(connectorMethodId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = Future { val result = ConnectorMethodProvider.provider.vend.getById(connectorMethodId) (result.isDefined, callContext) } - def isJsonConnectorMethodNameExists(connectorMethodName: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + def connectorMethodNameExists(connectorMethodName: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = Future { val result = ConnectorMethodProvider.provider.vend.getByMethodNameWithoutCache(connectorMethodName) (result.isDefined, callContext) @@ -2870,5 +2872,87 @@ object NewStyle { (unboxFullOrFail(connectorMethod, callContext, s"$ConnectorMethodNotFound Current CONNECTOR_METHOD_ID(${connectorMethodId})", 400), callContext) } + def isJsonDynamicResourceDocExists(requestVerb : String, requestUrl : String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + Future { + val result = DynamicResourceDocProvider.provider.vend.getByVerbAndUrl(requestVerb, requestUrl) + (result.isDefined, callContext) + } + + def createJsonDynamicResourceDoc(dynamicResourceDoc: JsonDynamicResourceDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicResourceDoc] = + Future { + val newInternalConnector = DynamicResourceDocProvider.provider.vend.create(dynamicResourceDoc) + val errorMsg = s"$UnknownError Can not create Dynamic Resource Doc in the backend. " + (unboxFullOrFail(newInternalConnector, callContext, errorMsg, 400), callContext) + } + + def updateJsonDynamicResourceDoc(entity: JsonDynamicResourceDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicResourceDoc] = + Future { + val updatedConnectorMethod = DynamicResourceDocProvider.provider.vend.update(entity: JsonDynamicResourceDoc) + val errorMsg = s"$UnknownError Can not update Dynamic Resource Doc in the backend. " + (unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext) + } + + def isJsonDynamicResourceDocExists(dynamicResourceDocId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + Future { + val result = DynamicResourceDocProvider.provider.vend.getById(dynamicResourceDocId) + (result.isDefined, callContext) + } + + def getJsonDynamicResourceDocs(callContext: Option[CallContext]): OBPReturnType[List[JsonDynamicResourceDoc]] = + Future { + val dynamicResourceDocs: List[JsonDynamicResourceDoc] = DynamicResourceDocProvider.provider.vend.getAll() + dynamicResourceDocs -> callContext + } + + def getJsonDynamicResourceDocById(dynamicResourceDocId: String, callContext: Option[CallContext]): OBPReturnType[JsonDynamicResourceDoc] = + Future { + val dynamicResourceDoc = DynamicResourceDocProvider.provider.vend.getById(dynamicResourceDocId) + (unboxFullOrFail(dynamicResourceDoc, callContext, s"$DynamicResourceDocNotFound Current DYNAMIC_RESOURCE_DOC_ID(${dynamicResourceDocId})", 400), callContext) + } + + def deleteJsonDynamicResourceDocById(dynamicResourceDocId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + Future { + val dynamicResourceDoc = DynamicResourceDocProvider.provider.vend.deleteById(dynamicResourceDocId) + (unboxFullOrFail(dynamicResourceDoc, callContext, s"$DynamicResourceDocDeleteError Current DYNAMIC_RESOURCE_DOC_ID(${dynamicResourceDocId})", 400), callContext) + } + + def createJsonDynamicMessageDoc(dynamicMessageDoc: JsonDynamicMessageDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicMessageDoc] = + Future { + val newInternalConnector = DynamicMessageDocProvider.provider.vend.create(dynamicMessageDoc) + val errorMsg = s"$UnknownError Can not create Dynamic Message Doc in the backend. " + (unboxFullOrFail(newInternalConnector, callContext, errorMsg, 400), callContext) + } + + def updateJsonDynamicMessageDoc(entity: JsonDynamicMessageDoc, callContext: Option[CallContext]): OBPReturnType[JsonDynamicMessageDoc] = + Future { + val updatedConnectorMethod = DynamicMessageDocProvider.provider.vend.update(entity: JsonDynamicMessageDoc) + val errorMsg = s"$UnknownError Can not update Dynamic Message Doc in the backend. " + (unboxFullOrFail(updatedConnectorMethod, callContext, errorMsg, 400), callContext) + } + + def isJsonDynamicMessageDocExists(process: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + Future { + val result = DynamicMessageDocProvider.provider.vend.getByProcess(process) + (result.isDefined, callContext) + } + + def getJsonDynamicMessageDocs(callContext: Option[CallContext]): OBPReturnType[List[JsonDynamicMessageDoc]] = + Future { + val dynamicMessageDocs: List[JsonDynamicMessageDoc] = DynamicMessageDocProvider.provider.vend.getAll() + dynamicMessageDocs -> callContext + } + + def getJsonDynamicMessageDocById(dynamicMessageDocId: String, callContext: Option[CallContext]): OBPReturnType[JsonDynamicMessageDoc] = + Future { + val dynamicMessageDoc = DynamicMessageDocProvider.provider.vend.getById(dynamicMessageDocId) + (unboxFullOrFail(dynamicMessageDoc, callContext, s"$DynamicMessageDocNotFound Current DYNAMIC_RESOURCE_DOC_ID(${dynamicMessageDocId})", 400), callContext) + } + + def deleteJsonDynamicMessageDocById(dynamicMessageDocId: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = + Future { + val dynamicMessageDoc = DynamicMessageDocProvider.provider.vend.deleteById(dynamicMessageDocId) + (unboxFullOrFail(dynamicMessageDoc, callContext, s"$DynamicMessageDocDeleteError", 400), 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 5b28726bd..a28edad56 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 @@ -3,8 +3,8 @@ package code.api.v4_0_0 import code.DynamicData.DynamicData import code.DynamicEndpoint.DynamicEndpointSwagger import code.accountattribute.AccountAttributeX -import code.api.ChargePolicy -import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{logoutLinkV400, _} +import code.api.{ChargePolicy, JsonResponseException} +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{jsonDynamicResourceDoc, logoutLinkV400, _} import code.api.util.APIUtil.{fullBoxOrException, _} import code.api.util.ApiRole._ import code.api.util.ApiTag._ @@ -23,12 +23,14 @@ import code.api.v2_0_0.{EntitlementJSONs, JSONFactory200} import code.api.v2_1_0._ import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0._ -import code.api.v4_0_0.DynamicEndpointHelper.DynamicReq +import code.api.v4_0_0.dynamic.DynamicEndpointHelper.DynamicReq import code.api.v4_0_0.JSONFactory400.{createBalancesJson, createBankAccountJSON, createCallsLimitJson, createNewCoreBankAccountJson} +import code.api.v4_0_0.dynamic.practise.PractiseEndpoint +import code.api.v4_0_0.dynamic.{CompiledObjects, DynamicEndpointHelper, DynamicEntityHelper, DynamicEntityInfo, EntityName, MockResponseHolder} import code.apicollection.MappedApiCollectionsProvider import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider import code.authtypevalidation.JsonAuthTypeValidation -import code.bankconnectors.{Connector, InternalConnector} +import code.bankconnectors.{Connector, DynamicConnector, InternalConnector} import code.connectormethod.{JsonConnectorMethod, JsonConnectorMethodMethodBody} import code.consent.{ConsentStatus, Consents} import code.dynamicEntity.{DynamicEntityCommons, ReferenceType} @@ -70,6 +72,12 @@ import org.apache.commons.collections4.CollectionUtils import org.apache.commons.lang3.StringUtils import java.util.Date +import code.dynamicMessageDoc.JsonDynamicMessageDoc +import code.dynamicResourceDoc.JsonDynamicResourceDoc + +import java.net.URLEncoder +import code.api.v4_0_0.dynamic.practise.DynamicEndpointCodeGenerator + import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer import scala.concurrent.Future @@ -4303,7 +4311,7 @@ trait APIMethods400 { InvalidJsonFormat, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle), + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle), Some(List(canCreateDynamicEndpoint))) lazy val createDynamicEndpoint: OBPEndpoint = { @@ -4354,7 +4362,7 @@ trait APIMethods400 { InvalidJsonFormat, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle), + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle), Some(List(canGetDynamicEndpoint))) lazy val getDynamicEndpoint: OBPEndpoint = { @@ -4393,7 +4401,7 @@ trait APIMethods400 { InvalidJsonFormat, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle), + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle), Some(List(canGetDynamicEndpoints))) lazy val getDynamicEndpoints: OBPEndpoint = { @@ -4426,7 +4434,7 @@ trait APIMethods400 { DynamicEndpointNotFoundByDynamicEndpointId, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle), + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle), Some(List(canDeleteDynamicEndpoint))) lazy val deleteDynamicEndpoint : OBPEndpoint = { @@ -4458,7 +4466,7 @@ trait APIMethods400 { InvalidJsonFormat, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle) + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle) ) lazy val getMyDynamicEndpoints: OBPEndpoint = { @@ -4491,7 +4499,7 @@ trait APIMethods400 { DynamicEndpointNotFoundByDynamicEndpointId, UnknownError ), - List(apiTagManageDynamicEndpoint, apiTagApi, apiTagNewStyle), + List(apiTagDynamicSwaggerDoc, apiTagApi, apiTagNewStyle), ) lazy val deleteMyDynamicEndpoint : OBPEndpoint = { @@ -6944,7 +6952,7 @@ trait APIMethods400 { | |The method_body is URL-encoded format String |""", - jsonConnectorMethod.copy(internalConnectorId=None), + jsonConnectorMethod.copy(connectorMethodId=None), jsonConnectorMethod, List( $UserNotLoggedIn, @@ -6963,7 +6971,7 @@ trait APIMethods400 { json.extract[JsonConnectorMethod] } - (isExists, callContext) <- NewStyle.function.isJsonConnectorMethodNameExists(jsonConnectorMethod.methodName, Some(cc)) + (isExists, callContext) <- NewStyle.function.connectorMethodNameExists(jsonConnectorMethod.methodName, Some(cc)) _ <- Helper.booleanToFuture(failMsg = s"$ConnectorMethodAlreadyExists Please use a different method_name(${jsonConnectorMethod.methodName})") { (!isExists) } @@ -7086,6 +7094,451 @@ trait APIMethods400 { } } + staticResourceDocs += ResourceDoc( + createDynamicResourceDoc, + implementedInApiVersion, + nameOf(createDynamicResourceDoc), + "POST", + "/management/dynamic-resource-docs", + "Create Dynamic Resource Doc", + s"""Create a Dynamic Resource Doc. + | + |The connector_method_body is URL-encoded format String + |""", + jsonDynamicResourceDoc.copy(dynamicResourceDocId=None), + jsonDynamicResourceDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + Some(List(canCreateDynamicResourceDoc))) + + lazy val createDynamicResourceDoc: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: Nil JsonPost json -> _ => { + cc => + for { + jsonDynamicResourceDoc <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $JsonDynamicResourceDoc", 400, cc.callContext) { + json.extract[JsonDynamicResourceDoc] + } + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat The request_verb must be one of ["POST", "PUT", "GET", "DELETE"]""") { + Set("POST", "PUT", "GET", "DELETE").contains(jsonDynamicResourceDoc.requestVerb) + } + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat When request_verb is "GET" or "DELETE", the example_request_body must be a blank String "" or just totally omit the field""") { + (jsonDynamicResourceDoc.requestVerb, jsonDynamicResourceDoc.exampleRequestBody) match { + case ("GET" | "DELETE", Some(JString(s))) => //we support the empty string "" here + StringUtils.isBlank(s) + case ("GET" | "DELETE", Some(requestBody)) => //we add the guard, we forbid any json objects in GET/DELETE request body. + requestBody == JNothing + case _ => true + } + } + _ = try { + CompiledObjects(jsonDynamicResourceDoc.exampleRequestBody, jsonDynamicResourceDoc.successResponseBody, jsonDynamicResourceDoc.methodBody) + } catch { + case e: Exception => + val jsonResponse = createErrorJsonResponse(s"$DynamicCodeCompileFail ${e.getMessage}", 400, cc.correlationId) + throw JsonResponseException(jsonResponse) + } + + (isExists, callContext) <- NewStyle.function.isJsonDynamicResourceDocExists(jsonDynamicResourceDoc.requestVerb, jsonDynamicResourceDoc.requestUrl, Some(cc)) + _ <- Helper.booleanToFuture(failMsg = s"$DynamicResourceDocAlreadyExists The combination of request_url(${jsonDynamicResourceDoc.requestUrl}) and request_verb(${jsonDynamicResourceDoc.requestVerb}) must be unique") { + (!isExists) + } + + (dynamicResourceDoc, callContext) <- NewStyle.function.createJsonDynamicResourceDoc(jsonDynamicResourceDoc, callContext) + } yield { + (dynamicResourceDoc, HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + updateDynamicResourceDoc, + implementedInApiVersion, + nameOf(updateDynamicResourceDoc), + "PUT", + "/management/dynamic-resource-docs/DYNAMIC-RESOURCE-DOC-ID", + "Update Dynamic Resource Doc", + s"""Update a Dynamic Resource Doc. + | + |The connector_method_body is URL-encoded format String + |""", + jsonDynamicResourceDoc.copy(dynamicResourceDocId = None), + jsonDynamicResourceDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + Some(List(canUpdateDynamicResourceDoc))) + + lazy val updateDynamicResourceDoc: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: dynamicResourceDocId :: Nil JsonPut json -> _ => { + cc => + for { + dynamicResourceDocBody <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $JsonDynamicResourceDoc", 400, cc.callContext) { + json.extract[JsonDynamicResourceDoc] + } + + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat The request_verb must be one of ["POST", "PUT", "GET", "DELETE"]""") { + Set("POST", "PUT", "GET", "DELETE").contains(dynamicResourceDocBody.requestVerb) + } + + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat When request_verb is "GET" or "DELETE", the example_request_body must be a blank String""") { + (dynamicResourceDocBody.requestVerb, dynamicResourceDocBody.exampleRequestBody) match { + case ("GET" | "DELETE", Some(JString(s))) => + StringUtils.isBlank(s) + case ("GET" | "DELETE", Some(requestBody)) => + requestBody == JNothing + case _ => true + } + } + + _ = try { + CompiledObjects(jsonDynamicResourceDoc.exampleRequestBody, jsonDynamicResourceDoc.successResponseBody, jsonDynamicResourceDoc.methodBody) + } catch { + case e: Exception => + val jsonResponse = createErrorJsonResponse(s"$DynamicCodeCompileFail ${e.getMessage}", 400, cc.correlationId) + throw JsonResponseException(jsonResponse) + } + + (_, callContext) <- NewStyle.function.getJsonDynamicResourceDocById(dynamicResourceDocId, cc.callContext) + + (dynamicResourceDoc, callContext) <- NewStyle.function.updateJsonDynamicResourceDoc(dynamicResourceDocBody.copy(dynamicResourceDocId = Some(dynamicResourceDocId)), callContext) + } yield { + (dynamicResourceDoc, HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteDynamicResourceDoc, + implementedInApiVersion, + nameOf(deleteDynamicResourceDoc), + "DELETE", + "/management/dynamic-resource-docs/DYNAMIC-RESOURCE-DOC-ID", + "Delete Dynamic Resource Doc", + s"""Delete a Dynamic Resource Doc. + |""", + EmptyBody, + BooleanBody(true), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + Some(List(canDeleteDynamicResourceDoc))) + + lazy val deleteDynamicResourceDoc: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: dynamicResourceDocId :: Nil JsonDelete _ => { + cc => + for { + (_, callContext) <- NewStyle.function.getJsonDynamicResourceDocById(dynamicResourceDocId, cc.callContext) + (dynamicResourceDoc, callContext) <- NewStyle.function.deleteJsonDynamicResourceDocById(dynamicResourceDocId, callContext) + } yield { + (dynamicResourceDoc, HttpCode.`204`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getDynamicResourceDoc, + implementedInApiVersion, + nameOf(getDynamicResourceDoc), + "GET", + "/management/dynamic-resource-docs/DYNAMIC-RESOURCE-DOC-ID", + "Get Dynamic Resource Doc by Id", + s"""Get a Dynamic Resource Doc by DYNAMIC-RESOURCE-DOC-ID. + | + |""", + EmptyBody, + jsonDynamicResourceDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + Some(List(canGetDynamicResourceDoc))) + + lazy val getDynamicResourceDoc: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: dynamicResourceDocId :: Nil JsonGet _ => { + cc => + for { + (dynamicResourceDoc, callContext) <- NewStyle.function.getJsonDynamicResourceDocById(dynamicResourceDocId, cc.callContext) + } yield { + (dynamicResourceDoc, HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getAllDynamicResourceDocs, + implementedInApiVersion, + nameOf(getAllDynamicResourceDocs), + "GET", + "/management/dynamic-resource-docs", + "Get all Dynamic Resource Docs", + s"""Get all Dynamic Resource Docs. + | + |""", + EmptyBody, + ListResult("dynamic-resource-docs", jsonDynamicResourceDoc::Nil), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + Some(List(canGetAllDynamicResourceDocs))) + + lazy val getAllDynamicResourceDocs: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: Nil JsonGet _ => { + cc => + for { + (dynamicResourceDocs, callContext) <- NewStyle.function.getJsonDynamicResourceDocs(cc.callContext) + } yield { + (ListResult("dynamic-resource-docs", dynamicResourceDocs), HttpCode.`200`(callContext)) + } + } + } + + + staticResourceDocs += ResourceDoc( + buildDynamicEndpointTemplate, + implementedInApiVersion, + nameOf(buildDynamicEndpointTemplate), + "POST", + "/management/dynamic-resource-docs/endpoint-code", + "Create Dynamic Resource Doc endpoint code", + s"""Create a Dynamic Resource Doc endpoint code. + | + |copy the response and past to ${nameOf(PractiseEndpoint)}, So you can have the benefits of + |auto compilation and debug + |""", + jsonResourceDocFragment, + jsonCodeTemplate, + List( + $UserNotLoggedIn, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle), + None) + + lazy val buildDynamicEndpointTemplate: OBPEndpoint = { + case "management" :: "dynamic-resource-docs" :: "endpoint-code" :: Nil JsonPost json -> _ => { + cc => + for { + resourceDocFragment <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $ResourceDocFragment", 400, cc.callContext) { + json.extract[ResourceDocFragment] + } + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat The request_verb must be one of ["POST", "PUT", "GET", "DELETE"]""") { + Set("POST", "PUT", "GET", "DELETE").contains(resourceDocFragment.requestVerb) + } + + _ <- Helper.booleanToFuture(failMsg = s"""$InvalidJsonFormat When request_verb is "GET" or "DELETE", the example_request_body must be a blank String""") { + (resourceDocFragment.requestVerb, resourceDocFragment.exampleRequestBody) match { + case ("GET" | "DELETE", Some(JString(s))) => + StringUtils.isBlank(s) + case ("GET" | "DELETE", Some(requestBody)) => + requestBody == JNothing + case _ => true + } + } + + code = DynamicEndpointCodeGenerator.buildTemplate(resourceDocFragment) + + } yield { + ("code" -> URLEncoder.encode(code, "UTF-8"), HttpCode.`201`(cc.callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + createDynamicMessageDoc, + implementedInApiVersion, + nameOf(createDynamicMessageDoc), + "POST", + "/management/dynamic-message-docs", + "Create Dynamic Message Doc", + s"""Create a Dynamic Message Doc. + |""", + jsonDynamicMessageDoc.copy(dynamicMessageDocId=None), + jsonDynamicMessageDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicMessageDoc, apiTagNewStyle), + Some(List(canCreateDynamicMessageDoc))) + + lazy val createDynamicMessageDoc: OBPEndpoint = { + case "management" :: "dynamic-message-docs" :: Nil JsonPost json -> _ => { + cc => + for { + dynamicMessageDoc <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $JsonDynamicMessageDoc", 400, cc.callContext) { + json.extract[JsonDynamicMessageDoc] + } + (dynamicMessageDocExisted, callContext) <- NewStyle.function.isJsonDynamicMessageDocExists(dynamicMessageDoc.process, cc.callContext) + _ <- Helper.booleanToFuture(failMsg = s"$DynamicMessageDocAlreadyExists The json body process(${dynamicMessageDoc.process}) already exists") { + (!dynamicMessageDocExisted) + } + connectorMethod = DynamicConnector.createFunction(dynamicMessageDoc.process, dynamicMessageDoc.decodedMethodBody) + errorMsg = if(connectorMethod.isEmpty) s"$ConnectorMethodBodyCompileFail ${connectorMethod.asInstanceOf[Failure].msg}" else "" + _ <- Helper.booleanToFuture(failMsg = errorMsg) { + connectorMethod.isDefined + } + (dynamicMessageDoc, callContext) <- NewStyle.function.createJsonDynamicMessageDoc(dynamicMessageDoc, callContext) + } yield { + (dynamicMessageDoc, HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + updateDynamicMessageDoc, + implementedInApiVersion, + nameOf(updateDynamicMessageDoc), + "PUT", + "/management/dynamic-message-docs/DYNAMIC-MESSAGE-DOC-ID", + "Update Dynamic Message Doc", + s"""Update a Dynamic Message Doc. + |""", + jsonDynamicMessageDoc.copy(dynamicMessageDocId=None), + jsonDynamicMessageDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicMessageDoc, apiTagNewStyle), + Some(List(canUpdateDynamicMessageDoc))) + + lazy val updateDynamicMessageDoc: OBPEndpoint = { + case "management" :: "dynamic-message-docs" :: dynamicMessageDocId :: Nil JsonPut json -> _ => { + cc => + for { + dynamicMessageDocBody <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $JsonDynamicMessageDoc", 400, cc.callContext) { + json.extract[JsonDynamicMessageDoc] + } + connectorMethod = DynamicConnector.createFunction(dynamicMessageDocBody.process, dynamicMessageDocBody.decodedMethodBody) + errorMsg = if(connectorMethod.isEmpty) s"$ConnectorMethodBodyCompileFail ${connectorMethod.asInstanceOf[Failure].msg}" else "" + _ <- Helper.booleanToFuture(failMsg = errorMsg) { + connectorMethod.isDefined + } + (_, callContext) <- NewStyle.function.getJsonDynamicMessageDocById(dynamicMessageDocId, cc.callContext) + (dynamicMessageDoc, callContext) <- NewStyle.function.updateJsonDynamicMessageDoc(dynamicMessageDocBody.copy(dynamicMessageDocId=Some(dynamicMessageDocId)), callContext) + } yield { + (dynamicMessageDoc, HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getDynamicMessageDoc, + implementedInApiVersion, + nameOf(getDynamicMessageDoc), + "GET", + "/management/dynamic-message-docs/DYNAMIC-MESSAGE-DOC-ID", + "Get Dynamic Message Doc by Id", + s"""Get a Dynamic Message Doc by DYNAMIC-MESSAGE-DOC-ID. + | + |""", + EmptyBody, + jsonDynamicMessageDoc, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagDynamicMessageDoc, apiTagNewStyle), + Some(List(canGetDynamicMessageDoc))) + + lazy val getDynamicMessageDoc: OBPEndpoint = { + case "management" :: "dynamic-message-docs" :: dynamicMessageDocId :: Nil JsonGet _ => { + cc => + for { + (dynamicMessageDoc, callContext) <- NewStyle.function.getJsonDynamicMessageDocById(dynamicMessageDocId, cc.callContext) + } yield { + (dynamicMessageDoc, HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getAllDynamicMessageDocs, + implementedInApiVersion, + nameOf(getAllDynamicMessageDocs), + "GET", + "/management/dynamic-message-docs", + "Get all Dynamic Message Docs", + s"""Get all Dynamic Message Docs. + | + |""", + EmptyBody, + ListResult("dynamic-message-docs", jsonDynamicMessageDoc::Nil), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagDynamicMessageDoc, apiTagNewStyle), + Some(List(canGetAllDynamicMessageDocs))) + + lazy val getAllDynamicMessageDocs: OBPEndpoint = { + case "management" :: "dynamic-message-docs" :: Nil JsonGet _ => { + cc => + for { + (dynamicMessageDocs, callContext) <- NewStyle.function.getJsonDynamicMessageDocs(cc.callContext) + } yield { + (ListResult("dynamic-message-docs", dynamicMessageDocs), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + deleteDynamicMessageDoc, + implementedInApiVersion, + nameOf(deleteDynamicMessageDoc), + "DELETE", + "/management/dynamic-message-docs/DYNAMIC-MESSAGE-DOC-ID", + "Delete Dynamic Message Doc", + s"""Delete a Dynamic Message Doc. + |""", + EmptyBody, + BooleanBody(true), + List( + $UserNotLoggedIn, + UserHasMissingRoles, + InvalidJsonFormat, + UnknownError + ), + List(apiTagDynamicMessageDoc, apiTagNewStyle), + Some(List(canDeleteDynamicMessageDoc))) + + lazy val deleteDynamicMessageDoc: OBPEndpoint = { + case "management" :: "dynamic-message-docs" :: dynamicMessageDocId :: Nil JsonDelete _ => { + cc => + for { + (_, callContext) <- NewStyle.function.getJsonDynamicMessageDocById(dynamicMessageDocId, cc.callContext) + (dynamicResourceDoc, callContext) <- NewStyle.function.deleteJsonDynamicMessageDocById(dynamicMessageDocId, callContext) + } yield { + (dynamicResourceDoc, HttpCode.`204`(callContext)) + } + } + } + } } diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index 77f3ca77e..d7d1e284b 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -27,7 +27,6 @@ package code.api.v4_0_0 import java.util.Date - import code.api.attributedefinition.AttributeDefinition import code.api.util.APIUtil import code.api.util.APIUtil.{stringOptionOrNull, stringOrNull} @@ -50,7 +49,9 @@ import code.standingorders.StandingOrderTrait import code.transactionrequests.TransactionRequests.TransactionChallengeTypes import code.userlocks.UserLocks import com.openbankproject.commons.model.{DirectDebitTrait, _} +import com.openbankproject.commons.util.JsonAble import net.liftweb.common.{Box, Full} +import net.liftweb.json.{Extraction, Formats, JValue, JsonAST} import scala.collection.immutable.List @@ -620,6 +621,13 @@ case class TransactionBankAccountJson( transaction_id: String ) +case class ResourceDocFragment( + requestVerb: String, + requestUrl: String, + exampleRequestBody: Option[JValue], + successResponseBody: Option[JValue] + ) extends JsonFieldReName + object JSONFactory400 { def createCallsLimitJson(rateLimiting: RateLimiting) : CallLimitJsonV400 = { diff --git a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala index 1c889d31c..034bf0664 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/OBPAPI4_0_0.scala @@ -38,6 +38,7 @@ import code.api.v3_0_0.APIMethods300 import code.api.v3_0_0.custom.CustomAPIMethods300 import code.api.v3_1_0.OBPAPI3_1_0.Implementations3_1_0 import code.api.v3_1_0.{APIMethods310, OBPAPI3_1_0} +import code.api.v4_0_0.dynamic.DynamicEndpoints import code.util.Helper.MdcLoggable import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.util.ApiVersion @@ -86,6 +87,16 @@ object OBPAPI4_0_0 extends OBPRestHelper with APIMethods130 with APIMethods140 w oauthServe(apiPrefix{Implementations4_0_0.genericEndpoint}, None) oauthServe(apiPrefix{Implementations4_0_0.dynamicEndpoint}, None) + /** + * Here is the place where we register the dynamicEndpoint, all the dynamic resource docs endpoints are here. + * Actually, we only register one endpoint for all the dynamic resource docs endpoints. + * For Liftweb, it just need to handle one endpoint, + * all the router functionalities are in OBP code. + * details: please also check code/api/v4_0_0/dynamic/DynamicEndpoints.findEndpoint method + * NOTE: this must be the last one endpoint to register into Liftweb + * Because firstly, Liftweb should look for the static endpoints --> then the dynamic ones. + */ + oauthServe(apiPrefix{DynamicEndpoints.dynamicEndpoint}, None) logger.info(s"version $version has been run! There are ${routes.length} routes.") diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicCompileEndpoint.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicCompileEndpoint.scala new file mode 100644 index 000000000..f8e080abf --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicCompileEndpoint.scala @@ -0,0 +1,30 @@ +package code.api.v4_0_0.dynamic + +import code.api.util.APIUtil.{OBPEndpoint, OBPReturnType, futureToBoxedResponse, scalaFutureToLaFuture} +import code.api.util.{CallContext, CustomJsonFormats} +import net.liftweb.common.Box +import net.liftweb.http.{JsonResponse, Req} + +/** + * this is super trait of dynamic compile endpoint, the dynamic compiled code should extends this trait and supply + * logic of process method + */ +trait DynamicCompileEndpoint { + implicit val formats = CustomJsonFormats.formats + + protected def process(callContext: CallContext, request: Req): Box[JsonResponse] + + val endpoint: OBPEndpoint = new OBPEndpoint { + override def isDefinedAt(x: Req): Boolean = true + + override def apply(request: Req): CallContext => Box[JsonResponse] = process(_, request) + } + + protected implicit def scalaFutureToBoxedJsonResponse[T](scf: OBPReturnType[T])(implicit m: Manifest[T]): Box[JsonResponse] = { + futureToBoxedResponse(scalaFutureToLaFuture(scf)) + } + protected def getPathParams(callContext: CallContext, request: Req): Map[String, String] = { + val Some(resourceDoc) = callContext.resourceDocument + resourceDoc.getPathParams(request.path.partPath) + } +} 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/dynamic/DynamicEndpointHelper.scala similarity index 99% rename from obp-api/src/main/scala/code/api/v4_0_0/DynamicEndpointHelper.scala rename to obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpointHelper.scala index 7b6fe5d80..6e0c1f7eb 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/dynamic/DynamicEndpointHelper.scala @@ -1,10 +1,4 @@ -package code.api.v4_0_0 - -import java.io.File -import java.nio.charset.Charset -import java.util -import java.util.regex.Pattern -import java.util.{Date, UUID} +package code.api.v4_0_0.dynamic import akka.http.scaladsl.model.{HttpMethods, HttpMethod => AkkaHttpMethod} import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT} @@ -31,6 +25,11 @@ import org.apache.commons.collections4.MapUtils import org.apache.commons.io.FileUtils import org.apache.commons.lang3.StringUtils +import java.io.File +import java.nio.charset.Charset +import java.util +import java.util.regex.Pattern +import java.util.{Date, UUID} import scala.collection.JavaConverters._ import scala.collection.immutable.List import scala.collection.mutable diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala new file mode 100644 index 000000000..719b4d17f --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEndpoints.scala @@ -0,0 +1,191 @@ +package code.api.v4_0_0.dynamic + +import code.api.util.APIUtil.{BooleanBody, DoubleBody, EmptyBody, LongBody, OBPEndpoint, PrimaryDataBody, ResourceDoc, StringBody} +import code.api.util.{CallContext, DynamicUtil} +import code.api.v4_0_0.dynamic.practise.{DynamicEndpointCodeGenerator, PractiseEndpointGroup} +import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.http.{JsonResponse, Req} +import net.liftweb.json.{JNothing, JValue} +import net.liftweb.json.JsonAST.{JBool, JDouble, JInt, JString} +import org.apache.commons.lang3.StringUtils + +import java.net.URLDecoder +import scala.collection.immutable.List +import scala.util.control.Breaks.{break, breakable} + +object DynamicEndpoints { + //TODO, better put all other dynamic endpoints into this list. eg: dynamicEntityEndpoints, dynamicSwaggerDocsEndpoints .... + private val endpointGroups: List[EndpointGroup] = PractiseEndpointGroup :: DynamicResourceDocsEndpointGroup :: Nil + + /** + * this will find dynamic endpoint by request. + * the dynamic endpoints can be in obp database or memory or generated by obp code. + * This will be the OBP Router for all the dynamic endpoints. + * + */ + private def findEndpoint(req: Req): Option[OBPEndpoint] = { + var foundEndpoint: Option[OBPEndpoint] = None + breakable{ + endpointGroups.foreach { endpointGroup => { + val maybeEndpoint: Option[OBPEndpoint] = endpointGroup.endpoints.find(_.isDefinedAt(req)) + if(maybeEndpoint.isDefined) { + foundEndpoint = maybeEndpoint + break + } + }} + } + foundEndpoint + } + + /** + * This endpoint will be registered into Liftweb. + * It is only one endpoint for Liftweb <---> but it mean many for obp dynamic endpoints + * Because inside the method body, we override the `isDefinedAt` method, + * We can loop all the dynamic endpoints from obp database (better check EndpointGroup.endpoints we generate the endpoints + * by resourceDocs, then we can create the endpoints object in memory). + * + */ + val dynamicEndpoint: OBPEndpoint = new OBPEndpoint { + override def isDefinedAt(req: Req): Boolean = findEndpoint(req).isDefined + + override def apply(req: Req): CallContext => Box[JsonResponse] = { + val Some(endpoint) = findEndpoint(req) + endpoint(req) + } + } + + def dynamicResourceDocs: List[ResourceDoc] = endpointGroups.flatMap(_.docs) +} + +trait EndpointGroup { + protected def resourceDocs: List[ResourceDoc] + + protected lazy val urlPrefix: String = "" + + // reset urlPrefix resourceDocs + def docs: List[ResourceDoc] = if(StringUtils.isBlank(urlPrefix)) { + resourceDocs + } else { + resourceDocs map { doc => + val newUrl = s"/$urlPrefix/${doc.requestUrl}".replace("//", "/") + val newDoc = doc.copy(requestUrl = newUrl) + newDoc.connectorMethods = doc.connectorMethods // copy method will not keep var value, So here reset it manually + newDoc + } + } + + /** + * this method will generate the endpoints from the resourceDocs. + */ + def endpoints: List[OBPEndpoint] = docs.map(wrapEndpoint) + + //fill callContext with resourceDoc and operationId + private def wrapEndpoint(resourceDoc: ResourceDoc): OBPEndpoint = { + + val endpointFunction = resourceDoc.wrappedWithAuthCheck(resourceDoc.partialFunction) + + new OBPEndpoint { + override def isDefinedAt(req: Req): Boolean = req.requestType.method == resourceDoc.requestVerb && endpointFunction.isDefinedAt(req) + + override def apply(req: Req): CallContext => Box[JsonResponse] = { + (callContext: CallContext) => { + // fill callContext with resourceDoc and operationId, this will map the resourceDoc to endpoint. + val newCallContext = callContext.copy(resourceDocument = Some(resourceDoc), operationId = Some(resourceDoc.operationId)) + endpointFunction(req)(newCallContext) + } + } + } + } +} + +/** + * This class will generate the ResourceDoc class fields(requestBody: Product, successResponse: Product and partialFunction: OBPEndpoint) + * by parameters: JValues and Strings. + * successResponseBody: Option[JValue] --> toCaseObject(from JValue --> Scala code --> DynamicUtil.compileScalaCode --> generate the object. + * methodBody: String --> prepare the template api level scala code --> DynamicUtil.compileScalaCode --> generate the api level code. + * + * @param exampleRequestBody exampleRequestBody from the post json body, it is JValue here. + * @param successResponseBody successResponseBody from the post json body,it is JValue here. + * @param methodBody it is url-encoded string for the api level code. + */ +case class CompiledObjects(exampleRequestBody: Option[JValue], successResponseBody: Option[JValue], methodBody: String) { + val decodedMethodBody = URLDecoder.decode(methodBody, "UTF-8") + val requestBody: Product = exampleRequestBody match { + //this case means, we accept the empty string "" from json post body, we need to map it to None. + case Some(JString(s)) if StringUtils.isBlank(s) => toCaseObject(None) + // Here we will generate the object by the JValue (exampleRequestBody) + case _ => toCaseObject(exampleRequestBody) + } + val successResponse: Product = toCaseObject(successResponseBody) + + val partialFunction: OBPEndpoint = { + + //If the requestBody is PrimaryDataBody, return None. otherwise, return the exampleRequestBody:Option[JValue] + // In side OBP resourceDoc, requestBody and successResponse must be Product type, + // both can not be the primitive type: `boolean, string, kong, int, long, double` and List. + // PrimaryDataBody is used for OBP mapping these types. + // Note: List and object will generate the `Case class`, `case class` must not be PrimaryDataBody. only these two + // possibilities: case class or PrimaryDataBody + val requestExample: Option[JValue] = if (requestBody.isInstanceOf[PrimaryDataBody[_]]) { + None + } else exampleRequestBody + + val responseExample: Option[JValue] = if (successResponse.isInstanceOf[PrimaryDataBody[_]]) { + None + } else successResponseBody + + // buildCaseClasses --> will generate the following case classes string, which are used for the scala template code. + // case class RequestRootJsonClass(name: String, age: Long) + // case class ResponseRootJsonClass(person_id: String, name: String, age: Long) + val (requestBodyCaseClasses, responseBodyCaseClasses) = DynamicEndpointCodeGenerator.buildCaseClasses(requestExample, responseExample) + + val code = + s""" + |import code.api.util.APIUtil.errorJsonResponse + |import code.api.util.CallContext + |import code.api.util.ErrorMessages.{InvalidJsonFormat, InvalidRequestPayload} + |import code.api.util.NewStyle.HttpCode + |import code.api.v4_0_0.dynamic.DynamicCompileEndpoint + |import net.liftweb.common.{Box, EmptyBox, Full} + |import net.liftweb.http.{JsonResponse, Req} + |import net.liftweb.json.MappingException + | + |import scala.concurrent.Future + | + |$requestBodyCaseClasses + | + |$responseBodyCaseClasses + | + |(new DynamicCompileEndpoint { + | override protected def process(callContext: CallContext, request: Req): Box[JsonResponse] = { + | $decodedMethodBody + | } + |}).endpoint + | + |""".stripMargin + val endpointMethod = DynamicUtil.compileScalaCode[OBPEndpoint](code) + + endpointMethod match { + case Full(func) => func + case Failure(msg: String, exception: Box[Throwable], _) => + throw exception.getOrElse(new RuntimeException(msg)) + case _ => throw new RuntimeException("compiled code return nothing") + } + } + + private def toCaseObject(jValue: Option[JValue]): Product = { + if (jValue.isEmpty || jValue.exists(JNothing ==)) { + EmptyBody + } else { + jValue.orNull match { + case JBool(b) => BooleanBody(b) + case JInt(l) => LongBody(l.toLong) + case JDouble(d) => DoubleBody(d) + case JString(s) => StringBody(s) + case v => DynamicUtil.toCaseObject(v) + } + } + } +} + + diff --git a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEntityHelper.scala similarity index 98% rename from obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala rename to obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEntityHelper.scala index 60be8b9c1..c9668c350 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/DynamicEntityHelper.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicEntityHelper.scala @@ -1,10 +1,10 @@ -package code.api.v4_0_0 +package code.api.v4_0_0.dynamic import code.api.util.APIUtil.{EmptyBody, ResourceDoc, authenticationRequiredMessage, generateUUID} import code.api.util.ApiRole.getOrCreateDynamicApiRole -import code.api.util.ApiTag.{ResourceDocTag, apiTagApi, apiTagDynamic, apiTagDynamicEndpoint, apiTagNewStyle} +import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{InvalidJsonFormat, UnknownError, UserHasMissingRoles, UserNotLoggedIn} -import code.api.util.{APIUtil, ApiRole, ApiTag, ExampleValue, NewStyle} +import code.api.util._ import com.openbankproject.commons.model.enums.{DynamicEntityFieldType, DynamicEntityOperation} import com.openbankproject.commons.util.ApiVersion import net.liftweb.json.JsonDSL._ diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicResourceDocsEndpointGroup.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicResourceDocsEndpointGroup.scala new file mode 100644 index 000000000..58f661faf --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/DynamicResourceDocsEndpointGroup.scala @@ -0,0 +1,60 @@ +package code.api.v4_0_0.dynamic + +import code.api.util.APIUtil.ResourceDoc +import code.api.util.{APIUtil, ApiRole, ApiTag} +import code.dynamicResourceDoc.{DynamicResourceDocProvider, JsonDynamicResourceDoc} +import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.List + +object DynamicResourceDocsEndpointGroup extends EndpointGroup { + override lazy val urlPrefix: String = APIUtil.getPropsValue("url.prefix.dynamic.resourceDoc", "dynamic-resource-doc") + + + override protected def resourceDocs: List[APIUtil.ResourceDoc] = + DynamicResourceDocProvider.provider.vend.getAllAndConvert(toResourceDoc) + + private val apiVersion : ScannedApiVersion = ApiVersion.v4_0_0 + + /** + * this is a function, convert JsonDynamicResourceDoc => ResourceDoc + * + * the core difference between JsonDynamicResourceDoc and ResourceDoc are the following: + * + * 1st: JsonDynamicResourceDoc.methodBody <---vs---> ResourceDoc no methodBody + * + * 2rd: JsonDynamicResourceDoc.exampleRequestBody : Option[JValue] <---vs---> ResourceDoc.exampleRequestBody: scala.Product + * + * 3rd: JsonDynamicResourceDoc no partialFunction <---vs---> partialFunction: OBPEndpoint + * + * .... + * + * We need to prepare the ResourceDoc fields from JsonDynamicResourceDoc. + * @CompiledObjects also see this class, + * + */ + private val toResourceDoc: JsonDynamicResourceDoc => ResourceDoc = { dynamicDoc => + val compiledObjects = CompiledObjects(dynamicDoc.exampleRequestBody, dynamicDoc.successResponseBody, dynamicDoc.methodBody) + ResourceDoc( + partialFunction = compiledObjects.partialFunction, //connectorMethodBody + implementedInApiVersion = apiVersion, + partialFunctionName = dynamicDoc.partialFunctionName + "_" + (dynamicDoc.requestVerb + dynamicDoc.requestUrl).hashCode, + requestVerb = dynamicDoc.requestVerb, + requestUrl = dynamicDoc.requestUrl, + summary = dynamicDoc.summary, + description = dynamicDoc.description, + exampleRequestBody = compiledObjects.requestBody,// compiled case object + successResponseBody = compiledObjects.successResponse, //compiled case object + errorResponseBodies = StringUtils.split(dynamicDoc.errorResponseBodies,",").toList, + tags = dynamicDoc.tags.split(",").map(ApiTag(_)).toList, + roles = Option(dynamicDoc.roles) + .filter(StringUtils.isNoneBlank(_)) + .map { it => + StringUtils.split(it, ",") + .map(ApiRole.getOrCreateDynamicApiRole(_)) + .toList + } + ) + } +} diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/DynamicEndpointCodeGenerator.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/DynamicEndpointCodeGenerator.scala new file mode 100644 index 000000000..daa11f468 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/DynamicEndpointCodeGenerator.scala @@ -0,0 +1,164 @@ +package code.api.v4_0_0.dynamic.practise + +import code.api.util.APIUtil.ResourceDoc +import code.api.v4_0_0.ResourceDocFragment +import com.google.common.base.CaseFormat +import com.openbankproject.commons.util.JsonUtils +import net.liftweb.json +import net.liftweb.json.JsonAST.{JBool, JDouble, JInt, JString} +import net.liftweb.json.{JArray, JObject, JValue} +import org.apache.commons.lang3.{ArrayUtils, StringUtils} + +object DynamicEndpointCodeGenerator { + + def buildTemplate(fragment: ResourceDocFragment) = { + val pathParamNames = ResourceDoc.findPathVariableNames(fragment.requestUrl) + + val pathVariables = if(ArrayUtils.isNotEmpty(pathParamNames)) { + val variables = pathParamNames.map(it => s"""val ${CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, it)} = pathParams("$it")""") + .mkString("\n ") + s""" // get Path Parameters, example: + | // if the requestUrl of resourceDoc is /hello/banks/BANK_ID/world + | // the request path is /hello/banks/bank_x/world + | //pathParams.get("BANK_ID") will get Option("bank_x") value + | val pathParams = getPathParams(callContext, request) + | $variables + |""".stripMargin + } else "" + + val (requestBodyCaseClasses, responseBodyCaseClasses) = buildCaseClasses(fragment.exampleRequestBody, fragment.successResponseBody) + + def requestEntityExp(str:String) = + s""" val requestEntity = request.json match { + | case Full(zson) => + | try { + | zson.extract[$str] + | } catch { + | case e: MappingException => + | return Full(errorJsonResponse(s"$$InvalidJsonFormat $${e.msg}")) + | } + | case _: EmptyBox => + | return Full(errorJsonResponse(s"$$InvalidRequestPayload Current request has no payload")) + | } + |""".stripMargin + + val requestEntity = fragment.exampleRequestBody match { + case Some(JBool(_)) => requestEntityExp("Boolean") + case Some(JInt(_)) => requestEntityExp("Long") + case Some(JDouble(_)) => requestEntityExp("Double") + case Some(JString(s)) if StringUtils.isNotBlank(s) => requestEntityExp("String") + case Some(JObject(_)) | Some(JArray(_)) => requestEntityExp("RequestRootJsonClass") + case _ => "" + } + + val responseEntity = fragment.successResponseBody match { + case Some(JBool(_)) => "val responseBody: Boolean = null" + case Some(JInt(_)) => "val responseBody: Long = null" + case Some(JDouble(_)) => "val responseBody: Double = null" + case Some(JString(_)) => "val responseBody: String = null" + case Some(JObject(_)) | Some(JArray(_)) => "val responseBody:ResponseRootJsonClass = null" + case _ => "val responseBody = null" + } + + s""" + |$requestBodyCaseClasses + | + |$responseBodyCaseClasses + | + | // request method + | val requestMethod = "${fragment.requestVerb}" + | val requestUrl = "${fragment.requestUrl}" + | + | // copy the whole method body as "dynamicResourceDoc" method body + | override protected def process(callContext: CallContext, request: Req): Box[JsonResponse] = { + | // please add import sentences here, those used by this method + | + | val Some(resourceDoc) = callContext.resourceDocument + | val hasRequestBody = request.body.isDefined + | + |$pathVariables + | + |$requestEntity + | + | // please add business logic here + | $responseEntity + | Future.successful { + | (responseBody, HttpCode.`200`(callContext.callContext)) + | } + | } + |""".stripMargin + } + + def buildTemplate(requestVerb: String, + requestUrl: String, + exampleRequestBody: Option[String], + successResponseBody: Option[String]): String = { + + buildTemplate( + ResourceDocFragment(requestVerb, requestUrl, + exampleRequestBody.map(json.parse(_)), + successResponseBody.map(json.parse(_)) + ) + ) + } + + /** + * also see @com.openbankproject.commons.util.JsonUtils#toCaseClasses + * it will generate the following case class strings: + * + * // all request case classes + * // case class RequestRootJsonClass(name: String, age: Long) + * // all response case classes + * // case class ResponseRootJsonClass(person_id: String, name: String, age: Long) + * + * @param exampleRequestBody : Option[JValue] + * @param successResponseBody: Option[JValue] + * @return + */ + def buildCaseClasses(exampleRequestBody: Option[JValue], successResponseBody: Option[JValue]): (String, String) = { + val requestBodyCaseClasses = if(exampleRequestBody.exists(it => it.isInstanceOf[JObject] || it.isInstanceOf[JArray])) { + val Some(requestBody) = exampleRequestBody + s""" // all request case classes + | ${JsonUtils.toCaseClasses(requestBody, "Request")} + |""".stripMargin + } else "" + + val responseBodyCaseClasses = if(successResponseBody.exists(it => it.isInstanceOf[JObject] || it.isInstanceOf[JArray])) { + val Some(responseBody) = successResponseBody + s""" // all response case classes + | ${JsonUtils.toCaseClasses(responseBody, "Response")} + |""".stripMargin + } else "" + + (requestBodyCaseClasses, responseBodyCaseClasses) + } + + /** + * by call this main method, you can create dynamic resource doc method body + * @param args + */ + def main(args: Array[String]): Unit = { + + val requestVerb = "POST" + val requestUrl = "/person/PERSON_ID" + + val requestBody = + """ + |{ + | "name": "Jhon", + | "age": 11 + |} + |""".stripMargin + val responseBoy = + """ + |{ + | "person_id": "person_id_value", + | "name": "Jhon", + | "age": 11 + |} + |""".stripMargin + + val generatedCode = buildTemplate(requestVerb, requestUrl, Option(requestBody), Option(responseBoy)) + println(generatedCode) + } +} diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpoint.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpoint.scala new file mode 100644 index 000000000..f9331a067 --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpoint.scala @@ -0,0 +1,65 @@ +package code.api.v4_0_0.dynamic.practise + +import code.api.util.APIUtil.errorJsonResponse +import code.api.util.CallContext +import code.api.util.ErrorMessages.{InvalidJsonFormat, InvalidRequestPayload} +import code.api.util.NewStyle.HttpCode +import code.api.v4_0_0.dynamic.DynamicCompileEndpoint +import net.liftweb.common.{Box, EmptyBox, Full} +import net.liftweb.http.{JsonResponse, Req} +import net.liftweb.json.MappingException + +import scala.concurrent.Future + +/** + * practise new endpoint at this object, don't commit you practise code to git + */ +object PractiseEndpoint extends DynamicCompileEndpoint { + // all request case classes + case class RequestRootJsonClass(name: String, age: Long, hobby: Option[List[String]]) + + + // all response case classes + case class ResponseRootJsonClass(my_user_id: String, name: String, age: Long, hobby: Option[List[String]]) + + + // request method + val requestMethod = "POST" + val requestUrl = "/my_user/MY_USER_ID" + + // copy the whole method body as "dynamicResourceDoc" method body + override protected def process(callContext: CallContext, request: Req): Box[JsonResponse] = { + // please add import sentences here, those used by this method + + val Some(resourceDoc) = callContext.resourceDocument + val hasRequestBody = request.body.isDefined + + // get Path Parameters, example: + // if the requestUrl of resourceDoc is /hello/banks/BANK_ID/world + // the request path is /hello/banks/bank_x/world + //pathParams.get("BANK_ID") will get Option("bank_x") value + val pathParams = getPathParams(callContext, request) + val myUserId = pathParams("MY_USER_ID") + + + val requestEntity = request.json match { + case Full(zson) => + try { + zson.extract[RequestRootJsonClass] + } catch { + case e: MappingException => + return Full(errorJsonResponse(s"$InvalidJsonFormat ${e.msg}")) + } + case _: EmptyBox => + return Full(errorJsonResponse(s"$InvalidRequestPayload Current request has no payload")) + } + + + // please add business logic here + val responseBody:ResponseRootJsonClass = ResponseRootJsonClass(s"${myUserId}_from_path", requestEntity.name, requestEntity.age, requestEntity.hobby) + Future.successful { + (responseBody, HttpCode.`200`(callContext.callContext)) + } + } + +} diff --git a/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpointGroup.scala b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpointGroup.scala new file mode 100644 index 000000000..7f809393e --- /dev/null +++ b/obp-api/src/main/scala/code/api/v4_0_0/dynamic/practise/PractiseEndpointGroup.scala @@ -0,0 +1,35 @@ +package code.api.v4_0_0.dynamic.practise + +import code.api.util.APIUtil +import code.api.util.APIUtil.{ResourceDoc, StringBody} +import code.api.util.ApiTag.{apiTagDynamicResourceDoc, apiTagNewStyle} +import code.api.util.ErrorMessages.UnknownError +import code.api.v4_0_0.dynamic.EndpointGroup +import com.openbankproject.commons.util.ApiVersion + +import scala.collection.immutable.List + +/** + * this is just for developer to create new dynamic endpoint, and debug it + */ +object PractiseEndpointGroup extends EndpointGroup{ + + override protected lazy val urlPrefix: String = "test-dynamic-resource-doc" + + override protected def resourceDocs: List[APIUtil.ResourceDoc] = ResourceDoc( + PractiseEndpoint.endpoint, + ApiVersion.v4_0_0, + "test-dynamic-resource-doc", + PractiseEndpoint.requestMethod, + PractiseEndpoint.requestUrl, + "A test endpoint", + s"""A test endpoint. + |Just for debug method body of dynamic resource doc + |""", + StringBody("Any request body"), + StringBody("Any response body"), + List( + UnknownError + ), + List(apiTagDynamicResourceDoc, apiTagNewStyle)) :: Nil +} diff --git a/obp-api/src/main/scala/code/bankconnectors/DynamicConnector.scala b/obp-api/src/main/scala/code/bankconnectors/DynamicConnector.scala new file mode 100644 index 000000000..848c230b0 --- /dev/null +++ b/obp-api/src/main/scala/code/bankconnectors/DynamicConnector.scala @@ -0,0 +1,52 @@ +package code.bankconnectors + +import code.api.util.DynamicUtil.compileScalaCode +import code.api.util.{CallContext, DynamicUtil} +import code.dynamicMessageDoc.{DynamicMessageDocProvider, JsonDynamicMessageDoc} +import net.liftweb.common.Box +import com.openbankproject.commons.ExecutionContext.Implicits.global +import scala.concurrent.Future +import code.api.util.APIUtil.{EntitlementAndScopeStatus, JsonResponseExtractor, OBPReturnType} + + +object DynamicConnector { + + + def invoke(process: String, args: Array[AnyRef], callContext: Option[CallContext]) = { + val function: Box[(Array[AnyRef], Option[CallContext]) => Future[Box[(AnyRef, Option[CallContext])]]] = + getFunction(process).asInstanceOf[Box[(Array[AnyRef], Option[CallContext]) => Future[Box[(AnyRef, Option[CallContext])]]]] + function.map(f =>f(args: Array[AnyRef], callContext: Option[CallContext])).openOrThrowException(s"There is no process $process, it should not be called here") + } + + private def getFunction(process: String) = { + DynamicMessageDocProvider.provider.vend.getByProcess(process) map { + case v :JsonDynamicMessageDoc => + createFunction(process, v.decodedMethodBody).openOrThrowException(s"InternalConnector method compile fail") + } + } + + + + /** + * dynamic create function + * @param process name of connector + * @param methodBody method body of connector method + * @return function of connector method that is dynamic created, can be Function0, Function1, Function2... + */ + def createFunction(process: String, methodBody:String): Box[Any] = + { + //messageDoc.process is a bit different with the methodName, we need tweak the format of it: + //eg: process("obp.getBank") ==> methodName("getBank") + val method = s""" + |${DynamicUtil.importStatements} + |def func(args: Array[AnyRef], callContext: Option[CallContext]): Future[Box[(AnyRef, Option[CallContext])]] = { + | $methodBody + |} + | + |func _ + |""".stripMargin + compileScalaCode(method) + } + +} + diff --git a/obp-api/src/main/scala/code/bankconnectors/InternalConnector.scala b/obp-api/src/main/scala/code/bankconnectors/InternalConnector.scala index ce8974005..a99890224 100644 --- a/obp-api/src/main/scala/code/bankconnectors/InternalConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/InternalConnector.scala @@ -1,15 +1,14 @@ package code.bankconnectors +import code.api.util.DynamicUtil.compileScalaCode import code.connectormethod.{ConnectorMethodProvider, JsonConnectorMethod} import com.github.dwickern.macros.NameOf.nameOf import net.liftweb.common.{Box, Failure} import net.sf.cglib.proxy.{Enhancer, MethodInterceptor, MethodProxy} import org.apache.commons.lang3.StringUtils - import java.lang.reflect.Method -import java.util.concurrent.ConcurrentHashMap +import code.api.util.DynamicUtil import scala.reflect.runtime.universe.{MethodSymbol, TermSymbol, typeOf} -import scala.tools.reflect.{ToolBox, ToolBoxError} object InternalConnector { @@ -26,10 +25,6 @@ object InternalConnector { // in this object, you must make sure this object is empty. } - // (methodName,methodBody) -> dynamic method function - // connector methods count is 230, make the initialCapacity a little bigger - private val dynamicMethods = new ConcurrentHashMap[(String, String), Any](300) - private val intercept:MethodInterceptor = (_: Any, method: Method, args: Array[AnyRef], _: MethodProxy) => { val methodName = method.getName if(methodName == nameOf(connector.callableMethods)) { @@ -37,48 +32,18 @@ object InternalConnector { } else if (methodName.contains("$default$")) { method.invoke(connector, args:_*) } else { - val result = getFunction(methodName).orNull match { - case func: Function0[AnyRef] => func() - case func: Function1[AnyRef,AnyRef] => func(args.head) - case func: Function2[AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1)) - case func: Function3[AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2)) - case func: Function4[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3)) - case func: Function5[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4)) - case func: Function6[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5)) - case func: Function7[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6)) - case func: Function8[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7)) - case func: Function9[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8)) - case func: Function10[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9)) - case func: Function11[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10)) - case func: Function12[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11)) - case func: Function13[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12)) - case func: Function14[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13)) - case func: Function15[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14)) - case func: Function16[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15)) - case func: Function17[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16)) - case func: Function18[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17)) - case func: Function19[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17), args.apply(18)) - case func: Function20[AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef,AnyRef] => func(args.head, args.apply(1), args.apply(2), args.apply(3), args.apply(4), args.apply(5), args.apply(6), args.apply(7), args.apply(8), args.apply(9), args.apply(10), args.apply(11), args.apply(12), args.apply(13), args.apply(14), args.apply(15), args.apply(16), args.apply(17), args.apply(18), args.apply(19)) - case null => throw new IllegalStateException(s"InternalConnector have no method $methodName, it should not be called on InternalConnector") - case _ => throw new IllegalStateException(s"InternalConnector have not correct method: $methodName") - } - result.asInstanceOf[AnyRef] + val function = getFunction(methodName) + DynamicUtil.executeFunction(methodName, function, args) } } private def getFunction(methodName: String) = { ConnectorMethodProvider.provider.vend.getByMethodNameWithCache(methodName) map { - case v @ JsonConnectorMethod(_, _, methodBody) => - dynamicMethods.computeIfAbsent( - methodName -> methodBody, - _ => createFunction(methodName, v.decodedMethodBody).openOrThrowException(s"InternalConnector method compile fail, method name $methodName") - ) + case v :JsonConnectorMethod => + createFunction(methodName, v.decodedMethodBody).openOrThrowException(s"InternalConnector method compile fail, method name $methodName") } } - private val toolBox = scala.reflect.runtime.currentMirror.mkToolBox() -// private val toolBox = runtimeMirror(getClass.getClassLoader).mkToolBox() - /** * dynamic create function * @param methodName method name of connector @@ -90,7 +55,7 @@ object InternalConnector { case Some(signature) => val method = s""" |def $methodName $signature = { - | $importStatements + | ${DynamicUtil.importStatements} | | $methodBody |} @@ -98,45 +63,16 @@ object InternalConnector { |$methodName _ |""".stripMargin - compile(method) - case None => Failure(s"method name $methodName not exists in Connector") + compileScalaCode(method) + case None => Failure(s"method name $methodName does not exist in the Connector") } - /** - * toolBox have bug that first compile fail, second or later compile success. - * @param code - * @return compiled function or Failure - */ - private def compile(code: String): Box[Any] = { - val tree = try { - toolBox.parse(code) - } catch { - case e: ToolBoxError => - return Failure(e.message) - } - - try { - val func: () => Any = toolBox.compile(tree) - Box.tryo(func()) - } catch { - case _: ToolBoxError => - // try compile again - try { - val func: () => Any = toolBox.compile(tree) - Box.tryo(func()) - } catch { - case e: ToolBoxError => - Failure(e.message) - } - } - - } private def callableMethods: Map[String, MethodSymbol] = { val dynamicMethods: Map[String, MethodSymbol] = ConnectorMethodProvider.provider.vend.getAll().map { case JsonConnectorMethod(_, methodName, _) => - methodName -> Box(methodNameToSymbols.get(methodName)).openOrThrowException(s"method name $methodName not exists in Connector") + methodName -> Box(methodNameToSymbols.get(methodName)).openOrThrowException(s"method name $methodName does not exist in the Connector") } toMap dynamicMethods @@ -156,56 +92,5 @@ object InternalConnector { val methodSignature = StringUtils.substringBeforeLast(signature, returnType) + ":" + returnType methodName -> methodSignature } - - /** - * common import statements those are used by connector method body - */ - private val importStatements = - """ - |import java.net.{ConnectException, URLEncoder, UnknownHostException} - |import java.util.Date - |import java.util.UUID.randomUUID - | - |import _root_.akka.stream.StreamTcpException - |import akka.http.scaladsl.model.headers.RawHeader - |import akka.http.scaladsl.model.{HttpProtocol, _} - |import akka.util.ByteString - |import code.api.APIFailureNewStyle - |import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions - |import code.api.cache.Caching - |import code.api.util.APIUtil.{AdapterImplementation, MessageDoc, OBPReturnType, saveConnectorMetric, _} - |import code.api.util.ErrorMessages._ - |import code.api.util.ExampleValue._ - |import code.api.util.{APIUtil, CallContext, OBPQueryParam} - |import code.api.v4_0_0.MockResponseHolder - |import code.bankconnectors._ - |import code.bankconnectors.vJune2017.AuthInfo - |import code.customer.internalMapping.MappedCustomerIdMappingProvider - |import code.kafka.KafkaHelper - |import code.model.dataAccess.internalMapping.MappedAccountIdMappingProvider - |import code.util.AkkaHttpClient._ - |import code.util.Helper.MdcLoggable - |import com.openbankproject.commons.dto.{InBoundTrait, _} - |import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA - |import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} - |import com.openbankproject.commons.model.{ErrorMessage, TopicTrait, _} - |import com.openbankproject.commons.util.{JsonUtils, ReflectUtils} - |// import com.tesobe.{CacheKeyFromArguments, CacheKeyOmit} - |import net.liftweb.common.{Box, Empty, _} - |import net.liftweb.json - |import net.liftweb.json.Extraction.decompose - |import net.liftweb.json.JsonDSL._ - |import net.liftweb.json.JsonParser.ParseException - |import net.liftweb.json.{JValue, _} - |import net.liftweb.util.Helpers.tryo - |import org.apache.commons.lang3.StringUtils - | - |import scala.collection.immutable.List - |import scala.collection.mutable.ArrayBuffer - |import scala.concurrent.duration._ - |import scala.concurrent.{Await, Future} - |import com.openbankproject.commons.dto._ - |""".stripMargin - } 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 54c6f21ba..963d08c2e 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 @@ -26,7 +26,6 @@ Berlin 13359, Germany import java.net.{ConnectException, URLEncoder, UnknownHostException} import java.util.Date import java.util.UUID.randomUUID - import _root_.akka.stream.StreamTcpException import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.model.{HttpProtocol, _} @@ -38,7 +37,7 @@ import code.api.util.APIUtil.{AdapterImplementation, MessageDoc, OBPReturnType, import code.api.util.ErrorMessages._ import code.api.util.ExampleValue._ import code.api.util.{APIUtil, CallContext, OBPQueryParam} -import code.api.v4_0_0.MockResponseHolder +import code.api.v4_0_0.dynamic.MockResponseHolder import code.bankconnectors._ import code.bankconnectors.vJune2017.AuthInfo import code.customer.internalMapping.MappedCustomerIdMappingProvider diff --git a/obp-api/src/main/scala/code/connectormethod/ConnectorMethodProvider.scala b/obp-api/src/main/scala/code/connectormethod/ConnectorMethodProvider.scala index 94daa2a65..083804a9f 100644 --- a/obp-api/src/main/scala/code/connectormethod/ConnectorMethodProvider.scala +++ b/obp-api/src/main/scala/code/connectormethod/ConnectorMethodProvider.scala @@ -13,7 +13,7 @@ object ConnectorMethodProvider extends SimpleInjector { def buildOne: MappedConnectorMethodProvider.type = MappedConnectorMethodProvider } -case class JsonConnectorMethod(internalConnectorId: Option[String], methodName: String, methodBody: String) extends JsonFieldReName{ +case class JsonConnectorMethod(connectorMethodId: Option[String], methodName: String, methodBody: String) extends JsonFieldReName{ def decodedMethodBody: String = URLDecoder.decode(methodBody, "UTF-8") } diff --git a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala index 1a6163680..d0b451dfe 100644 --- a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala +++ b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala @@ -501,7 +501,7 @@ object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommo checkFormat(fieldExample.isInstanceOf[JString], s"$DynamicEntityInstanceValidateFail The property of $fieldName's 'example' field should be type ${DynamicEntityFieldType.string}") checkFormat(ReferenceType.isLegalReferenceValue(fieldTypeName, fieldExample.asInstanceOf[JString].s), s"$DynamicEntityInstanceValidateFail The property of $fieldName's 'example' is illegal format.") } else { - // if go to here, means add new field type, but not supply corresponding value validation, that means a bug nead fix to avoid throw the follow Exception + // if go to here, means add new field type, but not supply corresponding value validation, that means a bug need fix to avoid throw the follow Exception throw new RuntimeException(s"DynamicEntity $entityName's field $fieldName, type is $fieldTypeName, this type is not do validation.") } diff --git a/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDoc.scala b/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDoc.scala new file mode 100644 index 000000000..fb1c286c9 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDoc.scala @@ -0,0 +1,43 @@ +package code.dynamicMessageDoc + +import code.util.UUIDString +import net.liftweb.json +import net.liftweb.mapper._ +import scala.collection.immutable.List + +class DynamicMessageDoc extends LongKeyedMapper[DynamicMessageDoc] with IdPK { + + override def getSingleton = DynamicMessageDoc + + object DynamicMessageDocId extends UUIDString(this) + object Process extends MappedString(this, 255) + object MessageFormat extends MappedString(this, 255) + object Description extends MappedString(this, 255) + object OutboundTopic extends MappedString(this, 255) + object InboundTopic extends MappedString(this, 255) + object ExampleOutboundMessage extends MappedText(this) + object ExampleInboundMessage extends MappedText(this) + object OutboundAvroSchema extends MappedText(this) + object InboundAvroSchema extends MappedText(this) + object AdapterImplementation extends MappedString(this, 255) + object MethodBody extends MappedText(this) +} + + +object DynamicMessageDoc extends DynamicMessageDoc with LongKeyedMetaMapper[DynamicMessageDoc] { + override def dbIndexes: List[BaseIndex[DynamicMessageDoc]] = UniqueIndex(DynamicMessageDocId) :: UniqueIndex(Process) :: super.dbIndexes + def getJsonDynamicMessageDoc(dynamicMessageDoc: DynamicMessageDoc) = JsonDynamicMessageDoc( + dynamicMessageDocId = Some(dynamicMessageDoc.DynamicMessageDocId.get), + process = dynamicMessageDoc.Process.get, + messageFormat = dynamicMessageDoc.MessageFormat.get, + description = dynamicMessageDoc.Description.get, + outboundTopic = dynamicMessageDoc.OutboundTopic.get, + inboundTopic = dynamicMessageDoc.InboundTopic.get, + exampleOutboundMessage = json.parse(dynamicMessageDoc.ExampleOutboundMessage.get), + exampleInboundMessage = json.parse(dynamicMessageDoc.ExampleInboundMessage.get), + outboundAvroSchema = dynamicMessageDoc.OutboundAvroSchema.get, + inboundAvroSchema = dynamicMessageDoc.InboundAvroSchema.get, + adapterImplementation = dynamicMessageDoc.AdapterImplementation.get, + methodBody = dynamicMessageDoc.MethodBody.get, + ) +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDocProvider.scala b/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDocProvider.scala new file mode 100644 index 000000000..1685a1030 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicMessageDoc/DynamicMessageDocProvider.scala @@ -0,0 +1,45 @@ +package code.dynamicMessageDoc + +import java.net.URLDecoder +import com.openbankproject.commons.model.JsonFieldReName +import net.liftweb.common.Box +import net.liftweb.json.JsonAST.JValue +import net.liftweb.util.SimpleInjector + +import scala.collection.immutable.List + +object DynamicMessageDocProvider extends SimpleInjector { + + val provider = new Inject(buildOne _) {} + + def buildOne: MappedDynamicMessageDocProvider.type = MappedDynamicMessageDocProvider +} + +case class JsonDynamicMessageDoc( + dynamicMessageDocId: Option[String], + process: String, + messageFormat: String, + description: String, + outboundTopic: String, + inboundTopic: String, + exampleOutboundMessage: JValue, + exampleInboundMessage: JValue, + outboundAvroSchema: String, + inboundAvroSchema: String, + adapterImplementation: String, + methodBody: String +) extends JsonFieldReName{ + def decodedMethodBody: String = URLDecoder.decode(methodBody, "UTF-8") +} + +trait DynamicMessageDocProvider { + + def getById(dynamicMessageDocId: String): Box[JsonDynamicMessageDoc] + def getByProcess(process: String): Box[JsonDynamicMessageDoc] + def getAll(): List[JsonDynamicMessageDoc] + + def create(entity: JsonDynamicMessageDoc): Box[JsonDynamicMessageDoc] + def update(entity: JsonDynamicMessageDoc): Box[JsonDynamicMessageDoc] + def deleteById(dynamicMessageDocId: String): Box[Boolean] + +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/dynamicMessageDoc/MappedDynamicMessageDocProvider.scala b/obp-api/src/main/scala/code/dynamicMessageDoc/MappedDynamicMessageDocProvider.scala new file mode 100644 index 000000000..75201b499 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicMessageDoc/MappedDynamicMessageDocProvider.scala @@ -0,0 +1,84 @@ +package code.dynamicMessageDoc + +import code.api.cache.Caching +import code.api.util.APIUtil +import com.tesobe.CacheKeyFromArguments +import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo +import net.liftweb.util.Props +import java.util.UUID.randomUUID +import code.util.Helper + +import scala.concurrent.duration.DurationInt + +object MappedDynamicMessageDocProvider extends DynamicMessageDocProvider { + + private val getDynamicMessageDocTTL : Int = { + if(Props.testMode) 0 + else APIUtil.getPropsValue(s"dynamicMessageDoc.cache.ttl.seconds", "40").toInt + } + + override def getById(dynamicMessageDocId: String): Box[JsonDynamicMessageDoc] = + DynamicMessageDoc.find(By(DynamicMessageDoc.DynamicMessageDocId, dynamicMessageDocId)) + .map(DynamicMessageDoc.getJsonDynamicMessageDoc) + + override def getByProcess(process: String): Box[JsonDynamicMessageDoc] = + DynamicMessageDoc.find(By(DynamicMessageDoc.Process, process)) + .map(DynamicMessageDoc.getJsonDynamicMessageDoc) + + + override def getAll(): List[JsonDynamicMessageDoc] = { + var cacheKey = (randomUUID().toString, randomUUID().toString, randomUUID().toString) + CacheKeyFromArguments.buildCacheKey { + Caching.memoizeSyncWithProvider (Some(cacheKey.toString())) (getDynamicMessageDocTTL second) { + DynamicMessageDoc.findAll() + .map(DynamicMessageDoc.getJsonDynamicMessageDoc) + }} + } + + override def create(entity: JsonDynamicMessageDoc): Box[JsonDynamicMessageDoc]= + tryo { + DynamicMessageDoc.create + .DynamicMessageDocId(APIUtil.generateUUID()) + .Process(entity.process) + .MessageFormat(entity.messageFormat) + .Description(entity.description) + .OutboundTopic(entity.outboundTopic) + .InboundTopic(entity.inboundTopic) + .ExampleOutboundMessage(Helper.prettyJson(entity.exampleOutboundMessage)) + .ExampleInboundMessage(Helper.prettyJson(entity.exampleInboundMessage)) + .OutboundAvroSchema(entity.outboundAvroSchema) + .InboundAvroSchema(entity.inboundAvroSchema) + .AdapterImplementation(entity.adapterImplementation) + .MethodBody(entity.methodBody) + .saveMe() + }.map(DynamicMessageDoc.getJsonDynamicMessageDoc) + + + override def update(entity: JsonDynamicMessageDoc): Box[JsonDynamicMessageDoc] = { + DynamicMessageDoc.find(By(DynamicMessageDoc.DynamicMessageDocId, entity.dynamicMessageDocId.getOrElse(""))) match { + case Full(v) => + tryo { + v.DynamicMessageDocId(entity.dynamicMessageDocId.getOrElse("")) + .Process(entity.process) + .MessageFormat(entity.messageFormat) + .Description(entity.description) + .OutboundTopic(entity.outboundTopic) + .InboundTopic(entity.inboundTopic) + .ExampleOutboundMessage(Helper.prettyJson(entity.exampleOutboundMessage)) + .ExampleInboundMessage(Helper.prettyJson(entity.exampleInboundMessage)) + .OutboundAvroSchema(entity.outboundAvroSchema) + .InboundAvroSchema(entity.inboundAvroSchema) + .AdapterImplementation(entity.adapterImplementation) + .MethodBody(entity.methodBody) + .saveMe() + }.map(DynamicMessageDoc.getJsonDynamicMessageDoc) + case _ => Empty + } + } + + override def deleteById(id: String): Box[Boolean] = tryo { + DynamicMessageDoc.bulkDelete_!!(By(DynamicMessageDoc.DynamicMessageDocId, id)) + } +} \ No newline at end of file diff --git a/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDoc.scala b/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDoc.scala new file mode 100644 index 000000000..c4ace87c6 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDoc.scala @@ -0,0 +1,47 @@ +package code.dynamicResourceDoc + +import code.util.UUIDString +import net.liftweb.json +import net.liftweb.mapper._ +import org.apache.commons.lang3.StringUtils + +import scala.collection.immutable.List + +class DynamicResourceDoc extends LongKeyedMapper[DynamicResourceDoc] with IdPK { + + override def getSingleton = DynamicResourceDoc + + object DynamicResourceDocId extends UUIDString(this) + object PartialFunctionName extends MappedString(this, 255) + object RequestVerb extends MappedString(this, 255) + object RequestUrl extends MappedString(this, 255) + object Summary extends MappedString(this, 255) + object Description extends MappedString(this, 255) + object ExampleRequestBody extends MappedString(this, 255) + object SuccessResponseBody extends MappedString(this, 255) + object ErrorResponseBodies extends MappedString(this, 255) + object Tags extends MappedString(this, 255) + object Roles extends MappedString(this, 255) + object MethodBody extends MappedText(this) + +} + + +object DynamicResourceDoc extends DynamicResourceDoc with LongKeyedMetaMapper[DynamicResourceDoc] { + override def dbIndexes: List[BaseIndex[DynamicResourceDoc]] = UniqueIndex(DynamicResourceDocId) :: UniqueIndex(RequestUrl,RequestVerb) :: super.dbIndexes + def getJsonDynamicResourceDoc(dynamicResourceDoc: DynamicResourceDoc) = JsonDynamicResourceDoc( + dynamicResourceDocId = Some(dynamicResourceDoc.DynamicResourceDocId.get), + methodBody = dynamicResourceDoc.MethodBody.get, + partialFunctionName = dynamicResourceDoc.PartialFunctionName.get, + requestVerb = dynamicResourceDoc.RequestVerb.get, + requestUrl = dynamicResourceDoc.RequestUrl.get, + summary = dynamicResourceDoc.Summary.get, + description = dynamicResourceDoc.Description.get, + exampleRequestBody = Option(dynamicResourceDoc.ExampleRequestBody.get).filter(StringUtils.isNotBlank).map(json.parse), + successResponseBody = Option(dynamicResourceDoc.SuccessResponseBody.get).filter(StringUtils.isNotBlank).map(json.parse), + errorResponseBodies = dynamicResourceDoc.ErrorResponseBodies.get, + tags = dynamicResourceDoc.Tags.get, + roles = dynamicResourceDoc.Roles.get + ) +} + diff --git a/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDocProvider.scala b/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDocProvider.scala new file mode 100644 index 000000000..51b28ad76 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicResourceDoc/DynamicResourceDocProvider.scala @@ -0,0 +1,52 @@ +package code.dynamicResourceDoc + +import com.openbankproject.commons.model.JsonFieldReName +import com.openbankproject.commons.util.JsonAble +import net.liftweb.common.Box +import net.liftweb.json +import net.liftweb.json.JsonAST.JNothing +import net.liftweb.json.{Formats, JValue, JsonAST} +import net.liftweb.util.SimpleInjector +import org.apache.commons.lang3.StringUtils + +import java.net.URLDecoder +import scala.collection.immutable.List + +object DynamicResourceDocProvider extends SimpleInjector { + + val provider = new Inject(buildOne _) {} + + def buildOne: MappedDynamicResourceDocProvider.type = MappedDynamicResourceDocProvider +} + +case class JsonDynamicResourceDoc( + dynamicResourceDocId: Option[String], + methodBody: String, + partialFunctionName: String, + requestVerb: String, + requestUrl: String, + summary: String, + description: String, + exampleRequestBody: Option[JValue], + successResponseBody: Option[JValue], + errorResponseBodies: String, + tags: String, + roles: String +) extends JsonFieldReName { + def decodedMethodBody: String = URLDecoder.decode(methodBody, "UTF-8") +} + +trait DynamicResourceDocProvider { + + def getById(dynamicResourceDocId: String): Box[JsonDynamicResourceDoc] + def getByVerbAndUrl(requestVerb: String, requestUrl: String): Box[JsonDynamicResourceDoc] + + def getAll(): List[JsonDynamicResourceDoc] = getAllAndConvert(identity) + + def getAllAndConvert[T: Manifest](transform: JsonDynamicResourceDoc => T): List[T] + + def create(entity: JsonDynamicResourceDoc): Box[JsonDynamicResourceDoc] + def update(entity: JsonDynamicResourceDoc): Box[JsonDynamicResourceDoc] + def deleteById(dynamicResourceDocId: String): Box[Boolean] + +} diff --git a/obp-api/src/main/scala/code/dynamicResourceDoc/MappedDynamicResourceDocProvider.scala b/obp-api/src/main/scala/code/dynamicResourceDoc/MappedDynamicResourceDocProvider.scala new file mode 100644 index 000000000..15f6222d2 --- /dev/null +++ b/obp-api/src/main/scala/code/dynamicResourceDoc/MappedDynamicResourceDocProvider.scala @@ -0,0 +1,90 @@ +package code.dynamicResourceDoc + +import code.api.cache.Caching +import code.api.util.APIUtil +import com.tesobe.CacheKeyFromArguments +import net.liftweb.common.{Box, Empty, Full} +import net.liftweb.json +import net.liftweb.mapper._ +import net.liftweb.util.Helpers.tryo +import net.liftweb.util.Props + +import java.util.UUID.randomUUID +import scala.concurrent.duration.DurationInt + +object MappedDynamicResourceDocProvider extends DynamicResourceDocProvider { + + private val getDynamicResourceDocTTL : Int = { + if(Props.testMode) 0 + else if(Props.devMode) 10 + else APIUtil.getPropsValue(s"dynamicResourceDoc.cache.ttl.seconds", "40").toInt + } + + override def getById(dynamicResourceDocId: String): Box[JsonDynamicResourceDoc] = DynamicResourceDoc + .find(By(DynamicResourceDoc.DynamicResourceDocId, dynamicResourceDocId)) + .map(DynamicResourceDoc.getJsonDynamicResourceDoc) + + override def getByVerbAndUrl(requestVerb: String, requestUrl: String): Box[JsonDynamicResourceDoc] = DynamicResourceDoc + .find(By(DynamicResourceDoc.RequestVerb, requestVerb), By(DynamicResourceDoc.RequestUrl, requestUrl)) + .map(DynamicResourceDoc.getJsonDynamicResourceDoc) + + override def getAllAndConvert[T: Manifest](transform: JsonDynamicResourceDoc => T): List[T] = { + var cacheKey = (randomUUID().toString, randomUUID().toString, randomUUID().toString) + CacheKeyFromArguments.buildCacheKey { + Caching.memoizeSyncWithProvider (Some(cacheKey.toString())) (getDynamicResourceDocTTL second) { + DynamicResourceDoc.findAll() + .map(doc => transform(DynamicResourceDoc.getJsonDynamicResourceDoc(doc))) + }} + } + + override def create(entity: JsonDynamicResourceDoc): Box[JsonDynamicResourceDoc]= + tryo { + val requestBody = entity.exampleRequestBody.map(json.compactRender(_)).orNull + val responseBody = entity.successResponseBody.map(json.compactRender(_)).orNull + + DynamicResourceDoc.create + .DynamicResourceDocId(APIUtil.generateUUID()) + .PartialFunctionName(entity.partialFunctionName) + .RequestVerb(entity.requestVerb) + .RequestUrl(entity.requestUrl) + .Summary(entity.summary) + .Description(entity.description) + .ExampleRequestBody(requestBody) + .SuccessResponseBody(responseBody) + .ErrorResponseBodies(entity.errorResponseBodies) + .Tags(entity.tags) + .Roles(entity.roles) + .MethodBody(entity.methodBody) + .saveMe() + }.map(DynamicResourceDoc.getJsonDynamicResourceDoc) + + + override def update(entity: JsonDynamicResourceDoc): Box[JsonDynamicResourceDoc] = { + DynamicResourceDoc.find(By(DynamicResourceDoc.DynamicResourceDocId, entity.dynamicResourceDocId.getOrElse(""))) match { + case Full(v) => + tryo { + val requestBody = entity.exampleRequestBody.map(json.compactRender(_)).orNull + val responseBody = entity.successResponseBody.map(json.compactRender(_)).orNull + v.PartialFunctionName(entity.partialFunctionName) + .RequestVerb(entity.requestVerb) + .RequestUrl(entity.requestUrl) + .Summary(entity.summary) + .Description(entity.description) + .ExampleRequestBody(requestBody) + .SuccessResponseBody(responseBody) + .ErrorResponseBodies(entity.errorResponseBodies) + .Tags(entity.tags) + .Roles(entity.roles) + .MethodBody(entity.methodBody) + .saveMe() + }.map(DynamicResourceDoc.getJsonDynamicResourceDoc) + case _ => Empty + } + } + + override def deleteById(id: String): Box[Boolean] = tryo { + DynamicResourceDoc.bulkDelete_!!(By(DynamicResourceDoc.DynamicResourceDocId, id)) + } +} + + diff --git a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala index 44bcc97c9..133bf203a 100644 --- a/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala +++ b/obp-api/src/main/scala/code/entitlement/MappedEntitlements.scala @@ -1,7 +1,6 @@ package code.entitlement - -import code.api.v4_0_0.DynamicEntityInfo +import code.api.v4_0_0.dynamic.DynamicEntityInfo import code.util.{MappedUUID, UUIDString} import net.liftweb.common.Box import net.liftweb.mapper._ diff --git a/obp-api/src/test/scala/RunMTLSWebApp.scala b/obp-api/src/test/scala/RunMTLSWebApp.scala index c9e3346c4..60a331dd5 100644 --- a/obp-api/src/test/scala/RunMTLSWebApp.scala +++ b/obp-api/src/test/scala/RunMTLSWebApp.scala @@ -40,6 +40,8 @@ import sun.security.provider.X509Factory object RunMTLSWebApp extends App { val servletContextPath = "/" + //set run mode value to "development", So the value is true of Props.devMode + System.setProperty("run.mode", "development") { val tempHTTPContext = JProxy.newProxyInstance(this.getClass.getClassLoader, Array(classOf[HTTPContext]), diff --git a/obp-api/src/test/scala/RunWebApp.scala b/obp-api/src/test/scala/RunWebApp.scala index 44ab74734..791c1ceb3 100644 --- a/obp-api/src/test/scala/RunWebApp.scala +++ b/obp-api/src/test/scala/RunWebApp.scala @@ -32,11 +32,12 @@ import java.lang.reflect.{Proxy => JProxy} import net.liftweb.http.LiftRules import net.liftweb.http.provider.HTTPContext import org.eclipse.jetty.server.Server -import org.eclipse.jetty.server.session.SessionHandler import org.eclipse.jetty.webapp.WebAppContext object RunWebApp extends App { val servletContextPath = "/" + //set run mode value to "development", So the value is true of Props.devMode + System.setProperty("run.mode", "development") /** * The above code is related to Chicken or the egg dilemma. diff --git a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala index c30674b8c..82a62495f 100644 --- a/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala +++ b/obp-api/src/test/scala/code/api/ResourceDocs1_4_0/ResourceDocsTest.scala @@ -1,4 +1,4 @@ -package code.api.v3_1_0 +package code.api.ResourceDocs1_4_0 import java.util diff --git a/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala index f4697a684..07fe6adfd 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/ConnectorMethodTest.scala @@ -1,6 +1,6 @@ /** Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH +Copyright (C) 2011-2021, TESOBE GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -83,11 +83,11 @@ class ConnectorMethodTest extends V400ServerSetup { connectorMethod.methodName should be (postConnectorMethod.methodName) connectorMethod.methodBody should be (postConnectorMethod.methodBody) - connectorMethod.internalConnectorId shouldNot be (null) + connectorMethod.connectorMethodId shouldNot be (null) Then(s"we test the $ApiEndpoint2") - val requestGet = (v4_0_0_Request / "management" / "connector-methods" / {connectorMethod.internalConnectorId.getOrElse("")}).GET <@ (user1) + val requestGet = (v4_0_0_Request / "management" / "connector-methods" / {connectorMethod.connectorMethodId.getOrElse("")}).GET <@ (user1) val responseGet = makeGetRequest(requestGet) @@ -98,7 +98,7 @@ class ConnectorMethodTest extends V400ServerSetup { connectorMethodJsonGet400.methodName should be (postConnectorMethod.methodName) connectorMethodJsonGet400.methodBody should be (postConnectorMethod.methodBody) - connectorMethod.internalConnectorId should be (connectorMethodJsonGet400.internalConnectorId) + connectorMethod.connectorMethodId should be (connectorMethodJsonGet400.connectorMethodId) Then(s"we test the $ApiEndpoint3") @@ -116,11 +116,11 @@ class ConnectorMethodTest extends V400ServerSetup { val connectorMethods = connectorMethodsJsonGetAll(0) (connectorMethods \ "method_name").values.toString should equal (postConnectorMethod.methodName) (connectorMethods \ "method_body").values.toString should equal (postConnectorMethod.methodBody) - (connectorMethods \ "internal_connector_id").values.toString should be (connectorMethodJsonGet400.internalConnectorId.get) + (connectorMethods \ "connector_method_id").values.toString should be (connectorMethodJsonGet400.connectorMethodId.get) Then(s"we test the $ApiEndpoint4") - val requestUpdate = (v4_0_0_Request / "management" / "connector-methods" / {connectorMethod.internalConnectorId.getOrElse("")}).PUT <@ (user1) + val requestUpdate = (v4_0_0_Request / "management" / "connector-methods" / {connectorMethod.connectorMethodId.getOrElse("")}).PUT <@ (user1) lazy val postConnectorMethodMethodBody = SwaggerDefinitionsJSON.jsonConnectorMethodMethodBody @@ -136,7 +136,7 @@ class ConnectorMethodTest extends V400ServerSetup { connectorMethodJsonGetAfterUpdated.methodBody should be (postConnectorMethodMethodBody.methodBody) connectorMethodJsonGetAfterUpdated.methodName should be (connectorMethodJsonGet400.methodName) - connectorMethodJsonGetAfterUpdated.internalConnectorId should be (connectorMethodJsonGet400.internalConnectorId) + connectorMethodJsonGetAfterUpdated.connectorMethodId should be (connectorMethodJsonGet400.connectorMethodId) } } @@ -159,7 +159,7 @@ class ConnectorMethodTest extends V400ServerSetup { connectorMethod.methodName should be (postConnectorMethod.methodName) connectorMethod.methodBody should be (postConnectorMethod.methodBody) - connectorMethod.internalConnectorId shouldNot be (null) + connectorMethod.connectorMethodId shouldNot be (null) Then(s"we test the $ApiEndpoint1 with the same methodName") diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicMessageDocTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicMessageDocTest.scala new file mode 100644 index 000000000..f398716df --- /dev/null +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicMessageDocTest.scala @@ -0,0 +1,250 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + */ +package code.api.v4_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages.{UserHasMissingRoles, DynamicMessageDocNotFound} +import code.api.util.{ApiRole} +import code.api.v4_0_0.APIMethods400.Implementations4_0_0 +import code.dynamicMessageDoc.{JsonDynamicMessageDoc} +import code.entitlement.Entitlement +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.{ErrorMessage} +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.JArray +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + + +class DynamicMessageDocTest extends V400ServerSetup { + + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createDynamicMessageDoc)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.updateDynamicMessageDoc)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getDynamicMessageDoc)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.getAllDynamicMessageDocs)) + object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.deleteDynamicMessageDoc)) + + feature("Test the DynamicMessageDoc endpoints") { + scenario("We create my DynamicMessageDoc and get,update", ApiEndpoint1,ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canCreateDynamicMessageDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canGetDynamicMessageDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canGetAllDynamicMessageDocs.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canUpdateDynamicMessageDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canDeleteDynamicMessageDoc.toString) + + val request = (v4_0_0_Request / "management" / "dynamic-message-docs").POST <@ (user1) + + lazy val postDynamicMessageDoc = SwaggerDefinitionsJSON.jsonDynamicMessageDoc.copy(dynamicMessageDocId = None) + + val postBody = write(postDynamicMessageDoc) + val response = makePostRequest(request, postBody) + Then("We should get a 201") + response.code should equal(201) + + val dynamicMessageDoc = response.body.extract[JsonDynamicMessageDoc] + + dynamicMessageDoc.dynamicMessageDocId shouldNot be (null) + dynamicMessageDoc.process should be (postDynamicMessageDoc.process) + dynamicMessageDoc.messageFormat should be (postDynamicMessageDoc.messageFormat) + dynamicMessageDoc.description should be (postDynamicMessageDoc.description) + dynamicMessageDoc.outboundTopic should be (postDynamicMessageDoc.outboundTopic) + dynamicMessageDoc.inboundTopic should be (postDynamicMessageDoc.inboundTopic) + dynamicMessageDoc.exampleOutboundMessage should be (postDynamicMessageDoc.exampleOutboundMessage) + dynamicMessageDoc.exampleInboundMessage should be (postDynamicMessageDoc.exampleInboundMessage) + dynamicMessageDoc.outboundAvroSchema should be (postDynamicMessageDoc.outboundAvroSchema) + dynamicMessageDoc.inboundAvroSchema should be (postDynamicMessageDoc.inboundAvroSchema) + dynamicMessageDoc.adapterImplementation should be (postDynamicMessageDoc.adapterImplementation) + + + Then(s"we test the $ApiEndpoint2") + val requestGet = (v4_0_0_Request / "management" / "dynamic-message-docs" / {dynamicMessageDoc.dynamicMessageDocId.getOrElse("")}).GET <@ (user1) + + + val responseGet = makeGetRequest(requestGet) + Then("We should get a 200") + responseGet.code should equal(200) + + val dynamicMessageDocJsonGet400 = responseGet.body.extract[JsonDynamicMessageDoc] + + dynamicMessageDoc.dynamicMessageDocId shouldNot be (postDynamicMessageDoc.dynamicMessageDocId) + dynamicMessageDoc.process should be (postDynamicMessageDoc.process) + dynamicMessageDoc.messageFormat should be (postDynamicMessageDoc.messageFormat) + dynamicMessageDoc.description should be (postDynamicMessageDoc.description) + dynamicMessageDoc.outboundTopic should be (postDynamicMessageDoc.outboundTopic) + dynamicMessageDoc.inboundTopic should be (postDynamicMessageDoc.inboundTopic) + dynamicMessageDoc.exampleOutboundMessage should be (postDynamicMessageDoc.exampleOutboundMessage) + dynamicMessageDoc.exampleInboundMessage should be (postDynamicMessageDoc.exampleInboundMessage) + dynamicMessageDoc.outboundAvroSchema should be (postDynamicMessageDoc.outboundAvroSchema) + dynamicMessageDoc.inboundAvroSchema should be (postDynamicMessageDoc.inboundAvroSchema) + dynamicMessageDoc.adapterImplementation should be (postDynamicMessageDoc.adapterImplementation) + + + Then(s"we test the $ApiEndpoint3") + val requestGetAll = (v4_0_0_Request / "management" / "dynamic-message-docs").GET <@ (user1) + + + val responseGetAll = makeGetRequest(requestGetAll) + Then("We should get a 200") + responseGetAll.code should equal(200) + + val dynamicMessageDocsJsonGetAll = responseGetAll.body \ "dynamic-message-docs" + + dynamicMessageDocsJsonGetAll shouldBe a [JArray] + + val dynamicMessageDocs = dynamicMessageDocsJsonGetAll(0) + + (dynamicMessageDocs \ "dynamic_message_doc_id").values.toString should equal (dynamicMessageDoc.dynamicMessageDocId.get) + (dynamicMessageDocs \ "process").values.toString should equal (postDynamicMessageDoc.process) + (dynamicMessageDocs \ "message_format").values.toString should equal (postDynamicMessageDoc.messageFormat) + (dynamicMessageDocs \ "description").values.toString should equal (postDynamicMessageDoc.description) + (dynamicMessageDocs \ "outbound_topic").values.toString should equal (postDynamicMessageDoc.outboundTopic) + (dynamicMessageDocs \ "inbound_topic").values.toString should equal (postDynamicMessageDoc.inboundTopic) + (dynamicMessageDocs \ "example_outbound_message") should equal (postDynamicMessageDoc.exampleOutboundMessage) + (dynamicMessageDocs \ "example_inbound_message") should equal (postDynamicMessageDoc.exampleInboundMessage) + (dynamicMessageDocs \ "outbound_avro_schema").values.toString should equal (postDynamicMessageDoc.outboundAvroSchema) + (dynamicMessageDocs \ "inbound_avro_schema").values.toString should equal (postDynamicMessageDoc.inboundAvroSchema) + (dynamicMessageDocs \ "adapter_implementation").values.toString should equal (postDynamicMessageDoc.adapterImplementation) + + + Then(s"we test the $ApiEndpoint4") + val requestUpdate = (v4_0_0_Request / "management" / "dynamic-message-docs" / {dynamicMessageDoc.dynamicMessageDocId.getOrElse("")}).PUT <@ (user1) + + val postDynamicMessageDocBody = SwaggerDefinitionsJSON.jsonDynamicMessageDoc.copy(process="getAccount") + + val responseUpdate = makePutRequest(requestUpdate,write(postDynamicMessageDocBody)) + Then("We should get a 200") + responseUpdate.code should equal(200) + + val responseGetAfterUpdated = makeGetRequest(requestGet) + Then("We should get a 200") + responseGetAfterUpdated.code should equal(200) + + val dynamicMessageDocJsonGetAfterUpdated = responseGetAfterUpdated.body.extract[JsonDynamicMessageDoc] + + dynamicMessageDocJsonGetAfterUpdated.process should be (postDynamicMessageDocBody.process) + + + Then(s"we test the $ApiEndpoint5") + val requestDelete = (v4_0_0_Request / "management" / "dynamic-message-docs" / {dynamicMessageDoc.dynamicMessageDocId.getOrElse("")}).DELETE <@ (user1) + + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204") + responseDelete.code should equal(204) + + val responseGetAfterDeleted = makeGetRequest(requestGet) + Then("We should get a 400") + Then("We should get a 400") + responseGetAfterDeleted.code should equal(400) + responseGetAfterDeleted.body.extract[ErrorMessage].message contains(DynamicMessageDocNotFound) should be (true) + } + } + + feature("Test the DynamicMessageDoc endpoints error cases") { +// may need it later +// scenario("We create my DynamicMessageDoc -- duplicated DynamicMessageDoc Name", ApiEndpoint1, VersionOfApi) { +// When("We make a request v4.0.0") +// +// Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canCreateDynamicMessageDoc.toString) +// +// +// val request = (v4_0_0_Request / "management" / "dynamic-message-docs").POST <@ (user1) +// +// lazy val postDynamicMessageDoc = SwaggerDefinitionsJSON.jsonDynamicMessageDoc +// +// val response = makePostRequest(request, write(postDynamicMessageDoc)) +// Then("We should get a 201") +// response.code should equal(201) +// +// val dynamicMessageDoc = response.body.extract[JsonDynamicMessageDoc] +// +// Then(s"we test the $ApiEndpoint1 with the same methodName") +// +// val response2 = makePostRequest(request, write(postDynamicMessageDoc)) +// Then("We should get a 400") +// response2.code should equal(400) +// response2.body.extract[ErrorMessage].message contains(DynamicMessageDocAlreadyExists) should be (true) +// } + + scenario("We create/get/getAll/update my DynamicMessageDoc without our proper roles", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + + val request = (v4_0_0_Request / "management" / "dynamic-message-docs").POST <@ (user1) + lazy val postDynamicMessageDoc = SwaggerDefinitionsJSON.jsonDynamicMessageDoc + val response = makePostRequest(request, write(postDynamicMessageDoc)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanCreateDynamicMessageDoc}") + + Then(s"we test the $ApiEndpoint2") + val requestGet = (v4_0_0_Request / "management" / "dynamic-message-docs" / "xx").GET <@ (user1) + + + val responseGet = makeGetRequest(requestGet) + Then("We should get a 403") + responseGet.code should equal(403) + responseGet.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanGetDynamicMessageDoc}") + + + Then(s"we test the $ApiEndpoint3") + val requestGetAll = (v4_0_0_Request / "management" / "dynamic-message-docs").GET <@ (user1) + + val responseGetAll = makeGetRequest(requestGetAll) + responseGetAll.code should equal(403) + responseGetAll.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanGetAllDynamicMessageDocs}") + + + Then(s"we test the $ApiEndpoint4") + + val requestUpdate = (v4_0_0_Request / "management" / "dynamic-message-docs" / "xx").PUT <@ (user1) + val responseUpdate = makePutRequest(requestUpdate,write(postDynamicMessageDoc)) + + responseUpdate.code should equal(403) + responseUpdate.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanUpdateDynamicMessageDoc}") + + Then(s"we test the $ApiEndpoint5") + + val requestDelete = (v4_0_0_Request / "management" / "dynamic-message-docs" / "xx").DELETE <@ (user1) + val responseDelete = makeDeleteRequest(requestDelete) + + responseDelete.code should equal(403) + responseDelete.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanDeleteDynamicMessageDoc}") + } + } + + +} diff --git a/obp-api/src/test/scala/code/api/v4_0_0/DynamicResourceDocTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/DynamicResourceDocTest.scala new file mode 100644 index 000000000..e16fbe89e --- /dev/null +++ b/obp-api/src/test/scala/code/api/v4_0_0/DynamicResourceDocTest.scala @@ -0,0 +1,248 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH +Osloerstrasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + */ +package code.api.v4_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages.{DynamicResourceDocAlreadyExists, DynamicResourceDocNotFound, UserHasMissingRoles} +import code.api.util.ApiRole +import code.api.v4_0_0.APIMethods400.Implementations4_0_0 +import code.dynamicResourceDoc.JsonDynamicResourceDoc +import code.entitlement.Entitlement +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.ErrorMessage +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json +import net.liftweb.json.JArray +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + + +class DynamicResourceDocTest extends V400ServerSetup { + + /** + * Test tags + * Example: To run tests with tag "getPermissions": + * mvn test -D tagsToInclude + * + * This is made possible by the scalatest maven plugin + */ + object VersionOfApi extends Tag(ApiVersion.v4_0_0.toString) + object ApiEndpoint1 extends Tag(nameOf(Implementations4_0_0.createDynamicResourceDoc)) + object ApiEndpoint2 extends Tag(nameOf(Implementations4_0_0.updateDynamicResourceDoc)) + object ApiEndpoint3 extends Tag(nameOf(Implementations4_0_0.getDynamicResourceDoc)) + object ApiEndpoint4 extends Tag(nameOf(Implementations4_0_0.getAllDynamicResourceDocs)) + object ApiEndpoint5 extends Tag(nameOf(Implementations4_0_0.deleteDynamicResourceDoc)) + + feature("Test the DynamicResourceDoc endpoints") { + scenario("We create my DynamicResourceDoc and get,update", ApiEndpoint1,ApiEndpoint2, ApiEndpoint3, ApiEndpoint4, VersionOfApi) { + When("We make a request v4.0.0") + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canCreateDynamicResourceDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canGetDynamicResourceDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canGetAllDynamicResourceDocs.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canUpdateDynamicResourceDoc.toString) + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canDeleteDynamicResourceDoc.toString) + + val request = (v4_0_0_Request / "management" / "dynamic-resource-docs").POST <@ (user1) + + lazy val postDynamicResourceDoc = SwaggerDefinitionsJSON.jsonDynamicResourceDoc.copy(dynamicResourceDocId = None) + + val response = makePostRequest(request, write(postDynamicResourceDoc)) + Then("We should get a 201") + response.code should equal(201) + + val dynamicResourceDoc = response.body.extract[JsonDynamicResourceDoc] + + dynamicResourceDoc.dynamicResourceDocId shouldNot be (null) + dynamicResourceDoc.methodBody should be (postDynamicResourceDoc.methodBody) + dynamicResourceDoc.partialFunctionName should be (postDynamicResourceDoc.partialFunctionName) + dynamicResourceDoc.requestVerb should be (postDynamicResourceDoc.requestVerb) + dynamicResourceDoc.requestUrl should be (postDynamicResourceDoc.requestUrl) + dynamicResourceDoc.summary should be (postDynamicResourceDoc.summary) + dynamicResourceDoc.description should be (postDynamicResourceDoc.description) + dynamicResourceDoc.errorResponseBodies should be (postDynamicResourceDoc.errorResponseBodies) + dynamicResourceDoc.tags should be (postDynamicResourceDoc.tags) + + dynamicResourceDoc.exampleRequestBody should be(postDynamicResourceDoc.exampleRequestBody) + dynamicResourceDoc.successResponseBody should be(postDynamicResourceDoc.successResponseBody) + + Then(s"we test the $ApiEndpoint2") + val requestGet = (v4_0_0_Request / "management" / "dynamic-resource-docs" / {dynamicResourceDoc.dynamicResourceDocId.getOrElse("")}).GET <@ (user1) + + + val responseGet = makeGetRequest(requestGet) + Then("We should get a 200") + responseGet.code should equal(200) + + val dynamicResourceDocJsonGet400 = responseGet.body.extract[JsonDynamicResourceDoc] + + dynamicResourceDoc.dynamicResourceDocId shouldNot be (postDynamicResourceDoc.dynamicResourceDocId) + dynamicResourceDoc.methodBody should be (postDynamicResourceDoc.methodBody) + dynamicResourceDoc.partialFunctionName should be (postDynamicResourceDoc.partialFunctionName) + dynamicResourceDoc.requestVerb should be (postDynamicResourceDoc.requestVerb) + dynamicResourceDoc.requestUrl should be (postDynamicResourceDoc.requestUrl) + dynamicResourceDoc.summary should be (postDynamicResourceDoc.summary) + dynamicResourceDoc.description should be (postDynamicResourceDoc.description) + dynamicResourceDoc.errorResponseBodies should be (postDynamicResourceDoc.errorResponseBodies) + dynamicResourceDoc.tags should be (postDynamicResourceDoc.tags) + + dynamicResourceDoc.exampleRequestBody should be(postDynamicResourceDoc.exampleRequestBody) + dynamicResourceDoc.successResponseBody should be(postDynamicResourceDoc.successResponseBody) + + Then(s"we test the $ApiEndpoint3") + val requestGetAll = (v4_0_0_Request / "management" / "dynamic-resource-docs").GET <@ (user1) + + + val responseGetAll = makeGetRequest(requestGetAll) + Then("We should get a 200") + responseGetAll.code should equal(200) + + val dynamicResourceDocsJsonGetAll = responseGetAll.body \ "dynamic-resource-docs" + + dynamicResourceDocsJsonGetAll shouldBe a [JArray] + + val dynamicResourceDocs = dynamicResourceDocsJsonGetAll(0) + + (dynamicResourceDocs \ "dynamic_resource_doc_id").values.toString should equal (dynamicResourceDoc.dynamicResourceDocId.get) + (dynamicResourceDocs \ "partial_function_name").values.toString should equal (postDynamicResourceDoc.partialFunctionName) + (dynamicResourceDocs \ "request_verb").values.toString should equal (postDynamicResourceDoc.requestVerb) + (dynamicResourceDocs \ "request_url").values.toString should equal (postDynamicResourceDoc.requestUrl) + (dynamicResourceDocs \ "summary").values.toString should equal (postDynamicResourceDoc.summary) + (dynamicResourceDocs \ "description").values.toString should equal (postDynamicResourceDoc.description) + (dynamicResourceDocs \ "example_request_body") should equal (postDynamicResourceDoc.exampleRequestBody.orNull) + (dynamicResourceDocs \ "success_response_body") should equal (postDynamicResourceDoc.successResponseBody.orNull) + (dynamicResourceDocs \ "error_response_bodies").values.toString should equal (postDynamicResourceDoc.errorResponseBodies) + (dynamicResourceDocs \ "tags").values.toString should equal (postDynamicResourceDoc.tags) + (dynamicResourceDocs \ "method_body").values.toString should equal (postDynamicResourceDoc.methodBody) + + + Then(s"we test the $ApiEndpoint4") + val requestUpdate = (v4_0_0_Request / "management" / "dynamic-resource-docs" / {dynamicResourceDoc.dynamicResourceDocId.getOrElse("")}).PUT <@ (user1) + + val postDynamicResourceDocBody = SwaggerDefinitionsJSON.jsonDynamicResourceDoc.copy(partialFunctionName="getAccount") + + val responseUpdate = makePutRequest(requestUpdate,write(postDynamicResourceDocBody)) + Then("We should get a 200") + responseUpdate.code should equal(200) + + val responseGetAfterUpdated = makeGetRequest(requestGet) + Then("We should get a 200") + responseGetAfterUpdated.code should equal(200) + + val dynamicResourceDocJsonGetAfterUpdated = responseGetAfterUpdated.body.extract[JsonDynamicResourceDoc] + + dynamicResourceDocJsonGetAfterUpdated.partialFunctionName should be (postDynamicResourceDocBody.partialFunctionName) + + + Then(s"we test the $ApiEndpoint5") + val requestDelete = (v4_0_0_Request / "management" / "dynamic-resource-docs" / {dynamicResourceDoc.dynamicResourceDocId.getOrElse("")}).DELETE <@ (user1) + + val responseDelete = makeDeleteRequest(requestDelete) + Then("We should get a 204") + responseDelete.code should equal(204) + + val responseGetAfterDeleted = makeGetRequest(requestGet) + Then("We should get a 400") + Then("We should get a 400") + responseGetAfterDeleted.code should equal(400) + responseGetAfterDeleted.body.extract[ErrorMessage].message contains(DynamicResourceDocNotFound) should be (true) + } + } + + feature("Test the DynamicResourceDoc endpoints error cases") { + scenario("We create my DynamicResourceDoc -- duplicated DynamicResourceDoc Name", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canCreateDynamicResourceDoc.toString) + + + val request = (v4_0_0_Request / "management" / "dynamic-resource-docs").POST <@ (user1) + + lazy val postDynamicResourceDoc = SwaggerDefinitionsJSON.jsonDynamicResourceDoc + + val response = makePostRequest(request, write(postDynamicResourceDoc)) + Then("We should get a 201") + response.code should equal(201) + + Then(s"we test the $ApiEndpoint1 with the same methodName") + + val response2 = makePostRequest(request, write(postDynamicResourceDoc)) + Then("We should get a 400") + response2.code should equal(400) + response2.body.extract[ErrorMessage].message contains(DynamicResourceDocAlreadyExists) should be (true) + + } + + scenario("We create/get/getAll/update my DynamicResourceDoc without our proper roles", ApiEndpoint1, VersionOfApi) { + When("We make a request v4.0.0") + + val request = (v4_0_0_Request / "management" / "dynamic-resource-docs").POST <@ (user1) + lazy val postDynamicResourceDoc = SwaggerDefinitionsJSON.jsonDynamicResourceDoc + val response = makePostRequest(request, write(postDynamicResourceDoc)) + Then("We should get a 403") + response.code should equal(403) + response.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanCreateDynamicResourceDoc}") + + Then(s"we test the $ApiEndpoint2") + val requestGet = (v4_0_0_Request / "management" / "dynamic-resource-docs" / "xx").GET <@ (user1) + + + val responseGet = makeGetRequest(requestGet) + Then("We should get a 403") + responseGet.code should equal(403) + responseGet.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanGetDynamicResourceDoc}") + + + Then(s"we test the $ApiEndpoint3") + val requestGetAll = (v4_0_0_Request / "management" / "dynamic-resource-docs").GET <@ (user1) + + val responseGetAll = makeGetRequest(requestGetAll) + responseGetAll.code should equal(403) + responseGetAll.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanGetAllDynamicResourceDocs}") + + + Then(s"we test the $ApiEndpoint4") + + val requestUpdate = (v4_0_0_Request / "management" / "dynamic-resource-docs" / "xx").PUT <@ (user1) + val responseUpdate = makePutRequest(requestUpdate,write(postDynamicResourceDoc)) + + responseUpdate.code should equal(403) + responseUpdate.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanUpdateDynamicResourceDoc}") + + Then(s"we test the $ApiEndpoint5") + + val requestDelete = (v4_0_0_Request / "management" / "dynamic-resource-docs" / "xx").DELETE <@ (user1) + val responseDelete = makeDeleteRequest(requestDelete) + + responseDelete.code should equal(403) + responseDelete.body.extract[ErrorMessage].message should equal(s"$UserHasMissingRoles${CanDeleteDynamicResourceDoc}") + } + } + + +} diff --git a/obp-api/src/test/scala/code/util/APIUtilTest.scala b/obp-api/src/test/scala/code/util/APIUtilTest.scala index d92df992b..a3666cc5d 100644 --- a/obp-api/src/test/scala/code/util/APIUtilTest.scala +++ b/obp-api/src/test/scala/code/util/APIUtilTest.scala @@ -204,7 +204,17 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop returnValue should be (Full(OBPDescending)) } } - + + implicit val fromDateOrdering = new Ordering[OBPFromDate] { + override def compare(x: OBPFromDate, y: OBPFromDate): Int = if (x.value.after(y.value)) { + 1 + } else if(y.value.after(x.value)) { + -1 + } else { + 0 + } + } + feature("test APIUtil.getFromDate method") { scenario(s"test the correct case") @@ -222,21 +232,41 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop returnValue.toString contains FilterDateFormatError should be (true) } - scenario(s"test the wrong case: wrong name (wrongName) in HTTPParam") + scenario("test the wrong case: wrong name (wrongName) in HTTPParam") { val httpParams: List[HTTPParam] = List(HTTPParam("wrongName", List(s"$DateWithMsExampleString"))) + val startTime = OBPFromDate(DefaultFromDate) val returnValue = getFromDate(httpParams) - returnValue should be (OBPFromDate(DefaultFromDate)) + returnValue shouldBe a[Full[OBPFromDate]] + + val currentTime = OBPFromDate(DefaultFromDate) + val beWithinTolerance = be >= startTime and be <= currentTime + returnValue.orNull should beWithinTolerance } - scenario(s"test the wrong case: wrong name (wrongName) and wrong values (wrongValue) in HTTPParam") + scenario("test the wrong case: wrong name (wrongName) and wrong values (wrongValue) in HTTPParam") { val httpParams: List[HTTPParam] = List(HTTPParam("wrongName", List("wrongValue"))) + val startTime = OBPFromDate(DefaultFromDate) val returnValue = getFromDate(httpParams) - returnValue should be (OBPFromDate(DefaultFromDate)) + returnValue shouldBe a[Full[OBPFromDate]] + + val currentTime = OBPFromDate(DefaultFromDate) + val beWithinTolerance = be >= startTime and be <= currentTime + returnValue.orNull should beWithinTolerance } } - + + implicit val toDateOrdering = new Ordering[OBPToDate] { + override def compare(x: OBPToDate, y: OBPToDate): Int = if (x.value.after(y.value)) { + 1 + } else if(y.value.after(x.value)) { + -1 + } else { + 0 + } + } + feature("test APIUtil.getToDate method") { scenario(s"test the correct case") @@ -257,15 +287,29 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop scenario(s"test the wrong case: wrong name (wrongName) in HTTPParam") { val httpParams: List[HTTPParam] = List(HTTPParam("wrongName", List(s"$DateWithMsExampleString"))) + + val startTime = OBPToDate(DefaultToDate) + val returnValue = getToDate(httpParams) - returnValue should be (OBPToDate(DefaultToDate)) + returnValue shouldBe a[Full[OBPToDate]] + + val currentTime = OBPToDate(DefaultToDate) + val beWithinTolerance = be >= startTime and be <= currentTime + returnValue.orNull should beWithinTolerance } scenario(s"test the wrong case: wrong name (wrongName) and wrong values (wrongValue) in HTTPParam") { val httpParams: List[HTTPParam] = List(HTTPParam("wrongName", List("wrongValue"))) + + val startTime = OBPToDate(DefaultToDate) + val returnValue = getToDate(httpParams) - returnValue should be (OBPToDate(DefaultToDate)) + returnValue shouldBe a[Full[OBPToDate]] + + val currentTime = OBPToDate(DefaultToDate) + val beWithinTolerance = be >= startTime and be <= currentTime + returnValue.orNull should beWithinTolerance } } diff --git a/obp-api/src/test/scala/code/util/DynamicUtilTest.scala b/obp-api/src/test/scala/code/util/DynamicUtilTest.scala new file mode 100644 index 000000000..c9673d1f2 --- /dev/null +++ b/obp-api/src/test/scala/code/util/DynamicUtilTest.scala @@ -0,0 +1,119 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2019, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + + */ + +package code.util + +import code.api.util._ +import code.setup.PropsReset +import com.openbankproject.commons.util.{JsonUtils, ReflectUtils} +import net.liftweb.common.Box +import net.liftweb.json +import org.scalatest.{FeatureSpec, FlatSpec, GivenWhenThen, Matchers, Tag} + +class DynamicUtilTest extends FlatSpec with Matchers { + object DynamicUtilsTag extends Tag("DynamicUtil") + + implicit val formats = code.api.util.CustomJsonFormats.formats + + + "DynamicUtil.compileScalaCode method" should "return correct function" taggedAs DynamicUtilsTag in { + val functionBody: Box[Int => Int] = DynamicUtil.compileScalaCode("def getBank(bankId : Int): Int = bankId+2; getBank _") + + val getBankResponse = functionBody.openOrThrowException("")(123) + + getBankResponse should be (125) + } + + + val zson = { + """ + |{ + | "name": "Sam", + | "age": [12], + | "isMarried": true, + | "weight": 12.11, + | "class": "2", + | "def": 12, + | "email": ["abc@def.com", "hijk@abc.com"], + | "address": [{ + | "name": "jieji", + | "code": 123123, + | "street":{"road": "gongbin", "number": 123}, + | "_optional_fields_": ["code"] + | }], + | "street": {"name": "hongqi", "width": 12.11}, + | "_optional_fields_": ["age", "weight", "address"] + |} + |""".stripMargin + } + val zson2 = """{"road": "gongbin", "number": 123}""" + val zson3 = """[{"road": "gongbin", "number": 123}]""" + + def buildFunction(jsonStr: String): String => Any = { + val caseClasses = JsonUtils.toCaseClasses(json.parse(jsonStr)) + + val code = + s""" + | $caseClasses + | + | // throws exception: net.liftweb.json.MappingException: + | //No usable value for name + | //Did not find value which can be converted into java.lang.String + | + |implicit val formats = code.api.util.CustomJsonFormats.formats + |(str: String) => { + | net.liftweb.json.parse(str).extract[RootJsonClass] + |} + |""".stripMargin + + val fun: Box[String => Any] = DynamicUtil.compileScalaCode(code) + fun.orNull + } + + "Parse json to dynamic case object" should "success" taggedAs DynamicUtilsTag in { + + val func = buildFunction(zson) + val func2 = buildFunction(zson2) + val func3 = buildFunction(zson3) + val value1 = func.apply(zson) + val value2 = func2.apply(zson2) + val value3 = func2.apply(zson3) + + + ReflectUtils.getNestedField(value1.asInstanceOf[AnyRef], "street", "name") should be ("hongqi") + ReflectUtils.getField(value1.asInstanceOf[AnyRef], "weight") shouldEqual Some(12.11) + ReflectUtils.getField(value2.asInstanceOf[AnyRef], "number") shouldEqual (123) + ReflectUtils.getField(value3.asInstanceOf[AnyRef], "number") shouldEqual (123) + } + + "DynamicUtil.toCaseObject method" should "return correct object" taggedAs DynamicUtilsTag in { + + val jValueZson2 = json.parse(zson2) + val zson2Object: Product = DynamicUtil.toCaseObject(jValueZson2) + zson2Object.isInstanceOf[Product] should be (true) + } +} \ No newline at end of file diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index 18ddce6c1..ac08617c6 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -7,7 +7,7 @@ com.tesobe obp-parent ../pom.xml - 1.8.2 + 1.9.0 obp-commons jar diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonSerializers.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonSerializers.scala index 169fe3c8b..e47056df2 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonSerializers.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonSerializers.scala @@ -40,9 +40,15 @@ object JsonAble { def unapply(jsonAble: JsonAble)(implicit format: Formats): Option[JValue] = Option(jsonAble).map(_.toJValue) } -object JsonAbleSerializer extends Serializer[JsonAble] { +trait ObpSerializer[T] extends Serializer[T] { + override final def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), T] = Functions.doNothing +} - override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), JsonAble] = Functions.doNothing +trait ObpDeSerializer[T] extends Serializer[T] { + override final def serialize(implicit format: Formats): PartialFunction[Any, json.JValue] = Functions.doNothing +} + +object JsonAbleSerializer extends ObpSerializer[JsonAble] { override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { case JsonAble(jValue) => jValue @@ -70,7 +76,7 @@ object EnumValueSerializer extends Serializer[EnumValue] { * deSerialize trait or abstract type json, this Serializer should always put at formats chain first, e.g: * DefaultFormats + AbstractTypeDeserializer + ...others */ -object AbstractTypeDeserializer extends Serializer[AnyRef] { +object AbstractTypeDeserializer extends ObpDeSerializer[AnyRef] { override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), AnyRef] = { case (TypeInfo(clazz, _), json) if Modifier.isAbstract(clazz.getModifiers) && ReflectUtils.isObpClass(clazz) => @@ -79,12 +85,9 @@ object AbstractTypeDeserializer extends Serializer[AnyRef] { implicit val manifest = ManifestFactory.classType[AnyRef](commonClass) json.extract[AnyRef](format, manifest) } - - override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = Functions.doNothing - } -object SimpleEnumDeserializer extends Serializer[SimpleEnum] { +object SimpleEnumDeserializer extends ObpDeSerializer[SimpleEnum] { private val simpleEnumClazz = classOf[SimpleEnum] override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), SimpleEnum] = { case (TypeInfo(clazz, _), json) if simpleEnumClazz.isAssignableFrom(clazz) => @@ -94,8 +97,6 @@ object SimpleEnumDeserializer extends Serializer[SimpleEnum] { .asInstanceOf[SimpleEnumCollection[SimpleEnum]] .valueOf(enumValue) } - - override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = Functions.doNothing } object BigDecimalSerializer extends Serializer[BigDecimal] { @@ -113,15 +114,13 @@ object BigDecimalSerializer extends Serializer[BigDecimal] { } } -object StringDeserializer extends Serializer[String] { +object StringDeserializer extends ObpDeSerializer[String] { private val IntervalClass = classOf[String] override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), String] = { case (TypeInfo(IntervalClass, _), json) if !json.isInstanceOf[JString] => compactRender(json) } - - override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = Functions.doNothing } /** @@ -255,7 +254,7 @@ object FiledRenameSerializer extends Serializer[JsonFieldReName] { /** * make tolerate for missing required constructor parameters */ -object JNothingSerializer extends Serializer[Any] { +object JNothingSerializer extends ObpDeSerializer[Any] { // This field is just a tag to declare all the missing fields are added, to avoid check missing field repeatedly val addedMissingFields = "addedMissingFieldsThisFieldIsJustBeTag" @@ -289,9 +288,6 @@ object JNothingSerializer extends Serializer[Any] { } } - override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = Functions.doNothing - - private[this] def unapply(arg: (TypeInfo, JValue))(implicit formats: Formats): Option[(TypeInfo, JValue, Map[String, Class[_]])] = { val (TypeInfo(clazz, _), jValue) = arg if (! ReflectUtils.isObpClass(clazz) || !jValue.isInstanceOf[JObject] || jValue == JNothing || jValue == JNull || isNoMissingFields(jValue)) { @@ -413,7 +409,7 @@ object ListResultSerializer extends Serializer[ListResult[_]] { /** * serialize DB Mapped object to JValue */ -object MapperSerializer extends Serializer[Mapper[_]] { +object MapperSerializer extends ObpSerializer[Mapper[_]] { /** * `call by name` method names those defined in Mapper trait. */ @@ -437,9 +433,6 @@ object MapperSerializer extends Serializer[Mapper[_]] { }).toMap json.Extraction.decompose(map) } - - - override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, json.JValue), Mapper[_]] = Functions.doNothing } @scala.annotation.meta.field diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala index f03a60d16..38b4a3074 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala @@ -1,14 +1,14 @@ package com.openbankproject.commons.util -import java.util.{Date, Objects} - +import com.openbankproject.commons.util.Functions.Implicits._ import net.liftweb.json import net.liftweb.json.JsonAST._ import net.liftweb.json.JsonDSL._ import net.liftweb.json.JsonParser.ParseException -import net.liftweb.json.{Diff, JNothing, JNull} -import org.apache.commons.lang3.StringUtils +import net.liftweb.json.{Diff, JDouble, JInt, JNothing, JNull, JString} +import org.apache.commons.lang3.{StringUtils, Validate} +import java.util.Objects import scala.reflect.runtime.universe import scala.reflect.runtime.universe.typeOf @@ -247,6 +247,43 @@ object JsonUtils { if (f.isDefinedAt(field, path)) f(field, path) else field } + /** + * peek all nested fields, call callback function for every field + * @param jValue + * @param f + * @return Unit + */ + def peekField(jValue: JValue)(f: (JField, String) => Unit): Unit = { + def buildPath(parentPath: String, currentFieldName: String): String = + if(parentPath == "") currentFieldName else s"$parentPath.$currentFieldName" + + def rec(v: JValue, path: String): Unit = v match { + case JObject(l) => l.foreach { field => + rec(field.value, buildPath(path, field.name)) + f(field, path) + } + + case JArray(l) => l.foreach(rec(_, path)) + case v => v // do nothing, just placeholder + } + rec(jValue, "") + } + + /** + * according predicate function to collect all fulfill fields + * @param jValue + * @param predicate + * @return JField to field path + */ + def collectField(jValue: JValue)(predicate: (JField, String) => Boolean): List[(JField, String)] = { + val fields = scala.collection.mutable.ListBuffer[(JField, String)]() + peekField(jValue) { (field, path) => + if(predicate(field, path)) + fields += field -> path + } + fields.toList + } + /** * enhance JValue, to support operations: !,+,-*,/,&,| * @param jValue @@ -612,5 +649,209 @@ object JsonUtils { buffer -= "$outer" // removed the nest class references buffer.toMap } - + + + /** + * is jValue type of JBool|JString|JDouble|JInt + * @param jValue + * @return true if jValue is type of JBool|JString|JDouble|JInt + */ + def isBasicType(jValue: JValue) = jValue match { + case JBool(_) | JString(_) | JDouble(_) | JInt(_) => true + case _ => false + } + + // scala reserved word mapping escaped string: "class" -> "`class`" + private val reservedToEscaped = + List("abstract", "case", "catch", "class", "def", "do", "else", "extends", "false", + "final", "finally", "for", "forSome", "if", "implicit", "import", "lazy", "match", + "new", "null", "object", "override", "package", "private", "protected", "return", + "sealed", "super", "this", "throw", "trait", "try", "true", "type", "val", "var", + "while", "with", "yield") + .toMapByValue(it => s"`$it`") + + private val optionalFieldName = "_optional_fields_" + + // if Option[Boolean], Option[Double], Option[Long] will lost type argument when do deserialize with lift-json: Option[Object] + // this will make generated scala code can't extract json to this object, So use java.lang.Xxx for these type in Option. + private def toScalaTypeName(jValue: JValue, isOptional: Boolean = false) = jValue match { + case _: JBool if isOptional => "Option[java.lang.Boolean]" + case _: JBool => "Boolean" + case _: JDouble if isOptional => "Option[java.lang.Double]" + case _: JDouble => "Double" + case _: JInt if isOptional => "Option[java.lang.Long]" + case _: JInt => "Long" + case _: JString if isOptional => "Option[String]" + case _: JString => "String" + case _: JObject if isOptional => "Option[AnyRef]" + case _: JObject => "AnyRef" + case _: JArray if isOptional => "Option[List[Any]]" + case _: JArray => "List[Any]" + case null | JNull | JNothing => throw new IllegalArgumentException(s"Json value must not be null") + } + + /** + * validate any nested array type field have the same structure, and not empty, and have not null item + * @param void + */ + private def validateJArray(jvalue: JValue): Unit = { + if(jvalue.isInstanceOf[JArray]) { + val rootJson: JObject = "" -> jvalue + validateJArray(rootJson) + } else { + peekField(jvalue) { + case (jField @ JField(fieldName, JArray(arr)), path) => + val fullFieldName = if(StringUtils.isBlank(path)) fieldName else s"$path.$fieldName" + // not empty and none item is null + Validate.isTrue(arr.nonEmpty, s"Json $fullFieldName should not be empty array.") + Validate.isTrue(arr.notExists(it => it == JNull || it == JNothing), s" Array json $fullFieldName should not contains null item.") + if(arr.size > 1) { + arr match { + case JBool(_) :: tail => Validate.isTrue(tail.forall(_.isInstanceOf[JBool]), s"All the items of Json $fullFieldName should be Boolean type.") + case JString(_) :: tail => Validate.isTrue(tail.forall(_.isInstanceOf[JString]), s"All the items of Json $fullFieldName should be String type.") + case JDouble(_) :: tail => Validate.isTrue(tail.forall(_.isInstanceOf[JDouble]), s"All the items of Json $fullFieldName should be number type.") + case JInt(_) :: tail => Validate.isTrue(tail.forall(_.isInstanceOf[JInt]), s"All the items of Json $fullFieldName should be integer type.") + case (head: JObject) :: tail => + Validate.isTrue(tail.forall(_.isInstanceOf[JObject]), s"All the items of Json $fullFieldName should be object type.") + def fieldNameToType(jObject: JObject) = jObject.obj.map(it => it.name -> getType(it.value)).toMap + val headFieldNameToType = fieldNameToType(head) + val allItemsHaveSameStructure = tail.map(it => fieldNameToType(it.asInstanceOf[JObject])).forall(headFieldNameToType ==) + Validate.isTrue(allItemsHaveSameStructure, s"All the items of Json $fullFieldName should the same structure.") + case JArray(_) :: tail => Validate.isTrue(tail.forall(_.isInstanceOf[JArray]), s"All the items of Json $fullFieldName should be array type.") + } + } + case v => v // do nothing, just as place holder + } + } + } + + private def getNestedJObjects(jObject: JObject, typeNamePrefix: String): List[String] = { + val nestedObjects = collectField(jObject) { + case (JField(_, _: JObject), _) => true + case (JField(_, JArray((_: JObject) :: _)), _) => true + case (JField(_, JArray((_: JArray) :: _)), _) => true + case _ => false + } + + def getParentFiledName(path: String) = path match { + case v if v.contains('.') => + StringUtils.substringAfterLast(path, ".") + case v => v + } + + val subTypes: List[String] = nestedObjects collect { + case (JField(name, v: JObject), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(name, JArray((v: JObject) :: _)), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(name, JArray(JArray((v: JObject) :: _) :: _)), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(name, JArray(JArray(JArray((v: JObject) :: _) :: _) :: _)), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(name, JArray(JArray(JArray(JArray((v: JObject) :: _) :: _) :: _) :: _)), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(name, JArray(JArray(JArray(JArray(JArray((v: JObject) :: _) :: _) :: _) :: _) :: _)), path) => + jObjectToCaseClass(v, typeNamePrefix, name, getParentFiledName(path)) + case (JField(_, JArray(JArray(JArray(JArray(JArray(JArray(_ :: _) :: _) :: _) :: _) :: _) :: _)), path) => + throw new IllegalArgumentException(s"Json field $path have too much nested level, max nested level be supported is 5.") + } toList + + subTypes + } + + /** + * generate case class string according json structure + * @param jvalue + * @return case class string + */ + def toCaseClasses(jvalue: JValue, typeNamePrefix: String = ""): String = { + validateJArray(jvalue) + jvalue match { + case _: JBool => s"type ${typeNamePrefix}RootJsonClass = Boolean" + case _: JString => s"type ${typeNamePrefix}RootJsonClass = String" + case _: JDouble => s"type ${typeNamePrefix}RootJsonClass = Double" + case _: JInt => s"type ${typeNamePrefix}RootJsonClass = Long" + case jObject: JObject => + validateJArray(jObject) + val allDefinitions = getNestedJObjects(jObject, typeNamePrefix) :+ jObjectToCaseClass(jObject, typeNamePrefix) + allDefinitions mkString "\n" + + case jArray: JArray => + validateJArray(jvalue) + + def buildArrayType(jArray: JArray):String = jArray.arr.head match { + case _: JBool => "List[Boolean]" + case _: JString => "List[String]" + case _: JDouble => "List[Double]" + case _: JInt => "List[Long]" + case v: JObject => + val itemsType = jObjectToCaseClass(v, typeNamePrefix, "RootItem") + s"""$itemsType + |List[${typeNamePrefix}RootItemJsonClass] + |""".stripMargin + case v: JArray => + val nestedItmType = buildArrayType(v) + // replace the last row to List[Xxx], e.g: + /* + case class Foo(i:Int) + Foo + --> + case class Foo(i:Int) + List[Foo] + */ + nestedItmType.replaceAll("(^|.*\\s+)(.+)\\s*$", "$1List[$2]") + } + // add type alias for last row + buildArrayType(jArray).replaceAll("(^|.*\\s+)(.+)\\s*$", s"$$1 type ${typeNamePrefix}RootJsonClass = $$2") + + case null | JNull | JNothing => throw new IllegalArgumentException(s"null value json can't generate case class") + } + + } + + + private def jObjectToCaseClass(jObject: JObject, typeNamePrefix: String, fieldName: String = "", parentFieldName: String = ""): String = { + val JObject(fields) = jObject + val optionalFields = (jObject \ optionalFieldName) match { + case JArray(arr) if arr.forall(_.isInstanceOf[JString]) => + arr.map(_.asInstanceOf[JString].s).toSet + case JNull | JNothing => Set.empty[String] + case _ => throw new IllegalArgumentException(s"Filed $optionalFieldName of $fieldName should be an array of String") + } + + def toCaseClassName(name: String) = s"$typeNamePrefix${fieldName.capitalize}${name.capitalize}JsonClass" + + val currentCaseClass: String = fields collect { + case JField(name, v) if isBasicType(v) => + val escapedFieldsName = reservedToEscaped.getOrElse(name, name) + val fieldType = toScalaTypeName(v, optionalFields.contains(name)) + s"$escapedFieldsName: $fieldType" + + case JField(name, _: JObject) => + val escapedFieldsName = reservedToEscaped.getOrElse(name, name) + val fieldType = if (optionalFields.contains(name)) s"Option[${toCaseClassName(name)}]" else toCaseClassName(name) + s"$escapedFieldsName: $fieldType" + + case JField(name, arr: JArray) if name != optionalFieldName => + val isOption: Boolean = optionalFields.contains(name) + def buildArrayType(jArray: JArray): String = jArray.arr.head match { + case _: JBool => "List[java.lang.Boolean]" + case _: JDouble => "List[java.lang.Double]" + case _: JInt => "List[java.lang.Long]" + case _: JString => "List[String]" + case _: JObject => s"List[${toCaseClassName(name)}]" + + case v: JArray => + val nestedItmType = buildArrayType(v) + s"List[$nestedItmType]" + } + + val fieldType = if (isOption) s"Option[${buildArrayType(arr)}]" else buildArrayType(arr) + + val escapedFieldsName = reservedToEscaped.getOrElse(name, name) + s"$escapedFieldsName: $fieldType" + } mkString(s"case class $typeNamePrefix${parentFieldName.capitalize}${if(fieldName.isEmpty) "Root" else fieldName.capitalize}JsonClass(", ", ", ")") + + currentCaseClass + } } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala index 32878b473..bda7df656 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala @@ -113,6 +113,20 @@ object ReflectUtils { * @return the field value of obj */ def getField(obj: AnyRef, fieldName: String): Any = operateField[Any](obj, fieldName)(Functions.doNothingFn) + /** + * get given object nested field value + * @param obj + * @param fieldName field name + * @return the field value of obj + */ + def getNestedField(obj: AnyRef, rootField: String, nestedFields: String*): Any = { + nestedFields.foldLeft(getField(obj, rootField)) { (parentObject, field) => + assert(parentObject != null, s"Can't read `$field` value from null.") + assert(parentObject.isInstanceOf[AnyRef], s"Value $parentObject must be AnyRef type.") + + getField(parentObject.asInstanceOf[AnyRef], field) + } + } /** * according object name get corresponding field value @@ -618,12 +632,14 @@ object ReflectUtils { tp.typeSymbol.isClass && !tp.typeSymbol.asClass.isTrait match { case false => Map.empty[String, ru.Type] case true => { - getPrimaryConstructor(tp) + import scala.collection.immutable.ListMap + val paramNameToTypeList = getPrimaryConstructor(tp) .paramLists .headOption .getOrElse(Nil) .map(it => (it.name.toString, it.info)) - .toMap + + ListMap(paramNameToTypeList:_*) } } /** diff --git a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala index 8f6bf246d..34adc8682 100644 --- a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala +++ b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala @@ -1,13 +1,15 @@ package com.openbankproject.commons.util -import java.util.Date +import net.liftweb.json +import java.util.Date import net.liftweb.json.Extraction.decompose import net.liftweb.json.Formats import org.scalatest.{FlatSpec, Matchers, Tag} class JsonUtilsTest extends FlatSpec with Matchers { object FunctionsTag extends Tag("JsonUtils") + implicit def formats: Formats = net.liftweb.json.DefaultFormats "collectFieldNames" should "return all the field names and path" taggedAs FunctionsTag in { @@ -39,5 +41,115 @@ class JsonUtilsTest extends FlatSpec with Matchers { names should contain ("nestField") names should not contain ("nestField1") } - + + def toCaseClass(str: String, typeNamePrefix: String = ""): String = JsonUtils.toCaseClasses(json.parse(str), typeNamePrefix) + + "object json String" should "generate correct case class" taggedAs FunctionsTag in { + + val zson = { + """ + |{ + | "name": "Sam", + | "age": 12, + | "isMarried": true, + | "weight": 12.11, + | "class": "2", + | "def": 12, + | "email": ["abc@def.com", "hijk@abc.com"], + | "address": [{ + | "name": "jieji", + | "code": 123123, + | "street":{"road": "gongbin", "number": 123} + | }], + | "street": {"name": "hongqi", "width": 12.11}, + | "_optional_fields_": ["age", "weight", "address"] + |} + |""".stripMargin + } + { + val expectedCaseClass = + """case class AddressStreetJsonClass(road: String, number: Long) + |case class AddressJsonClass(name: String, code: Long, street: AddressStreetJsonClass) + |case class StreetJsonClass(name: String, width: Double) + |case class RootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[AddressJsonClass]], street: StreetJsonClass)""".stripMargin + + val generatedCaseClass = toCaseClass(zson) + + generatedCaseClass should be(expectedCaseClass) + } + {// test type name prefix + val expectedCaseClass = + """case class RequestAddressStreetJsonClass(road: String, number: Long) + |case class RequestAddressJsonClass(name: String, code: Long, street: RequestAddressStreetJsonClass) + |case class RequestStreetJsonClass(name: String, width: Double) + |case class RequestRootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[RequestAddressJsonClass]], street: RequestStreetJsonClass)""".stripMargin + + val generatedCaseClass = toCaseClass(zson, "Request") + generatedCaseClass should be(expectedCaseClass) + } + } + + "List json" should "generate correct case class" taggedAs FunctionsTag in { + { + val listIntJson = """[1,2,3]""" + + toCaseClass(listIntJson) should be(""" type RootJsonClass = List[Long]""") + toCaseClass(listIntJson, "Response") should be(""" type ResponseRootJsonClass = List[Long]""") + } + { + val listObjectJson = + """[ + | { + | "name": "zs" + | "weight": 12.34 + | }, + | { + | "name": "ls" + | "weight": 21.43 + | } + |]""".stripMargin + val expectedCaseClass = """case class RootItemJsonClass(name: String, weight: Double) + | type RootJsonClass = List[RootItemJsonClass]""".stripMargin + + val expectedRequestCaseClass = """case class RequestRootItemJsonClass(name: String, weight: Double) + | type RequestRootJsonClass = List[RequestRootItemJsonClass]""".stripMargin + + + toCaseClass(listObjectJson) should be(expectedCaseClass) + toCaseClass(listObjectJson, "Request") should be(expectedRequestCaseClass) + } + } + + "List json have different type items" should "throw exception" taggedAs FunctionsTag in { + + val listJson = """["abc",2,3]""" + val listJson2 = + """[ + | { + | "name": "zs" + | "weight": 12.34 + | }, + | { + | "name": "ls" + | "weight": 21 + | } + |]""".stripMargin + val objectJson = + """{ + | "emails": [true, "abc@def.com"] + |}""".stripMargin + + val objectNestedListJson = + """{ + | "emails": { + | "list": [12.34, "abc@def.com"] + | } + |}""".stripMargin + + the [IllegalArgumentException] thrownBy toCaseClass(listJson) should have message "All the items of Json should be String type." + the [IllegalArgumentException] thrownBy toCaseClass(listJson2) should have message "All the items of Json should the same structure." + the [IllegalArgumentException] thrownBy toCaseClass(objectJson) should have message "All the items of Json emails should be Boolean type." + the [IllegalArgumentException] thrownBy toCaseClass(objectNestedListJson) should have message "All the items of Json emails.list should be number type." + } + } diff --git a/pom.xml b/pom.xml index a07a6fb68..3a2c1681e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.tesobe obp-parent - 1.8.2 + 1.9.0 pom Open Bank Project API Parent 2011