diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 577ad5ee1..e4c134783 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -124,8 +124,6 @@ org.scalatest scalatest_${scala.version} - 3.0.5 - test 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 ddca29b46..ae9d2a910 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 @@ -9429,37 +9429,28 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable */ private def convertId[T](obj: T, customerIdConverter: String=> String, accountIdConverter: String=> String): T = { //1st: We must not convert when connector == mapped. this will ignore the implicitly_convert_ids props. - //2rd: if connector != mapped, we still need the `implicitly_convert_ids == true` - if(APIUtil.getPropsValue("connector","mapped") != "mapped" && APIUtil.getPropsAsBoolValue("implicitly_convert_ids",false)){ - ReflectUtils.operateNestedValues(obj)(fieldMirror => { - val fieldValue = fieldMirror.get - val fieldSymbol: TermSymbol = fieldMirror.symbol - val fieldType: Type = fieldSymbol.info - val fieldName: String = fieldSymbol.name.toString.trim.toLowerCase - - val ownerSymbol: Type = fieldSymbol.owner.asType.toType - - if(fieldValue == null) { - // do nothing - } else if (ownerSymbol <:< typeOf[CustomerId] || - (fieldName == "customerid" && fieldType =:= typeOf[String]) || - (ownerSymbol <:< typeOf[Customer] && fieldName == "id" && fieldType =:= typeOf[String]) - ) { - val customerRef = customerIdConverter(fieldValue.asInstanceOf[String]) - fieldMirror.set(customerRef) - } else if(ownerSymbol <:< typeOf[AccountId] || - (fieldName == "accountid" && fieldType =:= typeOf[String]) || - (ownerSymbol <:< typeOf[CoreAccount] && fieldName == "id" && fieldType =:= typeOf[String])|| - (ownerSymbol <:< typeOf[AccountBalance] && fieldName == "id" && fieldType =:= typeOf[String])|| - (ownerSymbol <:< typeOf[AccountHeld] && fieldName == "id" && fieldType =:= typeOf[String]) - ) { - val accountRef = accountIdConverter(fieldValue.asInstanceOf[String]) - fieldMirror.set(accountRef) - } - }) - obj - } else - obj + //2rd: if connector != mapped, we still need the `implicitly_convert_ids == true` + + def isCustomerId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = { + ownerType =:= typeOf[CustomerId] || + (fieldName.equalsIgnoreCase("customerId") && fieldType =:= typeOf[String]) || + (ownerType <:< typeOf[Customer] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String]) + } + + def isAccountId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = { + ownerType <:< typeOf[AccountId] || + (fieldName.equalsIgnoreCase("accountId") && fieldType =:= typeOf[String]) + (ownerType <:< typeOf[CoreAccount] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])|| + (ownerType <:< typeOf[AccountBalance] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])|| + (ownerType <:< typeOf[AccountHeld] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String]) + } + + ReflectUtils.resetNestedFields(obj){ + case (fieldName, fieldType, fieldValue: String, ownerType) if isCustomerId(fieldName, fieldType, fieldValue, ownerType) => customerIdConverter(fieldValue) + case (fieldName, fieldType, fieldValue: String, ownerType) if isAccountId(fieldName, fieldType, fieldValue, ownerType) => accountIdConverter(fieldValue) + } + + obj } /** diff --git a/obp-commons/pom.xml b/obp-commons/pom.xml index 2b280f799..91e4084ed 100644 --- a/obp-commons/pom.xml +++ b/obp-commons/pom.xml @@ -13,6 +13,14 @@ jar Open Bank Project Commons + + + artima + Artima Maven Repository + http://repo.artima.com/releases + + + net.liftweb @@ -27,13 +35,23 @@ org.scalatest scalatest_${scala.version} - 3.0.5 - test + + + org.scalactic + scalactic_${scala.version} + + org.apache.maven.plugins + maven-surefire-plugin + ${scala.version} + + true + + net.alchim31.maven scala-maven-plugin @@ -47,6 +65,13 @@ ${scala.version} incremental true + + + com.artima.supersafe + supersafe_${scala.compiler} + 1.1.8 + + diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/OBPEnum.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/OBPEnum.scala index 8fb01d5a2..4c04a117a 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/OBPEnum.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/OBPEnum.scala @@ -71,8 +71,6 @@ object OBPEnumeration { def getValuesByClass[T <: EnumValue](clazz: Class[T]): List[T] = getEnumContainer(clazz).values - def getValues[T <: EnumValue: TypeTag]: List[T] = getEnumContainer(typeTag[T].tpe).values.map(_.asInstanceOf[T]) - def withNameOption(tp: Type, name: String): Option[EnumValue] = getEnumContainer(tp).withNameOption(name).map(_.asInstanceOf[EnumValue]) def withNameOption[T <: EnumValue](clazz: Class[T], name: String): Option[T] = getEnumContainer(clazz).withNameOption(name) 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 c38c2b6b2..593ac8bb2 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 @@ -56,14 +56,17 @@ object ReflectUtils { def setField[T](obj: AnyRef, fieldName: String, fieldValue: T): T = operateField[T](obj, fieldName, _.set(fieldValue)) /** - * modify given instance nested values + * modify given instance nested fields value * @param obj given instance to modify * @param predicate check whether current field value need to modify - * @param fn modify function + * @param fn modify function, signature is (fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type): Any + * fn result is calculated new value * @return modified instance + * + * @note be carefully, this method will modify immutable object state, before you call this method, you should very sure it is safe for your logic. */ - def operateNestedValues(obj: Any, predicate: Any => Boolean = isObpObject)(fn: ru.FieldMirror => Unit): Any = { - val recurseCallback = operateNestedValues(_: Any, predicate)(fn) + def resetNestedFields(obj: Any, predicate: Any => Boolean = isObpObject)(fn: PartialFunction[(String, Type, Any, Type), Any]): Any = { + val recurseCallback = resetNestedFields(_: Any, predicate)(fn) obj match { case null | None | Empty | Nil => obj case _: Unit => obj @@ -85,7 +88,7 @@ object ReflectUtils { case v: Array[_] => v.map(recurseCallback) case v: Map[_, _] => v.values.map(recurseCallback) case v: Iterable[_] => v.map(recurseCallback) - case v if(!predicate(v)) => v + case v if !predicate(v) => v case _ => { val tp = this.getType(obj) val instanceMirror: ru.InstanceMirror = mirror.reflect(obj) @@ -98,8 +101,18 @@ object ReflectUtils { constructFieldNames.foreach(it => { val fieldSymbol: ru.TermSymbol = getType(obj).member(ru.TermName(it)).asTerm.accessed.asTerm val fieldMirror: ru.FieldMirror = instanceMirror.reflectField(fieldSymbol) - recurseCallback(fieldMirror.get) - fn(fieldMirror) + val fieldValue: Any = fieldMirror.get + recurseCallback(fieldValue) + + //check whether field should modify, if PartialFunction check result is true, just modify it with new Value + val fieldName: String = it + val fieldType: Type = fieldSymbol.info + val ownerType: Type = fieldSymbol.owner.asType.toType + + if(fn.isDefinedAt(fieldName, fieldType, fieldValue, ownerType)) { + val newValue = fn(fieldName, fieldType, fieldValue, ownerType) + fieldMirror.set(newValue) + } }) obj } diff --git a/obp-commons/src/test/scala/com/openbankproject/commons/util/OBPEnumerationTest.scala b/obp-commons/src/test/scala/com/openbankproject/commons/util/OBPEnumerationTest.scala new file mode 100644 index 000000000..e19c768ba --- /dev/null +++ b/obp-commons/src/test/scala/com/openbankproject/commons/util/OBPEnumerationTest.scala @@ -0,0 +1,241 @@ +package com.openbankproject.commons.util + +import com.openbankproject.commons.util.Color.Color +import com.openbankproject.commons.util.Shape.Shape +import org.scalatest._ + +import scala.reflect.runtime.universe._ + +import org.scalatest.Tag + +// to show bad design of scala enumeration +object Shape extends Enumeration { + type Shape = Value + val Circle = Value + val Square = Value + val Other = Value +} + +object Color extends Enumeration { + type Color = Value + val Red = Value + val Green = Value + val Other = Value +} + +object OBPEnumTag extends Tag("OBPEnumeration") +/** + * just for demonstrate what problem of scala enumeration, so here just set to ignore + */ +@Ignore +class ScalaEnumerationTest extends FlatSpec with Matchers { + + it should "legal to create two overloaded methods with parameter Shape and Color" taggedAs(OBPEnumTag) in { + // if remove the comment of process method, will can't compile + object OverloadTest{ + def process(shape: Shape) = ??? +// def process(Color: Color) = ??? + } + } + + it should "have compile warnings when match case is not exhaustive with enumeration" taggedAs(OBPEnumTag) in { + val shape: Shape = Shape.Other + shape match { + case Shape.Other => println("hi other") + } + } + + "shape.isInstanceOf[Color] " should "return false" taggedAs(OBPEnumTag) in { + // the worst: confused type check + val shape: Shape = Shape.Other + + // shape is not Color, isColor should be false + val isColor = shape.isInstanceOf[Color] + isColor shouldBe false + } + "shape cast to Color" should "throw ClassCastException" taggedAs(OBPEnumTag) in { + val shape: Shape = Shape.Other + // shape cast to Color should throw ClassCastException + a[ClassCastException] should be thrownBy { + val color: Color = shape.asInstanceOf[Color] + } + } + "if shape can be cast to Color, casted value" should "match Color value" taggedAs(OBPEnumTag) in { + val shape: Shape = Shape.Other + val color: Color = shape.asInstanceOf[Color] + val wrong: String = color match { + case Color.Other => "match Color#other" + case _ => "NOT MATCH" + } + color.toString shouldBe "Other" + + wrong shouldBe "match Color#other" + } +} + + +// to demonstrate OBPEnumeration +sealed trait OBPShape extends EnumValue + +object OBPShape extends OBPEnumeration[OBPShape]{ + object Circle extends OBPShape + object Square extends OBPShape + object Other extends OBPShape +} + +sealed trait OBPColor extends EnumValue + +object OBPColor extends OBPEnumeration[OBPColor]{ + object Red extends OBPColor + object Green extends OBPColor + object Other extends OBPColor +} + +class OBPEnumerationTest extends FlatSpec with Matchers { + it should "legal to create two overloaded methods with parameter OBPShape and OBPColor" taggedAs(OBPEnumTag) in { + // first bad: can't overload for different enumeration + object OverloadTest{ + def process(shape: OBPShape) = ??? + def process(Color: OBPColor) = ??? + } + } + + it should "have compile warnings when match case is not exhaustive with enumeration" taggedAs(OBPEnumTag) in { + val shape: OBPShape = OBPShape.Other + shape match { + case OBPShape.Other => "hi other" + } + } + + "shape.isInstanceOf[OBPColor] " should "return false" taggedAs(OBPEnumTag) in { + // the worst: confused type check + val shape: OBPShape = OBPShape.Other + + // shape is not Color, isColor should be false + val isColor = shape.isInstanceOf[OBPColor] + isColor shouldBe false + } + + "shape cast to Color" should "throw ClassCastException" taggedAs(OBPEnumTag) in { + val shape: OBPShape = OBPShape.Other + // shape cast to Color should throw ClassCastException + a [ClassCastException] should be thrownBy { + val color = shape.asInstanceOf[OBPColor] + } + } + + "values" should "contains all values" taggedAs(OBPEnumTag) in { + OBPShape.values should contain only (OBPShape.Square, OBPShape.Circle, OBPShape.Other) + } + + "example" should "be one value of OBPShape enumeration" taggedAs(OBPEnumTag) in { + OBPShape.example shouldBe a [OBPShape] + } + "nameToValue" should "be name map to value of OBPShape enumeration" taggedAs(OBPEnumTag) in { + OBPShape.nameToValue should contain theSameElementsAs Map("Square" -> OBPShape.Square, "Circle" -> OBPShape.Circle, "Other" -> OBPShape.Other) + } + + it should "get a Some(v) value when call withNameOption to retrieve exists OBPShape value" taggedAs(OBPEnumTag) in { + OBPShape.withNameOption("Square") shouldBe Some(OBPShape.Square) + } + + it should "get a None value when call withNameOption to retrieve not exists OBPShape value" taggedAs(OBPEnumTag) in { + OBPShape.withNameOption("NOT_EXISTS") shouldBe None + } + + it should "get a value when call withName to retrieve exists OBPShape value" taggedAs(OBPEnumTag) in { + OBPShape.withName("Circle") shouldBe OBPShape.Circle + } + + it should "throw NoSuchElementException when call withName to retrieve not exists OBPShape value" taggedAs(OBPEnumTag) in { + a [NoSuchElementException] should be thrownBy { + OBPShape.withName("NOT_EXISTS") shouldBe OBPShape.Circle + } + } + + // the follow test is for OBPEnumeration utils functions + "call OBPEnumeration.getValuesByType with an OBPEnumeration type" should "get all values" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPColor] + OBPEnumeration.getValuesByType(unknownType) shouldBe(OBPColor.values) + } + "call OBPEnumeration.getValuesByClass with an OBPEnumeration type" should "get all values" taggedAs(OBPEnumTag) in { + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + OBPEnumeration.getValuesByClass(unknownClazz) should be(OBPShape.values) + } + "call OBPEnumeration.withNameOption with an OBPEnumeration type OR class and exists name" should "get correct Some(v)" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withNameOption(unknownType, "Square") shouldBe Some(OBPShape.Square) + OBPEnumeration.withNameOption(unknownClazz, "Circle") shouldBe Some(OBPShape.Circle) + } + "call OBPEnumeration.withNameOption with an OBPEnumeration type OR class and not exists name" should "get correct None" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withNameOption(unknownType, "NOT_EXISTS") shouldBe empty + OBPEnumeration.withNameOption(unknownClazz, "NOT_EXISTS") shouldBe empty + } + "call OBPEnumeration.withName with an OBPEnumeration type OR class and exists name" should "get correct value" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withName(unknownType, "Square") shouldBe OBPShape.Square + OBPEnumeration.withName(unknownClazz, "Circle") shouldBe OBPShape.Circle + } + "call OBPEnumeration.withName with an OBPEnumeration type OR class and not exists name" should "throw NoSuchElementException" taggedAs(OBPEnumTag) in { + a [NoSuchElementException] should be thrownBy { + val unknownType = typeOf[OBPShape] + OBPEnumeration.withName(unknownType, "NOT_EXISTS") + } + + a [NoSuchElementException] should be thrownBy { + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + OBPEnumeration.withName(unknownClazz, "NOT_EXISTS") + } + } + + + "call OBPEnumeration.withIndexOption with an OBPEnumeration type OR class and exists taggedAs(OBPEnumTag) index" should "get correct Some(v)" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withIndexOption(unknownType, 0) shouldBe Some(OBPShape.Circle) + OBPEnumeration.withIndexOption(unknownClazz, 1) shouldBe Some(OBPShape.Square) + } + "call OBPEnumeration.withIndexOption with an OBPEnumeration type OR class and not exists taggedAs(OBPEnumTag) index" should "get correct None" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withIndexOption(unknownType, 4) shouldBe empty + OBPEnumeration.withIndexOption(unknownClazz, -1) shouldBe empty + } + "call OBPEnumeration.withIndex with an OBPEnumeration type OR class and exists taggedAs(OBPEnumTag) index" should "get correct value" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + + OBPEnumeration.withIndex(unknownType, 0) shouldBe OBPShape.Circle + OBPEnumeration.withIndex(unknownClazz, 1) shouldBe OBPShape.Square + } + "call OBPEnumeration.withIndex with an OBPEnumeration type OR class and not exists taggedAs(OBPEnumTag) index" should "throw NoSuchElementException" taggedAs(OBPEnumTag) in { + a [NoSuchElementException] should be thrownBy { + val unknownType = typeOf[OBPShape] + OBPEnumeration.withIndex(unknownType, 8) + } + + a [NoSuchElementException] should be thrownBy { + val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]] + OBPEnumeration.withIndex(unknownClazz, -1) + } + } + + "call OBPEnumeration.getExampleByType" should "get one of values" taggedAs(OBPEnumTag) in { + val unknownType = typeOf[OBPShape] + OBPEnumeration.getExampleByType(unknownType) shouldBe(OBPShape.Circle) + } + + "call OBPEnumeration.getExampleByClass" should "get one of values" taggedAs(OBPEnumTag) in { + val unknownClazz = classOf[OBPColor].asInstanceOf[Class[EnumValue]] + OBPEnumeration.getExampleByClass(unknownClazz) shouldBe(OBPColor.Red) + } +} \ No newline at end of file diff --git a/obp-commons/src/test/scala/com/openbankproject/commons/util/ReflectUtilsTest.scala b/obp-commons/src/test/scala/com/openbankproject/commons/util/ReflectUtilsTest.scala new file mode 100644 index 000000000..44b7a64e3 --- /dev/null +++ b/obp-commons/src/test/scala/com/openbankproject/commons/util/ReflectUtilsTest.scala @@ -0,0 +1,32 @@ +package com.openbankproject.commons.util + +import org.scalatest.{FlatSpec, Matchers} +import scala.reflect.runtime.universe._ +import org.scalatest.Tag + +class ReflectUtilsTest extends FlatSpec with Matchers { + object ReflectUtilsTag extends Tag("ReflectUtils") + + case class Aperson(id: String, age: Int) + case class Agroup(manager: Aperson, id: Int, members: List[Aperson]) + + + "when modify Apersion#id to append suffix" should "all the not null id be end with suffix" taggedAs(ReflectUtilsTag) in { + val members = List(Aperson(null, 10), Aperson("p1-id", 20), Aperson("p2-id", 3)) + val group = Agroup(Aperson("m-id", 11), 3, members) + val someGroup = Some(group) + + val idSuffix = "---END" + + ReflectUtils.resetNestedFields(someGroup){ + case (fieldName, fieldType, fieldValue: String, ownerType) if(fieldName == "id" && ownerType =:= typeOf[Aperson]) => + fieldValue + idSuffix + } + + group.manager.id should endWith (idSuffix) + group.id shouldBe(3) + group.members.head.id shouldBe null + group.members.lift(1).get.id should endWith (idSuffix) + group.members.lift(2).get.id should endWith (idSuffix) + } +} diff --git a/pom.xml b/pom.xml index b45925766..5470e55ee 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,18 @@ lift-common_${scala.version} ${lift.version} + + org.scalatest + scalatest_${scala.version} + 3.0.8 + test + + + org.scalactic + scalactic_${scala.version} + 3.0.8 + test +