Skip to content
This repository has been archived by the owner on Apr 19, 2022. It is now read-only.

Latest commit

 

History

History
255 lines (162 loc) · 12.8 KB

README.asciidoc

File metadata and controls

255 lines (162 loc) · 12.8 KB

Niveau 3

L’objet du niveau 3 est de découvrir l’API qui permet de faire ces choses que seuls les processeurs d’annotations peuvent faire: lever de nouvelles erreurs ou avertissements de compilation et/ou créer de nouveaux fichiers sources, classes ou ressources pendant la compilation.

Exercice 1 : interrompre la compilation

L’objectif de cet exercice est de créer un processor qui lèvera une erreur de compilation s’il existe un champs annotée avec javax.inject.Inject.

Dans le répertoire exo1, vous trouverez deux projets Maven:

  • dans le sous-répertoire processor, le projet comporte le processor InjectedPropertyProcessor et un test unitaire pour chacune des étapes ci-dessous

  • dans le sous-répertoire subject, le projet comporte quelques sources utilisant l’annotation @Inject ce qui vous permettra de constater le comportement de InjectedPropertyProcessor

Ainsi, si vous compilez le projet processor sans les tests unitaires ((cd processor && mvn clean install -DskipTests)) et qu’ensuite vous compilez le projet subject ((cd subject && mvn clean install)), vous constaterez que ce dernier compile sans problème.

Étape 1 : lever une erreur

Comme il a été vu au niveau 1, le type de diagnostic Diagnostic.Kind.ERROR passé en argument des méthodes javax.annotation.processing.Messager#printMessage permet de remonter un diagnostique qui se traduira par une erreur de compilation et l’affichage d’un message dans les logs.

L’objectif de cette étape est d’implémenter la méthode process du processor fr.devoxx.niveau3.exo1.InjectedPropertyProcessor de sorte que la compilation s’arrète avec le message "Fields annotated with @Inject are not allowed".

Important

Dans la classe InjectedPropertyProcessor, implémentez la méthode process.

Pour cela, vous devrez:

  1. récupérez le TypeElement de l’annotation @Inject parmis les paramètres de la méthode init (puisque le procesor n'"écoute" qu’une seule annotation, c’est très simple)

  2. récupérez l' Element d’au moins une propriété annotée avec @Inject du RoundEnvironnement courant (cf. RoundEnvironnement)

  3. attention, l’annotation @Inject peut être utilisée sur des champs mais aussi des méthodes et des constructeurs

  4. lever une erreur de compilation avec le message attendu (cf. la constante InjectedPropertyProcessor#COMPILATION_ERROR_MSG) en utilisant la surcharge la plus simple de Messager#printMessage

Si votre implémentation est correcte, le test unitaire Etape1Test dans le projet processor sera passant. Si ensuite vous compilez le projet subject, la compilation échouera avec le message suivant:

[ERROR] Fields annotated with @Inject are not allowed

Étape 2 : indiquer le fichier

Comme il est frustrant de voir s’afficher une erreur de compilation sans aucune indication de l’endroit où se trouve l’erreur !

Important

Dans la classe InjectedPropertyProcessor, améliorez l’implémentation de la méthode process en utilisant la surcharge de la méthode Messager#printMessage qui prend un Element.

Pour vérifier votre implémentation, activez les tests de la classe Etape2Test (mettez à true le membre enable des annotations @Test) et constatez que celui-ci est passant.

Si ensuite vous compilez le projet subject, la compilation échouera avec le message suivant (indique une erreur à la ligne 12 de la classe SomeService) :

[ERROR] [some path]/niveau_3/exo1/subject/src/main/java/fr/devoxx/niveau3/exo1/SomeService.java:[12,23] Fields annotated with @Inject are not allowed

Étape 3 : indiquer l’annotation

Il est possible, très simplement, d’être encore plus précis. Si l’erreur provient d’une annotation (comme c’est le cas ici), il est possible d’indiquer la ligne sur laquelle se trouve cette annotation.

Tip

Pour trouver l' AnnotationMirror d’une annotation donnée sur un Element, la technique la plus simple est de comparer (via la méthode equals) l' Element de chaque AnnotationMirror à l' Element de l’annotation. Ce dernier aura été récupéré soit par la méthode adéquate de javax.lang.model.util.Elements soit via les paramètres de la méthode Processor#process.

Important

Dans la classe InjectedPropertyProcessor, améliorez encore l’implémentation de la méthode process en utilisant la surcharge de la méthode Messager#printMessage qui prend un AnnotationMirror.

Pour vérifier votre implémentation, activez les tests de la classe Etape3Test (mettez à true le membre enable des annotations @Test) et constatez que celui-ci est passant.

Si ensuite vous compilez le projet subject, la compilation échouera avec le message suivant (indique une erreur à la ligne 11, cad. celle de l’annotation) :

[ERROR] [some path]/niveau_3/exo1/subject/src/main/java/fr/devoxx/niveau3/exo1/SomeService.java:[11,3] Fields annotated with @Inject are not allowed

Exercice 2 : générer un fichier

L’objectif de cet exercice est de générer un fichier dans lequel figurera le prénom des personnages décédés du livre Game of Thrones.

Dans le répertoire exo2, vous trouverez un projet Maven constitué de deux sous modules:

  • dans le module processor se trouvent l’annotation Character et le processor UndertakerProcessor

  • dans le module subject se trouvent les membres des familles Baratheon et Stark. Chaque personnage est représenté par une classe, dont le nom est le prénom du personnage, annotée avec l’annotation Character.

L’annotation Character définie un membre dead de type boolean qui permet de savoir si le personnage est mort (ou pas…​ encore).

Étape 1 : récupérer une annotation

La première étape de l’exercice consiste à lire les informations contenues dans les sources dans le contexte d’un processor.

Les deux informations qui nous intéressent: la valeur du membre dead de l’annotation Character et le nom de la classe annotée.

Important

Dans la classe UndertakerProcessor, implémentez la méthode collectDeadCharactersFirstNames.

Pour cela, vous devrez:

  1. récupérez le TypeElement de l’annotation @Character parmis les paramètres de la méthode init (puisque le procesor n'"écoute" qu’une seule annotation, c’est très simple)

  2. récupérez les Element des classes annotées avec @Character du RoundEnvironnement courant (TODO lien DOC RoundEnvironnement)

  3. récupérez l’annotation @Character de chaque element afin de savoir si le personnage courant est mort ou pas (TODO lien doc Element)

  4. récupérez le nom de la classe et l’ajouter à la propriété deadCharacterFirstnames

Si votre implémentation est correcte, le test unitaire DeadCharacterSetTest sera passant.

Étape 2 : écrire un fichier

La seconde étape de l’exercice se consacre à la génération du fichier dead_characters.txt dans le package fr.devoxx.niveau3.exo2.

Dans le cadre du traitement d’annotations, l’écriture de fichiers (fichier source, classe ou ressource quelconque) se fait par le biais de l’interface javax.annotation.processing.Filer. Une instance peut être récupérée via la méthode getFiler() de l’instance de javax.annotation.processing.ProcessingEnvironment fournie en paramètre de la méthode init(ProcessingEnvironment) du processor.

Important

Dans la classe UndertakerProcessor, implémentez la méthode generateListing.

Pour cela, vous devrez:

  1. créer une instance de javax.tools.FileObject grâce à une méthode de l’interface javax.annotation.processing.Filer pour le fichier dead_characters.txt dans le package fr.devoxx.niveau3.exo2

  2. ouvrir un Writer ou un OutputStream

  3. écrire chaques valeurs de la propriété deadCharacterFirstnames dans le fichier (une valeur par ligne)

Si votre implémentation est correcte, le test unitaire DeadCharacterFileTest sera passant.

Exercice 3 : petit processing entre ennemis

Le but du jeu ici consiste à prendre en main l’API d’écriture de sources Java avec l’API JavaPoet dans le contexte d’un processor d'`@Erbfeind` que l’on va également écrire.

Erbfeind?

der Erbfeind (pl.: die Erbfeinde) [DE]: ennemi héréditaire [FR]

— dict.leo.org

Étape 1 : on s’active

Important
  1. observez l’implémentation initiale de ErbfeindGenerator

  2. compilez le projet exo3-processor puis exo3-subject sans les tests (mvn clean install -DskipTests)

À ce stade, "What’s going on here?" aurait du apparaître dans les logs affichés lors de la compilation.

Ou peut-être pas. Remémorez-vous l’ensemble des déclarations auxquelles doit procéder un processor.

Vous ne voyez toujours pas ?

Nous avons prétendu jusqu’ici qu’il s’agissait d’un processor d'`@Erbfeind` mais le processor l’a-t-il correctement déclaré ?

Important

Complétez la classe ErbfeindGenerator afin qu’il déclare être en mesure de traiter les annotations Erbfeind.

Étape 2 : un peu de poésie dans les sources !

Il est temps de générer à tout berzingue avec JavaPoet.

L’idée à cette étape est de générer la ou les classes définies par l’annotation @Erbfeind.

En d’autres termes, si une classe com.acme.A est annotée @Erbfeind({"B", "C"}), alors les classes com.acme.B et com.acme.C, pour le moment vides, doivent être générées.

N’hésitez pas à regarder le répertoire src/test/resources du processor pour bien comprendre ce qui est demandé.

Important

Complétez la classe ErbfeindGenerator de exo3-processor afin de faire passer les tests définis dans Etape2Test.

Étape 3 : l’ennemi de mon ennemi est mon ami ?

3.1 : l’ennemi de mon ennemi ?

Bon, bon, bon, c’est bien sympa de générer des classes vides, mais c’est un peu limité, non ?

Corsons un peu l’affaire avec cette difficulté supplémentaire : chaque "Erbfeind" généré doit lui-même être annoté avec son Erbfeind correspondant.

Pour reprendre l’exemple précédent, si com.acme.A est annotée @Erbfeind({"B", "C"}) alors les classes com.acme.B et com.acme.C, maintenant correctement générées, doivent être annotées @Erbfeind("A").

Important

ErbfeindGenerator doit être modifié de façon non compatible. Il est donc nécessaire de:

  1. commenter avec @Ignore le test Etape2Test#at_last_erbfeind_generates_simple_erbfeind_classes pour désactiver le test

  2. supprimer le @Ignore sur Etape3Test#at_last_erbfeind_generates_annotated_erbfeind_classes pour activer.

Important

Complétez ErbfeindGenerator.

Petite subtilité : si vous vous référez à la documentation de JavaPoet, vous devez générer comme valeur pour @Erbfeind (AnnotationSpec.builder est votre ami) un code de type String (i.e. "$S")

Constatez le problème ;)

3.2. : Don’t overwrite the sources, Luke

À ce stade, vous devriez obtenir une sortie similaire à la suivante :

What's going on here?
What's going on here?

java.lang.RuntimeException: java.lang.RuntimeException: Attempt to recreate a file for type i.can.haz.generation.A
	at com.sun.tools.javac.main.Main.compile(Main.java:553)
	at com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:129)
	at com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:138)
	[...]

Avant de lire la suite, prenez le temps de réfléchir à la cause du problème.

Comme l’indique la présence double du log "What’s going on here?", ErbfeindGenerator a visiblement été invoqué deux fois avant de crasher lamentablement (vous pouvez activer les logs de compilation pour avoir plus de détails).

Le premier round de compilation est logique : Etape3Test inclut d’entrée une classe A annotée Erbfeind. À cette étape, le processor génère donc les classes B et C, maintenant annotées elles aussi @Erbfeind.

Le second round est donc simplement initiée parce qu’il est nécessaire de traiter les classes B et C puisqu’elles sont annotées Erbfeind. C’est là que le bât blesse : ErbfeindGenerator va maintenant essayer de générer le fichier source de A. Problème de taille : ce fichier existe déjà et la spécification interdit formellement de modifier (écraser…​) des classes existantes.

Important

Corrigez la méthode write d’ErbfeindGenerator.

Indice : si une classe donnée existe, alors c’est que l’on peut récupérer son TypeElement associé par son nom qualifié (javax.lang.model.util.Elements a.k.a. elementUtils est votre ami).

Vous pouvez maintenant compiler le projet subject et admirer les sources générées (même Scala compile!).