{"id":18810415,"url":"https://github.com/absaoss/commons","last_synced_at":"2025-10-28T02:14:54.284Z","repository":{"id":37482143,"uuid":"181226710","full_name":"AbsaOSS/commons","owner":"AbsaOSS","description":"Selection of useful reusable components","archived":false,"fork":false,"pushed_at":"2023-08-01T14:54:25.000Z","size":290,"stargazers_count":8,"open_issues_count":3,"forks_count":1,"subscribers_count":19,"default_branch":"master","last_synced_at":"2024-04-12T07:05:50.768Z","etag":null,"topics":["commons","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AbsaOSS.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-13T20:43:46.000Z","updated_at":"2022-12-14T22:59:36.000Z","dependencies_parsed_at":"2023-01-23T20:01:07.861Z","dependency_job_id":null,"html_url":"https://github.com/AbsaOSS/commons","commit_stats":null,"previous_names":[],"tags_count":47,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AbsaOSS%2Fcommons","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AbsaOSS%2Fcommons/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AbsaOSS%2Fcommons/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AbsaOSS%2Fcommons/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AbsaOSS","download_url":"https://codeload.github.com/AbsaOSS/commons/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223603290,"owners_count":17172077,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["commons","scala"],"created_at":"2024-11-07T23:20:08.307Z","updated_at":"2025-10-28T02:14:54.225Z","avatar_url":"https://github.com/AbsaOSS.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"ABSA Commons\n===\nSelection of useful reusable components\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa.commons/commons_2.12/badge.svg)](https://search.maven.org/search?q=g:za.co.absa.commons)\n[![TeamCity build](https://teamcity.jetbrains.com/app/rest/builds/aggregated/strob:%28locator:%28buildType:%28id:OpenSourceProjects_AbsaOSS_Commons_AutoBuildWithScala212%29,branch:master%29%29/statusIcon.svg)](https://teamcity.jetbrains.com/viewType.html?buildTypeId=OpenSourceProjects_AbsaOSSSpline_AutomaticBuildsWithTests_Spark24\u0026branch=develop\u0026tab=buildTypeStatusDiv)\n[![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=AbsaOSS_commons\u0026metric=alert_status)](https://sonarcloud.io/dashboard?id=AbsaOSS_commons)\n[![SonarCloud Maintainability](https://sonarcloud.io/api/project_badges/measure?project=AbsaOSS_commons\u0026metric=sqale_rating)](https://sonarcloud.io/dashboard?id=AbsaOSS_commons)\n[![SonarCloud Reliability](https://sonarcloud.io/api/project_badges/measure?project=AbsaOSS_commons\u0026metric=reliability_rating)](https://sonarcloud.io/dashboard?id=AbsaOSS_commons)\n[![SonarCloud Security](https://sonarcloud.io/api/project_badges/measure?project=AbsaOSS_commons\u0026metric=security_rating)](https://sonarcloud.io/dashboard?id=AbsaOSS_commons)\n\n---\n\n# Building\n\n### Switch the codebase to the required Scala version.\nBy default, Scala 2.11 is used. To build _Commons_ for another Scala version, switch to the required Scala version first. \n```shell\n# E.g. to switch to Scala 2.13 use\nmvn scala-cross-build:change-version -Pscala-2.13\n```\n\n### Build the project\nWhen building the project activate a Scala profile corresponding to the Scala version of the codebase.\n```shell\n# E.g. for Scala 2.13 use\nmvn clean install -Pscala-2.13\n```\n\n### Building for all supported Scala versions\n```shell\n./build-all.sh\n```\n\n### Measuring code coverage\n```shell\n./mvn clean verify -Pcode-coverage \n```\nCode coverage will be generated on path:\n```\n{local-path}\\commons\\target\\jacoco\n```\n\n# Type extensions\n## AnyExtension\n```scala\nimport za.co.absa.commons.lang.extensions.AnyExtension._\n\n// Optionally call a method in a chain\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .optionally(_.withZ, maybeZ) // \u003c---- withZ is called with a `value` if `maybeZ` is `Some(value)`\n  .optionally(_.withABC(a, _, c), maybeB) // \u003c---- it also works with n-ary methods\n\n// Alternatively `having()` method can be used. It does the same thing as `optionally()`,\n// but because of re-arranged method parameters it's easier for the compiler to infer types.\n// See: https://github.com/AbsaOSS/commons/issues/56\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .having(maybeZ)(_.withZ)\n  \n// Conditionally call a method in a chain\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .when(shouldIncludeZ)(_.withZ(88))\n\n// ...same as `when()` but with inverted condition\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .unless(shouldExcludeZ)(_.withZ(88))\n```\n## ArrayExtension\n```scala\nimport za.co.absa.commons.lang.extensions.ArrayExtension._\n\nval arr = Array(1, 2, 3)\n\n// removes duplicates in an array;\n// an argument to distinctBy is a function which projects each array value into another value\n// that is used to determine whether two elements are duplicated\nval duplicatesEliminated = arr.distinctBy(identity)\n```\n## IteratorExtension\n```scala\nimport za.co.absa.commons.lang.extensions.IteratorExtension._\n\nval iter: Iterator[_] = ???\nval arr: Array[_] = ???\n\n// copy 42 items from the `iter` to the `arr` with array offset 7\niter.fetchToArray(arr, 7, 42) // returns a number of actually copied items\n```\n## NonOptionExtension\n```scala\nimport za.co.absa.commons.lang.extensions.NonOptionExtension._\n\n// returns the object as Some(_) if anyNonOptionObject is not null, None otherwise\nanyNonOptionObject.toOption\n```\n## OptionExtension\n```scala\nimport za.co.absa.commons.lang.extensions.OptionExtension._\n\nval someOption = Some(\"abc\")\n\n// returns Success(\"abc\")\nsomeOption.toTry(new Exception)\n\nval noneOption = None\nval e = new Exception\n\n// returns Failure(e)\nsomeOption.toTry(e)\n```\n## SeqExtension\n```scala\nimport za.co.absa.commons.lang.extensions.SeqExtension._\n\nSeq(1, 2, 2, 2, 1).groupConsecutiveBy[Int](a =\u003e a) // Seq(Seq(1), Seq(2, 2, 2), Seq(1))\nSeq(1, 2, 2, 2, 1).groupConsecutiveBy[Int](a =\u003e 1) // Seq(Seq(1, 2, 2, 2, 1))\nSeq(1, 24, 27, 2, 1).groupConsecutiveBy[Int](a =\u003e a.toString.length) // Seq(Seq(1), Seq(24, 27), Seq(2, 1))\n\nSeq(1, 2, 2, 2, 1).groupConsecutiveByPredicate(a =\u003e a == 2) // Seq(Seq(1), Seq(2, 2, 2), Seq(1))\nSeq(1, 1, 2, 2, 4, 1).groupConsecutiveByPredicate(a =\u003e a == 2) // Seq(Seq(1, 1), Seq(2), Seq(2), Seq(4), Seq(1))\n\nSeq(1, 24, 27, 2, 1).groupConsecutiveByOption[Int](\n  a =\u003e if(a.toString.length \u003e 1) Some(a.toString.length) else None\n) // Seq(Seq(1), Seq(24, 27), Seq(2), Seq(1))\n```\n## StringExtension\n```scala\nimport za.co.absa.commons.lang.extensions.StringExtension._\n\n\"abcba\".replaceChars(Map('a' -\u003e 'b', 'b' -\u003e 'a')) // \"bacab\"\n\n\"Hello world\".findFirstUnquoted(Set('w', 'e', 'l'), Set.empty) // Some(1)\n\"Hello world\".findFirstUnquoted(Set('w'), Set.empty) // Some(6)\n\"Hello world\".findFirstUnquoted(Set('a'), Set.empty) // None\n\"Hello `w`orld\".findFirstUnquoted(Set('w'), '`') // None\n\"Hello `world\".findFirstUnquoted(Set('w'), '`') // Some(7)\n\"`Hello` \\\\'world\".findFirstUnquoted(Set('w', 'e', 'l'), Set('\\'', '`')) // Some(10)\n\n\"Hello world\".hasUnquoted(Set('w', 'e', 'l'), Set('`')) // true\n\"`Hello world`\".hasUnquoted(Set('w', 'e', 'l'), Set('`')) // false\n\n\"Lorem i ipsum\".countUnquoted(Set('o', 'i'), Set.empty) // Map('o' -\u003e 1, 'i' -\u003e 2)\n\"Lorem `i` ipsum\".countUnquoted(Set('o', 'i'), Set('`')) // Map('o' -\u003e 1, 'i' -\u003e 1)\n\n\"aaa\" / \"123\" // \"aaa/123\"\n\"aaa/\" / \"123\" // \"aaa/123\"\n\n\"a\".nonEmptyOrElse(\"b\") // \"a\"\n\"\".nonEmptyOrElse(\"b\") // \"b\"\n\n\"\".coalesce(\"A\", \"\") // \"A\"\n\"\".coalesce(\"\", \"\", \"B\", \"\", \"C\") // \"B\"\n\"X\".coalesce(\"Y\", \"Z\") // \"X\"\n\n(null: String).nonBlankOption // None\n\"            \".nonBlankOption // None\n\" foo bar 42 \".nonBlankOption // Some(\" foo bar 42 \")\n```\n## TraversableExtension\n```scala\nimport za.co.absa.commons.lang.extensions.TraversableExtension._\n\nTraversable(1, 2, 3).toNonEmptyOption // Some(Traversable(1, 2, 3))\nTraversable().toNonEmptyOption // None\n```\n## TraversableOnceExtension\n```scala\nimport za.co.absa.commons.lang.extensions.TraversableOnceExtension._\n\nList(1, 2).distinctBy(identity) // List(1, 2)\nList(1, 2, 1).distinctBy(identity) // List(1, 2)\nList(1, 2, 1, 0, 5).distinctBy(a =\u003e a % 2) // List(1, 2)\n```\n\n\n# Collection implicits\n\n**Warning**: these are deprecated.\n\nUse type-specific `...Extension` instead, for example, `za.co.absa.commons.lang.extensions.IteratorExtension`.\n\n```scala\nimport CollectionImplicits._\n\nval iter: Iterator[_] = ???\nval arr: Array[_] = ???\n\n// copy 42 items from the `iter` to the `arr` with array offset 7\niter.fetchToArray(arr, 7, 42) // returns a number of actually copied items\n```\n\n```scala\nimport CollectionImplicits._\n\n// Get distinct elements by only comparing certain property(-es)\nval xs = Seq(\n   Foo(x = 1, ...), // A\n   Foo(x = 2, ...), // B\n   Foo(x = 1, ...), // C\n   Foo(x = 2, ...), // D\n   Foo(x = 3, ...), // E\n)\nxs.distinctBy(_.x) // returns elements A, B, E\n```\n\n# Graph Utils\n\n### Topological sorting (DAG only)\n```scala\nval myNodes: Seq[MyNode] = ??? // an arbitrary sequence of objects that can represent graph nodes \n\n// import extension methods\nimport za.co.absa.commons.graph.GraphImplicits._\n\nval sortedNodes = myNodes.sortedTopologicallyBy(_.id, _.refIds) // arguments are functions that return a self ID and outbound IDs for every node in the collection \n\n// ... or using implicit `DAGNodeIDMapping` instance instead of explicitly passing mapping functions as arguments\n\nimplicit object MyNodeIdMapping extends DAGNodeIdMapping[MyNode, NodeId] {\n   override def selfId(n: MyNode): NodeId = ???\n   override def refIds(n: MyNode): Traversable[NodeId] = ???\n}\n\nval sortedNodes = myNodes.sortedTopologically()\n```\n\n# Abstract Converters\nA simple stackable `Converter` trait with a simple memoized wrapper.\n### Usage \n```scala\n// 1. Define your converter\nclass AlchemicalConverter extends Converter {\n  override type From = Lead\n  override type To   = Gold\n  override def convert(arg: Lead): Gold = ??? // treat with dragon eyes and cosmic rays\n}\n\n// 2. Instantiate it\nval forge = new AlchemicalConverter\n// or a memoized variant\nval forge = new AlchemicalConverter with CachingConverter\n\n// 3. Use it\nforge.convert(... some lead ...) // returns some gold\n```\n\n# Type constraints\nUtility object that defines extended type constraints to be used in Scala type definitions.\nIn particular it defines a `not` type constraint.\n### Usage\n```scala\ntrait VegetarianMenu {\n  def add[A \u003c: Food : not[Meat]#λ](food: A)\n}\n```\n\n# Option implicits\n**Warning**: these are deprecated.\n\nUse type-specific `...Extension` instead, for example, `za.co.absa.commons.lang.extensions.StringExtension`.\n\n```scala\n// Strings\n(null: String).nonBlankOption // == None\n\"  \\t \\n \\r  \".nonBlankOption // == None\n\"foo bar\".nonBlankOption // == Some(\"foo bar\")\n\n// Collections\nSeq.empty.asOption // == None\nSeq(1, 2).asOption // == Some(Seq(1, 2))\n\n// Just another way of doing Option(foo)\nfoo.asOption\n\n// Optionally call a method in a chain\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .optionally(_.withZ, maybeZ) // \u003c---- withZ is called with a `value` if `maybeZ` is `Some(value)`\n  .optionally(_.withABC(a, _, c), maybeB) // \u003c---- it also works with n-ary methods\n\n// Alternatively `having()` method can be used. It does the same thing as `optionally()`,\n// but because of re-arranged method parameters it's easier for the compiler to infer types.\n// See: https://github.com/AbsaOSS/commons/issues/56\nnew MyBuilder\n  .withX(42)\n  .withY(77)\n  .having(maybeZ)(_.withZ)\n  ...\n```\n\n# UrisConnectionStringParser\nParses a connection string containing one or multiple URIs into a list of strings (each being one URI).\nInput URIs are supposed to have semi-colon-separated base URIs, and each can have multiple comma-separated hosts.\n```scala\nval connectionString = \"https://localhost:8080,host2:8080/rest_api;http://localhost:9000/rest_api\"\n\nUrisConnectionStringParser.parse(connectionString)\n// List(\"https://localhost:8080/rest_api\", \"https://host2:8080/rest_api\", \"http://localhost:9000/rest_api\")\n```\n\n# Commons Configuration\n### Implicits\nUseful methods for `org.apache.commons.configuration.Configuration`.\n\n```scala\nimport za.co.absa.commons.config.ConfigurationImplicits._\n\n// return value or throw\nconf.getRequiredInt(\"property.key\")\n\n// return Some(value) or None\nconf.getOptionalInt(\"property.key\")\n\n// return Map(\"conf.key1\" -\u003e 123, \"conf.key2\" -\u003e 456)\nconf.toMap[Int]\n\n```\nAvailable for String, Array[String], Boolean, BigDecimal, Byte, Short, Int, Float, Long and Double.\n\n### Configuration sub-classes\n\n##### UpperSnakeCaseEnvironmentConfiguration\nThis is an extension of `EnvironmentConfiguration` that converts key names between \ndot-separated _camelCase_ notation and _UPPER_SNAKE_CASE_ notation which is common for naming environment variables.\n\nSee: https://github.com/AbsaOSS/commons/issues/54\n```scala\n// Any of the following calls returns a value of FOO_BAR_BAZ environment variable\n(new UpperSnakeCaseEnvironmentConfiguration).getString(\"foo.bar.baz\")\n(new UpperSnakeCaseEnvironmentConfiguration).getString(\"fooBarBaz\")\n(new UpperSnakeCaseEnvironmentConfiguration).getString(\"foo.barBaz\")\n```\n\n### Typed Configuration\n\nTrait `ConfTyped` provides a DSL for creating a typed hierarchical configuration object.\nIt provides access to two main abstractions: `Conf` and `Prop`\n\nExample:\n\n```scala\nimport za.co.absa.commons.config._\n\nobject MyAppConfig extends ConfTyped {\n   val confSource: java.util.Properties = ???\n\n   override val rootPrefix = \"com.example\"\n\n   object Foo extends Conf(\"foo\") {\n      object Bar extends Conf(\"bar\") {\n         val baz: String = confSource getProperty Prop(\"baz\")\n         val qux: String = confSource getProperty Prop(\"qux\")\n      }\n   }\n}\n\n// somewhere in your application\n\nimport MyAppConfig._\n\nval baz = Foo.Bar.baz // mapped to the key \"com.example.foo.bar.baz\" in the \u003ccode\u003econfSource\u003c/code\u003e\nval baz = Foo.Bar.qux // mapped to the key \"com.example.foo.bar.qux\" in the \u003ccode\u003econfSource\u003c/code\u003e\n```\n\nNote that `ConfTyped` doesn't impose or depend on the way how the configuration values are loaded.\nIt only provides a convenient way to implicitly construct the configuration key names from the nested object structure.\n\nThe key names are obtained by calling `Prop(\"...\")` method.\nIt returns a full property key name that reflects the nesting structure of the `Conf` instances' names,\nconcatenated with dot (`.`) and prefixed with the `rootPrefix` if one is provided.\n\nAnother example of usage `ConfTyped`:\n```scala\nval props = new java.util.Properties with ConfTyped {\n   val foo = new Conf(\"foo\") {\n      val bar = new Conf(\"bar\") {\n         lazy val baz = getProperty(Prop(\"baz\"))\n      }\n   }\n}\n\nprops.put(\"foo.bar.baz\", \"42\")\n\nprintln(props.foo.bar.baz) // prints 42\n```\n\n# Reflection Utils\n### Basics\n##### Get direct sub-types of a sealed type\n```scala\nReflectionUtils.directSubClassesOf[Food] // == Seq(classOf[Vegetables], classOf[Meat], classOf[Fish])\n```\n##### Get `object` instances extending a sealed type\n```scala\nReflectionUtils.objectsOf[Currency] // == Seq(classOf[EUR], classOf[USD], classOf[CZK])\n```\n##### Get `object` instance by it's full type name (similar to `Class.forName(...)`, but for objects)\n```scala\nReflectionUtils.objectForName[MySingleton](\"com.example.MySingleton\") // == MySingleton\n```\n##### `objectForName` with more descriptive exception message in case there is something wrong with provided name.\n```scala\nReflectionUtils.objectForNameWithDescriptiveException[MySingleton](\"com.example.MySingleton\") // == MySingleton\n```\n##### Get private field value of an arbitrary class. (a typed variant of `field.get(o).asInstanceOf[T]`)\n```scala\nReflectionUtils.extractFieldValue[Int](foo, \"bar\")\n// or if you know a type where the field is declared\nReflectionUtils.extractFieldValue[Doh, Int](foo, \"bar\")\n```\n##### Extract object properties as a key-value map\n```scala\ncase class Person(name: String, age: Int, sex: Sex)\nval aPerson = Person(\"Alex\", 41, Male)\n\nReflectionUtils.extractProperties(aPerson) // == Map(\"name\" -\u003e \"Alex, \"age\" -\u003e 42, \"sex\" -\u003e Male)\n```\n##### Extract a case class argument default value (if exists)\n```scala\ncase class Button(title: String, isPressed = false)\nReflectionUtils.caseClassCtorArgDefaultValue[Int](classOf[Button], \"name\") // == None\nReflectionUtils.caseClassCtorArgDefaultValue[Int](classOf[Button], \"isPressed\") // == Some(false)\n```\n\n##### Get all interfaces/traits of a given class including inherited ones\n```scala\nReflectionUtils.ReflectionUtils.allInterfacesOf[A]\n// or\nReflectionUtils.ReflectionUtils.allInterfacesOf(aClass)\n```\n\n### Enumeration macros\n##### Obtain all instances of a sealed trait\nCan be used to e.g. in a _Case Object Enumeration_ pattern.\n\nA similar solution and the motivation is well describes in the\n[Scala Enumerations hell](https://medium.com/@yuriigorbylov/scala-enumerations-hell-5bdba2c1216) article.\nBut unlike the above approach `EnumerationMacros.sealedInstancesOf` utilizes Scala compiler macros,\nso that the instances are lookup at the compile time.  \n\n```scala\n  sealed trait Color\n  \n  object Color {\n    // returns Set(Red, Green, Blue)\n    val values: Set[Color] = EnumerationMacros.sealedInstancesOf[Color]\n\n    case object Red extends Color\n    case object Green extends Color\n    case object Blue extends Color\n  }\n``` \n\n### Run-time compilation\nIf you wants some code to be linked and executed at run-time, here's a simple way to do it:\n```scala\nval fn = ReflectionUtils.compile(q\"\"\" some scala code \"\"\")\nfn()\n```\nOf with input parameters:\n```scala\nval fn = ReflectionUtils.compile(q\"\"\"\n  val foo = arg(\"foo\")\n  val bar = arg(\"bar\")\n  foo + bar\n  \"\"\")\nfn(Map(\"foo\" -\u003e ..., \"bar\" -\u003e ...))\n```\n\n### Run-time value extractors\nSometimes you need to support different versions of some library with a breaking changes in API.\nIf there aren't too many breaking changes, or you only use a certain subset of an API then creating a proper adapter layer could be an overkill.\nIn that case simple run-time evaluation could be a decent alternative:\n\n##### Getting a value of from an accessor by name\n```scala\nobject FilenameExtractor extends AccessorMethodValueExtractor[String](\"filename\", \"name\", \"file\")\n\nval FilenameExtractor(fileName) = someObjectRepresentingAFile\n// The first matching accessor name with type wins\n```\n\n##### Safely matching on a type that might be missing at run-time.\nIf you try to pattern-match on a type that is missing from the classpath at runtime (e.g. optional dependency) you'll get `NoClassDefFoundError`. Though it looks strange as from the use case perspective if there is no `Foo` class there couldn't be a `Foo` instance. Logically one would expect it to just not match, but in fact it throws.\n```scala\naObject match {\n  ...\n  case foo: Foo =\u003e // \u003c----- this could throw NoClassDefFoundError !!\n  ...\n}\n```\nTo get a desired behavior you can use `SafeTypeMatchingExtractor`:\n```scala\nobject FooExtractor extends SafeTypeMatchingExtractor(\"com.example.Foo\")\n\naObject {\n  ...\n case FooExtractor(foo) =\u003e // do something with `foo`\n  ...\n}\n```\nor you can even make a fancy DSL from it:\n```scala\nobject `_: Foo` extends SafeTypeMatchingExtractor(\"com.example.Foo\")\n\naObject {\n  ...\n  case `_: Foo`(foo) =\u003e // do something with `foo`\n  ...\n}\n```\n\n\n# A project Build Info Utils\nA singleton that parses `build.properties` from the classpath and return version and timestamp as constants.\n### Usage\nCopy `build.properties.template` file and paste in into your classpath root as `build.properties`. Make sure the resource filtering is enabled on your project build.\nThen you can access it's content as simply as this:\n```scala\nBuildInfo.Version // returns `build.version` property from the `build.properties` file\nBuildInfo.Timestamp // returns `build.timestamp` property from the `build.properties` file\nBuildInfo.BuildProps // returns entire `build.properties` content as immutable Java `Properties`\n```\n### Other ways of usage \u0026 customization\nIf needed, you can customize a `build.properties` resource path and/or the property mapping.\n\n##### Custom `.properties` file\n```scala\n// loads '/foo/bar.properties' from the classpath\nobject MyBuildInfo extends BuildInfo(resourcePrefix = \"/foo/bar\")\n```\n##### Custom property mapping\n```scala\nobject MyBuildInfo extends BuildInfo(propMapping = PropMapping(\n  version = \"bld.ver\",  // binds \"Version\" field to \"bld.ver\" property\n  timestamp = \"bld.ttt\" // binds \"Timestamp\" field to \"bld.ttt\" property\n))\n```\n\nYou can also use `apply()` method instead of inheritance. It all depends on your preferred code style:\n\n```scala\nval myBuildInfo = BuildInfo(...)\n```\n\n# Error handling utils\n\n### Client/Server error cross-linking\nIn client-server application the errors sent to a client is often sanitised for privacy and security reasons.\nThis however complicates troubleshooting because it's difficult to find a much between a client error message\nand the corresponding exception details in the server logs.\n\nOne way to solve this issue is to generate a unique identifier that is then incorporated into\nthe server log on one hand, and is sent to the client along with a client friendly error message on the other hand.\nSuch unique ID will be easy to lookup in logs, and will precisely identify the root cause of the error seen by the client.\n\n```scala\n// somewhere on the server\ntry {\n  service.doSomething()\n} catch {\n  case NonFatal(e) =\u003e\n    import za.co.absa.commons.error\n    val errorRef = ErrorRef(e, \"oops!\")   \n    clientResponse.sendError(errorRef)\n}\n```\nThis way the exception `e` is silently logged into the server logs with the message\n```\n[ERROR] ... ERROR_ID [123e4567-e89b-12d3-a456-426614174000] oops!\n            caused by: NullPointerException in ...\n            \u003cstack trace\u003e\n```\n... while the client receives a serialized representation that only contains the error UUID, timestamp and the message \"oops!\".\n\nFor example:\n```json\n{\n   \"errorId\": \"123e4567-e89b-12d3-a456-426614174000\",\n   \"timestamp\": 1611945666787,\n   \"message\": \"oops!\"\n}\n```\n\n# IO Utils\n\n## Temporary file/directory\nAn easy way to create a temporary file or directory with the support for automatic recursive deletion (as `rm -rf`) on JVM shutdown.\n### Usage\n```scala\nval myTmpFile: Path = TempFile.deleteOnExit.path\nval myTmpDir: Path = TempDirectory.deleteOnExit.path\n\nval myTmpFile: String = TempFile.deleteOnExit.asString\nval myTmpDir: String = TempDirectory.deleteOnExit.asSTring\n\nval myTmpFile: URI = TempFile.deleteOnExit.toURI\nval myTmpDir: URI = TempDirectory.deleteOnExit.toURI\n```\nIt also mimics Java IO API for a similar purpose\n```scala\nTempFile(\"myPrefix\", \"mySuffix\")\n```\n\n## LocalFileSystemUtils\nAn object containing useful functions that operate on local file system.\n### Usage\n```scala\nval doesExist = LocalFileSystemUtils.localExists(\"/user/u1/somefile\") // true if this file exists, false otherwise\n\nif(doesExist) {\n  val fileContent = LocalFileSystemUtils.readLocalFile(\"/user/u1/somefile\") // full file as string\n}\n\nval tildeReplaced = LocalFileSystemUtils.replaceHome(\"~/Projects/somedir\") // path with replaces tilde with home directory path\n```\n\n# JSON (Json4s) Utils\nA set of stackable traits, serving a wrapper around the way how Json4s (de)serializers are created. Instead of relying on implicit `Formats` objects a stackable traits are used.\nThis API is also binary compatible to Json4s 3.2 and 3.3+ versions (Jackson and Native)\n### Usage\nThere are two default SerDe implementation that you can use out of the box:\n  - `DefaultJacksonJsonSerDe`\n  - `DefaultNativeJsonSerDe`\n\n```scala\nclass MyApp extends App with DefaultJacksonJsonSerDe {\n  FooBar.toJson // returns JSON string\n  FooBar.toPrettyJson // returns formatted JSON string\n  \"{...}\".fromJson[FooBar] // returns a FooBar instance\n}\n```\nOr you can create a singleton and use that instead:\n```scala\nobject JsonSerDe extends DefaultNativeJsonSerDe\nimport JsonSerDe._\nfooBar.toJson\n```\nIf you want another parser impl, then you do this:\n```scala\nobject MyJsonSerDe \n  extends AbstractJsonSerDe[MyJson]\n  with my.JsonMethods\n  with DefaultFormatsBuilder\n\nimport MyJsonSerDe._\nfooBar.toJson\n```\nIf you want custom formats then instead of mixing in `DefaultFormatsBuilder` simply override `def formats` method.\n\n# Version Utils\nA simple utility that parses version strings.\nIt supports SemVer 2.0 as well as a simple dot-separated version format.\nCan be used to compare the versions, for instance when implementing version predicates.\n\n### Example\n\n```scala\nimport Version._\n\nrequire(Version.asSimple(SPARK_VERSION) \u003e ver\"2.4\")\n// or\nrequire(Version.asSemVer(SomeLibVersion) \u003e semver\"1.2.3-beta.2\")\n```\n\nTo get a string representation from a `Version` instance `asString` extension method can be used:\n\n```scala\nval myVer: Version = semver\"1.2.3-beta.2+777.42\"\nmyVer.asString  // returns \"1.2.3-beta.2+777.42\"\n```  \n\nSemantic Versioning specific operations:\n\n```scala\nimport Version._\n\nval myVer = semver\"111.222.333-alpha.444+build.555\"\n\nmyVer.major      == 111\nmyVer.minor      == 222\nmyVer.patch      == 333\nmyVer.core       == semver\"111.222.333\"\nmyVer.preRelease == ver\"alpha.444\"\nmyVer.buildMeta  == ver\"build.555\"\n```\n\n# S3 Utils\n\n### S3 Location Utils\nProvides simple means of checking a string to appear to be a valid S3 Location and parsing it into a `S3Location`.\nThat way, one can easily obtain the `protocol`, `bucketName`, and `path`.\n- recognized `protocol`s are `s3`, `s3n` and `s3a`\n- `bucketName` is checked according to the\n  [official S3 Bucket naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules)\n  and [official S3 Access Point Alias naming rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-access-points.html)\n- `path` content is not checked in any way\n\n```scala\nimport za.co.absa.commons.s3._\nimport za.co.absa.commons.s3.SimpleS3Location._\n\n\"s3a://mybucket.some.where/my/path1\".isValidS3Path // yields true\n\nval s3loc: S3Location = \"s3://mybucket-123/path/to/file.ext\".toSimpleS3Location.get\ns3loc.protocol // holds \"s3\"\ns3loc.bucketName // holds \"mybucket-123\"\ns3loc.path // holds \"path/to/file.ext\"\n\n\"s3x://bogus#$%/xxx\".toSimpleS3Location // yields None\n\n\"s3a://mybucket.some/my/path1\".withTrailSlash // yields SimpleS3Location(\"s3a\", \"mybucket.some\", \"my/path1/\")\n\"s3a://mybucket.some/my/path1/\".withoutTrailSlash // yields SimpleS3Location(\"s3a\", \"mybucket.some\", \"my/path1\")\n```\n\n\n---\n\n    Copyright 2019 ABSA Group Limited\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n    \n        http://www.apache.org/licenses/LICENSE-2.0\n    \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabsaoss%2Fcommons","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabsaoss%2Fcommons","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabsaoss%2Fcommons/lists"}