Browse Source

Make ResourceReader a public API (#5334)

Describe proposed changes and the issue being fixed

Fixes
[CMP-8134](https://youtrack.jetbrains.com/issue/CMP-8134/Switch-the-ClassLoader-used-by-ResourceReader)


## Testing
- [x] Add Test to **desktopTest** SourceSet
- (Optional) This should be tested by QA

## Release Notes
### Features - Resources
- Added `JvmResourceReader` API and made `LocalResourceReader` public to
allow providing a custom classloader for desktop target
pull/5348/head
Lamberto Basti 6 months ago committed by GitHub
parent
commit
c4cf4c57e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      components/resources/library/build.gradle.kts
  2. 6
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt
  3. 7
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt
  4. 21
      components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt
  5. 39
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/CustomClassloaderTest.kt
  6. 1
      components/resources/library/src/desktopTest/resources/hello.txt
  7. 6
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt
  8. 54
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt
  9. 15
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt
  10. 70
      components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

10
components/resources/library/build.gradle.kts

@ -217,3 +217,13 @@ tasks.register<GeneratePluralRuleListsTask>("generatePluralRuleLists") { @@ -217,3 +217,13 @@ tasks.register<GeneratePluralRuleListsTask>("generatePluralRuleLists") {
outputFile = projectDir.file("src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt")
samplesOutputFile = projectDir.file("src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt")
}
tasks {
val desktopTestProcessResources =
named<ProcessResources>("desktopTestProcessResources")
withType<Test> {
dependsOn(desktopTestProcessResources)
environment("RESOURCES_PATH", desktopTestProcessResources.map { it.destinationDir.absolutePath }.get())
}
}

6
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt

@ -8,7 +8,11 @@ import androidx.compose.runtime.ProvidableCompositionLocal @@ -8,7 +8,11 @@ import androidx.compose.runtime.ProvidableCompositionLocal
import java.io.FileNotFoundException
import java.io.InputStream
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader = DefaultAndroidResourceReader
@ExperimentalResourceApi
internal object DefaultAndroidResourceReader : ResourceReader {
private val assets: AssetManager by lazy {
val context = androidContext ?: error(
"Android context is not initialized. " +

7
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt

@ -24,7 +24,8 @@ suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.r @@ -24,7 +24,8 @@ suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.r
@InternalResourceApi
fun getResourceUri(path: String): String = DefaultResourceReader.getUri(path)
internal interface ResourceReader {
@ExperimentalResourceApi
interface ResourceReader {
suspend fun read(path: String): ByteArray
suspend fun readPart(path: String, offset: Long, size: Long): ByteArray
fun getUri(path: String): String
@ -32,10 +33,12 @@ internal interface ResourceReader { @@ -32,10 +33,12 @@ internal interface ResourceReader {
internal expect fun getPlatformResourceReader(): ResourceReader
@ExperimentalResourceApi
internal val DefaultResourceReader = getPlatformResourceReader()
//ResourceReader provider will be overridden for tests
internal val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader }
@ExperimentalResourceApi
val LocalResourceReader = staticCompositionLocalOf { DefaultResourceReader }
//For an android preview we need to initialize the resource reader with the local context
internal expect val ProvidableCompositionLocal<ResourceReader>.currentOrPreview: ResourceReader

21
components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt

@ -1,10 +1,20 @@ @@ -1,10 +1,20 @@
package org.jetbrains.compose.resources
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader =
JvmResourceReader.Default
@ExperimentalResourceApi
class JvmResourceReader(
private val classLoader: ClassLoader
) : ResourceReader {
companion object {
internal val Default = JvmResourceReader(JvmResourceReader::class.java.classLoader)
}
override suspend fun read(path: String): ByteArray {
val resource = getResourceAsStream(path)
return resource.use { input -> input.readBytes() }
@ -31,17 +41,12 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou @@ -31,17 +41,12 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
}
override fun getUri(path: String): String {
val classLoader = getClassLoader()
val resource = classLoader.getResource(path) ?: throw MissingResourceException(path)
return resource.toURI().toString()
}
private fun getResourceAsStream(path: String): InputStream {
val classLoader = getClassLoader()
return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path)
}
private fun getClassLoader(): ClassLoader {
return this.javaClass.classLoader ?: error("Cannot find class loader")
}
}

39
components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/CustomClassloaderTest.kt

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
@file:OptIn(ExperimentalTestApi::class)
package org.jetbrains.compose.resources
import androidx.compose.ui.test.ExperimentalTestApi
import kotlinx.coroutines.test.runTest
import java.net.URLClassLoader
import kotlin.io.path.Path
import kotlin.io.path.isDirectory
import kotlin.test.Test
import kotlin.test.assertEquals
class CustomClassloaderTest {
val RESOURCES_PATH
get() = System.getenv("RESOURCES_PATH")
?.let { Path(it) }
?.takeIf { it.isDirectory() }
?: error("RESOURCES_PATH environment variable is not set or is not a directory")
@Test
fun testCustomClassloader() = runTest {
val actualResourceText =
CustomClassloaderTest::class
.java
.classLoader
.getResourceAsStream("hello.txt")
?.readAllBytes()
?.toString(Charsets.UTF_8)
val classloader = URLClassLoader(arrayOf(RESOURCES_PATH.toUri().toURL()), null)
val reader = JvmResourceReader(classloader)
assertEquals(
expected = actualResourceText,
actual = reader.read("hello.txt").toString(Charsets.UTF_8)
)
}
}

1
components/resources/library/src/desktopTest/resources/hello.txt

@ -0,0 +1 @@ @@ -0,0 +1 @@
Hello World!

6
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt

@ -25,8 +25,12 @@ import platform.Foundation.readDataOfLength @@ -25,8 +25,12 @@ import platform.Foundation.readDataOfLength
import platform.Foundation.seekToFileOffset
import platform.posix.memcpy
@ExperimentalResourceApi
@OptIn(BetaInteropApi::class)
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
internal actual fun getPlatformResourceReader(): ResourceReader = DefaultIOsResourceReader
@ExperimentalResourceApi
internal object DefaultIOsResourceReader : ResourceReader {
private val composeResourcesDir: String by lazy { findComposeResourcesPath() }
override suspend fun read(path: String): ByteArray {

54
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt

@ -9,12 +9,14 @@ import org.w3c.files.Blob @@ -9,12 +9,14 @@ import org.w3c.files.Blob
import org.w3c.xhr.XMLHttpRequest
import kotlin.js.Promise
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader {
if (isInTestEnvironment()) return TestJsResourceReader
return DefaultJsResourceReader
}
private val DefaultJsResourceReader = object : ResourceReader {
@ExperimentalResourceApi
internal object DefaultJsResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readAsBlob(path).asByteArray()
}
@ -46,36 +48,34 @@ private val DefaultJsResourceReader = object : ResourceReader { @@ -46,36 +48,34 @@ private val DefaultJsResourceReader = object : ResourceReader {
}
// It uses a synchronous XmlHttpRequest (blocking!!!)
private val TestJsResourceReader by lazy {
object : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readByteArray(path)
}
private object TestJsResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readByteArray(path)
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
}
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest()
request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined")
request.send()
if (request.status == 200.toShort()) {
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
val text = request.responseText
val bytes = Uint8Array(text.length)
js("for (var i = 0; i < text.length; i++) { bytes[i] = text.charCodeAt(i) & 0xFF; }")
return bytes.unsafeCast<ByteArray>()
}
throw MissingResourceException("$resPath")
private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest()
request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined")
request.send()
if (request.status == 200.toShort()) {
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
val text = request.responseText
val bytes = Uint8Array(text.length)
js("for (var i = 0; i < text.length; i++) { bytes[i] = text.charCodeAt(i) & 0xFF; }")
return bytes.unsafeCast<ByteArray>()
}
throw MissingResourceException("$resPath")
}
}

15
components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt

@ -2,10 +2,21 @@ package org.jetbrains.compose.resources @@ -2,10 +2,21 @@ package org.jetbrains.compose.resources
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.*
import platform.Foundation.NSBundle
import platform.Foundation.NSData
import platform.Foundation.NSFileHandle
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.closeFile
import platform.Foundation.fileHandleForReadingAtPath
import platform.Foundation.readDataOfLength
import platform.posix.memcpy
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader = DefaultMacOsResourceReader
@ExperimentalResourceApi
internal object DefaultMacOsResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray {
val data = readData(getPathOnDisk(path))
return ByteArray(data.length.toInt()).apply {

70
components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

@ -23,12 +23,14 @@ private external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: @@ -23,12 +23,14 @@ private external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr:
@JsFun("(blob) => blob.arrayBuffer()")
private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer>
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader {
if (isInTestEnvironment()) return TestWasmResourceReader
return DefaultWasmResourceReader
}
private val DefaultWasmResourceReader = object : ResourceReader {
@ExperimentalResourceApi
internal object DefaultWasmResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readAsBlob(path).asByteArray()
}
@ -72,45 +74,43 @@ private val DefaultWasmResourceReader = object : ResourceReader { @@ -72,45 +74,43 @@ private val DefaultWasmResourceReader = object : ResourceReader {
}
// It uses a synchronous XmlHttpRequest (blocking!!!)
private val TestWasmResourceReader by lazy {
object : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readByteArray(path)
}
private object TestWasmResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray {
return readByteArray(path)
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
}
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest()
request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined")
request.send()
if (request.status == 200.toShort()) {
return requestResponseAsByteArray(request).asByteArray()
}
println("Request status is not 200 - $resPath, status: ${request.status}")
throw MissingResourceException("$resPath")
private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest()
request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined")
request.send()
if (request.status == 200.toShort()) {
return requestResponseAsByteArray(request).asByteArray()
}
println("Request status is not 200 - $resPath, status: ${request.status}")
throw MissingResourceException(resPath)
}
private fun Int8Array.asByteArray(): ByteArray {
val array = this
val size = array.length
private fun Int8Array.asByteArray(): ByteArray {
val array = this
val size = array.length
@OptIn(UnsafeWasmMemoryApi::class)
return withScopedMemoryAllocator { allocator ->
val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
jsExportInt8ArrayToWasm(array, size, dstAddress)
ByteArray(size) { i -> (memBuffer + i).loadByte() }
}
@OptIn(UnsafeWasmMemoryApi::class)
return withScopedMemoryAllocator { allocator ->
val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
jsExportInt8ArrayToWasm(array, size, dstAddress)
ByteArray(size) { i -> (memBuffer + i).loadByte() }
}
}
}

Loading…
Cancel
Save