Skip to content

Instantly share code, notes, and snippets.

@bgmf
Created January 21, 2021 15:48
Show Gist options
  • Save bgmf/05b8b4598bc02ccf83b226cc2313f579 to your computer and use it in GitHub Desktop.
Save bgmf/05b8b4598bc02ccf83b226cc2313f579 to your computer and use it in GitHub Desktop.
Failing GraalVM native-image build using Kotlin & JavaFX
package eu.dzim.kfxg
import eu.dzim.kfxg.fx.*
import eu.dzim.kfxg.res.resource
import eu.dzim.kfxg.utils.*
import javafx.application.Application
import javafx.event.EventHandler
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Scene
import javafx.scene.control.Separator
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Priority
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.scene.text.TextAlignment
import javafx.stage.Stage
import java.net.URI
import java.util.*
class App : Application() {
override fun start(primaryStage: Stage) {
logger.info("Application is starting...")
resource.also {
logger.fine("Resource: locale = ${resource.locale}")
logger.fine("Resource: resource-bundle = ${resource.resourceBundle}")
logger.fine("Resource: title = ${resource.getString("title")}")
logger.fine("Resource: title(!) = ${resource.getGuaranteedString("title")}")
logger.fine("Resource: title(b) = ${resource.getBinding("title")}")
}
logger.fine("java.library.path=${System.getProperty("java.library.path")}")
val task = SystemLanguageTask().apply {
setOnSucceeded {
(it.source.value as? Locale)?.also { l ->
logger.info("Switching locale to '$l'.")
resource.setLocale(l)
}
}
setOnFailed {
logger.severe("Couldn't detect the systems language.", it.source.exception)
}
}
ExecutorHelper.async()?.also {
logger.info("Submitting SystemLanguageTask...")
it.submit(task)
}
primaryStage.apply {
minWidth = 640.0
minHeight = 480.0
title = "Title"
titleProperty().bind(resource.getBinding("title"))
scene = StackPane().apply {
children += BorderPane().apply {
styleClass += "main-root"
// style = "-fx-background-color: transparent;"
center = VBox(10.0).apply {
alignment = Pos.TOP_CENTER
region { vgrow = Priority.ALWAYS }
label("Test App") {
margin = Insets(0.0, 0.0, 0.0, 50.0)
}
button("Download from 'openjdk.java.net'...") {
action { download() }
}
button("Print System Properties...") {
action { printSystemProperties(logger) }
}
hbox(10.0) {
region { hgrow = Priority.ALWAYS }
val b = button("Show current Locale...")
val l = label()
b.action { l.text = resource.locale?.toString() ?: "n/a" }
region { hgrow = Priority.ALWAYS }
}
button("Switch Locale...") {
action {
val def = resource.locale
if ("de" != def?.language) {
resource.setLanguage("de")
logger.fine("Resource: (DE) title(!) = ${resource.getGuaranteedString("title")}")
} else {
resource.setLanguage("en")
logger.fine("Resource: (EN) title(!) = ${resource.getGuaranteedString("title")}")
}
logger.fine("Switched locale to ${resource.locale}")
}
}
button("Show Dialog...") {
action {
OverlayDialog.showDialog<Boolean>(FXSize(max = 250), FXSize(max = 150)) {
VBox(10.0).apply {
hbox {
alignment = Pos.TOP_CENTER
label {
hgrow = Priority.ALWAYS
textAlignment = TextAlignment.CENTER
text = "dialog.title".translate()
}
}
region { vgrow = Priority.ALWAYS }
label {
maxWidth = Double.MAX_VALUE
alignment = Pos.CENTER
textAlignment = TextAlignment.CENTER
text = "dialog.content".translate()
}
region { vgrow = Priority.ALWAYS }
children += Separator()
hbox(5.0) {
region { hgrow = Priority.ALWAYS }
button {
text = "dialog.ok".translate()
action { it.close { true } }
}
button {
text = "dialog.cancel".translate()
action { it.close { false } }
}
}
}
}?.also {
logger.info("Dialog closed with button '${if (it) "dialog.ok".translate() else "dialog.cancel".translate()}'")
} ?: logger.warning("Dialog closed with no result.")
}
}
region { vgrow = Priority.ALWAYS }
}
}
}.also {
OverlayDialog.initDialog(it)
}.let {
Scene(it, minWidth, minHeight)
}.apply {
fill = Color.TRANSPARENT
}
onCloseRequest = EventHandler {
logger.info("Application is closing...")
ExecutorHelper.shutdown()
}
}.show()
}
private fun download() {
downloadString(URI.create("https://openjdk.java.net/"))?.also {
logger.fine(it)
}
}
companion object {
val logger by lazy { createLogger() }
}
}
package eu.dzim.kfxg.utils
import java.util.logging.Level
import java.util.logging.LogRecord
import java.util.logging.SimpleFormatter
import java.util.logging.StreamHandler
class ConsoleHandler : StreamHandler() {
init {
level = Level.ALL
formatter = SimpleFormatter()
setOutputStream(System.out)
}
override fun publish(record: LogRecord?) {
super.publish(record)
flush()
}
override fun close() {
flush()
}
}
package eu.dzim.kfxg.utils
import javafx.application.Platform
import javafx.util.Pair
import java.lang.Thread.UncaughtExceptionHandler
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicLong
import java.util.function.Consumer
import java.util.function.Supplier
import java.util.logging.Logger
fun execAsync(): ExecutorService? = ExecutorHelper.async()
fun execAsync(ifNull: () -> Unit = {}, op: ExecutorService.() -> Unit) {
execAsync()?.also(op) ?: run(ifNull)
}
fun execOnAsync(ifNull: () -> Unit = {}, op: () -> Unit) {
execAsync(ifNull) { submit(op) }
}
fun execOnAsync(ifNull: () -> Unit = {}, op: Runnable) {
execAsync(ifNull) { submit(op) }
}
fun execFx(): Executor? = ExecutorHelper.fx()
fun execFx(ifNull: () -> Unit = {}, op: Executor.() -> Unit) {
execFx()?.also(op) ?: run(ifNull)
}
fun execOnFx(ifNull: () -> Unit = {}, op: () -> Unit) {
execFx(ifNull) { execute(op) }
}
fun <T> doAsynchronously(
prepareOnUI: () -> Unit = {},
failOnUI: Exception.() -> Unit = {},
finishOnUI: T?.() -> Unit = {},
doAsync: () -> T?,
) {
ExecutorHelper.doAsynchronously(
prepareOnUI,
doAsync,
{ e -> e?.failOnUI() },
{ t -> t.finishOnUI() },
)
}
object ExecutorHelper {
private val LOGGER = Logger.getLogger(ExecutorHelper::class.java.name)
private var fx: Executor? = null
private var async: ExecutorService? = null
private var shutdown = false
// var fx: Executor? = null
// get() {
// if (shutdown) {
// LOGGER.warning("ExecutorHelper is already in shutdown. Can't accept new requests.")
// return null
// }
// if (field == null) field = Executor { treatment: Runnable? -> PlatformHelper.run(treatment) }
// return field
// }
// private set
// var async: ExecutorService? = null
// get() {
// if (shutdown) {
// LOGGER.warning("ExecutorHelper is already in shutdown. Can't accept new requests.")
// return null
// }
// if (field == null) field = initExecutorService("task-%d")
// return field
// }
// private set
@Suppress("MoveSuspiciousCallableReferenceIntoParentheses")
fun fx(): Executor? {
if (shutdown) {
LOGGER.warning("ExecutorHelper is already in shutdown. Can't accept new requests.")
return null
}
if (fx == null) fx = Executor { runOnFXThread { it.run() } }
return fx
}
fun async(): ExecutorService? {
if (shutdown) {
LOGGER.warning("ExecutorHelper is already in shutdown. Can't accept new requests.")
return null
}
if (async == null) async = initExecutorService("task-%d")
return async
}
fun <T> doAsynchronously(
prepareOnUI: Runnable?,
doAsync: Supplier<T?>?,
failOnUI: Consumer<Exception?>?,
finishOnUI: Consumer<T?>?,
) {
if (shutdown) {
LOGGER.warning("ExecutorHelper is already in shutdown. Can't accept new requests.")
return
}
val fx = fx()
val async = async()
if (fx == null || async == null) {
LOGGER.warning("Couldn't initiate necessary Executors. Therefore I couldn't execute the request.")
return
}
CompletableFuture
.supplyAsync<Any?>({
try {
prepareOnUI?.run()
} catch (e: Exception) {
LOGGER.severe("Exception caught during on-UI-preparation: " + e.message, e)
}
null
}, fx)
.thenApplyAsync({
val result: Pair<T?, Exception?>
if (doAsync == null) {
result = Pair(null, null)
return@thenApplyAsync result
}
result = try {
Pair(doAsync.get(), null)
} catch (e: Exception) {
LOGGER.severe("Exception caught during async-task: " + e.message, e)
Pair(null, e)
}
result
}, async)
.thenAcceptAsync({ pair: Pair<T?, Exception?> ->
if (pair.value != null && failOnUI != null) {
try {
failOnUI.accept(pair.value)
} catch (e: Exception) {
LOGGER.severe("Exception caught during on-UI-failure handling: " + e.message, e)
}
}
if (finishOnUI != null) {
try {
finishOnUI.accept(pair.key)
} catch (e: Exception) {
LOGGER.severe("Exception caught during on-UI-finish handling: " + e.message, e)
}
}
}, fx)
}
fun shutdown() {
shutdown = true
if (fx != null) {
fx = null
LOGGER.fine("FX Executor is dereferenced.")
}
if (async != null) shutdownExecutorService(async)
}
fun runOnFXThread(op: () -> Unit) {
if (Platform.isFxApplicationThread()) {
op()
} else {
Platform.runLater(op)
}
}
private fun initExecutorService(nameFormat: String): ExecutorService {
return Executors.newSingleThreadExecutor(
createThreadFactory(
nameFormat,
null,
null,
) { t: Thread, e: Throwable ->
LOGGER.severe(
String.format(
"Uncaught exception on thread '%s' (id=%s, prio=%d). State was %s. Message was: %s",
t.name,
t.id,
t.priority,
t.state,
e.message,
),
e,
)
})
}
private fun createThreadFactory(
nameFormat: String?,
daemon: Boolean?,
priority: Int?,
uncaughtExceptionHandler: UncaughtExceptionHandler?,
): ThreadFactory {
val backingThreadFactory = Executors.defaultThreadFactory()
val count = if (nameFormat != null) AtomicLong(0L) else null
return ThreadFactory { runnable: Runnable? ->
val thread = backingThreadFactory.newThread(runnable)
if (nameFormat != null) thread.name = String.format(Locale.ROOT, nameFormat, count!!.getAndIncrement())
if (daemon != null) thread.isDaemon = daemon
if (priority != null) thread.priority = priority
if (uncaughtExceptionHandler != null) thread.uncaughtExceptionHandler = uncaughtExceptionHandler
thread
}
}
private fun shutdownExecutorService(executorService: ExecutorService?) {
executorService!!.shutdown()
try {
executorService.awaitTermination(1, TimeUnit.SECONDS)
} catch (e: InterruptedException) {
LOGGER.severe("Problem while waiting for async scheduler termination.", e)
} finally {
executorService.shutdownNow()
LOGGER.fine((if (executorService === async) "Async" else "Unknown") + " Executor is shutting down now.")
}
}
}
package eu.dzim.kfxg.fx
import javafx.beans.value.ObservableValue
import javafx.event.EventTarget
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.*
import javafx.scene.layout.*
import java.lang.reflect.Method
/**
* Helper data class containing the `min`, `pref` and `max` value for either width or height of a JavaFX node.
*/
data class FXSize(
val min: Number? = null,
val pref: Number? = null,
val max: Number? = null,
)
fun Region.applySize(width: FXSize? = null, height: FXSize? = null) {
width?.also {
it.min?.also { v -> minWidth = v.toDouble() } ?: run { minWidth = Region.USE_PREF_SIZE }
it.pref?.also { v -> prefWidth = v.toDouble() }
it.max?.also { v -> maxWidth = v.toDouble() } ?: run { maxWidth = Double.MAX_VALUE }
} ?: run {
minWidth = Region.USE_PREF_SIZE
maxWidth = Double.MAX_VALUE
}
height?.also {
it.min?.also { v -> minHeight = v.toDouble() } ?: run { minHeight = Region.USE_PREF_SIZE }
it.pref?.also { v -> prefHeight = v.toDouble() }
it.max?.also { v -> maxHeight = v.toDouble() } ?: run { maxHeight = Double.MAX_VALUE }
} ?: run {
minHeight = Region.USE_PREF_SIZE
maxHeight = Double.MAX_VALUE
}
}
/*
* DISCLAIMER: this is an stripped down extract from TornadoFX (for example Layouts.kt)
*/
fun EventTarget.borderPane(op: BorderPane.() -> Unit = {}): BorderPane = opcr(this, BorderPane(), op)
fun EventTarget.hbox(spacing: Number? = null, alignment: Pos? = null, op: HBox.() -> Unit = {}): HBox {
val hbox = HBox()
if (alignment != null) hbox.alignment = alignment
if (spacing != null) hbox.spacing = spacing.toDouble()
return opcr(this, hbox, op)
}
fun EventTarget.vbox(spacing: Number? = null, alignment: Pos? = null, op: VBox.() -> Unit = {}): VBox {
val vbox = VBox()
if (alignment != null) vbox.alignment = alignment
if (spacing != null) vbox.spacing = spacing.toDouble()
return opcr(this, vbox, op)
}
var Node.hgrow: Priority?
get() = HBox.getHgrow(this)
set(value) {
HBox.setHgrow(this, value)
}
var Node.vgrow: Priority?
get() = VBox.getVgrow(this)
set(value) {
VBox.setVgrow(this, value)
}
var Node.borderPaneAlignment: Pos?
get() = BorderPane.getAlignment(this)
set(value) {
BorderPane.setAlignment(this, value)
}
var Node.margin: Insets?
get() = when (this.parent) {
is VBox -> VBox.getMargin(this)
is HBox -> HBox.getMargin(this)
is BorderPane -> BorderPane.getMargin(this)
else -> null
}
set(value) {
when (this.parent) {
is VBox -> VBox.setMargin(this, value)
is HBox -> HBox.setMargin(this, value)
is BorderPane -> BorderPane.setMargin(this, value)
else -> null
}
}
fun EventTarget.region(op: Region.() -> Unit = {}) = opcr(this, Region(), op)
fun EventTarget.label(text: String = "", graphic: Node? = null, op: Label.() -> Unit = {}) = Label(text).attachTo(this, op) {
if (graphic != null) it.graphic = graphic
}
fun EventTarget.button(text: String = "", graphic: Node? = null, op: Button.() -> Unit = {}) = Button(text).attachTo(this, op) {
if (graphic != null) it.graphic = graphic
}
fun EventTarget.button(text: ObservableValue<String>, graphic: Node? = null, op: Button.() -> Unit = {}) = Button().attachTo(this, op) {
it.textProperty().bind(text)
if (graphic != null) it.graphic = graphic
}
fun ButtonBase.action(op: () -> Unit) = setOnAction { op() }
/**
* Add the given node to the pane, invoke the node operation and return the node. The `opcr` name
* is an acronym for "op connect & return".
*/
inline fun <T : Node> opcr(parent: EventTarget, node: T, op: T.() -> Unit = {}) = node.apply {
parent.addChildIfPossible(this)
op(this)
}
/**
* Attaches the node to the pane and invokes the node operation.
*/
inline fun <T : Node> T.attachTo(parent: EventTarget, op: T.() -> Unit = {}): T = opcr(parent, this, op)
/**
* Attaches the node to the pane and invokes the node operation.
* Because the framework sometimes needs to setup the node, another lambda can be provided
*/
internal inline fun <T : Node> T.attachTo(
parent: EventTarget,
after: T.() -> Unit,
before: (T) -> Unit
) = this.also(before).attachTo(parent, after)
/**
* Find the list of children from a Parent node. Gleaned code from ControlsFX for this.
*/
fun EventTarget.getChildList(): MutableList<Node>? = when (this) {
is SplitPane -> items
is ToolBar -> items
is Pane -> children
is Group -> children
is HBox -> children
is VBox -> children
is Control -> (skin as? SkinBase<*>)?.children ?: getChildrenReflectively()
is Parent -> getChildrenReflectively()
else -> null
}
@Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun Parent.getChildrenReflectively(): MutableList<Node>? {
val getter = this.javaClass.findMethodByName("getChildren")
if (getter != null && java.util.List::class.java.isAssignableFrom(getter.returnType)) {
getter.isAccessible = true
return getter.invoke(this) as MutableList<Node>
}
return null
}
fun Class<*>.findMethodByName(name: String): Method? {
val method = (declaredMethods + methods).find { it.name == name }
if (method != null) return method
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
if (superclass == java.lang.Object::class.java) return null
return superclass.findMethodByName(name)
}
@Suppress("UNNECESSARY_SAFE_CALL")
fun EventTarget.addChildIfPossible(node: Node, index: Int? = null) {
if (this is Node) {
// val target = builderTarget
// if (target != null) {
// // Trick to get around the disallowed use of invoke on out projected types
// @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
// target!!(this).value = node
// return
// }
}
when (this) {
is ScrollPane -> content = node
is Tab -> {
// Map the tab to the UIComponent for later retrieval. Used to close tab with UIComponent.close()
// and to connect the onTabSelected callback
content = node
}
is ButtonBase -> {
graphic = node
}
is BorderPane -> {
} // Either pos = builder { or caught by builderTarget above
is TabPane -> {
val tab = Tab(node.toString(), node)
tabs.add(tab)
}
is TitledPane -> {
when (content) {
is Pane -> content.addChildIfPossible(node, index)
is Node -> {
val container = VBox()
container.children.addAll(content, node)
content = container
}
else -> content = node
}
}
is CustomMenuItem -> {
content = node
}
is MenuItem -> {
graphic = node
}
else -> getChildList()?.apply {
if (!contains(node)) {
if (index != null && index < size)
add(index, node)
else
add(node)
}
}
}
}
# src/main/resources/res
handlers=eu.dzim.kfxg.utils.ConsoleHandler
.level=INFO
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS.%1$tL;%4$s;%3$s;%2$s;%5$s%6$s%n
java.util.logging.FileHandler.pattern=./output%g.log
java.util.logging.FileHandler.limit=50000000
java.util.logging.FileHandler.count=10
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
eu.dzim.kfxg.utils.ConsoleHandler.level=ALL
eu.dzim.kfxg.utils.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
eu.dzim.kfxg.level=ALL
package eu.dzim.kfxg
import javafx.application.Application
fun main(args: Array<String>) {
Application.launch(App::class.java, *args)
}
package eu.dzim.kfxg.fx
import eu.dzim.kfxg.utils.createLogger
import eu.dzim.kfxg.utils.severe
import javafx.application.Platform
import javafx.geometry.Pos
import javafx.scene.effect.BlurType
import javafx.scene.effect.DropShadow
import javafx.scene.layout.Background
import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.paint.Color
class OverlayDialog(
private val container: StackPane,
) {
private var _result: Any? = null
private var initialized = false
private val lock = Any()
private var overlay = StackPane()
fun <T> show(
width: FXSize? = null,
height: FXSize? = null,
contentSupplier: () -> Pane,
): T? {
init()
val content = contentSupplier().apply {
applySize(width, height)
setStyle("-fx-background-color: white; -fx-padding: 5.0; -fx-background-radius: 5.0; -fx-border-color: black; -fx-border-radius: 5.0;")
effect = DropShadow(BlurType.GAUSSIAN, Color.web("#000000"), 25.0, 0.1,0.0, 0.0)
}
StackPane.setAlignment(content, Pos.CENTER)
overlay.apply {
children += content
opacity = 1.0
toFront()
}
// This forces the dialog to be modal
try {
@Suppress("UNCHECKED_CAST")
_result = Platform.enterNestedEventLoop(lock) as T?
} catch (e: Exception) {
logger.severe(e.message ?: "", e)
}
return getResult()
}
fun <T> close(resultSupplier: (() -> T?)? = null) {
resultSupplier?.also { _result = it() }
overlay.apply {
children.clear()
opacity = 0.0
toBack()
}
try {
@Suppress("UNCHECKED_CAST")
Platform.exitNestedEventLoop(lock, _result)
} catch (e: Exception) {
logger.severe(e.message ?: "", e)
}
initialized = false
}
@Suppress("UNCHECKED_CAST")
fun <T> getResult(): T? = _result as? T?
fun <T> setResult(result: T?) {
_result = result
}
private fun init() {
if (initialized) return
StackPane.setAlignment(overlay, Pos.CENTER)
overlay = container.lookup(".overlay") as? StackPane ?: StackPane().apply {
styleClass += "overlay"
alignment = Pos.CENTER
background = Background(BackgroundFill(Color.rgb(0, 0, 0, 0.7), null, null))
opacity = 0.0
}
if (!container.children.contains(overlay)) {
container.children += overlay
}
overlay.toBack()
initialized = true
}
companion object {
val logger by lazy { createLogger() }
private var container: StackPane? = null
private var dialog: OverlayDialog? = null
fun initDialog(container: StackPane) {
Companion.container = container
dialog = OverlayDialog(container)
}
fun <T> showDialog(
width: FXSize? = null,
height: FXSize? = null,
contentSupplier: (OverlayDialog) -> Pane,
): T? =
dialog?.let { it.show<T>(width, height) { contentSupplier(it) } }
val isDialogShown: Boolean get() = dialog?.overlay?.children?.isNotEmpty() == true
fun closeOpenDialog() {
if (isDialogShown) dialog?.close<Any>()
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.dzim</groupId>
<artifactId>kotlin-javafx-graal</artifactId>
<version>0.0.0</version>
<packaging>jar</packaging>
<name>KotlinFX-Graal</name>
<description>Simple app to showcase GraalVM native-image issue.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<kotlin.version>1.4.21</kotlin.version>
<kotlin.code.style>official</kotlin.code.style>
<junit.version>4.12</junit.version>
<javafx.version>15</javafx.version>
<javafx.plugin.version>0.0.5</javafx.plugin.version>
<client.plugin.version>0.1.35</client.plugin.version>
<mainClassName>eu.dzim.kfxg.MainKt</mainClassName>
</properties>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>gluon-releases</id>
<url>http://nexus.gluonhq.com/nexus/content/repositories/releases/</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.2</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>${javafx.plugin.version}</version>
<configuration>
<mainClass>${mainClassName}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>client-maven-plugin</artifactId>
<version>${client.plugin.version}</version>
<configuration>
<!-- <target>ios|android|host</target> -->
<mainClass>${mainClassName}</mainClass>
<!-- basically ignored, I could not read and properties using ResourceBundle -->
<bundlesList>
<list>res.strings</list>
<list>res.strings_de</list>
<list>res.strings_en</list>
</bundlesList>
<!-- Workaround: write some custom logic, to circumvent the ResourceBundle issue - easy -->
<resourcesList>
<list>logging.properties</list>
<list>res/strings.properties</list>
<list>res/strings_de.properties</list>
<list>res/strings_en.properties</list>
</resourcesList>
<reflectionList>
<!-- UI, when you don't use reflection, the main classes are enough -->
<list>eu.dzim.kfxg.MainKt</list>
<list>eu.dzim.kfxg.App</list>
<!-- other, e.g. logging -->
<list>eu.dzim.kfxg.utils.ConsoleHandler</list>
<!-- our own jackson data classes -->
<!-- Jackson -->
<list>com.fasterxml.jackson.core.JsonFactory</list>
</reflectionList>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>11</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
# src/main/resources/META-INF/substrate/config - remove this line!
[
{
"name": "com.fasterxml.jackson.databind.ObjectMapper",
"methods": [
{ "name": "<init>", "parameterTypes": [] },
{ "name": "<init>", "parameterTypes": ["com.fasterxml.jackson.core.JsonFactory"] }
]
}
]
package eu.dzim.kfxg.res
import eu.dzim.kfxg.utils.createLogger
import eu.dzim.kfxg.utils.severe
import javafx.beans.binding.*
import javafx.beans.property.ReadOnlyObjectProperty
import javafx.beans.property.ReadOnlyObjectWrapper
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.*
val resource: Resource
get() = SimpleResourceImpl
/**
* - always to path structure for package, like '/res'
* - resource property file names as usual
* - only language will be used from the locales, so no 'strings_de_DE.properties', only 'string_de.properties'
*/
object SimpleResourceImpl : SimpleBaseResource("res", "strings", Locale.ENGLISH) {
var instance: SimpleResourceImpl? = null
get() {
if (field == null) field = this
return field
}
private set
}
interface Resource {
fun setLanguage(language: String): Boolean
fun setLocale(locale: Locale): Boolean
fun localeProperty(): ReadOnlyObjectProperty<Locale?>
val locale: Locale?
fun resourceBundleProperty(): ReadOnlyObjectProperty<ResourceBundle?>
val resourceBundle: ResourceBundle?
fun getGuaranteedString(key: String): String
fun getGuaranteedString(locale: Locale, key: String): String
fun getGuaranteedString(key: String, vararg args: Any): String
fun getGuaranteedString(locale: Locale, key: String, vararg args: Any): String
fun getString(key: String): String?
fun getString(key: String, vararg args: Any): String?
fun getBoolean(key: String): Boolean?
fun getBoolean(key: String, defaultValue: Boolean?): Boolean?
fun getInteger(key: String): Int?
fun getInteger(key: String, defaultValue: Int?): Int?
fun getLong(key: String): Long?
fun getLong(key: String, defaultValue: Long?): Long?
fun getDouble(key: String): Double?
fun getDouble(key: String, defaultValue: Double?): Double?
fun getBinding(key: String, vararg parameter: Any): Binding<String?>
fun <T> getBinding(key: String, defaultValue: T?, vararg parameter: Any): Binding<T?>
fun setParentResource(parentResource: Resource)
}
interface Disposable {
fun dispose()
}
class DisposableHolder {
private val disposables: List<Disposable> = Collections.synchronizedList(ArrayList())
fun disposeAll() {
disposables.stream().forEach { it.dispose() }
}
}
abstract class SimpleBaseResource protected constructor(
private val resourcePath: String = DEFAULT_PACKAGE_NAME,
private val resourceFilePrefix: String = DEFAULT_PROPERTIES_NAME,
private val defaultLocale: Locale? = Locale.ENGLISH
) : Resource, Disposable {
private var parentResource: Resource?
private val bundleName: String
private val localeRO = ReadOnlyObjectWrapper(this, "locale", Locale.getDefault())
private val localizedResources: MutableMap<Locale, Properties?> = HashMap()
private val resourcesRO = ReadOnlyObjectWrapper(this, "resources", null as Properties?)
private val keyStringBindings: MutableMap<String, StringBinding?> = HashMap()
private val keyBooleanBindings: MutableMap<String, BooleanBinding?> = HashMap()
private val keyIntegerBindings: MutableMap<String, IntegerBinding?> = HashMap()
private val keyLongBindings: MutableMap<String, LongBinding?> = HashMap()
private val keyDoubleBindings: MutableMap<String, DoubleBinding?> = HashMap()
@Synchronized
override fun setLanguage(language: String): Boolean {
val next = Locale(language.toLowerCase())
return setLocale(next)
}
override fun setParentResource(parentResource: Resource) {
this.parentResource = parentResource
}
@Synchronized
override fun setLocale(locale: Locale): Boolean {
val current = this.locale
if (current == null || current != locale) {
if (localizedResources[locale] != null) {
resourcesRO.set(localizedResources[locale])
} else {
resourcesRO.set(loadProperties(resourcePath, resourceFilePrefix, locale))
localizedResources[locale] = resourcesRO.get()
}
localeRO.set(locale)
}
return false
}
@Synchronized
override fun localeProperty(): ReadOnlyObjectProperty<Locale?> = localeRO.readOnlyProperty
override val locale: Locale?
get() = localeRO.get()
@Synchronized
override fun resourceBundleProperty(): ReadOnlyObjectProperty<ResourceBundle?> =
ReadOnlyObjectWrapper(this, "resourceBundle", null as ResourceBundle?).readOnlyProperty
override val resourceBundle: ResourceBundle?
get() = null
@Synchronized
override fun getGuaranteedString(key: String): String =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getGuaranteedString(key)?.trim { it <= ' ' } ?: "!$key!"
}
override fun getGuaranteedString(locale: Locale, key: String): String {
var rb = localizedResources[locale]
if (rb == null) {
rb = loadProperties(resourcePath, resourceFilePrefix, locale)
localizedResources[locale] = rb
}
return if (localizedResources[locale] != null) {
try {
rb?.getProperty(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getGuaranteedString(locale, key)?.trim { it <= ' ' } ?: "!$key!"
}
} else {
getGuaranteedString(key)
}
}
@Synchronized
override fun getGuaranteedString(key: String, vararg args: Any): String {
val value = try {
getPropertyWithFallback(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getGuaranteedString(key, *args)?.trim { it <= ' ' } ?: "!$key!"
}
return if (value.startsWith("!") && value.endsWith("!"))
value
else
String.format(locale, value, *args)
}
override fun getGuaranteedString(locale: Locale, key: String, vararg args: Any): String {
var rb = localizedResources[locale]
if (rb == null) {
rb = loadProperties(resourcePath, resourceFilePrefix, locale)
localizedResources[locale] = rb
}
return if (localizedResources[locale] != null) {
val value = try {
rb?.getProperty(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getGuaranteedString(locale, key, *args)?.trim { it <= ' ' } ?: "!$key!"
}
if (value.startsWith("!") && value.endsWith("!")) value
else String.format(locale, value, *args)
} else {
getGuaranteedString(key, *args)
}
}
@Synchronized
override fun getString(key: String): String? =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getString(key)
}
@Synchronized
override fun getString(key: String, vararg args: Any): String? {
val value: String = try {
getPropertyWithFallback(key)?.trim { it <= ' ' }
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getString(key, *args) ?: "!$key!"
}
return if (value.startsWith("!") && value.endsWith("!"))
null
else
String.format(locale, value, *args)
}
@Synchronized
override fun getBoolean(key: String): Boolean? =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }?.toBoolean()
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getBoolean(key)
}
@Synchronized
override fun getBoolean(key: String, defaultValue: Boolean?): Boolean? =
getBoolean(key) ?: defaultValue
@Synchronized
override fun getInteger(key: String): Int? =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }?.toInt()
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getInteger(key)
} catch (e: NumberFormatException) {
null
}
@Synchronized
override fun getInteger(key: String, defaultValue: Int?): Int? =
getInteger(key) ?: defaultValue
@Synchronized
override fun getLong(key: String): Long? =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }?.toLong()
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getLong(key)
} catch (e: NumberFormatException) {
null
}
@Synchronized
override fun getLong(key: String, defaultValue: Long?): Long? =
getLong(key) ?: defaultValue
@Synchronized
override fun getDouble(key: String): Double? =
try {
getPropertyWithFallback(key)?.trim { it <= ' ' }?.toDouble()
?: throw MissingResourceException("key '$key' not found", this::class.simpleName, key)
} catch (e: MissingResourceException) {
parentResource?.getDouble(key)
} catch (e: NumberFormatException) {
null
}
@Synchronized
override fun getDouble(key: String, defaultValue: Double?): Double? =
getDouble(key) ?: defaultValue
override fun getBinding(key: String, vararg parameter: Any): Binding<String?> =
createStringBinding(key, null, *parameter)!!
@Suppress("UNCHECKED_CAST")
override fun <T> getBinding(key: String, defaultValue: T?, vararg parameter: Any): Binding<T?> {
return when (defaultValue) {
null -> getBinding(key, *parameter) as Binding<T?>
is String -> createStringBinding(key, defaultValue as String, *parameter) as Binding<T?>
is Boolean -> createBooleanBinding(key, defaultValue as Boolean) as Binding<T?>
is Long -> createLongBinding(key, defaultValue as Long) as Binding<T?>
is Int -> createIntegerBinding(key, defaultValue as Int) as Binding<T?>
is Double -> createDoubleBinding(key, defaultValue as Double) as Binding<T?>
else -> throw IllegalArgumentException("Unknown type to create a binding for: " + defaultValue.javaClass.name)
}
}
private fun getPropertyWithFallback(key: String): String? {
// try to get the property from the current active locale
try {
resourcesRO.get()?.getProperty(key)
} catch (e: MissingResourceException) {
null
}?.run { return this }
// then try to find the property in the properties of the default locale
if (locale != null && defaultLocale != null && locale != defaultLocale) {
var rb = localizedResources[defaultLocale]
if (rb == null) {
rb = loadProperties(resourcePath, resourceFilePrefix, locale)
localizedResources[defaultLocale] = rb
}
try {
localizedResources[defaultLocale]?.getProperty(key)
} catch (e: MissingResourceException) {
null
}?.run { return this }
}
// finally attempt to find the property in the root locale
try {
localizedResources[Locale.ROOT]?.getProperty(key)
} catch (e: MissingResourceException) {
null
}?.run { return this }
return null
}
private fun createStringBinding(key: String, defaultValue: String?, vararg parameter: Any): StringBinding? {
return if (parameter.isEmpty()) {
var binding = keyStringBindings[key]
if (binding != null) return binding
binding = Bindings.createStringBinding(
{
if (defaultValue == null) getGuaranteedString(key).trim { it <= ' ' }
else getString(key) ?: defaultValue
},
resourcesRO
)
keyStringBindings[key] = binding
binding
} else {
Bindings.createStringBinding({
if (defaultValue == null)
getGuaranteedString(key, *parameter).trim { it <= ' ' }
else
String.format(locale, getString(key) ?: defaultValue, *parameter)
}, resourcesRO)
}
}
private fun createBooleanBinding(key: String, defaultValue: Boolean): BooleanBinding? {
var binding = keyBooleanBindings[key]
if (binding != null) return binding
binding = Bindings.createBooleanBinding({ getBoolean(key, defaultValue) }, resourcesRO)
keyBooleanBindings[key] = binding
return binding
}
private fun createIntegerBinding(key: String, defaultValue: Int): IntegerBinding? {
var binding = keyIntegerBindings[key]
if (binding != null) return binding
binding = Bindings.createIntegerBinding({ getInteger(key, defaultValue) }, resourcesRO)
keyIntegerBindings[key] = binding
return binding
}
private fun createLongBinding(key: String, defaultValue: Long): LongBinding? {
var binding = keyLongBindings[key]
if (binding != null) return binding
binding = Bindings.createLongBinding({ getLong(key, defaultValue) }, resourcesRO)
keyLongBindings[key] = binding
return binding
}
private fun createDoubleBinding(key: String, defaultValue: Double): DoubleBinding? {
var binding = keyDoubleBindings[key]
if (binding != null) return binding
binding = Bindings.createDoubleBinding({ getDouble(key, defaultValue) }, resourcesRO)
keyDoubleBindings[key] = binding
return binding
}
override fun dispose() {
keyStringBindings.clear()
keyBooleanBindings.clear()
keyIntegerBindings.clear()
keyLongBindings.clear()
keyDoubleBindings.clear()
}
private fun loadProperties(path: String, file: String, locale: Locale?): Properties? {
val fullPath = String.format(
"/%s/%s%s.properties",
path.trimStart('/').trimEnd('/'),
file,
if (locale == null || locale.language.isEmpty()) "" else "_${locale.language}",
)
if (this::class.java.getResource(fullPath) != null) {
try {
return this::class.java.getResourceAsStream(fullPath).let {
Properties().apply { load(InputStreamReader(it, StandardCharsets.UTF_8)) }
}
} catch (e: Exception) {
logger.severe("Could not load property resource file at: $fullPath", e)
}
} else {
logger.severe("Could not find property resource file at: $fullPath")
}
return null
}
companion object {
private val DEFAULT_PACKAGE_NAME = SimpleBaseResource::class.java.getPackage().name
private const val DEFAULT_PROPERTIES_NAME = "strings"
private val logger by lazy { createLogger() }
}
init {
require(resourcePath.isNotEmpty()) {
"The path with the strings.properties file must not be empty!"
}
require(resourceFilePrefix.isNotEmpty()) {
"The resource file prefix (e.g. 'strings' for 'strings.properties') must not be empty!"
}
bundleName = String.format("%s.%s", resourcePath, resourceFilePrefix)
val locale =
if (defaultLocale != null) Locale(defaultLocale.language.toLowerCase())
else Locale(Locale.getDefault().language.toLowerCase())
this.localeRO.set(locale)
if (localizedResources[locale] != null) {
resourcesRO.set(localizedResources[locale])
} else {
resourcesRO.set(loadProperties(resourcePath, resourceFilePrefix, locale))
localizedResources[locale] = resourcesRO.get()
}
// always fill the root locale data from the resource without language specification
localizedResources[Locale.ROOT] = loadProperties(resourcePath, resourceFilePrefix, Locale.ROOT)
parentResource = null
}
}
# src/main/resources/res
title=Title
# src/main/resources/res
title=German Title
dialog.ok=OK
dialog.cancel=Abbrechen
dialog.title=Dialog (de)
dialog.content=Das ist ein Dialog.
# src/main/resources/res
title=English Title
dialog.ok=OK
dialog.cancel=Cancel
dialog.title=Dialog (en)
dialog.content=This is a dialog.
package eu.dzim.kfxg.utils
import javafx.concurrent.Task
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.stream.Collectors
class SystemLanguageTask : Task<Locale?>() {
override fun call(): Locale? =
getSystemLanguage()
private fun getSystemLanguage(): Locale? =
System.getProperty("os.name")?.toLowerCase()
?.let {
when {
it.contains("windows") ->
if (System.getProperty("os.version").contains("10")) {
findLanguageWin10(handleProcess(CMD_LANG_WINDOWS_10))
} else {
findLanguageWinFallback(handleProcess(CMD_LANG_WINDOWS_FALLBACK))
}
it.contains("mac") -> findLanguageLinux(handleProcess(CMD_LANG_MAC))
it.contains("linux") -> findLanguageLinux(handleProcess(CMD_LANG_LINUX))
else -> null
}
}
private fun handleProcess(command: String): List<String> {
val process = Runtime.getRuntime().exec(command)
if (0 != process.waitFor()) return listOf()
process.waitFor()
@Suppress("UnnecessaryVariable")
val text = BufferedReader(
InputStreamReader(process.inputStream, StandardCharsets.UTF_8)
).lines().collect(Collectors.toList())
return text
}
}
package eu.dzim.kfxg.utils
import eu.dzim.kfxg.App
import eu.dzim.kfxg.res.resource
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.logging.Level
import java.util.logging.LogManager
import java.util.logging.Logger
import java.util.stream.Collectors
private var logManagerInitialized = false
private val logManager = LogManager.getLogManager()?.apply {
if (!logManagerInitialized) {
synchronized(logManagerInitialized) {
getLogger(Logger.GLOBAL_LOGGER_NAME).level = Level.ALL
try {
readConfiguration(App::class.java.getResourceAsStream("/logging.properties"))
} catch (e: Exception) {
System.err.println("Problem while reading the log configuration: ${e.message}")
e.printStackTrace()
}
logManagerInitialized = true
}
}
}
fun Any.createLogger(): Logger {
logManager // only to force the log manager to be initialized
return Logger.getLogger(this::class.qualifiedName)
}
val Any.newLogger
get() = createLogger()
object WithLogger {
val logger by lazy { createLogger() }
}
fun Logger.severe(message: String, exception: Throwable) =
log(Level.SEVERE, message, exception)
fun Logger.warning(message: String, exception: Throwable) =
log(Level.WARNING, message, exception)
fun Logger.info(message: String, exception: Throwable) =
log(Level.INFO, message, exception)
fun Logger.fine(message: String, exception: Throwable) =
log(Level.FINE, message, exception)
fun printSystemProperties(logger: Logger? = null) {
arrayOf(
"file.separator", "java.class.path", "java.home", "java.vendor", "java.vendor.url", "java.version",
"line.separator", "os.arch", "os.name", "os.version", "path.separator", "user.dir", "user.home", "user.name",
"user.language", "user.country",
)
.map { "$it=${System.getProperty(it)}" }
.forEach {
if (logger == null) println(it)
else logger.fine(it)
}
}
fun handleProcess(command: String): List<String> {
val process = Runtime.getRuntime().exec(command)
if (0 != process.waitFor()) return listOf()
process.waitFor()
@Suppress("UnnecessaryVariable")
val text = BufferedReader(
InputStreamReader(process.inputStream, StandardCharsets.UTF_8)
).lines().collect(Collectors.toList())
return text
}
fun Array<String>.fireProcess(op: () -> File? = { null }): Process =
ProcessBuilder(*this).apply {
op()?.also { directory(it) }
environment()
redirectErrorStream(true)
// inheritIO()
}.start()
/**
* Takes this [String] as the key for the wrapped [eu.dzim.kfxg.res.Resource] translation utility.
*/
fun String.translate(vararg parameter: Any): String =
if (parameter.isNotEmpty()) resource.getGuaranteedString(this, *parameter)
else resource.getGuaranteedString(this)
const val CMD_LANG_WINDOWS_10 =
"cmd.exe /c powershell \"write-host(Get-WinSystemLocale | Select -ExpandProperty \"TwoLetterISOLanguageName\")\""
const val CMD_LANG_WINDOWS_FALLBACK = "cmd.exe /c \"systeminfo | findstr ;\""
const val CMD_LANG_MAC = "defaults read -g AppleLanguages"
const val CMD_LANG_LINUX = "sh -c env"
fun findLanguageWin10(text: List<String>): Locale? =
text.firstOrNull()
?.let { Locale(it) }
fun findLanguageWinFallback(text: List<String>): Locale? =
text.filterNot { it.isEmpty() || it.isBlank() }
.map { it.split(":")[1].trim() }
.map { it.split(";")[0].trim() }
.firstOrNull()
?.let { it.split("-")[0] }
?.let { Locale(it) }
fun findLanguageMac(text: List<String>): Locale? =
text.filterNot { it.contains("(") || it.contains(")") }
.map { it.trim().trimEnd(',').trim('"') }
.filterNot { it.isEmpty() }
.firstOrNull()
?.let { it.split("-")[0] }
?.let { Locale(it) }
fun findLanguageLinux(text: List<String>): Locale? =
text.firstOrNull { it.contains("LANG") }
?.let { it.split("=")[1].split("_")[0] }
?.let { Locale(it) }
fun downloadString(uri: URI): String? {
val client: HttpClient = HttpClient.newHttpClient()
val request = HttpRequest.newBuilder()
.uri(uri)
.build()
return try {
val result = client
.send(request, HttpResponse.BodyHandlers.ofString())
.body()
result
} catch (e: Exception) {
when (e) {
is IOException -> WithLogger.logger.severe("IO exception during the operation: ${e.message}")
else -> WithLogger.logger.severe("Something went wrong while performing the operation: ${e.message}")
}
null
}
}
@bgmf
Copy link
Author

bgmf commented Jan 25, 2021

Update: While I've used the older version of GraalVM 20.3.0, the same error unfortunately occurs with the current 21.0.0.

@bgmf
Copy link
Author

bgmf commented Feb 5, 2021

The issue for this problem can be found here (#3143).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment