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 6f907e9f1..1ee249f7f 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -36,6 +36,9 @@ object ErrorMessages { // DynamicEntity Exceptions (OBP-09XXX) val DynamicEntityNotFoundByDynamicEntityId = "OBP-09001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id." + val DynamicEntityEntityNameAlreadyExists = "OBP-09002: DynamicEntity's entityName already exists. Please specify a different value for entityName." + val DynamicEntityEntityNotExists = "OBP-09003: DynamicEntity not exists. Please check entityName." + val DynamicEntityMissArgument = "OBP-09004: DynamicEntity process related argument is missing." // General messages (OBP-10XXX) val InvalidJsonFormat = "OBP-10001: Incorrect json format." 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 eab60faf8..6d5803011 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -28,11 +28,14 @@ import code.util.Helper import code.views.Views import code.webhook.AccountWebhook import com.github.dwickern.macros.NameOf.nameOf -import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, ProductAttributeType} +import com.openbankproject.commons.model.enums.DynamicEntityOperation.{CREATE, UPDATE} +import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} import com.openbankproject.commons.model.{AccountApplication, Bank, Customer, CustomerAddress, Product, ProductCollection, ProductCollectionItem, TaxResidence, UserAuthContext, UserAuthContextUpdate, _} import com.tesobe.CacheKeyFromArguments import net.liftweb.common.{Box, Empty, Full} import net.liftweb.http.provider.HTTPParam +import net.liftweb.json +import net.liftweb.json.{JArray, JBool, JDouble, JInt, JObject, JString, JValue} import net.liftweb.util.Helpers.tryo import org.apache.commons.lang3.StringUtils @@ -1397,8 +1400,25 @@ object NewStyle { } } - def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT): Future[Box[DynamicEntityT]] = Future { - DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) + def createOrUpdateDynamicEntity(dynamicEntity: DynamicEntityT, callContext: Option[CallContext]): Future[Box[DynamicEntityT]] = { + val existsDynamicEntity = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(dynamicEntity.entityName) + + val isEntityNameNotChange = existsDynamicEntity.isEmpty || + existsDynamicEntity.filter(_.dynamicEntityId == dynamicEntity.dynamicEntityId).isDefined + + isEntityNameNotChange match { + case true => Future { + DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) + } + case false => { + val entityNameExists = existsDynamicEntity.isDefined + // validate whether entityName is exists + val errorMsg = s"$DynamicEntityEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'." + Helper.booleanToFuture(errorMsg)(!entityNameExists).map { _ => + DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity) + } + } + } } def deleteDynamicEntity(dynamicEntityId: String): Future[Box[Boolean]] = Future { @@ -1413,6 +1433,11 @@ object NewStyle { } } + def getDynamicEntityByEntityName(entityName : String, callContext: Option[CallContext]): OBPReturnType[Box[DynamicEntityT]] = Future { + val boxedDynamicEntity = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(entityName) + (boxedDynamicEntity, callContext) + } + private[this] val dynamicEntityTTL = APIUtil.getPropsValue(s"dynamicEntity.cache.ttl.seconds", "0").toInt def getDynamicEntities(): List[DynamicEntityT] = { @@ -1451,6 +1476,50 @@ object NewStyle { i => (unboxFullOrFail(i._1, callContext, s"$CreateTransactionsException"), i._2) } + def invokeDynamicConnector(operation: DynamicEntityOperation, entityName: String, requestBody: Option[JObject], entityId: Option[String], callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = { + val dynamicEntityBox = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(entityName) + val dynamicEntity = unboxFullOrFail(dynamicEntityBox, callContext, DynamicEntityEntityNotExists) + + if(operation == CREATE || operation == UPDATE) { + val jsonTypeMap = Map[String, Class[_]]( + ("boolean", classOf[JBool]), + ("string", classOf[JString]), + ("array", classOf[JArray]), + ("integer", classOf[JInt]), + ("number", classOf[JDouble]), + ) + val definitionJson = json.parse(dynamicEntity.metadataJson).asInstanceOf[JObject] + val entity = (definitionJson \ entityName).asInstanceOf[JObject] + val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet + + val fieldNameToTypeName: Map[String, String] = (entity \ "properties") + .asInstanceOf[JObject] + .obj + .map(field => (field.name, (field.value \ "type").asInstanceOf[JString].s)) + .toMap + + val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName + .mapValues(jsonTypeMap(_)) + val bodyJson = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody.")) + val fields = bodyJson.obj.filter(it => fieldNameToType.keySet.contains(it.name)) + + // if there are field type are not match the definitions, there must be bug. + val invalidTypes = fields.filterNot(it => fieldNameToType(it.name).isInstance(it.value)) + val invalidTypeNames = invalidTypes.map(_.name).mkString("[", ",", "]") + val missingRequiredFields = requiredFieldNames.filterNot(it => fields.exists(_.name == it)) + val missingFieldNames = missingRequiredFields.mkString("[", ",", "]") + + Helper.booleanToFuture(s"$InvalidJsonFormat these field type not correct: $invalidTypeNames")(invalidTypes.isEmpty).flatMap{ _ => + Helper.booleanToFuture(s"$InvalidJsonFormat some required fields are missing: $missingFieldNames")(missingRequiredFields.isEmpty) + }.flatMap{ _ => + Connector.connector.vend.dynamicEntityProcess(operation, entityName, requestBody, entityId, callContext) + } + + } else { + Connector.connector.vend.dynamicEntityProcess(operation, entityName, requestBody, entityId, 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 c3ca52c06..3893d9441 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 @@ -25,6 +25,7 @@ import net.liftweb.http.rest.RestHelper import net.liftweb.json.Serialization.write import code.api.util.ExampleValue.{dynamicEntityRequestBodyExample, dynamicEntityResponseBodyExample} import com.openbankproject.commons.model.enums.DynamicEntityFieldType +import com.openbankproject.commons.model.enums.DynamicEntityOperation.{CREATE, DELETE, GET_ALL, GET_ONE, UPDATE} import net.liftweb.util.StringHelpers import org.atteo.evo.inflector.English import net.liftweb.json._ @@ -763,11 +764,11 @@ trait APIMethods400 { postedData <- NewStyle.function.tryons(failMsg, 400, callContext) { validateDynamicEntityJson(json) - val JField(name, _) = json.asInstanceOf[JObject].obj.head - DynamicEntityCommons(name, compactRender(json)) + val JField(entityName, _) = json.asInstanceOf[JObject].obj.head + DynamicEntityCommons(entityName, compactRender(json)) } - Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData) + Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData, callContext) } yield { val commonsData: DynamicEntityCommons = dynamicEntity (commonsData.jValue, HttpCode.`201`(callContext)) @@ -822,7 +823,7 @@ trait APIMethods400 { (_, _) <- NewStyle.function.getDynamicEntityById(dynamicEntityId, callContext) - Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData) + Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData, callContext) } yield { val commonsData: DynamicEntityCommons = dynamicEntity (commonsData.jValue, HttpCode.`200`(callContext)) @@ -869,41 +870,43 @@ trait APIMethods400 { lazy val genericEndpoint: OBPEndpoint = { - case EntityName(entityName) :: Nil JsonGet req => { - cc => - Future { - import net.liftweb.json.JsonDSL._ - val listName = StringHelpers.snakify(English.plural(entityName)) - val resultList = MockerConnector.getAll(entityName) - - val jValue: JValue = listName -> resultList - - (jValue, HttpCode.`200`(Some(cc))) - } + case EntityName(entityName) :: Nil JsonGet req => { cc => + val listName = StringHelpers.snakify(English.plural(entityName)) + for { + (Full(resultList: JObject), _) <- NewStyle.function.invokeDynamicConnector(GET_ALL, entityName, None, None, Some(cc)) + } yield { + import net.liftweb.json.JsonDSL._ + val jValue: JObject = listName -> resultList + (jValue, HttpCode.`200`(Some(cc))) + } } - case EntityName(entityName, id) JsonGet req => { - cc => - Future { - (MockerConnector.getSingle(entityName, id), HttpCode.`200`(Some(cc))) - } + case EntityName(entityName, id) JsonGet req => {cc => + for { + (Full(entity), _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), Some(cc)) + } yield { + (entity, HttpCode.`200`(Some(cc))) + } } - case EntityName(entityName) :: Nil JsonPost json -> _ => { - cc => - Future { - (MockerConnector.persist(entityName, json.asInstanceOf[JObject]), HttpCode.`201`(Some(cc))) - } + case EntityName(entityName) :: Nil JsonPost json -> _ => {cc => + for { + (Full(entity), _) <- NewStyle.function.invokeDynamicConnector(CREATE, entityName, Some(json.asInstanceOf[JObject]), None, Some(cc)) + } yield { + (entity, HttpCode.`201`(Some(cc))) + } } - case EntityName(entityName, id) JsonPut json -> _ => { - cc => - Future { - (MockerConnector.persist(entityName, json.asInstanceOf[JObject], Some(id)), HttpCode.`200`(Some(cc))) - } + case EntityName(entityName, id) JsonPut json -> _ => { cc => + for { + (Full(entity), _) <- NewStyle.function.invokeDynamicConnector(UPDATE, entityName, Some(json.asInstanceOf[JObject]), Some(id), Some(cc)) + } yield { + (entity, HttpCode.`200`(Some(cc))) + } } - case EntityName(entityName, id) JsonDelete req => { - cc => - Future { - (MockerConnector.delete(entityName, id), HttpCode.`200`(Some(cc))) - } + case EntityName(entityName, id) JsonDelete req => { cc => + for { + (Full(deleteResult), _) <- NewStyle.function.invokeDynamicConnector(DELETE, entityName, None, Some(id), Some(cc)) + } yield { + (deleteResult, HttpCode.`200`(Some(cc))) + } } } diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 28713da65..96e1e7411 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -31,12 +31,12 @@ import code.transactionrequests.{TransactionRequestTypeCharge, TransactionReques import code.users.Users import code.util.Helper._ import code.views.Views -import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, ProductAttributeType} +import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType} import com.openbankproject.commons.model.{AccountApplication, Bank, CounterpartyTrait, CustomerAddress, Product, ProductCollection, ProductCollectionItem, TaxResidence, TransactionRequestStatus, UserAuthContext, UserAuthContextUpdate, _} - import com.tesobe.CacheKeyFromArguments import net.liftweb.common.{Box, Empty, Failure, Full} import net.liftweb.json.Extraction.decompose +import net.liftweb.json.JObject import net.liftweb.json.JsonAST.JValue import net.liftweb.mapper.By import net.liftweb.util.Helpers.tryo @@ -1862,4 +1862,18 @@ trait Connector extends MdcLoggable with CustomJsonFormats{ chargePolicy: String, callContext: Option[CallContext]): OBPReturnType[Box[TransactionId]] = Future{(Failure(setUnimplementedError), callContext)} + /** + * DynamicEntity process function + * @param operation type of operation, this is an enumeration + * @param entityName DynamicEntity's entity name + * @param requestBody content of request + * @param entityId id of given DynamicEntity + * @param callContext + * @return result DynamicEntity process + */ + def dynamicEntityProcess(operation: DynamicEntityOperation, + entityName: String, + requestBody: Option[JObject], + entityId: Option[String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)} } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 63604cc88..c4b72553a 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -12,6 +12,7 @@ import code.api.util.APIUtil.{OBPReturnType, isValidCurrencyISOCode, saveConnect import code.api.util.ErrorMessages._ import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA import code.api.util._ +import code.api.v4_0_0.MockerConnector import code.atms.Atms.Atm import code.atms.MappedAtm import code.branches.Branches.Branch @@ -51,11 +52,14 @@ import code.views.Views import com.google.common.cache.CacheBuilder import com.nexmo.client.NexmoClient import com.nexmo.client.sms.messages.TextMessage -import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, ProductAttributeType, StrongCustomerAuthentication} +import com.openbankproject.commons.model.enums.DynamicEntityOperation.{CREATE, DELETE, GET_ALL, GET_ONE, UPDATE} +import com.openbankproject.commons.model.enums.{AccountAttributeType, CardAttributeType, DynamicEntityOperation, ProductAttributeType, StrongCustomerAuthentication} import com.openbankproject.commons.model.{AccountApplication, AccountAttribute, Product, ProductAttribute, ProductCollectionItem, TaxResidence, _} import com.tesobe.CacheKeyFromArguments import com.tesobe.model.UpdateBankAccount import net.liftweb.common._ +import net.liftweb.json.JsonAST.{JArray, JBool} +import net.liftweb.json.{JObject, JValue} import net.liftweb.mapper.{By, _} import net.liftweb.util.Helpers.{tryo, _} import net.liftweb.util.Mailer @@ -2785,4 +2789,29 @@ object LocalMappedConnector extends Connector with MdcLoggable { (boxedData, callContext) } + override def dynamicEntityProcess(operation: DynamicEntityOperation, + entityName: String, + requestBody: Option[JObject], + entityId: Option[String], + callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future { + val processResult: Box[JValue] = operation match { + case GET_ALL => Full { + JArray(MockerConnector.getAll(entityName).toList) + } + case GET_ONE => { + val boxedEntity: Box[JValue] = MockerConnector.getSingle(entityName, entityId.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument the entityId is required."))) + boxedEntity + } + case CREATE | UPDATE => { + val body = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody.")) + val persistedEntity = MockerConnector.persist(entityName, body, entityId) + Full(persistedEntity) + } + case DELETE => { + val deleteResult = MockerConnector.delete(entityName, entityId.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument the entityId is required. "))) + deleteResult.map(JBool(_)) + } + } + (processResult, callContext) + } } diff --git a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala index 5cf48a4d1..e4b21a5f8 100644 --- a/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala +++ b/obp-api/src/main/scala/code/dynamicEntity/DynamicEntityProvider.scala @@ -52,6 +52,8 @@ object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommo trait DynamicEntityProvider { def getById(dynamicEntityId: String): Box[DynamicEntityT] + def getByEntityName(entityName: String): Box[DynamicEntityT] + def getDynamicEntities(): List[DynamicEntityT] def createOrUpdate(dynamicEntity: DynamicEntityT): Box[DynamicEntityT] diff --git a/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala index 4ac2a8baa..f12613096 100644 --- a/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala +++ b/obp-api/src/main/scala/code/dynamicEntity/MapppedDynamicEntityProvider.scala @@ -13,6 +13,10 @@ object MappedDynamicEntityProvider extends DynamicEntityProvider with CustomJson By(DynamicEntity.DynamicEntityId, dynamicEntityId) ) + override def getByEntityName(entityName: String): Box[DynamicEntityT] = DynamicEntity.find( + By(DynamicEntity.EntityName, entityName) + ) + override def getDynamicEntities(): List[DynamicEntity] = { DynamicEntity.findAll() } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index e27f3b7e5..49a26eb2e 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -45,6 +45,7 @@ object PemCertificateRole extends OBPEnumeration[PemCertificateRole] { object PSP_PI extends Value } //------api enumerations end ---- + sealed trait DynamicEntityFieldType extends EnumValue object DynamicEntityFieldType extends OBPEnumeration[DynamicEntityFieldType]{ object string extends Value @@ -53,4 +54,16 @@ object DynamicEntityFieldType extends OBPEnumeration[DynamicEntityFieldType]{ object boolean extends Value // object array extends Value // object `object` extends Value //TODO in the future, we consider support nested type +} + +/** + * connector support operation type for DynamicEntity + */ +sealed trait DynamicEntityOperation extends EnumValue +object DynamicEntityOperation extends OBPEnumeration[DynamicEntityOperation] { + object GET_ALL extends Value + object GET_ONE extends Value + object CREATE extends Value + object UPDATE extends Value + object DELETE extends Value } \ No newline at end of file