
grizzled.config.config.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grizzled-scala_2.11 Show documentation
Show all versions of grizzled-scala_2.11 Show documentation
A general-purpose Scala utility library
The newest version!
/**
* Classes and objects to aid in the parsing of INI-style configuration
* files. This package is similar, in concept, to the Python
* `ConfigParser` module (though its implementation and capabilities
* differ quite a bit).
*/
package grizzled.config
import grizzled.file.Includer
import grizzled.file.filter.BackslashContinuedLineIterator
import grizzled.string.template.UnixShellStringTemplate
import scala.annotation.tailrec
import scala.io.Source
import scala.util.matching.Regex
import scala.util.{Try, Success, Failure}
/** Some commonly used type aliases
*/
package object Types {
type NotFoundFunction = (String, String) => Try[Option[String]]
}
/**
* Used as a wrapper to pass a section to callbacks.
*/
class Section(val name: String, val options: Map[String, String]) {
override def toString = "[" + name + "]"
}
/** To create your own value converter, implement this trait.
*
* @tparam T the type the converter returns
*/
trait ValueConverter[T] {
/** Convert an option value to the appropriate type.
*
* @param sectionName the name of the section, for error messages
* @param optionName the name of the option, for error messages
* @param value the option's value, as a string
*
* @return `Success(value)` on success; `Failure(error)` on error
*/
def convert(sectionName: String,
optionName: String,
value: String): Try[T]
}
/** A configuration-related exception, used primarily inside `Failure`
* objects.
*
* @param message the exception message
* @param exception a nested exception
*/
@SuppressWarnings(Array("org.wartremover.warts.Null"))
class ConfigurationException(val message: String,
val exception: Throwable = null)
extends Exception(message, exception)
object ConfigurationException {
def apply(message: String): ConfigurationException =
new ConfigurationException(message)
def apply(message: String, exception: Throwable): ConfigurationException =
new ConfigurationException(message, exception)
}
/** A specific kind of configuration exception, tied to a specific section
* and option.
*/
@SuppressWarnings(Array("org.wartremover.warts.Null"))
class ConfigurationOptionException(message: String,
val section: String,
val option: String,
exception: Throwable = null)
extends ConfigurationException(message, exception) {
}
/** An INI-style configuration file parser.
*
* `Configuration` implements an in-memory store for a configuration file
* whose syntax is reminiscent of classic Windows .INI files, though with
* many extensions.
*
* '''Syntax'''
*
* A configuration file is broken into sections, and each section is
* introduced by a section name in brackets. For example:
*
* {{{
* [main]
* installation.directory=/usr/local/foo
* program.directory: /usr/local/foo/programs
*
* [search]
* searchCommand: find /usr/local/foo -type f -name '*.class'
*
* [display]
* searchFailedMessage=Search failed, sorry.
* }}}
*
* Notes and caveats:
*
* At least one section is required.
*
* Sections may be empty.
*
* It is an error to have any variable definitions before the first
* section header.
* The section names "system" and "env" are reserved. They don't really
* exist, but they're used during variable substitution (see below)
* to substitute from `System.properties` and the environment,
* respectively.
*
* '''Section Name Syntax'''
*
* There can be any amount of whitespace before and after the brackets
* in a section name; the whitespace is ignored. Section names may consist
* of alphanumeric characters and underscores. Anything else is not
* permitted.
*
* '''Variable Syntax'''
*
* Each section contains zero or more variable settings. Similar to a Java
* `Properties` file, the variables are specified as name/value pairs,
* separated by an equal sign ("=") or a colon (":"). Variable names are
* case-sensitive by default, though the case-sensitivity (and other
* aspects of the variable name) may be changed by subclassing
* `Configuration` and providing your own version of the
* `transformOptionName()` method. Variable names may contain
* alphanumerics, underscores, and hyphens (-). Variable values may contain
* anything at all. The parser ignores whitespace on either side of the "="
* or ":"; that is, leading whitespace in the value is skipped. The way to
* include leading whitespace in a value is escape the whitespace
* characters with backslashes. (See below).
*
* '''Continuation Lines'''
*
* Variable definitions may span multiple lines; each line to be
* continued must end with a backslash ("\") character, which escapes the
* meaning of the newline, causing it to be treated like a space character.
* The following line is treated as a logical continuation of the first
* line. Unlike Java properties files, however, leading whitespace is
* ''not'' removed from continued lines.
*
* Only variable definition lines may be continued. Section header
* lines, comment lines (see below) and include directives (see below)
* cannot span multiple lines.
*
* '''Expansions of Variable Values'''
*
* The configuration parser preprocesses each variable's value, replacing
* embedded metacharacter sequences and substituting variable references.
* You can use backslashes to escape the special characters that the parser
* uses to recognize metacharacter and variable sequences; you can also use
* single quotes. See ''Suppressing Metacharacter Expansion and Variable
* Substitution'', below, for more details.
*
* '''Metacharacters'''
*
* The parser recognizes Java-style ASCII escape sequences `\t`, `\n`,
* `\r`, `\\`, `\ ` (a backslash and a space), and `\``u`''xxxx'' are
* recognized and converted to single characters. Note that metacharacter
* expansion is performed ''before'' variable substitution.
*
* '''Variable Substitution'''
*
* A variable value can interpolate the values of other variables, using
* a variable substitution syntax. The general form of a variable reference
* is `\${sectionName.varName}`.
*
* `sectionName` is the name of the section containing the variable to
* substitute; if omitted, it defaults to the current section. `varName` is
* the name of the variable to substitute.
*
* If a variable reference specifies a section name, the referenced section
* must precede the current section. It is not possible to substitute the value
* of a variable in a section that occurs later in the file.
*
* The section names "system" and "env" are reserved for special
* "pseudosections."
*
* The "system" pseudosection is used to interpolate values from
* `System.properties` For instance, `\${system.user.home}` substitutes the
* value of the `user.home` system property (typically, the home directory
* of the user running the program). Similarly, `\${system.user.name}`
* substitutes the user's name.
*
* The "env" pseudosection is used to interpolate values from the
* environment. On UNIX systems, for instance, `\${env.HOME}` substitutes
* user's home directory (and is, therefore, a synonym for
* `\${system.user.home}`. On some versions of Windows, `\${env.USERNAME}`
* will substitute the name of the user running the program. Note: On UNIX
* systems, environment variable names are typically case-sensitive; for
* instance, `\${env.USER}` and `\${env.user}` refer to different environment
* variables. On Windows systems, environment variable names are typically
* case-insensitive; `\${env.USERNAME}` and `\${env.username}` are
* equivalent.
*
* '''Notes and caveats:'''
*
* `Configuration` uses the
* `grizzled.string.template.UnixShellVariableSubstituter`
* class to do variable substitution, so it honors all the syntax conventions
* supported by that class.
*
* Variable substitutions are only permitted within variable values. They are
* ignored in variable names, section names, include directives and comments.
*
* Variable substitution is performed ''after'' metacharacter expansion (so
* don't include metacharacter sequences in your variable names).
*
* To include a literal "\$" character in a variable value, escape it with a
* backslash, e.g., "`var=value with \\$ dollar sign`"
*
* '''Suppressing Metacharacter Expansion and Variable Substitution'''
*
* To prevent the parser from interpreting metacharacter sequences,
* variable substitutions and other special characters, use the "->"
* assignment operator, instead of ":" or "=".
*
* For example, suppose you want to set variable "prompt" to the
* literal value "Enter value. To specify a newline, use \n." The following
* configuration file line will do the trick:
*
* {{{
* prompt -> Enter value. To specify a newline, use \n
* }}}
*
* Similarly, to set variable "abc" to the literal string "\${foo}"
* suppressing the parser's attempts to expand "\${foo}" as a variable
* reference, you could use:
*
* {{{
* abc -> \${foo}
* }}}
*
* Note: It's also possible, though hairy, to escape the special meaning
* of special characters via the backslash character. For instance, you can
* escape the variable substitution lead-in character, '\$', with a
* backslash. e.g., "\\$". This technique is not recommended, however,
* because you have to double-escape any backslash characters that you want
* to be preserved literally. For instance, to get "\t", you must specify
* "\\\\t". To get a literal backslash, specify "\\\\". (Yes, that's four
* backslashes, just to get a single unescaped one.) This double-escaping
* is a regrettable side effect of how the configuration file parses
* variable values: It makes two separate passes over the value (one for
* metacharacter expansion and another for variable expansion). Each of
* those passes honors and processes backslash escapes. This problem would
* go away if the configuration file parser parsed both metacharacter
* sequences and variable substitutions itself, in one pass. It doesn't
* currently do that, because it uses the separate
* `grizzled.string.template.UnixShellStringTemplate` class
* `grizzled.GrizzledString.translateMetachars()` method to do the
* variable substitution and metacharacter translation. In general, you're
* better off just sticking with the "->" assignment operator.
*
* '''Includes'''
*
* A special include directive permits inline inclusion of another
* configuration file. The include directive takes two forms:
*
* {{{
* %include "path"
* %include "URL"
* }}}
*
* For example:
*
* {{{
* %include "/home/bmc/mytools/common.cfg"
* %include "http://configs.example.com/mytools/common.cfg"
* }}}
*
* If the include path is not a URL, and is not an absolute path, its
* location is relative to the file that's trying to include it.
*
* The included file may contain any content that is valid for this
* parser. It may contain just variable definitions (i.e., the contents of
* a section, without the section header), or it may contain a complete
* configuration file, with individual sections. Since
* `Configuration` recognizes a variable syntax that is
* essentially identical to Java's properties file syntax, it's also legal
* to include a properties file, provided it's included within a valid
* section.
*
* Note: Attempting to include a file from itself, either directly or
* indirectly, will cause the parser to throw an exception.
*
* '''Comments and Blank Lines'''
*
* A comment line is a one whose first non-whitespace character is a "#".
* A blank line is a line containing no content, or one containing only
* white space. Blank lines and comments are ignored.
*
* '''Caller-supplied Predefined Sections'''
*
* Calling applications may supply predefined sections and options, in
* the form of a map. These sections may then be used by other sections,
* via variable references. The predefined sections are defined in a map of
* maps. The outer map is keyed by predefined section name. The inner maps
* consist of options and their values. For instance, to read a
* configuration file, giving it access to certain command line parameters,
* you could do something like this:
*
*
* def main(args: Array[String]): Unit = {
* val configFile = args(0)
* val name = args(1)
* val ipAddress = args(2)
* val sections = Map("args" -> Map("name" -> name, "ip" -> ipAddress))
* val config = Configuration(configFile, sections)
* ...
* }
*
*
* Note that contents of the configuration file can override the predefined
* sections.
*
* Applications may also provide a "not found" function that is called to
* resolve options that are not found in the table. Such a function can be
* used to supply on-demand sections and values. For example, suppose you
* want to do something crazy, such as look up any not-found values in a
* database. (This is probably a very bad idea, but it makes a good example.)
* You might do something like this:
* {{{
* def findInDatabase(sectionName: String, optionName: String):
* Either[String, Option[String]] = {
*
* val select = "SELECT value FROM config WHERE section = ? and option = ?"
* ...
* }
*
* val config = Configuration(configFile, notFoundFunction = findInDatabase)
* }}}
*
* @param contents the predefined sections. An empty map means
* there are no predefined sections.
* @param sectionNamePattern Regular expression that matches legal section
* names. The section name portion must be in
* a group. Default: ([a-zA-Z0-9_]+)
* @param commentPattern Regular expression that matches comment lines.
* Default: "^\s*(#.*)$"
* @param normalizeOptionName function to call to convert an option name to
* a key
* @param notFoundFunction function to call if an option is not found,
* or None. Not called on error, only on not found.
* @param safe `true` does "safe" substitutions, with
* substitutions of nonexistent values replaced by
* empty strings. `false` ensures that bad
* substitutions result in errors (or `None` in
* functions, like `get()`, that return `Option`
* values).
*/
final class Configuration private[config](
private val contents: Map[String, Map[String, Value]],
private val sectionNamePattern: Regex,
private val commentPattern: Regex,
private val normalizeOptionName: (String => String),
private val notFoundFunction: Option[Types.NotFoundFunction] = None,
private val safe: Boolean = true) {
private val SectionName = sectionNamePattern
private val VariableName = """([a-zA-Z0-9_.]+)""".r
private val FullVariableRef = (SectionName.toString + """\.""" +
VariableName.toString).r
/** Key used for an option, allowing storage of original option string and
* transformed value.
*/
private case class OptionKey(originalKey: String) {
val transformedKey = normalizeOptionName(originalKey)
override def equals(other: Any) = {
other match {
case k: OptionKey => k.transformedKey == transformedKey
case _ => false
}
}
override def hashCode = transformedKey.hashCode
override lazy val toString = s"OptionKey<$originalKey, $transformedKey>"
}
private val sections = contents.map { case (key, options) =>
key -> options.map { case (name, value) => OptionKey(name) -> value }
}
/** Get the list of section names.
*
* @return the section names, in a iterator
*/
def sectionNames: Iterator[String] = sections.keysIterator
/** Get a section. Similar to `Map.get`, this method returns `Some(Section)`
* if the section exists, and `None` if it does not.
*
* @param name the section to get
*
* @return `Some(Section)` or `None`
*/
def getSection(name: String): Option[Section] = {
sections.get(name).map { m =>
val sectionMap = m.map {
case (option, value) if value.isRaw =>
option -> Some(value.value)
case (option, value) =>
option -> resolveOpt(name, value.value)
}
.collect { case (key, Some(value)) =>
key.originalKey -> value
}
new Section(name, sectionMap)
}
}
/** Works like `Map.get()`, returning `Some(string)` if the value
* is found, `None` if not. Does not throw exceptions.
*
* @param sectionName the section name
* @param optionName the option name
*
* @return `Some(value)` if the section and option exist, `None` if
* either the section or option cannot be found.
*/
def get(sectionName: String, optionName: String): Option[String] = {
tryGet(sectionName, optionName).getOrElse(None)
}
/** Like `get()`, except that this method returns an `Either`, allowing
* errors to be captured and processed.
*
* NOTE: Prefer `tryGet()`, as this method may eventually go away.
*
* @param sectionName the section name
* @param optionName the option name
*
* @return `Left(error)` on error. `Right(None)` if not found.
* `Right(Some(value))` if found and processed.
*/
def getEither(sectionName: String, optionName: String):
Either[String, Option[String]] = {
tryGet(sectionName, optionName) match {
case Success(opt) => Right(opt)
case Failure(ex) => Left(ex.getMessage)
}
}
/** Like `get()`, except that this method returns a `Try`, allowing
* errors to be captured and processed.
*
* @param sectionName the section name
* @param optionName the option name
*
* @return `Failure(error)` on error. `Success(None)` if not found.
* `Success(Some(value))` if found and processed.
*/
def tryGet(sectionName: String, optionName: String): Try[Option[String]] = {
def handleNotFound = {
notFoundFunction.map { _(sectionName, optionName) }
.getOrElse(Success(None))
}
sectionName match {
case "env" =>
Success(Option(System.getenv(optionName)))
case "system" =>
Success(Option(System.getProperties.getProperty(optionName)))
case _ if ! hasSection(sectionName) =>
handleNotFound
case _ =>
val key = OptionKey(optionName)
sections(sectionName).get(key).map { value =>
if (value.isRaw)
Success(Some(value.value))
else
tryResolving(sectionName, value.value)
}
.getOrElse(handleNotFound)
}
}
/** Get a value as an instance of specified type. This method retrieves the
* value of an option from a section and, using the specified (or implicit)
* converter, attempts to convert the option's to the specified type. If you
* import `grizzled.config.Configuration.Implicits._`, you'll bring implicit
* converters for various common types into scope.
*
* @param sectionName the section from which to retrieve the value
* @param optionName the name of the option whose value is to be returned
* @tparam T the desired type of the result
* @param converter a `ValueConverter` object that will handle the
* actual conversion.
*
* @return `None` if not found or not convertible, `Some(value)` if found
* and converted. If you want to distinguish between "not found" and
* "cannot convert", use `asEither()`.
*/
def asOpt[T](sectionName: String, optionName: String)
(implicit converter: ValueConverter[T]): Option[T] = {
asTry(sectionName, optionName)(converter).getOrElse(None)
}
/** Get a value as an instance of specified type. This method retrieves the
* value of an option from a section and, using the specified (or implicit)
* converter, attempts to convert the option's to the specified type. If you
* import `grizzled.config.Configuration.Implicits._`, you'll bring implicit
* converters for various common types into scope.
*
* If `safe` is `true` (as defined when the `Configuration` object is built),
* substitutions of nonexistent variables will result in empty strings for
* where the substitutions were specified (e.g., `val\${section1.notValid}`
* will result in the string "val"). If `safe` is `false`, substitutions
* of nonexistent values will result in an error (i.e., a `Left` result).
*
* @param sectionName the section from which to retrieve the value
* @param optionName the name of the option whose value is to be returned
* @tparam T the desired type of the result
* @param converter a `ValueConverter` object that will handle the
* actual conversion.
*
* @return `Left(error)` on conversion error. `Right(None)` if not found.
* `Right(Some(value))` if found and converted.
*/
def asEither[T](sectionName: String, optionName: String)
(implicit converter: ValueConverter[T]):
Either[String, Option[T]] = {
asTry(sectionName, optionName)(converter) match {
case Success(opt) => Right(opt)
case Failure(ex) => Left(ex.getMessage)
}
}
/** Get a value as an instance of specified type. This method retrieves the
* value of an option from a section and, using the specified (or implicit)
* converter, attempts to convert the option's to the specified type. If you
* import `grizzled.config.Configuration.Implicits._`, you'll bring implicit
* converters for various common types into scope.
*
* If `safe` is `true` (as defined when the `Configuration` object is built),
* substitutions of nonexistent variables will result in empty strings for
* where the substitutions were specified (e.g., `val\${section1.notValid}`
* will result in the string "val"). If `safe` is `false`, substitutions
* of nonexistent values will result in an error (i.e., a `Left` result).
*
* @param sectionName the section from which to retrieve the value
* @param optionName the name of the option whose value is to be returned
* @tparam T the desired type of the result
* @param converter a `ValueConverter` object that will handle the
* actual conversion.
*
* @return `Failure(error)` on conversion error. `Success(None)` if not
* found. `Success(Some(value))` if found and converted.
*/
def asTry[T](sectionName: String, optionName: String)
(implicit converter: ValueConverter[T]):
Try[Option[T]] = {
def optionallyConvert(valueOpt: Option[String]): Try[Option[T]] = {
valueOpt.map { value =>
converter.convert(sectionName, optionName, value).map(Some(_))
}
.getOrElse(Success(None))
}
for { valueOpt <- tryGet(sectionName, optionName)
res <- optionallyConvert(valueOpt) }
yield res
}
/** Works like `Map.getOrElse()`, returning an option value or a
* default, if the option has no value. Does not throw exceptions.
* Calling this function is the same as:
* {{{
* get(sectionName, optionName).getOrElse(default)
* }}}
*
* @param sectionName the section name
* @param optionName the option name
* @param default the default value
*
* @return The option's value if the section and option exist, the
* default if either the section or option cannot be found.
*/
def getOrElse(sectionName: String,
optionName: String,
default: String): String = {
get(sectionName, optionName).getOrElse(default)
}
/** Retrieve a value, splitting it into a list of strings.
* Returns `Some(list)` if the key is found, and `None` otherwise.
*
* @param sectionName the section name
* @param optionName the option name
* @param separators separator regex to use. Default: [\s,]
*/
def getAsList(sectionName: String,
optionName: String,
separators: Regex = """[\s,]""".r): Option[List[String]] = {
get(sectionName, optionName).map(s =>
separators.split(s).filter(_.length > 0)
).map(_.toList)
}
/** Add a value to the configuration, returning a new object. If the
* option already exists in the specified section, it is replaced in
* the new configuration. Otherwise, it's added. If the section doesn't
* exist, it's created and the option is added.
*
* Example:
* {{{
* val cfg = Configuration(...)
* val newCfg = cfg + ("myNewSection", "optionName", "value")
* }}}
*
* @param section the section name
* @param option the option name
* @param value the value
*
* @return a new `Configuration` object with the change applied.
*/
def +(section: String, option: String, value: String): Configuration = {
val existing = contents.get(section)
val newSection = existing.map { sectionMap =>
sectionMap + (option -> Value(value))
}.
getOrElse(Map(option -> Value(value)))
val newContents = contents + (section -> newSection)
new Configuration(contents = newContents,
sectionNamePattern = this.sectionNamePattern,
commentPattern = this.commentPattern,
normalizeOptionName = this.normalizeOptionName,
notFoundFunction = this.notFoundFunction,
safe = this.safe)
}
/** Add multiple (section -> (option -> value)) triplets to the configuration,
* returning the new configuration. Example use:
*
* {{{
* val cfg = Configuration(...)
* val newCfg = cfg ++ (("newSection1" -> ("option1" -> "value1")),
* ("newSection2" -> ("option1" -> "value1")),
* ("newSection1" -> ("option3" -> "value3")))
* }}}
*
* @param values one or more (section -> (option -> value)) triplets
*
* @return new configuration
*/
def ++(values: (String, (String, String))*): Configuration = {
// Broken into pieces for easier reading. Types added for the same
// reason.
// Group the passed-in (section, (option, value)) tuples by section name.
val t1: Map[String, Seq[(String, (String, String))]] = values.groupBy(_._1)
// Map t1 so that we:
//
// (a) drop the section name from each map value (so that each map value
// is an (option, value) pair, and
// (b) map the "value" part of (option, value) from a String to a Value.
//
// Then, we'll end up with a new contents map we can merge with the
// existing one.
val t2: Map[String, Map[String, Value]] = t1.map { case (sect, entries) =>
val optionsAndVals = for { (_, ov) <- entries
(option, valueString) = ov }
yield (option, Value(valueString))
(sect, optionsAndVals.toMap)
}
// Finally, merge the two maps.
val newContents = t2.map { case (sectionName, optionsMap) =>
val optExistingOptions = contents.get(sectionName)
// If there's an existing map, addFile the new map to the existing one.
// Otherwise, just use the new one.
val newOptionsMap = optExistingOptions.map { existingOptionsMap =>
existingOptionsMap ++ optionsMap
}
.getOrElse(optionsMap)
(sectionName, newOptionsMap)
}
// Finally, construct the new Configuration.
new Configuration(contents = newContents,
sectionNamePattern = this.sectionNamePattern,
commentPattern = this.commentPattern,
normalizeOptionName = this.normalizeOptionName,
notFoundFunction = this.notFoundFunction,
safe = this.safe)
}
/** Add new sections to the configuration. Example usage:
*
* {{{
* val cfg = Configuration(...)
* val newCfg = cfg ++ Map(
* "newSection1" -> Map("option1" -> "value1",
* "option2" -> "value2"),
* "newSection2" -> Map("option1" -> "value1")
* )
* }}}
*
* @param newValues A map of (section -> Map(option -> value)) values
*
* @return new configuration
*/
def ++(newValues: Map[String, Map[String, String]]): Configuration = {
val sequence = for { (sectionName, optionsMap) <- newValues.toSeq
optionValue <- optionsMap.toSeq }
yield (sectionName, optionValue)
++(sequence: _*)
}
/** Remove a value from the configuration, returning a new object. If the
* section or option don't exist, the original configuration is returned
* (not a copy). If the section and option exist, the option is removed.
* If the section is then empty, it's also removed.
*
* @param section the section name
* @param option the option name
*
* @return a new `Configuration` object with the change applied, or the
* original configuration if the section or option weren't
* there.
*/
def -(section: String, option: String): Configuration = {
val optNewContents = for { sectionMap <- contents.get(section)
value <- sectionMap.get(option) }
yield {
val newSection = sectionMap - option
if (newSection.isEmpty)
contents - section
else
contents + (section -> newSection)
}
optNewContents.map { newContents =>
new Configuration(contents = newContents,
sectionNamePattern = this.sectionNamePattern,
commentPattern = this.commentPattern,
normalizeOptionName = this.normalizeOptionName,
notFoundFunction = this.notFoundFunction,
safe = this.safe)
}.
getOrElse(this)
}
/** Remove multiple (section -> option) pairs from the configuration,
* returning the new configuration. Example use:
*
* {{{
* val cfg = Configuration(...)
* val newCfg = cfg -- (("newSection1" -> "option1"),
* ("newSection2" -> "option1"),
* ("newSection1" -> "option3"))
* }}}
*
* @param values sequence of (section, option) pairs
*
* @return new configuration
*/
def --(values: Seq[(String, String)]): Configuration = {
// Group the passed-in (section, option) pairs by section name.
val grouped: Map[String, Seq[(String, String)]] = values.groupBy(_._1)
// Strip the section name from the grouped values.
val groupedValuesMap = grouped.map { case (section, seq) =>
(section, seq.map(_._2))
}
// Create a new content map by subtracting the options from existing
// sections. Note that we may well end up with empty sections.
val newContents1 = contents.map { case (sectionName, optionsMap) =>
groupedValuesMap.get(sectionName).map { removeOptions =>
sectionName -> (optionsMap -- removeOptions)
}.
getOrElse {
sectionName -> optionsMap
}
}
// Now, remove empty sections.
val newContents2 = newContents1.filter { case (section, optionsMap) =>
optionsMap.nonEmpty
}
if (contents == newContents2) {
this
}
else {
// Finally, construct the new Configuration.
new Configuration(contents = newContents2,
sectionNamePattern = this.sectionNamePattern,
commentPattern = this.commentPattern,
normalizeOptionName = this.normalizeOptionName,
notFoundFunction = this.notFoundFunction,
safe = this.safe)
}
}
/** Determine whether the configuration contains a named section.
*
* @param sectionName the new section's name
*
* @return `true` if the configuration has a section with that name,
* `false` otherwise
*/
def hasSection(sectionName: String): Boolean = sections contains sectionName
/** Get all options in a section.
*
* @param sectionName the section name
*
* @return a map of all options and their values for the section. If
* the section doesn't exist, an empty map is returned.
*/
def options(sectionName: String): Map[String, String] = {
getSection(sectionName).map { section =>
section.options
}.
getOrElse(Map.empty[String, String])
}
/** Get the list of option names.
*
* @param sectionName the section's name
*
* @return a list of option names in that section. The iterator will be
* empty if the section doesn't exist.
*/
def optionNames(sectionName: String): Iterator[String] = {
getSection(sectionName).map { section =>
section.options.keys.iterator
}.
getOrElse(Seq.empty[String].iterator)
}
/** Invoke a code block on each section whose name matches a regular
* expression.
*
* @param regex the regular expression to match
* @param code the block of code to invoke with each section
*/
def forMatchingSections(regex: Regex)(code: Section => Unit): Unit = {
for (name <- sectionNames; if regex.findFirstIn(name).nonEmpty)
code(new Section(name, options(name)))
}
/** Return a sequence of sections whose name match matches a regular
* expression.
*
* @param regex the regular expression to match
*/
def matchingSections(regex: Regex): Seq[Section] = {
sectionNames.filter { name => regex.findFirstIn(name).nonEmpty }
.map { name => new Section(name, options(name)) }
.toSeq
}
// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------
private def resolveOpt(sectionName: String, value: String): Option[String] = {
tryResolving(sectionName, value) match {
case Failure(e) => None
case Success(opt) => opt
}
}
private def tryResolving(sectionName: String, value: String):
Try[Option[String]] = {
import grizzled.string.Implicits.String._
val template = new UnixShellStringTemplate(templateResolve(sectionName, _),
"[a-zA-Z0-9_.]+",
safe)
template.sub(value.translateMetachars) match {
case Success(s) => Success(Some(s))
case Failure(e) => Failure(
new ConfigurationOptionException(
section = sectionName,
option = value,
message = s"Can't get '$value' from $sectionName: ${e.getMessage}",
exception = e
)
)
}
}
private def templateResolve(sectionName: String,
variableName: String): Option[String] = {
variableName match {
case FullVariableRef(section, option) => rawValue(section, option)
case VariableName(option) => rawValue(sectionName, option)
case _ => None
}
}
private def rawValue(sectionName: String, optionName: String): Option[String] = {
sectionName match {
case "env" => Option(System.getenv(optionName))
case "system" => Option(System.getProperties.getProperty(optionName))
case _ if !hasSection(sectionName) => None
case _ =>
val key = OptionKey(optionName)
sections(sectionName).get(key).flatMap { value =>
resolveOpt(sectionName, value.value)
}
}
}
}
/**
* Companion object for the `Configuration` class
*/
object Configuration {
val DefaultSectionNamePattern: Regex = """([a-zA-Z0-9_]+)""".r
val DefaultCommentPattern: Regex = """^\s*(#.*)$""".r
private def DefaultOptionNameTransformer(name: String) = name.toLowerCase()
/** Read a configuration file, returning an `Either`, instead of throwing
* an exception on error.
*
* @param source `scala.io.Source` object to read
* @param sectionNamePattern Regular expression that matches legal section
* names. Defaults as described above.
* @param commentPattern Regular expression that matches comment lines.
* Default: "^\s*(#.*)$"
* @param normalizeOptionName Partial function used to transform option names
* into keys. The default function transforms
* the names to lower case.
* @param notFoundFunction a function to call if an option isn't found in
* the configuration, or None. The function
* must take a section name and an option name as
* parameters. It must return `Failure` on error,
* `Success(None)` if the value isn't found, and
* Success `Right(Some(string))` if the value is
* found.
* @param safe `true` does "safe" substitutions, with
* substitutions of nonexistent values replaced by
* empty strings. `false` ensures that bad
* substitutions result in errors (or `None` in
* functions, like `get()`, that return `Option`
* values).
*
* @return `Right(config)` on success, `Left(error)` on error.
*/
def apply(source: Source,
sectionNamePattern: Regex = Configuration.DefaultSectionNamePattern,
commentPattern: Regex = Configuration.DefaultCommentPattern,
normalizeOptionName: (String => String) = DefaultOptionNameTransformer,
notFoundFunction: Option[Types.NotFoundFunction] = None,
safe: Boolean = true):
Either[String, Configuration] = {
load(source, sectionNamePattern, commentPattern) match {
case Success(map) =>
Right(new Configuration(map,
sectionNamePattern,
commentPattern,
normalizeOptionName,
notFoundFunction,
safe))
case Failure(ex) =>
Left(ex.getMessage)
}
}
/** Read a configuration file, returning an `Either`, instead of throwing
* an exception on error.
*
* @param source `scala.io.Source` object to read
* @param sectionNamePattern Regular expression that matches legal section
* names. Defaults as described above.
* @param commentPattern Regular expression that matches comment lines.
* Default: "^\s*(#.*)$"
* @param normalizeOptionName Partial function used to transform option names
* into keys. The default function transforms
* the names to lower case.
* @param notFoundFunction a function to call if an option isn't found in
* the configuration, or None. The function
* must take a section name and an option name as
* parameters. It must return `Failure` on error,
* `Success(None)` if the value isn't found, and
* Success `Right(Some(string))` if the value is
* found.
* @param safe `true` does "safe" substitutions, with
* substitutions of nonexistent values replaced by
* empty strings. `false` ensures that bad
* substitutions result in errors (or `None` in
* functions, like `get()`, that return `Option`
* values).
*
*@return `Success(config)` on success, `Failure(exception)` on error.
*/
def read(source: Source,
sectionNamePattern: Regex = Configuration.DefaultSectionNamePattern,
commentPattern: Regex = Configuration.DefaultCommentPattern,
normalizeOptionName: (String => String) = DefaultOptionNameTransformer,
notFoundFunction: Option[Types.NotFoundFunction] = None,
safe: Boolean = true):
Try[Configuration] = {
load(source, sectionNamePattern, commentPattern).map { map =>
new Configuration(map,
sectionNamePattern,
commentPattern,
normalizeOptionName,
notFoundFunction,
safe)
}
}
/** Read a configuration file, permitting some predefined sections to be
* added to the configuration before it is read. The predefined sections
* are defined in a map of maps. The outer map is keyed by predefined
* section name. The inner maps consist of options and their values.
* For instance, to read a configuration file, giving it access to
* certain command line parameters, you could do something like this:
*
* {{{
* object Foo {
* def main(args: Array[String]) = {
* // You'd obviously want to do some real argument checking here.
* val configFile = args(0)
* val name = args(1)
* val ipAddress = args(2)
* val sections = Map("args" -> Map("name" -> name, "ip" -> ipAddress))
* val config = Configuration(Source.fromFile(new File(configFile)), sections)
* ...
* }
* }
* }}}
*
* @param source `scala.io.Source` object to read
* @param sections the predefined sections. An empty map means
* there are no predefined sections.
*
* @return `Right[Configuration]` on success, `Left(error)` on error.
*/
def apply(source: Source, sections: Map[String, Map[String, String]]):
Either[String, Configuration] = {
apply(source, sections)
}
/** Read a configuration file, permitting some predefined sections to be
* added to the configuration before it is read. The predefined sections
* are defined in a map of maps. The outer map is keyed by predefined
* section name. The inner maps consist of options and their values.
* For instance, to read a configuration file, giving it access to
* certain command line parameters, you could do something like this:
*
* {{{
* object Foo {
* def main(args: Array[String]) = {
* // You'd obviously want to do some real argument checking here.
* val configFile = args(0)
* val name = args(1)
* val ipAddress = args(2)
* val sections = Map("args" -> Map("name" -> name, "ip" -> ipAddress))
* val config = Configuration.read(Source.fromFile(new File(configFile)), sections)
* ...
* }
* }
* }}}
*
* @param source `scala.io.Source` object to read
* @param sections the predefined sections. An empty map means
* there are no predefined sections.
*
* @return `Success[Configuration]` on success, `Failure(exception)` on error.
*/
def read(source: Source, sections: Map[String, Map[String, String]]):
Try[Configuration] = {
read(source, sections)
}
/** Read a configuration file, permitting some predefined sections to be
* added to the configuration before it is read. The predefined sections
* are defined in a map of maps. The outer map is keyed by predefined
* section name. The inner maps consist of options and their values.
* For instance, to read a configuration file, giving it access to
* certain command line parameters, you could do something like this:
*
* {{{
* object Foo {
* def main(args: Array[String]) = {
* // You'd obviously want to do some real argument checking here.
* val configFile = args(0)
* val name = args(1)
* val ipAddress = args(2)
* val sections = Map("args" -> Map("name" -> name, "ip" -> ipAddress))
* val config = Configuration(Source.fromFile(new File(configFile)), sections)
* ...
* }
* }
* }}}
*
* @param source `scala.io.Source` object to read
* @param sections the predefined sections. An empty map means
* there are no predefined sections.
* not (`true`). Default: `false`
* @param sectionNamePattern Regular expression that matches legal section
* names.
* @param commentPattern Regular expression that matches comment lines.
*
* @return `Right(config)` on success, `Left(error)` on error.
*/
def apply(source: Source,
sections: Map[String, Map[String, String]],
sectionNamePattern: Regex,
commentPattern: Regex):
Either[String, Configuration] = {
apply(source, sections, sectionNamePattern, commentPattern)
}
/** Read a configuration file, permitting some predefined sections to be
* added to the configuration before it is read. The predefined sections
* are defined in a map of maps. The outer map is keyed by predefined
* section name. The inner maps consist of options and their values.
* For instance, to read a configuration file, giving it access to
* certain command line parameters, you could do something like this:
*
* {{{
* object Foo {
* def main(args: Array[String]) = {
* // You'd obviously want to do some real argument checking here.
* val configFile = args(0)
* val name = args(1)
* val ipAddress = args(2)
* val sections = Map("args" -> Map("name" -> name, "ip" -> ipAddress))
* val config = Configuration.read(Source.fromFile(new File(configFile)), sections)
* ...
* }
* }
* }}}
*
* @param source `scala.io.Source` object to read
* @param sections the predefined sections. An empty map means
* there are no predefined sections.
* not (`true`). Default: `false`
* @param sectionNamePattern Regular expression that matches legal section
* names.
* @param commentPattern Regular expression that matches comment lines.
*
* @return `Success[Configuration]` on success, `Failure(exception)` on error.
*/
def read(source: Source,
sections: Map[String, Map[String, String]],
sectionNamePattern: Regex,
commentPattern: Regex):
Either[String, Configuration] = {
read(source, sections, sectionNamePattern, commentPattern)
}
// --------------------------------------------------------------------------
// Objects
// --------------------------------------------------------------------------
/** Import this object's contents (`import Configuration.Implicits._`)
* to get the implicit converters.
*/
object Implicits {
/** Value converter for Boolean values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object BooleanValueConverter extends ValueConverter[Boolean] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Boolean] = {
import grizzled.string.util._
strToBoolean(value) match {
case Left(error) =>
Failure(ConfigurationException(
s"""Section "$sectionName", option "$optionName": """ +
s""""$value" is not boolean: $error"""
))
case Right(b) => Success(b)
}
}
}
/** Value converter for integer values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object IntConverter extends ValueConverter[Int] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Int] = {
Try { Integer.parseInt(value) }
}
}
/** Value converter for long integer values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object LongConverter extends ValueConverter[Long] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Long] = {
Try { java.lang.Long.parseLong(value) }
}
}
/** Value converter for float values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object FloatConverter extends ValueConverter[Float] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Float] = {
Try { java.lang.Float.parseFloat(value) }
}
}
/** Value converter for long integer values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object DoubleConverter extends ValueConverter[Double] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Double] = {
Try { java.lang.Double.parseDouble(value) }
}
}
/** Value converter for String values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object StringConverter extends ValueConverter[String] {
def convert(sectionName: String,
optionName: String,
value: String): Try[String] = {
Success(value)
}
}
/** Value converter for Character values, for use with
* `Configuration.asEither()`, `Configuration.asOpt()`, and `asTry()`.
*/
implicit object CharConverter extends ValueConverter[Character] {
def convert(sectionName: String,
optionName: String,
value: String): Try[Character] = {
if (value.length == 1)
Success(value(0))
else
Failure(ConfigurationException(
s"""Section "$sectionName", option "$optionName": "$value" is """ +
"not a character."
))
}
}
}
// --------------------------------------------------------------------------
// Private methods
// --------------------------------------------------------------------------
/** Map a user-supplied section map into an internal one.
*
* @param sectionMap A map of section names to options
*
* @return the internal version of the same map
*/
private def mapSectionMap(sectionMap: Map[String, Map[String, String]]):
Map[String, Map[String, Value]] = {
sectionMap.map { case (sectionName: String, values: Map[String, String]) =>
sectionName -> values.map { case (k, v) => k -> Value(v, isRaw = true) }
}
}
/** Load configuration data from the specified source into this object.
* Clears the configuration first.
*
* @param source `scala.io.Source` object to read
*
* @return this object, for convenience
*/
private def load(
source: Source,
sectionNamePattern: Regex = Configuration.DefaultSectionNamePattern,
commentPattern: Regex = Configuration.DefaultCommentPattern
): Try[Map[String, Map[String, Value]]] = {
val SectionName = sectionNamePattern
val SectionNameString= SectionName.toString
val ValidSection = ("""^\s*\[""" + SectionNameString + """\]\s*$""").r
val BadSectionFormat = """^\s*(\[[^\]]*)$""".r
val BadSectionName = """^\s*\[(.*)\]\s*$""".r
val CommentLine = commentPattern
val BlankLine = """^(\s*)$""".r
val VariableNameString = """([a-zA-Z0-9_.]+)"""
val RawAssignment = ("""^\s*""" + VariableNameString + """\s*->\s*(.*)$""").r
val Assignment = ("""^\s*""" + VariableNameString + """\s*[:=]\s*(.*)$""").r
def processLine(line: String,
curSection: Option[String],
curMap: Map[String, Map[String, Value]]):
Try[(Option[String], Map[String, Map[String, Value]])] = {
line match {
case CommentLine(_) => Success((curSection, curMap))
case BlankLine(_) => Success((curSection, curMap))
case ValidSection(name) =>
val newMap = curMap ++ Map(name -> Map.empty[String, Value])
Success((Some(name), newMap))
case BadSectionFormat(section) =>
Failure(ConfigurationException(
s"""Badly formatted section: "$section"."""
))
case BadSectionName(name) =>
Failure(ConfigurationException(s"""Bad section name: "$name"."""))
case Assignment(optionName, value) =>
curSection.map { sectionName =>
val sectionMap = curMap.getOrElse(sectionName,
Map.empty[String, Value])
val newSection = sectionMap + (optionName -> Value(value))
Success((curSection, curMap ++ Map(sectionName -> newSection)))
}.
getOrElse(
Failure(ConfigurationException(
s"""Assignment "$optionName=$value" occurs before the first """ +
"section"
))
)
case RawAssignment(optionName, value) =>
curSection.map { sectionName =>
val sectionMap = curMap.getOrElse(sectionName,
Map.empty[String, Value])
val newSection = sectionMap +
(optionName -> Value(value, isRaw = true))
val newMap = curMap + (sectionName -> newSection)
Success((curSection, newMap))
}.
getOrElse(
Failure(ConfigurationException(
s"""Assignment "$optionName=$value" occurs before the first """ +
"section"
))
)
case _ =>
Failure(ConfigurationException(
s"""Unrecognized configuration line: "$line"."""
))
}
}
@tailrec
def processLines(lines: Iterator[String],
curSection: Option[String],
curMap: Map[String, Map[String, Value]]):
Try[Map[String, Map[String, Value]]] = {
if (lines.hasNext) {
val line = lines.next
processLine(line, curSection, curMap) match {
case Success((section, map)) =>
if (lines.hasNext)
processLines(lines, section, curMap ++ map)
else
Success(curMap ++ map)
case Failure(ex) => Failure(ex)
}
}
else {
Success(curMap)
}
}
for { inc <- Includer(source)
it = new BackslashContinuedLineIterator(inc)
m <- processLines(it, None, Map.empty[String, Map[String, Value]]) }
yield m
}
}
private[config] final case class Value(value: String, isRaw: Boolean = false) {
override val toString = s"Value"
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy