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

Enhance router usability #124

Merged
merged 2 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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