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

Keep default values for fields if parameter isn't provided #71

Merged
merged 13 commits into from
Mar 24, 2024
Merged
7 changes: 4 additions & 3 deletions ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String, Object>.`

Expand Down Expand Up @@ -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 '*'.`

Expand Down
6 changes: 4 additions & 2 deletions docs/controller/4-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object>`. If the annotated field is final,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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<Object>) 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);
}
}

Expand All @@ -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")
Clashsoft marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> map = (Map<String, Object>) field.get(instance);
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,8 @@ public static Stream<Field> getAllFieldsWithAnnotation(Class<?> clazz, Class<? e
public static void callMethodsForFieldInstances(Object instance, Collection<Field> fields, Consumer<Object> 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);
Expand Down
36 changes: 32 additions & 4 deletions framework/src/test/java/org/fulib/fx/app/FrameworkTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -186,11 +189,13 @@ public void modalTest() {
@Test
public void params() {
ParamController controller = new ParamController();
StringProperty property = new SimpleStringProperty("string");
Map<String, Object> params = Map.of(
"integer", 1,
"string", "string",
"character", 'a',
"bool", true
"bool", true,
"property", property
);
runAndWait(() -> app.show(controller, params));

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class ParamController {
@ParamsMap
private Map<String, Object> fieldParamsMap;

@Param("property")
private StringProperty stringProperty;

private Character setterMultiParams1;
private Boolean setterMultiParams2;

Expand Down Expand Up @@ -73,10 +76,6 @@ public int getFieldParam() {
return fieldParam;
}

public String getFieldPropertyParam() {
return fieldPropertyParam.get();
}

public StringProperty fieldPropertyParamProperty() {
return fieldPropertyParam;
}
Expand All @@ -101,4 +100,7 @@ public Boolean getSetterMultiParams2() {
return setterMultiParams2;
}

public StringProperty stringPropertyProperty() {
return stringProperty;
}
}