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") {
outputFile = projectDir.file("src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt") 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") 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
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream 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 { private val assets: AssetManager by lazy {
val context = androidContext ?: error( val context = androidContext ?: error(
"Android context is not initialized. " + "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
@InternalResourceApi @InternalResourceApi
fun getResourceUri(path: String): String = DefaultResourceReader.getUri(path) fun getResourceUri(path: String): String = DefaultResourceReader.getUri(path)
internal interface ResourceReader { @ExperimentalResourceApi
interface ResourceReader {
suspend fun read(path: String): ByteArray suspend fun read(path: String): ByteArray
suspend fun readPart(path: String, offset: Long, size: Long): ByteArray suspend fun readPart(path: String, offset: Long, size: Long): ByteArray
fun getUri(path: String): String fun getUri(path: String): String
@ -32,10 +33,12 @@ internal interface ResourceReader {
internal expect fun getPlatformResourceReader(): ResourceReader internal expect fun getPlatformResourceReader(): ResourceReader
@ExperimentalResourceApi
internal val DefaultResourceReader = getPlatformResourceReader() internal val DefaultResourceReader = getPlatformResourceReader()
//ResourceReader provider will be overridden for tests //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 //For an android preview we need to initialize the resource reader with the local context
internal expect val ProvidableCompositionLocal<ResourceReader>.currentOrPreview: ResourceReader 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 @@
package org.jetbrains.compose.resources package org.jetbrains.compose.resources
import java.io.EOFException
import java.io.IOException
import java.io.InputStream 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 { override suspend fun read(path: String): ByteArray {
val resource = getResourceAsStream(path) val resource = getResourceAsStream(path)
return resource.use { input -> input.readBytes() } return resource.use { input -> input.readBytes() }
@ -31,17 +41,12 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
} }
override fun getUri(path: String): String { override fun getUri(path: String): String {
val classLoader = getClassLoader()
val resource = classLoader.getResource(path) ?: throw MissingResourceException(path) val resource = classLoader.getResource(path) ?: throw MissingResourceException(path)
return resource.toURI().toString() return resource.toURI().toString()
} }
private fun getResourceAsStream(path: String): InputStream { private fun getResourceAsStream(path: String): InputStream {
val classLoader = getClassLoader()
return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) 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 @@
@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 @@
Hello World!

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

@ -25,8 +25,12 @@ import platform.Foundation.readDataOfLength
import platform.Foundation.seekToFileOffset import platform.Foundation.seekToFileOffset
import platform.posix.memcpy import platform.posix.memcpy
@ExperimentalResourceApi
@OptIn(BetaInteropApi::class) @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() } private val composeResourcesDir: String by lazy { findComposeResourcesPath() }
override suspend fun read(path: String): ByteArray { 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
import org.w3c.xhr.XMLHttpRequest import org.w3c.xhr.XMLHttpRequest
import kotlin.js.Promise import kotlin.js.Promise
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader { internal actual fun getPlatformResourceReader(): ResourceReader {
if (isInTestEnvironment()) return TestJsResourceReader if (isInTestEnvironment()) return TestJsResourceReader
return DefaultJsResourceReader return DefaultJsResourceReader
} }
private val DefaultJsResourceReader = object : ResourceReader { @ExperimentalResourceApi
internal object DefaultJsResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray { override suspend fun read(path: String): ByteArray {
return readAsBlob(path).asByteArray() return readAsBlob(path).asByteArray()
} }
@ -46,36 +48,34 @@ private val DefaultJsResourceReader = object : ResourceReader {
} }
// It uses a synchronous XmlHttpRequest (blocking!!!) // It uses a synchronous XmlHttpRequest (blocking!!!)
private val TestJsResourceReader by lazy { private object TestJsResourceReader : ResourceReader {
object : ResourceReader { override suspend fun read(path: String): ByteArray {
override suspend fun read(path: String): ByteArray { return readByteArray(path)
return readByteArray(path) }
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt()) return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
} }
override fun getUri(path: String): String { override fun getUri(path: String): String {
val location = window.location val location = window.location
return getResourceUrl(location.origin, location.pathname, path) return getResourceUrl(location.origin, location.pathname, path)
} }
private fun readByteArray(path: String): ByteArray { private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path) val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest() val request = XMLHttpRequest()
request.open("GET", resPath, false) request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined") request.overrideMimeType("text/plain; charset=x-user-defined")
request.send() request.send()
if (request.status == 200.toShort()) { if (request.status == 200.toShort()) {
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually // For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
val text = request.responseText val text = request.responseText
val bytes = Uint8Array(text.length) val bytes = Uint8Array(text.length)
js("for (var i = 0; i < text.length; i++) { bytes[i] = text.charCodeAt(i) & 0xFF; }") js("for (var i = 0; i < text.length; i++) { bytes[i] = text.charCodeAt(i) & 0xFF; }")
return bytes.unsafeCast<ByteArray>() return bytes.unsafeCast<ByteArray>()
}
throw MissingResourceException("$resPath")
} }
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
import kotlinx.cinterop.addressOf import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned 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 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 { override suspend fun read(path: String): ByteArray {
val data = readData(getPathOnDisk(path)) val data = readData(getPathOnDisk(path))
return ByteArray(data.length.toInt()).apply { 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:
@JsFun("(blob) => blob.arrayBuffer()") @JsFun("(blob) => blob.arrayBuffer()")
private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer>
@ExperimentalResourceApi
internal actual fun getPlatformResourceReader(): ResourceReader { internal actual fun getPlatformResourceReader(): ResourceReader {
if (isInTestEnvironment()) return TestWasmResourceReader if (isInTestEnvironment()) return TestWasmResourceReader
return DefaultWasmResourceReader return DefaultWasmResourceReader
} }
private val DefaultWasmResourceReader = object : ResourceReader { @ExperimentalResourceApi
internal object DefaultWasmResourceReader : ResourceReader {
override suspend fun read(path: String): ByteArray { override suspend fun read(path: String): ByteArray {
return readAsBlob(path).asByteArray() return readAsBlob(path).asByteArray()
} }
@ -72,45 +74,43 @@ private val DefaultWasmResourceReader = object : ResourceReader {
} }
// It uses a synchronous XmlHttpRequest (blocking!!!) // It uses a synchronous XmlHttpRequest (blocking!!!)
private val TestWasmResourceReader by lazy { private object TestWasmResourceReader : ResourceReader {
object : ResourceReader { override suspend fun read(path: String): ByteArray {
override suspend fun read(path: String): ByteArray { return readByteArray(path)
return readByteArray(path) }
}
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt()) return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
} }
override fun getUri(path: String): String { override fun getUri(path: String): String {
val location = window.location val location = window.location
return getResourceUrl(location.origin, location.pathname, path) return getResourceUrl(location.origin, location.pathname, path)
} }
private fun readByteArray(path: String): ByteArray { private fun readByteArray(path: String): ByteArray {
val resPath = WebResourcesConfiguration.getResourcePath(path) val resPath = WebResourcesConfiguration.getResourcePath(path)
val request = XMLHttpRequest() val request = XMLHttpRequest()
request.open("GET", resPath, false) request.open("GET", resPath, false)
request.overrideMimeType("text/plain; charset=x-user-defined") request.overrideMimeType("text/plain; charset=x-user-defined")
request.send() request.send()
if (request.status == 200.toShort()) { if (request.status == 200.toShort()) {
return requestResponseAsByteArray(request).asByteArray() return requestResponseAsByteArray(request).asByteArray()
}
println("Request status is not 200 - $resPath, status: ${request.status}")
throw MissingResourceException("$resPath")
} }
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 { @OptIn(UnsafeWasmMemoryApi::class)
val array = this return withScopedMemoryAllocator { allocator ->
val size = array.length val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
@OptIn(UnsafeWasmMemoryApi::class) jsExportInt8ArrayToWasm(array, size, dstAddress)
return withScopedMemoryAllocator { allocator -> ByteArray(size) { i -> (memBuffer + i).loadByte() }
val memBuffer = allocator.allocate(size)
val dstAddress = memBuffer.address.toInt()
jsExportInt8ArrayToWasm(array, size, dstAddress)
ByteArray(size) { i -> (memBuffer + i).loadByte() }
}
} }
} }
} }

Loading…
Cancel
Save