How to read a text file from resources in Kotlin?
Asked Answered
Y

15

190

I want to write a Spek test in Kotlin.
How to read an HTML file from the src/test/resources folder?

class MySpec : Spek(
    {
        describe("blah blah") {
            given("blah blah") {
                var fileContent: String = ""
                beforeEachTest {
                    // How to read the file.html in src/test/resources/html/
                    fileContent = ...
                }
                it("should blah blah") {
                    ...
                }
            }
        }
    }
)
Yusuk answered 11/3, 2017 at 19:45 Comment(0)
A
211
val fileContent = MySpec::class.java.getResource("/html/file.html").readText()
Acetylene answered 11/3, 2017 at 20:44 Comment(7)
For me this didn't work, I had to change it to this::class.java.classLoader.getResource("/html/file.html").readText()Grouch
For me both these options worked in an Android app (notice the extra / in one of them, which has to be removed in the other): this::class.java.getResource("/html/file.html").readText() and this::class.java.classLoader.getResource("html/file.html").‌​readText()Barbaraanne
val fileContent = javaClass.getResource("/html/file.html").readText() does the job even shorterSordid
Oddly enough I always need a leading slash. E.g if the file is in the resources root directory, you still have to refer to it as "/file.html"Gati
I tried every solution, does anyone tested this? only is a copy paste of others solutionsJared
how to read for IOS in KMMGuardado
I couldn't get this to work until I read here the difference between getResource and classLoader.getResource : https://mcmap.net/q/82431/-what-is-the-difference-between-class-getresource-and-classloader-getresource @SomaiahKumbera leading slash makes the path absolute, see linked post.Aberdare
S
85

No idea why this is so hard, but the simplest way I've found (without having to refer to a particular class) is:

fun getResourceAsText(path: String): String? =
    object {}.javaClass.getResource(path)?.readText()

It returns null if no resource with this name is found (as documented).

And then passing in an absolute URL, e.g.

val html = getResourceAsText("/www/index.html")!!
Seabolt answered 27/10, 2018 at 2:2 Comment(11)
is {} required? Why not just javaClass.getResource(path).readText()?Autolycus
javaClass must be called on an object according to the docs kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/java-class.html If it works without then go for ya life :)Seabolt
One downside with the method above is it creates a new object for every resource access. Would be better to store the dummy object outside the function.Seabolt
@RussellBriggs tbh I don't think that matters much. The performance of an object creation is not really an issue if you do disk access!Fling
One snag here is that javaClass is apparently nullable?Shellans
@Shellans I don't think that's the case, but Class.getResource does indeed return null if no such resource could be found. I've edited the answer to expose that.Bernardina
@Bernardina I had Kotlin refuse to compile my code unless I !! asserted that the value returned from javaClass itself wasn't null. So it very much does seem to be the case, and perhaps has something to do with Kotlin supporting running on more than the JVM?Shellans
@Shellans I've checked the docs for javaClass and the result doesn't seem to be nullable. Also, see this snippet on Kotlin Playground. It compiles correctly. So I'm out of ideas what could make javaClass nullable in your case.Bernardina
Relating to "supporting running on more than the JVM", I'm not really sure what that means.Bernardina
@Bernardina Compose can also run on JavaScript if you're also targeting the web. But yes, when I checked the docs, I saw the same thing. Like I said, it was the compiler which rejected me. I guess it isn't such a hard thing to work around anyway. I already have to deal with the resource itself being null too, so I can just use ?. to dodge it.Shellans
Great answer, short and sweet!Collins
R
32

another slightly different solution:

@Test
fun basicTest() {
    "/html/file.html".asResource {
        // test on `it` here...
        println(it)
    }

}

fun String.asResource(work: (String) -> Unit) {
    val content = this.javaClass::class.java.getResource(this).readText()
    work(content)
}
Rella answered 30/7, 2017 at 18:22 Comment(9)
Nice usage of an extension function! But why do you use a lambda function here? That does not make much sense to me. Furthermore, the this part did not work for me. Thus I recommend the following: fun String.asResource(): URL? = object {}.javaClass.getResource(this)Fling
I like this method of working on it where you declare the file, then work on the contents of that file. Personal preference I guess. this in the example above refers to the string object.Rella
that's terrible abuse of extension function. Loading files is not a concern of String class.Kumasi
it is in this context. i wouldn't make this globally available or use it outside of test classes. I consider it more of a mapping function here.Rella
@Kumasi Use of extension functions and extension properties is by definition an abuse, since it pretends to add members to a class it doesn't have internal access to. If you don't like it, don't use it. Some of us are focused on making our code more straightforward rather than worrying about OO purity.Disjunction
@RyanLundy no it's not, extension functions are perfectly ok in OOP. The whole idea of extension function is that it does not break encapsulation and remains a pure function, T.x() is just a normalized version of x(T). The problem with your code snippet is not that it uses extension function, but that it breaks domain boundariesKumasi
@Kumasi how though? is this an impure function? i could write asResource(filename) just as well.Rella
@Rella that's not the point. I'm not implying that your function is impure, I'm implying that it breaks separation of concern. It's not cohesive for the String to handle classloader. fun asResource(path:String, work: (String) -> Unit) is the proper signature. //// Extension functions are ok in OOP, in fact, they are way better than the hack that "default" keyword is, however they often confuse developers as you can seemingly implement methods where they don't belong. Great feature requires great careKumasi
When I read code like "/html/file.html".asResource I wonder whether the resource contains the literal content "/html/file.html". That's the real problem with putting this on string. It might have been less jarring if the extension function were on Path. But I agree that it would be even better to just have a global function like loadResource(String)Shellans
Y
19

A slightly different solution:

class MySpec : Spek({
    describe("blah blah") {
        given("blah blah") {

            var fileContent = ""

            beforeEachTest {
                html = this.javaClass.getResource("/html/file.html").readText()
            }

            it("should blah blah") {
                ...
            }
        }
    }
})
Yusuk answered 11/3, 2017 at 21:58 Comment(4)
For some reason this didn't work for me. Only explicitly calling the class worked. Just adding for others. I think it has something to do with tornadofxCoition
After creating a test input file in /src/test/resources, this.javaClass.getResource("/<test input filename>") worked as expected. Thanks for the solution above.Fantast
what to do, if fileContent is not String and I won't create any dummy object?Sanborn
leading slash before the path seems mandatory here, whereas in Java I usually omit it.Glomerulonephritis
I
15

Kotlin + Spring way:

@Autowired
private lateinit var resourceLoader: ResourceLoader

fun load() {
    val html = resourceLoader.getResource("classpath:html/file.html").file
        .readText(charset = Charsets.UTF_8)
}
Isotonic answered 29/3, 2019 at 20:4 Comment(4)
For me this caused problems later. Whilst i dockerised my jar there was problem with the files missing ..Disillusion
@kris what part of this code is causing problems?and how did you solve it?Isotonic
with root cause java.io.FileNotFoundException: class path resource [email/next_interaction_deadline.txt] cannot be resolved to absolute file path because it does not reside in the file system. Fix was just to use the first response.Disillusion
"resourceLoader.getResource().file" is what causes FileNotFoundException. There's no File, There is an inputStream instead. So this is the correct way is: "resourceLoader.getResource().inputStream.bufferedReader().use { it.readText() }"Regality
P
12

Using Google Guava library Resources class:

import com.google.common.io.Resources;

val fileContent: String = Resources.getResource("/html/file.html").readText()
Prefiguration answered 22/5, 2019 at 22:54 Comment(1)
it is nice that Guave reports a file name if resource is not found - much better for troubleshootingKai
M
11
val fileContent = javaClass.getResource("/html/file.html").readText()
Marnie answered 12/3, 2019 at 6:5 Comment(0)
S
8

This is the way that I prefer to do it:

fun getResourceText(path: String): String {
    return File(ClassLoader.getSystemResource(path).file).readText()
}
Shinleaf answered 10/8, 2020 at 4:10 Comment(1)
This may work for some people, but generally the resource may not be loaded by the system class loader.Shellans
Y
7
private fun loadResource(file: String) = {}::class.java.getResource(file).readText()
Yusuk answered 21/4, 2020 at 14:24 Comment(0)
M
5

this top-level kotlin function will do the job in any case

fun loadResource(path: String): URL {
    return Thread.currentThread().contextClassLoader.getResource(path)
}

or if you want a more robust function

fun loadResource(path: String): URL {
    val resource = Thread.currentThread().contextClassLoader.getResource(path)
    requireNotNull(resource) { "Resource $path not found" }
    return resource
}
Maite answered 20/5, 2021 at 17:6 Comment(3)
why does this return a URL?Lidda
@Lidda be not confused is the url of the resource, not a webpageMaite
Good solution, but the name is poor: it does not in fact load anything, it just determines the URL to do so.Peracid
R
2

I prefer reading resources in this way:

object {}.javaClass.getResourceAsStream("/html/file.html")?.use { it.reader(Charsets.UTF_8).readText() }

Explenation:

  • getResourceAsStream instead getResource. The resource on classpath can be basically anywhere. e.g. packed inside another .jar file.
    In these situations accessing resource via URL class returned from getResource method will fail. But accessing via method getResourceAsStream works in every situation.
  • object {} - This is not nice syntax, but it is not dependent on name of your class MyClass and works even in static (compenion object) block.
  • use to close stream - in most cases it is not necessary, but there can be some special classloaders, which may need it.
  • reader(Charsets.UTF_8) - UTF_8 is default encoding, but I prefer to be explicit. If you will encode your resource files in other encoding e.g. ISO-8859-2 you will not overlook it.
Repentant answered 14/7, 2022 at 12:3 Comment(0)
H
1

FYI: In all the above cases. getResource() is unsafe way of using nullable.

Haven't tried locally but I prefer this way:

fun readFile(resourcePath: String) = String::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"

Or even as custom datatype function

private fun String.asResource() = this::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"

and then you can call directly on path like:

// For suppose
val path = "/src/test/resources"
val content = path.asResource()
Hydrometeor answered 15/6, 2021 at 4:19 Comment(1)
Interesting. this::class.java.getResource doesn't give me the warning about it being null, whereas javaClass.getResource does. In any case putting this as an extension method on string doesn't make a lot of sense.Shellans
H
0

Another variation that handles null resource in place:

val content = object {}.javaClass
    .getResource("/html/file.html")
    ?.let(URL::readText)
    ?: error("Cannot open/find the file")
//  ?: "default text" // Instead of error()
Hulahula answered 17/2, 2023 at 20:40 Comment(0)
T
0
fun Any.resourceString(path: String): String =
    this.javaClass.getResource(path)?.readText() ?: error("Can't load resource at $path")

Call to this extension function looks relative to the class where call is made.

Twelvemonth answered 12/4, 2023 at 23:31 Comment(0)
J
-3

You might find the File class useful:

import java.io.File

fun main(args: Array<String>) {
  val content = File("src/main/resources/input.txt").readText()
  print(content)
} 
Jesusa answered 2/4, 2019 at 10:1 Comment(2)
This answer is misleading. That does not load a "resource" but loads file straight from file system, instead of the classpath. It will no longer work after application is assembled as you will try to refer to nonexisting files, instead of loading them from the jar file.Kumasi
@ben Thanks for your comment. Question was about reading file from resource in kotlin Spek test.Jesusa

© 2022 - 2024 — McMap. All rights reserved.