简体   繁体   中英

How do I provide basic configuration for a Scala application?

I am working on a small GUI application written in Scala. There are a few settings that the user will set in the GUI and I want them to persist between program executions. Basically I want a scala.collections.mutable.Map that automatically persists to a file when modified.

This seems like it must be a common problem, but I have been unable to find a lightweight solution. How is this problem typically solved?

I do a lot of this, and I use .properties files (it's idiomatic in Java-land). I keep my config pretty straight-forward by design, though. If you have nested config constructs you might want a different format like YAML (if humans are the main authors) or JSON or XML (if machines are the authors).

Here's some example code for loading props, manipulating as Scala Map, then saving as .properties again:

import java.io._
import java.util._
import scala.collection.JavaConverters._

val f = new File("test.properties")
// test.properties:
//   foo=bar
//   baz=123

val props = new Properties

// Note: in real code make sure all these streams are 
// closed carefully in try/finally
val fis = new InputStreamReader(new FileInputStream(f), "UTF-8")
props.load(fis)
fis.close()

println(props) // {baz=123, foo=bar}

val map = props.asScala // Get to Scala Map via JavaConverters
map("foo") = "42"
map("quux") = "newvalue"

println(map)   // Map(baz -> 123, quux -> newvalue, foo -> 42)
println(props) // {baz=123, quux=newvalue, foo=42}

val fos = new OutputStreamWriter(new FileOutputStream(f), "UTF-8")
props.store(fos, "")
fos.close()

Here's an example of using XML and a case class for reading a config. A real class can be nicer than a map. (You could also do what sbt and at least one project do, take the config as Scala source and compile it in; saving it is less automatic. Or as a repl script. I haven't googled, but someone must have done that.)

Here's the simpler code.

This version uses a case class:

case class PluginDescription(name: String, classname: String) {
  def toXML: Node = {
    <plugin>
      <name>{name}</name>
      <classname>{classname}</classname>
    </plugin>
  }
}
object PluginDescription {

  def fromXML(xml: Node): PluginDescription = {
    // extract one field
    def getField(field: String): Option[String] = {
      val text = (xml \\ field).text.trim
      if (text == "") None else Some(text)
    }
    def extracted = {
      val name = "name"
      val claas = "classname"
      val vs = Map(name -> getField(name), claas -> getField(claas))
      if (vs.values exists (_.isEmpty)) fail()
      else PluginDescription(name = vs(name).get, classname = vs(claas).get)
    }
    def fail() = throw new RuntimeException("Bad plugin descriptor.")
    // check the top-level tag
    xml match {
      case <plugin>{_*}</plugin>  => extracted
      case _                      => fail()
    }
  }
}

This code reflectively calls the apply of a case class. The use case is that fields missing from config can be supplied by default args. No type conversions here. Eg, case class Config(foo: String = "bar") .

// isn't it easier to write a quick loop to reflect the field names?
import scala.reflect.runtime.{currentMirror => cm, universe => ru}
import ru._

def fromXML(xml: Node): Option[PluginDescription] = {
  def extract[A]()(implicit tt: TypeTag[A]): Option[A] = {
    // extract one field
    def getField(field: String): Option[String] = {
      val text = (xml \\ field).text.trim
      if (text == "") None else Some(text)
    } 

    val apply = ru.newTermName("apply")
    val module = ru.typeOf[A].typeSymbol.companionSymbol.asModule
    val ts = module.moduleClass.typeSignature
    val m = (ts member apply).asMethod
    val im = cm reflect (cm reflectModule module).instance
    val mm = im reflectMethod m

    def getDefault(i: Int): Option[Any] = {
      val n = ru.newTermName("apply$default$" + (i+1))
      val m = ts member n
      if (m == NoSymbol) None
      else Some((im reflectMethod m.asMethod)())
    } 
    def extractArgs(pss: List[List[Symbol]]): List[Option[Any]] =
      pss.flatten.zipWithIndex map (p => getField(p._1.name.encoded) orElse getDefault(p._2))
    val args = extractArgs(m.paramss)
    if (args exists (!_.isDefined)) None
    else Some(mm(args.flatten: _*).asInstanceOf[A])
  } 
  // check the top-level tag
  xml match {
    case <plugin>{_*}</plugin>  => extract[PluginDescription]()
    case _                      => None
  } 
} 

XML has loadFile and save , it's too bad there seems to be no one-liner for Properties .

$ scala
Welcome to Scala version 2.10.0-RC5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_06).
Type in expressions to have them evaluated.
Type :help for more information.

scala> import reflect.io._
import reflect.io._

scala> import java.util._
import java.util._

scala> import java.io.{StringReader, File=>JFile}
import java.io.{StringReader, File=>JFile}

scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._

scala> val p = new Properties
p: java.util.Properties = {}

scala> p load new StringReader(
     | (new File(new JFile("t.properties"))).slurp)

scala> p.asScala
res2: scala.collection.mutable.Map[String,String] = Map(foo -> bar)

As it all boils down to serializing a map / object to a file, your choices are:

  • classic serialization to Bytecode
  • serialization to XML
  • serialization to JSON (easy using Jackson, or Lift-JSON)
  • use of a properties file (ugly, no utf-8 support)
  • serialization to a proprietary format (ugly, why reinvent the wheel)

I suggest to convert Map to Properties and vice versa. "*.properties" files are standard for storing configuration in Java world, why not use it for Scala?

The common way are *. properties, *.xml, since scala supports xml natively, so it would be easier using xml config then in java.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM