Skip to content

Commit

Permalink
Enhance router usability (#124)
Browse files Browse the repository at this point in the history
* refactor(framework): Enhance router usability
* docs: Update ERROR_CODES.md

---------

Co-authored-by: LeStegii <LeStegii@users.noreply.github.com>
  • Loading branch information
LeStegii and LeStegii authored Jul 22, 2024
1 parent 5f1af12 commit 35debb4
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 104 deletions.
4 changes: 2 additions & 2 deletions ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ❌
Expand Down Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion framework/src/main/java/org/fulib/fx/FulibFxApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public static void setResourcesPath(@NotNull Path path) {
*/
@SuppressWarnings("unchecked")
public @NotNull <T extends Node> 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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
142 changes: 96 additions & 46 deletions framework/src/main/java/org/fulib/fx/controller/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,8 +24,8 @@
@Singleton
public class Router {

private final TraversableTree<Field> routes;
private final SizeableTraversableQueue<Pair<Either<TraversableNodeTree.Node<Field>, Object>, Map<String, Object>>> history;
private final TraversableTree<Provider<?>> routes;
private final SizeableTraversableQueue<Pair<Either<TraversableNodeTree.Node<Provider<?>>, Object>, Map<String, Object>>> history;

@Inject
Lazy<ControllerManager> manager;
Expand Down Expand Up @@ -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.
Expand All @@ -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.
* <p>
* The route has to start with a slash, otherwise it will be added automatically.
* <p>
* 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);
}

/**
Expand All @@ -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<Object, Parent> 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<Field> node = ((TraversableNodeTree<Field>) this.routes).currentNode();
Provider<?> provider = this.routes.traverse(route);
TraversableNodeTree.Node<Provider<?>> node = ((TraversableNodeTree<Provider<?>>) 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) {
Expand All @@ -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<Either<TraversableNodeTree.Node<Field>, Object>, Map<String, Object>> pair) {
public void addToHistory(Pair<Either<TraversableNodeTree.Node<Provider<?>>, Object>, Map<String, Object>> pair) {
this.history.insert(pair);
}

Expand Down Expand Up @@ -177,12 +228,12 @@ public Pair<Object, Node> forward() {
}
}

private Pair<Object, Node> navigate(Pair<Either<TraversableNodeTree.Node<Field>, Object>, Map<String, Object>> pair) {
private Pair<Object, Node> navigate(Pair<Either<TraversableNodeTree.Node<Provider<?>>, Object>, Map<String, Object>> pair) {
var either = pair.getKey();
either.getLeft().ifPresent(node -> ((TraversableNodeTree<Field>) routes).setCurrentNode(node)); // If the history contains a route, set it as the current node
either.getLeft().ifPresent(node -> ((TraversableNodeTree<Provider<?>>) 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
Expand All @@ -201,17 +252,10 @@ private Pair<Object, Node> navigate(Pair<Either<TraversableNodeTree.Node<Field>,
* @return The current controller object and its parameters
*/
public Pair<Object, Map<String, Object>> current() {
Either<TraversableNodeTree.Node<Field>, Object> either = this.history.current().getKey();
Either<TraversableNodeTree.Node<Provider<?>>, 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()
);
Expand All @@ -226,4 +270,10 @@ public Pair<Object, Map<String, Object>> current() {
public void setHistorySize(int size) {
this.history.setSize(size);
}


@Override
public String toString() {
return "Router(\n" + this.routes + "\n)";
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

15 changes: 15 additions & 0 deletions framework/src/main/java/org/fulib/fx/data/TraversableNodeTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,19 @@ public void removeChild(Node<E> child) {

}

@Override
public String toString() {
return this.toString(this.root, 0);
}

private String toString(Node<E> 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<E> child : node.children()) {
builder.append(toString(child, depth + 1));
}
return builder.delete(builder.length() - 2, builder.length()).toString();
}

}
19 changes: 14 additions & 5 deletions framework/src/main/java/org/fulib/fx/util/ControllerUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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()));
}
}

Expand All @@ -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);
Expand Down
Loading

0 comments on commit 35debb4

Please sign in to comment.