diff --git a/.gitignore b/.gitignore index f747703..89cb78a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,8 @@ -.idea/libraries/ -.idea/modules.xml -.idea/.name -.idea/compiler.xml -.idea/workspace.xml +/.idea *.iml .kobalt/ .gradle/ gradle.properties kobaltw build/ -out/ \ No newline at end of file +out/ diff --git a/.travis.yml b/.travis.yml index 2a44aa8..84f3879 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,8 @@ script: ./b clean build sudo: false jdk: - - oraclejdk8 + - oraclejdk11 + - oraclejdk14 cache: directories: diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 31dceb7..0000000 --- a/build.gradle +++ /dev/null @@ -1,144 +0,0 @@ -group 'com.natpryce' -version(hasProperty("-version") ? property("-version") : "SNAPSHOT") - -println "building version ${version}" - - -buildscript { - ext.kotlin_version = '1.2.41' - repositories { - mavenCentral() - jcenter() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -apply plugin: 'kotlin' -apply plugin: 'maven' -apply plugin: 'signing' - -repositories { - mavenCentral() -} - -dependencies { - compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - compileOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testCompile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - testCompile 'junit:junit:4.12' - testCompile 'com.natpryce:hamkrest:1.4.0.0' -} - -jar { - manifest { - attributes 'Implementation-Title': 'konfig', - 'Implementation-Vendor': 'com.natpryce', - 'Implementation-Version': version - } -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource -} - -artifacts { - archives sourcesJar -} - - -task javadocJar(type: Jar) { - classifier 'javadoc' - from 'build/javadoc' -} - -test { - include 'com/natpryce/konfig/**' - scanForTestClasses true - reports { - junitXml.enabled = true - html.enabled = true - } - - beforeTest { desc -> - print "${desc.className.substring("com.natpryce.konfig.".length())}: ${desc.name.replace("_", " ")}" - } - afterTest { desc, result -> - println " -> ${result.resultType}" - } -} - -artifacts { - archives sourcesJar, javadocJar -} - -signing { - required { hasProperty("sign") || gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives -} - -task ossrhAuthentication << { - if (!(project.hasProperty('ossrh.username') && project.hasProperty('ossrh.password'))) { - throw new InvalidUserDataException("no OSSRH username and/or password!") - } -} - -uploadArchives { - dependsOn ossrhAuthentication - - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: project.properties["ossrh.username"], - password: project.properties["ossrh.password"]) - } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: project.properties["ossrh.username"], - password: project.properties["ossrh.password"]) - } - - pom.project { - name 'Konfig' - packaging 'jar' - description 'Konfiguration for Cotlin... no, Configuration for Kotlin' - url 'https://github.com/npryce/konfig' - - scm { - connection 'git@github.com:npryce/konfig.git' - url 'https://github.com/npryce/konfig' - } - - licenses { - license { - name 'Apache 2.0' - url 'http://opensource.org/licenses/Apache-2.0' - } - } - - developers { - developer { - id 'npryce' - name 'Nat Pryce' - } - developer { - id 'dmcg' - name 'Duncan McGregor' - } - } - } - } - } -} - -task wrapper(type: Wrapper) { - gradleVersion = '4.7' - distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" -} - diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7cd2409 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,70 @@ +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + kotlin("jvm") version "1.3.72" + maven + signing +} + +group = "com.natpryce" +version = findProperty("-version") ?: "SNAPSHOT" + +println("building version $version") + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + + testImplementation(kotlin("test")) + testImplementation("junit:junit:4.12") + testImplementation("com.natpryce:hamkrest:1.4.0.0") +} + +java { + withSourcesJar() +} + +tasks { + jar { + manifest { + attributes( + mapOf( + "Implementation-Title" to "konfig", + "Implementation-Vendor" to "com.natpryce", + "Implementation-Version" to project.version.toString() + ) + ) + } + } + + test { + useJUnit() + + testLogging { + events(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED) + } + } + + register("ossrhAuthentication") { + doLast { + if (!(project.hasProperty("ossrh.username") && project.hasProperty("ossrh.password"))) { + throw InvalidUserDataException("no OSSRH username and/or password!") + } + } + } +} + +artifacts { + add("archives", tasks["sourcesJar"]) +} + +signing { + setRequired({ hasProperty("sign") || gradle.taskGraph.hasTask("uploadArchives") }) + listOf("jar", "sourcesJar").forEach { t -> sign(tasks[t]) } +} + +apply(from = "publishing.gradle") diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c..490fda8 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6a3091..a4b4429 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..2fe81a7 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -138,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..9109989 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,103 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/publishing.gradle b/publishing.gradle new file mode 100644 index 0000000..4285ecb --- /dev/null +++ b/publishing.gradle @@ -0,0 +1,50 @@ +// The legacy `maven` plugin is a pain to use from Kotlin, so leaving it as-is in groovy for the moment + +uploadArchives { + dependsOn(tasks.ossrhAuthentication) + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: project.properties["ossrh.username"], + password: project.properties["ossrh.password"]) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: project.properties["ossrh.username"], + password: project.properties["ossrh.password"]) + } + + pom.project { + name "Konfig" + packaging "jar" + description "Konfiguration for Cotlin... no, Configuration for Kotlin" + url "https://github.com/npryce/konfig" + + scm { + connection "git@github.com:npryce/konfig.git" + url "https://github.com/npryce/konfig" + } + + licenses { + license { + name "Apache 2.0" + url "http://opensource.org/licenses/Apache-2.0" + } + } + + developers { + developer { + id "npryce" + name "Nat Pryce" + } + developer { + id "dmcg" + name "Duncan McGregor" + } + } + } + } + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 8d30ee2..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'konfig' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a494eb3 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "konfig" + diff --git a/src/main/kotlin/com/natpryce/konfig/cli.kt b/src/main/kotlin/com/natpryce/konfig/cli.kt index 27d8bf1..74809b3 100644 --- a/src/main/kotlin/com/natpryce/konfig/cli.kt +++ b/src/main/kotlin/com/natpryce/konfig/cli.kt @@ -18,7 +18,7 @@ data class CommandLineOption( if (long.startsWith("-")) throw IllegalArgumentException("long flag must not be specified with leading '-'") if (short != null && short.startsWith("-")) throw IllegalArgumentException("short flag must not be specified with leading '-'") } - + val longFlag = "--$long" val shortFlag = short?.let { "-$it" } } @@ -31,33 +31,33 @@ private class CommandLineConfiguration( private val optionsUsed: Map, CommandLineProperty> ) : Configuration { - + private val optionsByKey = allOptions.associateBy { it.configKey } internal val location: Location = Location("command-line parameters") - + override fun getOrNull(key: Key) = optionsUsed[key]?.let { key.parse(PropertyLocation(key, location, it.flagUsed), it.value) } - + override fun searchPath(key: Key<*>): List { val opt = optionsByKey[key] - + val result = ArrayList() - + if (opt != null) { if (opt.shortFlag != null) { result.add(PropertyLocation(key, location, opt.shortFlag)) } result.add(PropertyLocation(key, location, opt.longFlag)) } - + return result } - + override fun locationOf(key: Key<*>): PropertyLocation? { return optionsUsed[key]?.let { PropertyLocation(key, location, it.flagUsed) } } - + override fun list(): List>> { return listOf(location to optionsUsed.values.associateBy({ it.flagUsed }, { it.value })) } @@ -75,26 +75,26 @@ fun parseArgs(args: Array, PrintWriter(helpOutput).printHelp(programName, argMetavar, options) helpExit() } - + val files = ArrayList() val properties = HashMap, CommandLineProperty>() val shortOpts: Map = options.filter { it.short != null }.associateBy({ "-${it.short!!}" }, { it }) val longOpts: Map = options.associateBy({ "--${it.long}" }, { it }) - - var i = 0; + + var i = 0 while (i < args.size) { val arg = args[i] - + fun Map.configNameFor(opt: String) = this[opt]?.configKey ?: throw Misconfiguration("unrecognised command-line option $arg") - + fun storeNextArg(configNameByOpt: Map, opt: String) { i++ if (i >= args.size) throw Misconfiguration("no argument for $arg command-line option") - + properties[configNameByOpt.configNameFor(opt)] = CommandLineProperty(arg, args[i]) } - + when { arg.startsWith("--") -> { if (arg.contains('=')) { @@ -113,22 +113,22 @@ fun parseArgs(args: Array, files.add(arg) } } - + i++ } - + return Pair(CommandLineConfiguration(options.asList(), properties), files) } fun PrintWriter.printHelp(programName: String, argMetavar: String, options: Array) { val helpOptionLine = "-h, --help" to "show this help message and exit" - + val helpLines = options.map { (it.short?.let { s -> "-$s ${it.metavar}, " } ?: "") + "--${it.long}=${it.metavar}" to it.description } + helpOptionLine - + val optHelpLength = helpLines.map { it.first.length }.max()!! - + println("Usage: $programName [options] $argMetavar ...") println() println("Options:") diff --git a/src/main/kotlin/com/natpryce/konfig/konfig.kt b/src/main/kotlin/com/natpryce/konfig/konfig.kt index bf08df3..4ee1430 100644 --- a/src/main/kotlin/com/natpryce/konfig/konfig.kt +++ b/src/main/kotlin/com/natpryce/konfig/konfig.kt @@ -3,9 +3,11 @@ package com.natpryce.konfig import java.io.File -import java.io.InputStream +import java.io.Reader import java.net.URI import java.net.URL +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.util.Properties /** @@ -134,7 +136,7 @@ fun Configuration.missingPropertyMessage(key: Key<*>) = "${key.name} property not found; searched:\n${searchPath(key).description}" val List.description: String - get() = map { " - ${it.description}" }.joinToString(separator = "\n", postfix = "\n") + get() = joinToString(separator = "\n", postfix = "\n") { " - ${it.description}" } abstract class LocatedConfiguration : Configuration { @@ -183,37 +185,38 @@ class ConfigurationProperties( fun systemProperties() = ConfigurationProperties(System.getProperties(), Location("system properties")) /** - * Load from resources relative to a class + * Load from resources relative to a class using ISO-8859-1 by default. */ - fun fromResource(relativeToClass: Class<*>, resourceName: String) = - loadFromResource(resourceName, relativeToClass.getResource(resourceName)) - + fun fromResource(relativeToClass: Class<*>, resourceName: String, charset: Charset = StandardCharsets.ISO_8859_1) = + loadFromResource(resourceName, relativeToClass.getResource(resourceName), charset) + /** - * Load from resource within the system classloader. + * Load from resource within the system classloader using ISO-8859-1 by default. */ - fun fromResource(resourceName: String): ConfigurationProperties { + fun fromResource(resourceName: String, charset: Charset = StandardCharsets.ISO_8859_1): ConfigurationProperties { val classLoader = ClassLoader.getSystemClassLoader() - return loadFromResource(resourceName, classLoader.getResource(resourceName)) + return loadFromResource(resourceName, classLoader.getResource(resourceName), charset) } - - private fun loadFromResource(resourceName: String, resourceUrl: URL?): ConfigurationProperties { - return load(resourceUrl?.openStream(), Location("resource $resourceName", resourceUrl?.toURI())) { + + private fun loadFromResource(resourceName: String, resourceUrl: URL?, charset: Charset): ConfigurationProperties { + return load(resourceUrl?.openStream()?.reader(charset), Location("resource $resourceName", resourceUrl?.toURI())) { "resource $resourceName not found" } } /** - * Load from file + * Load from file using ISO-8859-1 by default. */ - fun fromFile(file: File) = load(if (file.exists()) file.inputStream() else null, Location(file.absolutePath, file.toURI())) { - "file $file does not exist" - } - - private fun load(input: InputStream?, location: Location, errorMessageFn: () -> String) = - (input ?: throw Misconfiguration(errorMessageFn())).use { - ConfigurationProperties(Properties().apply { load(input) }, location) + fun fromFile(file: File, charset: Charset = StandardCharsets.ISO_8859_1) = + load(if (file.exists()) file.reader(charset) else null, Location(file)) { + "file $file does not exist" } - + + private fun load(reader: Reader?, location: Location, errorMessageFn: () -> String) = + (reader ?: throw Misconfiguration(errorMessageFn())).use { + ConfigurationProperties(Properties().apply { load(reader) }, location) + } + /** * Load from optional file */ @@ -354,11 +357,11 @@ class Subset( override fun searchPath(key: Key<*>) = configuration.searchPath(full(key)) override fun locationOf(key: Key<*>) = if (keyIsInSubset(key.name)) configuration.locationOf(key) else null - + override fun list() = configuration.list().map { it.first to it.second.filterKeys(this::keyIsInSubset) } - + private fun keyIsInSubset(k: String) = (prefix.isEmpty() || k.startsWith(prefix)) && (suffix.isEmpty() || k.endsWith(suffix)) - + private fun full(key: Key) = key.copy(name = prefix + key.name + suffix) } diff --git a/src/main/kotlin/com/natpryce/konfig/magic.kt b/src/main/kotlin/com/natpryce/konfig/magic.kt index 5b67c58..ffe9dd3 100644 --- a/src/main/kotlin/com/natpryce/konfig/magic.kt +++ b/src/main/kotlin/com/natpryce/konfig/magic.kt @@ -14,10 +14,15 @@ open class PropertyKeys : Iterable> { open class PropertyGroup(private val outer: PropertyGroup? = null) : PropertyKeys() { private fun outer() = outer ?: javaClass.enclosingClass?.kotlin?.objectInstance as? PropertyGroup - private fun name() : String = namePrefix() + groupName() - private fun namePrefix() = outer()?.name()?.let { it + "." } ?: "" - private fun groupName() = javaClass.kotlin.simpleName?.substringBefore("$") ?: - throw IllegalArgumentException("cannot determine name of property group") + private fun name(): String = namePrefix() + groupName() + private fun namePrefix() = outer()?.name()?.let { "$it." } ?: "" + + // anonymous objects don't get names in their KClass, so we have to resort to the JVM's name + private fun groupName() = javaClass.kotlin.simpleName?.substringBefore("$") + ?: javaClass.name.split("$").let { chunks -> + chunks.getOrNull(chunks.size - 2) + ?: throw IllegalArgumentException("cannot determine name of property group for $javaClass") + } fun key(keySimpleName: String, type: (PropertyLocation, String) -> T): Key { return Key((name() + "." + keySimpleName).replace('_', '-'), type) diff --git a/src/main/kotlin/com/natpryce/konfig/property_types.kt b/src/main/kotlin/com/natpryce/konfig/property_types.kt index 16ed240..c120fee 100644 --- a/src/main/kotlin/com/natpryce/konfig/property_types.kt +++ b/src/main/kotlin/com/natpryce/konfig/property_types.kt @@ -24,8 +24,7 @@ typealias PropertyType = (PropertyLocation, String) -> T fun propertyType(typeName: String, parse: (String) -> ParseResult): PropertyType { return { location, stringValue -> - val parsed = parse(stringValue) - when (parsed) { + when (val parsed = parse(stringValue)) { is ParseResult.Success -> parsed.value is ParseResult.Failure -> { @@ -104,16 +103,18 @@ inline fun enumType(allowed: Map) = enumType(T::cla fun enumType(enumType: Class, allowed: Map) = propertyType(enumType) { str -> allowed[str] ?.let { ParseResult.Success(it) } - ?: ParseResult.Failure(IllegalArgumentException("invalid value: $str; must be one of: ${allowed.keys}")) + ?: ParseResult.Failure(IllegalArgumentException("invalid value: $str; must be one of: ${allowed.keys}")) } -fun > enumType(enumClass: Class, allowed: Iterable) = enumType(enumClass, allowed.associate { it.name to it }) +fun > enumType(enumClass: Class, allowed: Iterable) = enumType(enumClass, + allowed.associateBy { it.name }) -inline fun > enumType(allowed: Iterable) = enumType(T::class.java, allowed.associate { it.name to it }) +inline fun > enumType(allowed: Iterable) = enumType(T::class.java, + allowed.associateBy { it.name }) inline fun enumType(vararg allowed: Pair) = enumType(mapOf(*allowed)) inline fun > enumType(vararg allowed: T) = enumType(listOf(*allowed)) -fun > enumType(enumClass: java.lang.Class) = enumType(enumClass, EnumSet.allOf(enumClass)) +fun > enumType(enumClass: Class) = enumType(enumClass, EnumSet.allOf(enumClass)) inline fun > enumType() = enumType(T::class.java) /** diff --git a/src/test/kotlin/com/natpryce/unittests/konfig/konfig_tests.kt b/src/test/kotlin/com/natpryce/unittests/konfig/konfig_tests.kt index 8225c32..4ba3125 100644 --- a/src/test/kotlin/com/natpryce/unittests/konfig/konfig_tests.kt +++ b/src/test/kotlin/com/natpryce/unittests/konfig/konfig_tests.kt @@ -23,6 +23,7 @@ import com.natpryce.konfig.stringType import com.natpryce.konfig.wrappedAs import org.junit.Test import java.io.File +import java.nio.charset.StandardCharsets import java.util.Properties import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -363,7 +364,15 @@ class FromResources { assertThat(config[a], equalTo(1)) assertThat(config[b], equalTo("two")) } - + + @Test + fun can_load_utf8_from_resource() { + val config = ConfigurationProperties.fromResource(javaClass, "utf8.properties", StandardCharsets.UTF_8) + + assertThat(config[Key("snowman", stringType)], equalTo("\u2603")) + assertThat(config[Key("mahjong", stringType)], equalTo("\uD83C\uDC06")) + } + @Test fun can_load_from_absolute_resource() { val config = ConfigurationProperties.fromResource("com/natpryce/unittests/konfig/example.properties") @@ -383,17 +392,28 @@ class FromResources { @Test fun can_load_from_optional_file() { val config = ConfigurationProperties.fromOptionalFile(File("src/test/resources/com/natpryce/unittests/konfig/example.properties")) - + assertThat(config[a], equalTo(1)) assertThat(config[b], equalTo("two")) } - + @Test fun should_return_empty_config_when_load_from_optional_file_thats_not_present() { val config = ConfigurationProperties.fromOptionalFile(File("not.available.file")) - + assertThat(config, equalTo(EmptyConfiguration)) } + + @Test + fun can_load_utf8_from_file() { + val config = ConfigurationProperties.fromFile( + File("src/test/resources/com/natpryce/unittests/konfig/utf8.properties"), + StandardCharsets.UTF_8 + ) + + assertThat(config[Key("snowman", stringType)], equalTo("\u2603")) + assertThat(config[Key("mahjong", stringType)], equalTo("\uD83C\uDC06")) + } } class Wrapping { @@ -421,4 +441,4 @@ class IntrinsicLocation { assertThat("ignores calls to Konfig functions", l.description, !startsWith("intrinsic: com.natpryce.konfig.")) } -} \ No newline at end of file +} diff --git a/src/test/resources/com/natpryce/unittests/konfig/utf8.properties b/src/test/resources/com/natpryce/unittests/konfig/utf8.properties new file mode 100644 index 0000000..410b74e --- /dev/null +++ b/src/test/resources/com/natpryce/unittests/konfig/utf8.properties @@ -0,0 +1,2 @@ +snowman = ☃ +mahjong = 🀆