diff --git a/ERROR_CODES.md b/ERROR_CODES.md index ea6bab1..ffed2f3 100644 --- a/ERROR_CODES.md +++ b/ERROR_CODES.md @@ -341,7 +341,7 @@ This error is thrown if the framework tries to register a field as a route provi with `@Route`. This should never happen if the framework is used correctly. -### 3002: `Route '*' already leads to '*' but was tried to be registered for '*'.` +### 3002: `Route '*' already leads to a controller/component of type '*'.` - Runtime: ✅ - Annotation Processor: ❌ @@ -376,7 +376,7 @@ public class Routes { ### 3004: `Field '*' in class '*' is not a valid provider field.` -- Runtime: ✅ +- Runtime: ❌ - Annotation Processor: ✅ This error is thrown when a field annotated with `@Route` is not a `Provider`. diff --git a/framework/src/main/java/org/fulib/fx/FulibFxApp.java b/framework/src/main/java/org/fulib/fx/FulibFxApp.java index 1cfbf0a..5e107ba 100644 --- a/framework/src/main/java/org/fulib/fx/FulibFxApp.java +++ b/framework/src/main/java/org/fulib/fx/FulibFxApp.java @@ -147,7 +147,7 @@ public static void setResourcesPath(@NotNull Path path) { */ @SuppressWarnings("unchecked") public @NotNull T initAndRender(@NotNull String route, @NotNull Map<@NotNull String, @Nullable Object> params, @Nullable DisposableContainer onDestroy) { - Object component = this.frameworkComponent.router().getController(route); + Object component = this.frameworkComponent.router().getRoute(route); if (!ControllerUtil.isComponent(component)) { throw new IllegalArgumentException(error(1000).formatted(component.getClass().getName())); } 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 c6a6ac5..880d146 100644 --- a/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java +++ b/framework/src/main/java/org/fulib/fx/controller/ControllerManager.java @@ -15,7 +15,6 @@ import org.fulib.fx.annotation.event.OnKey; import org.fulib.fx.annotation.event.OnRender; import org.fulib.fx.controller.building.ControllerBuildFactory; -import org.fulib.fx.controller.exception.IllegalControllerException; import org.fulib.fx.controller.internal.FxSidecar; import org.fulib.fx.controller.internal.ReflectionSidecar; import org.fulib.fx.data.disposable.RefreshableCompositeDisposable; @@ -117,7 +116,7 @@ public void init(@NotNull Object instance, @NotNull Map<@NotNull String, @Nullab // Check if the instance is a controller if (!ControllerUtil.isControllerOrComponent(instance)) { - throw new IllegalControllerException(error(1001).formatted(instance.getClass().getName())); + throw new RuntimeException(error(1001).formatted(instance.getClass().getName())); } getSidecar(instance).init(instance, parameters); diff --git a/framework/src/main/java/org/fulib/fx/controller/Router.java b/framework/src/main/java/org/fulib/fx/controller/Router.java index 8fe86c2..90c42c4 100644 --- a/framework/src/main/java/org/fulib/fx/controller/Router.java +++ b/framework/src/main/java/org/fulib/fx/controller/Router.java @@ -4,16 +4,9 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.util.Pair; -import org.fulib.fx.FulibFxApp; import org.fulib.fx.annotation.Route; -import org.fulib.fx.annotation.controller.Component; -import org.fulib.fx.annotation.controller.Controller; -import org.fulib.fx.controller.exception.ControllerDuplicatedRouteException; -import org.fulib.fx.controller.exception.ControllerInvalidRouteException; import org.fulib.fx.data.*; import org.fulib.fx.util.ControllerUtil; -import org.fulib.fx.util.FrameworkUtil; -import org.fulib.fx.util.ReflectionUtil; import org.fulib.fx.util.reflection.Reflection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,8 +24,8 @@ @Singleton public class Router { - private final TraversableTree routes; - private final SizeableTraversableQueue, Object>, Map>> history; + private final TraversableTree> routes; + private final SizeableTraversableQueue>, Object>, Map>> history; @Inject Lazy manager; @@ -60,6 +53,15 @@ public void registerRoutes(@NotNull Object routes) { Reflection.getFieldsWithAnnotation(routes.getClass(), Route.class).forEach(this::registerRoute); } + /** + * Checks if a router class has been registered already. + * + * @return True if a router class has been registered already. + */ + public boolean routesRegistered() { + return this.routerObject != null; + } + /** * Registers a field as a route. @@ -79,13 +81,53 @@ private void registerRoute(@NotNull Field field) { Route annotation = field.getAnnotation(Route.class); String route = annotation.value().equals("$name") ? "/" + field.getName() : annotation.value(); + try { + field.setAccessible(true); + Provider provider = (Provider) field.get(routerObject); + registerRoute(route, provider); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + + /** + * Registers a route with the given provider. + * When adding a route, the route has to be unique, otherwise an exception will be thrown. + *

+ * The route has to start with a slash, otherwise it will be added automatically. + *

+ * This method doesn't check if the provider provides a valid controller or component. + * + * @param route The route to register + * @param provider The provider to register + * @throws RuntimeException If the route is already registered + */ + public void registerRoute(@NotNull String route, @NotNull Provider provider) { // Make sure the route starts with a slash to prevent issues with the traversal route = route.startsWith("/") ? route : "/" + route; + checkDuplicatedRoute(route, provider); + + this.routes.insert(route, provider); + + } + + private void checkDuplicatedRoute(String route, Provider provider) { if (this.routes.containsPath(route)) { - throw new ControllerDuplicatedRouteException(route, field.getType(), this.routes.get(route).getType()); + Object oldController = this.routes.get(route).get(); + throw new RuntimeException(error(3002).formatted(route, oldController == null ? "null" : oldController.getClass().getName())); + } + } + + private void checkContainsRoute(String route) { + if (!this.routes.containsPath(route)) { + String message = error(3005).formatted(route); + if (this.routes.containsPath("/" + route)) { + message += " " + note(3005).formatted("/" + route); + } + throw new RuntimeException(message); } - this.routes.insert(route, field); } /** @@ -95,35 +137,27 @@ private void registerRoute(@NotNull Field field) { * @param route The route of the controller * @param parameters The parameters to pass to the controller * @return A pair containing the controller instance and the rendered parent (will be the same if the controller is a component) - * @throws ControllerInvalidRouteException If the route couldn't be found + * @throws RuntimeException If the route couldn't be found */ public @NotNull Pair renderRoute(@NotNull String route, @NotNull Map<@NotNull String, @Nullable Object> parameters) { // Check if the route exists and has a valid controller - if (!this.routes.containsPath(route)) { - String message = error(3005).formatted(route); - if (this.routes.containsPath("/" + route)) { - message += " " + note(3005).formatted("/" + route); - } - throw new ControllerInvalidRouteException(message); - } + checkContainsRoute(route); // Get the provider and the controller class - Field provider = this.routes.traverse(route); - TraversableNodeTree.Node node = ((TraversableNodeTree) this.routes).currentNode(); + Provider provider = this.routes.traverse(route); + TraversableNodeTree.Node> node = ((TraversableNodeTree>) this.routes).currentNode(); // Since we visited this route with the given parameters, we can add it to the history this.addToHistory(new Pair<>(Either.left(node), parameters)); - Class controllerClass = ReflectionUtil.getProvidedClass(Objects.requireNonNull(provider)); - // Check if the provider is providing a valid controller/component - if (controllerClass == null) { - throw new RuntimeException(error(3004).formatted(provider.getName(), routerObject.getClass().getName())); - } - if (!controllerClass.isAnnotationPresent(Controller.class) && !controllerClass.isAnnotationPresent(Component.class)) { + // Get the instance of the controller + Object controllerInstance = provider.get(); + Class controllerClass = controllerInstance.getClass(); + + if (!ControllerUtil.isControllerOrComponent(controllerClass)) { throw new RuntimeException(error(1001).formatted(controllerClass.getName())); } - // Get the instance of the controller - Object controllerInstance = ReflectionUtil.getInstanceOfProviderField(provider, this.routerObject); + Node renderedNode = this.manager.get().initAndRender(controllerInstance, parameters); if (renderedNode instanceof Parent parent) { @@ -134,18 +168,35 @@ private void registerRoute(@NotNull Field field) { } /** - * Returns the controller with the given route without initializing and rendering it. + * Returns the controller or component with the given route without initializing and rendering it. * The route will be seen as absolute, meaning it will be treated as a full path. * * @param route The route of the controller * @return The controller instance */ - public Object getController(String route) { - Field provider = this.routes.get(route.startsWith("/") ? route : "/" + route); - return ReflectionUtil.getInstanceOfProviderField(provider, this.routerObject); + public Object getRoute(String route) { + String absoluteRoute = absolute(route); + checkContainsRoute(absoluteRoute); + Provider provider = this.routes.get(absoluteRoute); + return provider.get(); + } + + private String absolute(String route) { + return route.startsWith("/") ? route : "/" + route; + } + + /** + * Returns whether the router contains the given route. + * The route will be seen as absolute, meaning it will be treated as a full path. + * + * @param route The route to check + * @return True if the route exists + */ + public boolean containsRoute(String route) { + return this.routes.containsPath(absolute(route)); } - public void addToHistory(Pair, Object>, Map> pair) { + public void addToHistory(Pair>, Object>, Map> pair) { this.history.insert(pair); } @@ -177,12 +228,12 @@ public Pair forward() { } } - private Pair navigate(Pair, Object>, Map> pair) { + private Pair navigate(Pair>, Object>, Map> pair) { var either = pair.getKey(); - either.getLeft().ifPresent(node -> ((TraversableNodeTree) routes).setCurrentNode(node)); // If the history contains a route, set it as the current node + either.getLeft().ifPresent(node -> ((TraversableNodeTree>) routes).setCurrentNode(node)); // If the history contains a route, set it as the current node Object controller = either.isLeft() ? - ReflectionUtil.getInstanceOfProviderField(either.getLeft().orElseThrow().value(), this.routerObject) : // Get the controller instance from the provider + Objects.requireNonNull(either.getLeft().orElseThrow().value()).get() : // Get the controller instance from the provider either.getRight().orElseThrow(); // Get the controller instance from the history this.manager.get().cleanup(); // Cleanup the current controller @@ -201,17 +252,10 @@ private Pair navigate(Pair, * @return The current controller object and its parameters */ public Pair> current() { - Either, Object> either = this.history.current().getKey(); + Either>, Object> either = this.history.current().getKey(); return new Pair<>( either.isLeft() ? - either.getLeft().map(node -> { - try { - Objects.requireNonNull(node.value()).setAccessible(true); - return ((Provider) Objects.requireNonNull(node.value()).get(routerObject)).get(); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - }).orElseThrow() : + either.getLeft().map(node -> ((Provider) Objects.requireNonNull(node.value()).get())).orElseThrow() : either.getRight().orElseThrow(), this.history.current().getValue() ); @@ -226,4 +270,10 @@ public Pair> current() { public void setHistorySize(int size) { this.history.setSize(size); } + + + @Override + public String toString() { + return "Router(\n" + this.routes + "\n)"; + } } diff --git a/framework/src/main/java/org/fulib/fx/controller/exception/ControllerDuplicatedRouteException.java b/framework/src/main/java/org/fulib/fx/controller/exception/ControllerDuplicatedRouteException.java deleted file mode 100644 index d3c78a2..0000000 --- a/framework/src/main/java/org/fulib/fx/controller/exception/ControllerDuplicatedRouteException.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.fulib.fx.controller.exception; - -import static org.fulib.fx.util.FrameworkUtil.error; - -/** - * Exception thrown if a route has been added to the router twice. - */ -public class ControllerDuplicatedRouteException extends RuntimeException { - - public ControllerDuplicatedRouteException(String route, Class oldController, Class newController) { - super(error(3002).formatted(route, oldController.getName(), newController.getName())); - } -} diff --git a/framework/src/main/java/org/fulib/fx/controller/exception/ControllerInvalidRouteException.java b/framework/src/main/java/org/fulib/fx/controller/exception/ControllerInvalidRouteException.java deleted file mode 100644 index 8d6e9dc..0000000 --- a/framework/src/main/java/org/fulib/fx/controller/exception/ControllerInvalidRouteException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.fulib.fx.controller.exception; - -/** - * Thrown when navigating to an invalid route. - */ -public class ControllerInvalidRouteException extends RuntimeException { - - public ControllerInvalidRouteException(String message) { - super(message); - } - -} diff --git a/framework/src/main/java/org/fulib/fx/controller/exception/IllegalControllerException.java b/framework/src/main/java/org/fulib/fx/controller/exception/IllegalControllerException.java deleted file mode 100644 index 5cd7fe7..0000000 --- a/framework/src/main/java/org/fulib/fx/controller/exception/IllegalControllerException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.fulib.fx.controller.exception; - -public class IllegalControllerException extends RuntimeException { - public IllegalControllerException(String message) { - super(message); - } -} diff --git a/framework/src/main/java/org/fulib/fx/controller/exception/InvalidRouteFieldException.java b/framework/src/main/java/org/fulib/fx/controller/exception/InvalidRouteFieldException.java deleted file mode 100644 index f61a489..0000000 --- a/framework/src/main/java/org/fulib/fx/controller/exception/InvalidRouteFieldException.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.fulib.fx.controller.exception; - -import java.lang.reflect.Field; - -import static org.fulib.fx.util.FrameworkUtil.error; - -public class InvalidRouteFieldException extends RuntimeException { - - public InvalidRouteFieldException(Field field) { - super(error(3003).formatted(field.getName(), field.getDeclaringClass().getName())); - } - -} diff --git a/framework/src/main/java/org/fulib/fx/data/TraversableNodeTree.java b/framework/src/main/java/org/fulib/fx/data/TraversableNodeTree.java index 900f816..182696b 100644 --- a/framework/src/main/java/org/fulib/fx/data/TraversableNodeTree.java +++ b/framework/src/main/java/org/fulib/fx/data/TraversableNodeTree.java @@ -198,4 +198,19 @@ public void removeChild(Node child) { } + @Override + public String toString() { + return this.toString(this.root, 0); + } + + private String toString(Node node, int depth) { + StringBuilder builder = new StringBuilder(); + builder.append("\t".repeat(Math.max(0, depth))); + builder.append(node.id.isBlank() ? "[empty]" : node.id).append(" ").append(node.value()).append("\n"); + for (Node child : node.children()) { + builder.append(toString(child, depth + 1)); + } + return builder.delete(builder.length() - 2, builder.length()).toString(); + } + } diff --git a/framework/src/main/java/org/fulib/fx/util/ControllerUtil.java b/framework/src/main/java/org/fulib/fx/util/ControllerUtil.java index c40c45b..fa234fe 100644 --- a/framework/src/main/java/org/fulib/fx/util/ControllerUtil.java +++ b/framework/src/main/java/org/fulib/fx/util/ControllerUtil.java @@ -8,7 +8,6 @@ import org.fulib.fx.annotation.event.OnInit; import org.fulib.fx.annotation.event.OnKey; import org.fulib.fx.annotation.event.OnRender; -import org.fulib.fx.controller.exception.InvalidRouteFieldException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -98,6 +97,16 @@ public static boolean isController(@Nullable Class clazz) { return clazz != null && clazz.isAnnotationPresent(Controller.class) && !clazz.isAnnotationPresent(Component.class); } + /** + * Checks if a class is a controller or a component. + * + * @param clazz The class to check + * @return True if the class is a controller or a component + */ + public static boolean isControllerOrComponent(@Nullable Class clazz) { + return isController(clazz) || isComponent(clazz); + } + /** * Checks if the given field is a field that can provide a component. * @@ -128,11 +137,11 @@ public static boolean canProvideSubComponent(Field field) { * A valid route field is a field that is annotated with {@link Route} and is of type {@link Provider} where the generic type is a class annotated with {@link Controller} or {@link Component}. * * @param field The field to check - * @throws InvalidRouteFieldException If the field is not a valid route field + * @throws RuntimeException If the field is not a valid route field */ public static void requireControllerProvider(@NotNull Field field) { - if (isControllerOrComponent(getProvidedClass(field))) { - throw new InvalidRouteFieldException(field); + if (!isControllerOrComponent(getProvidedClass(field))) { + throw new RuntimeException(error(3003).formatted(field.getName(), field.getDeclaringClass().getName())); } } @@ -157,7 +166,7 @@ public static boolean isEventMethod(@NotNull Method method) { * If a method overrides another method and the overridden method is an event method, calling the superclass method * results in the subclass method being called twice due to how java handles method overrides. * - * @param method The method to check + * @param method The method to check */ public static void checkOverrides(Method method) { Method overridden = ReflectionUtil.getOverriding(method); diff --git a/framework/src/main/resources/org/fulib/fx/lang/error.properties b/framework/src/main/resources/org/fulib/fx/lang/error.properties index 9ec2b30..624cae9 100644 --- a/framework/src/main/resources/org/fulib/fx/lang/error.properties +++ b/framework/src/main/resources/org/fulib/fx/lang/error.properties @@ -28,7 +28,7 @@ # Routes 3000=Class '%s' has already been registered as the router class. 3001=Field '%s' is not annotated with @Route -3002=Route '%s' already leads to '%s' but was tried to be registered for '%s'. +3002=Route '%s' already leads to a controller/component of type '%s'. 3003=Field '%s' in class '%s' is annotated with @Route but is not a Provider providing a controller/component. 3004=Field '%s' in class '%s' is not a valid provider field. 3005=Route '%s' could not be found. 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 b0480eb..4da22b5 100644 --- a/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java +++ b/framework/src/test/java/org/fulib/fx/app/FrameworkTest.java @@ -19,7 +19,6 @@ import org.fulib.fx.app.controller.subcomponent.basic.ButtonSubComponent; import org.fulib.fx.app.controller.types.BasicComponent; import org.fulib.fx.constructs.Modals; -import org.fulib.fx.controller.exception.ControllerInvalidRouteException; import org.junit.jupiter.api.Test; import org.testfx.framework.junit5.ApplicationTest; @@ -92,7 +91,7 @@ public void controllerTypes() { verifyThat("Root Component", Node::isVisible); sleep(200); - assertThrows(ControllerInvalidRouteException.class, () -> app.show("/controller/invalid")); + assertThrows(RuntimeException.class, () -> app.show("/controller/invalid")); } /**