diff --git a/.travis.yml b/.travis.yml index d957beebf..980ed49e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,32 @@ # use Docker-based container (instead of OpenVZ) sudo: false -cache: - directories: - - $HOME/.ivy2/cache - # Cache the sbt launcher, currently the Travis VM preinstalls 0.13.5 - - $HOME/.sbt/launchers/0.13.7 - # Cache scala, currently the Travis VM preinstalls 2.11.2 & 2.10.4 - #- $HOME/.sbt/boot/scala-$TRAVIS_SCALA_VERSION - - # Updates regarding Travis VM preinstalls: - # https://github.com/travis-ci/travis-cookbooks/blob/master/changes.md - language: scala -jdk: - - oraclejdk8 +before_install: curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh +install: jabba install "adopt@~1.$TRAVIS_JDK.0-0" && jabba use "$_" && java -Xmx32m -version -script: - - sbt ++$TRAVIS_SCALA_VERSION test doc +jobs: + include: + - stage: jdk8 + name: "test and docs on jdk8" + script: sbt ++$TRAVIS_SCALA_VERSION test doc + - stage: jdk11 + name: "test and docs on jdk11" + script: sbt ++$TRAVIS_SCALA_VERSION test doc + env: TRAVIS_JDK=11 +env: + global: + - TRAVIS_JDK=8 + +before_cache: # Remove to avoid unnecessary cache updates - find $HOME/.sbt -name "*.lock" -delete - find $HOME/.ivy2 -name "ivydata-*.properties" -delete + +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot + - $HOME/.jabba/jdk diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3227a333..255892ab2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ fine... just be willing to revise it! Before we can accept pull requests, you will need to agree to the Typesafe Contributor License Agreement online, using your GitHub account - it takes 30 seconds. You can do this at -http://www.typesafe.com/contribute/cla +https://www.lightbend.com/contribute/cla Expect that most PRs will need revision before merge. If people suggest revisions, you can make them yourself or wait for a @@ -23,38 +23,4 @@ more revision will likely be needed. # Making a release -To make a release you'll need to be a maintainer with GitHub -permissions to push to the master and gh-pages branches, and -Sonatype permissions to publish. - -Here are the steps, which should be automated but aren't (PR -welcome!): - - 1. write release notes in NEWS.md following the format - already in there. update README with the new version. - Commit. - 2. create a signed git tag "vX.Y.Z" - 3. start sbt; `show version` should confirm that the version was - taken from the tag - 4. clean - 5. test (double check that release works) - 6. doc (double check that docs build, plus build docs - to be copied to gh-pages later) - 7. if test or doc fails, delete the tag, fix it, start over. - 8. publishSigned - 9. make a separate clone of the repo in another directory and - check out the gh-pages branch - 10. /bin/rm -rf latest/api on gh-pages checkout - 11. copy config/target/api from master checkout to vX.Y.Z in - gh-pages so you have vX.Y.Z/index.html - 12. copy config/target/api from master checkout into latest/ - so you have latest/api/index.html - 13. commit all that to gh-pages, check the diff for sanity - (latest/api should be mostly just changed timestamps) - 14. push gh-pages - 15. log into sonatype website and go through the usual hoops - (under Staging Repositories, search for com.typesafe, verify the - artifacts in it, close, release) - 16. push the "vX.Y.Z" tag - 17. announce release, possibly wait for maven central to sync - first +See RELEASING.md diff --git a/HOCON.md b/HOCON.md index 168a0a33c..9f8bd837d 100644 --- a/HOCON.md +++ b/HOCON.md @@ -109,7 +109,7 @@ the config file to the computer program. ## Syntax Much of this is defined with reference to JSON; you can find the -JSON spec at http://json.org/ of course. +JSON spec at https://json.org/ of course. ### Unchanged from JSON @@ -317,11 +317,6 @@ String value concatenation is allowed in field keys, in addition to field values and array elements. Objects and arrays do not make sense as field keys. -Note: Akka 2.0 (and thus Play 2.0) contains an embedded -implementation of the config lib which does not support array and -object value concatenation; it only supports string value -concatenation. - #### String value concatenation String value concatenation is the trick that makes unquoted @@ -739,9 +734,6 @@ optional (`${?a}` not `${a}`), which allows `a += b` to be the first mention of `a` in the file (it is not necessary to have `a = []` first). -Note: Akka 2.0 (and thus Play 2.0) contains an embedded -implementation of the config lib which does not support `+=`. - #### Examples of Self-Referential Substitutions In isolation (with no merges involved), a self-referential field @@ -959,11 +951,6 @@ word `"include"`, only unquoted `include` is special: { "include" : 42 } -Note: Akka 2.0 (and thus Play 2.0) contains an embedded -implementation of the config lib which does not support the -`url()`/`file()`/`classpath()` syntax. Only the heuristic `include -"foo"` syntax is supported in that version. - #### Include semantics: merging An _including file_ contains the include statement and an @@ -1296,7 +1283,27 @@ must be lowercase. Exactly these strings are supported: - `m`, `minute`, `minutes` - `h`, `hour`, `hours` - `d`, `day`, `days` + +### Period Format + +Similar to the `getDuration()` method, there is a `getPeriod()` method +available for getting time units as a `java.time.Period`. +This can use the general "units format" described above; bare +numbers are taken to be in days, while strings are +parsed as a number plus an optional unit string. + +The supported unit strings for period are case sensitive and +must be lowercase. Exactly these strings are supported: + + - `d`, `day`, `days` + - `w`, `week`, `weeks` + - `m`, `mo`, `month`, `months` (note that if you are using `getTemporal()` + which may return either a `java.time.Duration` or a `java.time.Period` + you will want to use `mo` rather than `m` to prevent your unit being + parsed as minutes) + - `y`, `year`, `years` + ### Size in bytes format Implementations may wish to support a `getBytes()` returning a @@ -1317,7 +1324,7 @@ such that following the standard leads to people being confused. Worse, common usage varies based on whether people are talking about RAM or disk sizes, and various existing operating systems and apps do all kinds of different things. See -http://en.wikipedia.org/wiki/Binary_prefix#Deviation_between_powers_of_1024_and_powers_of_1000 +https://en.wikipedia.org/wiki/Binary_prefix#Deviation_between_powers_of_1024_and_powers_of_1000 for examples. It appears impossible to sort this out without causing confusion for someone sometime. @@ -1407,7 +1414,7 @@ way to get rid of default fallback values they don't want. It may be useful to merge Java properties data with data loaded from JSON or HOCON. See the Java properties spec here: -http://download.oracle.com/javase/7/docs/api/java/util/Properties.html#load%28java.io.Reader%29 +https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html#load-java.io.Reader- Java properties parse as a one-level map from string keys to string values. diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt index d64569567..62589edd1 100644 --- a/LICENSE-2.0.txt +++ b/LICENSE-2.0.txt @@ -1,7 +1,7 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -193,7 +193,7 @@ 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 + https://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, diff --git a/NEWS.md b/NEWS.md index 22d4cfb65..aeb66d0d5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,41 @@ +# 1.4.0: October 11, 2019 + +- `application.conf` variables can now override variables in `reference.conf` [#619](https://github.com/lightbend/config/issues/167) +- performance improvement through capacity hint for `ArrayList` [#648](https://github.com/lightbend/config/pull/648) + +(This was previously published as 1.3.5-RC1 but deemed a large enough change to require a minor version rather than a patch) + +# 1.3.4: April 18, 2019 + +- it is now possible to override any configuration setting from environment variables ([#620](/../../pull/620)) thanks to [@andreaTP](https://github.com/andreaTP) +- added support for integer keys that are longer than `Int` ([#568](/../../pull/568)) thanks to [@michalmela](https://github.com/michalmela) +- `Missing` exception now has a reference to the origin `Config` ([#585](/../../pull/585)) thanks to [@vagola](https://github.com/vagola) +- performance improvements to `resolve()` ([#578](/../../pull/578)) thanks to [@sam-token](https://github.com/sam-token) +- config file syntax is now resolved when parsing `InputStream` ([#573](/../../pull/573)) thanks to [@ntviet18](https://github.com/ntviet18) +- it is now possible to use `@Optional` on keys that are reserved words ([#570](/../../pull/570)) thanks to [@radist-nt](https://github.com/radist-nt) +- `ValidationProblem` is now serializable ([#437](/../../pull/437)) thanks to [@iantabolt](https://github.com/iantabolt) + +All of the merged PRs can be found in the [release milestone](https://github.com/lightbend/config/milestone/4?closed=1). + +# 1.3.3: February 22, 2018 + +- artifact now includes `Automatic-Module-Name` which makes it consumable as Java 9 module. +- minor issue fix. All of the fixed issues can be found in the [milestone page](https://github.com/lightbend/config/milestone/3?closed=1). + +# 1.3.2: October 6, 2017 + +- environment variables are now able to be resolved to lists in + the same fashion as system properties. +- added `getPeriod()` which returns time units as + `java.time.Period`. Currently supported periods are days, weeks, + months and years. [More information here](HOCON.md#period-format). +- `ConfigResolveOptions` now has `appendResolver(...)` which allows + having custom behavior when unresolved substitutions are encountered + during resolution. +- Config Beans now support `Set` collection. +- a few other small bugfixes. All of the fixed issues can be found + in the [milestone page](https://github.com/lightbend/config/milestone/1?closed=1). + # 1.3.1: September 24, 2016 - added `include required("foo")` syntax to specify includes that @@ -208,7 +246,7 @@ Thank you to contributors with commits since v1.2.1 tag: - build jar using Java 1.6 (and enforce this in build) - change getDuration to return unboxed long instead of boxed - API documentation improvements - http://typesafehub.github.io/config/latest/api/ + https://lightbend.github.io/config/latest/api/ # 1.1.0-9f31d6308e7ebbc3d7904b64ebb9f61f7e22a968: January 6, 2014 @@ -226,7 +264,7 @@ Thank you to contributors with commits since v1.2.1 tag: - new API Config.getDuration() replaces getMilliseconds and getNanoseconds. (should it return `long` instead of `Long` even though it's been in git for a while? weigh in at - https://github.com/typesafehub/config/issues/119 ) + https://github.com/lightbend/config/issues/119 ) - new API ConfigResolveOptions.setAllowUnresolved lets you partially-resolve a Config - new API Config.isResolved lets you check on resolution status diff --git a/README.md b/README.md index b291be549..e2f29a466 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ Configuration library for JVM languages. [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.typesafe/config/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.typesafe/config) -[![Build Status](https://travis-ci.org/typesafehub/config.svg?branch=master)](https://travis-ci.org/typesafehub/config) +[![Build Status](https://travis-ci.org/lightbend/config.svg?branch=master)](https://travis-ci.org/lightbend/config) If you have questions or are working on a pull request or just curious, please feel welcome to join the chat room: -[![Join chat https://gitter.im/typesafehub/config](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/typesafehub/config?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join chat https://gitter.im/lightbend/config](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lightbend/config?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Overview @@ -71,7 +71,6 @@ to merge it in. - [Inheritance](#inheritance) - [Optional system or env variable overrides](#optional-system-or-env-variable-overrides) - [Concatenation](#concatenation) - - [`reference.conf` can't refer to `application.conf`](#referenceconf-cant-refer-to-applicationconf) - [Miscellaneous Notes](#miscellaneous-notes) - [Debugging Your Configuration](#debugging-your-configuration) - [Supports Java 8 and Later](#supports-java-8-and-later) @@ -88,16 +87,14 @@ to merge it in. - [Python port](#python-port) - [C++ port](#c-port) - [JavaScript port](#javascript-port) + - [C# port](#c-port-1) - [Linting tool](#linting-tool) + - [Online playground](#online-playground) ## Essential Information -### License - -The license is Apache 2.0, see LICENSE-2.0.txt. - ### Binary Releases Version 1.2.1 and earlier were built for Java 6, while newer @@ -108,12 +105,12 @@ You can find published releases on Maven Central. com.typesafe config - 1.3.1 + 1.4.0 sbt dependency: - libraryDependencies += "com.typesafe" % "config" % "1.3.1" + libraryDependencies += "com.typesafe" % "config" % "1.4.0" Link for direct download if you don't use a dependency manager: @@ -122,15 +119,15 @@ Link for direct download if you don't use a dependency manager: ### Release Notes Please see NEWS.md in this directory, -https://github.com/typesafehub/config/blob/master/NEWS.md +https://github.com/lightbend/config/blob/master/NEWS.md ### API docs - - Online: http://typesafehub.github.com/config/latest/api/ + - Online: https://lightbend.github.io/config/latest/api/ - also published in jar form - consider reading this README first for an intro - for questions about the `.conf` file format, read - [HOCON.md](https://github.com/typesafehub/config/blob/master/HOCON.md) + [HOCON.md](https://github.com/lightbend/config/blob/master/HOCON.md) in this directory ### Bugs and Patches @@ -141,10 +138,10 @@ requests on GitHub. Before we can accept pull requests, you will need to agree to the Typesafe Contributor License Agreement online, using your GitHub account - it takes 30 seconds. You can do this at -http://www.typesafe.com/contribute/cla +https://www.lightbend.com/contribute/cla Please see -[CONTRIBUTING](https://github.com/typesafehub/config/blob/master/CONTRIBUTING.md) +[CONTRIBUTING](https://github.com/lightbend/config/blob/master/CONTRIBUTING.md) for more including how to make a release. ### Build @@ -165,7 +162,7 @@ Scala dependency. ### Longer Examples -See the examples in the `examples/` [directory](https://github.com/typesafehub/config/tree/master/examples). +See the examples in the `examples/` [directory](https://github.com/lightbend/config/tree/master/examples). You can run these from the sbt console with the commands `project config-simple-app-java` and then `run`. @@ -189,7 +186,7 @@ Objects are immutable, so methods on `Config` which transform the configuration return a new `Config`. Other types such as `ConfigParseOptions`, `ConfigResolveOptions`, `ConfigObject`, etc. are also immutable. See the -[API docs](http://typesafehub.github.com/config/latest/api/) for +[API docs](https://lightbend.github.io/config/latest/api/) for details of course. ### Schemas and Validation @@ -198,7 +195,7 @@ There isn't a schema language or anything like that. However, two suggested tools are: - use the - [checkValid() method](http://typesafehub.github.io/config/latest/api/com/typesafe/config/Config.html#checkValid-com.typesafe.config.Config-java.lang.String...-) + [checkValid() method](https://lightbend.github.io/config/latest/api/com/typesafe/config/Config.html#checkValid-com.typesafe.config.Config-java.lang.String...-) - access your config through a Settings class with a field for each setting, and instantiate it on startup (immediately throwing an exception if any settings are missing) @@ -235,9 +232,6 @@ The idea is that libraries and frameworks should ship with a `application.conf`, or if they want to create multiple configurations in a single JVM, they could use `ConfigFactory.load("myapp")` to load their own `myapp.conf`. -(Applications _can_ provide a `reference.conf` also if they want, -but you may not find it necessary to separate it from -`application.conf`.) Libraries and frameworks should default to `ConfigFactory.load()` if the application does not provide a custom `Config` object. This @@ -282,23 +276,14 @@ system properties. The substitution syntax `${foo.bar}` will be resolved twice. First, all the `reference.conf` files are merged and then the result gets resolved. Second, all the `application.conf` are -layered over the `reference.conf` and the result of that gets -resolved again. +layered over the unresolved `reference.conf` and the result of that +gets resolved again. The implication of this is that the `reference.conf` stack has to be self-contained; you can't leave an undefined value `${foo.bar}` -to be provided by `application.conf`, or refer to `${foo.bar}` in -a way that you want to allow `application.conf` to -override. However, `application.conf` can refer to a `${foo.bar}` -in `reference.conf`. - -This can be frustrating at times, but possible workarounds -include: - - * putting an `application.conf` in a library jar, alongside the -`reference.conf`, with values intended for later resolution. - * putting some logic in code instead of building up values in the - config itself. +to be provided by `application.conf`. It is however possible to +override a variable that `reference.conf` refers to, as long as +`reference.conf` also defines that variable itself. ### Merging config trees @@ -435,7 +420,7 @@ values into multiple places in your code. You have been warned! ### Understanding `Config` and `ConfigObject` To read and modify configuration, you'll use the -[Config](http://typesafehub.github.io/config/latest/api/com/typesafe/config/Config.html) +[Config](https://lightbend.github.io/config/latest/api/com/typesafe/config/Config.html) interface. A `Config` looks at a JSON-equivalent data structure as a one-level map from paths to values. So if your JSON looks like this: @@ -450,7 +435,7 @@ this: Using the `Config` interface, you could write `conf.getInt("foo.bar")`. The `foo.bar` string is called a _path expression_ -([HOCON.md](https://github.com/typesafehub/config/blob/master/HOCON.md) +([HOCON.md](https://github.com/lightbend/config/blob/master/HOCON.md) has the syntax details for these expressions). Iterating over this `Config`, you would get two entries; `"foo.bar" : 42` and `"foo.baz" : 43`. When iterating a `Config` you will not find @@ -463,7 +448,7 @@ skip `null` values. You can also look at a `Config` in the way most JSON APIs would, through the -[ConfigObject](http://typesafehub.github.io/config/latest/api/com/typesafe/config/ConfigObject.html) +[ConfigObject](https://lightbend.github.io/config/latest/api/com/typesafe/config/ConfigObject.html) interface. This interface represents an object node in the JSON tree. `ConfigObject` instances come in multi-level trees, and the keys do not have any syntax (they are just strings, not path @@ -475,15 +460,15 @@ expressions). Iterating over the above example as a In `ConfigObject`, `null` values are visible (distinct from missing values), just as they are in JSON. -`ConfigObject` is a subtype of [ConfigValue](http://typesafehub.github.io/config/latest/api/com/typesafe/config/ConfigValue.html), where the other +`ConfigObject` is a subtype of [ConfigValue](https://lightbend.github.io/config/latest/api/com/typesafe/config/ConfigValue.html), where the other subtypes are the other JSON types (list, string, number, boolean, null). `Config` and `ConfigObject` are two ways to look at the same internal data structure, and you can convert between them for free using -[Config.root()](http://typesafehub.github.io/config/latest/api/com/typesafe/config/Config.html#root%28%29) +[Config.root()](https://lightbend.github.io/config/latest/api/com/typesafe/config/Config.html#root--) and -[ConfigObject.toConfig()](http://typesafehub.github.io/config/latest/api/com/typesafe/config/ConfigObject.html#toConfig%28%29). +[ConfigObject.toConfig()](https://lightbend.github.io/config/latest/api/com/typesafe/config/ConfigObject.html#toConfig--). ### ConfigBeanFactory @@ -506,7 +491,7 @@ particular value manually). The JSON superset is called "Human-Optimized Config Object Notation" or HOCON, and files use the suffix `.conf`. See -[HOCON.md](https://github.com/typesafehub/config/blob/master/HOCON.md) +[HOCON.md](https://github.com/lightbend/config/blob/master/HOCON.md) in this directory for more detail. After processing a `.conf` file, the result is always just a JSON @@ -680,6 +665,24 @@ value just disappear if the substitution is not found: // this array could have one or two elements path = [ "a", ${?OPTIONAL_A} ] +By setting the JVM property `-Dconfig.override_with_env_vars=true` +it is possible to override any configuration value using environment +variables even if an explicit substitution is not specified. + +The environment variable value will override any pre-existing value +and also any value provided as Java property. + +With this option enabled only environment variables starting with +`CONFIG_FORCE_` are considered, and the name is mangled as follows: + + - the prefix `CONFIG_FORCE_` is stripped + - single underscore(`_`) is converted into a dot(`.`) + - double underscore(`__`) is converted into a dash(`-`) + - triple underscore(`___`) is converted into a single underscore(`_`) + +i.e. The environment variable `CONFIG_FORCE_a_b__c___d` set the +configuration key `a.b-c_d` + ### Concatenation Values _on the same line_ are concatenated (for strings and @@ -693,9 +696,12 @@ string `foo` are concatenated into a string `42 foo`: When concatenating values into a string, leading and trailing whitespace is stripped but whitespace between values is kept. -Unquoted strings also support substitutions of course: +Quoted or unquoted strings can also concatenate with substitutions of course: tasks-url : ${base-url}/tasks + tasks-url : ${base-url}"tasks:colon-must-be-quoted" + +Note: the `${}` syntax must be outside the quotes! A concatenation can refer to earlier values of the same field: @@ -742,14 +748,6 @@ concatenation, but not object/array concatenation. `+=` does not work in Play/Akka 2.0 either. Post-2.0 versions support these features. -### `reference.conf` can't refer to `application.conf` - -Please see this -earlier section; all `reference.conf` have substitutions -resolved first, without `application.conf` in the stack, so the -reference stack has to be self-contained. - ## Miscellaneous Notes ### Debugging Your Configuration @@ -760,8 +758,19 @@ If you have trouble with your configuration, some useful tips. output on stderr describing each file that is loaded. Note: this feature is not included in the older version in Play/Akka 2.0. - - Use `myConfig.root().render()` to get a `Config` printed out as a + - Use `myConfig.root().render()` to get a `Config` as a string with comments showing where each value came from. + This string can be printed out on console or logged to + a file etc. + - If you see errors like + `com.typesafe.config.ConfigException$Missing: No configuration setting found for key foo`, + and you're sure that key is defined in your config file, they might appear e.g. + when you're loading configuration from a thread that's not the JVM's main thread. + Try passing the `ClassLoader` in manually - e.g. with `ConfigFactory.load(getClass().getClassLoader())` + or setting the context class loader. + If you don't pass one, Lightbend Config uses the calling thread's `contextClassLoader`, and in some cases, + it may not have your configuration files in its classpath, + so loading the config on that thread can yield unexpected, erroneous results. ### Supports Java 8 and Later @@ -771,7 +780,7 @@ version 1.2.1 and earlier will work with Java 6. Please use 1.2.1 if you need Java 6 support, though some people have expressed interest in a branch off of 1.3.x supporting Java 7. If you want to work on that branch you might bring it up -on [chat](https://gitter.im/typesafehub/config). We can release a +on [chat](https://gitter.im/lightbend/config). We can release a jar for Java 7 if someone(s) steps up to maintain the branch. The master branch does not use Java 8 "gratuitously" but some APIs that use Java 8 types will need to be removed. @@ -840,7 +849,7 @@ format. * Ficus https://github.com/ceedubs/ficus * configz https://github.com/arosien/configz * configs https://github.com/kxbmap/configs -  * config-annotation https://github.com/zhongl/config-annotation + * config-annotation https://github.com/zhongl/config-annotation * PureConfig https://github.com/pureconfig/pureconfig * Simple Scala Config https://github.com/ElderResearch/ssc * konfig https://github.com/vpon/konfig @@ -849,17 +858,20 @@ format. * validated-config https://github.com/carlpulley/validated-config * Cedi Config https://github.com/ccadllc/cedi-config * Cfg https://github.com/carueda/cfg + * circe-config https://github.com/circe/circe-config + * args4c https://github.com/aaronp/args4c #### Clojure wrappers for the Java library * beamly-core.config https://github.com/beamly/beamly-core.config - + #### Kotlin wrappers for the Java library * config4k https://github.com/config4k/config4k - + #### Scala port - * SHocon https://github.com/unicredit/shocon (work with both Scala and Scala.Js) + * SHocon https://github.com/akka-js/shocon (Supports Scala.js and Scala Native) + * sconfig https://github.com/ekrich/sconfig (Supports JVM, Scala Native, and Scala.js) #### Ruby port @@ -878,9 +890,33 @@ format. * https://github.com/puppetlabs/cpp-hocon #### JavaScript port - + * https://github.com/yellowblood/hocon-js (missing features, under development) - + +#### C# port + + * https://github.com/akkadotnet/HOCON + +#### Rust port + + * https://github.com/mockersf/hocon.rs + #### Linting tool * A web based linting tool http://www.hoconlint.com/ + +#### Online playground + + * https://hocon-playground.herokuapp.com/ + +# Maintenance notes + +## License + +The license is Apache 2.0, see LICENSE-2.0.txt. + +## Maintained by + +This project is maintained mostly by [@havocp](https://github.com/havocp) and [@akka-team](https://github.com/orgs/lightbend/teams/akka-team/members). + +Feel free to ping above maintainers for code review or discussions. Pull requests are very welcome–thanks in advance! diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..08d4723b1 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,37 @@ +# Making a release + +To make a release you'll need to be a maintainer with GitHub +permissions to push to the master and gh-pages branches, and +Sonatype permissions to publish. + +Here are the steps, which should be automated but aren't (PR +welcome!): + + 1. write release notes in NEWS.md following the format + already in there. update README with the new version. + Commit. + 2. create a signed git tag "vX.Y.Z" + 3. start sbt; `show version` should confirm that the version was + taken from the tag + 4. clean + 5. test (double check that release works) + 6. doc (double check that docs build, plus build docs + to be copied to gh-pages later) + 7. if test or doc fails, delete the tag, fix it, start over. + 8. publishSigned + 9. make a separate clone of the repo in another directory and + check out the gh-pages branch + 10. /bin/rm -rf latest/api on gh-pages checkout + 11. copy config/target/api from master checkout to vX.Y.Z in + gh-pages so you have vX.Y.Z/index.html + 12. copy config/target/api from master checkout into latest/ + so you have latest/api/index.html + 13. commit all that to gh-pages, check the diff for sanity + (latest/api should be mostly just changed timestamps) + 14. push gh-pages + 15. log into sonatype website and go through the usual hoops + (under Staging Repositories, search for com.typesafe, verify the + artifacts in it, close, release) + 16. push the "vX.Y.Z" tag + 17. announce release, possibly wait for maven central to sync + first diff --git a/build.sbt b/build.sbt index 65be9256f..631d5c8d9 100644 --- a/build.sbt +++ b/build.sbt @@ -2,21 +2,178 @@ // update NEWS, update version in README.md, tag, then // publishSigned. // Release tags should follow: http://semver.org/ +import scalariform.formatter.preferences._ -enablePlugins(GitVersioning) -git.baseVersion := "1.3.0" +ThisBuild / git.baseVersion := "1.4.0" +ThisBuild / organization := "com.typesafe" +ThisBuild / Compile / scalacOptions := List("-unchecked", "-deprecation", "-feature") +ThisBuild / Test / scalacOptions := List("-unchecked", "-deprecation", "-feature") +ThisBuild / scalaVersion := "2.12.8" -organization in GlobalScope := "com.typesafe" +ThisBuild / scmInfo := Option( + ScmInfo(url("https://github.com/lightbend/config"), "scm:git@github.com:lightbend/config.git") +) +ThisBuild / developers := List( + Developer( + id = "havocp", + name = "Havoc Pennington", + email = "@havocp", + url = url("http://ometer.com/") + ) +) +ThisBuild / description := "configuration library for JVM languages using HOCON files" +ThisBuild / licenses := List("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")) +ThisBuild / homepage := Option(url("https://github.com/lightbend/config")) +ThisBuild / pomIncludeRepository := { _ => false } +ThisBuild / publishTo := { + val nexus = "https://oss.sonatype.org/" + if ((ThisBuild / isSnapshot).value) Option("Sonatype OSS Snapshots" at nexus + "content/repositories/snapshots") + else Option("Sonatype OSS Staging" at nexus + "service/local/staging/deploy/maven2") +} +ThisBuild / publishMavenStyle := true -scalacOptions in GlobalScope in Compile := Seq("-unchecked", "-deprecation", "-feature") -scalacOptions in GlobalScope in Test := Seq("-unchecked", "-deprecation", "-feature") +lazy val root = (project in file(".")) + .enablePlugins(GitVersioning) + .aggregate( + testLib, configLib, + simpleLibScala, simpleAppScala, complexAppScala, + simpleLibJava, simpleAppJava, complexAppJava + ) + .settings(commonSettings) + .settings(nocomma { + name := "config-root" + git.baseVersion := (ThisBuild / git.baseVersion).value + doc / aggregate := false + doc := (configLib / Compile / doc).value + packageDoc / aggregate := false + packageDoc := (configLib / Compile / packageDoc).value + checkstyle / aggregate := false + checkstyle := (configLib / Compile / checkstyle).value + useGpg := true + PgpKeys.publishSigned / aggregate := false + PgpKeys.publishSigned := (PgpKeys.publishSigned in configLib).value + PgpKeys.publishLocalSigned / aggregate := false + PgpKeys.publishLocalSigned := (PgpKeys.publishLocalSigned in configLib).value + }) -scalaVersion in ThisBuild := "2.10.4" +lazy val configLib = Project("config", file("config")) + .enablePlugins(SbtOsgi) + .dependsOn(testLib % "test->test") + .settings(osgiSettings) + .settings(nocomma { + autoScalaLibrary := false + crossPaths := false + libraryDependencies += "net.liftweb" %% "lift-json" % "3.3.0" % Test + libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % Test -useGpg := true + Compile / compile / javacOptions ++= Seq("-source", "1.8", "-target", "1.8", + "-g", "-Xlint:unchecked") -aggregate in PgpKeys.publishSigned := false -PgpKeys.publishSigned := (PgpKeys.publishSigned in configLib).value + Compile / doc / javacOptions ++= Seq("-group", s"Public API (version ${version.value})", "com.typesafe.config:com.typesafe.config.parser", + "-group", "Internal Implementation - Not ABI Stable", "com.typesafe.config.impl") + javadocSourceBaseUrl := { + for (gitHead <- com.typesafe.sbt.SbtGit.GitKeys.gitHeadCommit.value) + yield s"https://github.com/lightbend/config/blob/$gitHead/config/src/main/java" + } + // because we test some global state such as singleton caches, + // we have to run tests in serial. + Test / parallelExecution := false -aggregate in PgpKeys.publishLocalSigned := false -PgpKeys.publishLocalSigned := (PgpKeys.publishLocalSigned in configLib).value + test / fork := true + Test / fork := true + run / fork := true + Test/ run / fork := true + + //env vars for tests + Test / envVars ++= Map("testList.0" -> "0", + "testList.1" -> "1", + "CONFIG_FORCE_b" -> "5", + "CONFIG_FORCE_testList_0" -> "10", + "CONFIG_FORCE_testList_1" -> "11", + "CONFIG_FORCE_42___a" -> "1", + "CONFIG_FORCE_a_b_c" -> "2", + "CONFIG_FORCE_a__c" -> "3", + "CONFIG_FORCE_a___c" -> "4", + "CONFIG_FORCE_akka_version" -> "foo", + "CONFIG_FORCE_akka_event__handler__dispatcher_max__pool__size" -> "10") + + OsgiKeys.exportPackage := Seq("com.typesafe.config", "com.typesafe.config.impl") + publish := sys.error("use publishSigned instead of plain publish") + publishLocal := sys.error("use publishLocalSigned instead of plain publishLocal") + Compile / packageBin / packageOptions += + Package.ManifestAttributes("Automatic-Module-Name" -> "typesafe.config" ) + scalariformPreferences := scalariformPreferences.value + .setPreference(IndentSpaces, 4) + .setPreference(FirstArgumentOnNewline, Preserve) + + checkstyleConfigLocation := CheckstyleConfigLocation.File((baseDirectory.value / "checkstyle-config.xml").toString) + + Compile / checkstyle := { + val log = streams.value.log + (Compile / checkstyle).value + val resultFile = (Compile / checkstyleOutputFile).value + val results = scala.xml.XML.loadFile(resultFile) + val errorFiles = results \\ "checkstyle" \\ "file" + + def errorFromXml(node: scala.xml.NodeSeq): (String, String, String) = { + val line: String = (node \ "@line" text) + val msg: String = (node \ "@message" text) + val source: String = (node \ "@source" text) + (line, msg, source) + } + def errorsFromXml(fileNode: scala.xml.NodeSeq): Seq[(String, String, String, String)] = { + val name: String = (fileNode \ "@name" text) + val errors = (fileNode \\ "error") map { e => errorFromXml(e) } + errors map { case (line, error, source) => (name, line, error, source) } + } + + val errors = errorFiles flatMap { f => errorsFromXml(f) } + + if (errors.nonEmpty) { + for (e <- errors) { + log.error(s"${e._1}:${e._2}: ${e._3} (from ${e._4})") + } + throw new RuntimeException(s"Checkstyle failed with ${errors.size} errors") + } + log.info("No errors from checkstyle") + } + + // add checkstyle as a dependency of doc + Compile / doc := ((Compile / doc).dependsOn(Compile / checkstyle)).value + + findbugsReportType := Some(FindbugsReport.Html) + findbugsReportPath := Some(crossTarget.value / "findbugs.html") + findbugsEffort := FindbugsEffort.Maximum + findbugsMaxMemory := 2000 + }) + +lazy val commonSettings: Seq[Setting[_]] = Def.settings( + unpublished, + scalariformPreferences := scalariformPreferences.value + .setPreference(IndentSpaces, 4) + .setPreference(FirstArgumentOnNewline, Preserve), + Compile / compile / javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), +) + +def proj(id: String, base: File) = Project(id, base) settings commonSettings + +lazy val testLib = proj("config-test-lib", file("test-lib")) + +lazy val simpleLibScala = proj("config-simple-lib-scala", file("examples/scala/simple-lib")) dependsOn configLib +lazy val simpleAppScala = proj("config-simple-app-scala", file("examples/scala/simple-app")) dependsOn simpleLibScala +lazy val complexAppScala = proj("config-complex-app-scala", file("examples/scala/complex-app")) dependsOn simpleLibScala + +lazy val simpleLibJava = proj("config-simple-lib-java", file("examples/java/simple-lib")) dependsOn configLib +lazy val simpleAppJava = proj("config-simple-app-java", file("examples/java/simple-app")) dependsOn simpleLibJava +lazy val complexAppJava = proj("config-complex-app-java", file("examples/java/complex-app")) dependsOn simpleLibJava + +val unpublished = Seq( + // no artifacts in this project + publishArtifact := false, + // make-pom has a more specific publishArtifact setting already + // so needs specific override + makePom / publishArtifact := false, + // no docs to publish + packageDoc / publishArtifact := false, + publish / skip := true +) diff --git a/config/build.sbt b/config/build.sbt deleted file mode 100644 index 7d3fbe218..000000000 --- a/config/build.sbt +++ /dev/null @@ -1,90 +0,0 @@ -import de.johoop.findbugs4sbt.FindBugs._ -import de.johoop.findbugs4sbt.{ Effort, ReportType } -import de.johoop.jacoco4sbt.JacocoPlugin.jacoco -import com.typesafe.sbt.SbtScalariform -import com.typesafe.sbt.SbtScalariform.ScalariformKeys -import scalariform.formatter.preferences._ - -SbtScalariform.scalariformSettings - -val formatPrefs = FormattingPreferences() - .setPreference(IndentSpaces, 4) - -ScalariformKeys.preferences in Compile := formatPrefs -ScalariformKeys.preferences in Test := formatPrefs - -fork in test := true -fork in Test := true -fork in run := true -fork in run in Test := true - -//env vars for tests -envVars in Test ++= Map("testList.0" -> "0", "testList.1" -> "1") - -autoScalaLibrary := false -crossPaths := false - -libraryDependencies += "net.liftweb" %% "lift-json" % "2.5" % "test" -libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test" - -externalResolvers += "Scala Tools Snapshots" at "http://scala-tools.org/repo-snapshots/" - -checkstyleConfigLocation := CheckstyleConfigLocation.File((baseDirectory.value / "checkstyle-config.xml").toString) - -checkstyle in Compile := { - val log = streams.value.log - (checkstyle in Compile).value - val resultFile = (checkstyleOutputFile in Compile).value - val results = scala.xml.XML.loadFile(resultFile) - val errorFiles = results \\ "checkstyle" \\ "file" - - def errorFromXml(node: scala.xml.NodeSeq): (String, String, String) = { - val line: String = (node \ "@line" text) - val msg: String = (node \ "@message" text) - val source: String = (node \ "@source" text) - (line, msg, source) - } - def errorsFromXml(fileNode: scala.xml.NodeSeq): Seq[(String, String, String, String)] = { - val name: String = (fileNode \ "@name" text) - val errors = (fileNode \\ "error") map { e => errorFromXml(e) } - errors map { case (line, error, source) => (name, line, error, source) } - } - - val errors = errorFiles flatMap { f => errorsFromXml(f) } - - if (errors.nonEmpty) { - for (e <- errors) { - log.error(s"${e._1}:${e._2}: ${e._3} (from ${e._4})") - } - throw new RuntimeException(s"Checkstyle failed with ${errors.size} errors") - } - log.info("No errors from checkstyle") -} - -// add checkstyle as a dependency of doc -doc in Compile <<= (doc in Compile).dependsOn(checkstyle in Compile) - -findbugsSettings -findbugsReportType := Some(ReportType.Html) -findbugsReportPath := Some(crossTarget.value / "findbugs.html") -findbugsEffort := Effort.Maximum -findbugsMaxMemory := 2000 - -jacoco.settings - -javacOptions in (Compile, compile) ++= Seq("-source", "1.6", "-target", "1.8", - "-g", "-Xlint:unchecked") - -// because we test some global state such as singleton caches, -// we have to run tests in serial. -parallelExecution in Test := false - -javacOptions in (Compile, doc) ++= Seq("-group", s"Public API (version ${version.value})", "com.typesafe.config:com.typesafe.config.parser", - "-group", "Internal Implementation - Not ABI Stable", "com.typesafe.config.impl") - -javadocSourceBaseUrl := { - for (gitHead <- com.typesafe.sbt.SbtGit.GitKeys.gitHeadCommit.value) - yield s"https://github.com/typesafehub/config/blob/$gitHead/config/src/main/java" -} - -javaVersionPrefix in javaVersionCheck := Some("1.8") diff --git a/config/checkstyle-config.xml b/config/checkstyle-config.xml index b5b71d2b6..148167f31 100644 --- a/config/checkstyle-config.xml +++ b/config/checkstyle-config.xml @@ -1,8 +1,7 @@ - + "-//Checkstyle//DTD Check Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> diff --git a/config/checkstyle-suppressions.xml b/config/checkstyle-suppressions.xml index 21fb8e51e..300751899 100644 --- a/config/checkstyle-suppressions.xml +++ b/config/checkstyle-suppressions.xml @@ -1,6 +1,7 @@ - + * You can find an example app and library on + * href="https://github.com/lightbend/config/tree/master/examples">on * GitHub. Also be sure to read the package overview which * describes the big picture as shown in those examples. @@ -54,7 +56,7 @@ * in a JSON object; it's just a string that's the key in a map. A "path" is a * parseable expression with a syntax and it refers to a series of keys. Path * expressions are described in the spec for + * href="https://github.com/lightbend/config/blob/master/HOCON.md">spec for * Human-Optimized Config Object Notation. In brief, a path is * period-separated so "a.b.c" looks for key c in object b in object a in the * root object. Sometimes double quotes are needed around special characters in @@ -108,7 +110,7 @@ *

* Substitutions are the ${foo.bar} syntax in config * files, described in the specification. Resolving substitutions replaces these references with real * values. * @@ -187,7 +189,7 @@ public interface Config extends ConfigMergeable { /** * Returns a replacement config with all substitutions (the * ${foo.bar} syntax, see the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec) resolved. Substitutions are looked up using this * Config as the root object, that is, a substitution * ${foo.bar} will be replaced with the result of @@ -668,7 +670,7 @@ public interface Config extends ConfigMergeable { * the value is already a number, then it's left alone; if it's a string, * it's parsed understanding unit suffixes such as "128K", as documented in * the the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec. * * @param path @@ -688,7 +690,7 @@ public interface Config extends ConfigMergeable { * the value is already a number, then it's left alone; if it's a string, * it's parsed understanding unit suffixes such as "128K", as documented in * the the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec. * * @since 1.3.0 @@ -709,7 +711,7 @@ public interface Config extends ConfigMergeable { * Get value as a duration in milliseconds. If the value is already a * number, then it's left alone; if it's a string, it's parsed understanding * units suffixes like "10m" or "5ns" as documented in the the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec. * * @deprecated As of release 1.1, replaced by {@link #getDuration(String, TimeUnit)} @@ -752,7 +754,7 @@ public interface Config extends ConfigMergeable { * number, then it's taken as milliseconds and then converted to the * requested TimeUnit; if it's a string, it's parsed understanding units * suffixes like "10m" or "5ns" as documented in the the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec. * * @since 1.2.0 @@ -776,7 +778,7 @@ public interface Config extends ConfigMergeable { * already a number, then it's taken as milliseconds; if it's * a string, it's parsed understanding units suffixes like * "10m" or "5ns" as documented in the the + * href="https://github.com/lightbend/config/blob/master/HOCON.md">the * spec. This method never returns null. * * @since 1.3.0 @@ -793,6 +795,44 @@ public interface Config extends ConfigMergeable { */ Duration getDuration(String path); + /** + * Gets a value as a java.time.Period. If the value is + * already a number, then it's taken as days; if it's + * a string, it's parsed understanding units suffixes like + * "10d" or "5w" as documented in the the + * spec. This method never returns null. + * + * @since 1.3.0 + * + * @param path + * path expression + * @return the period value at the requested path + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to Long or String + * @throws ConfigException.BadValue + * if value cannot be parsed as a number of the given TimeUnit + */ + Period getPeriod(String path); + + /** + * Gets a value as a java.time.temporal.TemporalAmount. + * This method will first try get get the value as a java.time.Duration, and if unsuccessful, + * then as a java.time.Period. + * This means that values like "5m" will be parsed as 5 minutes rather than 5 months + * @param path path expression + * @return the temporal value at the requested path + * @throws ConfigException.Missing + * if value is absent or null + * @throws ConfigException.WrongType + * if value is not convertible to Long or String + * @throws ConfigException.BadValue + * if value cannot be parsed as a TemporalAmount + */ + TemporalAmount getTemporal(String path); + /** * Gets a list value (with any element type) as a {@link ConfigList}, which * implements {@code java.util.List}. Throws if the path is diff --git a/config/src/main/java/com/typesafe/config/ConfigException.java b/config/src/main/java/com/typesafe/config/ConfigException.java index 58d541704..2e6d99ab7 100644 --- a/config/src/main/java/com/typesafe/config/ConfigException.java +++ b/config/src/main/java/com/typesafe/config/ConfigException.java @@ -58,22 +58,22 @@ private void writeObject(java.io.ObjectOutputStream out) throws IOException { ConfigImplUtil.writeOrigin(out, origin); } - private void readObject(java.io.ObjectInputStream in) throws IOException, - ClassNotFoundException { - in.defaultReadObject(); - ConfigOrigin origin = ConfigImplUtil.readOrigin(in); + // For deserialization - uses reflection to set the final origin field on the object + private static void setOriginField(T hasOriginField, Class clazz, + ConfigOrigin origin) throws IOException { // circumvent "final" Field f; try { - f = ConfigException.class.getDeclaredField("origin"); + f = clazz.getDeclaredField("origin"); } catch (NoSuchFieldException e) { - throw new IOException("ConfigException has no origin field?", e); + throw new IOException(clazz.getSimpleName() + " has no origin field?", e); } catch (SecurityException e) { - throw new IOException("unable to fill out origin field in ConfigException", e); + throw new IOException("unable to fill out origin field in " + + clazz.getSimpleName(), e); } f.setAccessible(true); try { - f.set(this, origin); + f.set(hasOriginField, origin); } catch (IllegalArgumentException e) { throw new IOException("unable to set origin field", e); } catch (IllegalAccessException e) { @@ -81,6 +81,13 @@ private void readObject(java.io.ObjectInputStream in) throws IOException, } } + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + ConfigOrigin origin = ConfigImplUtil.readOrigin(in); + setOriginField(this, ConfigException.class, origin); + } + /** * Exception indicating that the type of a value does not match the type you * requested. @@ -119,6 +126,10 @@ public Missing(String path, Throwable cause) { cause); } + public Missing(ConfigOrigin origin, String path) { + this(origin, "No configuration setting found for key '" + path + "'", null); + } + public Missing(String path) { this(path, null); } @@ -127,9 +138,6 @@ protected Missing(ConfigOrigin origin, String message, Throwable cause) { super(origin, message, cause); } - protected Missing(ConfigOrigin origin, String message) { - this(origin, message, null); - } } /** @@ -276,13 +284,25 @@ public Parse(ConfigOrigin origin, String message) { public static class UnresolvedSubstitution extends Parse { private static final long serialVersionUID = 1L; + private final String detail; + public UnresolvedSubstitution(ConfigOrigin origin, String detail, Throwable cause) { super(origin, "Could not resolve substitution to a value: " + detail, cause); + this.detail = detail; } public UnresolvedSubstitution(ConfigOrigin origin, String detail) { this(origin, detail, null); } + + private UnresolvedSubstitution(UnresolvedSubstitution wrapped, ConfigOrigin origin, String message) { + super(origin, message, wrapped); + this.detail = wrapped.detail; + } + + public UnresolvedSubstitution addExtraDetail(String extra) { + return new UnresolvedSubstitution(this, origin(), String.format(extra, detail)); + } } /** @@ -310,10 +330,10 @@ public NotResolved(String message) { * {@link ConfigException.ValidationFailed} exception thrown from * checkValid() includes a list of problems encountered. */ - public static class ValidationProblem { + public static class ValidationProblem implements Serializable { final private String path; - final private ConfigOrigin origin; + final private transient ConfigOrigin origin; final private String problem; public ValidationProblem(String path, ConfigOrigin origin, String problem) { @@ -347,6 +367,20 @@ public String problem() { return problem; } + // We customize serialization because ConfigOrigin isn't + // serializable and we don't want it to be + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + ConfigImplUtil.writeOrigin(out, origin); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, + ClassNotFoundException { + in.defaultReadObject(); + ConfigOrigin origin = ConfigImplUtil.readOrigin(in); + setOriginField(this, ValidationProblem.class, origin); + } + @Override public String toString() { return "ValidationProblem(" + path + "," + origin + "," + problem + ")"; diff --git a/config/src/main/java/com/typesafe/config/ConfigFactory.java b/config/src/main/java/com/typesafe/config/ConfigFactory.java index 7938dc918..12f9aaec0 100644 --- a/config/src/main/java/com/typesafe/config/ConfigFactory.java +++ b/config/src/main/java/com/typesafe/config/ConfigFactory.java @@ -28,7 +28,7 @@ * from a resource and nothing else. * *

You can find an example app and library on + * href="https://github.com/lightbend/config/tree/master/examples">on * GitHub. Also be sure to read the package * overview which describes the big picture as shown in those @@ -36,6 +36,7 @@ */ public final class ConfigFactory { private static final String STRATEGY_PROPERTY_NAME = "config.strategy"; + private static final String OVERRIDE_WITH_ENV_PROPERTY_NAME = "config.override_with_env_vars"; private ConfigFactory() { } @@ -210,7 +211,8 @@ public static Config load(Config config, ConfigResolveOptions resolveOptions) { * @return resolved configuration with overrides and fallbacks added */ public static Config load(ClassLoader loader, Config config, ConfigResolveOptions resolveOptions) { - return defaultOverrides(loader).withFallback(config).withFallback(defaultReference(loader)) + return defaultOverrides(loader).withFallback(config) + .withFallback(ConfigImpl.defaultReferenceUnresolved(loader)) .resolve(resolveOptions); } @@ -367,6 +369,56 @@ public static Config defaultReference(ClassLoader loader) { return ConfigImpl.defaultReference(loader); } + /** + * Obtains the default reference configuration, which is currently created + * by merging all resources "reference.conf" found on the classpath and + * overriding the result with system properties. + * + *

+ * While the returned reference configuration is guaranteed to be + * resolvable (that is, there will be no substitutions that cannot be + * resolved), it is returned in an unresolved state for the purpose of + * allowing substitutions to be overridden by a config layer that falls + * back to this one. + * + *

+ * Libraries and frameworks should ship with a "reference.conf" in their + * jar. + * + *

+ * The reference config must be looked up in the class loader that contains + * the libraries that you want to use with this config, so the + * "reference.conf" for each library can be found. Use + * {@link #defaultReference(ClassLoader)} if the context class loader is not + * suitable. + * + *

+ * The {@link #load()} methods merge this configuration for you + * automatically. + * + *

+ * Future versions may look for reference configuration in more places. It + * is not guaranteed that this method only looks at + * "reference.conf". + * + * @return the unresolved default reference config for the context class + * loader + */ + public static Config defaultReferenceUnresolved() { + return defaultReferenceUnresolved(checkedContextClassLoader("defaultReferenceUnresolved")); + } + + /** + * Like {@link #defaultReferenceUnresolved()} but allows you to specify a + * class loader to use rather than the current context class loader. + * + * @param loader class loader to look for resources in + * @return the unresolved default reference config for this class loader + */ + public static Config defaultReferenceUnresolved(ClassLoader loader) { + return ConfigImpl.defaultReferenceUnresolved(loader); + } + /** * Obtains the default override configuration, which currently consists of * system properties. The returned override configuration will already have @@ -383,7 +435,11 @@ public static Config defaultReference(ClassLoader loader) { * @return the default override configuration */ public static Config defaultOverrides() { - return systemProperties(); + if (getOverrideWithEnv()) { + return systemEnvironmentOverrides().withFallback(systemProperties()); + } else { + return systemProperties(); + } } /** @@ -394,7 +450,7 @@ public static Config defaultOverrides() { * @return the default override configuration */ public static Config defaultOverrides(ClassLoader loader) { - return systemProperties(); + return defaultOverrides(); } /** @@ -497,6 +553,7 @@ public static void invalidateCaches() { // all caches ConfigImpl.reloadSystemPropertiesConfig(); ConfigImpl.reloadEnvVariablesConfig(); + ConfigImpl.reloadEnvVariablesOverridesConfig(); } /** @@ -549,6 +606,50 @@ public static Config systemProperties() { return ConfigImpl.systemPropertiesAsConfig(); } + /** + * Gets a Config containing the system's environment variables + * used to override configuration keys. + * Environment variables taken in considerations are starting with + * {@code CONFIG_FORCE_} + * + *

+ * Environment variables are mangled in the following way after stripping the prefix "CONFIG_FORCE_": + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Env VarConfig
_   [1 underscore]. [dot]
__  [2 underscore]- [dash]
___ [3 underscore]_ [underscore]
+ * + *

+ * A variable like: {@code CONFIG_FORCE_a_b__c___d} + * is translated to a config key: {@code a.b-c_d} + * + *

+ * This method can return a global immutable singleton, so it's preferred + * over parsing system properties yourself. + *

+ * {@link #defaultOverrides} will include the system environment variables as + * overrides if `config.override_with_env_vars` is set to `true`. + * + * @return system environment variable overrides parsed into a Config + */ + public static Config systemEnvironmentOverrides() { + return ConfigImpl.envVariablesOverridesAsConfig(); + } + /** * Gets a Config containing the system's environment variables. * This method can return a global immutable singleton. @@ -570,7 +671,7 @@ public static Config systemEnvironment() { /** * Converts a Java {@link java.util.Properties} object to a * {@link ConfigObject} using the rules documented in the HOCON + * href="https://github.com/lightbend/config/blob/master/HOCON.md">HOCON * spec. The keys in the Properties object are split on the * period character '.' and treated as paths. The values will all end up as * string values. If you have both "a=foo" and "a.b=bar" in your properties @@ -1063,4 +1164,10 @@ private static ConfigLoadingStrategy getConfigLoadingStrategy() { return new DefaultConfigLoadingStrategy(); } } + + private static Boolean getOverrideWithEnv() { + String overrideWithEnv = System.getProperties().getProperty(OVERRIDE_WITH_ENV_PROPERTY_NAME); + + return Boolean.parseBoolean(overrideWithEnv); + } } diff --git a/config/src/main/java/com/typesafe/config/ConfigMergeable.java b/config/src/main/java/com/typesafe/config/ConfigMergeable.java index 7af36f7bf..58b2663e8 100644 --- a/config/src/main/java/com/typesafe/config/ConfigMergeable.java +++ b/config/src/main/java/com/typesafe/config/ConfigMergeable.java @@ -27,7 +27,7 @@ public interface ConfigMergeable { * *

* The semantics of merging are described in the spec + * href="https://github.com/lightbend/config/blob/master/HOCON.md">spec * for HOCON. Merging typically occurs when either the same object is * created twice in the same file, or two config files are both loaded. For * example: diff --git a/config/src/main/java/com/typesafe/config/ConfigParseOptions.java b/config/src/main/java/com/typesafe/config/ConfigParseOptions.java index fcfb4d69a..9dc7a121a 100644 --- a/config/src/main/java/com/typesafe/config/ConfigParseOptions.java +++ b/config/src/main/java/com/typesafe/config/ConfigParseOptions.java @@ -4,6 +4,8 @@ package com.typesafe.config; +import com.typesafe.config.impl.ConfigImplUtil; + /** * A set of options related to parsing. * @@ -62,6 +64,18 @@ public ConfigParseOptions setSyntax(ConfigSyntax syntax) { this.includer, this.classLoader); } + /** + * Set the file format. If set to null, assume {@link ConfigSyntax#CONF}. + * + * @param filename + * a configuration file name + * @return options with the syntax set + */ + public ConfigParseOptions setSyntaxFromFilename(String filename) { + ConfigSyntax syntax = ConfigImplUtil.syntaxFromExtension(filename); + return setSyntax(syntax); + } + /** * Gets the current syntax option, which may be null for "any". * @return the current syntax or null diff --git a/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java b/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java index 2b7140416..526da1eff 100644 --- a/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java +++ b/config/src/main/java/com/typesafe/config/ConfigResolveOptions.java @@ -6,7 +6,7 @@ /** * A set of options related to resolving substitutions. Substitutions use the * ${foo.bar} syntax and are documented in the HOCON + * href="https://github.com/lightbend/config/blob/master/HOCON.md">HOCON * spec. *

* Typically this class would be used with the method @@ -29,10 +29,13 @@ public final class ConfigResolveOptions { private final boolean useSystemEnvironment; private final boolean allowUnresolved; + private final ConfigResolver resolver; - private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved) { + private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolved, + ConfigResolver resolver) { this.useSystemEnvironment = useSystemEnvironment; this.allowUnresolved = allowUnresolved; + this.resolver = resolver; } /** @@ -42,7 +45,7 @@ private ConfigResolveOptions(boolean useSystemEnvironment, boolean allowUnresolv * @return the default resolve options */ public static ConfigResolveOptions defaults() { - return new ConfigResolveOptions(true, false); + return new ConfigResolveOptions(true, false, NULL_RESOLVER); } /** @@ -64,7 +67,7 @@ public static ConfigResolveOptions noSystem() { * @return options with requested setting for use of environment variables */ public ConfigResolveOptions setUseSystemEnvironment(boolean value) { - return new ConfigResolveOptions(value, allowUnresolved); + return new ConfigResolveOptions(value, allowUnresolved, resolver); } /** @@ -91,7 +94,55 @@ public boolean getUseSystemEnvironment() { * @since 1.2.0 */ public ConfigResolveOptions setAllowUnresolved(boolean value) { - return new ConfigResolveOptions(useSystemEnvironment, value); + return new ConfigResolveOptions(useSystemEnvironment, value, resolver); + } + + /** + * Returns options where the given resolver used as a fallback if a + * reference cannot be otherwise resolved. This resolver will only be called + * after resolution has failed to substitute with a value from within the + * config itself and with any other resolvers that have been appended before + * this one. Multiple resolvers can be added using, + * + *

+     *     ConfigResolveOptions options = ConfigResolveOptions.defaults()
+     *         .appendResolver(primary)
+     *         .appendResolver(secondary)
+     *         .appendResolver(tertiary);
+     * 
+ * + * With this config unresolved references will first be resolved with the + * primary resolver, if that fails then the secondary, and finally if that + * also fails the tertiary. + * + * If all fallbacks fail to return a substitution "allow unresolved" + * determines whether resolution fails or continues. + *` + * @param value the resolver to fall back to + * @return options that use the given resolver as a fallback + * @since 1.3.2 + */ + public ConfigResolveOptions appendResolver(ConfigResolver value) { + if (value == null) { + throw new ConfigException.BugOrBroken("null resolver passed to appendResolver"); + } else if (value == this.resolver) { + return this; + } else { + return new ConfigResolveOptions(useSystemEnvironment, allowUnresolved, + this.resolver.withFallback(value)); + } + } + + /** + * Returns the resolver to use as a fallback if a substitution cannot be + * otherwise resolved. Never returns null. This method is mostly used by the + * config lib internally, not by applications. + * + * @return the non-null fallback resolver + * @since 1.3.2 + */ + public ConfigResolver getResolver() { + return this.resolver; } /** @@ -104,4 +155,22 @@ public ConfigResolveOptions setAllowUnresolved(boolean value) { public boolean getAllowUnresolved() { return allowUnresolved; } + + /** + * Singleton resolver that never resolves paths. + */ + private static final ConfigResolver NULL_RESOLVER = new ConfigResolver() { + + @Override + public ConfigValue lookup(String path) { + return null; + } + + @Override + public ConfigResolver withFallback(ConfigResolver fallback) { + return fallback; + } + + }; + } diff --git a/config/src/main/java/com/typesafe/config/ConfigResolver.java b/config/src/main/java/com/typesafe/config/ConfigResolver.java new file mode 100644 index 000000000..a380a04f4 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/ConfigResolver.java @@ -0,0 +1,38 @@ +package com.typesafe.config; + +/** + * Implement this interface and provide an instance to + * {@link ConfigResolveOptions#appendResolver ConfigResolveOptions.appendResolver()} + * to provide custom behavior when unresolved substitutions are encountered + * during resolution. + * @since 1.3.2 + */ +public interface ConfigResolver { + + /** + * Returns the value to substitute for the given unresolved path. To get the + * components of the path use {@link ConfigUtil#splitPath(String)}. If a + * non-null value is returned that value will be substituted, otherwise + * resolution will continue to consider the substitution as still + * unresolved. + * + * @param path the unresolved path + * @return the value to use as a substitution or null + */ + public ConfigValue lookup(String path); + + /** + * Returns a new resolver that falls back to the given resolver if this + * one doesn't provide a substitution itself. + * + * It's important to handle the case where you already have the fallback + * with a "return this", i.e. this method should not create a new object if + * the fallback is the same one you already have. The same fallback may be + * added repeatedly. + * + * @param fallback the previous includer for chaining + * @return a new resolver + */ + public ConfigResolver withFallback(ConfigResolver fallback); + +} diff --git a/config/src/main/java/com/typesafe/config/ConfigSyntax.java b/config/src/main/java/com/typesafe/config/ConfigSyntax.java index ed560296c..749387d7c 100644 --- a/config/src/main/java/com/typesafe/config/ConfigSyntax.java +++ b/config/src/main/java/com/typesafe/config/ConfigSyntax.java @@ -5,7 +5,7 @@ /** * The syntax of a character stream (JSON, HOCON + * href="https://github.com/lightbend/config/blob/master/HOCON.md">HOCON * aka ".conf", or Java properties). @@ -21,7 +21,7 @@ public enum ConfigSyntax { JSON, /** * The JSON-superset HOCON format. Associated with the .conf file extension * and application/hocon Content-Type. */ diff --git a/config/src/main/java/com/typesafe/config/Optional.java b/config/src/main/java/com/typesafe/config/Optional.java index 4645ed5fb..5498b8f41 100644 --- a/config/src/main/java/com/typesafe/config/Optional.java +++ b/config/src/main/java/com/typesafe/config/Optional.java @@ -1,6 +1,7 @@ package com.typesafe.config; import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -8,6 +9,7 @@ * Allows an config property to be {@code null}. */ @Documented +@Inherited @Retention(RetentionPolicy.RUNTIME) public @interface Optional { diff --git a/config/src/main/java/com/typesafe/config/impl/BadMap.java b/config/src/main/java/com/typesafe/config/impl/BadMap.java new file mode 100644 index 000000000..04b375367 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/BadMap.java @@ -0,0 +1,137 @@ +package com.typesafe.config.impl; + +/** + * A terrible Map that isn't as expensive as HashMap to copy and + * add one item to. Please write something real if you see this + * and get cranky. + */ +final class BadMap { + final static class Entry { + final int hash; + final Object key; + final Object value; + final Entry next; + + Entry(int hash, Object k, Object v, Entry next) { + this.hash = hash; + this.key = k; + this.value = v; + this.next = next; + } + + Object find(Object k) { + if (key.equals(k)) + return value; + else if (next != null) + return next.find(k); + else + return null; + } + } + + private final int size; + private final Entry[] entries; + + private final static Entry[] emptyEntries = {}; + + BadMap() { + this(0, emptyEntries); + } + + private BadMap(int size, Entry[] entries) { + this.size = size; + this.entries = entries; + } + + BadMap copyingPut(K k, V v) { + int newSize = size + 1; + Entry[] newEntries; + if (newSize > entries.length) { + // nextPrime doesn't always return a prime larger than + // we passed in, so this block may not actually change + // the entries size. the "-1" is to ensure we use + // array length 2 when going from 0 to 1. + newEntries = new Entry[nextPrime((newSize * 2) - 1)]; + } else { + newEntries = new Entry[entries.length]; + } + + if (newEntries.length == entries.length) { + System.arraycopy(entries, 0, newEntries, 0, entries.length); + } else { + rehash(entries, newEntries); + } + + int hash = Math.abs(k.hashCode()); + store(newEntries, hash, k, v); + return new BadMap<>(newSize, newEntries); + } + + private static void store(Entry[] entries, int hash, K k, V v) { + int i = hash % entries.length; + Entry old = entries[i]; // old may be null + entries[i] = new Entry(hash, k, v, old); + } + + private static void store(Entry[] entries, Entry e) { + int i = e.hash % entries.length; + Entry old = entries[i]; // old may be null + if (old == null && e.next == null) { + // share the entry since it has no "next" + entries[i] = e; + } else { + // bah, have to copy it + entries[i] = new Entry(e.hash, e.key, e.value, old); + } + } + + private static void rehash(Entry[] src, Entry[] dest) { + for (Entry entry : src) { + // have to store each "next" element individually; they may belong in different indices + while (entry != null) { + store(dest, entry); + entry = entry.next; + } + } + } + + @SuppressWarnings("unchecked") + V get(K k) { + if (entries.length == 0) { + return null; + } else { + int hash = Math.abs(k.hashCode()); + int i = hash % entries.length; + Entry e = entries[i]; + if (e == null) + return null; + else + return (V) e.find(k); + } + } + + + private final static int[] primes = { + /* Skip some early ones that are close together */ + 2, /* 3, */ 5, /* 7, */ 11, /* 13, */ 17, /* 19, */ 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, + 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, + 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, + 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, + 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, + 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, + 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997, 1009, + /* now we start skipping some, this is arbitrary */ + 2053, 3079, 4057, 7103, 10949, 16069, 32609, 65867, 104729 + }; + + private static int nextPrime(int i) { + for (int p : primes) { + if (p > i) + return p; + } + /* oh well */ + return primes[primes.length - 1]; + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java b/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java index 69da879b4..902311359 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java @@ -11,9 +11,11 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.time.Duration; +import java.util.Set; import com.typesafe.config.Config; import com.typesafe.config.ConfigObject; @@ -160,6 +162,8 @@ private static Object getValue(Class beanClass, Type parameterType, Class return config.getAnyRef(configPropName); } else if (parameterClass == List.class) { return getListValue(beanClass, parameterType, parameterClass, config, configPropName); + } else if (parameterClass == Set.class) { + return getSetValue(beanClass, parameterType, parameterClass, config, configPropName); } else if (parameterClass == Map.class) { // we could do better here, but right now we don't. Type[] typeArgs = ((ParameterizedType)parameterType).getActualTypeArguments(); @@ -186,6 +190,10 @@ private static Object getValue(Class beanClass, Type parameterType, Class } } + private static Object getSetValue(Class beanClass, Type parameterType, Class parameterClass, Config config, String configPropName) { + return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPropName)); + } + private static Object getListValue(Class beanClass, Type parameterType, Class parameterClass, Config config, String configPropName) { Type elementType = ((ParameterizedType)parameterType).getActualTypeArguments()[0]; @@ -277,7 +285,10 @@ private static boolean hasAtLeastOneBeanProperty(Class clazz) { private static boolean isOptionalProperty(Class beanClass, PropertyDescriptor beanProp) { Field field = getField(beanClass, beanProp.getName()); - return field != null && (field.getAnnotationsByType(Optional.class).length > 0); + return field != null + ? field.getAnnotationsByType(Optional.class).length > 0 + || beanProp.getReadMethod().getAnnotationsByType(Optional.class).length > 0 + : beanProp.getReadMethod().getAnnotationsByType(Optional.class).length > 0; } private static Field getField(Class beanClass, String fieldName) { diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java index 235608a3e..29ffc70a1 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java @@ -261,7 +261,8 @@ private AbstractConfigNodeValue parseValue(Token t) { private ConfigNodePath parseKey(Token token) { if (flavor == ConfigSyntax.JSON) { if (Tokens.isValueWithType(token, ConfigValueType.STRING)) { - return PathParser.parsePathNodeExpression(Collections.singletonList(token).iterator(), null); + return PathParser.parsePathNodeExpression(Collections.singletonList(token).iterator(), + baseOrigin.withLineNumber(lineNumber)); } else { throw parseError("Expecting close brace } or a field name here, got " + token); @@ -279,7 +280,8 @@ private ConfigNodePath parseKey(Token token) { } putBack(t); // put back the token we ended with - return PathParser.parsePathNodeExpression(expression.iterator(), null); + return PathParser.parsePathNodeExpression(expression.iterator(), + baseOrigin.withLineNumber(lineNumber)); } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigImpl.java b/config/src/main/java/com/typesafe/config/impl/ConfigImpl.java index 9cf49913b..f14c12af6 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigImpl.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -32,6 +32,7 @@ * For use only by the {@link com.typesafe.config} package. */ public class ConfigImpl { + private static final String ENV_VAR_OVERRIDE_PREFIX = "CONFIG_FORCE_"; private static class LoaderCache { private Config currentSystemProperties; @@ -301,7 +302,13 @@ private static Properties getSystemProperties() { final Properties systemProperties = System.getProperties(); final Properties systemPropertiesCopy = new Properties(); synchronized (systemProperties) { - systemPropertiesCopy.putAll(systemProperties); + for (Map.Entry entry: systemProperties.entrySet()) { + // Java 11 introduces 'java.version.date', but we don't want that to + // overwrite 'java.version' + if (!entry.getKey().toString().startsWith("java.version.")) { + systemPropertiesCopy.put(entry.getKey(), entry.getValue()); + } + } } return systemPropertiesCopy; } @@ -360,19 +367,82 @@ public static void reloadEnvVariablesConfig() { EnvVariablesHolder.envVariables = loadEnvVariables(); } + + + private static AbstractConfigObject loadEnvVariablesOverrides() { + Map env = new HashMap(System.getenv()); + Map result = new HashMap(System.getenv()); + + for (String key : env.keySet()) { + if (key.startsWith(ENV_VAR_OVERRIDE_PREFIX)) { + result.put(ConfigImplUtil.envVariableAsProperty(key, ENV_VAR_OVERRIDE_PREFIX), env.get(key)); + } + } + + return PropertiesParser.fromStringMap(newSimpleOrigin("env variables overrides"), result); + } + + private static class EnvVariablesOverridesHolder { + static volatile AbstractConfigObject envVariables = loadEnvVariablesOverrides(); + } + + static AbstractConfigObject envVariablesOverridesAsConfigObject() { + try { + return EnvVariablesOverridesHolder.envVariables; + } catch (ExceptionInInitializerError e) { + throw ConfigImplUtil.extractInitializerError(e); + } + } + + public static Config envVariablesOverridesAsConfig() { + return envVariablesOverridesAsConfigObject().toConfig(); + } + + public static void reloadEnvVariablesOverridesConfig() { + // ConfigFactory.invalidateCaches() relies on this having the side + // effect that it drops all caches + EnvVariablesOverridesHolder.envVariables = loadEnvVariablesOverrides(); + } + public static Config defaultReference(final ClassLoader loader) { return computeCachedConfig(loader, "defaultReference", new Callable() { @Override public Config call() { - Config unresolvedResources = Parseable - .newResources("reference.conf", - ConfigParseOptions.defaults().setClassLoader(loader)) - .parse().toConfig(); + Config unresolvedResources = unresolvedReference(loader); return systemPropertiesAsConfig().withFallback(unresolvedResources).resolve(); } }); } + private static Config unresolvedReference(final ClassLoader loader) { + return computeCachedConfig(loader, "unresolvedReference", new Callable() { + @Override + public Config call() { + return Parseable.newResources("reference.conf", + ConfigParseOptions.defaults().setClassLoader(loader)) + .parse().toConfig(); + } + }); + } + + /** + * This returns the unresolved reference configuration, but before doing so, + * it verifies that the reference configuration resolves, to ensure that it + * is self contained and doesn't depend on any higher level configuration + * files. + */ + public static Config defaultReferenceUnresolved(final ClassLoader loader) { + // First, verify that `reference.conf` resolves by itself. + try { + defaultReference(loader); + } catch (ConfigException.UnresolvedSubstitution e) { + throw e.addExtraDetail("Could not resolve substitution in reference.conf to a value: %s. All reference.conf files are required to be fully, independently resolvable, and should not require the presence of values for substitutions from further up the hierarchy."); + } + // Now load the unresolved version + return unresolvedReference(loader); + } + + private static class DebugHolder { private static String LOADS = "loads"; private static String SUBSTITUTIONS = "substitutions"; diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java b/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java index 279f5ec84..1a8ffc1db 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java @@ -15,6 +15,7 @@ import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigOrigin; +import com.typesafe.config.ConfigSyntax; /** * Internal implementation detail, not ABI stable, do not touch. @@ -233,4 +234,69 @@ static String toCamelCase(String originalName) { } return nameBuilder.toString(); } + + private static char underscoreMappings(int num) { + // Rationale on name mangling: + // + // Most shells (e.g. bash, sh, etc.) doesn't support any character other + // than alphanumeric and `_` in environment variables names. + // In HOCON the default separator is `.` so it is directly translated to a + // single `_` for convenience; `-` and `_` are less often present in config + // keys but they have to be representable and the only possible mapping is + // `_` repeated. + switch (num) { + case 1: return '.'; + case 2: return '-'; + case 3: return '_'; + default: return 0; + } + } + + static String envVariableAsProperty(String variable, String prefix) throws ConfigException { + StringBuilder builder = new StringBuilder(); + + String strippedPrefix = variable.substring(prefix.length(), variable.length()); + + int underscores = 0; + for (char c : strippedPrefix.toCharArray()) { + if (c == '_') { + underscores++; + } else { + if (underscores > 0 && underscores < 4) { + builder.append(underscoreMappings(underscores)); + } else if (underscores > 3) { + throw new ConfigException.BadPath(variable, "Environment variable contains an un-mapped number of underscores."); + } + underscores = 0; + builder.append(c); + } + } + + if (underscores > 0 && underscores < 4) { + builder.append(underscoreMappings(underscores)); + } else if (underscores > 3) { + throw new ConfigException.BadPath(variable, "Environment variable contains an un-mapped number of underscores."); + } + + return builder.toString(); + } + + /** + * Guess configuration syntax from given filename. + * + * @param filename configuration filename + * @return configuration syntax if a match is found. Otherwise, null. + */ + public static ConfigSyntax syntaxFromExtension(String filename) { + if (filename == null) + return null; + else if (filename.endsWith(".json")) + return ConfigSyntax.JSON; + else if (filename.endsWith(".conf")) + return ConfigSyntax.CONF; + else if (filename.endsWith(".properties")) + return ConfigSyntax.PROPERTIES; + else + return null; + } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java index b2ce04553..1e562ace2 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java @@ -59,7 +59,7 @@ private AbstractConfigValue parseConcatenation(ConfigNodeConcatenation n) { if (flavor == ConfigSyntax.JSON) throw new ConfigException.BugOrBroken("Found a concatenation node in JSON"); - List values = new ArrayList(); + List values = new ArrayList(n.children().size()); for (AbstractConfigNode node : n.children()) { AbstractConfigValue v = null; @@ -192,7 +192,7 @@ private void parseInclude(Map values, ConfigNodeInc // we really should make this work, but for now throwing an // exception is better than producing an incorrect result. - // See https://github.com/typesafehub/config/issues/160 + // See https://github.com/lightbend/config/issues/160 if (arrayCount > 0 && obj.resolveStatus() != ResolveStatus.RESOLVED) throw parseError("Due to current limitations of the config parser, when an include statement is nested inside a list value, " + "${} substitutions inside the included file cannot be resolved correctly. Either move the include outside of the list value or " @@ -247,7 +247,7 @@ private AbstractConfigObject parseObject(ConfigNodeObject n) { // we really should make this work, but for now throwing // an exception is better than producing an incorrect // result. See - // https://github.com/typesafehub/config/issues/160 + // https://github.com/lightbend/config/issues/160 if (arrayCount > 0) throw parseError("Due to current limitations of the config parser, += does not work nested inside a list. " + "+= expands to a ${} substitution and the path in ${} cannot currently refer to list elements. " diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java index 8d3c7c0c0..077503d40 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigReference.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigReference.java @@ -6,6 +6,8 @@ import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigRenderOptions; +import com.typesafe.config.ConfigResolveOptions; +import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; /** @@ -88,7 +90,8 @@ ResolveResult resolveSubstitutions(ResolveContext v = result.value; newContext = result.context; } else { - v = null; + ConfigValue fallback = context.options().getResolver().lookup(expr.path().render()); + v = (AbstractConfigValue) fallback; } } catch (NotPossibleToResolve e) { if (ConfigImpl.traceSubstitutionsEnabled()) 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 9f66af195..ad5146301 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parseable.java +++ b/config/src/main/java/com/typesafe/config/impl/Parseable.java @@ -180,6 +180,8 @@ final private AbstractConfigValue parseValue(ConfigOrigin origin, return rawParseValue(origin, finalOptions); } catch (IOException e) { if (finalOptions.getAllowMissing()) { + trace(e.getMessage() + ". Allowing Missing File, this can be turned off by setting" + + " ConfigParseOptions.allowMissing = false"); return SimpleConfigObject.emptyMissing(origin); } else { trace("exception loading " + origin.description() + ": " + e.getClass().getName() @@ -324,17 +326,6 @@ public String toString() { return getClass().getSimpleName(); } - private static ConfigSyntax syntaxFromExtension(String name) { - if (name.endsWith(".json")) - return ConfigSyntax.JSON; - else if (name.endsWith(".conf")) - return ConfigSyntax.CONF; - else if (name.endsWith(".properties")) - return ConfigSyntax.PROPERTIES; - else - return null; - } - private static Reader readerFromStream(InputStream input) { return readerFromStream(input, "UTF-8"); } @@ -572,7 +563,7 @@ protected Reader reader(ConfigParseOptions options) throws IOException { @Override ConfigSyntax guessSyntax() { - return syntaxFromExtension(input.getPath()); + return ConfigImplUtil.syntaxFromExtension(input.getPath()); } @Override @@ -641,7 +632,7 @@ protected Reader reader() throws IOException { @Override ConfigSyntax guessSyntax() { - return syntaxFromExtension(input.getName()); + return ConfigImplUtil.syntaxFromExtension(input.getName()); } @Override @@ -754,7 +745,7 @@ protected AbstractConfigObject rawParseValue(ConfigOrigin origin, @Override ConfigSyntax guessSyntax() { - return syntaxFromExtension(resource); + return ConfigImplUtil.syntaxFromExtension(resource); } static String parent(String resource) { diff --git a/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java b/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java index cf98a900d..941d44f65 100644 --- a/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java +++ b/config/src/main/java/com/typesafe/config/impl/ResolveMemos.java @@ -1,8 +1,5 @@ package com.typesafe.config.impl; -import java.util.HashMap; -import java.util.Map; - /** * This exists because we have to memoize resolved substitutions as we go * through the config tree; otherwise we could end up creating multiple copies @@ -11,14 +8,14 @@ final class ResolveMemos { // note that we can resolve things to undefined (represented as Java null, // rather than ConfigNull) so this map can have null values. - final private Map memos; + final private BadMap memos; - private ResolveMemos(Map memos) { + private ResolveMemos(BadMap memos) { this.memos = memos; } ResolveMemos() { - this(new HashMap()); + this(new BadMap<>()); } AbstractConfigValue get(MemoKey key) { @@ -26,10 +23,6 @@ AbstractConfigValue get(MemoKey key) { } ResolveMemos put(MemoKey key, AbstractConfigValue value) { - // completely inefficient, but so far nobody cares about resolve() - // performance, we can clean it up someday... - Map copy = new HashMap(memos); - copy.put(key, value); - return new ResolveMemos(copy); + return new ResolveMemos(memos.copyingPut(key, value)); } } diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java index c31471244..c0d6b65c7 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -7,7 +7,11 @@ import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.DateTimeException; import java.time.Duration; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; @@ -149,7 +153,7 @@ static private AbstractConfigValue findKeyOrNull(AbstractConfigObject self, Stri ConfigValueType expected, Path originalPath) { AbstractConfigValue v = self.peekAssumingResolved(key, originalPath); if (v == null) - throw new ConfigException.Missing(originalPath.render()); + throw new ConfigException.Missing(self.origin(), originalPath.render()); if (expected != null) v = DefaultTransformer.transform(v, expected); @@ -322,6 +326,21 @@ public Duration getDuration(String path) { return Duration.ofNanos(nanos); } + @Override + public Period getPeriod(String path){ + ConfigValue v = find(path, ConfigValueType.STRING); + return parsePeriod((String) v.unwrapped(), v.origin(), path); + } + + @Override + public TemporalAmount getTemporal(String path){ + try{ + return getDuration(path); + } catch (ConfigException.BadValue e){ + return getPeriod(path); + } + } + @SuppressWarnings("unchecked") private List getHomogeneousUnwrappedList(String path, ConfigValueType expected) { @@ -583,6 +602,90 @@ private static String getUnits(String s) { return s.substring(i + 1); } + /** + * Parses a period string. If no units are specified in the string, it is + * assumed to be in days. The returned period is in days. + * The purpose of this function is to implement the period-related methods + * in the ConfigObject interface. + * + * @param input + * the string to parse + * @param originForException + * origin of the value being parsed + * @param pathForException + * path to include in exceptions + * @return duration in days + * @throws ConfigException + * if string is invalid + */ + public static Period parsePeriod(String input, + ConfigOrigin originForException, String pathForException) { + String s = ConfigImplUtil.unicodeTrim(input); + String originalUnitString = getUnits(s); + String unitString = originalUnitString; + String numberString = ConfigImplUtil.unicodeTrim(s.substring(0, s.length() + - unitString.length())); + ChronoUnit units; + + // this would be caught later anyway, but the error message + // is more helpful if we check it here. + if (numberString.length() == 0) + throw new ConfigException.BadValue(originForException, + pathForException, "No number in period value '" + input + + "'"); + + if (unitString.length() > 2 && !unitString.endsWith("s")) + unitString = unitString + "s"; + + // note that this is deliberately case-sensitive + if (unitString.equals("") || unitString.equals("d") || unitString.equals("days")) { + units = ChronoUnit.DAYS; + + } else if (unitString.equals("w") || unitString.equals("weeks")) { + units = ChronoUnit.WEEKS; + + } else if (unitString.equals("m") || unitString.equals("mo") || unitString.equals("months")) { + units = ChronoUnit.MONTHS; + + } else if (unitString.equals("y") || unitString.equals("years")) { + units = ChronoUnit.YEARS; + + } else { + throw new ConfigException.BadValue(originForException, + pathForException, "Could not parse time unit '" + + originalUnitString + + "' (try d, w, mo, y)"); + } + + try { + return periodOf(Integer.parseInt(numberString), units); + } catch (NumberFormatException e) { + throw new ConfigException.BadValue(originForException, + pathForException, "Could not parse duration number '" + + numberString + "'"); + } + } + + + private static Period periodOf(int n, ChronoUnit unit){ + if(unit.isTimeBased()){ + throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); + } + + switch (unit){ + case DAYS: + return Period.ofDays(n); + case WEEKS: + return Period.ofWeeks(n); + case MONTHS: + return Period.ofMonths(n); + case YEARS: + return Period.ofYears(n); + default: + throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); + } + } + /** * Parses a duration string. If no units are specified in the string, it is * assumed to be in milliseconds. The returned duration is in nanoseconds. diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index cf5f5ea07..ef69c6885 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -5,6 +5,7 @@ import java.io.ObjectStreamException; import java.io.Serializable; +import java.math.BigInteger; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; @@ -426,15 +427,14 @@ private static boolean isAllDigits(String s) { int length = s.length(); // empty string doesn't count as a number + // string longer than "max number of digits in a long" cannot be parsed as a long if (length == 0) return false; for (int i = 0; i < length; ++i) { char c = s.charAt(i); - if (Character.isDigit(c)) - continue; - else + if (!Character.isDigit(c)) return false; } return true; @@ -449,7 +449,7 @@ public int compare(String a, String b) { boolean aDigits = isAllDigits(a); boolean bDigits = isAllDigits(b); if (aDigits && bDigits) { - return Integer.compare(Integer.parseInt(a), Integer.parseInt(b)); + return new BigInteger(a).compareTo(new BigInteger(b)); } else if (aDigits) { return -1; } else if (bDigits) { diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java index a3dc3e4d5..9e05f9e91 100644 --- a/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigOrigin.java @@ -364,7 +364,7 @@ static ConfigOrigin mergeOrigins(Collection stack) { Iterator i = stack.iterator(); return mergeTwo((SimpleConfigOrigin) i.next(), (SimpleConfigOrigin) i.next()); } else { - List remaining = new ArrayList(); + List remaining = new ArrayList(stack.size()); for (ConfigOrigin o : stack) { remaining.add((SimpleConfigOrigin) o); } diff --git a/config/src/main/java/com/typesafe/config/impl/package.html b/config/src/main/java/com/typesafe/config/impl/package.html index 4df65c25e..52592b0db 100644 --- a/config/src/main/java/com/typesafe/config/impl/package.html +++ b/config/src/main/java/com/typesafe/config/impl/package.html @@ -17,7 +17,7 @@ you're interested in browsing implementation details. None of the ABI under impl has any guarantees; it will change whenever someone feels like changing it. If you feel you need access to something -in impl, please +in impl, please file a feature request.

diff --git a/config/src/main/java/com/typesafe/config/package.html b/config/src/main/java/com/typesafe/config/package.html index 7d7fe7f29..8f5d9f454 100644 --- a/config/src/main/java/com/typesafe/config/package.html +++ b/config/src/main/java/com/typesafe/config/package.html @@ -9,14 +9,14 @@

-An API for loading and using configuration files, see the project site +An API for loading and using configuration files, see the project site for more information.

Typically you would load configuration with a static method from {@link com.typesafe.config.ConfigFactory} and then use it with methods in the {@link com.typesafe.config.Config} interface. Configuration may be in the form of JSON files, -Java properties, or HOCON files; you may also +Java properties, or HOCON files; you may also build your own configuration in code or from your own file formats.

@@ -26,8 +26,8 @@ If you use the default configuration from {@link com.typesafe.config.ConfigFactory#load()} there's no need to pass a configuration to your libraries and frameworks, as long as they all default to this same default, which they should. -
Example application code: Java and Scala. -
Showing a couple of more special-purpose features, a more complex example: Java and Scala. +
Example application code: Java and Scala. +
Showing a couple of more special-purpose features, a more complex example: Java and Scala.

@@ -36,21 +36,21 @@ call {@link com.typesafe.config.ConfigFactory#load()} to get the default one. Typically a library might offer two constructors, one with a Config parameter and one which uses {@link com.typesafe.config.ConfigFactory#load()}. -
Example library code: Java and Scala. +
Example library code: Java and Scala.

-Check out the full examples directory on GitHub. +Check out the full examples directory on GitHub.

What else to read:

  • The overview documentation for interface {@link com.typesafe.config.Config}.
  • -
  • The README for the library.
  • +
  • The README for the library.
  • If you want to use .conf files in addition to .json and .properties, - see the README for some short examples - and the full HOCON spec for the long version.
  • + see the README for some short examples + and the full HOCON spec for the long version.

diff --git a/config/src/main/java/com/typesafe/config/parser/ConfigNode.java b/config/src/main/java/com/typesafe/config/parser/ConfigNode.java index 43ac3f51b..e9954e9a7 100644 --- a/config/src/main/java/com/typesafe/config/parser/ConfigNode.java +++ b/config/src/main/java/com/typesafe/config/parser/ConfigNode.java @@ -10,8 +10,8 @@ * Note: at present there is no way to obtain an instance of this interface, so * please ignore it. A future release will make syntax tree nodes available in * the public API. If you are interested in working on it, please see: https://github.com/typesafehub/config/issues/300 + * href="https://github.com/lightbend/config/issues/300" + * >https://github.com/lightbend/config/issues/300 * *

* Because this object is immutable, it is safe to use from multiple threads and diff --git a/config/src/main/java/com/typesafe/config/parser/package.html b/config/src/main/java/com/typesafe/config/parser/package.html index ef099cc4d..b0e7b3cea 100644 --- a/config/src/main/java/com/typesafe/config/parser/package.html +++ b/config/src/main/java/com/typesafe/config/parser/package.html @@ -14,7 +14,7 @@ the com.typesafe.config package instead. You would use the raw parser if you're doing something like reading, modifying, and re-saving a config file. For info on the main config API this parser is a part of, -see the project site. +see the project site.

diff --git a/config/src/test/java/beanconfig/ObjectsConfig.java b/config/src/test/java/beanconfig/ObjectsConfig.java index f3009c96d..11d040f50 100644 --- a/config/src/test/java/beanconfig/ObjectsConfig.java +++ b/config/src/test/java/beanconfig/ObjectsConfig.java @@ -8,6 +8,7 @@ public static class ValueObject { @Optional private String optionalValue; private String mandatoryValue; + private String Default; public String getMandatoryValue() { return mandatoryValue; @@ -24,6 +25,15 @@ public String getOptionalValue() { public void setOptionalValue(String optionalValue) { this.optionalValue = optionalValue; } + + @Optional + public String getDefault() { + return Default; + } + + public void setDefault(String Default) { + this.Default = Default; + } } private ValueObject valueObject; diff --git a/config/src/test/java/beanconfig/SetsConfig.java b/config/src/test/java/beanconfig/SetsConfig.java new file mode 100644 index 000000000..b81540df9 --- /dev/null +++ b/config/src/test/java/beanconfig/SetsConfig.java @@ -0,0 +1,139 @@ +package beanconfig; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigMemorySize; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigValue; + +import java.time.Duration; +import java.util.Set; + +public class SetsConfig { + + Set empty; + Set ofInt; + Set ofString; + Set ofDouble; + Set ofLong; + Set ofNull; + Set ofBoolean; + Set ofObject; + Set ofConfig; + Set ofConfigObject; + Set ofConfigValue; + Set ofDuration; + Set ofMemorySize; + Set ofStringBean; + + public Set getEmpty() { + return empty; + } + + public void setEmpty(Set empty) { + this.empty = empty; + } + + public Set getOfInt() { + return ofInt; + } + + public void setOfInt(Set ofInt) { + this.ofInt = ofInt; + } + + public Set getOfString() { + return ofString; + } + + public void setOfString(Set ofString) { + this.ofString = ofString; + } + + public Set getOfDouble() { + return ofDouble; + } + + public void setOfDouble(Set ofDouble) { + this.ofDouble = ofDouble; + } + + public Set getOfNull() { + return ofNull; + } + + public void setOfNull(Set ofNull) { + this.ofNull = ofNull; + } + + public Set getOfBoolean() { + return ofBoolean; + } + + public void setOfBoolean(Set ofBoolean) { + this.ofBoolean = ofBoolean; + } + + public Set getOfObject() { + return ofObject; + } + + public void setOfObject(Set ofObject) { + this.ofObject = ofObject; + } + + public Set getOfLong() { + return ofLong; + } + + public void setOfLong(Set ofLong) { + this.ofLong = ofLong; + } + + public Set getOfConfig() { + return ofConfig; + } + + public void setOfConfig(Set ofConfig) { + this.ofConfig = ofConfig; + } + + public Set getOfConfigObject() { + return ofConfigObject; + } + + public void setOfConfigObject(Set ofConfigObject) { + this.ofConfigObject = ofConfigObject; + } + + public Set getOfConfigValue() { + return ofConfigValue; + } + + public void setOfConfigValue(Set ofConfigValue) { + this.ofConfigValue = ofConfigValue; + } + + public Set getOfDuration() { + return ofDuration; + } + + public void setOfDuration(Set ofDuration) { + this.ofDuration = ofDuration; + } + + public Set getOfMemorySize() { + return ofMemorySize; + } + + public void setOfMemorySize(Set ofMemorySize) { + this.ofMemorySize = ofMemorySize; + } + + public Set getOfStringBean() { + return ofStringBean; + } + + public void setOfStringBean(Set ofStringBean) { + this.ofStringBean = ofStringBean; + } +} diff --git a/config/src/test/resources/beanconfig/beanconfig01.conf b/config/src/test/resources/beanconfig/beanconfig01.conf index 791553dea..2a2ea2ed5 100644 --- a/config/src/test/resources/beanconfig/beanconfig01.conf +++ b/config/src/test/resources/beanconfig/beanconfig01.conf @@ -101,5 +101,31 @@ "valueObject": { "mandatoryValue": "notNull" } + }, + "sets" : { + "empty" : [], + "ofInt" : [1, 2, 3, 2, 3], + "ofString" : [ ${strings.a}, ${strings.b}, ${strings.c} ], + "of-double" : [3.14, 4.14, 4.14, 5.14], + "of-long" : { "1" : 32, "2" : 42, "3" : 52 }, // object-to-list conversion + "ofNull" : [null, null, null], + "ofBoolean" : [true, false, false], + "ofArray" : [${arrays.ofString}, ${arrays.ofString}, ${arrays.ofString}], + "ofObject" : [${numbers}, ${booleans}, ${strings}], + "ofConfig" : [${numbers}, ${booleans}, ${strings}], + "ofConfigObject" : [${numbers}, ${booleans}, ${strings}], + "ofConfigValue" : [1, 2, "a"], + "ofDuration" : [1, 2h, 3 days], + "ofMemorySize" : [1024, 1M, 1G], + "ofStringBean" : [ + { + abcd : "testAbcdOne" + yes : "testYesOne" + }, + { + abcd : "testAbcdTwo" + yes : "testYesTwo" + } + ] } } diff --git a/config/src/test/resources/test01.conf b/config/src/test/resources/test01.conf index 1370bf852..e40eb7f5d 100644 --- a/config/src/test/resources/test01.conf +++ b/config/src/test/resources/test01.conf @@ -65,6 +65,14 @@ "minusLargeNanos" : -4878955355435272204ns }, + "periods" : { + "day" : 1d, + "dayAsNumber": 2, + "week": 3 weeks, + "month": 5 mo, + "year": 8y + }, + "memsizes" : { "meg" : 1M, "megsList" : [1M, 1024K, 1048576], diff --git a/config/src/test/resources/test01.properties b/config/src/test/resources/test01.properties index 94eec4470..71de4e0be 100644 --- a/config/src/test/resources/test01.properties +++ b/config/src/test/resources/test01.properties @@ -2,3 +2,4 @@ fromProps.abc=abc fromProps.one=1 fromProps.bool=true +fromProps.specialChars=hello^^ diff --git a/config/src/test/resources/test04.conf b/config/src/test/resources/test04.conf index 1aa55df07..cf024f00f 100644 --- a/config/src/test/resources/test04.conf +++ b/config/src/test/resources/test04.conf @@ -174,7 +174,7 @@ akka { mongodb { # Any specified collection name will be used as a prefix for collections that use durable mongo mailboxes - uri = "mongodb://localhost/akka.mailbox" # Follow Mongo URI Spec - http://www.mongodb.org/display/DOCS/Connections + uri = "mongodb://localhost/akka.mailbox" # Follow Mongo URI Spec - https://docs.mongodb.com/manual/reference/connection-string/ # Configurable timeouts for certain ops timeout { diff --git a/config/src/test/resources/test12.conf b/config/src/test/resources/test12.conf new file mode 100644 index 000000000..b7aff622b --- /dev/null +++ b/config/src/test/resources/test12.conf @@ -0,0 +1,43 @@ +// this checks sorting map keys, where keys that look like numbers are treated differently +// specifically tests very long numbers which fit neither in an Integer nor in a Long + +"10" = "42" +sth = 42 +"1" = "42" +"12" = "42" +"123" = "42" +"1234" = "42" +"12345" = "42" +"123456" = "42" +"1234567" = "42" +"12345678" = "42" +"123456789" = "42" +"1234567890" = "42" +"12345678901" = "42" +"123456789012" = "42" +"1234567890123" = "42" +"12345678901234" = "42" +"123456789012345" = "42" +"1234567890123456" = "42" +"12345678901234567" = "42" +"123456789012345678" = "42" +"1234567890123456789" = "42" +"12345678901234567891" = "42" +"123456789012345678912" = "42" +"1234567890123456789123" = "42" +"12345678901234567891234" = "42" +"123456789012345678912345" = "42" +"1234567890123456789123456" = "42" +"12345678901234567891234567" = "42" +"123456789012345678912345678" = "42" +"1234567890123456789123456789" = "42" +"12345678901234567891234567890" = "42" +"123456789012345678912345678901" = "42" +"1234567890123456789123456789012" = "42" +"12345678901234567891234567890123" = "42" +"123456789012345678912345678901234" = "42" +"1234567890123456789123456789012345" = "42" +"12345678901234567891234567890123456" = "42" +"123456789012345678912345678901234567" = "42" +"1234567890123456789123456789012345678" = "42" +"12345678901234567891234567890123456789" = "42" \ No newline at end of file diff --git a/config/src/test/resources/test13-application-override-substitutions.conf b/config/src/test/resources/test13-application-override-substitutions.conf new file mode 100644 index 000000000..506b2cada --- /dev/null +++ b/config/src/test/resources/test13-application-override-substitutions.conf @@ -0,0 +1 @@ +b = "overridden" \ No newline at end of file diff --git a/config/src/test/resources/test13-reference-bad-substitutions.conf b/config/src/test/resources/test13-reference-bad-substitutions.conf new file mode 100644 index 000000000..1b2615077 --- /dev/null +++ b/config/src/test/resources/test13-reference-bad-substitutions.conf @@ -0,0 +1 @@ +a = ${b} \ No newline at end of file diff --git a/config/src/test/resources/test13-reference-with-substitutions.conf b/config/src/test/resources/test13-reference-with-substitutions.conf new file mode 100644 index 000000000..fcf7f995c --- /dev/null +++ b/config/src/test/resources/test13-reference-with-substitutions.conf @@ -0,0 +1,2 @@ +a = ${b} +b = "b" \ No newline at end of file diff --git a/config/src/test/scala/ApiExamples.scala b/config/src/test/scala/ApiExamples.scala index 2a6fce76a..5d464dce3 100644 --- a/config/src/test/scala/ApiExamples.scala +++ b/config/src/test/scala/ApiExamples.scala @@ -52,7 +52,7 @@ class ApiExamples { class EnhancedConfig(c: Config) { def getAny(path: String): Any = c.getAnyRef(path) } - implicit def config2enhanced(c: Config) = new EnhancedConfig(c) + implicit def config2enhanced(c: Config): EnhancedConfig = new EnhancedConfig(c) // somewhat nicer now val e: Int = conf.getAny("ints.fortyTwo") match { diff --git a/config/src/test/scala/Profiling.scala b/config/src/test/scala/Profiling.scala index 5cd412ef1..e66768b5f 100644 --- a/config/src/test/scala/Profiling.scala +++ b/config/src/test/scala/Profiling.scala @@ -36,7 +36,7 @@ object Util { } def time(body: () => Unit, iterations: Int): Double = { - timeHelper(body, iterations, false) + timeHelper(body, iterations, retried = false) } def loop(args: Seq[String], body: () => Unit) { @@ -57,10 +57,10 @@ object FileLoad extends App { } } - val ms = Util.time(task, 4000) + val ms = Util.time(() => task(), 4000) println("file load: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } object Resolve extends App { @@ -73,10 +73,10 @@ object Resolve extends App { } } - val ms = Util.time(task, 3000000) + val ms = Util.time(() => task(), 3000000) println("resolve: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } object GetExistingPath extends App { @@ -88,10 +88,10 @@ object GetExistingPath extends App { } } - val ms = Util.time(task, 2000000) + val ms = Util.time(() => task(), 2000000) println("GetExistingPath: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } object GetSeveralExistingPaths extends App { @@ -105,10 +105,10 @@ object GetSeveralExistingPaths extends App { } } - val ms = Util.time(task, 5000000) + val ms = Util.time(() => task(), 5000000) println("GetSeveralExistingPaths: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } object HasPathOnMissing extends App { @@ -120,10 +120,10 @@ object HasPathOnMissing extends App { } } - val ms = Util.time(task, 20000000) + val ms = Util.time(() => task(), 20000000) println("HasPathOnMissing: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } object CatchExceptionOnMissing extends App { @@ -146,9 +146,9 @@ object CatchExceptionOnMissing extends App { } anotherStackFrame(40) { () => - val ms = Util.time(task, 300000) + val ms = Util.time(() => task(), 300000) println("CatchExceptionOnMissing: " + ms + "ms") - Util.loop(args, task) + Util.loop(args, () => task()) } } diff --git a/config/src/test/scala/Rendering.scala b/config/src/test/scala/Rendering.scala index 0d8be469e..1cc264be0 100644 --- a/config/src/test/scala/Rendering.scala +++ b/config/src/test/scala/Rendering.scala @@ -33,6 +33,7 @@ object RenderExample extends App { render("test01") render("test06") render("test05") + render("test12") } object RenderOptions extends App { @@ -62,7 +63,7 @@ object RenderOptions extends App { val rendered = allBooleanLists(4).foldLeft(0) { (count, values) => - val formatted = values(0) + val formatted = values.head val originComments = values(1) val comments = values(2) val json = values(3) diff --git a/config/src/test/scala/com/typesafe/config/impl/BadMapTest.scala b/config/src/test/scala/com/typesafe/config/impl/BadMapTest.scala new file mode 100644 index 000000000..bfc7af173 --- /dev/null +++ b/config/src/test/scala/com/typesafe/config/impl/BadMapTest.scala @@ -0,0 +1,94 @@ +package com.typesafe.config.impl + +import org.junit.Assert._ +import org.junit.Test + +class BadMapTest extends TestUtils { + @Test + def copyingPut(): Unit = { + val map = new BadMap[String, String]() + val copy = map.copyingPut("key", "value") + + assertNull(map.get("key")) + assertEquals("value", copy.get("key")) + } + + @Test + def retrieveOldElement(): Unit = { + val map = new BadMap[String, String]() + .copyingPut("key1", "value1") + .copyingPut("key2", "value2") + .copyingPut("key3", "value3") + + assertEquals("value1", map.get("key1")) + assertEquals("value2", map.get("key2")) + assertEquals("value3", map.get("key3")) + } + + @Test + def putOverride(): Unit = { + val map = new BadMap[String, String]() + .copyingPut("key", "value1") + .copyingPut("key", "value2") + .copyingPut("key", "value3") + + assertEquals("value3", map.get("key")) + } + + @Test + def notFound(): Unit = { + val map = new BadMap[String, String]() + + assertNull(map.get("invalid key")) + } + + @Test + def putMany(): Unit = { + val entries = (1 to 1000).map(i => (s"key$i", s"value$i")) + var map = new BadMap[String, String]() + + for ((key, value) <- entries) { + map = map.copyingPut(key, value) + } + + for ((key, value) <- entries) { + assertEquals(value, map.get(key)) + } + } + + @Test + def putSameHash(): Unit = { + val hash = 2 + val entries = (1 to 10).map(i => (new UniqueKeyWithHash(hash), s"value$i")) + var map = new BadMap[UniqueKeyWithHash, String]() + + for ((key, value) <- entries) { + map = map.copyingPut(key, value) + } + + for ((key, value) <- entries) { + assertEquals(value, map.get(key)) + } + } + + @Test + def putSameHashModLength(): Unit = { + // given that the table will eventually be the following size, we insert entries who should + // eventually all share the same index and then later be redistributed once rehashed + val size = 11 + val entries = (1 to size * 2).map(i => (new UniqueKeyWithHash(size * i), s"value$i")) + var map = new BadMap[UniqueKeyWithHash, String]() + + for ((key, value) <- entries) { + map = map.copyingPut(key, value) + } + + for ((key, value) <- entries) { + assertEquals(value, map.get(key)) + } + } + + private class UniqueKeyWithHash(hash: Int) { + override def hashCode(): Int = hash + } +} diff --git a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala index aa70347e7..acf898c3f 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala @@ -346,7 +346,7 @@ class ConcatenationTest extends TestUtils { } // We would ideally make this case NOT throw an exception but we need to do some work - // to get there, see https://github.com/typesafehub/config/issues/160 + // to get there, see https://github.com/lightbend/config/issues/160 @Test def plusEqualsMultipleTimesNestedInArray() { val e = intercept[ConfigException.Parse] { @@ -357,7 +357,7 @@ class ConcatenationTest extends TestUtils { } // We would ideally make this case NOT throw an exception but we need to do some work - // to get there, see https://github.com/typesafehub/config/issues/160 + // to get there, see https://github.com/lightbend/config/issues/160 @Test def plusEqualsMultipleTimesNestedInPlusEquals() { val e = intercept[ConfigException.Parse] { @@ -367,7 +367,7 @@ class ConcatenationTest extends TestUtils { assertTrue(e.getMessage.contains("limitation")) } - // from https://github.com/typesafehub/config/issues/177 + // from https://github.com/lightbend/config/issues/177 @Test def arrayConcatenationInDoubleNestedDelayedMerge() { val unresolved = parseConfig("""d { x = [] }, c : ${d}, c { x += 1, x += 2 }""") @@ -375,7 +375,7 @@ class ConcatenationTest extends TestUtils { assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala) } - // from https://github.com/typesafehub/config/issues/177 + // from https://github.com/lightbend/config/issues/177 @Test def arrayConcatenationAsPartOfDelayedMerge() { val unresolved = parseConfig(""" c { x: [], x : ${c.x}[1], x : ${c.x}[2] }""") @@ -383,7 +383,7 @@ class ConcatenationTest extends TestUtils { assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala) } - // from https://github.com/typesafehub/config/issues/177 + // from https://github.com/lightbend/config/issues/177 @Test def arrayConcatenationInDoubleNestedDelayedMerge2() { val unresolved = parseConfig("""d { x = [] }, c : ${d}, c { x : ${c.x}[1], x : ${c.x}[2] }""") @@ -391,7 +391,7 @@ class ConcatenationTest extends TestUtils { assertEquals(Seq(1, 2), conf.getIntList("c.x").asScala) } - // from https://github.com/typesafehub/config/issues/177 + // from https://github.com/lightbend/config/issues/177 @Test def arrayConcatenationInTripleNestedDelayedMerge() { val unresolved = parseConfig("""{ r: { d.x=[] }, q: ${r}, q : { d { x = [] }, c : ${q.d}, c { x : ${q.c.x}[1], x : ${q.c.x}[2] } } }""") diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala index 3ff873771..648e0c4ee 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -15,14 +15,14 @@ import java.util.Properties class ConfParserTest extends TestUtils { - def parseWithoutResolving(s: String) = { + def parseWithoutResolving(s: String): AbstractConfigValue = { val options = ConfigParseOptions.defaults(). setOriginDescription("test conf string"). setSyntax(ConfigSyntax.CONF) - Parseable.newString(s, options).parseValue().asInstanceOf[AbstractConfigValue] + Parseable.newString(s, options).parseValue() } - def parse(s: String) = { + def parse(s: String): AbstractConfigValue = { val tree = parseWithoutResolving(s) // resolve substitutions so we can test problems with that, like cycles or @@ -38,7 +38,7 @@ class ConfParserTest extends TestUtils { @Test def invalidConfThrows(): Unit = { // be sure we throw - for (invalid <- whitespaceVariations(invalidConf, false)) { + for (invalid <- whitespaceVariations(invalidConf, validInLift = false)) { addOffendingJsonToException("config", invalid.test) { intercept[ConfigException] { parse(invalid.test) @@ -152,7 +152,7 @@ class ConfParserTest extends TestUtils { } } catch { case e: Throwable => - System.err.println("failed on: '" + invalid + "'"); + System.err.println("failed on: '" + invalid + "'") throw e; } } @@ -267,9 +267,9 @@ class ConfParserTest extends TestUtils { { s: String => s.replace(",\n", " \n \n , \n \n ") }, { s: String => dropCurlies(s) }) - var tested = 0; + var tested = 0 for (v <- valids; change <- changes) { - tested += 1; + tested += 1 val obj = parseConfig(change(v)) assertEquals(3, obj.root.size()) assertEquals("y", obj.getString("a")) @@ -351,7 +351,7 @@ class ConfParserTest extends TestUtils { } @Test - def toStringForParseables() { + def toStringForParseablesWorks() { // just be sure the toString don't throw, to get test coverage val options = ConfigParseOptions.defaults() Parseable.newFile(new File("foo"), options).toString @@ -366,7 +366,7 @@ class ConfParserTest extends TestUtils { } private def assertComments(comments: Seq[String], conf: Config, path: String) { - assertEquals(comments, conf.getValue(path).origin().comments().asScala.toSeq) + assertEquals(comments, conf.getValue(path).origin().comments().asScala) } private def assertComments(comments: Seq[String], conf: Config, path: String, index: Int) { diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala index 8c85e4550..b7e56ebf7 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala @@ -7,7 +7,7 @@ import beanconfig.EnumsConfig.{ Solution, Problem } import com.typesafe.config._ import java.io.{ InputStream, InputStreamReader } -import java.time.Duration; +import java.time.Duration import beanconfig._ import org.junit.Assert._ @@ -19,7 +19,7 @@ import scala.collection.mutable.ArrayBuffer class ConfigBeanFactoryTest extends TestUtils { @Test - def toCamelCase() { + def testToCamelCase() { assertEquals("configProp", ConfigImplUtil.toCamelCase("config-prop")) assertEquals("configProp", ConfigImplUtil.toCamelCase("configProp")) assertEquals("fooBar", ConfigImplUtil.toCamelCase("foo-----bar")) @@ -122,16 +122,49 @@ class ConfigBeanFactoryTest extends TestUtils { ConfigMemorySize.ofBytes(1073741824)), beanConfig.getOfMemorySize.asScala) - val stringsConfigOne = new StringsConfig(); + val stringsConfigOne = new StringsConfig() stringsConfigOne.setAbcd("testAbcdOne") stringsConfigOne.setYes("testYesOne") - val stringsConfigTwo = new StringsConfig(); + val stringsConfigTwo = new StringsConfig() stringsConfigTwo.setAbcd("testAbcdTwo") stringsConfigTwo.setYes("testYesTwo") assertEquals(List(stringsConfigOne, stringsConfigTwo).asJava, beanConfig.getOfStringBean) } + @Test + def testCreateSet() { + val beanConfig: SetsConfig = ConfigBeanFactory.create(loadConfig().getConfig("sets"), classOf[SetsConfig]) + assertNotNull(beanConfig) + assertEquals(Set().asJava, beanConfig.getEmpty) + assertEquals(Set(1, 2, 3).asJava, beanConfig.getOfInt) + assertEquals(Set(32L, 42L, 52L).asJava, beanConfig.getOfLong) + assertEquals(Set("a", "b", "c").asJava, beanConfig.getOfString) + assertEquals(3, beanConfig.getOfObject.size) + assertEquals(3, beanConfig.getOfDouble.size) + assertEquals(3, beanConfig.getOfConfig.size) + assertTrue(beanConfig.getOfConfig.iterator().next().isInstanceOf[Config]) + assertEquals(3, beanConfig.getOfConfigObject.size) + assertTrue(beanConfig.getOfConfigObject.iterator().next().isInstanceOf[ConfigObject]) + assertEquals(Set(intValue(1), intValue(2), stringValue("a")), + beanConfig.getOfConfigValue.asScala) + assertEquals(Set(Duration.ofMillis(1), Duration.ofHours(2), Duration.ofDays(3)), + beanConfig.getOfDuration.asScala) + assertEquals(Set(ConfigMemorySize.ofBytes(1024), + ConfigMemorySize.ofBytes(1048576), + ConfigMemorySize.ofBytes(1073741824)), + beanConfig.getOfMemorySize.asScala) + + val stringsConfigOne = new StringsConfig() + stringsConfigOne.setAbcd("testAbcdOne") + stringsConfigOne.setYes("testYesOne") + val stringsConfigTwo = new StringsConfig() + stringsConfigTwo.setAbcd("testAbcdTwo") + stringsConfigTwo.setYes("testYesTwo") + + assertEquals(Set(stringsConfigOne, stringsConfigTwo).asJava, beanConfig.getOfStringBean) + } + @Test def testCreateDuration() { val beanConfig: DurationsConfig = ConfigBeanFactory.create(loadConfig().getConfig("durations"), classOf[DurationsConfig]) @@ -167,7 +200,7 @@ class ConfigBeanFactoryTest extends TestUtils { assertEquals("abcd", beanConfig.getConfig.getString("abcd")) assertEquals(3, beanConfig.getConfigObj.toConfig.getInt("intVal")) assertEquals(stringValue("hello world"), beanConfig.getConfigValue) - assertEquals(List(1, 2, 3).map(intValue(_)), beanConfig.getList.asScala) + assertEquals(List(1, 2, 3).map(intValue), beanConfig.getList.asScala) assertEquals(true, beanConfig.getUnwrappedMap.get("shouldBeInt")) assertEquals(42, beanConfig.getUnwrappedMap.get("should-be-boolean")) } @@ -178,6 +211,7 @@ class ConfigBeanFactoryTest extends TestUtils { assertNotNull(beanConfig) assertNotNull(beanConfig.getValueObject) assertNull(beanConfig.getValueObject.getOptionalValue) + assertNull(beanConfig.getValueObject.getDefault) assertEquals("notNull", beanConfig.getValueObject.getMandatoryValue) } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala index fb8bca20b..9cba2eb0a 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala @@ -1,6 +1,7 @@ package com.typesafe.config.impl -import com.typesafe.config.{ ConfigException, ConfigSyntax, ConfigParseOptions } +import com.typesafe.config.ConfigSyntax.JSON +import com.typesafe.config.{ ConfigException, ConfigParseOptions, ConfigSyntax } import org.junit.Assert._ import org.junit.Test @@ -14,7 +15,7 @@ class ConfigDocumentParserTest extends TestUtils { private def parseJSONFailuresTest(origText: String, containsMessage: String) { var exceptionThrown = false val e = intercept[ConfigException] { - ConfigDocumentParser.parse(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + ConfigDocumentParser.parse(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) } assertTrue(e.getMessage.contains(containsMessage)) } @@ -25,7 +26,7 @@ class ConfigDocumentParserTest extends TestUtils { assertEquals(expectedRenderedText, node.render()) assertTrue(node.isInstanceOf[ConfigNodeSimpleValue]) - val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) assertEquals(expectedRenderedText, nodeJSON.render()) assertTrue(nodeJSON.isInstanceOf[ConfigNodeSimpleValue]) } @@ -35,7 +36,7 @@ class ConfigDocumentParserTest extends TestUtils { assertEquals(origText, node.render()) assertTrue(node.isInstanceOf[ConfigNodeComplexValue]) - val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) assertEquals(origText, nodeJSON.render()) assertTrue(nodeJSON.isInstanceOf[ConfigNodeComplexValue]) } @@ -45,7 +46,7 @@ class ConfigDocumentParserTest extends TestUtils { assertEquals(origText, node.render()) val e = intercept[ConfigException] { - ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) } assertTrue(e.getMessage.contains(containsMessage)) } @@ -58,7 +59,7 @@ class ConfigDocumentParserTest extends TestUtils { } @Test - def parseSuccess { + def parseSuccess() { parseTest("foo:bar") parseTest(" foo : bar ") parseTest("""include "foo.conf" """) @@ -185,7 +186,7 @@ class ConfigDocumentParserTest extends TestUtils { ] } """ - val node = ConfigDocumentParser.parse(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + val node = ConfigDocumentParser.parse(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) assertEquals(origText, node.render()) } @@ -249,7 +250,7 @@ class ConfigDocumentParserTest extends TestUtils { } @Test - def parseSingleValuesFailures { + def parseSingleValuesFailures() { // Parse Simple Value throws on leading and trailing whitespace, comments, or newlines parseLeadingTrailingFailure(" 123") parseLeadingTrailingFailure("123 ") @@ -267,17 +268,17 @@ class ConfigDocumentParserTest extends TestUtils { // Check that concatenations in JSON will throw an error var origText = "123 456 \"abc\"" - var e = intercept[ConfigException] { ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) } + var e = intercept[ConfigException] { ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) } assertTrue("expected message for parsing concat as json", e.getMessage.contains("Parsing JSON and the value set in withValueText was either a concatenation or had trailing whitespace, newlines, or comments")) // Check that keys with no separators and object values in JSON will throw an error origText = """{"foo" { "bar" : 12 } }""" - e = intercept[ConfigException] { ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax((ConfigSyntax.JSON))) } + e = intercept[ConfigException] { ConfigDocumentParser.parseValue(tokenize(origText), fakeOrigin(), ConfigParseOptions.defaults().setSyntax(JSON)) } assertTrue("expected failure for key foo followed by token", e.getMessage.contains("""Key '"foo"' may not be followed by token: '{'""")) } @Test - def parseEmptyDocument { + def parseEmptyDocument() { val node = ConfigDocumentParser.parse(tokenize(""), fakeOrigin(), ConfigParseOptions.defaults()) assertTrue(node.value().isInstanceOf[ConfigNodeObject]) assertTrue(node.value().children().isEmpty()) diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala index 1106f6041..8811d64b4 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala @@ -28,7 +28,7 @@ class ConfigDocumentTest extends TestUtils { } @Test - def configDocumentReplace { + def configDocumentReplace() { // Can handle parsing/replacement with a very simple map configDocumentReplaceConfTest("""{"a":1}""", """{"a":2}""", "2", "a") configDocumentReplaceJsonTest("""{"a":1}""", """{"a":2}""", "2", "a") @@ -37,7 +37,7 @@ class ConfigDocumentTest extends TestUtils { configDocumentReplaceConfTest("a: b\nc = d", "a: b\nc = 12", "12", "c") // Can handle parsing/replacement with a complicated map - var origText = + val origText = """{ "a":123, "b": 123.456, diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala index 712e1c438..61047c405 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigSubstitutionTest.scala @@ -79,7 +79,7 @@ class ConfigSubstitutionTest extends TestUtils { def resolveNull() { val s = subst("bar.null") val v = resolveWithoutFallbacks(s, simpleObject) - assertEquals(nullValue(), v) + assertEquals(nullValue, v) } @Test @@ -126,13 +126,13 @@ class ConfigSubstitutionTest extends TestUtils { @Test def resolveMissingInString() { - val s = substInString("bar.missing", true /* optional */ ) + val s = substInString("bar.missing", optional = true) val v = resolveWithoutFallbacks(s, simpleObject) // absent object becomes empty string assertEquals(stringValue("start<>end"), v) intercept[ConfigException.UnresolvedSubstitution] { - val s2 = substInString("bar.missing", false /* optional */ ) + val s2 = substInString("bar.missing", optional = false) resolveWithoutFallbacks(s2, simpleObject) } } @@ -654,9 +654,9 @@ class ConfigSubstitutionTest extends TestUtils { values.put("a", child.relativized(new Path("a"))) // this "foo" should NOT be used. - values.put("foo", stringValue("in parent")); + values.put("foo", stringValue("in parent")) - val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values)); + val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values)) assertEquals("in child", resolved.getString("a.bar")) } @@ -670,9 +670,9 @@ class ConfigSubstitutionTest extends TestUtils { values.put("a", child.relativized(new Path("a"))) // so this "foo" SHOULD be used - values.put("foo", stringValue("in parent")); + values.put("foo", stringValue("in parent")) - val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values)); + val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values)) assertEquals("in parent", resolved.getString("a.bar")) } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index d88b365c4..eeda74e70 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -3,13 +3,16 @@ */ package com.typesafe.config.impl +import java.time.temporal.{ ChronoUnit, TemporalUnit } + import org.junit.Assert._ import org.junit._ import com.typesafe.config._ import java.util.concurrent.TimeUnit + import scala.collection.JavaConverters._ import com.typesafe.config.ConfigResolveOptions -import java.util.concurrent.TimeUnit.{ SECONDS, NANOSECONDS, MICROSECONDS, MILLISECONDS, MINUTES, DAYS, HOURS } +import java.util.concurrent.TimeUnit.{ DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS, SECONDS } class ConfigTest extends TestUtils { @@ -22,7 +25,7 @@ class ConfigTest extends TestUtils { ConfigResolveOptions.noSystem()).asInstanceOf[AbstractConfigObject].toConfig } - def mergeUnresolved(toMerge: AbstractConfigObject*) = { + def mergeUnresolved(toMerge: AbstractConfigObject*): AbstractConfigObject = { if (toMerge.isEmpty) { SimpleConfigObject.empty() } else { @@ -30,7 +33,7 @@ class ConfigTest extends TestUtils { } } - def merge(toMerge: AbstractConfigObject*) = { + def merge(toMerge: AbstractConfigObject*): AbstractConfigObject = { val obj = mergeUnresolved(toMerge: _*) resolveNoSystem(obj, obj) match { case x: AbstractConfigObject => x @@ -43,34 +46,31 @@ class ConfigTest extends TestUtils { def makeTrees(objects: Seq[AbstractConfigObject]): Iterator[AbstractConfigObject] = { objects.length match { case 0 => Iterator.empty - case 1 => { + case 1 => Iterator(objects(0)) - } - case 2 => { + case 2 => Iterator(objects(0).withFallback(objects(1))) - } - case n => { + case n => val leftSplits = for { - i <- (1 until n) + i <- 1 until n pair = objects.splitAt(i) first = pair._1.reduceLeft(_.withFallback(_)) second = pair._2.reduceLeft(_.withFallback(_)) } yield first.withFallback(second) val rightSplits = for { - i <- (1 until n) + i <- 1 until n pair = objects.splitAt(i) first = pair._1.reduceRight(_.withFallback(_)) second = pair._2.reduceRight(_.withFallback(_)) } yield first.withFallback(second) leftSplits.iterator ++ rightSplits.iterator - } } } val trees = makeTrees(allObjects).toSeq for (tree <- trees) { // if this fails, we were not associative. - if (!trees(0).equals(tree)) + if (!trees.head.equals(tree)) throw new AssertionError("Merge was not associative, " + "verify that it should not be, then don't use associativeMerge " + "for this one. two results were: \none: " + trees(0) + "\ntwo: " + @@ -349,7 +349,7 @@ class ConfigTest extends TestUtils { val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : 57 } } } """) val merged = mergeUnresolved(fixUpCycle, cycleObject) val v = resolveNoSystem(subst("foo"), merged) - assertEquals(intValue(57), v); + assertEquals(intValue(57), v) } @Test @@ -399,7 +399,7 @@ class ConfigTest extends TestUtils { val resolved = resolveNoSystem(merged, merged) assertEquals(3, resolved.root.size()) - assertEquals(42, resolved.getInt("j")); + assertEquals(42, resolved.getInt("j")) assertEquals(2, resolved.getInt("b.y")) assertEquals(3, resolved.getInt("c.z")) } @@ -565,7 +565,7 @@ class ConfigTest extends TestUtils { // to get null we have to use the get() method from Map, // which takes a key and not a path - assertEquals(nullValue(), conf.getObject("nulls").get("null")) + assertEquals(nullValue, conf.getObject("nulls").get("null")) assertNull(conf.root.get("notinthefile")) // get stuff with getValue @@ -811,6 +811,13 @@ class ConfigTest extends TestUtils { assertDurationAsTimeUnit(HOURS) assertDurationAsTimeUnit(DAYS) + // periods + assertEquals(1, conf.getPeriod("periods.day").get(ChronoUnit.DAYS)) + assertEquals(2, conf.getPeriod("periods.dayAsNumber").getDays) + assertEquals(3 * 7, conf.getTemporal("periods.week").get(ChronoUnit.DAYS)) + assertEquals(5, conf.getTemporal("periods.month").get(ChronoUnit.MONTHS)) + assertEquals(8, conf.getTemporal("periods.year").get(ChronoUnit.YEARS)) + // should get size in bytes assertEquals(1024 * 1024L, conf.getBytes("memsizes.meg")) assertEquals(1024 * 1024L, conf.getBytes("memsizes.megAsNumber")) @@ -941,8 +948,8 @@ class ConfigTest extends TestUtils { // include should have overridden the "ints" value in test03 assertEquals(42, conf.getInt("test01.ints.fortyTwo")) // include should have been overridden by 42 - assertEquals(42, conf.getInt("test01.booleans")); - assertEquals(42, conf.getInt("test01.booleans")); + assertEquals(42, conf.getInt("test01.booleans")) + assertEquals(42, conf.getInt("test01.booleans")) // include should have gotten .properties and .json also assertEquals("abc", conf.getString("test01.fromProps.abc")) assertEquals("A", conf.getString("test01.fromJsonA")) @@ -973,10 +980,10 @@ class ConfigTest extends TestUtils { // check that includes into the root object work and that // "substitutions look relative-to-included-file first then at root second" works - assertEquals("This is in the included file", conf.getString("a")); - assertEquals("This is in the including file", conf.getString("b")); - assertEquals("This is in the included file", conf.getString("subtree.a")); - assertEquals("This is in the including file", conf.getString("subtree.b")); + assertEquals("This is in the included file", conf.getString("a")) + assertEquals("This is in the including file", conf.getString("b")) + assertEquals("This is in the included file", conf.getString("subtree.a")) + assertEquals("This is in the including file", conf.getString("subtree.b")) } @Test @@ -1083,6 +1090,53 @@ class ConfigTest extends TestUtils { assertEquals(10, resolved.getInt("bar.nested.a.q")) } + @Test + def testEnvVariablesNameMangling() { + assertEquals("a", ConfigImplUtil.envVariableAsProperty("prefix_a", "prefix_")) + assertEquals("a.b", ConfigImplUtil.envVariableAsProperty("prefix_a_b", "prefix_")) + assertEquals("a.b.c", ConfigImplUtil.envVariableAsProperty("prefix_a_b_c", "prefix_")) + assertEquals("a.b-c-d", ConfigImplUtil.envVariableAsProperty("prefix_a_b__c__d", "prefix_")) + assertEquals("a.b_c_d", ConfigImplUtil.envVariableAsProperty("prefix_a_b___c___d", "prefix_")) + + intercept[ConfigException.BadPath] { + ConfigImplUtil.envVariableAsProperty("prefix_____", "prefix_") + } + intercept[ConfigException.BadPath] { + ConfigImplUtil.envVariableAsProperty("prefix_a_b___c____d", "prefix_") + } + } + + @Test + def testLoadWithEnvSubstitutions() { + System.setProperty("config.override_with_env_vars", "true") + + try { + val loader02 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("test02.conf").toURI.toURL())) + + val loader04 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("test04.conf").toURI.toURL())) + + val conf02 = withContextClassLoader(loader02) { + ConfigFactory.load() + } + + val conf04 = withContextClassLoader(loader04) { + ConfigFactory.load() + } + + assertEquals(1, conf02.getInt("42_a")) + assertEquals(2, conf02.getInt("a.b.c")) + assertEquals(3, conf02.getInt("a-c")) + assertEquals(4, conf02.getInt("a_c")) + + assertEquals("foo", conf04.getString("akka.version")) + assertEquals(10, conf04.getInt("akka.event-handler-dispatcher.max-pool-size")) + } finally { + System.clearProperty("config.override_with_env_vars") + } + } + @Test def renderRoundTrip() { val allBooleans = true :: false :: Nil @@ -1097,11 +1151,11 @@ class ConfigTest extends TestUtils { .setOriginComments(originComments) .setComments(comments) .setJson(json) - }.toSeq + } for (i <- 1 to 10) { val numString = i.toString - val name = "/test" + { if (numString.size == 1) "0" else "" } + numString + val name = "/test" + { if (numString.length == 1) "0" else "" } + numString val conf = ConfigFactory.parseResourcesAnySyntax(classOf[ConfigTest], name, ConfigParseOptions.defaults().setAllowMissing(false)) for (renderOptions <- optionsCombos) { @@ -1124,7 +1178,7 @@ class ConfigTest extends TestUtils { if (renderOptions.getJson() && !(renderOptions.getComments() || renderOptions.getOriginComments())) { // should get valid JSON if we don't have comments and are resolved val json = try { - ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)); + ConfigFactory.parseString(resolvedRender, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) } catch { case e: Exception => System.err.println("resolvedRender is not valid json: " + resolvedRender) @@ -1233,4 +1287,70 @@ class ConfigTest extends TestUtils { val resolved = unresolved.resolveWith(source) assertEquals(43, resolved.getInt("foo")) } + + /** + * A resolver that replaces paths that start with a particular prefix with + * strings where that prefix has been replaced with another prefix. + */ + class DummyResolver(prefix: String, newPrefix: String, fallback: ConfigResolver) extends ConfigResolver { + + override def lookup(path: String): ConfigValue = { + if (path.startsWith(prefix)) + ConfigValueFactory.fromAnyRef(newPrefix + path.substring(prefix.length)) + else if (fallback != null) + fallback.lookup(path) + else + null + } + + override def withFallback(f: ConfigResolver): ConfigResolver = { + if (fallback == null) + new DummyResolver(prefix, newPrefix, f) + else + new DummyResolver(prefix, newPrefix, fallback.withFallback(f)) + } + + } + + private def runFallbackTest(expected: String, source: String, + allowUnresolved: Boolean, resolvers: ConfigResolver*): Unit = { + val unresolved = ConfigFactory.parseString(source) + var options = ConfigResolveOptions.defaults().setAllowUnresolved(allowUnresolved) + for (resolver <- resolvers) + options = options.appendResolver(resolver) + val obj = unresolved.resolve(options).root() + assertEquals(expected, obj.render(ConfigRenderOptions.concise().setJson(false))) + } + + @Test + def resolveFallback(): Unit = { + runFallbackTest( + "x=a,y=b", + "x=${a},y=${b}", false, + new DummyResolver("", "", null)) + runFallbackTest( + "x=\"a.b.c\",y=\"a.b.d\"", + "x=${a.b.c},y=${a.b.d}", false, + new DummyResolver("", "", null)) + runFallbackTest( + "x=${a.b.c},y=${a.b.d}", + "x=${a.b.c},y=${a.b.d}", true, + new DummyResolver("x.", "", null)) + runFallbackTest( + "x=${a.b.c},y=\"e.f\"", + "x=${a.b.c},y=${d.e.f}", true, + new DummyResolver("d.", "", null)) + runFallbackTest( + "w=\"Y.c.d\",x=${a},y=\"X.b\",z=\"Y.c\"", + "x=${a},y=${a.b},z=${a.b.c},w=${a.b.c.d}", true, + new DummyResolver("a.b.", "Y.", null), + new DummyResolver("a.", "X.", null)) + + runFallbackTest("x=${a.b.c}", "x=${a.b.c}", true, new DummyResolver("x.", "", null)) + val e = intercept[ConfigException.UnresolvedSubstitution] { + runFallbackTest("x=${a.b.c}", "x=${a.b.c}", false, new DummyResolver("x.", "", null)) + } + assertTrue(e.getMessage.contains("${a.b.c}")) + } + } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala index 50add6786..8d2ec3846 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigValueTest.scala @@ -140,7 +140,7 @@ class ConfigValueTest extends TestUtils { "_z_e_d_C_o_n_f_i_g_V_a_l_u_e00000000000000010C0000_x_p_w_10200000025050000001906" + "0000000D000B_f_a_k_e_ _o_r_i_g_i_n090000000100010400000001000103000000010001_x" - val a = nullValue() + val a = nullValue val b = checkSerializable(expectedSerialization, a) assertNull("b is null", b.unwrapped) } @@ -280,13 +280,13 @@ class ConfigValueTest extends TestUtils { } /** - * Reproduces the issue #461. - *

- * We use a custom de-/serializer that encodes String objects in a JDK-incompatible way. Encoding used here - * is rather simplistic: a long indicating the length in bytes (JDK uses a variable length integer) followed - * by the string's bytes. Running this test with the original SerializedConfigValue.readExternal() - * implementation results in an EOFException thrown during deserialization. - */ + * Reproduces the issue #461. + *

+ * We use a custom de-/serializer that encodes String objects in a JDK-incompatible way. Encoding used here + * is rather simplistic: a long indicating the length in bytes (JDK uses a variable length integer) followed + * by the string's bytes. Running this test with the original SerializedConfigValue.readExternal() + * implementation results in an EOFException thrown during deserialization. + */ @Test def configConfigCustomSerializable() { val aMap = configMap("a" -> 1, "b" -> 2, "c" -> 3) @@ -295,7 +295,7 @@ class ConfigValueTest extends TestUtils { assertEquals(expected, actual) } - + @Test def configListEquality() { val aScalaSeq = Seq(1, 2, 3) map { intValue(_): AbstractConfigValue } @@ -424,11 +424,11 @@ class ConfigValueTest extends TestUtils { longValue(11).toString() doubleValue(3.14).toString() stringValue("hi").toString() - nullValue().toString() + nullValue.toString() boolValue(true).toString() val emptyObj = SimpleConfigObject.empty() emptyObj.toString() - (new SimpleConfigList(fakeOrigin(), Collections.emptyList[AbstractConfigValue]())).toString() + new SimpleConfigList(fakeOrigin(), Collections.emptyList[AbstractConfigValue]()).toString() subst("a").toString() substInString("b").toString() val dm = new ConfigDelayedMerge(fakeOrigin(), List[AbstractConfigValue](subst("a"), subst("b")).asJava) @@ -499,7 +499,7 @@ class ConfigValueTest extends TestUtils { val l: ConfigList = new SimpleConfigList(fakeOrigin(), scalaSeq.asJava) - assertEquals(scalaSeq(0), l.get(0)) + assertEquals(scalaSeq.head, l.get(0)) assertEquals(scalaSeq(1), l.get(1)) assertEquals(scalaSeq(2), l.get(2)) @@ -510,7 +510,7 @@ class ConfigValueTest extends TestUtils { assertEquals(1, l.indexOf(scalaSeq(1))) - assertFalse(l.isEmpty()); + assertFalse(l.isEmpty()) assertEquals(scalaSeq, l.iterator().asScala.toSeq) @@ -611,12 +611,12 @@ class ConfigValueTest extends TestUtils { val obj = parseConfig("{ a : " + a + ", b : " + b + ", c : " + c + ", d : " + d + "}") assertEquals(Seq(a, b, c, d), - Seq("a", "b", "c", "d") map { obj.getString(_) }) + Seq("a", "b", "c", "d") map { obj.getString }) // make sure it still works if we're doing concatenation val obj2 = parseConfig("{ a : xx " + a + " yy, b : xx " + b + " yy, c : xx " + c + " yy, d : xx " + d + " yy}") assertEquals(Seq(a, b, c, d) map { "xx " + _ + " yy" }, - Seq("a", "b", "c", "d") map { obj2.getString(_) }) + Seq("a", "b", "c", "d") map { obj2.getString }) } @Test @@ -625,25 +625,25 @@ class ConfigValueTest extends TestUtils { val values = new java.util.HashMap[String, AbstractConfigValue]() if (!empty) values.put("hello", intValue(37)) - new SimpleConfigObject(SimpleConfigOrigin.newSimple(desc), values); + new SimpleConfigObject(SimpleConfigOrigin.newSimple(desc), values) } def m(values: AbstractConfigObject*) = { AbstractConfigObject.mergeOrigins(values: _*).description() } // simplest case - assertEquals("merge of a,b", m(o("a", false), o("b", false))) + assertEquals("merge of a,b", m(o("a", empty = false), o("b", empty = false))) // combine duplicate "merge of" - assertEquals("merge of a,x,y", m(o("a", false), o("merge of x,y", false))) - assertEquals("merge of a,b,x,y", m(o("merge of a,b", false), o("merge of x,y", false))) + assertEquals("merge of a,x,y", m(o("a", empty = false), o("merge of x,y", empty = false))) + assertEquals("merge of a,b,x,y", m(o("merge of a,b", empty = false), o("merge of x,y", empty = false))) // ignore empty objects - assertEquals("a", m(o("foo", true), o("a", false))) + assertEquals("a", m(o("foo", empty = true), o("a", empty = false))) // unless they are all empty, pick the first one - assertEquals("foo", m(o("foo", true), o("a", true))) + assertEquals("foo", m(o("foo", empty = true), o("a", empty = true))) // merge just one - assertEquals("foo", m(o("foo", false))) + assertEquals("foo", m(o("foo", empty = false))) // merge three - assertEquals("merge of a,b,c", m(o("a", false), o("b", false), o("c", false))) + assertEquals("merge of a,b,c", m(o("a", empty = false), o("b", empty = false), o("c", empty = false))) } @Test @@ -661,7 +661,7 @@ class ConfigValueTest extends TestUtils { assertTrue(obj.hasPath("b")) // hasPath() is false for null values but containsKey is true - assertEquals(nullValue(), obj.root.get("a")) + assertEquals(nullValue, obj.root.get("a")) assertTrue(obj.root.containsKey("a")) assertFalse(obj.hasPath("a")) @@ -729,7 +729,7 @@ class ConfigValueTest extends TestUtils { assertEquals(-1, noFilename.lineNumber()) assertEquals("foo: 3", filenameWithLine.description()) - assertEquals("bar: 4", noFilenameWithLine.description()); + assertEquals("bar: 4", noFilenameWithLine.description()) assertEquals(3, filenameWithLine.lineNumber()) assertEquals(4, noFilenameWithLine.lineNumber()) @@ -944,10 +944,10 @@ class ConfigValueTest extends TestUtils { }) def top(v: SimpleConfigList) = v.origin def middle(v: SimpleConfigList) = v.get(0).origin - def bottom(v: SimpleConfigList) = if (v.get(0).isInstanceOf[ConfigList]) - Some(v.get(0).asInstanceOf[ConfigList].get(0).origin) - else - None + def bottom(v: SimpleConfigList) = v.get(0) match { + case list: ConfigList => Some(list.get(0).origin) + case _ => None + } //System.err.println("values=\n " + values.map(v => top(v).description + ", " + middle(v).description + ", " + bottom(v).map(_.description)).mkString("\n ")) for (v <- values) { @@ -982,8 +982,8 @@ class ConfigValueTest extends TestUtils { @Test def renderSorting(): Unit = { - val config = parseConfig("""0=a,1=b,2=c,3=d,10=e,20=f,30=g""") + val config = parseConfig("""0=a,1=b,2=c,999999999999999999999999999999999999999999999=0,3=d,10=e,20a=f,20=g,30=h""") val rendered = config.root.render(ConfigRenderOptions.concise()) - assertEquals("""{"0":"a","1":"b","2":"c","3":"d","10":"e","20":"f","30":"g"}""", rendered) + assertEquals("""{"0":"a","1":"b","2":"c","3":"d","10":"e","20":"g","30":"h","999999999999999999999999999999999999999999999":0,"20a":"f"}""", rendered) } } diff --git a/config/src/test/scala/com/typesafe/config/impl/EquivalentsTest.scala b/config/src/test/scala/com/typesafe/config/impl/EquivalentsTest.scala index 3dd6d9938..62176cfe2 100644 --- a/config/src/test/scala/com/typesafe/config/impl/EquivalentsTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/EquivalentsTest.scala @@ -63,7 +63,7 @@ class EquivalentsTest extends TestUtils { val (originals, others) = files.partition({ f => f.getName().startsWith("original.") }) if (originals.isEmpty) throw new RuntimeException("Need a file named 'original' in " + equiv.getPath()) - if (originals.size > 1) + if (originals.length > 1) throw new RuntimeException("Multiple 'original' files in " + equiv.getPath() + ": " + originals) val original = parse(originals(0)) diff --git a/config/src/test/scala/com/typesafe/config/impl/HttpTest.scala b/config/src/test/scala/com/typesafe/config/impl/HttpTest.scala index 4cd56f064..bc8cb7e59 100644 --- a/config/src/test/scala/com/typesafe/config/impl/HttpTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/HttpTest.scala @@ -92,7 +92,7 @@ object HttpTest { private var server: Option[ToyHttp] = None - def port = server.map(_.port).getOrElse(throw new Exception("http server isn't running")) + def port: Int = server.map(_.port).getOrElse(throw new Exception("http server isn't running")) def baseUrl = s"http://127.0.0.1:$port" private def handleThreeTypes(request: Request, json: String, props: String, hocon: String): Response = { diff --git a/config/src/test/scala/com/typesafe/config/impl/JsonTest.scala b/config/src/test/scala/com/typesafe/config/impl/JsonTest.scala index 6ba421f74..c9a2a1170 100644 --- a/config/src/test/scala/com/typesafe/config/impl/JsonTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/JsonTest.scala @@ -3,29 +3,28 @@ */ package com.typesafe.config.impl -import org.junit.Assert._ -import org.junit._ -import net.liftweb.{ json => lift } import java.io.Reader -import java.io.StringReader +import java.util + import com.typesafe.config._ -import java.util.HashMap -import java.util.Collections +import net.liftweb.{ json => lift } +import org.junit.Assert._ +import org.junit._ class JsonTest extends TestUtils { def parse(s: String): ConfigValue = { val options = ConfigParseOptions.defaults(). setOriginDescription("test json string"). - setSyntax(ConfigSyntax.JSON); - Parseable.newString(s, options).parseValue(); + setSyntax(ConfigSyntax.JSON) + Parseable.newString(s, options).parseValue() } def parseAsConf(s: String): ConfigValue = { val options = ConfigParseOptions.defaults(). setOriginDescription("test conf string"). - setSyntax(ConfigSyntax.CONF); - Parseable.newString(s, options).parseValue(); + setSyntax(ConfigSyntax.CONF) + Parseable.newString(s, options).parseValue() } private[this] def toLift(value: ConfigValue): lift.JValue = { @@ -56,13 +55,11 @@ class JsonTest extends TestUtils { liftValue match { case lift.JObject(fields) => - val m = new HashMap[String, AbstractConfigValue]() + val m = new util.HashMap[String, AbstractConfigValue]() fields.foreach({ field => m.put(field.name, fromLift(field.value)) }) new SimpleConfigObject(fakeOrigin(), m) case lift.JArray(values) => - new SimpleConfigList(fakeOrigin(), values.map(fromLift(_)).asJava) - case lift.JField(name, value) => - throw new IllegalStateException("either JField was a toplevel from lift-json or this function is buggy") + new SimpleConfigList(fakeOrigin(), values.map(fromLift).asJava) case lift.JInt(i) => if (i.isValidInt) intValue(i.intValue) else longValue(i.longValue) case lift.JBool(b) => @@ -147,7 +144,7 @@ class JsonTest extends TestUtils { var tested = 0 // be sure we do the same thing as Lift when we build our JSON "DOM" - for (valid <- whitespaceVariations(validJson, true)) { + for (valid <- whitespaceVariations(validJson, validInLift = true)) { val liftAST = if (valid.liftBehaviorUnexpected) { SimpleConfigObject.empty() } else { diff --git a/config/src/test/scala/com/typesafe/config/impl/ParseableReaderTest.scala b/config/src/test/scala/com/typesafe/config/impl/ParseableReaderTest.scala new file mode 100644 index 000000000..3ded66625 --- /dev/null +++ b/config/src/test/scala/com/typesafe/config/impl/ParseableReaderTest.scala @@ -0,0 +1,30 @@ +package com.typesafe.config.impl + +import java.io.InputStreamReader + +import com.typesafe.config.{ ConfigException, ConfigFactory, ConfigParseOptions } +import org.hamcrest.CoreMatchers.containsString +import org.junit.Assert.{ assertEquals, assertThat } +import org.junit.Test + +class ParseableReaderTest extends TestUtils { + + @Test + def parse(): Unit = { + val filename = "/test01.properties" + val configInput = new InputStreamReader(getClass.getResourceAsStream(filename)) + val config = ConfigFactory.parseReader(configInput, ConfigParseOptions.defaults() + .setSyntaxFromFilename(filename)) + assertEquals("hello^^", config.getString("fromProps.specialChars")) + } + + @Test + def parseIncorrectFormat(): Unit = { + val filename = "/test01.properties" + val configInput = new InputStreamReader(getClass.getResourceAsStream(filename)) + val e = intercept[ConfigException.Parse] { + ConfigFactory.parseReader(configInput) + } + assertThat(e.getMessage, containsString("Expecting end of input or a comma, got '^'")) + } +} diff --git a/config/src/test/scala/com/typesafe/config/impl/PathTest.scala b/config/src/test/scala/com/typesafe/config/impl/PathTest.scala index 20a4d626c..9c53a9303 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PathTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PathTest.scala @@ -15,13 +15,13 @@ class PathTest extends TestUtils { // note: foo.bar is a single key here val a = Path.newKey("foo.bar") // check that newKey worked - assertEquals(path("foo.bar"), a); + assertEquals(path("foo.bar"), a) val sameAsA = Path.newKey("foo.bar") val differentKey = Path.newKey("hello") // here foo.bar is two elements val twoElements = Path.newPath("foo.bar") // check that newPath worked - assertEquals(path("foo", "bar"), twoElements); + assertEquals(path("foo", "bar"), twoElements) val sameAsTwoElements = Path.newPath("foo.bar") checkEqualObjects(a, a) diff --git a/config/src/test/scala/com/typesafe/config/impl/PropertiesTest.scala b/config/src/test/scala/com/typesafe/config/impl/PropertiesTest.scala index 75cfe0fec..b20278e47 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PropertiesTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PropertiesTest.scala @@ -108,7 +108,7 @@ class PropertiesTest extends TestUtils { val conf = ConfigFactory.parseProperties(props, ConfigParseOptions.defaults()) val reference = ConfigFactory.parseString("{ a : [0,1,2,3,4] }") - assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala.toSeq) + assertEquals(Seq(0, 1, 2, 3, 4), conf.getIntList("a").asScala) conf.checkValid(reference) } diff --git a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala index 7d63026f2..1643a4a31 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala @@ -653,6 +653,26 @@ class PublicApiTest extends TestUtils { } } + @Test + def loadEnvironmentVariablesOverridesIfConfigured(): Unit = { + assertEquals("config.override_with_env_vars is not set", null, System.getProperty("config.override_with_env_vars")) + + System.setProperty("config.override_with_env_vars", "true") + + try { + val loaderB2 = new TestClassLoader(this.getClass().getClassLoader(), + Map("reference.conf" -> resourceFile("b_2.conf").toURI.toURL())) + + val configB2 = withContextClassLoader(loaderB2) { + ConfigFactory.load() + } + + assertEquals(5, configB2.getInt("b")) + } finally { + System.clearProperty("config.override_with_env_vars") + } + } + @Test def usesContextClassLoaderForApplicationConf() { val loaderA1 = new TestClassLoader(this.getClass().getClassLoader(), @@ -906,7 +926,7 @@ class PublicApiTest extends TestUtils { } // We would ideally make this case NOT throw an exception but we need to do some work - // to get there, see https://github.com/typesafehub/config/issues/160 + // to get there, see https://github.com/lightbend/config/issues/160 @Test def detectIncludeFromList() { val e = intercept[ConfigException.Parse] { @@ -1070,7 +1090,7 @@ class PublicApiTest extends TestUtils { @Test def heuristicIncludeChecksClasspath(): Unit = { - // from https://github.com/typesafehub/config/issues/188 + // from https://github.com/lightbend/config/issues/188 withScratchDirectory("heuristicIncludeChecksClasspath") { dir => val f = new File(dir, "foo.conf") writeFile(f, """ @@ -1132,6 +1152,28 @@ include "onclasspath" // missing underneath missing intercept[ConfigException.Missing] { conf.getIsNull("x.c.y") } } + + @Test + def applicationConfCanOverrideReferenceConf(): Unit = { + val loader = new TestClassLoader(this.getClass.getClassLoader, + Map( + "reference.conf" -> resourceFile("test13-reference-with-substitutions.conf").toURI.toURL, + "application.conf" -> resourceFile("test13-application-override-substitutions.conf").toURI.toURL)) + + assertEquals("b", ConfigFactory.defaultReference(loader).getString("a")) + assertEquals("overridden", ConfigFactory.load(loader).getString("a")) + } + + @Test(expected = classOf[ConfigException.UnresolvedSubstitution]) + def referenceConfMustResolveIndependently(): Unit = { + val loader = new TestClassLoader(this.getClass.getClassLoader, + Map( + "reference.conf" -> resourceFile("test13-reference-bad-substitutions.conf").toURI.toURL, + "application.conf" -> resourceFile("test13-application-override-substitutions.conf").toURI.toURL)) + + ConfigFactory.load(loader) + } + } class TestStrategy extends DefaultConfigLoadingStrategy { @@ -1145,4 +1187,4 @@ object TestStrategy { private var invocations = 0 def getIncovations() = invocations def increment() = invocations += 1 -} \ No newline at end of file +} diff --git a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala index 13ccd1600..d33e215fe 100644 --- a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -249,7 +249,7 @@ abstract trait TestUtils { "possibly caused by http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6446627", nf) case e: Exception => - System.err.println(e.getStackTrace.toString); + e.printStackTrace(System.err) throw new AssertionError("failed to make a copy via serialization", e) } @@ -551,7 +551,7 @@ abstract trait TestUtils { { s: String => s.replace(" ", "") }, // this would break with whitespace in a key or value { s: String => s.replace(":", " : ") }, // could break with : in a key or value { s: String => s.replace(",", " , ") } // could break with , in a key or value - ) + ) tests flatMap { t => if (t.whitespaceMatters) { Seq(t) @@ -574,7 +574,7 @@ abstract trait TestUtils { protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i, null) protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l, null) protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b) - protected def nullValue() = new ConfigNull(fakeOrigin()) + protected def nullValue = new ConfigNull(fakeOrigin()) protected def stringValue(s: String) = new ConfigString.Quoted(fakeOrigin(), s) protected def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d, null) diff --git a/config/src/test/scala/com/typesafe/config/impl/TokenizerTest.scala b/config/src/test/scala/com/typesafe/config/impl/TokenizerTest.scala index 96afa1664..79750bc83 100644 --- a/config/src/test/scala/com/typesafe/config/impl/TokenizerTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/TokenizerTest.scala @@ -198,12 +198,12 @@ class TokenizerTest extends TestUtils { """\"\""", // file ends with a backslash "$", // file ends with a $ "${" // file ends with a ${ - ) + ) for (t <- invalidTests) { val tokenized = tokenizeAsList(t) - val maybeProblem = tokenized.find(Tokens.isProblem(_)) - assertTrue(s"expected failure for <$t> but got ${t}", maybeProblem.isDefined) + val maybeProblem = tokenized.find(Tokens.isProblem) + assertTrue(s"expected failure for <$t> but got $t", maybeProblem.isDefined) } } @@ -247,9 +247,9 @@ class TokenizerTest extends TestUtils { abstract class NumberTest(val s: String, val result: Token) case class LongTest(override val s: String, override val result: Token) extends NumberTest(s, result) case class DoubleTest(override val s: String, override val result: Token) extends NumberTest(s, result) - implicit def pair2inttest(pair: (String, Int)) = LongTest(pair._1, tokenLong(pair._2)) - implicit def pair2longtest(pair: (String, Long)) = LongTest(pair._1, tokenLong(pair._2)) - implicit def pair2doubletest(pair: (String, Double)) = DoubleTest(pair._1, tokenDouble(pair._2)) + implicit def pair2inttest(pair: (String, Int)): LongTest = LongTest(pair._1, tokenLong(pair._2)) + implicit def pair2longtest(pair: (String, Long)): LongTest = LongTest(pair._1, tokenLong(pair._2)) + implicit def pair2doubletest(pair: (String, Double)): DoubleTest = DoubleTest(pair._1, tokenDouble(pair._2)) val tests = List[NumberTest](("1", 1), ("1.2", 1.2), @@ -302,7 +302,7 @@ class TokenizerTest extends TestUtils { for (invalid <- "+`^?!@*&\\") { val tokenized = tokenizeAsList(invalid.toString) assertEquals(3, tokenized.size) - assertEquals(Tokens.START, tokenized(0)) + assertEquals(Tokens.START, tokenized.head) assertEquals(Tokens.END, tokenized(2)) val problem = tokenized(1) assertTrue("reserved char is a problem", Tokens.isProblem(problem)) diff --git a/config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala b/config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala index 0bf39c2c0..70214b435 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ToyHttp.scala @@ -25,10 +25,7 @@ final class ToyHttp(handler: ToyHttp.Request => ToyHttp.Response) { private final val serverSocket = new ServerSocket() serverSocket.bind(new InetSocketAddress("127.0.0.1", 0)) final val port = serverSocket.getLocalPort - private final val thread = new Thread(new Runnable() { - override def run() = - mainLoop(); - }) + private final val thread = new Thread(() => mainLoop()) thread.setDaemon(true) thread.setName("ToyHttp") @@ -118,8 +115,8 @@ final class ToyHttp(handler: ToyHttp.Request => ToyHttp.Response) { //val stuff = new java.io.ByteArrayOutputStream //val writer = new PrintWriter(new OutputStreamWriter(stuff, StandardCharsets.UTF_8)) val writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)) - val dateFormat = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + val dateFormat = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US) + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")) writer.append(s"HTTP/1.1 ${response.code} ${codeText(response.code)}\r\n") writer.append(s"Date: ${dateFormat.format(new Date)}\r\n") diff --git a/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala index b4ee9a1ba..0a7f7e3e3 100644 --- a/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/UnitParserTest.scala @@ -3,6 +3,9 @@ */ package com.typesafe.config.impl +import java.time.{ LocalDate, Period } +import java.time.temporal.ChronoUnit + import org.junit.Assert._ import org.junit._ import com.typesafe.config._ @@ -40,7 +43,34 @@ class UnitParserTest extends TestUtils { assertTrue(e2.getMessage.contains("duration number")) } - // https://github.com/typesafehub/config/issues/117 + @Test + def parsePeriod(): Unit = { + val oneYears = List( + "1y", "1 y", "1year", "1 years", " 1y ", " 1 y ", + "365", "365d", "365 d", "365 days", " 365 days ", "365day", + "12m", "12mo", "12 m", " 12 mo ", "12 months", "12month") + val epochDate = LocalDate.ofEpochDay(0) + val oneYear = ChronoUnit.DAYS.between(epochDate, epochDate.plus(Period.ofYears(1))) + for (y <- oneYears) { + val period = SimpleConfig.parsePeriod(y, fakeOrigin(), "test") + val dayCount = ChronoUnit.DAYS.between(epochDate, epochDate.plus(period)) + assertEquals(oneYear, dayCount) + } + + // bad units + val e = intercept[ConfigException.BadValue] { + SimpleConfig.parsePeriod("100 dollars", fakeOrigin(), "test") + } + assertTrue(s"${e.getMessage} was not the expected error message", e.getMessage.contains("time unit")) + + // bad number + val e2 = intercept[ConfigException.BadValue] { + SimpleConfig.parsePeriod("1 00 seconds", fakeOrigin(), "test") + } + assertTrue(s"${e2.getMessage} was not the expected error message", e2.getMessage.contains("time unit 'seconds'")) + } + + // https://github.com/lightbend/config/issues/117 // this broke because "1d" is a valid double for parseDouble @Test def parseOneDayAsMilliseconds(): Unit = { @@ -127,7 +157,7 @@ class UnitParserTest extends TestUtils { @Test def parseHugeMemorySizes(): Unit = { def parseMem(s: String): Long = SimpleConfig.parseBytes(s, fakeOrigin(), "test") - def assertOutOfRange(s: String) = { + def assertOutOfRange(s: String): Unit = { val fail = intercept[ConfigException.BadValue] { parseMem(s) } diff --git a/config/src/test/scala/com/typesafe/config/impl/UtilTest.scala b/config/src/test/scala/com/typesafe/config/impl/UtilTest.scala index cf31a7166..6d0bb160c 100644 --- a/config/src/test/scala/com/typesafe/config/impl/UtilTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/UtilTest.scala @@ -3,6 +3,7 @@ */ package com.typesafe.config.impl +import com.typesafe.config.ConfigSyntax import org.junit.Assert._ import org.junit._ @@ -57,7 +58,7 @@ class UtilTest extends TestUtils { assertTrue(ConfigImplUtil.equalsHandlingNull("", "")) } - val lotsOfStrings = (invalidJson ++ validConf).map(_.test) + val lotsOfStrings: List[String] = (invalidJson ++ validConf).map(_.test) private def roundtripJson(s: String) { val rendered = ConfigImplUtil.renderJsonString(s) @@ -90,4 +91,29 @@ class UtilTest extends TestUtils { roundtripUnquoted(s) } } + + @Test + def syntaxFromExtensionConf(): Unit = { + assertEquals(ConfigSyntax.CONF, ConfigImplUtil.syntaxFromExtension("application.conf")) + } + + @Test + def syntaxFromExtensionJson(): Unit = { + assertEquals(ConfigSyntax.JSON, ConfigImplUtil.syntaxFromExtension("application.json")) + } + + @Test + def syntaxFromExtensionProperties(): Unit = { + assertEquals(ConfigSyntax.PROPERTIES, ConfigImplUtil.syntaxFromExtension("application.properties")) + } + + @Test + def syntaxFromExtensionUnknown(): Unit = { + assertNull(ConfigImplUtil.syntaxFromExtension("application.exe")) + } + + @Test + def syntaxFromExtensionNull(): Unit = { + assertNull(ConfigImplUtil.syntaxFromExtension(null)) + } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ValidationTest.scala b/config/src/test/scala/com/typesafe/config/impl/ValidationTest.scala index 6dce7bfee..5e45aad61 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ValidationTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ValidationTest.scala @@ -97,6 +97,21 @@ class ValidationTest extends TestUtils { checkValidationException(e, expecteds) } + @Test + def validationFailedSerializable(): Unit = { + // Reusing a previous test case to generate an error + val reference = parseConfig("""{ a : [{},{},{}] }""") + val conf = parseConfig("""{ a : 42 }""") + val e = intercept[ConfigException.ValidationFailed] { + conf.checkValid(reference) + } + + val expecteds = Seq(WrongType("a", 1, "list", "number")) + + val actual = checkSerializableNoMeaningfulEquals(e) + checkValidationException(actual, expecteds) + } + @Test def validationAllowsListOverriddenWithSameTypeList() { val reference = parseConfig("""{ a : [1,2,3] }""") diff --git a/examples/scala/complex-app/src/main/scala/ComplexApp.scala b/examples/scala/complex-app/src/main/scala/ComplexApp.scala index 507da8cb1..68d530303 100644 --- a/examples/scala/complex-app/src/main/scala/ComplexApp.scala +++ b/examples/scala/complex-app/src/main/scala/ComplexApp.scala @@ -35,7 +35,7 @@ object ComplexApp extends App { ////////// // "config2" shows how to configure a library with a custom settings subtree - val config2 = ConfigFactory.load("complex2"); + val config2 = ConfigFactory.load("complex2") // use the config ourselves println("config2, complex-app.something=" + config2.getString("complex-app.something")) diff --git a/examples/scala/simple-lib/src/main/scala/simplelib/SimpleLib.scala b/examples/scala/simple-lib/src/main/scala/simplelib/SimpleLib.scala index 06b03c0a5..5c02c0920 100644 --- a/examples/scala/simple-lib/src/main/scala/simplelib/SimpleLib.scala +++ b/examples/scala/simple-lib/src/main/scala/simplelib/SimpleLib.scala @@ -40,9 +40,9 @@ class SimpleLibSettings(config: Config) { // note that these fields are NOT lazy, because if we're going to // get any exceptions, we want to get them on startup. - val foo = config.getString("simple-lib.foo") - val hello = config.getString("simple-lib.hello") - val whatever = config.getString("simple-lib.whatever") + val foo: String = config.getString("simple-lib.foo") + val hello: String = config.getString("simple-lib.hello") + val whatever: String = config.getString("simple-lib.whatever") } // This is a different way to do SimpleLibContext, using the diff --git a/project/Build.scala b/project/Build.scala deleted file mode 100644 index 00d33767c..000000000 --- a/project/Build.scala +++ /dev/null @@ -1,119 +0,0 @@ -import sbt._ -import Keys._ -import com.etsy.sbt.checkstyle.CheckstylePlugin.autoImport._ -import com.typesafe.sbt.osgi.SbtOsgi -import com.typesafe.sbt.osgi.SbtOsgi.autoImport._ -import com.typesafe.sbt.JavaVersionCheckPlugin.autoImport._ - -object ConfigBuild extends Build { - val unpublished = Seq( - // no artifacts in this project - publishArtifact := false, - // make-pom has a more specific publishArtifact setting already - // so needs specific override - publishArtifact in makePom := false, - // no docs to publish - publishArtifact in packageDoc := false, - // can't seem to get rid of ivy files except by no-op'ing the entire publish task - publish := {}, - publishLocal := {} - ) - - object sonatype extends PublishToSonatype { - def projectUrl = "https://github.com/typesafehub/config" - def developerId = "havocp" - def developerName = "Havoc Pennington" - def developerUrl = "http://ometer.com/" - def scmUrl = "git://github.com/typesafehub/config.git" - } - - override val settings = super.settings ++ Seq(isSnapshot <<= isSnapshot or version(_ endsWith "-SNAPSHOT")) - - lazy val commonSettings: Seq[Setting[_]] = unpublished ++ Seq(javaVersionPrefix in javaVersionCheck := None) - - lazy val rootSettings: Seq[Setting[_]] = - commonSettings ++ - Seq(aggregate in doc := false, - doc := (doc in (configLib, Compile)).value, - aggregate in packageDoc := false, - packageDoc := (packageDoc in (configLib, Compile)).value, - aggregate in checkstyle := false, - checkstyle := (checkstyle in (configLib, Compile)).value) - - lazy val root = Project(id = "root", - base = file("."), - settings = rootSettings) aggregate(testLib, configLib, - simpleLibScala, simpleAppScala, complexAppScala, - simpleLibJava, simpleAppJava, complexAppJava) - - lazy val configLib = Project(id = "config", - base = file("config"), - settings = - sonatype.settings ++ - osgiSettings ++ - Seq( - OsgiKeys.exportPackage := Seq("com.typesafe.config", "com.typesafe.config.impl"), - publish := sys.error("use publishSigned instead of plain publish"), - publishLocal := sys.error("use publishLocalSigned instead of plain publishLocal") - )).enablePlugins(SbtOsgi) dependsOn testLib % "test->test" - - def project(id: String, base: File) = Project(id, base, settings = commonSettings) - - lazy val testLib = project("config-test-lib", file("test-lib")) - - lazy val simpleLibScala = project("config-simple-lib-scala", file("examples/scala/simple-lib")) dependsOn configLib - lazy val simpleAppScala = project("config-simple-app-scala", file("examples/scala/simple-app")) dependsOn simpleLibScala - lazy val complexAppScala = project("config-complex-app-scala", file("examples/scala/complex-app")) dependsOn simpleLibScala - - lazy val simpleLibJava = project("config-simple-lib-java", file("examples/java/simple-lib")) dependsOn configLib - lazy val simpleAppJava = project("config-simple-app-java", file("examples/java/simple-app")) dependsOn simpleLibJava - lazy val complexAppJava = project("config-complex-app-java", file("examples/java/complex-app")) dependsOn simpleLibJava -} - -// from https://raw.github.com/paulp/scala-improving/master/project/PublishToSonatype.scala - -abstract class PublishToSonatype { - val ossSnapshots = "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" - val ossStaging = "Sonatype OSS Staging" at "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - - def projectUrl: String - def developerId: String - def developerName: String - def developerUrl: String - - def licenseName = "Apache License, Version 2.0" - def licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0" - def licenseDistribution = "repo" - def scmUrl: String - def scmConnection = "scm:git:" + scmUrl - - def generatePomExtra: xml.NodeSeq = { - { projectUrl } - - - { licenseName } - { licenseUrl } - { licenseDistribution } - - - - { scmUrl } - { scmConnection } - - - - { developerId } - { developerName } - { developerUrl } - - - } - - def settings: Seq[Setting[_]] = Seq( - publishMavenStyle := true, - publishTo <<= isSnapshot { (snapshot) => Some(if (snapshot) ossSnapshots else ossStaging) }, - publishArtifact in Test := false, - pomIncludeRepository := (_ => false), - pomExtra := generatePomExtra - ) -} diff --git a/project/build.properties b/project/build.properties index 43b8278c6..c0bab0494 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.11 +sbt.version=1.2.8 diff --git a/project/linksource.scala b/project/linksource.scala index 01c3226de..7e98040fb 100644 --- a/project/linksource.scala +++ b/project/linksource.scala @@ -1,4 +1,4 @@ -import sbt._ +import sbt.{Def, _} import Keys._ import plugins.JvmPlugin @@ -12,7 +12,7 @@ object LinkSourcePlugin extends AutoPlugin { override def trigger = allRequirements override def requires = JvmPlugin - override lazy val projectSettings = Seq( + override lazy val projectSettings: Seq[Def.Setting[_ >: Option[String] with Task[Seq[String]] with Task[File] <: Product]] = Seq( javadocSourceBaseUrl := None, javacOptions in (Compile, doc) := { val old = (javacOptions in doc).value @@ -26,18 +26,19 @@ object LinkSourcePlugin extends AutoPlugin { val dir = (target in doc in Compile).value - javadocSourceBaseUrl.value.foreach { url => - rewriteSourceLinks(dir, url, streams.value.log) + (javadocSourceBaseUrl.value, streams.value) match { + case (Some(url), streamz) => + rewriteSourceLinks(dir, url, streamz.log) + case _ => } result } ) - private def rewriteSourceLinks(dir: File, sourceBaseUrl: String, log: Logger): Unit = { // Convert to - // "https://github.com/typesafehub/config/blob/v1.2.1/config/src/main/java/com/typesafe/config/Config.java#L165" + // "https://github.com/lightbend/config/blob/v1.2.1/config/src/main/java/com/typesafe/config/Config.java#L165" // in all .html files found underneath dir val origRegex = "href=\".*src-html/([^\"]+)\"".r def listFiles(d: File): Seq[File] = IO.listFiles(d).toSeq.flatMap { f => diff --git a/project/plugins.sbt b/project/plugins.sbt index de21c2d70..38196f0ac 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,10 +1,10 @@ -addSbtPlugin("de.johoop" % "findbugs4sbt" % "1.4.0") -addSbtPlugin("de.johoop" % "jacoco4sbt" % "2.1.6") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-osgi" % "0.8.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.1") +addSbtPlugin("com.github.sbt" % "sbt-findbugs" % "2.0.0") +addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.1.0") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0-M2") +addSbtPlugin("com.typesafe.sbt" % "sbt-osgi" % "0.9.3") +addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2") -addSbtPlugin("com.etsy" % "sbt-checkstyle-plugin" % "3.0.0") +addSbtPlugin("com.etsy" % "sbt-checkstyle-plugin" % "3.1.1") -addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.5") -addSbtPlugin("com.typesafe.sbt" % "sbt-javaversioncheck" % "0.1.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") +addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.0")