From 61d6fb4c04d16946341176c8b2a131465ee7631d Mon Sep 17 00:00:00 2001 From: Sasa Zejnilovic Date: Wed, 5 May 2021 20:51:46 +0200 Subject: [PATCH 1/4] #109 make configs into case classes --- .../datasetComparison/DatasetComparator.scala | 5 ++--- .../datasetComparison/config/ManualConfig.scala | 14 ++++++++------ .../datasetComparison/config/TypesafeConfig.scala | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala index cd3cbca..94a5261 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala @@ -18,7 +18,7 @@ package za.co.absa.hermes.datasetComparison import org.apache.spark.sql.functions._ import org.apache.spark.sql.types.{StructField, StructType} -import org.apache.spark.sql.{Column, DataFrame, SparkSession} +import org.apache.spark.sql.{Column, DataFrame} import za.co.absa.commons.spark.SchemaUtils import za.co.absa.hermes.datasetComparison.config.{DatasetComparisonConfig, TypesafeConfig} import za.co.absa.hermes.utils.HelperFunctions @@ -42,8 +42,7 @@ class DatasetComparator(dataFrameReference: DataFrame, dataFrameActual: DataFrame, keys: Set[String] = Set.empty[String], config: DatasetComparisonConfig = new TypesafeConfig(None), - optionalSchema: Option[StructType] = None) - (implicit sparkSession: SparkSession) { + optionalSchema: Option[StructType] = None){ /** * Case class created for the single purpose of holding a pair of reference and tested data in any form together. diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala index 0ea0c84..a60de9a 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala @@ -16,9 +16,11 @@ package za.co.absa.hermes.datasetComparison.config -class ManualConfig( - val errorColumnName: String, - val actualPrefix: String, - val expectedPrefix: String, - val allowDuplicates: Boolean -) extends DatasetComparisonConfig {} +import com.typesafe.config.ConfigFactory + +case class ManualConfig( + errorColumnName: String, + actualPrefix: String, + expectedPrefix: String, + allowDuplicates: Boolean +) extends DatasetComparisonConfig diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala index 914d532..1c3ec20 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala @@ -18,7 +18,7 @@ package za.co.absa.hermes.datasetComparison.config import com.typesafe.config.{Config, ConfigFactory} -class TypesafeConfig(path: Option[String]) extends DatasetComparisonConfig { +case class TypesafeConfig(path: Option[String]) extends DatasetComparisonConfig { private val conf: Config = path match { case Some(x) => ConfigFactory.load(x) case None => ConfigFactory.load() From 9d427eee1e44b9a297695ddfa584e369e3d51dd8 Mon Sep 17 00:00:00 2001 From: Sasa Zejnilovic Date: Wed, 5 May 2021 20:58:44 +0200 Subject: [PATCH 2/4] #109 make configs into case classes --- .../co/absa/hermes/datasetComparison/config/ManualConfig.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala index a60de9a..416520f 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala @@ -16,8 +16,6 @@ package za.co.absa.hermes.datasetComparison.config -import com.typesafe.config.ConfigFactory - case class ManualConfig( errorColumnName: String, actualPrefix: String, From 67ad1a8c415817347e55aeb9b3d7b95b176c2b8d Mon Sep 17 00:00:00 2001 From: Sasa Zejnilovic Date: Wed, 5 May 2021 21:01:00 +0200 Subject: [PATCH 3/4] #109 make configs into case classes --- .../za/co/absa/hermes/datasetComparison/DatasetComparator.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala index 94a5261..22100b8 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala @@ -36,7 +36,6 @@ import za.co.absa.hermes.utils.HelperFunctions * @param optionalSchema Optional schema to cherry-pick columns form the two dataframes to compare. For example, if you * have a timestamp column that will never be the same, you provide a schema without that timestamp * and it will not be compared. - * @param sparkSession Implicit spark session. */ class DatasetComparator(dataFrameReference: DataFrame, dataFrameActual: DataFrame, From 016f9ca23a2e5108ac2c3472b94c4e77a6b1e987 Mon Sep 17 00:00:00 2001 From: Sasa Zejnilovic Date: Thu, 6 May 2021 12:12:32 +0200 Subject: [PATCH 4/4] #109 make config into one class in DC --- .../datasetComparison/DatasetComparator.scala | 3 +- .../DatasetComparisonConfig.scala | 36 ++++++++++++++----- .../DatasetComparisonJob.scala | 3 +- .../config/ManualConfig.scala | 24 ------------- .../config/TypesafeConfig.scala | 31 ---------------- .../DatasetComparatorSuite.scala | 31 ++-------------- ...ala => DatasetComparisonConfigSuite.scala} | 22 +++++++++--- .../config/ManualConfigSuite.scala | 35 ------------------ .../InfoFileComparisonConfig.scala | 8 ++--- 9 files changed, 53 insertions(+), 140 deletions(-) rename datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/{config => }/DatasetComparisonConfig.scala (53%) delete mode 100644 datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala delete mode 100644 datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala rename datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/{config/TypeSafeConfigSuite.scala => DatasetComparisonConfigSuite.scala} (59%) delete mode 100644 datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/ManualConfigSuite.scala diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala index 22100b8..0d08c75 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparator.scala @@ -20,7 +20,6 @@ import org.apache.spark.sql.functions._ import org.apache.spark.sql.types.{StructField, StructType} import org.apache.spark.sql.{Column, DataFrame} import za.co.absa.commons.spark.SchemaUtils -import za.co.absa.hermes.datasetComparison.config.{DatasetComparisonConfig, TypesafeConfig} import za.co.absa.hermes.utils.HelperFunctions /** @@ -40,7 +39,7 @@ import za.co.absa.hermes.utils.HelperFunctions class DatasetComparator(dataFrameReference: DataFrame, dataFrameActual: DataFrame, keys: Set[String] = Set.empty[String], - config: DatasetComparisonConfig = new TypesafeConfig(None), + config: DatasetComparisonConfig = DatasetComparisonConfig.default, optionalSchema: Option[StructType] = None){ /** diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/DatasetComparisonConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfig.scala similarity index 53% rename from datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/DatasetComparisonConfig.scala rename to datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfig.scala index 1c1de84..826e73f 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/DatasetComparisonConfig.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfig.scala @@ -14,15 +14,15 @@ * limitations under the License. */ -package za.co.absa.hermes.datasetComparison.config +package za.co.absa.hermes.datasetComparison +import com.typesafe.config.{Config, ConfigFactory} import scala.util.{Failure, Success, Try} -abstract class DatasetComparisonConfig { - val errorColumnName: String - val actualPrefix: String - val expectedPrefix: String - val allowDuplicates: Boolean +case class DatasetComparisonConfig(errorColumnName: String, + actualPrefix: String, + expectedPrefix: String, + allowDuplicates: Boolean) { def validate(): Try[DatasetComparisonConfig] = { @@ -38,9 +38,9 @@ abstract class DatasetComparisonConfig { } for { - _errColumnName <- validateColumnName(errorColumnName, "errorColumnName") - _actualPrefix <- validateColumnName(actualPrefix, "actualPrefix") - _expectedPrefix <- validateColumnName(expectedPrefix, "expectedPrefix") + _ <- validateColumnName(errorColumnName, "errorColumnName") + _ <- validateColumnName(actualPrefix, "actualPrefix") + _ <- validateColumnName(expectedPrefix, "expectedPrefix") } yield this } @@ -52,3 +52,21 @@ abstract class DatasetComparisonConfig { | Allow duplicities in dataframes (allowDuplicates) -> "$allowDuplicates"""".stripMargin } } + +object DatasetComparisonConfig { + def fromTypeSafeConfig(path: Option[String] = None): DatasetComparisonConfig = { + val conf: Config = path match { + case Some(x) => ConfigFactory.load(x) + case None => ConfigFactory.load() + } + + val errorColumnName: String = conf.getString("dataset-comparison.errColumn") + val actualPrefix: String = conf.getString("dataset-comparison.actualPrefix") + val expectedPrefix: String = conf.getString("dataset-comparison.expectedPrefix") + val allowDuplicates: Boolean = conf.getBoolean("dataset-comparison.allowDuplicates") + + DatasetComparisonConfig(errorColumnName, actualPrefix, expectedPrefix, allowDuplicates) + } + + def default: DatasetComparisonConfig = fromTypeSafeConfig() +} diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonJob.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonJob.scala index 29d7300..d85a20b 100644 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonJob.scala +++ b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonJob.scala @@ -22,7 +22,6 @@ import org.apache.spark.SPARK_VERSION import org.apache.spark.sql.SparkSession import org.apache.spark.sql.types.{DataType, StructType} import za.co.absa.hermes.datasetComparison.cliUtils.{CliParameters, CliParametersParser} -import za.co.absa.hermes.datasetComparison.config.{DatasetComparisonConfig, TypesafeConfig} import za.co.absa.hermes.datasetComparison.dataFrame.Utils import za.co.absa.hermes.utils.SparkCompatibility @@ -89,7 +88,7 @@ object DatasetComparisonJob { } def getConfig(configPath: Option[String]): DatasetComparisonConfig = { - val config = new TypesafeConfig(configPath).validate().get + val config = DatasetComparisonConfig.fromTypeSafeConfig(configPath).validate().get scribe.info(config.getLoggableString) config } diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala deleted file mode 100644 index 416520f..0000000 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/ManualConfig.scala +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2019 ABSA Group Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package za.co.absa.hermes.datasetComparison.config - -case class ManualConfig( - errorColumnName: String, - actualPrefix: String, - expectedPrefix: String, - allowDuplicates: Boolean -) extends DatasetComparisonConfig diff --git a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala b/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala deleted file mode 100644 index 1c3ec20..0000000 --- a/datasetComparison/src/main/scala/za/co/absa/hermes/datasetComparison/config/TypesafeConfig.scala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2019 ABSA Group Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package za.co.absa.hermes.datasetComparison.config - -import com.typesafe.config.{Config, ConfigFactory} - -case class TypesafeConfig(path: Option[String]) extends DatasetComparisonConfig { - private val conf: Config = path match { - case Some(x) => ConfigFactory.load(x) - case None => ConfigFactory.load() - } - - val errorColumnName: String = conf.getString("dataset-comparison.errColumn") - val actualPrefix: String = conf.getString("dataset-comparison.actualPrefix") - val expectedPrefix: String = conf.getString("dataset-comparison.expectedPrefix") - val allowDuplicates: Boolean = conf.getBoolean("dataset-comparison.allowDuplicates") -} diff --git a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparatorSuite.scala b/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparatorSuite.scala index ffaa7d8..237af9f 100644 --- a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparatorSuite.scala +++ b/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparatorSuite.scala @@ -20,11 +20,12 @@ import org.apache.spark.sql.Column import org.apache.spark.sql.types.{StringType, StructType} import org.scalatest.{BeforeAndAfterAll, FunSuite} import za.co.absa.hermes.datasetComparison.cliUtils.CliParameters -import za.co.absa.hermes.datasetComparison.config.ManualConfig import za.co.absa.hermes.datasetComparison.dataFrame.{Parameters, Utils} import za.co.absa.hermes.utils.SparkTestBase class DatasetComparatorSuite extends FunSuite with SparkTestBase with BeforeAndAfterAll { + private val manualConfig = DatasetComparisonConfig("errCol", "actual", "expected", true) + test("Test a positive comparison") { val cliOptions = new CliParameters( Parameters("csv", Map("delimiter" -> ","), getClass.getResource("/dataSample2.csv").toString), @@ -37,13 +38,6 @@ class DatasetComparatorSuite extends FunSuite with SparkTestBase with BeforeAndA val df1 = Utils.loadDataFrame(cliOptions.referenceDataParameters) val df2 = Utils.loadDataFrame(cliOptions.actualDataParameters) - val manualConfig = new ManualConfig( - "errCol", - "actual", - "expected", - true - ) - val expectedResult = ComparisonResult( 10, 10, 0, 0, 10, List( @@ -75,13 +69,6 @@ class DatasetComparatorSuite extends FunSuite with SparkTestBase with BeforeAndA val df1 = Utils.loadDataFrame(cliOptions.referenceDataParameters) val df2 = Utils.loadDataFrame(cliOptions.actualDataParameters) - val manualConfig = new ManualConfig( - "errCol", - "actual", - "expected", - true - ) - val expectedResult = ComparisonResult( 10, 10, 0, 0, 10, List( @@ -117,13 +104,6 @@ class DatasetComparatorSuite extends FunSuite with SparkTestBase with BeforeAndA val df1 = Utils.loadDataFrame(cliOptions.referenceDataParameters) val df2 = Utils.loadDataFrame(cliOptions.actualDataParameters) - val manualConfig = new ManualConfig( - "errCol", - "actual", - "expected", - true - ) - val schema = new StructType() .add("_c01", StringType, true) .add("_c1", StringType, true) @@ -148,13 +128,6 @@ class DatasetComparatorSuite extends FunSuite with SparkTestBase with BeforeAndA val df1 = Utils.loadDataFrame(cliOptions.referenceDataParameters) val df2 = Utils.loadDataFrame(cliOptions.actualDataParameters) - val manualConfig = new ManualConfig( - "errCol", - "actual", - "expected", - true - ) - val result = new DatasetComparator(df1, df2, cliOptions.keys, manualConfig).compare assert(9 == result.refRowCount) assert(10 == result.newRowCount) diff --git a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/TypeSafeConfigSuite.scala b/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfigSuite.scala similarity index 59% rename from datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/TypeSafeConfigSuite.scala rename to datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfigSuite.scala index 5c5face..84f310d 100644 --- a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/TypeSafeConfigSuite.scala +++ b/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/DatasetComparisonConfigSuite.scala @@ -14,13 +14,27 @@ * limitations under the License. */ -package za.co.absa.hermes.datasetComparison.config +package za.co.absa.hermes.datasetComparison import org.scalatest.FunSuite -class TypeSafeConfigSuite extends FunSuite { +class DatasetComparisonConfigSuite extends FunSuite { + test("Manual Config Correct") { + val conf = DatasetComparisonConfig("errCol", "_actual", "_expected", false) + assert(conf.validate().isSuccess) + assert("errCol" == conf.errorColumnName) + assert("_actual" == conf.actualPrefix) + assert("_expected" == conf.expectedPrefix) + assert(!conf.allowDuplicates) + } + + test("Manual Config Bad Column Name") { + val conf = DatasetComparisonConfig("errCol", "_actua l", "_expected", false) + assert(conf.validate().isFailure) + } + test("Default Config Loaded") { - val conf = new TypesafeConfig(None) + val conf = DatasetComparisonConfig.default assert("errCol" == conf.errorColumnName) assert("actual" == conf.actualPrefix) assert("expected" == conf.expectedPrefix) @@ -28,7 +42,7 @@ class TypeSafeConfigSuite extends FunSuite { } test("Config with provided path loaded") { - val conf = new TypesafeConfig(Some("confData/application.conf")) + val conf = DatasetComparisonConfig.fromTypeSafeConfig(Some("confData/application.conf")) assert("errCol2" == conf.errorColumnName) assert("actual2" == conf.actualPrefix) diff --git a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/ManualConfigSuite.scala b/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/ManualConfigSuite.scala deleted file mode 100644 index f62cfbd..0000000 --- a/datasetComparison/src/test/scala/za/co/absa/hermes/datasetComparison/config/ManualConfigSuite.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 ABSA Group Limited - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package za.co.absa.hermes.datasetComparison.config - -import org.scalatest.FunSuite - -class ManualConfigSuite extends FunSuite { - test("Manual Config Correct") { - val conf = new ManualConfig("errCol", "_actual", "_expected", false) - assert(conf.validate().isSuccess) - assert("errCol" == conf.errorColumnName) - assert("_actual" == conf.actualPrefix) - assert("_expected" == conf.expectedPrefix) - assert(!conf.allowDuplicates) - } - - test("Manual Config Bad Column Name") { - val conf = new ManualConfig("errCol", "_actua l", "_expected", false) - assert(conf.validate().isFailure) - } -} diff --git a/infoFileComparison/src/main/scala/za/co/absa/hermes/infoFileComparison/InfoFileComparisonConfig.scala b/infoFileComparison/src/main/scala/za/co/absa/hermes/infoFileComparison/InfoFileComparisonConfig.scala index 9e2bb7f..421cb01 100644 --- a/infoFileComparison/src/main/scala/za/co/absa/hermes/infoFileComparison/InfoFileComparisonConfig.scala +++ b/infoFileComparison/src/main/scala/za/co/absa/hermes/infoFileComparison/InfoFileComparisonConfig.scala @@ -35,12 +35,12 @@ object InfoFileComparisonConfig { case None => ConfigFactory.load() } - import collection.JavaConversions._ + import collection.JavaConverters._ - val versionMetaKeys = conf.getStringList("info-file-comparison.atum-models.versionMetaKeys").toList - val keysToIgnore = conf.getStringList("info-file-comparison.atum-models.ignoredMetaKeys").toList + val versionMetaKeys = conf.getStringList("info-file-comparison.atum-models.versionMetaKeys").asScala.toList + val keysToIgnore = conf.getStringList("info-file-comparison.atum-models.ignoredMetaKeys").asScala.toList InfoFileComparisonConfig(versionMetaKeys, keysToIgnore) } - def empty: InfoFileComparisonConfig = InfoFileComparisonConfig(List.empty[String], List.empty[String]) + val empty: InfoFileComparisonConfig = InfoFileComparisonConfig(List.empty[String], List.empty[String]) }