Created
January 21, 2021 15:48
-
-
Save bgmf/05b8b4598bc02ccf83b226cc2313f579 to your computer and use it in GitHub Desktop.
Failing GraalVM native-image build using Kotlin & JavaFX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package eu.dzim.kfxg | |
import javafx.application.Application | |
fun main(args: Array<String>) { | |
Application.launch(App::class.java, *args) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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"] } | |
] | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# src/main/resources/res | |
title=Title |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# src/main/resources/res | |
title=German Title | |
dialog.ok=OK | |
dialog.cancel=Abbrechen | |
dialog.title=Dialog (de) | |
dialog.content=Das ist ein Dialog. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# src/main/resources/res | |
title=English Title | |
dialog.ok=OK | |
dialog.cancel=Cancel | |
dialog.title=Dialog (en) | |
dialog.content=This is a dialog. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
Update: While I've used the older version of GraalVM 20.3.0
, the same error unfortunately occurs with the current 21.0.0
.
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
Ignore that the
App.kt
became quite messy, but it's only to prove one point:On Linux GraalVM's
native-image
will fail, when the UI-DSL's structure becomes to deep.I experience the problem on a Laptop with Ubuntu 20.04 LTS. I use GraalVM 20.3.0, Community Edition.
The code maybe quite messy, actually it's derived from a very small test app, where I tried to play around with JavaFX and GraalVM to build a minimal running example on Linux and Windows, to showcase it to some colleagues at work.
In fact on
Utils.kt
I literally dumped a couple of functions to keep other files "as less ugly as possible". I may have failed, though. 🤷And
KotlinFX.kt
... Well it borrowed very heavily from TornadoFX (which is a great library/framework), but the DSL is quite a thing.Execute with
$ export GRAALVM_HOME=/home/daniel/<path-to>/graalvm-ce/; export JAVA_HOME=$GRAALVM_HOME
then proceed with
$ mvn clean client:build
The build will eventually fail with
target/client/log/process-compile-.log
Other logs look basically the same, not much to see, except the exception...