Skip to content

Commit

Permalink
Annotation Processor: Sidecar Generator (#76)
Browse files Browse the repository at this point in the history
* wip: Generate Sidecar classes

* wip: Use Sidecar instances for init

* chore: Add some TODOs

* fix(processor): Correctly handle @params in sidecar generator

* feat(processor): Subcontroller init in sidecar

* feat(processor): Destroy sidecars

* fix(framework): Remember missing sidecars as null in map

* feat(processor): Generate Sidecar render

* test(framework): Change some member visibilities

* feat(processor): Handle @ParamsMap field in Sidecar

* feat(processor): Handle @param fields with WritableValue

* refactor(processor): Better helper fields in FxClassGenerator

* refactor(processor): Add FxClassGenerator.streamMethods

* feat(processor): Call inherited methods

* feat(processor): Sidecar getResources and getTitle

* fix(processor): Properly convert string literals

* fix(processor): Better sidecar code for WritableValues

* test(framework): Add test for "@ParamsMap final" field

* test(framework): Use annotation processor

* fix(processor): Handle relative view paths

* test(framework): Remove NonExtendingComponent

* docs: Explain Annotation Processor in Readme

* refactor(framework): Move reflection code into ReflectionSidecar

* docs(framework): Some documentation

* fix(framework): Handle missing resource bundle and pre-load title

* refactor(framework): Pre-load @resource field

* refactor(framework): Pre-load @subcomponent fields

* refactor(framework): Pre-load init, render and destroy methods

* refactor(processor): Rename generate methods

* fix(processor): Remove printStackTrace

* chore(processor): Remove TODO line

* fix(ludo): Method visibility

* fix(processor): Consider inherited fields for generated methods

* feat(framework): Only support non-private methods and fields

* docs: Update ERROR_CODES.md

* fix: Address own review comments

* refactor(framework): Cache all annotated fields and methods in ReflectionSidecar

* fix(framework): A warning

* docs: Update ERROR_CODES.md

---------

Co-authored-by: LeStegii <paulmertens63@gmail.com>
Co-authored-by: LeStegii <LeStegii@users.noreply.github.com>
Co-authored-by: Clashsoft <Clashsoft@users.noreply.github.com>
  • Loading branch information
4 people authored Apr 16, 2024
1 parent 9e953cd commit ee641e6
Show file tree
Hide file tree
Showing 21 changed files with 1,112 additions and 483 deletions.
22 changes: 22 additions & 0 deletions ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,28 @@ public class MyComponent extends ImageView { // Wrong, should extend Parent (or
show(new MyComponent()); // Wrong, should not be able to show a controller that does not provide a parent as its view
```

### 1012: `Cannot access private * '*' in class '*' annotated with '*'.`

- Runtime: ✅
- Annotation Processor: ❌Caught by the compiler)

This error is thrown when the framework tries to access a private field or method.

```java
public class MyController {

@Param("key") // Wrong, should not be private
private String string;

@onInit() // Wrong, should not be private
private void init() {
// ...
}

// ...
}
```

## Resources

### 2000: `Could not find resource '*'.`
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ This can be done by calling the `refresh` method of the `FulibFxApp` class.

<img width="640" height="360" src="docs/assets/hot-reload.gif" alt="GIF showing the hot reload feature">

### 🏭 Annotation Processor

The framework provides an annotation processor that hooks into the Java compiler.
It can check for various errors and warnings during compilation, allowing you to find problems early on without having to run the application.
Additionally, the annotation processor generates auxiliary classes that encapsulate some of the functionality of your controllers and components, like calling the initialization methods or providing the title.
This can greatly improve the performance at runtime and simplifies debugging by reducing stack traces and allowing you to debug the generated code.
The annotation processor is optional but strongly recommended.

### 🧷 Utilities

The framework provides a few utility classes and data structures to simplify the creation of JavaFX applications.
Expand Down Expand Up @@ -166,9 +174,9 @@ behaviour. To avoid this, you should try to not modify objects passed as paramet
of the object and modify the copy or modify the object before passing it to the controller.

### 4. The framework doesn't compile even though the view file exists
The framework uses an annotation processor to check if the view file exists. If the view file is not found, the processor
will throw an error. Please make sure that the view file is in the correct location and that `options.sourcepath` is set
correctly in your `compileJava` task (see [Annotation Processor](#-installation)).
The framework uses an [annotation processor](#-annotation-processor) to check if the view file exists.
If the view file is not found, the processor will throw an error.
Please make sure that the view file is in the correct location and that `options.sourcepath` is set correctly in your `compileJava` task (see [Installation](#-installation)).

### 5. The SceneBuilder doesn't recognize my controller
When using the SceneBuilder, it might not recognize your controller's FXML file. This can happen when the FXML file contains
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,35 @@
@SuppressWarnings("unused")
public class ControllerAnnotationProcessor extends AbstractProcessor {

private FxClassGenerator generator;

public ControllerAnnotationProcessor() {
}

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.generator = new FxClassGenerator(processingEnv);
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

// Check if the element is a valid component
for (Element element : roundEnv.getElementsAnnotatedWith(Component.class)) {
checkComponent(element);
checkDoubleAnnotation(element); // Check if a class is annotated with both @Controller and @Component
if (element instanceof TypeElement typeElement) {
generator.generateSidecar(typeElement);
}
}

// Check if the element is a valid controller
for (Element element : roundEnv.getElementsAnnotatedWith(Controller.class)) {
checkController(element);
if (element instanceof TypeElement typeElement) {
generator.generateSidecar(typeElement);
}
}

for (Element element : roundEnv.getElementsAnnotatedWith(SubComponent.class)) {
Expand Down Expand Up @@ -176,12 +193,27 @@ private void checkDoubleAnnotation(Element element) {
}

private void checkViewResource(Element element, String view) {
String packageName = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName().toString();

// relativize the view path -- while it starts with ../, remove the last package segment
// this is necessary to avoid an IllegalArgumentException when calling getResource,
// because it does not allow relative paths
while (view.startsWith("../")) {
int index = packageName.lastIndexOf('.');
if (index == -1) {
final String viewPath = packageName + "/" + view; // no replace needed, packageName has no more '.'s
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(2000).formatted(viewPath), element);
return;
}
packageName = packageName.substring(0, index);
view = view.substring(3);
}

try {
// Check if the specified view file exists in the source path
final FileObject resource = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH,
processingEnv.getElementUtils().getPackageOf(element).getQualifiedName().toString(), view);
final FileObject resource = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, packageName, view);
} catch (IOException e) {
String viewPath = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName().toString().replace('.', '/') + "/" + view;
final String viewPath = packageName.replace('.', '/') + "/" + view;
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error(2000).formatted(viewPath), element);
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, note(2000));
}
Expand Down
Loading

0 comments on commit ee641e6

Please sign in to comment.