A feature I believe is long overdue in SubCut is the ability to load configuration from files that can be altered without needing re-compilation. While it has always been possible (even easy) to write such property loaders yourself and bind them into the subcut modules, I really wanted to add the functionality into the core subcut library and also provide some level of early problem detection and type support, including custom types.
As of SubCut 2.5, this feature is now available for Java property files at present, with XML and JSON support to follow soon (JSON will likely be a separate extension library so that subcut retains its current policy of no dependencies other than standard scala libs).
The BindingModule for using Property file based bindings is called PropertyFileModule
and can be used as follows:
-
Include a
PropertyFileModule
definition in your bindings definition, e.g.implicit val bindings = PropertyFileModule.fromResourceFile("somepropfileonclasspath.txt") ~ ProjectBindings ~ GlobalBindings
or by mixing it in to a standard binding module definition:
val withBothParsers = PropertyMappings.Standard + ("Seq[String]" -> seqStringParser) + ("Person" -> personParser) implicit val bindings: BindingModule = newBindingModule( module => { module <~ PropertyFileModule.fromResourceFile("custompropbindings.txt", withBothParsers) module.bind[Person] idBy "Fred" toSingle Person("Fred", "Smith", 33) module.bind[Int] idBy "aprogint" toSingle 33 })
More details on the definition of the
PropertyFileModule
can be found below, along with thefromResourceFile
method and what thePropertyMappings.Standard
and the custom parsers definitions are all about (but in a nutshell, you can define your own types to parse from property files using this mechanism). -
Create a property file to load the configuration from. Some examples:
system.database.url = http://mydbserver:3306/somedb system.database.timeout = 5 seconds
This is the simplest form of property bindings, and assumes string values for the bindings used. The
PropertyFileModule
is also capable of parsing non-string types if you specify them in the property file like this:system.database.name = mydbserver system.database.port.[Int] = 3306 system.database.timeout.[Duration] = 5 seconds
the type to use goes at the end after a ., and using the
[]
brackets (which scala uses to denote types). This must be the last segment of the property key, and will not be used as part of the name (so the names above will besystem.database.name
,system.database.port
andsystem.database.timeout
, and will have typesString
,Int
andDuration
respectively. Standard parsers are provided forString, Int, Long, Boolean, Float, Double, Char and Duration
and you can also add your own. -
Use the injected bindings as normal. The type is enforced (and parsed) as the module is loaded, so there should be no runtime surprises when you inject the values. For example, a
[Duration]
defined in the property file must have a valid duration definition and will be parsed and checked when the property module is loaded. When you want to use it, you just use it like this:class DBConfig(implicit val bindingModule: BindingModule) extends Injectable { val dbName = inject [String] ("system.database.name") val dbPort = inject [Int] ("system.database.port") val dbTimeout = inject [Duration] ("system.database.timeout") val dbUrl = s"http://$dbName:$dbPort" }
attempting to inject the system.database.timeout as anything other than a Duration (e.g. trying to inject it as a String) will give a binding not found exception. The type safety is ensured by the parsing of the property file.
String, common primitive, and duration parsers are provided, but naturally you will find yourself wanting to create
property parsers for your own types and use those to load a property file. Fortunately this is very easy. Simply
create an extension of PropertyParser[YourTypeHere]
and then add a map entry YourTypeHere -> newPropertyParser
to
the PropertyMappings.Standard map when you create the PropertyFileModule to parse the file. For example:
case class Person(first: String, last: String, age: Int)
val seqStringParser = new PropertyParser[Seq[String]] {
def parse(propString: String): Seq[String] = propString.split(',').map(_.trim).toList
}
val personParser = new PropertyParser[Person] {
def parse(propString: String): Person = {
val fields = propString.split(',').map(_.trim)
Person(fields(1), fields(0), fields(2).toInt)
}
}
val customParserMap = PropertyMappings.Standard + ("Seq[String]" -> seqStringParser) + ("Person" -> personParser)
implicit val binding = PropertyFileModule.fromResourceFile("custompropbindings.txt", customParserMap)
The string you use in the parser map for the key should correspond to the type you will specify in the property file.
For example, in the above code, we define parsers for Seq[String]
and a Person
types, so these would be specified
in the property file like this:
# our new custom bindings
seq.of.strings.[Seq[String]] = hello, there, today
some.person.[Person] = Wall, Dick, 25
# some other standard bindings
simple1 = hello
someInt.[Int] = 6
Then once the property file has loaded without errors, you can inject your custom types along with the standard ones:
class PropertyInjectedClass(implicit val bindingModule: BindingModule) extends Injectable {
val seqOfString = inject[Seq[String]]("seq.of.strings") // inject the Seq[String]
val person = inject[Person]("some.person") // inject the Person
val simpleString = inject[String]("simple1")
val someInt = inject[Int]("someInt")
}
The form PropertyFileModule.fromResourceFile(filename, mappings)
(mappings is optional) is provided as a convenient way
of finding a property file on the classpath and loading that, but if you want to use a specific path to a file, just use
the standard apply
method on PropertyFileModule
that takes a java.io.File
to load, e.g.:
PropertyFileModule(new File("/home/user/.config/myapp/properties.txt"))
or using a custom mapping
PropertyFileModule(new File("/home/user/.config/myapp/properties.txt", customMappings))