diff --git a/akka-actor/src/main/scala/akka/config/Config.scala b/akka-actor/src/main/scala/akka/config/Config.scala index 1be08b14ae..c5b2ea0c2d 100644 --- a/akka-actor/src/main/scala/akka/config/Config.scala +++ b/akka-actor/src/main/scala/akka/config/Config.scala @@ -5,8 +5,8 @@ package akka.config import akka.AkkaException -import akka.actor.{EventHandler} -import net.lag.configgy.{Config => CConfig, Configgy, ParseException} +import akka.actor.EventHandler +import akka.configgy.{Config => CConfig, Configgy, ParseException} import java.net.InetSocketAddress import java.lang.reflect.Method diff --git a/akka-actor/src/main/scala/akka/configgy/Attributes.scala b/akka-actor/src/main/scala/akka/configgy/Attributes.scala new file mode 100644 index 0000000000..7a071970ae --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/Attributes.scala @@ -0,0 +1,434 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.util.regex.Pattern +import javax.{management => jmx} +import scala.collection.{immutable, mutable, Map} +import scala.util.Sorting +import extensions._ + + +private[configgy] abstract class Cell +private[configgy] case class StringCell(value: String) extends Cell +private[configgy] case class AttributesCell(attr: Attributes) extends Cell +private[configgy] case class StringListCell(array: Array[String]) extends Cell + + +/** + * Actual implementation of ConfigMap. + * Stores items in Cell objects, and handles interpolation and key recursion. + */ +private[configgy] class Attributes(val config: Config, val name: String) extends ConfigMap { + + private val cells = new mutable.HashMap[String, Cell] + private var monitored = false + var inheritFrom: Option[ConfigMap] = None + + def this(config: Config, name: String, copyFrom: ConfigMap) = { + this(config, name) + copyFrom.copyInto(this) + } + + def keys: Iterator[String] = cells.keysIterator + + def getName() = name + + override def toString() = { + val buffer = new StringBuilder("{") + buffer ++= name + buffer ++= (inheritFrom match { + case Some(a: Attributes) => " (inherit=" + a.name + ")" + case None => "" + }) + buffer ++= ": " + for (key <- sortedKeys) { + buffer ++= key + buffer ++= "=" + buffer ++= (cells(key) match { + case StringCell(x) => "\"" + x.quoteC + "\"" + case AttributesCell(x) => x.toString + case StringListCell(x) => x.mkString("[", ",", "]") + }) + buffer ++= " " + } + buffer ++= "}" + buffer.toString + } + + override def equals(obj: Any) = { + if (! obj.isInstanceOf[Attributes]) { + false + } else { + val other = obj.asInstanceOf[Attributes] + (other.sortedKeys.toList == sortedKeys.toList) && + (cells.keys forall (k => { cells(k) == other.cells(k) })) + } + } + + /** + * Look up a value cell for a given key. If the key is compound (ie, + * "abc.xyz"), look up the first segment, and if it refers to an inner + * Attributes object, recursively look up that cell. If it's not an + * Attributes or it doesn't exist, return None. For a non-compound key, + * return the cell if it exists, or None if it doesn't. + */ + private def lookupCell(key: String): Option[Cell] = { + val elems = key.split("\\.", 2) + if (elems.length > 1) { + cells.get(elems(0)) match { + case Some(AttributesCell(x)) => x.lookupCell(elems(1)) + case None => inheritFrom match { + case Some(a: Attributes) => + a.lookupCell(key) + case _ => None + } + case _ => None + } + } else { + cells.get(elems(0)) match { + case x @ Some(_) => x + case None => inheritFrom match { + case Some(a: Attributes) => a.lookupCell(key) + case _ => None + } + } + } + } + + /** + * Determine if a key is compound (and requires recursion), and if so, + * return the nested Attributes block and simple key that can be used to + * make a recursive call. If the key is simple, return None. + * + * If the key is compound, but nested Attributes objects don't exist + * that match the key, an attempt will be made to create the nested + * Attributes objects. If one of the key segments already refers to an + * attribute that isn't a nested Attribute object, a ConfigException + * will be thrown. + * + * For example, for the key "a.b.c", the Attributes object for "a.b" + * and the key "c" will be returned, creating the "a.b" Attributes object + * if necessary. If "a" or "a.b" exists but isn't a nested Attributes + * object, then an ConfigException will be thrown. + */ + @throws(classOf[ConfigException]) + private def recurse(key: String): Option[(Attributes, String)] = { + val elems = key.split("\\.", 2) + if (elems.length > 1) { + val attr = (cells.get(elems(0)) match { + case Some(AttributesCell(x)) => x + case Some(_) => throw new ConfigException("Illegal key " + key) + case None => createNested(elems(0)) + }) + attr.recurse(elems(1)) match { + case ret @ Some((a, b)) => ret + case None => Some((attr, elems(1))) + } + } else { + None + } + } + + def replaceWith(newAttributes: Attributes): Unit = { + // stash away subnodes and reinsert them. + val subnodes = for ((key, cell @ AttributesCell(_)) <- cells.toList) yield (key, cell) + cells.clear + cells ++= newAttributes.cells + for ((key, cell) <- subnodes) { + newAttributes.cells.get(key) match { + case Some(AttributesCell(newattr)) => + cell.attr.replaceWith(newattr) + cells(key) = cell + case None => + cell.attr.replaceWith(new Attributes(config, "")) + } + } + } + + private def createNested(key: String): Attributes = { + val attr = new Attributes(config, if (name.equals("")) key else (name + "." + key)) + if (monitored) { + attr.setMonitored + } + cells(key) = new AttributesCell(attr) + attr + } + + def getString(key: String): Option[String] = { + lookupCell(key) match { + case Some(StringCell(x)) => Some(x) + case Some(StringListCell(x)) => Some(x.toList.mkString("[", ",", "]")) + case _ => None + } + } + + def getConfigMap(key: String): Option[ConfigMap] = { + lookupCell(key) match { + case Some(AttributesCell(x)) => Some(x) + case _ => None + } + } + + def configMap(key: String): ConfigMap = makeAttributes(key, true) + + private[configgy] def makeAttributes(key: String): Attributes = makeAttributes(key, false) + + private[configgy] def makeAttributes(key: String, withInherit: Boolean): Attributes = { + if (key == "") { + return this + } + recurse(key) match { + case Some((attr, name)) => + attr.makeAttributes(name, withInherit) + case None => + val cell = if (withInherit) lookupCell(key) else cells.get(key) + cell match { + case Some(AttributesCell(x)) => x + case Some(_) => throw new ConfigException("Illegal key " + key) + case None => createNested(key) + } + } + } + + def getList(key: String): Seq[String] = { + lookupCell(key) match { + case Some(StringListCell(x)) => x + case Some(StringCell(x)) => Array[String](x) + case _ => Array[String]() + } + } + + def setString(key: String, value: String): Unit = { + if (monitored) { + config.deepSet(name, key, value) + return + } + + recurse(key) match { + case Some((attr, name)) => attr.setString(name, value) + case None => cells.get(key) match { + case Some(AttributesCell(_)) => throw new ConfigException("Illegal key " + key) + case _ => cells.put(key, new StringCell(value)) + } + } + } + + def setList(key: String, value: Seq[String]): Unit = { + if (monitored) { + config.deepSet(name, key, value) + return + } + + recurse(key) match { + case Some((attr, name)) => attr.setList(name, value) + case None => cells.get(key) match { + case Some(AttributesCell(_)) => throw new ConfigException("Illegal key " + key) + case _ => cells.put(key, new StringListCell(value.toArray)) + } + } + } + + def setConfigMap(key: String, value: ConfigMap): Unit = { + if (monitored) { + config.deepSet(name, key, value) + return + } + + recurse(key) match { + case Some((attr, name)) => attr.setConfigMap(name, value) + case None => + val subName = if (name == "") key else (name + "." + key) + cells.get(key) match { + case Some(AttributesCell(_)) => + cells.put(key, new AttributesCell(new Attributes(config, subName, value))) + case None => + cells.put(key, new AttributesCell(new Attributes(config, subName, value))) + case _ => + throw new ConfigException("Illegal key " + key) + } + } + } + + def contains(key: String): Boolean = { + recurse(key) match { + case Some((attr, name)) => attr.contains(name) + case None => cells.contains(key) + } + } + + def remove(key: String): Boolean = { + if (monitored) { + return config.deepRemove(name, key) + } + + recurse(key) match { + case Some((attr, name)) => attr.remove(name) + case None => { + cells.removeKey(key) match { + case Some(_) => true + case None => false + } + } + } + } + + def asMap: Map[String, String] = { + var ret = immutable.Map.empty[String, String] + for ((key, value) <- cells) { + value match { + case StringCell(x) => ret = ret.update(key, x) + case StringListCell(x) => ret = ret.update(key, x.mkString("[", ",", "]")) + case AttributesCell(x) => + for ((k, v) <- x.asMap) { + ret = ret.update(key + "." + k, v) + } + } + } + ret + } + + def toConfigString: String = { + toConfigList().mkString("", "\n", "\n") + } + + private def toConfigList(): List[String] = { + val buffer = new mutable.ListBuffer[String] + for (key <- Sorting.stableSort(cells.keys.toList)) { + cells(key) match { + case StringCell(x) => + buffer += (key + " = \"" + x.quoteC + "\"") + case StringListCell(x) => + buffer += (key + " = [") + buffer ++= x.map { " \"" + _.quoteC + "\"," } + buffer += "]" + case AttributesCell(node) => + buffer += (key + node.inheritFrom.map { " (inherit=\"" + _.asInstanceOf[Attributes].name + "\")" }.getOrElse("") + " {") + buffer ++= node.toConfigList().map { " " + _ } + buffer += "}" + } + } + buffer.toList + } + + def subscribe(subscriber: Subscriber) = { + config.subscribe(name, subscriber) + } + + // substitute "$(...)" strings with looked-up vars + // (and find "\$" and replace them with "$") + private val INTERPOLATE_RE = """(? "" + case attr :: xs => attr.getString(key) match { + case Some(x) => x + case None => lookup(key, xs) + } + } + } + + s.regexSub(INTERPOLATE_RE) { m => + if (m.matched == "\\$") { + "$" + } else { + lookup(m.group(1), List(this, root, EnvironmentAttributes)) + } + } + } + + protected[configgy] def interpolate(key: String, s: String): String = { + recurse(key) match { + case Some((attr, name)) => attr.interpolate(this, s) + case None => interpolate(this, s) + } + } + + /* set this node as part of a monitored config tree. once this is set, + * all modification requests go through the root Config, so validation + * will happen. + */ + protected[configgy] def setMonitored: Unit = { + if (monitored) { + return + } + + monitored = true + for (cell <- cells.values) { + cell match { + case AttributesCell(x) => x.setMonitored + case _ => // pass + } + } + } + + protected[configgy] def isMonitored = monitored + + // make a deep copy of the Attributes tree. + def copy(): Attributes = { + copyInto(new Attributes(config, name)) + } + + def copyInto[T <: ConfigMap](attr: T): T = { + inheritFrom match { + case Some(a: Attributes) => a.copyInto(attr) + case _ => + } + for ((key, value) <- cells.elements) { + value match { + case StringCell(x) => attr(key) = x + case StringListCell(x) => attr(key) = x + case AttributesCell(x) => attr.setConfigMap(key, x.copy()) + } + } + attr + } + + def asJmxAttributes(): Array[jmx.MBeanAttributeInfo] = { + cells.map { case (key, value) => + value match { + case StringCell(_) => + new jmx.MBeanAttributeInfo(key, "java.lang.String", "", true, true, false) + case StringListCell(_) => + new jmx.MBeanAttributeInfo(key, "java.util.List", "", true, true, false) + case AttributesCell(_) => + null + } + }.filter { x => x ne null }.toList.toArray + } + + def asJmxDisplay(key: String): AnyRef = { + cells.get(key) match { + case Some(StringCell(x)) => x + case Some(StringListCell(x)) => java.util.Arrays.asList(x: _*) + case x => null + } + } + + def getJmxNodes(prefix: String, name: String): List[(String, JmxWrapper)] = { + (prefix + ":type=Config,name=" + (if (name == "") "(root)" else name), new JmxWrapper(this)) :: cells.flatMap { item => + val (key, value) = item + value match { + case AttributesCell(x) => + x.getJmxNodes(prefix, if (name == "") key else (name + "." + key)) + case _ => Nil + } + }.toList + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/Config.scala b/akka-actor/src/main/scala/akka/configgy/Config.scala new file mode 100644 index 0000000000..4b22167a2f --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/Config.scala @@ -0,0 +1,410 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.io.File +import java.lang.management.ManagementFactory +import javax.{management => jmx} +import scala.collection.{Map, Set} +import scala.collection.{immutable, mutable} +import extensions._ + + +private abstract class Phase +private case object VALIDATE_PHASE extends Phase +private case object COMMIT_PHASE extends Phase + + +private class SubscriptionNode { + var subscribers = new mutable.HashSet[Subscriber] + var map = new mutable.HashMap[String, SubscriptionNode] + + def get(name: String): SubscriptionNode = { + map.get(name) match { + case Some(x) => x + case None => + val node = new SubscriptionNode + map(name) = node + node + } + } + + override def toString() = { + val out = new StringBuilder("%d" format subscribers.size) + if (map.size > 0) { + out.append(" { ") + for (key <- map.keys) { + out.append(key) + out.append("=") + out.append(map(key).toString) + out.append(" ") + } + out.append("}") + } + out.toString + } + + @throws(classOf[ValidationException]) + def validate(key: List[String], current: Option[ConfigMap], replacement: Option[ConfigMap], phase: Phase): Unit = { + if ((current == None) && (replacement == None)) { + // someone has subscribed to a nonexistent node... ignore. + return + } + + // first, call all subscribers for this node. + for (subscriber <- subscribers) { + phase match { + case VALIDATE_PHASE => subscriber.validate(current, replacement) + case COMMIT_PHASE => subscriber.commit(current, replacement) + } + } + + /* if we're walking a key, lookup the next segment's subscribers and + * continue the validate/commit. if the key is exhausted, call + * subscribers for ALL nodes below this one. + */ + var nextNodes: Iterator[(String, SubscriptionNode)] = null + key match { + case Nil => nextNodes = map.elements + case segment :: _ => { + map.get(segment) match { + case None => return // done! + case Some(node) => nextNodes = Iterator.single((segment, node)) + } + } + } + + for ((segment, node) <- nextNodes) { + val subCurrent = current match { + case None => None + case Some(x) => x.getConfigMap(segment) + } + val subReplacement = replacement match { + case None => None + case Some(x) => x.getConfigMap(segment) + } + node.validate(if (key == Nil) Nil else key.tail, subCurrent, subReplacement, phase) + } + } +} + + +/** + * An attribute map of key/value pairs and subscriptions, where values may + * be other attribute maps. Config objects represent the "root" of a nested + * set of attribute maps, and control the flow of subscriptions and events + * for subscribers. + */ +class Config extends ConfigMap { + private var root = new Attributes(this, "") + private val subscribers = new SubscriptionNode + private val subscriberKeys = new mutable.HashMap[Int, (SubscriptionNode, Subscriber)] + private var nextKey = 1 + + private var jmxNodes: List[String] = Nil + private var jmxPackageName: String = "" + private var jmxSubscriptionKey: Option[SubscriptionKey] = None + private var reloadAction: Option[() => Unit] = None + + + /** + * Importer for resolving "include" lines when loading config files. + * By default, it's a FilesystemImporter based on the current working + * directory. + */ + var importer: Importer = new FilesystemImporter(new File(".").getCanonicalPath) + + + /** + * Read config data from a string and use it to populate this object. + */ + def load(data: String) { + reloadAction = Some(() => configure(data)) + reload() + } + + /** + * Read config data from a file and use it to populate this object. + */ + def loadFile(filename: String) { + reloadAction = Some(() => configure(importer.importFile(filename))) + reload() + } + + /** + * Read config data from a file and use it to populate this object. + */ + def loadFile(path: String, filename: String) { + importer = new FilesystemImporter(path) + loadFile(filename) + } + + /** + * Reloads the configuration from whatever source it was previously loaded + * from, undoing any in-memory changes. This is a no-op if the configuration + * data has not be loaded from a source (file or string). + */ + def reload() { + reloadAction.foreach(_()) + } + + private def configure(data: String) { + val newRoot = new Attributes(this, "") + new ConfigParser(newRoot, importer) parse data + + if (root.isMonitored) { + // throws exception if validation fails: + List(VALIDATE_PHASE, COMMIT_PHASE) foreach (p => subscribers.validate(Nil, Some(root), Some(newRoot), p)) + } + + if (root.isMonitored) newRoot.setMonitored + root.replaceWith(newRoot) + } + + override def toString = root.toString + + + // ----- subscriptions + + private[configgy] def subscribe(key: String, subscriber: Subscriber): SubscriptionKey = synchronized { + root.setMonitored + var subkey = nextKey + nextKey += 1 + var node = subscribers + if (key ne null) { + for (segment <- key.split("\\.")) { + node = node.get(segment) + } + } + node.subscribers += subscriber + subscriberKeys += Pair(subkey, (node, subscriber)) + new SubscriptionKey(this, subkey) + } + + private[configgy] def subscribe(key: String)(f: (Option[ConfigMap]) => Unit): SubscriptionKey = { + subscribe(key, new Subscriber { + def validate(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit = { } + def commit(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit = { + f(replacement) + } + }) + } + + def subscribe(subscriber: Subscriber) = subscribe(null.asInstanceOf[String], subscriber) + + override def subscribe(f: (Option[ConfigMap]) => Unit): SubscriptionKey = subscribe(null.asInstanceOf[String])(f) + + private[configgy] def unsubscribe(subkey: SubscriptionKey) = synchronized { + subscriberKeys.get(subkey.id) match { + case None => false + case Some((node, sub)) => { + node.subscribers -= sub + subscriberKeys -= subkey.id + true + } + } + } + + /** + * Return a formatted string of all the subscribers, useful for debugging. + */ + def debugSubscribers() = synchronized { + "subs=" + subscribers.toString + } + + /** + * Un-register this object from JMX. Any existing JMX nodes for this config object will vanish. + */ + def unregisterWithJmx() = { + val mbs = ManagementFactory.getPlatformMBeanServer() + for (name <- jmxNodes) mbs.unregisterMBean(new jmx.ObjectName(name)) + jmxNodes = Nil + for (key <- jmxSubscriptionKey) unsubscribe(key) + jmxSubscriptionKey = None + } + + /** + * Register this object as a tree of JMX nodes that can be used to view and modify the config. + * This has the effect of subscribing to the root node, in order to reflect changes to the + * config object in JMX. + * + * @param packageName the name (usually your app's package name) that config objects should + * appear inside + */ + def registerWithJmx(packageName: String): Unit = { + val mbs = ManagementFactory.getPlatformMBeanServer() + val nodes = root.getJmxNodes(packageName, "") + val nodeNames = nodes.map { case (name, bean) => name } + // register any new nodes + nodes.filter { name => !(jmxNodes contains name) }.foreach { case (name, bean) => + try { + mbs.registerMBean(bean, new jmx.ObjectName(name)) + } catch { + case x: jmx.InstanceAlreadyExistsException => + // happens in unit tests. + } + } + // unregister nodes that vanished + (jmxNodes -- nodeNames).foreach { name => mbs.unregisterMBean(new jmx.ObjectName(name)) } + + jmxNodes = nodeNames + jmxPackageName = packageName + if (jmxSubscriptionKey == None) { + jmxSubscriptionKey = Some(subscribe { _ => registerWithJmx(packageName) }) + } + } + + + // ----- modifications that happen within monitored Attributes nodes + + @throws(classOf[ValidationException]) + private def deepChange(name: String, key: String, operation: (ConfigMap, String) => Boolean): Boolean = synchronized { + val fullKey = if (name == "") (key) else (name + "." + key) + val newRoot = root.copy + val keyList = fullKey.split("\\.").toList + + if (! operation(newRoot, fullKey)) { + return false + } + + // throws exception if validation fails: + subscribers.validate(keyList, Some(root), Some(newRoot), VALIDATE_PHASE) + subscribers.validate(keyList, Some(root), Some(newRoot), COMMIT_PHASE) + + if (root.isMonitored) newRoot.setMonitored + root.replaceWith(newRoot) + true + } + + private[configgy] def deepSet(name: String, key: String, value: String) = { + deepChange(name, key, { (newRoot, fullKey) => newRoot(fullKey) = value; true }) + } + + private[configgy] def deepSet(name: String, key: String, value: Seq[String]) = { + deepChange(name, key, { (newRoot, fullKey) => newRoot(fullKey) = value; true }) + } + + private[configgy] def deepSet(name: String, key: String, value: ConfigMap) = { + deepChange(name, key, { (newRoot, fullKey) => newRoot.setConfigMap(fullKey, value); true }) + } + + private[configgy] def deepRemove(name: String, key: String): Boolean = { + deepChange(name, key, { (newRoot, fullKey) => newRoot.remove(fullKey) }) + } + + + // ----- implement AttributeMap by wrapping our root object: + + def getString(key: String): Option[String] = root.getString(key) + def getConfigMap(key: String): Option[ConfigMap] = root.getConfigMap(key) + def configMap(key: String): ConfigMap = root.configMap(key) + def getList(key: String): Seq[String] = root.getList(key) + def setString(key: String, value: String): Unit = root.setString(key, value) + def setList(key: String, value: Seq[String]): Unit = root.setList(key, value) + def setConfigMap(key: String, value: ConfigMap): Unit = root.setConfigMap(key, value) + def contains(key: String): Boolean = root.contains(key) + def remove(key: String): Boolean = root.remove(key) + def keys: Iterator[String] = root.keys + def asMap(): Map[String, String] = root.asMap() + def toConfigString = root.toConfigString + def copy(): ConfigMap = root.copy() + def copyInto[T <: ConfigMap](m: T): T = root.copyInto(m) + def inheritFrom = root.inheritFrom + def inheritFrom_=(config: Option[ConfigMap]) = root.inheritFrom=(config) + def getName(): String = root.name +} + + +object Config { + /** + * Create a config object from a config file of the given path + * and filename. The filename must be relative to the path. The path is + * used to resolve filenames given in "include" lines. + */ + def fromFile(path: String, filename: String): Config = { + val config = new Config + try { + config.loadFile(path, filename) + } catch { + case e: Throwable => + //Logger.get.critical(e, "Failed to load config file '%s/%s'", path, filename) + throw e + } + config + } + + /** + * Create a Config object from a config file of the given filename. + * The base folder will be extracted from the filename and used as a base + * path for resolving filenames given in "include" lines. + */ + def fromFile(filename: String): Config = { + val n = filename.lastIndexOf('/') + if (n < 0) { + fromFile(new File(".").getCanonicalPath, filename) + } else { + fromFile(filename.substring(0, n), filename.substring(n + 1)) + } + } + + /** + * Create a Config object from the given named resource inside this jar + * file, using the system class loader. "include" lines will also operate + * on resource paths. + */ + def fromResource(name: String): Config = { + fromResource(name, ClassLoader.getSystemClassLoader) + } + + /** + * Create a Config object from a string containing a config file's contents. + */ + def fromString(data: String): Config = { + val config = new Config + config.load(data) + config + } + + /** + * Create a Config object from the given named resource inside this jar + * file, using a specific class loader. "include" lines will also operate + * on resource paths. + */ + def fromResource(name: String, classLoader: ClassLoader): Config = { + val config = new Config + try { + config.importer = new ResourceImporter(classLoader) + config.loadFile(name) + } catch { + case e: Throwable => + //Logger.get.critical(e, "Failed to load config resource '%s'", name) + throw e + } + config + } + + /** + * Create a Config object from a map of String keys and String values. + */ + def fromMap(m: Map[String, String]) = { + val config = new Config + for ((k, v) <- m.elements) { + config(k) = v + } + config + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/ConfigMap.scala b/akka-actor/src/main/scala/akka/configgy/ConfigMap.scala new file mode 100644 index 0000000000..89c624564b --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/ConfigMap.scala @@ -0,0 +1,402 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import scala.collection.Map +import scala.util.Sorting + + +class ConfigException(reason: String) extends Exception(reason) + + +/** + * Abstract trait for a map of string keys to strings, string lists, or (nested) ConfigMaps. + * Integers and booleans may also be stored and retrieved, but they are converted to/from + * strings in the process. + */ +trait ConfigMap { + private val TRUE = "true" + private val FALSE = "false" + + + // ----- required methods + + /** + * Lookup an entry in this map, and if it exists and can be represented + * as a string, return it. Strings will be returned as-is, and string + * lists will be returned as a combined string. Nested AttributeMaps + * will return None as if there was no entry present. + */ + def getString(key: String): Option[String] + + /** + * Lookup an entry in this map, and if it exists and is a nested + * ConfigMap, return it. If the entry is a string or string list, + * it will return None as if there was no entry present. + */ + def getConfigMap(key: String): Option[ConfigMap] + + /** + * Lookup an entry in this map, and if it exists and is a nested + * ConfigMap, return it. If not, create an empty map with this name + * and return that. + * + * @throws ConfigException if the key already refers to a string or + * string list + */ + def configMap(key: String): ConfigMap + + /** + * Lookup an entry in this map, and if it exists and can be represented + * as a string list, return it. String lists will be returned as-is, and + * strings will be returned as an array of length 1. If the entry doesn't + * exist or is a nested ConfigMap, an empty sequence is returned. + */ + def getList(key: String): Seq[String] + + /** + * Set a key/value pair in this map. If an entry already existed with + * that key, it's replaced. + * + * @throws ConfigException if the key already refers to a nested + * ConfigMap + */ + def setString(key: String, value: String): Unit + + /** + * Set a key/value pair in this map. If an entry already existed with + * that key, it's replaced. + * + * @throws ConfigException if the key already refers to a nested + * ConfigMap + */ + def setList(key: String, value: Seq[String]): Unit + + /** + * Put a nested ConfigMap inside this one. If an entry already existed with + * that key, it's replaced. The ConfigMap is deep-copied at insert-time. + * + * @throws ConfigException if the key already refers to a value that isn't + * a nested ConfigMap + */ + def setConfigMap(key: String, value: ConfigMap): Unit + + /** + * Returns true if this map contains the given key. + */ + def contains(key: String): Boolean + + /** + * Remove an entry with the given key, if it exists. Returns true if + * an entry was actually removed, false if not. + */ + def remove(key: String): Boolean + + /** + * Return an iterator across the keys of this map. + */ + def keys: Iterator[String] + + /** + * Return a new (immutable) map containing a deep copy of all the keys + * and values from this AttributeMap. Keys from nested maps will be + * compound (like `"inner.name"`). + */ + def asMap(): Map[String, String] + + /** + * Subscribe to changes on this map. Any changes (including deletions) + * that occur on this node will be sent through the subscriber to + * validate and possibly commit. See {@link Subscriber} for details + * on the validate/commit process. + * + * @return a key which can be used to cancel the subscription + */ + def subscribe(subscriber: Subscriber): SubscriptionKey + + /** + * Make a deep copy of this ConfigMap. Any inheritance chains will be + * deep-copied, but the inheritance will not be preserved: the copied + * ConfigMap stands alone as its own set of objects, reflecting the + * frozen state of any inherited ConfigMaps. + */ + def copy(): ConfigMap + + /** + * Make this ConfigMap inherit default values from another ConfigMap. + * Any attributes that aren't explicitly set will fall back to the inherited + * ConfigMap on lookup. + */ + def inheritFrom_=(config: Option[ConfigMap]): Unit + + /** + * Return any ConfigMap that is used as a fall back on lookups. + */ + def inheritFrom: Option[ConfigMap] + + + // ----- convenience methods + + /** + * If the requested key is present, return its value. Otherwise, return + * the given default value. + */ + def getString(key: String, defaultValue: String): String = { + getString(key) match { + case Some(x) => x + case None => defaultValue + } + } + + /** + * If the requested key is present and can be converted into an int + * (via `String.toInt`), return that int. Otherwise, return `None`. + */ + def getInt(key: String): Option[Int] = { + getString(key) match { + case Some(x) => { + try { + Some(x.toInt) + } catch { + case _: NumberFormatException => None + } + } + case None => None + } + } + + /** + * If the requested key is present and can be converted into an int + * (via `String.toInt`), return that int. Otherwise, + * return the given default value. + */ + def getInt(key: String, defaultValue: Int): Int = { + getInt(key) match { + case Some(n) => n + case None => defaultValue + } + } + + /** + * If the requested key is present and can be converted into a long + * (via `String.toLong`), return that long. Otherwise, return `None`. + */ + def getLong(key: String): Option[Long] = { + getString(key) match { + case Some(x) => { + try { + Some(x.toLong) + } catch { + case _: NumberFormatException => None + } + } + case None => None + } + } + + /** + * If the requested key is present and can be converted into a long + * (via `String.toLong`), return that long. Otherwise, + * return the given default value. + */ + def getLong(key: String, defaultValue: Long): Long = { + getLong(key) match { + case Some(n) => n + case None => defaultValue + } + } + + /** + * If the requested key is present and can be converted into a double + * (via `String.toDouble`), return that double. Otherwise, return `None`. + */ + def getDouble(key: String): Option[Double] = { + getString(key) match { + case Some(x) => { + try { + Some(x.toDouble) + } catch { + case _: NumberFormatException => None + } + } + case None => None + } + } + + /** + * If the requested key is present and can be converted into a double + * (via `String.toDouble`), return that double. Otherwise, + * return the given default value. + */ + def getDouble(key: String, defaultValue: Double): Double = { + getDouble(key) match { + case Some(n) => n + case None => defaultValue + } + } + + /** + * If the requested key is present and can be converted into a bool + * (by being either `"true"` or `"false"`), + * return that bool. Otherwise, return `None`. + */ + def getBool(key: String): Option[Boolean] = { + getString(key) match { + case Some(x) => + if (x != TRUE && x != FALSE) throw new ConfigException("invalid boolean value") + Some(x.equals(TRUE)) + case None => None + } + } + + /** + * If the requested key is present and can be converted into a bool + * (by being either `"true"` or `"false"`), + * return that bool. Otherwise, return the given default value. + */ + def getBool(key: String, defaultValue: Boolean): Boolean = { + getBool(key) match { + case Some(b) => b + case None => defaultValue + } + } + + /** + * Set the given key to an int value, by converting it to a string + * first. + */ + def setInt(key: String, value: Int): Unit = setString(key, value.toString) + + /** + * Set the given key to a long value, by converting it to a string + * first. + */ + def setLong(key: String, value: Long): Unit = setString(key, value.toString) + + /** + * Set the given key to a double value, by converting it to a string + * first. + */ + def setDouble(key: String, value: Double): Unit = setString(key, value.toString) + + /** + * Set the given key to a bool value, by converting it to a string + * first. + */ + def setBool(key: String, value: Boolean): Unit = { + setString(key, if (value) TRUE else FALSE) + } + + /** + * Return the keys of this map, in sorted order. + */ + def sortedKeys() = { + // :( why does this have to be done manually? + val keys = this.keys.toList.toArray + Sorting.quickSort(keys) + keys + } + + /** + * Subscribe to changes on this ConfigMap, but don't bother with + * validating. Whenever this ConfigMap changes, a new copy will be + * passed to the given function. + */ + def subscribe(f: (Option[ConfigMap]) => Unit): SubscriptionKey = { + subscribe(new Subscriber { + def validate(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit = { } + def commit(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit = { + f(replacement) + } + }) + } + + /** + * Convert this ConfigMap into a string which could be written into a config + * file and parsed by configgy. + */ + def toConfigString: String + + def copyInto[T <: ConfigMap](configMap: T): T + + def copyInto(obj: AnyRef) { + val cls = obj.getClass + //val log = Logger.get(cls) + val methods = cls.getMethods().filter { method => + method.getName().endsWith("_$eq") && method.getParameterTypes().size == 1 + }.toList + keys.foreach { key => + val setters = methods.filter { _.getName() == key + "_$eq" } +/* if (setters.size == 0) { + log.warning("Ignoring config key '%s' which doesn't have a setter in class %s", key, cls) + }*/ + setters.foreach { method => + val expectedType = method.getParameterTypes().first.getCanonicalName + val param = expectedType match { + case "int" => getInt(key) + case "long" => getLong(key) + case "float" => getDouble(key).map { _.toFloat } + case "double" => getDouble(key) + case "boolean" => getBool(key) + case "java.lang.String" => getString(key) + case _ => None // ignore for now + } + param.map { p => method.invoke(obj, p.asInstanceOf[Object]) } + } + } + } + + /** + * If the requested key is present, return its value as a string. Otherwise, throw a + * ConfigException. `toInt` and `toBoolean` may be called on the + * returned string if an int or bool is desired. + */ + def apply(key: String): String = getString(key) match { + case None => throw new ConfigException("undefined config: " + key) + case Some(v) => v + } + + /** Equivalent to `getString(key, defaultValue)`. */ + def apply(key: String, defaultValue: String) = getString(key, defaultValue) + + /** Equivalent to `getInt(key, defaultValue)`. */ + def apply(key: String, defaultValue: Int) = getInt(key, defaultValue) + + /** Equivalent to `getLong(key, defaultValue)`. */ + def apply(key: String, defaultValue: Long) = getLong(key, defaultValue) + + /** Equivalent to `getBool(key, defaultValue)`. */ + def apply(key: String, defaultValue: Boolean) = getBool(key, defaultValue) + + /** Equivalent to `setString(key, value)`. */ + def update(key: String, value: String) = setString(key, value) + + /** Equivalent to `setInt(key, value)`. */ + def update(key: String, value: Int) = setInt(key, value) + + /** Equivalent to `setLong(key, value)`. */ + def update(key: String, value: Long) = setLong(key, value) + + /** Equivalent to `setBool(key, value)`. */ + def update(key: String, value: Boolean) = setBool(key, value) + + /** Equivalent to `setList(key, value)`. */ + def update(key: String, value: Seq[String]) = setList(key, value) + + /** Get the name of the current config map. */ + def getName(): String +} diff --git a/akka-actor/src/main/scala/akka/configgy/ConfigParser.scala b/akka-actor/src/main/scala/akka/configgy/ConfigParser.scala new file mode 100644 index 0000000000..8d8694f041 --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/ConfigParser.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import scala.collection.mutable.Stack +import scala.util.parsing.combinator._ +import scala.util.parsing.input.CharSequenceReader +import extensions._ + + +/** + * An exception thrown when parsing a config file, if there was an error + * during parsing. The `reason` string will contain the parsing + * error details. + */ +class ParseException(reason: String, cause: Throwable) extends Exception(reason, cause) { + def this(reason: String) = this(reason, null) + def this(cause: Throwable) = this(null, cause) +} + + +private[configgy] class ConfigParser(var attr: Attributes, val importer: Importer) extends RegexParsers { + + val sections = new Stack[String] + var prefix = "" + + // Stack reversed iteration order from 2.7 to 2.8!! + def sectionsString = sections.toList.reverse.mkString(".") + + // tokens + override val whiteSpace = """(\s+|#[^\n]*\n)+""".r + val numberToken: Parser[String] = """-?\d+(\.\d+)?""".r + val stringToken: Parser[String] = ("\"" + """([^\\\"]|\\[^ux]|\\\n|\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2})*""" + "\"").r + val identToken: Parser[String] = """([\da-zA-Z_][-\w]*)(\.[a-zA-Z_][-\w]*)*""".r + val assignToken: Parser[String] = """=|\?=""".r + val tagNameToken: Parser[String] = """[a-zA-Z][-\w]*""".r + + + def root = rep(includeFile | includeOptFile | assignment | toggle | sectionOpen | sectionClose | + sectionOpenBrace | sectionCloseBrace) + + def includeFile = "include" ~> string ^^ { + case filename: String => + new ConfigParser(attr.makeAttributes(sectionsString), importer) parse importer.importFile(filename) + } + + def includeOptFile = "include?" ~> string ^^ { + case filename: String => + new ConfigParser(attr.makeAttributes(sections.mkString(".")), importer) parse importer.importFile(filename, false) + } + + def assignment = identToken ~ assignToken ~ value ^^ { + case k ~ a ~ v => if (a match { + case "=" => true + case "?=" => ! attr.contains(prefix + k) + }) v match { + case x: Long => attr(prefix + k) = x + case x: String => attr(prefix + k) = x + case x: Array[String] => attr(prefix + k) = x + case x: Boolean => attr(prefix + k) = x + } + } + + def toggle = identToken ~ trueFalse ^^ { case k ~ v => attr(prefix + k) = v } + + def sectionOpen = "<" ~> tagNameToken ~ rep(tagAttribute) <~ ">" ^^ { + case name ~ attrList => openBlock(name, attrList) + } + def tagAttribute = opt(whiteSpace) ~> (tagNameToken <~ "=") ~ string ^^ { case k ~ v => (k, v) } + def sectionClose = " tagNameToken <~ ">" ^^ { name => closeBlock(Some(name)) } + + def sectionOpenBrace = tagNameToken ~ opt("(" ~> rep(tagAttribute) <~ ")") <~ "{" ^^ { + case name ~ attrListOption => openBlock(name, attrListOption.getOrElse(Nil)) + } + def sectionCloseBrace = "}" ^^ { x => closeBlock(None) } + + private def openBlock(name: String, attrList: List[(String, String)]) = { + val parent = if (sections.size > 0) attr.makeAttributes(sectionsString) else attr + sections push name + prefix = sectionsString + "." + val newBlock = attr.makeAttributes(sectionsString) + for ((k, v) <- attrList) k match { + case "inherit" => + newBlock.inheritFrom = Some(if (parent.getConfigMap(v).isDefined) parent.makeAttributes(v) else attr.makeAttributes(v)) + case _ => + throw new ParseException("Unknown block modifier") + } + } + + private def closeBlock(name: Option[String]) = { + if (sections.isEmpty) { + failure("dangling close tag") + } else { + val last = sections.pop + if (name.isDefined && last != name.get) { + failure("got closing tag for " + name.get + ", expected " + last) + } else { + prefix = if (sections.isEmpty) "" else sectionsString + "." + } + } + } + + + def value: Parser[Any] = number | string | stringList | trueFalse + def number = numberToken ^^ { x => if (x.contains('.')) x else x.toLong } + def string = stringToken ^^ { s => attr.interpolate(prefix, s.substring(1, s.length - 1).unquoteC) } + def stringList = "[" ~> repsep(string | numberToken, opt(",")) <~ (opt(",") ~ "]") ^^ { list => list.toArray } + def trueFalse: Parser[Boolean] = ("(true|on)".r ^^ { x => true }) | ("(false|off)".r ^^ { x => false }) + + + def parse(in: String): Unit = { + parseAll(root, in) match { + case Success(result, _) => result + case x @ Failure(msg, z) => throw new ParseException(x.toString) + case x @ Error(msg, _) => throw new ParseException(x.toString) + } + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/Configgy.scala b/akka-actor/src/main/scala/akka/configgy/Configgy.scala new file mode 100644 index 0000000000..1097fe145d --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/Configgy.scala @@ -0,0 +1,98 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.io.File + + +/** + * Main API entry point into the configgy library. + */ +object Configgy { + private var _config: Config = null + + /** + * The base Config object for this server. This will only be defined + * after calling one of `configure` or `configureFromResource`. + */ + def config = _config + + /** + * Sets the base Config object for this server. You might want to + * call one of the configure methods instead of this, but if those + * don't work for your needs, use this as a fallback. + */ + def config_=(c: Config) { + _config = c + } + + /** + * Configure the server by loading a config file from the given path + * and filename. The filename must be relative to the path. The path is + * used to resolve filenames given in "include" lines. + */ + def configure(path: String, filename: String): Unit = { + config = Config.fromFile(path, filename) + } + + /** + * Configure the server by loading a config file from the given filename. + * The base folder will be extracted from the filename and used as a base + * path for resolving filenames given in "include" lines. + */ + def configure(filename: String): Unit = { + val n = filename.lastIndexOf('/') + if (n < 0) { + configure(new File(".").getCanonicalPath, filename) + } else { + configure(filename.substring(0, n), filename.substring(n + 1)) + } + } + + /** + * Reload the previously-loaded config file from disk. Any changes will + * take effect immediately. **All** subscribers will be called to + * verify and commit the change (even if their nodes didn't actually + * change). + */ + def reload() = _config.reload() + + /** + * Configure the server by loading a config file from the given named + * resource inside this jar file. "include" lines will also operate + * on resource paths. + */ + def configureFromResource(name: String) = { + config = Config.fromResource(name) + } + + /** + * Configure the server by loading a config file from the given named + * resource inside this jar file, using a specific class loader. + * "include" lines will also operate on resource paths. + */ + def configureFromResource(name: String, classLoader: ClassLoader) = { + config = Config.fromResource(name, classLoader) + } + + /** + * Configure the server by loading config data from given string. + */ + def configureFromString(data: String) = { + config = Config.fromString(data) + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/EnvironmentAttributes.scala b/akka-actor/src/main/scala/akka/configgy/EnvironmentAttributes.scala new file mode 100644 index 0000000000..05a6c17664 --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/EnvironmentAttributes.scala @@ -0,0 +1,92 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.net.InetAddress +import scala.collection.{immutable, mutable} +import scala.collection.JavaConversions + +/** + * A ConfigMap that wraps the system environment. This is used as a + * fallback when looking up "$(...)" substitutions in config files. + */ +private[configgy] object EnvironmentAttributes extends ConfigMap { + + private val env = immutable.Map.empty[String, String] ++ (JavaConversions.asMap(System.getenv()).elements) + + // deal with java.util.Properties extending + // java.util.Hashtable[Object, Object] and not + // java.util.Hashtable[String, String] + private def getSystemProperties(): mutable.HashMap[String,String] = { + val map = new mutable.HashMap[String, String] + for (entry <- JavaConversions.asMap(System.getProperties()).elements) { + entry match { + case (k: String, v: String) => map.put(k, v) + case _ => + } + } + map + } + + def getName() = "" + + def getString(key: String): Option[String] = { + getSystemProperties().get(key).orElse(env.get(key)) + } + + def getConfigMap(key: String): Option[ConfigMap] = None + def configMap(key: String): ConfigMap = error("not implemented") + + def getList(key: String): Seq[String] = getString(key) match { + case None => Array[String]() + case Some(x) => Array[String](x) + } + + def setString(key: String, value: String): Unit = error("read-only attributes") + def setList(key: String, value: Seq[String]): Unit = error("read-only attributes") + def setConfigMap(key: String, value: ConfigMap): Unit = error("read-only attributes") + + def contains(key: String): Boolean = { + env.contains(key) || getSystemProperties().contains(key) + } + + def remove(key: String): Boolean = error("read-only attributes") + def keys: Iterator[String] = (getSystemProperties().keySet ++ env.keySet).elements + def asMap(): Map[String, String] = error("not implemented") + def toConfigString = error("not implemented") + def subscribe(subscriber: Subscriber): SubscriptionKey = error("not implemented") + def copy(): ConfigMap = this + def copyInto[T <: ConfigMap](m: T) = m + def inheritFrom: Option[ConfigMap] = None + def inheritFrom_=(config: Option[ConfigMap]) = error("not implemented") + + + try { + val addr = InetAddress.getLocalHost + val ip = addr.getHostAddress + val dns = addr.getHostName + + if (ip ne null) { + env("HOSTIP") = ip + } + if (dns ne null) { + env("HOSTNAME") = dns + } + } catch { + case _ => // pass + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/Importer.scala b/akka-actor/src/main/scala/akka/configgy/Importer.scala new file mode 100644 index 0000000000..51d1a1e3a4 --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/Importer.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.io.{BufferedReader, File, FileInputStream, InputStream, InputStreamReader} + + +/** + * An interface for finding config files and reading them into strings for + * parsing. This is used to handle `include` directives in config files. + */ +trait Importer { + /** + * Imports a requested file and returns the string contents of that file, + * if the file exists, and empty string if it does not exist and `required` + * is false. + * + * If the file couldn't be imported, throws a `ParseException`. + */ + @throws(classOf[ParseException]) + def importFile(filename: String, required: Boolean): String + + /** + * Imports a requested file and returns the string contents of that file. + * If the file couldn't be imported, throws a `ParseException`. + */ + @throws(classOf[ParseException]) + def importFile(filename: String): String = importFile(filename, true) + + private val BUFFER_SIZE = 8192 + + /** + * Exhaustively reads an InputStream and converts it into a String (using + * UTF-8 encoding). This is meant as a helper function for custom Importer + * classes. + * + * No exceptions are caught! + */ + protected def streamToString(in: InputStream): String = { + val reader = new BufferedReader(new InputStreamReader(in, "UTF-8")) + val buffer = new Array[Char](BUFFER_SIZE) + val out = new StringBuilder + var n = 0 + while (n >= 0) { + n = reader.read(buffer, 0, buffer.length) + if (n >= 0) { + out.append(buffer, 0, n) + } + } + try { + in.close() + } catch { + case _ => + } + out.toString + } +} + + +/** + * An Importer that looks for imported config files in the filesystem. + * This is the default importer. + */ +class FilesystemImporter(val baseFolder: String) extends Importer { + def importFile(filename: String, required: Boolean): String = { + var f = new File(filename) + if (! f.isAbsolute) { + f = new File(baseFolder, filename) + } + if (!required && !f.exists) { + "" + } else { + try { + streamToString(new FileInputStream(f)) + } catch { + case x => throw new ParseException(x.toString) + } + } + } +} + + +/** + * An Importer that looks for imported config files in the java resources + * of the system class loader (usually the jar used to launch this app). + */ +class ResourceImporter(classLoader: ClassLoader) extends Importer { + def importFile(filename: String, required: Boolean): String = { + try { + val stream = classLoader.getResourceAsStream(filename) + if (stream eq null) { + if (required) { + throw new ParseException("Can't find resource: " + filename) + } + "" + } else { + streamToString(stream) + } + } catch { + case x => throw new ParseException(x.toString) + } + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/JmxWrapper.scala b/akka-actor/src/main/scala/akka/configgy/JmxWrapper.scala new file mode 100644 index 0000000000..17c7d62a8b --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/JmxWrapper.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import javax.{management => jmx} +import scala.collection.JavaConversions + +class JmxWrapper(node: Attributes) extends jmx.DynamicMBean { + + //private val log = Logger.get + + val operations: Array[jmx.MBeanOperationInfo] = Array( + new jmx.MBeanOperationInfo("set", "set a string value", + Array( + new jmx.MBeanParameterInfo("key", "java.lang.String", "config key"), + new jmx.MBeanParameterInfo("value", "java.lang.String", "string value") + ), "void", jmx.MBeanOperationInfo.ACTION), + new jmx.MBeanOperationInfo("remove", "remove a value", + Array( + new jmx.MBeanParameterInfo("key", "java.lang.String", "config key") + ), "void", jmx.MBeanOperationInfo.ACTION), + new jmx.MBeanOperationInfo("add_list", "append a value to a list", + Array( + new jmx.MBeanParameterInfo("key", "java.lang.String", "config key"), + new jmx.MBeanParameterInfo("value", "java.lang.String", "value") + ), "void", jmx.MBeanOperationInfo.ACTION), + new jmx.MBeanOperationInfo("remove_list", "remove a value to a list", + Array( + new jmx.MBeanParameterInfo("key", "java.lang.String", "config key"), + new jmx.MBeanParameterInfo("value", "java.lang.String", "value") + ), "void", jmx.MBeanOperationInfo.ACTION) + ) + + def getMBeanInfo() = { + new jmx.MBeanInfo("akka.configgy.ConfigMap", "configuration node", node.asJmxAttributes(), + null, operations, null, new jmx.ImmutableDescriptor("immutableInfo=false")) + } + + def getAttribute(name: String): AnyRef = node.asJmxDisplay(name) + + def getAttributes(names: Array[String]): jmx.AttributeList = { + val rv = new jmx.AttributeList + for (name <- names) rv.add(new jmx.Attribute(name, getAttribute(name))) + rv + } + + def invoke(actionName: String, params: Array[Object], signature: Array[String]): AnyRef = { + actionName match { + case "set" => + params match { + case Array(name: String, value: String) => + try { + node.setString(name, value) + } catch { + case e: Exception => + //log.warning("exception: %s", e.getMessage) + throw e + } + case _ => + throw new jmx.MBeanException(new Exception("bad signature " + params.toList.toString)) + } + case "remove" => + params match { + case Array(name: String) => + node.remove(name) + case _ => + throw new jmx.MBeanException(new Exception("bad signature " + params.toList.toString)) + } + case "add_list" => + params match { + case Array(name: String, value: String) => + node.setList(name, node.getList(name).toList ++ List(value)) + case _ => + throw new jmx.MBeanException(new Exception("bad signature " + params.toList.toString)) + } + case "remove_list" => + params match { + case Array(name: String, value: String) => + node.setList(name, node.getList(name).toList - value) + case _ => + throw new jmx.MBeanException(new Exception("bad signature " + params.toList.toString)) + } + case _ => + throw new jmx.MBeanException(new Exception("no such method")) + } + null + } + + def setAttribute(attr: jmx.Attribute): Unit = { + attr.getValue() match { + case s: String => + node.setString(attr.getName(), s) + case _ => + throw new jmx.InvalidAttributeValueException() + } + } + + def setAttributes(attrs: jmx.AttributeList): jmx.AttributeList = { + for (attr <- JavaConversions.asBuffer(attrs.asList)) setAttribute(attr) + attrs + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/RuntimeEnvironment.scala b/akka-actor/src/main/scala/akka/configgy/RuntimeEnvironment.scala new file mode 100644 index 0000000000..18a4fab96a --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/RuntimeEnvironment.scala @@ -0,0 +1,143 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import java.io.File +import java.util.Properties +import scala.collection.mutable +import extensions._ + + +/** + * Use information in a local `build.properties` file to determine runtime + * environment info like the package name, version, and installation path. + * This can be used to automatically load config files from a `config/` path + * relative to the executable jar. + * + * An example of how to generate a `build.properties` file is included in + * configgy's ant files, and also in the "scala-build" github project here: + * + * + * You have to pass in a class from your package in order to identify the + * location of the `build.properties` file. + */ +class RuntimeEnvironment(cls: Class[_]) { + // load build info, if present. + private var buildProperties = new Properties + try { + buildProperties.load(cls.getResource("build.properties").openStream) + } catch { + case _ => + } + + val jarName = buildProperties.getProperty("name", "unknown") + val jarVersion = buildProperties.getProperty("version", "0.0") + val jarBuild = buildProperties.getProperty("build_name", "unknown") + val jarBuildRevision = buildProperties.getProperty("build_revision", "unknown") + val stageName = System.getProperty("stage", "production") + val savedOverrides = new mutable.HashMap[String, String] + + + /** + * Return the path this jar was executed from. Depends on the presence of + * a valid `build.properties` file. Will return `None` if it couldn't + * figure out the environment. + */ + lazy val jarPath: Option[String] = { + val paths = System.getProperty("java.class.path").split(System.getProperty("path.separator")) + findCandidateJar(paths, jarName, jarVersion).flatMap { path => + val parent = new File(path).getParentFile + if (parent == null) None else Some(parent.getCanonicalPath) + } + } + + def findCandidateJar(paths: Seq[String], name: String, version: String): Option[String] = { + val pattern = ("(.*?)" + name + "(?:_[\\d.]+)?-" + version + "\\.jar$").r + paths.find { path => + pattern.findFirstIn(path).isDefined + } + } + + /** + * Config filename, as determined from this jar's runtime path, possibly + * overridden by a command-line option. + */ + var configFilename: String = jarPath match { + case Some(path) => path + "/config/" + stageName + ".conf" + case None => "/etc/" + jarName + ".conf" + } + + /** + * Perform baseline command-line argument parsing. Responds to `--help`, + * `--version`, and `-f` (which overrides the config filename). + */ + def parseArgs(args: List[String]): Unit = { + args match { + case "-f" :: filename :: xs => + configFilename = filename + parseArgs(xs) + case "-D" :: keyval :: xs => + keyval.split("=", 2).toList match { + case key :: value :: Nil => + savedOverrides(key) = value + parseArgs(xs) + case _ => + println("Unknown -D option (must be '-D key=value'): " + keyval) + help + } + case "--help" :: xs => + help + case "--version" :: xs => + println("%s %s (%s)".format(jarName, jarVersion, jarBuild)) + case Nil => + case unknown :: _ => + println("Unknown command-line option: " + unknown) + help + } + } + + private def help = { + println + println("%s %s (%s)".format(jarName, jarVersion, jarBuild)) + println("options:") + println(" -f ") + println(" load config file (default: %s)".format(configFilename)) + println + System.exit(0) + } + + /** + * Parse any command-line arguments (using `parseArgs`) and then load the + * config file as determined by `configFilename` into the default config + * block. + */ + def load(args: Array[String]) = { + savedOverrides.clear() + val choppedArgs = args.flatMap { arg => + if (arg.length > 2 && arg.startsWith("-D")) { + List("-D", arg.substring(2)) + } else { + List(arg) + } + } + parseArgs(choppedArgs.toList) + Configgy.configure(configFilename) + for ((key, value) <- savedOverrides) { + Configgy.config(key) = value + } + } +} diff --git a/akka-actor/src/main/scala/akka/configgy/Subscriber.scala b/akka-actor/src/main/scala/akka/configgy/Subscriber.scala new file mode 100644 index 0000000000..789d07f7fa --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/Subscriber.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + + +/** + * Interface for receiving notifications of changes to a {@link Config} + * object. When subscribed to an {@link AttributeMap} node, these methods + * will be called to validate, and optionally commit, any changes that would + * affect that node. + * + *

Changes happen in two phases: First, any affected subscribers are asked + * to validate the change by having their {@link #validate} method called. + * If any validate method throws a ValidationException, + * the change is aborted, and the exception is thrown to the code that tried + * to make that change. If all of the validate methods return + * successfully, the {@link #commit} methods will be called to confirm that + * the change has been validated and is being committed. + */ +trait Subscriber { + /** + * Validate a potential change to the subscribed config node. It the node + * didn't exist prior to this potential change, current will + * be None. Similarly, if the node is being removed by this + * change, replacement will be None. Never will + * both parameters be None. + * + *

To reject the change, throw ValidationException. A + * normal return validates the config change and potentially permits it to + * be committed. + * + * @param current the current config node, if it exists + * @param replacement the new config node, if it will exist + */ + @throws(classOf[ValidationException]) + def validate(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit + + /** + * Commit this change to the subscribed config node. If this method is + * called, a prior call to validate with these parameters + * succeeded for all subscribers, and the change is now active. As with + * validate, either current or + * replacement (but not both) may be None. + * + * @param current the current (now previous) config node, if it existed + * @param replacement the new (now current) config node, if it exists + */ + def commit(current: Option[ConfigMap], replacement: Option[ConfigMap]): Unit +} + + +/** + * Key returned by a call to AttributeMap.subscribe which may + * be used to unsubscribe from config change events. + */ +class SubscriptionKey private[configgy](val config: Config, private[configgy] val id: Int) { + /** + * Remove the subscription referenced by this key. After unsubscribing, + * no more validate/commit events will be sent to this subscriber. + */ + def unsubscribe() = config.unsubscribe(this) +} + + +/** + * Exception thrown by Subscriber.validate when a config change + * must be rejected. If returned by a modification to a {@link Config} or + * {@link AttributeMap} node, the modification has failed. + */ +class ValidationException(reason: String) extends Exception(reason) diff --git a/akka-actor/src/main/scala/akka/configgy/extensions.scala b/akka-actor/src/main/scala/akka/configgy/extensions.scala new file mode 100644 index 0000000000..6936c3eaec --- /dev/null +++ b/akka-actor/src/main/scala/akka/configgy/extensions.scala @@ -0,0 +1,156 @@ +/* + * Copyright 2009 Robey Pointer + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package akka.configgy + +import scala.util.matching.Regex + + +final class ConfiggyString(wrapped: String) { + /** + * For every section of a string that matches a regular expression, call + * a function to determine a replacement (as in python's + * `re.sub`). The function will be passed the Matcher object + * corresponding to the substring that matches the pattern, and that + * substring will be replaced by the function's result. + * + * For example, this call: + * + * "ohio".regexSub("""h.""".r) { m => "n" } + * + * will return the string `"ono"`. + * + * The matches are found using `Matcher.find()` and so + * will obey all the normal java rules (the matches will not overlap, + * etc). + * + * @param re the regex pattern to replace + * @param replace a function that takes Regex.MatchData objects and + * returns a string to substitute + * @return the resulting string with replacements made + */ + def regexSub(re: Regex)(replace: (Regex.MatchData => String)): String = { + var offset = 0 + var out = new StringBuilder + + for (m <- re.findAllIn(wrapped).matchData) { + if (m.start > offset) { + out.append(wrapped.substring(offset, m.start)) + } + + out.append(replace(m)) + offset = m.end + } + + if (offset < wrapped.length) { + out.append(wrapped.substring(offset)) + } + out.toString + } + + private val QUOTE_RE = "[\u0000-\u001f\u007f-\uffff\\\\\"]".r + + /** + * Quote a string so that unprintable chars (in ASCII) are represented by + * C-style backslash expressions. For example, a raw linefeed will be + * translated into "\n". Control codes (anything below 0x20) + * and unprintables (anything above 0x7E) are turned into either + * "\xHH" or "\\uHHHH" expressions, depending on + * their range. Embedded backslashes and double-quotes are also quoted. + * + * @return a quoted string, suitable for ASCII display + */ + def quoteC(): String = { + regexSub(QUOTE_RE) { m => + m.matched.charAt(0) match { + case '\r' => "\\r" + case '\n' => "\\n" + case '\t' => "\\t" + case '"' => "\\\"" + case '\\' => "\\\\" + case c => + if (c <= 255) { + "\\x%02x".format(c.asInstanceOf[Int]) + } else { + "\\u%04x" format c.asInstanceOf[Int] + } + } + } + } + + // we intentionally don't unquote "\$" here, so it can be used to escape interpolation later. + private val UNQUOTE_RE = """\\(u[\dA-Fa-f]{4}|x[\dA-Fa-f]{2}|[/rnt\"\\])""".r + + /** + * Unquote an ASCII string that has been quoted in a style like + * {@link #quoteC} and convert it into a standard unicode string. + * "\\uHHHH" and "\xHH" expressions are unpacked + * into unicode characters, as well as "\r", "\n", + * "\t", "\\", and '\"'. + * + * @return an unquoted unicode string + */ + def unquoteC() = { + regexSub(UNQUOTE_RE) { m => + val ch = m.group(1).charAt(0) match { + // holy crap! this is terrible: + case 'u' => Character.valueOf(Integer.valueOf(m.group(1).substring(1), 16).asInstanceOf[Int].toChar) + case 'x' => Character.valueOf(Integer.valueOf(m.group(1).substring(1), 16).asInstanceOf[Int].toChar) + case 'r' => '\r' + case 'n' => '\n' + case 't' => '\t' + case x => x + } + ch.toString + } + } + + /** + * Turn a string of hex digits into a byte array. This does the exact + * opposite of `Array[Byte]#hexlify`. + */ + def unhexlify(): Array[Byte] = { + val buffer = new Array[Byte](wrapped.length / 2) + for (i <- 0.until(wrapped.length, 2)) { + buffer(i/2) = Integer.parseInt(wrapped.substring(i, i+2), 16).toByte + } + buffer + } +} + + +final class ConfiggyByteArray(wrapped: Array[Byte]) { + /** + * Turn an Array[Byte] into a string of hex digits. + */ + def hexlify(): String = { + val out = new StringBuffer + for (b <- wrapped) { + val s = (b.toInt & 0xff).toHexString + if (s.length < 2) { + out append '0' + } + out append s + } + out.toString + } +} + + +object extensions { + implicit def stringToConfiggyString(s: String): ConfiggyString = new ConfiggyString(s) + implicit def byteArrayToConfiggyByteArray(b: Array[Byte]): ConfiggyByteArray = new ConfiggyByteArray(b) +} diff --git a/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala b/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala index 357b5e9e80..3a53275417 100644 --- a/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala +++ b/akka-actor/src/main/scala/akka/dispatch/Dispatchers.scala @@ -9,7 +9,7 @@ import akka.actor.newUuid import akka.config.Config._ import akka.util.{Duration} -import net.lag.configgy.ConfigMap +import akka.configgy.ConfigMap import java.util.concurrent.ThreadPoolExecutor.{AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy, DiscardPolicy} import java.util.concurrent.TimeUnit diff --git a/akka-actor/src/test/scala/akka/dispatch/DispatchersSpec.scala b/akka-actor/src/test/scala/akka/dispatch/DispatchersSpec.scala index d09c088c99..b1eace3344 100644 --- a/akka-actor/src/test/scala/akka/dispatch/DispatchersSpec.scala +++ b/akka-actor/src/test/scala/akka/dispatch/DispatchersSpec.scala @@ -7,7 +7,7 @@ import java.util.concurrent.{CountDownLatch, TimeUnit} import org.scalatest.junit.JUnitSuite import org.junit.Test -import net.lag.configgy.Config +import akka.configgy.Config import scala.reflect.{Manifest} import akka.dispatch._ diff --git a/project/build/AkkaProject.scala b/project/build/AkkaProject.scala index f15462e694..c6208b5d4f 100644 --- a/project/build/AkkaProject.scala +++ b/project/build/AkkaProject.scala @@ -133,8 +133,6 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { lazy val commons_io = "commons-io" % "commons-io" % "2.0.1" % "compile" //ApacheV2 - lazy val configgy = "net.lag" % "configgy" % "2.0.2-nologgy" % "compile" //ApacheV2 - lazy val javax_servlet_30 = "org.glassfish" % "javax.servlet" % JAVAX_SERVLET_VERSION % "provided" //CDDL v1 lazy val jetty = "org.eclipse.jetty" % "jetty-server" % JETTY_VERSION % "compile" //Eclipse license @@ -297,8 +295,6 @@ class AkkaParentProject(info: ProjectInfo) extends DefaultProject(info) { // ------------------------------------------------------------------------------------------------------------------- class AkkaActorProject(info: ProjectInfo) extends AkkaDefaultProject(info, distPath) { - val configgy = Dependencies.configgy - // testing val junit = Dependencies.junit val scalatest = Dependencies.scalatest