diff --git a/ERROR_CODES.md b/ERROR_CODES.md index fdb25478..366a379e 100644 --- a/ERROR_CODES.md +++ b/ERROR_CODES.md @@ -143,8 +143,7 @@ public class NotAController { // Wrong, should be a controller or component - Runtime: ✅ - Annotation Processor: ✅ -This error is thrown when a method annotated with `@onKey` has more than one parameter or a parameter that is not of type -`KeyEvent`. +This error is thrown when a method annotated with `@onKey` has more than one parameter or a parameter that is not of type `KeyEvent`. ```java @@ -259,7 +258,7 @@ This can happen if the field isn't initialized. - Runtime: ✅ - Annotation Processor: ❌ -This error is thrown when a title is specified using a language key, but no resource bundle is provided using `@Resource` +This error is thrown when a title is specified using a language key, but no resource bundle is provided using `@Resource`. in the controller or component class. ## Routes @@ -359,6 +358,7 @@ This error is thrown when the framework fails to put a parameter value into a fi - Annotation Processor: ❌ This error is thrown when the framework fails to call the set method of a property field with the parameter value. +This can happen if the property is not initialized. ### 4002: `Field '*' annotated with @ParamsMap in class '*' is not of type Map.` @@ -450,6 +450,7 @@ public class MyController { - Annotation Processor: ❌ This error is thrown if the type of the parameter value does not match the type of the field. +This error also occurs if the expected type is a primitive type and the provided value is `null`. ### 4008: `Parameter '*' in method '*' in class '*' is of type '*' but the provided value is of type '*'.` diff --git a/docs/controller/4-parameters.md b/docs/controller/4-parameters.md index 07abf369..4fc7199f 100644 --- a/docs/controller/4-parameters.md +++ b/docs/controller/4-parameters.md @@ -10,7 +10,9 @@ the field will be injected with the value of the parameter before the controller 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`), the value will be set to 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. 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`. If the annotated field is final, @@ -53,7 +55,7 @@ public class FooController { } ``` -If a controller expects an argument but no argument with a suitable name is passed, `null` will be passed instead. +If a controller expects an argument but no argument with a suitable name is passed, `null` will be passed instead, except for fields which will be left unchanged ("default value"). Any arguments not expected by the controller will be ignored. If an argument is provided, but the type doesn't match the type of the field or method parameter, an exception will be diff --git a/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java b/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java index 3c8968e6..48316624 100644 --- a/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java +++ b/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java @@ -35,10 +35,7 @@ import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.reflect.*; import java.net.MalformedURLException; import java.net.URL; import java.util.*; @@ -464,29 +461,51 @@ private void callMethodsWithAnnotation(@NotNull Object instance, @NotNull Class< private void fillParametersIntoFields(@NotNull Object instance, @NotNull Map<@NotNull String, @Nullable Object> parameters) { // Fill the parameters into fields annotated with @Param for (Field field : Reflection.getAllFieldsWithAnnotation(instance.getClass(), Param.class).toList()) { + + Param paramAnnotation = field.getAnnotation(Param.class); + String param = paramAnnotation.value(); + + // Don't fill the parameter if it's not present (field will not be overwritten, "default value") + if (!parameters.containsKey(param)) continue; + + Class fieldType = field.getType(); try { - boolean accessible = field.canAccess(instance); field.setAccessible(true); + Object value = parameters.get(param); + Object fieldValue = field.get(instance); + // If the field is a WriteableValue, use the setValue method - if (WritableValue.class.isAssignableFrom(field.getType())) { - field.get(instance).getClass().getMethod("setValue", Object.class).invoke(field.get(instance), parameters.get(field.getAnnotation(Param.class).value())); - } else { - Object value = parameters.get(field.getAnnotation(Param.class).value()); - if (value == null) { // If the value is null, we don't need to check the type - field.set(instance, null); - } else if (Reflection.canBeAssigned(field.getType(), value)) { - field.set(instance, value); - } else { - throw new RuntimeException(error(4007).formatted(field.getAnnotation(Param.class).value(), field.getName(), instance.getClass().getName(), field.getType().getName(), value.getClass().getName())); + if (WritableValue.class.isAssignableFrom(fieldType) && !(value instanceof WritableValue)) { + + // We cannot call setValue on a non-existing property + if (fieldValue == null) { + throw new RuntimeException(error(4001).formatted(param, field.getName(), instance.getClass().getName())); } + + try { + // noinspection unchecked + ((WritableValue) field.get(instance)).setValue(value); + } catch (ClassCastException e) { + throw new RuntimeException(error(4007).formatted(param, field.getName(), instance.getClass().getName(), fieldType.getName(), value == null ? "null" : value.getClass().getName())); + } + } + + // If not, set the field's value directly + else if (value == null) { + // If the value is null and the field is a primitive, throw an error + if (fieldType.isPrimitive()) { + throw new RuntimeException(error(4007).formatted(param, field.getName(), instance.getClass().getName(), fieldType.getName(), "null")); + } + field.set(instance, null); // If the value is null and the field is not a primitive, no type check is necessary + } else if (Reflection.canBeAssigned(fieldType, value)) { + field.set(instance, value); // If the value is not null, we need a type check (respects primitive types) + } else { + throw new RuntimeException(error(4007).formatted(param, field.getName(), instance.getClass().getName(), fieldType.getName(), value.getClass().getName())); } - field.setAccessible(accessible); } catch (IllegalAccessException e) { - throw new RuntimeException(error(4000).formatted(field.getAnnotation(Param.class).value(), field.getName(), instance.getClass().getName()), e); - } catch (InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(error(4001).formatted(field.getAnnotation(Param.class).value(), field.getName(), instance.getClass().getName()), e); + throw new RuntimeException(error(4000).formatted(param, field.getName(), instance.getClass().getName()), e); } } @@ -498,10 +517,9 @@ private void fillParametersIntoFields(@NotNull Object instance, @NotNull Map<@No } try { - boolean accessible = field.canAccess(instance); field.setAccessible(true); - // Check if field is final + // If the map is final, clear it and put all parameters into it if (Modifier.isFinal(field.getModifiers())) { @SuppressWarnings("unchecked") Map map = (Map) field.get(instance); @@ -510,7 +528,6 @@ private void fillParametersIntoFields(@NotNull Object instance, @NotNull Map<@No } else { field.set(instance, parameters); } - field.setAccessible(accessible); } catch (IllegalAccessException e) { throw new RuntimeException(error(4010).formatted(field.getName(), instance.getClass().getName()), e); } diff --git a/framework/src/main/java/org/fulib/fx/util/reflection/Reflection.java b/framework/src/main/java/org/fulib/fx/util/reflection/Reflection.java index 30d93758..b0af44f9 100644 --- a/framework/src/main/java/org/fulib/fx/util/reflection/Reflection.java +++ b/framework/src/main/java/org/fulib/fx/util/reflection/Reflection.java @@ -54,10 +54,8 @@ public static Stream getAllFieldsWithAnnotation(Class clazz, Class fields, Consumer method) { for (Field field : fields) { try { - boolean accessible = field.canAccess(instance); field.setAccessible(true); Object component = field.get(instance); - field.setAccessible(accessible); method.accept(component); } catch (IllegalAccessException e) { throw new RuntimeException(error(9000).formatted(field.getName(), instance.getClass().getName()), e); diff --git a/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java b/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java index 09b9b7a8..c6a8609d 100644 --- a/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java +++ b/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java @@ -1,7 +1,10 @@ package org.fulib.fx.app; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import org.fulib.fx.FulibFxApp; +import org.fulib.fx.app.controller.InvalidParamController; import org.fulib.fx.app.controller.types.BasicComponent; import org.fulib.fx.app.controller.ParamController; import org.fulib.fx.app.controller.TitleController; @@ -186,11 +189,13 @@ public void modalTest() { @Test public void params() { ParamController controller = new ParamController(); + StringProperty property = new SimpleStringProperty("string"); Map params = Map.of( "integer", 1, "string", "string", "character", 'a', - "bool", true + "bool", true, + "property", property ); runAndWait(() -> app.show(controller, params)); @@ -200,12 +205,35 @@ public void params() { assertEquals("string", controller.fieldPropertyParamProperty().get()); - assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true), controller.getOnInitParamsMap()); - assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true), controller.getSetterParamsMap()); - assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true), controller.getFieldParamsMap()); + assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true, "property", property), controller.getOnInitParamsMap()); + assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true, "property", property), controller.getSetterParamsMap()); + assertEquals(Map.of("integer", 1, "string", "string", "character", 'a', "bool", true, "property", property), controller.getFieldParamsMap()); assertEquals('a', controller.getSetterMultiParams1()); assertEquals(true, controller.getSetterMultiParams2()); + + assertEquals(property, controller.stringPropertyProperty()); + + runAndWait(() -> + assertThrows( + RuntimeException.class, // Fails because the field is of type Integer, but a String is provided + () -> app.show(new InvalidParamController(), Map.of("one", "string")) + ) + ); + + runAndWait(() -> + assertThrows( + RuntimeException.class, // Fails because the property expects an Integer, but a String is provided + () -> app.show(new InvalidParamController(), Map.of("two", "123")) + ) + ); + + runAndWait(() -> + assertThrows( + RuntimeException.class, // Fails because the property is null + () -> app.show(new InvalidParamController(), Map.of("three", 123)) + ) + ); } @Test diff --git a/framework/src/test/java/org/fulib/fx/app/controller/InvalidParamController.java b/framework/src/test/java/org/fulib/fx/app/controller/InvalidParamController.java new file mode 100644 index 00000000..800316b9 --- /dev/null +++ b/framework/src/test/java/org/fulib/fx/app/controller/InvalidParamController.java @@ -0,0 +1,26 @@ +package org.fulib.fx.app.controller; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.scene.layout.VBox; +import org.fulib.fx.annotation.controller.Controller; +import org.fulib.fx.annotation.param.Param; + +@Controller(view = "#render") +public class InvalidParamController { + + @Param("one") + private Integer integer; + + @Param("two") + private IntegerProperty integerProperty = new SimpleIntegerProperty(); + + @Param("three") + private IntegerProperty integerProperty2; + + public VBox render() { + return new VBox(); + } + + +} diff --git a/framework/src/test/java/org/fulib/fx/app/controller/ParamController.java b/framework/src/test/java/org/fulib/fx/app/controller/ParamController.java index 6b6143c1..cb5ce8c6 100644 --- a/framework/src/test/java/org/fulib/fx/app/controller/ParamController.java +++ b/framework/src/test/java/org/fulib/fx/app/controller/ParamController.java @@ -28,6 +28,9 @@ public class ParamController { @ParamsMap private Map fieldParamsMap; + @Param("property") + private StringProperty stringProperty; + private Character setterMultiParams1; private Boolean setterMultiParams2; @@ -73,10 +76,6 @@ public int getFieldParam() { return fieldParam; } - public String getFieldPropertyParam() { - return fieldPropertyParam.get(); - } - public StringProperty fieldPropertyParamProperty() { return fieldPropertyParam; } @@ -101,4 +100,7 @@ public Boolean getSetterMultiParams2() { return setterMultiParams2; } + public StringProperty stringPropertyProperty() { + return stringProperty; + } }