diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala
index fa244d926..8a44119c7 100644
--- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala
+++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCaller.scala
@@ -18,43 +18,75 @@ package za.co.absa.enceladus.dao.rest
import org.apache.commons.lang.exception.ExceptionUtils
import org.slf4j.LoggerFactory
import org.springframework.web.client.{ResourceAccessException, RestClientException}
+import za.co.absa.enceladus.dao.rest.CrossHostApiCaller.logger
import za.co.absa.enceladus.dao.{DaoException, RetryableException}
+import scala.annotation.tailrec
import scala.util.{Failure, Random, Try}
-protected object CrossHostApiCaller {
+object CrossHostApiCaller {
- def apply(apiBaseUrls: List[String]): CrossHostApiCaller = {
- new CrossHostApiCaller(apiBaseUrls, Random.nextInt(apiBaseUrls.size))
+ private val logger = LoggerFactory.getLogger(classOf[CrossHostApiCaller])
+
+ final val DefaultUrlsRetryCount: Int = 0
+
+ private def createInstance(apiBaseUrls: Seq[String], urlsRetryCount: Int, startWith: Option[Int]): CrossHostApiCaller = {
+ val maxTryCount: Int = (if (urlsRetryCount < 0) {
+ logger.warn(s"Urls retry count cannot be negative ($urlsRetryCount). Using default number of retries instead ($DefaultUrlsRetryCount).") //scalastyle:ignore maxLineLength
+ DefaultUrlsRetryCount
+ } else {
+ urlsRetryCount
+ }) + 1
+ val currentHostIndex = startWith.getOrElse(Random.nextInt(Math.max(apiBaseUrls.size, 1)))
+ new CrossHostApiCaller(apiBaseUrls.toVector, maxTryCount, currentHostIndex)
}
+ def apply(apiBaseUrls: Seq[String], urlsRetryCount: Int = DefaultUrlsRetryCount, startWith: Option[Int] = None): CrossHostApiCaller = {
+ createInstance(apiBaseUrls, urlsRetryCount, startWith)
+ }
}
-protected class CrossHostApiCaller(apiBaseUrls: List[String], var currentHostIndex: Int) extends ApiCaller {
- private val logger = LoggerFactory.getLogger(this.getClass)
+protected class CrossHostApiCaller private(apiBaseUrls: Vector[String], maxTryCount: Int, private var currentHostIndex: Int)
+ extends ApiCaller {
+
+ def baseUrlsCount: Int = apiBaseUrls.size
+
+ def currentBaseUrl: String = apiBaseUrls(currentHostIndex)
+
+ def nextBaseUrl(): String = {
+ currentHostIndex = (currentHostIndex + 1) % baseUrlsCount
+ currentBaseUrl
+ }
- private val maxAttempts = apiBaseUrls.size - 1
def call[T](fn: String => T): T = {
+ def logFailure(error: Throwable, url: String, attemptNumber: Int, nextUrl: Option[String]): Unit = {
+ val rootCause = ExceptionUtils.getRootCauseMessage(error)
+ val switching = nextUrl.map(s => s", switching host to $s").getOrElse("")
+ logger.warn(s"Request failed on host $url (attempt $attemptNumber of $maxTryCount)$switching - $rootCause")
+ }
- def attempt(index: Int, attemptCount: Int = 0): Try[T] = {
- currentHostIndex = index
- val currentBaseUrl = apiBaseUrls(index)
- Try {
- fn(currentBaseUrl)
+ @tailrec
+ def attempt(url: String, attemptNumber: Int, urlsTried: Int): Try[T] = {
+ val result =Try {
+ fn(url)
}.recoverWith {
case e @ (_: ResourceAccessException | _: RestClientException) => Failure(DaoException("Server non-responsive", e))
- }.recoverWith {
- case e: RetryableException if attemptCount < maxAttempts =>
- val nextIndex = (index + 1) % apiBaseUrls.size
- val nextBaseUrl = apiBaseUrls(nextIndex)
- val rootCause = ExceptionUtils.getRootCauseMessage(e)
- logger.warn(s"Request failed on host $currentBaseUrl, switching host to $nextBaseUrl - $rootCause")
- attempt(nextIndex, attemptCount + 1)
+ }
+ //using match instead of recoverWith to make the function @tailrec
+ result match {
+ case Failure(e: RetryableException) if attemptNumber < maxTryCount =>
+ logFailure(e, url, attemptNumber, None)
+ attempt(url, attemptNumber + 1, urlsTried)
+ case Failure(e: RetryableException) if urlsTried < baseUrlsCount =>
+ val nextUrl = nextBaseUrl()
+ logFailure(e, url, attemptNumber, Option(nextUrl))
+ attempt(nextUrl, 1, urlsTried + 1)
+ case _ => result
}
}
- attempt(currentHostIndex).get
+ attempt(currentBaseUrl,1, 1).get
}
}
diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala
index a4787d203..719af163f 100644
--- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala
+++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParser.scala
@@ -29,7 +29,7 @@ object MenasConnectionStringParser {
.replaceAll("/$", "")
.replaceAll("/api$", "")
)
- .toSet
+ .distinct
.toList
}
diff --git a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala
index 51bd04ecf..5edde52d5 100644
--- a/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala
+++ b/dao/src/main/scala/za/co/absa/enceladus/dao/rest/RestDaoFactory.scala
@@ -16,16 +16,33 @@
package za.co.absa.enceladus.dao.rest
import za.co.absa.enceladus.dao.auth.MenasCredentials
+import za.co.absa.enceladus.dao.rest.RestDaoFactory.AvailabilitySetup.{Fallback, AvailabilitySetup, RoundRobin}
object RestDaoFactory {
+ object AvailabilitySetup extends Enumeration {
+ final type AvailabilitySetup = Value
+
+ final val RoundRobin = Value("roundrobin")
+ final val Fallback = Value("fallback")
+ }
+
+ final val DefaultAvailabilitySetup: AvailabilitySetup = RoundRobin
+
private val restTemplate = RestTemplateSingleton.instance
- def getInstance(authCredentials: MenasCredentials, apiBaseUrls: List[String]): MenasRestDAO = {
- val apiCaller = CrossHostApiCaller(apiBaseUrls)
+ def getInstance(authCredentials: MenasCredentials,
+ apiBaseUrls: List[String],
+ urlsRetryCount: Option[Int] = None,
+ menasSetup: AvailabilitySetup = DefaultAvailabilitySetup): MenasRestDAO = {
+ val startsWith = if (menasSetup == Fallback) {
+ Option(0)
+ } else {
+ None
+ }
+ val apiCaller = CrossHostApiCaller(apiBaseUrls, urlsRetryCount.getOrElse(CrossHostApiCaller.DefaultUrlsRetryCount), startsWith)
val authClient = AuthClient(authCredentials, apiCaller)
val restClient = new RestClient(authClient, restTemplate)
new MenasRestDAO(apiCaller, restClient)
}
-
}
diff --git a/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..1f0955d45
--- /dev/null
+++ b/dao/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala
index 67301e22b..29ae6fc0b 100644
--- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala
+++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/CrossHostApiCallerSuite.scala
@@ -17,6 +17,7 @@ package za.co.absa.enceladus.dao.rest
import org.mockito.Mockito
import org.springframework.web.client.ResourceAccessException
+import za.co.absa.enceladus.dao.rest.CrossHostApiCaller.DefaultUrlsRetryCount
import za.co.absa.enceladus.dao.{DaoException, UnauthorizedException}
class CrossHostApiCallerSuite extends BaseTestSuite {
@@ -27,12 +28,23 @@ class CrossHostApiCallerSuite extends BaseTestSuite {
Mockito.reset(restClient)
}
+ "CrossHostApiCaller" should {
+ "cycle through urls" in {
+ val crossHostApiCaller = CrossHostApiCaller(Vector("a", "b", "c", "d"), DefaultUrlsRetryCount, startWith = Some(1))
+ crossHostApiCaller.nextBaseUrl() should be("c")
+ crossHostApiCaller.nextBaseUrl() should be("d")
+ crossHostApiCaller.nextBaseUrl() should be("a")
+ crossHostApiCaller.nextBaseUrl() should be("b")
+ crossHostApiCaller.nextBaseUrl() should be("c")
+ }
+ }
+
"CrossHostApiCaller::call" should {
"return the result of the first successful call" when {
"there are no failures" in {
Mockito.when(restClient.sendGet[String]("a")).thenReturn("success")
- val result = new CrossHostApiCaller(List("a", "b", "c"), 0).call { str =>
+ val result = CrossHostApiCaller(Vector("a", "b", "c"), DefaultUrlsRetryCount, startWith = Some(0)).call { str =>
restClient.sendGet[String](str)
}
@@ -42,16 +54,33 @@ class CrossHostApiCallerSuite extends BaseTestSuite {
"only some calls fail with a retryable exception" in {
Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A"))
- Mockito.when(restClient.sendGet[String]("b")).thenReturn("success")
+ Mockito.when(restClient.sendGet[String]("b"))
+ .thenThrow(DaoException("Something went wrong B"))
+ .thenReturn("success")
- val result = new CrossHostApiCaller(List("a", "b", "c"), 0).call { str =>
+ val result = CrossHostApiCaller(Vector("a", "b", "c"), 2, Some(0)).call { str =>
+ restClient.sendGet[String](str)
+ }
+
+ result should be("success")
+ Mockito.verify(restClient, Mockito.times(3)).sendGet[String]("a")
+ Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("b")
+ Mockito.verify(restClient, Mockito.never()).sendGet[String]("c")
+ }
+
+ "despite retry count is negative" in {
+ Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A"))
+ Mockito.when(restClient.sendGet[String]("b")).thenThrow(DaoException("Something went wrong B"))
+ Mockito.when(restClient.sendGet[String]("c")).thenReturn("success")
+
+ val result = CrossHostApiCaller(Vector("a", "b", "c"), -2, Some(0)).call { str =>
restClient.sendGet[String](str)
}
result should be("success")
Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("a")
Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("b")
- Mockito.verify(restClient, Mockito.never()).sendGet[String]("c")
+ Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("c")
}
}
@@ -62,7 +91,7 @@ class CrossHostApiCallerSuite extends BaseTestSuite {
Mockito.when(restClient.sendGet[String]("c")).thenThrow(DaoException("Something went wrong C"))
val exception = intercept[DaoException] {
- new CrossHostApiCaller(List("a", "b", "c"), 0).call { str =>
+ CrossHostApiCaller(Vector("a", "b", "c"), 0, Some(0)).call { str =>
restClient.sendGet[String](str)
}
}
@@ -73,12 +102,29 @@ class CrossHostApiCallerSuite extends BaseTestSuite {
Mockito.verify(restClient, Mockito.times(1)).sendGet[String]("c")
}
+ "all calls fail with a retryable exception over multiple attempts" in {
+ Mockito.when(restClient.sendGet[String]("a")).thenThrow(DaoException("Something went wrong A"))
+ Mockito.when(restClient.sendGet[String]("b")).thenThrow(DaoException("Something went wrong B"))
+ Mockito.when(restClient.sendGet[String]("c")).thenThrow(DaoException("Something went wrong C"))
+
+ val exception = intercept[DaoException] {
+ CrossHostApiCaller(Vector("a", "b", "c"), 1, Some(0)).call { str =>
+ restClient.sendGet[String](str)
+ }
+ }
+
+ exception.getMessage should be("Something went wrong C")
+ Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("a")
+ Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("b")
+ Mockito.verify(restClient, Mockito.times(2)).sendGet[String]("c")
+ }
+
"any call fails with a non-retryable exception" in {
Mockito.when(restClient.sendGet[String]("a")).thenThrow(new ResourceAccessException("Something went wrong A"))
Mockito.when(restClient.sendGet[String]("b")).thenThrow(UnauthorizedException("Wrong credentials"))
val exception = intercept[UnauthorizedException] {
- new CrossHostApiCaller(List("a", "b", "c"), 0).call { str =>
+ CrossHostApiCaller(Vector("a", "b", "c"), 0, Some(0)).call { str =>
restClient.sendGet[String](str)
}
}
@@ -89,6 +135,17 @@ class CrossHostApiCallerSuite extends BaseTestSuite {
Mockito.verify(restClient, Mockito.never()).sendGet[String]("c")
}
}
+
+ "fail on not having Urls" when {
+ "none are provided" in {
+ val exception = intercept[IndexOutOfBoundsException] {
+ CrossHostApiCaller(Vector()).call { str =>
+ restClient.sendGet[String](str)
+ }
+ }
+ exception.getMessage should be ("0")
+ }
+ }
}
}
diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala
index 6c41f7bec..fce399bff 100644
--- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala
+++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/MenasConnectionStringParserSuite.scala
@@ -202,6 +202,24 @@ class MenasConnectionStringParserSuite extends BaseTestSuite {
exception.getMessage should be("Malformed Menas connection string")
}
}
- }
+ "keep the order of urls" when {
+ val expectedList = List(
+ "http://host1:8080/menas",
+ "http://host2:9000/menas",
+ "http://host3:8080/menas",
+ "http://host4:9000/menas",
+ "http://localhost:8080/menas",
+ "http://localhost:8090/menas"
+ )
+ "they are full fledged urls separated by semicolon" in {
+ val result = MenasConnectionStringParser.parse("http://host1:8080/menas;http://host2:9000/menas;http://host3:8080/menas;http://host4:9000/menas;http://localhost:8080/menas;http://localhost:8090/menas")
+ result should be(expectedList)
+ }
+ "varied hosts separated by comma within one url" in {
+ val result = MenasConnectionStringParser.parse("http://host1:8080,host2:9000,host3:8080,host4:9000,localhost:8080,localhost:8090/menas")
+ result should be(expectedList)
+ }
+ }
+ }
}
diff --git a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala
index 5d546150e..e1f2042d2 100644
--- a/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala
+++ b/dao/src/test/scala/za/co/absa/enceladus/dao/rest/RestDaoFactorySuite.scala
@@ -15,12 +15,15 @@
package za.co.absa.enceladus.dao.rest
+import org.mockito.MockitoSugar.withObjectMocked
+import org.mockito.{ArgumentMatchersSugar, Mockito}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import za.co.absa.enceladus.dao.UnauthorizedException
import za.co.absa.enceladus.dao.auth.{InvalidMenasCredentials, MenasKerberosCredentials, MenasPlainCredentials}
+import za.co.absa.enceladus.dao.rest.RestDaoFactory.AvailabilitySetup
-class RestDaoFactorySuite extends AnyWordSpec with Matchers {
+class RestDaoFactorySuite extends AnyWordSpec with Matchers with ArgumentMatchersSugar {
private val menasApiBaseUrls = List("http://localhost:8080/menas/api")
@@ -47,6 +50,44 @@ class RestDaoFactorySuite extends AnyWordSpec with Matchers {
exception.getMessage should be("No Menas credentials provided")
}
}
+ "properly adjusts the starting URL based on the setup type " when {
+ val fooCrossHostApiCaller = CrossHostApiCaller(Seq.empty)
+ val plainCredentials = MenasPlainCredentials("user", "changeme")
+ "when it's round-robin" in {
+ withObjectMocked[CrossHostApiCaller.type] {
+ Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller)
+ val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls)
+ getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient])
+ Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply(
+ menasApiBaseUrls,
+ CrossHostApiCaller.DefaultUrlsRetryCount,
+ None)
+ }
+ }
+ "when it's fallback" in {
+ withObjectMocked[CrossHostApiCaller.type] {
+ Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller)
+ val plainCredentials = MenasPlainCredentials("user", "changeme")
+ val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls, None, AvailabilitySetup.Fallback)
+ getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient])
+ Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply(
+ menasApiBaseUrls,
+ CrossHostApiCaller.DefaultUrlsRetryCount,
+ Option(0))
+ }
+ }
+ "when the setup type is not specified" in {
+ withObjectMocked[CrossHostApiCaller.type] {
+ Mockito.when(CrossHostApiCaller.apply(any[Seq[String]], any[Int], any[Option[Int]])).thenReturn(fooCrossHostApiCaller)
+ val restDao = RestDaoFactory.getInstance(plainCredentials, menasApiBaseUrls)
+ getAuthClient(restDao.restClient).getClass should be(classOf[LdapAuthClient])
+ Mockito.verify(CrossHostApiCaller, Mockito.times(1)).apply(
+ menasApiBaseUrls,
+ CrossHostApiCaller.DefaultUrlsRetryCount,
+ None)
+ }
+ }
+ }
}
private def getAuthClient(restClient: RestClient): AuthClient = {
@@ -54,5 +95,5 @@ class RestDaoFactorySuite extends AnyWordSpec with Matchers {
field.setAccessible(true)
field.get(restClient).asInstanceOf[AuthClient]
}
-
}
+
diff --git a/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala b/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala
index 5d8a0379a..793070d47 100644
--- a/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala
+++ b/data-model/src/test/scala/za/co/absa/enceladus/model/dataFrameFilter/DataFrameFilterSuite.scala
@@ -56,7 +56,7 @@ class DataFrameFilterSuite extends AnyFunSuite {
assert(filterExpr2.semanticEquals(expected))
}
- test("Three filters joined with an and condidion") {
+ test("Three filters joined with an and condition") {
val f1 = DiffersFilter("column1", "v1")
val f2 = DiffersFilter("column2", "v2")
val f3 = DiffersFilter("column3", "v3")
diff --git a/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js b/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js
index 53a97f6a2..9d640688a 100644
--- a/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js
+++ b/menas/ui/components/dataset/conformanceRule/ConformanceRuleDialog.js
@@ -29,6 +29,9 @@ class ConformanceRuleDialog {
this._ruleForms = new ConformanceRuleFormRepository(this);
this._rules = this._ruleForms.all;
+ this._schemaService = new SchemaService(this.model, eventBus);
+ this.filterEdit = new FilterEdit(sap.ui.getCore(), "MappingConformanceRule--", this._schemaService);
+
this.model.setProperty("/rules", this.rules);
this.model.setProperty("/dataTypes", this._ruleForms.byType("CastingConformanceRule").dataTypes);
}
@@ -105,6 +108,13 @@ class ConformanceRuleDialog {
let newRule = $.extend(true, {}, this.model.getProperty("/newRule"));
this.beforeSubmitChanges(newRule);
this.resetRuleValidation();
+
+ newRule.hasValidFilter = true; // default for non-MappingConformanceRules
+ if (newRule._t === "MappingConformanceRule") {
+ const validFilter = this.filterEdit.validateFilterData();
+ newRule.hasValidFilter = validFilter;
+ }
+
if (this.ruleForms.byType(newRule._t).isValid(newRule, this.controller._transitiveSchemas, currentDataset.conformance)) {
if (this.model.getProperty("/newRule/isEdit")) {
this.updateRule(currentDataset, newRule);
@@ -166,6 +176,10 @@ class ConformanceRuleDialog {
const model = new sap.ui.model.json.JSONModel(mappingTableSchema);
model.setSizeLimit(5000);
this._dialog.setModel(model, "mappingTableSchema");
+
+ const colNames = FilterEdit.extractFieldNamesInDepth(mappingTableSchema.fields);
+ const columnNamesModel = new sap.ui.model.json.JSONModel({columnNames: colNames});
+ this._dialog.setModel(columnNamesModel, "suggestedColumns");
}
});
const datasetSchema = this._dialog.getModel("schema").oData;
@@ -285,6 +299,8 @@ class ConformanceRuleDialog {
}
if (currentRule._t === "MappingConformanceRule") {
+ const filterModel = new sap.ui.model.json.JSONModel();
+
if (!currentRule.isEdit) {
newRule.newJoinConditions = [];
newRule.newOutputColumns = [];
@@ -310,9 +326,16 @@ class ConformanceRuleDialog {
newRule.newJoinConditions = aNewJoinConditions;
newRule.newOutputColumns = aNewOutputColumns;
+
+ const filters = [FilterTreeUtils.addNiceNamesToFilterData(newRule.mappingTableFilter)];
+ filterModel.setProperty("/editingFilters", filters);
+
}
this.mappingTableService.getAllVersions(newRule.mappingTable, sap.ui.getCore().byId("mappingTableVersionSelect"));
this.selectMappingTableVersion(newRule.mappingTable, newRule.mappingTableVersion);
+
+ this._dialog.setModel(filterModel, "filterEdit"); // filter editing has its own named model ("filterEdit")
+ this.filterEdit.bindFilterEditControls(this._dialog);
}
if (!newRule.isEdit && newRule.order === undefined) {
@@ -339,6 +362,19 @@ class ConformanceRuleDialog {
});
delete newRule.newOutputColumns;
delete newRule.joinConditions;
+
+ const updatedFilters = this._dialog.getModel("filterEdit").getProperty("/editingFilters");
+ if (updatedFilters) {
+ if (updatedFilters.length > 1) {
+ console.error(`Multiple root filters found, aborting: ${JSON.stringify(updatedFilters)}`);
+ sap.m.MessageToast.show("Invalid filter update found (multiple roots), no filter update done");
+ } else {
+ const cleanedFilter = FilterTreeUtils.removeDeletedNodesFromFilterData(updatedFilters[0]);
+ const updatedFilter = FilterTreeUtils.removeNiceNamesFromFilterData(cleanedFilter);
+ const schemaFilledFilter = this.filterEdit.applyValueTypesFromSchema(updatedFilter);
+ newRule.mappingTableFilter = schemaFilledFilter
+ }
+ }
}
}
@@ -372,6 +408,11 @@ class ConformanceRuleDialog {
resetRuleValidation() {
const newRule = this.model.getProperty("/newRule");
this.ruleForms.byType(newRule._t).reset();
+
+ if (newRule._t === "MappingConformanceRule") {
+ this.filterEdit.resetFilterValidation();
+ }
+
}
}
diff --git a/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js b/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js
index 891ce97b6..b27121f4a 100644
--- a/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js
+++ b/menas/ui/components/dataset/conformanceRule/ConformanceRuleForm.js
@@ -162,20 +162,7 @@ class CastingConformanceRuleForm extends ConformanceRuleForm {
}
get dataTypes() {
- return [
- {type: "boolean"},
- {type: "byte"},
- {type: "short"},
- {type: "integer"},
- {type: "long"},
- {type: "float"},
- {type: "double"},
- {type: "decimal(38,18)"},
- {type: "string"},
- {type: "date"},
- {type: "timestamp"},
- {type: "binary"}
- ]
+ return DataTypeUtils.dataTypesAsTypes;
}
get outputDataTypeControl() {
@@ -382,7 +369,8 @@ class MappingConformanceRuleForm extends ConformanceRuleForm {
isCorrectlyConfigured(rule) {
return this.hasValidInputColumn(rule.targetAttribute)
& this.hasValidOutputColumns(rule)
- & this.hasValidJoinConditions(rule.newJoinConditions);
+ & this.hasValidJoinConditions(rule.newJoinConditions)
+ & rule.hasValidFilter;
}
hasValidJoinConditions(fieldValue = []) {
diff --git a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml
index 33fd46501..8ce064040 100644
--- a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml
+++ b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/add.fragment.xml
@@ -17,4 +17,6 @@
+
+
diff --git a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/display.fragment.xml b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/display.fragment.xml
index 096afa429..a7c9a9675 100644
--- a/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/display.fragment.xml
+++ b/menas/ui/components/dataset/conformanceRule/MappingConformanceRule/display.fragment.xml
@@ -47,6 +47,10 @@
+
+
+
+
diff --git a/menas/ui/components/dataset/conformanceRule/upsert.fragment.xml b/menas/ui/components/dataset/conformanceRule/upsert.fragment.xml
index 91dc3aaf7..d9e98cb6a 100644
--- a/menas/ui/components/dataset/conformanceRule/upsert.fragment.xml
+++ b/menas/ui/components/dataset/conformanceRule/upsert.fragment.xml
@@ -15,7 +15,7 @@