I have the following class that uses fluent setters in order to allow concatenation:
/**
* Data that affects the way an animation is played.
*/
public class AnimationSettings {
private Duration duration = Duration.seconds(1);
private Curve curve = Curve.LINEAR;
/**
* @param duration duration of the animation to set
* @return this for concatenation
*/
public AnimationSettings withDuration(Duration duration) {
this.duration = duration;
return this;
}
/**
* @return duration of the animation
*/
public Duration getDuration() {
return duration;
}
/**
* @param curve curve of the animation to set
* @return this for concatenation
*/
public AnimationSettings withCurve(Curve curve) {
this.curve = curve;
return this;
}
/**
* @return curve of the animation
*/
public Curve getCurve() {
return curve;
}
}
What I would like to achieve is creating a new instance and set those values from an FXML file, like this:
<AnimatedNode>
<settings>
<AnimationSettings duration="900ms" curve="LINEAR"/>
</settings>
</AnimatedNode>
However, an error is given as the attributes duration
and curve
can't be found.
After some investigation, I realized an FXML attribute should be linked to both a getter and setter, strictly named getX
and setX
, so my withX
is not seen as a setter and the attribute cannot be set.
I was wondering if there is some annotation I am not aware of that tells JavaFX that a method should be treated as a setter, or some other way, as I think renaming those methods to setX
would lose the 'fluent' feel.
I made this work with a custom Builder. It's probably more effort than it is worth. I would recommend just implementing both withCurve()
and setCurve()
in your AnimationSettings
class.
However, for demo purposes I added
package org.jamesd.examples.buildertest;
public enum Curve {
LINEAR
}
Left AnimationSettings
as-is:
package org.jamesd.examples.buildertest;
public class AnimationSettings {
private Duration duration = Duration.seconds(1);
private Curve curve = Curve.LINEAR;
/**
* @param duration duration of the animation to set
* @return this for concatenation
*/
public AnimationSettings withDuration(Duration duration) {
this.duration = duration;
return this;
}
/**
* @return duration of the animation
*/
public Duration getDuration() {
return duration;
}
/**
* @param curve curve of the animation to set
* @return this for concatenation
*/
public AnimationSettings withCurve(Curve curve) {
this.curve = curve;
return this;
}
/**
* @return curve of the animation
*/
public Curve getCurve() {
return curve;
}
}
Then I created a builder for AnimationSettings
. This is based on the current implementation of JavaFXFontBuilder
. This implementation makes it a Map<String, Object>
implementation, with the put(...)
method accepting the name of a property and its value. You can also implement this using Java Bean style setCurve(...)
and setDuration(...)
methods.
package org.jamesd.examples.buildertest;
import javafx.util.Builder;
import javafx.util.Duration;
import java.util.AbstractMap;
import java.util.Set;
public class AnimationSettingsBuilder extends AbstractMap<String, Object> implements Builder<AnimationSettings> {
private Duration duration = Duration.seconds(1);
private Curve curve = Curve.LINEAR;
@Override
public AnimationSettings build() {
return new AnimationSettings()
.withCurve(curve)
.withDuration(duration);
}
@Override
public Object put(String key, Object value) {
if ("duration".equals(key)) {
// can parse units here if you want more flexibility, but for simplicity:
duration = Duration.seconds(Double.parseDouble((String)value));
} else if ("curve".equals(key)) {
curve = Curve.valueOf((String)value);
}
return null;
}
@Override
public boolean containsKey(Object key) {
return false; // False in this context means that the property is NOT read only
}
@Override
public Object get(Object key) {
return null; // In certain cases, get is also required to return null for read-write "properties"
}
@Override
public Set<Entry<String, Object>> entrySet() {
return null;
}
}
And then finally a builder factory. The builder factory has to implement the getBuilder(...)
method. This just wraps a default implementation, returns the builder defined above to build AnimationSettings
instances, and delegates to the default builder factory for other classes.
package org.jamesd.examples.buildertest;
import javafx.fxml.JavaFXBuilderFactory;
import javafx.util.Builder;
import javafx.util.BuilderFactory;
public class AnimationSettingsBuilderFactory implements BuilderFactory {
private final BuilderFactory defaultBuilderFactory = new JavaFXBuilderFactory();
@Override
public Builder<?> getBuilder(Class<?> type) {
if (type == AnimationSettings.class) {
return new AnimationSettingsBuilder();
}
return defaultBuilderFactory.getBuilder(type);
}
}
For simple testing, here is Test.fxml
:
<?xml version="1.0" encoding="UTF-8"?>
<?import org.jamesd.examples.buildertest.AnimationSettings?>
<AnimationSettings xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
duration="2.0" curve="LINEAR">
</AnimationSettings>
and Test.java
:
package org.jamesd.examples.buildertest;
import javafx.fxml.FXMLLoader;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
FXMLLoader loader = new FXMLLoader(Test.class.getResource("Test.fxml"));
loader.setBuilderFactory(new AnimationSettingsBuilderFactory());
AnimationSettings settings = loader.load();
System.out.println("Duration: "+settings.getDuration());
System.out.println("Curve: "+settings.getCurve());
}
}