All Downloads are FREE. Search and download functionalities are using the official Maven repository.

grizzled.config.config.scala Maven / Gradle / Ivy

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