From c1e4b9b3df127ac5f2e07c5add8df34f6f4fd1d4 Mon Sep 17 00:00:00 2001 From: Derrick Burns Date: Mon, 9 Jun 2014 19:06:03 -0700 Subject: [PATCH] Added support for YAML files. --- config/build.sbt | 2 + .../com/typesafe/config/ConfigSyntax.java | 7 +- .../com/typesafe/config/impl/Parseable.java | 2 + .../typesafe/config/impl/SimpleIncluder.java | 19 +- .../com/typesafe/config/impl/YAMLParser.java | 198 ++++++++++++++++++ 5 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/impl/YAMLParser.java diff --git a/config/build.sbt b/config/build.sbt index d98130a8..4890a619 100644 --- a/config/build.sbt +++ b/config/build.sbt @@ -18,6 +18,8 @@ libraryDependencies += "net.liftweb" %% "lift-json" % "2.5" % "test" libraryDependencies += "com.novocode" % "junit-interface" % "0.10-M4" % "test" +libraryDependencies += "org.yaml" % "snakeyaml" % "1.12" + externalResolvers += "Scala Tools Snapshots" at "http://scala-tools.org/repo-snapshots/" seq(findbugsSettings : _*) diff --git a/config/src/main/java/com/typesafe/config/ConfigSyntax.java b/config/src/main/java/com/typesafe/config/ConfigSyntax.java index ed560296..50e399f3 100644 --- a/config/src/main/java/com/typesafe/config/ConfigSyntax.java +++ b/config/src/main/java/com/typesafe/config/ConfigSyntax.java @@ -32,5 +32,10 @@ public enum ConfigSyntax { * >Java properties format. Associated with the .properties * file extension and text/x-java-properties Content-Type. */ - PROPERTIES; + PROPERTIES, + /** + * Standard YAML 1.2 format. Associated with the + * .yml file extension. + */ + YAML; } diff --git a/config/src/main/java/com/typesafe/config/impl/Parseable.java b/config/src/main/java/com/typesafe/config/impl/Parseable.java index 70374267..db447516 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parseable.java +++ b/config/src/main/java/com/typesafe/config/impl/Parseable.java @@ -217,6 +217,8 @@ public abstract class Parseable implements ConfigParseable { ConfigParseOptions finalOptions) throws IOException { if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { return PropertiesParser.parse(reader, origin); + } else if( finalOptions.getSyntax() == ConfigSyntax.YAML ) { + return YAMLParser.parse(reader, origin); } else { Iterator tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); return Parser.parse(tokens, origin, finalOptions, includeContext()); diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleIncluder.java b/config/src/main/java/com/typesafe/config/impl/SimpleIncluder.java index 2cde2f92..4d076d27 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleIncluder.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleIncluder.java @@ -166,7 +166,7 @@ class SimpleIncluder implements FullIncluder { // loading app.{conf,json,properties} from the filesystem. static ConfigObject fromBasename(NameSource source, String name, ConfigParseOptions options) { ConfigObject obj; - if (name.endsWith(".conf") || name.endsWith(".json") || name.endsWith(".properties")) { + if (name.endsWith(".conf") || name.endsWith(".json") || name.endsWith(".properties") || name.endsWith(".yml")) { ConfigParseable p = source.nameToParseable(name, options); obj = p.parse(p.options().setAllowMissing(options.getAllowMissing())); @@ -174,6 +174,8 @@ class SimpleIncluder implements FullIncluder { ConfigParseable confHandle = source.nameToParseable(name + ".conf", options); ConfigParseable jsonHandle = source.nameToParseable(name + ".json", options); ConfigParseable propsHandle = source.nameToParseable(name + ".properties", options); + ConfigParseable ymlHandle = source.nameToParseable(name + ".yml", options); + boolean gotSomething = false; List fails = new ArrayList(); @@ -200,6 +202,17 @@ class SimpleIncluder implements FullIncluder { fails.add(e); } } + + if (syntax == null || syntax == ConfigSyntax.YAML) { + try { + ConfigObject parsed = ymlHandle.parse(jsonHandle.options() + .setAllowMissing(false).setSyntax(ConfigSyntax.YAML)); + obj = obj.withFallback(parsed); + gotSomething = true; + } catch (ConfigException.IO e) { + fails.add(e); + } + } if (syntax == null || syntax == ConfigSyntax.PROPERTIES) { try { @@ -217,7 +230,7 @@ class SimpleIncluder implements FullIncluder { // the individual exceptions should have been logged already // with tracing enabled ConfigImpl.trace("Did not find '" + name - + "' with any extension (.conf, .json, .properties); " + + "' with any extension (.conf, .json, .properties, .yml); " + "exceptions should have been logged above."); } @@ -238,7 +251,7 @@ class SimpleIncluder implements FullIncluder { } else if (!gotSomething) { if (ConfigImpl.traceLoadsEnabled()) { ConfigImpl.trace("Did not find '" + name - + "' with any extension (.conf, .json, .properties); but '" + name + + "' with any extension (.conf, .json, .properties, .yml); but '" + name + "' is allowed to be missing. Exceptions from load attempts should have been logged above."); } } diff --git a/config/src/main/java/com/typesafe/config/impl/YAMLParser.java b/config/src/main/java/com/typesafe/config/impl/YAMLParser.java new file mode 100644 index 00000000..81a0b4d0 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/YAMLParser.java @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2011-2012 Typesafe Inc. + */ +package com.typesafe.config.impl; + + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.yaml.snakeyaml.Yaml; + +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigOrigin; + + +final class YAMLParser { + + static AbstractConfigObject parse(Reader reader, + ConfigOrigin origin) throws IOException { + Yaml yaml = new Yaml(); + Map yamlMap = (Map) yaml.load(reader); + + Map pathMap = new HashMap(); + for (Map.Entry entry : yamlMap.entrySet()) { + Object key = entry.getKey(); + if (key instanceof String) { + Path path = pathFromPropertyKey((String) key); + pathMap.put(path, entry.getValue()); + } + } + return fromPathMap(origin, pathMap, true /* from properties */); + + } + + static String lastElement(String path) { + int i = path.lastIndexOf('.'); + if (i < 0) + return path; + else + return path.substring(i + 1); + } + + static String exceptLastElement(String path) { + int i = path.lastIndexOf('.'); + if (i < 0) + return null; + else + return path.substring(0, i); + } + + static Path pathFromPropertyKey(String key) { + String last = lastElement(key); + String exceptLast = exceptLastElement(key); + Path path = new Path(last, null); + while (exceptLast != null) { + last = lastElement(exceptLast); + exceptLast = exceptLastElement(exceptLast); + path = new Path(last, path); + } + return path; + } + + static AbstractConfigObject fromPathMap(ConfigOrigin origin, + Map pathExpressionMap) { + Map pathMap = new HashMap(); + for (Map.Entry entry : pathExpressionMap.entrySet()) { + Object keyObj = entry.getKey(); + if (!(keyObj instanceof String)) { + throw new ConfigException.BugOrBroken( + "Map has a non-string as a key, expecting a path expression as a String"); + } + Path path = Path.newPath((String) keyObj); + pathMap.put(path, entry.getValue()); + } + return fromPathMap(origin, pathMap, false /* from properties */); + } + + private static AbstractConfigObject fromPathMap(ConfigOrigin origin, + Map pathMap, boolean convertedFromProperties) { + /* + * First, build a list of paths that will have values, either string or + * object values. + */ + Set scopePaths = new HashSet(); + Set valuePaths = new HashSet(); + for (Path path : pathMap.keySet()) { + // add value's path + valuePaths.add(path); + + // all parent paths are objects + Path next = path.parent(); + while (next != null) { + scopePaths.add(next); + next = next.parent(); + } + } + + if (convertedFromProperties) { + /* + * If any string values are also objects containing other values, + * drop those string values - objects "win". + */ + valuePaths.removeAll(scopePaths); + } else { + /* If we didn't start out as properties, then this is an error. */ + for (Path path : valuePaths) { + if (scopePaths.contains(path)) { + throw new ConfigException.BugOrBroken( + "In the map, path '" + + path.render() + + "' occurs as both the parent object of a value and as a value. " + + "Because Map has no defined ordering, this is a broken situation."); + } + } + } + + /* + * Create maps for the object-valued values. + */ + Map root = new HashMap(); + Map> scopes = new HashMap>(); + + for (Path path : scopePaths) { + Map scope = new HashMap(); + scopes.put(path, scope); + } + + /* Store string values in the associated scope maps */ + for (Path path : valuePaths) { + Path parentPath = path.parent(); + Map parent = parentPath != null ? scopes + .get(parentPath) : root; + + String last = path.last(); + Object rawValue = pathMap.get(path); + AbstractConfigValue value; + if (convertedFromProperties) { + if (rawValue instanceof String) { + value = new ConfigString(origin, (String) rawValue); + } else { + // silently ignore non-string values in Properties + value = null; + } + } else { + value = ConfigImpl.fromAnyRef(pathMap.get(path), origin, + FromMapMode.KEYS_ARE_PATHS); + } + if (value != null) + parent.put(last, value); + } + + /* + * Make a list of scope paths from longest to shortest, so children go + * before parents. + */ + List sortedScopePaths = new ArrayList(); + sortedScopePaths.addAll(scopePaths); + // sort descending by length + Collections.sort(sortedScopePaths, new Comparator() { + @Override + public int compare(Path a, Path b) { + // Path.length() is O(n) so in theory this sucks + // but in practice we can make Path precompute length + // if it ever matters. + return b.length() - a.length(); + } + }); + + /* + * Create ConfigObject for each scope map, working from children to + * parents to avoid modifying any already-created ConfigObject. This is + * where we need the sorted list. + */ + for (Path scopePath : sortedScopePaths) { + Map scope = scopes.get(scopePath); + + Path parentPath = scopePath.parent(); + Map parent = parentPath != null ? scopes + .get(parentPath) : root; + + AbstractConfigObject o = new SimpleConfigObject(origin, scope, + ResolveStatus.RESOLVED, false /* ignoresFallbacks */); + parent.put(scopePath.last(), o); + } + + // return root config object + return new SimpleConfigObject(origin, root, ResolveStatus.RESOLVED, + false /* ignoresFallbacks */); + } +}