I'm attempting to create a Java LinkedList
from a sequence of JavaFX Shape
objects stored in a YAML file using the SnakeYAML Java library.
I wrote the following simple Java class for saving & loading LinkedList
objects to & from YAML files:
package com.example.test_1;
import javafx.stage.FileChooser;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.LinkedList;
public class YAML
{
static public LinkedList<Object> YAML_ListLoad()
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Load");
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("YAML File", "*.yaml"));
File file = fileChooser.showOpenDialog(App.MainStage);
if (file == null)
{
System.out.println("Unable to load YAML file because the entered directory is invalid!");
}
else
{
try
{
return YAML_ListRead(file.getAbsolutePath());
}
catch (IOException e)
{
System.out.println("Encountered error while loading YAML file!");
}
}
return null;
}
static public LinkedList<Object> YAML_ListRead(String filePath) throws IOException
{
Yaml yaml = new Yaml();
FileReader fileReader = new FileReader(filePath);
return yaml.load(fileReader);
}
static public boolean YAML_ListSave(LinkedList<Object> list)
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save");
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("YAML File", "*.yaml"));
fileChooser.setInitialFileName("file.yaml");
File file = fileChooser.showSaveDialog(App.MainStage);
if (file == null)
{
System.out.println("Unable to save YAML file because the entered directory is invalid!");
}
else
{
try
{
YAML_ListWrite(file.getAbsolutePath(), list);
return true;
}
catch (IOException e)
{
System.out.println("Encountered error while saving YAML file!");
}
}
return false;
}
static public void YAML_ListWrite(String filePath, LinkedList<Object> list) throws IOException
{
Yaml yaml = new Yaml();
FileWriter fileWriter = new FileWriter(filePath);
yaml.dump(list, fileWriter);
}
}
Where YAML_ListLoad
is a function that returns a LinkedList
of objects it loads from whichever file the user chooses using the FileChooser
, and YAML_ListSave
is a function that saves the provided LinkedList
to a YAML file that the user chooses.
The YAML class above is intended to be ran from the following testing functions:
public void Save()
{
LinkedList<Object> shapes = new LinkedList<Object>();
shapes.add(new Circle(5, Color.RED));
shapes.add(new Circle(10, Color.BLUE));
YAML.YAML_ListSave(shapes);
}
public void Load()
{
LinkedList<Object> shapes = YAML.YAML_ListLoad();
System.out.println(shapes);
}
Where Save
simply creates 2 JavaFX Circle
objects, and attempts to save them in a YAML file, and Load
simply tries to load a YAML file into a LinkedList
.
In the code above, the saving functions appear to work well. Running the Save
function in the runner excerpt above results in the following YAML file being created:
- !!javafx.scene.shape.Circle
accessibleHelp: null
accessibleRole: NODE
accessibleRoleDescription: null
accessibleText: null
blendMode: null
cache: false
cacheHint: DEFAULT
centerX: 0.0
centerY: 0.0
clip: null
cursor: null
depthTest: INHERIT
disable: false
effect: null
eventDispatcher: !!com.sun.javafx.scene.NodeEventDispatcher {}
fill: !!javafx.scene.paint.Color {}
focusTraversable: false
id: null
inputMethodRequests: null
layoutX: 0.0
layoutY: 0.0
managed: true
mouseTransparent: false
nodeOrientation: INHERIT
onContextMenuRequested: null
onDragDetected: null
onDragDone: null
onDragDropped: null
onDragEntered: null
onDragExited: null
onDragOver: null
onInputMethodTextChanged: null
onKeyPressed: null
onKeyReleased: null
onKeyTyped: null
onMouseClicked: null
onMouseDragEntered: null
onMouseDragExited: null
onMouseDragOver: null
onMouseDragReleased: null
onMouseDragged: null
onMouseEntered: null
onMouseExited: null
onMouseMoved: null
onMousePressed: null
onMouseReleased: null
onRotate: null
onRotationFinished: null
onRotationStarted: null
onScroll: null
onScrollFinished: null
onScrollStarted: null
onSwipeDown: null
onSwipeLeft: null
onSwipeRight: null
onSwipeUp: null
onTouchMoved: null
onTouchPressed: null
onTouchReleased: null
onTouchStationary: null
onZoom: null
onZoomFinished: null
onZoomStarted: null
opacity: 1.0
pickOnBounds: false
radius: 5.0
rotate: 0.0
rotationAxis: &id001 {}
scaleX: 1.0
scaleY: 1.0
scaleZ: 1.0
smooth: true
stroke: null
strokeDashOffset: 0.0
strokeLineCap: SQUARE
strokeLineJoin: MITER
strokeMiterLimit: 10.0
strokeType: CENTERED
strokeWidth: 1.0
style: ''
translateX: 0.0
translateY: 0.0
translateZ: 0.0
userData: null
viewOrder: 0.0
visible: true
- !!javafx.scene.shape.Circle
accessibleHelp: null
accessibleRole: NODE
accessibleRoleDescription: null
accessibleText: null
blendMode: null
cache: false
cacheHint: DEFAULT
centerX: 0.0
centerY: 0.0
clip: null
cursor: null
depthTest: INHERIT
disable: false
effect: null
eventDispatcher: !!com.sun.javafx.scene.NodeEventDispatcher {}
fill: !!javafx.scene.paint.Color {}
focusTraversable: false
id: null
inputMethodRequests: null
layoutX: 0.0
layoutY: 0.0
managed: true
mouseTransparent: false
nodeOrientation: INHERIT
onContextMenuRequested: null
onDragDetected: null
onDragDone: null
onDragDropped: null
onDragEntered: null
onDragExited: null
onDragOver: null
onInputMethodTextChanged: null
onKeyPressed: null
onKeyReleased: null
onKeyTyped: null
onMouseClicked: null
onMouseDragEntered: null
onMouseDragExited: null
onMouseDragOver: null
onMouseDragReleased: null
onMouseDragged: null
onMouseEntered: null
onMouseExited: null
onMouseMoved: null
onMousePressed: null
onMouseReleased: null
onRotate: null
onRotationFinished: null
onRotationStarted: null
onScroll: null
onScrollFinished: null
onScrollStarted: null
onSwipeDown: null
onSwipeLeft: null
onSwipeRight: null
onSwipeUp: null
onTouchMoved: null
onTouchPressed: null
onTouchReleased: null
onTouchStationary: null
onZoom: null
onZoomFinished: null
onZoomStarted: null
opacity: 1.0
pickOnBounds: false
radius: 10.0
rotate: 0.0
rotationAxis: *id001
scaleX: 1.0
scaleY: 1.0
scaleZ: 1.0
smooth: true
stroke: null
strokeDashOffset: 0.0
strokeLineCap: SQUARE
strokeLineJoin: MITER
strokeMiterLimit: 10.0
strokeType: CENTERED
strokeWidth: 1.0
style: ''
translateX: 0.0
translateY: 0.0
translateZ: 0.0
userData: null
viewOrder: 0.0
visible: true
However, attempting to load that created YAML file back into a LinkedList
by calling Save
results in many errors, including:
Caused by: Cannot create property=eventDispatcher for JavaBean=Circle[centerX=0.0, centerY=0.0, radius=0.0, fill=0x000000ff]
in 'reader', line 1, column 3:
- !!javafx.scene.shape.Circle
^
Can't construct a java object for tag:yaml.org,2002:com.sun.javafx.scene.NodeEventDispatcher; exception=java.lang.InstantiationException: NoSuchMethodException:com.sun.javafx.scene.NodeEventDispatcher.<init>()
in 'reader', line 16, column 20:
eventDispatcher: !!com.sun.javafx.scene.NodeEvent ...
^
in 'reader', line 16, column 20:
eventDispatcher: !!com.sun.javafx.scene.NodeEvent ...
^
Can't construct a java object for tag:yaml.org,2002:com.sun.javafx.scene.NodeEventDispatcher; exception=java.lang.InstantiationException: NoSuchMethodException:com.sun.javafx.scene.NodeEventDispatcher.<init>()
in 'reader', line 16, column 20:
eventDispatcher: !!com.sun.javafx.scene.NodeEvent ...
^
It would appear to me that the entries marked by !!
in the YAML file are treated as some sort of null
or empty entries. It appears that the YAML parser isn't able to parse those.
What would be the proper way of loading a sequence of JavaFX Shape
objects from a YAML file? Is it even possible to load JavaFX objects using SnakeYAML?
Thanks for reading my post, any guidance is appreciated.
An example of serializing and deserializing shapes based on a YAML file format.
It uses Jackson to assist with the marshaling operations. Jackson (currently) uses SnakeYAML as its YAML implementation, though that is completely transparent to the usage via the Jackson API.
It only allows a single shape type (a Circle
) but could be extended to handle other shapes without much difficulty.
App Output
Wrote: /var/folders/87/r96kpgbj5tjdpsgml9gb_g8c0000gn/T/shapes-4277733445362494936.yaml
---
- !<Circle>
centerX: 40.0
centerY: 50.0
radius: 20.0
fill: "BLUE"
- !<Circle>
centerX: 20.0
centerY: 20.0
radius: 10.0
fill: "RED"
Read: /var/folders/87/r96kpgbj5tjdpsgml9gb_g8c0000gn/T/shapes-4277733445362494936.yaml
[CircleModel[centerX=40.0, centerY=50.0, radius=20.0, fill=BLUE], CircleModel[centerX=20.0, centerY=20.0, radius=10.0, fill=RED]]
Alternate implementation using FXML
You could store the objects as FXML, then you can load them using the FXMLLoader
.
That may work well if you have some tool to create the shapes and export them in FXML format.
However, if you need to create an FXML file programmatically from model objects in your application, that might be a little difficult. I know of no easily available 3rd party library to do that. Jackson, similar to that used here for YAML, could create basic FXML files with a bit of additional programming. However, that is outside the scope of this answer.
Using an alternate data format (JSON or XML)
To use JSON as a serialization format, in the ShapeRepository
class, use:
private final JsonFactory serializationFactory = new JsonFactory();
instead of the YamlFactory
:
private final JsonFactory serializationFactory = new YAMLFactory();
Similarly for XML, see:
App Code
module-info.java
module com.example.persistentcirclesapp {
requires javafx.graphics;
requires com.fasterxml.jackson.dataformat.yaml;
requires com.fasterxml.jackson.databind;
exports com.example.persistentcirclesapp;
}
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>PersistentCirclesApp</artifactId>
<version>1.0-SNAPSHOT</version>
<name>PersistentCirclesApp</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>21.0.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.15.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.15.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
ShapeApplication.java
package com.example.persistentcirclesapp;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
public class ShapeApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
ShapeRepository shapeRepository = new ShapeRepository();
shapeRepository.save(
List.of(
new CircleModel(40, 50, 20, "BLUE"),
new CircleModel(20, 20, 10, "RED")
)
);
List<ShapeModel> shapes = shapeRepository.load();
ShapeRenderer shapeRenderer = new ShapeRenderer();
stage.setScene(new Scene(shapeRenderer.render(shapes)));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
ShapeModel.java
package com.example.persistentcirclesapp;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_OBJECT
)
@JsonSubTypes({
@JsonSubTypes.Type(value = CircleModel.class, name = "Circle")
})
public sealed interface ShapeModel permits CircleModel {}
CircleModel.java
package com.example.persistentcirclesapp;
public record CircleModel(
double centerX,
double centerY,
double radius,
String fill
) implements ShapeModel {}
ShapeRepository.java
package com.example.persistentcirclesapp;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class ShapeRepository {
private final Path shapeRepositoryPath;
private final JsonFactory serializationFactory = new YAMLFactory();
private final ObjectMapper mapper =
new ObjectMapper(serializationFactory);
private final CollectionType shapeCollectionType = mapper.getTypeFactory().constructCollectionType(
List.class, ShapeModel.class
);
private final ObjectWriter writer = new ObjectMapper(serializationFactory)
.writerFor(shapeCollectionType);
private final ObjectReader reader = new ObjectMapper(serializationFactory)
.readerFor(shapeCollectionType);
public ShapeRepository() throws IOException {
shapeRepositoryPath = Files.createTempFile("shapes-", ".yaml");
}
public ShapeRepository(Path shapeRepositoryPath) {
this.shapeRepositoryPath = shapeRepositoryPath;
}
public void save(List<ShapeModel> shapes) throws IOException {
writer.writeValue(
shapeRepositoryPath.toFile(),
shapes
);
System.out.println("Wrote: " + shapeRepositoryPath + "\n" + Files.readString(shapeRepositoryPath));
}
public List<ShapeModel> load() throws IOException {
List<ShapeModel> shapes = reader.readValue(
shapeRepositoryPath.toFile()
);
System.out.println("Read: " + shapeRepositoryPath + "\n" + shapes);
return shapes;
}
}
ShapeRenderer.java
package com.example.persistentcirclesapp;
import javafx.scene.layout.Pane;
import java.util.List;
public class ShapeRenderer {
private final ShapeFactory shapeFactory = new ShapeFactory();
public Pane render(List<ShapeModel> shapes) {
Pane pane = new Pane();
shapes.stream()
.map(shapeFactory::createShape)
.forEach(shape ->
pane.getChildren().add(shape)
);
return pane;
}
}
ShapeFactory.java
package com.example.persistentcirclesapp;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
public class ShapeFactory {
public Shape createShape(ShapeModel shapeModel) {
return switch(shapeModel) {
case CircleModel circleModel ->
new Circle(
circleModel.centerX(),
circleModel.centerY(),
circleModel.radius(),
Paint.valueOf(circleModel.fill())
);
};
}
public ShapeModel createModel(Shape shape) {
return switch(shape) {
case Circle circle ->
new CircleModel(
circle.getCenterX(),
circle.getCenterY(),
circle.getRadius(),
circle.getFill().toString()
);
default ->
throw new IllegalArgumentException("Unsupported shape type " + shape.getClass().getName());
};
}
}