Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Param method option #120

Merged
merged 10 commits into from
Jul 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
import org.fulib.fx.util.ControllerUtil;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
Expand All @@ -30,16 +27,6 @@ public class FxClassGenerator {
private final ProcessingEnvironment processingEnv;
private final ProcessingHelper helper;

/**
* The (erased) `javafx.beans.value.WritableValue` type.
*/
private final TypeMirror writableValue;

/**
* The `setValue` method of `javafx.beans.value.WritableValue`.
*/
private final ExecutableElement genericSetValue;

/**
* The `javafx.scene.Parent` type.
*/
Expand All @@ -53,17 +40,6 @@ public FxClassGenerator(ProcessingHelper helper, ProcessingEnvironment processin
this.helper = helper;
this.processingEnv = processingEnv;

final TypeElement writableValue = processingEnv.getElementUtils().getTypeElement("javafx.beans.value.WritableValue");
this.writableValue = processingEnv.getTypeUtils().erasure(writableValue.asType());

genericSetValue = writableValue.getEnclosedElements()
.stream()
.filter(e -> e instanceof ExecutableElement)
.map(e -> (ExecutableElement) e)
.filter(e -> "setValue".equals(e.getSimpleName().toString()))
.findFirst()
.orElseThrow();

parent = processingEnv.getElementUtils().getTypeElement("javafx.scene.Parent").asType();
pane = processingEnv.getElementUtils().getTypeElement("javafx.scene.layout.Pane").asType();
}
Expand Down Expand Up @@ -154,29 +130,20 @@ private void generateParametersIntoFields(PrintWriter out, TypeElement component
out.printf(" if (params.containsKey(%s)) {%n", paramNameLiteral);

// TODO field must be public, package-private or protected -- add a diagnostic if it's private
if (processingEnv.getTypeUtils().isAssignable(field.asType(), writableValue)) {
// We use the `setValue` method to infer the actual type of the field,
// E.g. if the field is a `StringProperty` which extends `WritableValue<String>`,
// we can infer that the actual type is `String`.
final ExecutableType asMemberOf = (ExecutableType) processingEnv.getTypeUtils().asMemberOf((DeclaredType) field.asType(), genericSetValue);
final TypeMirror typeArg = asMemberOf.getParameterTypes().get(0);
final String writableType = typeArg.toString();
if (field.getModifiers().contains(Modifier.FINAL)) {
out.printf(" instance.%s.setValue((%s) params.get(%s));%n", fieldName, writableType, paramNameLiteral);
} else {
// final Object param = params.get(<paramNameLiteral>);
// if (param instanceof <writableValue>) {
// instance.<fieldName> = (<fieldType>) param);
// } else {
// instance.<fieldName>.setValue((<writableType>) param);
// }
out.printf(" final Object param = params.get(%s);%n", paramNameLiteral);
out.printf(" if (param instanceof %s) {%n", writableValue);
out.printf(" instance.%s = (%s) param;%n", fieldName, fieldType);
out.println(" } else {");
out.printf(" instance.%s.setValue((%s) param);%n", fieldName, writableType);
out.println(" }");
}
final String methodName = param.method();
if (methodName != null && !methodName.isEmpty()) {
// this is some hacky way to get the @Param().type().
// param.type() does not work because we cannot access the Class<?> instance at compile time.
// So we have to read the AnnotationMirror.
final AnnotationMirror paramMirror = field.getAnnotationMirrors().stream()
.filter(a -> "org.fulib.fx.annotation.param.Param".equals(a.getAnnotationType().toString()))
.findFirst().orElseThrow();
final String methodParamType = paramMirror.getElementValues().values().stream()
.map(Object::toString)
.filter(s -> s.endsWith(".class"))
.map(s -> s.substring(0, s.length() - 6)) // remove ".class"
.findFirst().orElse("Object"); // else case also happens when type is not specified in the annotation (default Object)
out.printf(" instance.%s.%s((%s) params.get(%s));%n", fieldName, methodName, methodParamType, paramNameLiteral);
} else {
out.printf(" instance.%s = (%s) params.get(%s);%n", fieldName, fieldType, paramNameLiteral);
}
Expand Down
21 changes: 9 additions & 12 deletions docs/controller/4-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@ To pass parameters to a controller, an additional argument can be provided to th
strings and objects. The strings specify the argument's name and the objects are the value of the argument. For example,
`show("/route/to/controller", Map.of("key", value, "key2", value2))` will pass the value `value` to the argument `key`.

To use a passed argument in a field or method, you have to annotate it with `@Param("key")`. The name of the parameter
will be used to match it to the map of parameters passed to the `show()` method. If the annotation is used on a field,
the field will be injected with the value of the parameter before the controller is initialized. If the annotation is used
on a method, the method will be called with the value of the parameter before the controller is initialized. If the
annotation is used on a method parameter of a render/init method, the method will be called with the value of the parameter.

If `@Param` is used on a field containing a `WriteableValue` (e.g. a `StringProperty`), its value will be set to the
parameter's value if the parameter has the correct type (e.g. a `String` for a `StringProperty`). If the parameter is
a `WritableValue` as well, the logic will be the same as for a normal field.
To use a passed argument in a field or method, you have to annotate it with `@Param("key")`.
The name of the parameter will be used to match it to the map of parameters passed to the `show()` method.
If the annotation is used on a field, the field will be injected with the value of the parameter before the controller is initialized.
If the annotation is used on a method, the method will be called with the value of the parameter before the controller is initialized.
If the annotation is used on a method parameter of a render/init method, the method will be called with the value of the parameter.

Instead of accessing the parameters one by one, you can also use the `@ParamsMap` annotation to inject a map of all parameters.
This annotation can be used for fields and method parameters of type `Map<String, Object>`. If the annotated field is final,
Expand All @@ -34,9 +30,10 @@ public class FooController {
@Param("baba")
private Bar bar;

// The set method of the writable value will be called with the parameter 'fofo'
@Param("fofo")
private ObjectProperty<Foo> foo = new SimpleObjectProperty<>();
// The setValue(T) method of the ObjectProperty will be called with the parameter named 'fofo'
// Note that the erased parameter type of ObjectProperty.setValue is Object, so we specify that as the type.
@Param(value = "fofo", method = "setValue", type = Object.class)
private final ObjectProperty<Foo> foo = new SimpleObjectProperty<>();

// This field will be injected with a map of all parameters before the controller is initialized
@ParamsMap
Expand Down
31 changes: 24 additions & 7 deletions docs/tutorial/data-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,36 @@ Instead of defining a Runnable or Consumer directly, you can create a Property a
```java
@Component
public class MyComponent {

@Param("colorChange")
// option 1: The subcomponent reuses the parent's property instance.
@Param("color")
ObjectProperty<Color> color;

@Param("colorBind")
// As this field is final, bind() will be used instead
final ObjectProperty<Color> otherColor = new SimpleObjectProperty<>();
// Option 2: The subcomponent uses its own property instance and binds it to the parent's property.
// Writing to this property will NOT update the parent's property.
// We have to unbind using destroy.
@Param(value = "color", method = "bind", type = Object.class)
final ObjectProperty<Color> colorBind = new SimpleObjectProperty<>();

// Option 3: The subcomponent uses its own property instance and binds it to the parent's property bidirectionally.
// Writing to this property will update the parent's property.
// We have to manually bind using subscriber to allow for unbinding.
final ObjectProperty<Color> colorBindBidi = new SimpleObjectProperty<>();
Subscriber subscriber = ...;

@OnInit
void init(@Param("color") ObjectProperty<Color> color) {
subscriber.bindBidirectional(colorBindBidi, color);
}

void onButtonClicked() {
Color newColor = ...;
color.set(newColor);
}

@OnDestroy
void destroy() {
colorBind.unbind();
}
}
```

Expand All @@ -143,8 +161,7 @@ public class MyController {
@OnRender
void createSubs() {
ObjectProperty<Color> color = new SimpleObjectProperty(Color.WHITE);
ObjectProperty<Color> colorBind = new SimpleObjectProperty(Color.WHITE);
MyComponent component = app.initAndRender(new MyComponent(), Map.of("colorChange", color, "colorBind", colorBind));
MyComponent component = app.initAndRender(new MyComponent(), Map.of("color", color));
subscriber.listen(color, (observable, oldValue, newValue) -> { // Use subscribers to prevent memory leaks
System.out.println(newValue);
});
Expand Down
12 changes: 12 additions & 0 deletions framework/src/main/java/org/fulib/fx/annotation/param/Param.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@
*/
String value();

/**
* The method of the field's class that will be called with the parameter's value.
* Useful for {@code final} fields or {@link javafx.beans.property.Property} fields.
* @return the method name
*/
String method() default "";

/**
* When using {@link #method()}, this specifies the type of the first (and only) parameter of that method.
* @return the type of the method parameter
*/
Class<?> type() default Object.class;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.fulib.fx.controller.internal;

import javafx.beans.value.WritableValue;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
Expand Down Expand Up @@ -146,17 +145,18 @@ private void fillParametersIntoFields(@NotNull Object instance, @NotNull Map<@No
Object value = parameters.get(param);
Object fieldValue = field.get(instance);

// If the field is a WriteableValue, use the setValue method
if (WritableValue.class.isAssignableFrom(fieldType) && !(value instanceof WritableValue)) {
String method = paramAnnotation.method();
if (method != null && !method.isEmpty()) {

// We cannot call setValue on a non-existing property
// We cannot call the method on a non-existing property
if (fieldValue == null) {
throw new RuntimeException(error(4001).formatted(param, field.getName(), instance.getClass().getName()));
}

try {
((WritableValue<Object>) field.get(instance)).setValue(value);
} catch (ClassCastException e) {
final Method methodName = field.getType().getMethod(method, paramAnnotation.type());
methodName.invoke(fieldValue, value);
} catch (ClassCastException | ReflectiveOperationException e) {
throw new RuntimeException(error(4007).formatted(param, field.getName(), instance.getClass().getName(), fieldType.getName(), value == null ? "null" : value.getClass().getName()));
Clashsoft marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class ParamController {
private String setterParam;
@Param("integer")
int fieldParam;
@Param("string")
@Param(value = "string", method = "set", type = String.class)
final StringProperty fieldPropertyParam = new SimpleStringProperty();

private Map<String, Object> onInitParamsMap;
Expand Down