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

org.clapper.avsl.config.scala Maven / Gradle / Ivy

The newest version!
/*
  ---------------------------------------------------------------------------
  This software is released under a BSD license, adapted from
  http://opensource.org/licenses/bsd-license.php

  Copyright (c) 2010, Brian M. Clapper
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

   * Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

   * Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

   * Neither the names "clapper.org", "AVSL", nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  ---------------------------------------------------------------------------
*/

/**
  * 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

// ---------------------------------------------------------------------------
// 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 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)
}

/**
  * Convenience object that contains no arguments.
  */
object NoConfiguredArguments
extends ConfiguredArguments(Map.empty[String, String])

/**
  * The configuration handler.
  */
class AVSLConfiguration(source: Source) extends Configuration {
  def this(url: URL) = this(Source.fromURL(url))

  load(source)

  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.get(namePiece) match {
            case None       => current // not configured
            case Some(node) => node
          }

        case namePiece :: tail =>
          current.children.get(namePiece) match {
            case None       => current // not configured
            case Some(node) => find(tail, node)
          }

        case Nil =>
          rootNode
      }
    }

    val node = name match {
      case Logger.RootLoggerName =>
        rootNode
      case _ =>
        find(name.split("""\.""").toList, rootNode)
    }

    node.config match {
      case None         => rootNode.config.get
      case Some(config) => config
    }
  }

  /** 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 != "")
      throw new AVSLConfigException(errorMessage)
  }

  private def stringToOption(s: String): Option[String] =
    if (s == "") 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] ++
                  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 node = cursor.children.get(leaf) match {
            case Some(node) if (node.config != None) =>
              throw new AVSLConfigException(
                "Multiple loggers for " + node.config.get.pattern
              )

            case Some(node) =>
              // Previously filled-in stub node.
              LoggerConfigNode(leaf, Some(config), node.children)

            case None =>
              LoggerConfigNode(leaf, Some(config), noChildren)
          }
        
          cursor.children += (leaf -> node)
          node
        }

        case mid :: tail => {
          val node = cursor.children.get(mid) match {
            case Some(node) =>
              node
            case None =>
              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("Logger \"%s\" refers to unknown handler \"%s\""
               format (logger.name, 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 match {
          case None         => Nil
          case Some(config) => checkHandlers(config, config.handlerNames)
        }

      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 = 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 = 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) =  {
        "Handler \"" + handler.name + "\" refers to unknown " +
        "formatter \"" + handler.formatterName + "\""
      }

      handlers.values.filter(h => badFormatter(h.formatterName)).
        map(h => Some(badFormatterMessage(h))).map(_.get).mkString("\n")
    }

    doValidation match {
      case "" => None
      case s  => Some(s)
    }
  }
}

/**
  * 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) match {
      case Some(value) =>
        value
      case None =>
        throw new AVSLMissingRequiredOptionException(section.name,
                                                     option)
    }
  }

  protected def configuredLevel: LogLevel = {
    section.options.get(AVSLConfiguration.LevelKeyword) match {
      case Some(value) =>
        LogLevel.fromString(value) match {
          case Some(level) =>
            level
          case None =>
            throw new AVSLConfigSectionException(
              section.name, "Bad log level: \"" + value + "\""
            )
        }

      case None =>
        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 match {
        case None    => ""
        case Some(l) => l.handlerNames.mkString(", ")
      }

      def level = node.config match {
        case None    => ""
        case Some(l) => l.level.toString
      }

      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) match {
      case None =>
        throw new AVSLConfigException("Unknown formatter: \"" + name + "\"")
      case Some(cls) =>
        cls
    }
  }
}

// ---------------------------------------------------------------------------
// 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 match {
      case None      => None
      case Some(url) => Some(new AVSLConfiguration(url))
    }
  }

  private def find: Option[URL] = {
    def search(functions: List[() => Option[URL]]): Option[URL] = {
      functions match {
        case function :: Nil =>
          function()

        case function :: tail =>
          function() match {
            case None      => search(tail)
            case Some(url) => Some(url)
          }

        case Nil =>
          None
      }
    }

    search(SearchPath)
  }

  private def resource(): Option[URL] = {
    this.getClass.getClassLoader.getResource("avsl.conf") match {
      case null => None
      case url  => Some(url)
    }
  }

  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] = {
    val s = getValue
    if ((s == null) || (s.trim.length == 0))
      None
    else
      urlOrFile(label, s)
  }

  private def urlOrFile(label: String, s: String): Option[URL] = {
    try {
      Some(new URL(s))
    }

    catch {
      case _: MalformedURLException => {
        val f = new File(s)
        if (! f.exists) {
          println("Warning: " + label + " specifies nonexistent " +
                  "file \"" + f.getPath + "\"")
          None
        }
        else
          Some(f.toURI.toURL)
      }
    }
  }
}

/**
  * Utility methods.
  */
private[config] object Util {
  def lookupClass(name: Option[String],
                  aliases: Map[String, Class[_]]): Option[Class[_]] = {
    name match {
      case Some(name) if (aliases.keySet.contains(name)) =>
        Some(aliases(name))

      case Some(name) => {
        try {
          Some(Class.forName(name))
        }
        catch {
          case _: ClassNotFoundException =>
            throw new AVSLConfigException("Cannot load class " +
                                          name)
        }
      }

      case None =>
        None
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy