org.clapper.avsl.config.scala Maven / Gradle / Ivy
The newest version!
/**
* AVSL logging classes.
*/
package org.clapper.avsl.config
import org.clapper.avsl._
import org.clapper.avsl.formatter._
import org.clapper.avsl.handler._
import grizzled.config.{Configuration, Section}
import scala.annotation.tailrec
import scala.io.Source
import scala.collection.mutable.{Map => MutableMap, Set => MutableSet}
import java.net.{MalformedURLException, URL}
import java.io.File
import scala.util.Try
// ---------------------------------------------------------------------------
// Classes
// ---------------------------------------------------------------------------
/**
* Arguments for a formatter or handler.
*/
class ConfiguredArguments(argMap: Map[String, String]) {
/** Get a named value from the arguments, throwing an exception if
* not found.
*
* @param name the name of the parameter to retrieve
*
* @return the value
*
* @throws NoSuchElementException if not found
*/
def apply(name: String) = argMap(name)
/** Get the argument map.
*
* @return the map
*/
val toMap = argMap
/** Get a named value from the arguments.
*
* @param name the name of the parameter to retrieve
*
* @return `Some(value)` if found, `None` if not.
*/
def get(name: String): Option[String] = argMap.get(name)
/** Get a named value from the arguments, supplying a default if not
* found.
*
* @param name the name of the parameter to retrieve
*
* @return the retrieved value, or `default` if not found.
*/
def getOrElse(name: String, default: String) =
argMap.getOrElse(name, default)
/** Provide a reasonable string representation.
*
* @return a string representation
*/
override val toString = {
"ConfiguredArguments(" +
argMap.map { case (k, v) => s"$k -> $v"}.mkString(", ") +
")"
}
}
/**
* Convenience object that contains no arguments.
*/
object NoConfiguredArguments
extends ConfiguredArguments(Map.empty[String, String])
/**
* The configuration handler.
*/
class AVSLConfiguration(source: Source) {
def this(url: URL) = this(Source.fromURL(url))
val config = Configuration.read(source).get
val loggerTree = getLoggers
val handlers = getHandlers
val formatters = getFormatters
validate
def loggerConfigFor(name: String): LoggerConfig = {
val rootNode = loggerTree.rootNode
def find(namePieces: List[String], current: LoggerConfigNode):
LoggerConfigNode = {
namePieces match {
case namePiece :: Nil =>
current.children.getOrElse(namePiece, current)
case namePiece :: tail =>
current.children.get(namePiece).map { node => find(tail, node) }.
getOrElse(current)
case Nil =>
rootNode
}
}
val node = name match {
case Logger.RootLoggerName =>
rootNode
case _ =>
find(name.split("""\.""").toList, rootNode)
}
node.config.getOrElse(rootNode.config.get)
}
/** Validate the loggers, handlers and formatters.
*/
private def validate = {
// Individual validators return an error message (Some(msg)) or None.
val errorMessage = validateLoggers.getOrElse("") +
validateFormatters.getOrElse("") +
validateHandlers.getOrElse("")
if (! errorMessage.isEmpty)
throw new AVSLConfigException(errorMessage)
}
private def stringToOption(s: String): Option[String] =
if (s.isEmpty) None else Some(s)
/** Extract the logger configuration sections, map them into a tree (by
* splitting dot-separated class names into individual name nodes), and
* return the result. The root logger will be at the top of the tree,
* whether configured or not.
*
* @return the logger configuration tree
*/
private def getLoggers: LoggerConfigTree = {
val re = ("^" + AVSLConfiguration.LoggerPrefix).r
val configs = Map.empty[String, LoggerConfig] ++
config.matchingSections(re).map(new LoggerConfig(this, _)).
map(cfg => (cfg.name, cfg))
def makeRoot = {
val sectionName = AVSLConfiguration.LoggerPrefix +
Logger.RootLoggerName
// Make a root node with the default handler name. The
// default handler is automatically created.
val args = Map("level" -> "error",
"handlers" -> AVSLConfiguration.DefaultHandlerName)
new LoggerConfig(this, new Section(sectionName, args))
}
val root = configs.getOrElse(Logger.RootLoggerName, makeRoot)
val topNode = makeTree(root, configs.values)
new LoggerConfigTree(topNode)
}
/** Map the logger configuration items into a tree.
*
* @param root the root logger configuration item
* @param configs all the logger configuration items from the config
*
* @return the top-level (root) logger configuration node
*/
private def makeTree(root: LoggerConfig,
configs: Iterable[LoggerConfig]): LoggerConfigNode = {
def noChildren = MutableMap.empty[String, LoggerConfigNode]
val rootNode = LoggerConfigNode(root.pattern, Some(root), noChildren)
/** Create a node for a logger configuration item and insert it
* into the tree
*/
@tailrec def insert(cursor: LoggerConfigNode,
config: LoggerConfig,
patternParts: List[String]): LoggerConfigNode = {
patternParts match {
case leaf :: Nil => {
val childOpt = cursor.children.get(leaf)
if (childOpt.exists { node => node.config.isDefined }) {
throw new AVSLConfigException("Multiple loggers for " +
childOpt.get.config.get.pattern)
}
// If the node has children, use them...
val newChildren = childOpt map { _.children } getOrElse (noChildren)
val node = LoggerConfigNode(leaf, Some(config), newChildren)
cursor.children += (leaf -> node)
node
}
case mid :: tail => {
val node = cursor.children.getOrElse(mid, LoggerConfigNode(mid, None,
noChildren))
cursor.children += (mid -> node)
insert(node, config, tail)
}
case Nil =>
cursor
}
}
for (config <- configs; if (config.name != root.name))
insert(rootNode, config, config.pattern.split("""\.""").toList)
rootNode
}
/** Validate the loggers.
*/
private def validateLoggers: Option[String] = {
def checkHandlers(logger: LoggerConfig,
handlersToCheck: List[String]): List[String] = {
def checkMany(handlersToCheck: List[String]): List[Option[String]] = {
handlersToCheck match {
case Nil => Nil
case handler :: Nil => List(checkOne(handler))
case handler :: tail => checkOne(handler) :: checkMany(tail)
}
}
def checkOne(handler: String): Option[String] = {
if (this.handlers.contains(handler))
None
else
Some(s"""Logger "${logger.name}" refers to unknown """ +
s"""handler "$handler".""")
}
// Map from list of Option[String] values to strings, filtering
// out the None elements.
val handlerNames = logger.handlerNames.filter(_ != "")
checkMany(handlerNames).filter(_ != None).map(_.get)
}
def checkNode(node: LoggerConfigNode): List[String] = {
val errors =
node.config.map {
config => checkHandlers(config, config.handlerNames)
}.
getOrElse(Nil)
errors ::: checkNodes(node.children.values.toList)
}
def checkNodes(nodes: List[LoggerConfigNode]): List[String] = {
nodes match {
case node :: Nil => checkNode(node)
case node :: tail => checkNode(node) ++ checkNodes(tail)
case Nil => Nil
}
}
stringToOption(checkNode(loggerTree.rootNode) mkString "\n")
}
/** Get the formatters.
*/
private def getFormatters: Map[String, FormatterConfig] = {
val defaultFormatter = FormatterConfig.default(this)
val re = ("^" + AVSLConfiguration.FormatterPrefix).r
val configs = config.matchingSections(re).map(new FormatterConfig(this, _))
Map(defaultFormatter.name -> defaultFormatter) ++
configs.map(c => (c.name, c))
}
/** Validate the formatters.
*/
private def validateFormatters: Option[String] = None
/** Get the handlers.
*/
private def getHandlers: Map[String, HandlerConfig] = {
val defaultHandler = HandlerConfig.default(this)
val re = ("^" + AVSLConfiguration.HandlerPrefix).r
val configs = config.matchingSections(re).map(new HandlerConfig(this, _))
Map(defaultHandler.name -> defaultHandler) ++
configs.map(cfg => (cfg.name, cfg))
}
/** Validate the handlers.
*/
private def validateHandlers: Option[String] = {
def doValidation: String = {
def badFormatter(name: String) = ! this.formatters.contains(name)
def badFormatterMessage(handler: HandlerConfig) = {
s"""Handler "${handler.name}" refers to unknown """ +
s"""formatter "${handler.formatterName}"."""
}
handlers.values.filter(h => badFormatter(h.formatterName)).
map(h => Some(badFormatterMessage(h))).map(_.get).mkString("\n")
}
stringToOption(doValidation)
}
}
/**
* Common configuration methods used by all configuration sections.
*/
private[avsl] trait ConfigurationItem {
val config: AVSLConfiguration
val section: Section
protected def requiredString(option: String): String = {
section.options.get(option).getOrElse {
throw new AVSLMissingRequiredOptionException(section.name, option)
}
}
protected def configuredLevel: LogLevel = {
section.options.get(AVSLConfiguration.LevelKeyword).map { value =>
LogLevel.fromString(value).getOrElse {
throw new AVSLConfigSectionException(
section.name, s"Bad log level: $value"
)
}
}.
getOrElse {
throw new AVSLMissingRequiredOptionException(
section.name, AVSLConfiguration.LevelKeyword
)
}
}
protected def classOption(keyword: String,
aliases: Map[String,Class[_]]): Option[Class[_]] = {
Util.lookupClass(section.options.get(keyword), aliases)
}
protected def getArgs(filterOp: String => Boolean): ConfiguredArguments = {
val argMap = Map.empty[String, String] ++
section.options.keys.filter(filterOp).
map(k => (k, section.options(k)))
new ConfiguredArguments(argMap)
}
}
/**
* Information about a configured logger. These items are arranged in a
* hierarchy, by name (which is usually a class name), with the root logger
* at the top.
*/
private[avsl] class LoggerConfig(val config: AVSLConfiguration,
val section: Section)
extends ConfigurationItem {
val name = section.name.replace(AVSLConfiguration.LoggerPrefix, "")
val pattern = if (name == "root") "" else requiredString("pattern")
val level = configuredLevel
val handlerNames = section.options.
getOrElse(AVSLConfiguration.HandlersKeyword, "").
split("""[\s,]+""").
toList
if (name == "")
throw new AVSLConfigSectionException(
section.name, "Bad logger section name: \"" + section.name + "\""
)
override def toString = name
}
/**
* The logger tree.
*/
private[avsl] class LoggerConfigTree(val rootNode: LoggerConfigNode) {
import java.io.{OutputStreamWriter, PrintStream, PrintWriter, Writer}
def printTree(stream: PrintStream): Unit =
printTree(new OutputStreamWriter(stream))
def printTree(writer: Writer): Unit = {
val out = new PrintWriter(writer)
def printSubtree(node: LoggerConfigNode, indentation: Int = 0): Unit = {
def indent = " " * indentation
def handlerNames = node.config.map {_.handlerNames.mkString(", ") }.
getOrElse("")
def level = node.config.map { _.level.toString }.getOrElse("")
val label = if (node.name == "") "ROOT" else node.name
out.println(indent + label + ": children=" +
node.children.values.map(_.name).mkString(", ") +
", handlers=" + handlerNames + ", level=" + level)
for (c <- node.children.values)
printSubtree(c, indentation + 1)
}
printSubtree(rootNode)
out.flush
}
}
/**
* A node in the logger tree.
*/
private[avsl]
case class LoggerConfigNode(val name: String,
val config: Option[LoggerConfig],
val children: MutableMap[String, LoggerConfigNode]) {
override def toString = if (name == "") "" else name
}
/**
* Information about a configured handler.
*/
private[avsl] class HandlerConfig(val config: AVSLConfiguration,
val section: Section)
extends ConfigurationItem {
val ClassAliases = Map("DefaultHandler" -> classOf[ConsoleHandler],
"ConsoleHandler" -> classOf[ConsoleHandler],
"FileHandler" -> classOf[FileHandler],
"EmailHandler" -> classOf[EmailHandler],
"NullHandler" -> classOf[NullHandler])
val DefaultHandlerClass = classOf[ConsoleHandler]
val name = section.name.replace(AVSLConfiguration.HandlerPrefix, "")
val level = configuredLevel
val args = getArgs(! isReserved(_))
val formatterName = requiredString(AVSLConfiguration.FormatterKeyword)
val handlerClass = classOption(AVSLConfiguration.ClassKeyword, ClassAliases).
getOrElse(DefaultHandlerClass)
if (name == "")
throw new AVSLConfigSectionException(
section.name, "Bad handler section name: \"" + section.name + "\""
)
private def isReserved(s: String): Boolean = {
(s == AVSLConfiguration.ClassKeyword) ||
(s == AVSLConfiguration.FormatterKeyword) ||
(s == AVSLConfiguration.LevelKeyword)
}
}
private[avsl] object HandlerConfig {
def default(config: AVSLConfiguration) = {
val DefaultFormatter = AVSLConfiguration.DefaultFormatterName
val defaultHandlerSection = new Section(
AVSLConfiguration.DefaultHandlerName,
Map(AVSLConfiguration.LevelKeyword -> LogLevel.Error.label,
AVSLConfiguration.FormatterKeyword -> DefaultFormatter)
)
new HandlerConfig(config, defaultHandlerSection)
}
}
/**
* Information about a configured formatter.
*/
private[avsl] class FormatterConfig(val config: AVSLConfiguration,
val section: Section)
extends ConfigurationItem {
val name = section.name.replace(AVSLConfiguration.FormatterPrefix, "")
val args = getArgs(! isReserved(_))
val formatterClass = classOption(AVSLConfiguration.ClassKeyword,
FormatterConfig.ClassAliases).
getOrElse(FormatterConfig.DefaultFormatterClass)
if (name == "")
throw new AVSLConfigSectionException(
section.name, "Bad formatter section name: \"" + section.name + "\""
)
private def isReserved(s: String): Boolean =
(s == AVSLConfiguration.ClassKeyword)
}
private[avsl] object FormatterConfig {
val DefaultFormatterName = "DefaultFormatter"
val DefaultFormatterClass = classOf[SimpleFormatter]
val ClassAliases = Map(DefaultFormatterName -> classOf[SimpleFormatter],
"NullFormatter" -> classOf[NullFormatter])
def default(config: AVSLConfiguration) = {
val DefaultFormatter = AVSLConfiguration.DefaultFormatterName
val defaultFormatterSection = new Section(
AVSLConfiguration.DefaultFormatterName,
Map(AVSLConfiguration.ClassKeyword -> DefaultFormatterName)
)
new FormatterConfig(config, defaultFormatterSection)
}
def formatterClassForName(name: String) = {
Util.lookupClass(Some(name), ClassAliases).getOrElse {
throw new AVSLConfigException(s"Unknown formatter: $name")
}
}
}
// ---------------------------------------------------------------------------
// Companion Object
// ---------------------------------------------------------------------------
//private[avsl]
object AVSLConfiguration {
val PropertyName = "org.clapper.avsl.config"
val EnvVariable = "AVSL_CONFIG"
val DefaultName = "avsl.conf"
val LevelKeyword = "level"
val HandlerPrefix = "handler_"
val LoggerPrefix = "logger_"
val FormatterPrefix = "formatter_"
val FormatterKeyword = "formatter"
val HandlersKeyword = "handlers"
val ClassKeyword = "class"
val DefaultHandlerName = "***default***"
val DefaultFormatterName = "***default***"
private val SearchPath = List(sysProperty _,
envVariable _,
resource _)
def apply(source: Source): AVSLConfiguration = new AVSLConfiguration(source)
def apply(): Option[AVSLConfiguration] = {
find.map (new AVSLConfiguration(_) )
}
private def find: Option[URL] = {
def search(functions: List[() => Option[URL]]): Option[URL] = {
functions match {
case function :: Nil =>
function()
case function :: tail =>
function().orElse { search(tail) }
case Nil =>
None
}
}
search(SearchPath)
}
private def resource(): Option[URL] = {
Option(this.getClass.getClassLoader.getResource("avsl.conf"))
}
private def envVariable(): Option[URL] =
urlString("Environment variable " + EnvVariable,
System.getenv(EnvVariable))
private def sysProperty(): Option[URL] =
urlString("-D" + PropertyName, System.getProperty(PropertyName))
private def urlString(label: String, getValue: => String): Option[URL] = {
Option(getValue).flatMap { s =>
if (s.trim.length == 0) None else urlOrFile(label, s)
}
}
private def urlOrFile(label: String, s: String): Option[URL] = {
Try {
Some(new URL(s))
}.
recover {
case _: MalformedURLException => {
val f = new File(s)
if (! f.exists) {
println(s"Warning: $label specifies nonexistent file ${f.getPath}")
None
}
else
Some(f.toURI.toURL)
}
}.
get
}
}
/** Utility methods.
*/
private[config] object Util {
def lookupClass(nameOpt: Option[String],
aliases: Map[String, Class[_]]): Option[Class[_]] = {
nameOpt.map { name =>
if (aliases.keySet.contains(name))
aliases(name)
else {
Try {
Class.forName(name)
}.
recover {
case _: ClassNotFoundException =>
throw new AVSLConfigException(s"Cannot load class $name")
}.
get
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy