:: Enseignements :: Master :: M1 :: 2022-2023 :: Java Avancé ::
[LOGO]

Projet de Java Avancé Master 1 - 2022


Exercice 1 - CloneWar

Le but du projet Clone est d'écrire une application web qui analyse des fichiers jar (Java Archive) pour détecter des codes communs (on parle de clones).
L'application CloneWar est composée d'un back-end écrit en Java offrant différents services REST permettant d'accéder aux informations de l'analyse d'une archive et d'un front-end écrit en JavaScript affichant ces informations et en particulier les codes sources considérés comme des clones.
L’application doit permettre...
  • ... d'ajouter un nouvel artefact. Un artefact est constitué de deux archives différentes, l'archive "main" contient le bytecode (les .class) de l'artefact et l'archive "source" contient le code source associé (les .java).
    Les méta-données de l'archive "main" (le numéro de version, les développeurs, etc...) sont extraites du pom.xml de déploiement.
  • ... d'indexer un artefact, en calculant pour chaque groupe d'instructions (bytecodes) de l'archive "main", un hash et d'y associer un nom de fichier ainsi qu'un numéro de ligne.
  • ... d'afficher pour un artefact, tous les clones présents dans les autres artefacts ainsi que dans l'artefact lui-même. Un calcul de score doit être affiché pour indiquer le pourcentage de code commun entre les artefacts. De plus, pour chaque clone, il existe une vue qui affiche les codes sources correspondants sous forme d'un "diff" en couleur.
En termes d'interface graphique, votre application doit être composée de deux écrans.
  • La première page sert d'écran d'accueil. Elle affiche l'ensemble des artefacts disponibles. Pour chacun, elle indique les artefacts les plus proches en fonction du score (seulement quelques uns, pas tous !). Pour cet écran, uniquement les informations essentielles d'un artefact doivent être affichées : le nom de l'artefact, l'URL correspondante, la date d'ajout de l'artefact, si une analyse/indexation de l'artefact est en cours ( avec une barre de progression si possible !).
  • La seconde page affiche, pour un artefact donné, l'ensemble des méta-données disponibles ainsi que les clones détectés dans chaque autre artefact, par ordre de taille des clones (nombre d'instructions clonées). Pour chaque clone, un "diff" des parties des codes sources correspondants doit être affiché visuellement, avec le code de l'artefact courant à gauche et le code du clone à droite, à la façon dont un IDE affiche les différences entre deux codes.

Pour détecter les clones, on va utiliser l'algorithme de Karp-Rabin mais au lieu de le faire fonctionner sur des chaînes de caractère, on vous demande de le faire fonctionner sur les instructions en bytecode. L'idée est que pour chaque suite d'instructions de bytecode, on va calculer un hash et stocker dans une base de données, l'association de ce hash avec le fichier et la ligne de la première instruction de la suite d'instructions.
Détecter les clones revient à regarder si il n'existe pas deux hashs égaux correspondant à deux parties de code distinctes. Il faut aussi regrouper les zones communes pour que si il y a plusieurs hashs successif qui sont égaux, on indique à l'utilisateur qu'il y a un seul clone et pas plusieurs. Enfin, si vous n'utilisez pas un hachage parfait, il peut aussi y avoir des collisions, deux zones ont la même valeur de hash mais la suite d'instructions est pas la même, il faut dans ce cas, ne pas remonter les zones comme étant des clones.
Pour que l'on puisse reconnaître des clones indépendamment du nom des variables, des champs, des méthodes ou des classes, on va abstraire le flot d'instructions.
Par exemple,
      Toy toy = new Toy("buzz");
      int b = Integer.parseInt(toy.name());
    
correspond en bytecode à
      NEW Toy
      DUP
      LDC "hello"
      INVOKESPECIAL Toy.<init>(Ljava/lang/String;)V
      ASTORE 1
      ALOAD 1
      INVOKEVIRTUAL Toy.name ()Ljava/lang/String;
      INVOKESTATIC Integer.parseInt (Ljava/lang/String;)I
      ISTORE 2
    
on peut estimer que la constante "HELLO" ou le nom Integer.parseInt() est important pour la détection tandis que Toy, name ou le type des variables locales ne le sont pas, et donc abstraire le code comme ceci
      NEW
      DUP
      LDC "hello"
      INVOKESPECIAL <init>
      STORE 1
      LOAD 1
      INVOKEVIRTUAL
      INVOKESTATIC Integer.parseInt
      STORE 2
    
La façon d'abstraire ci-dessus est un exemple, c'est à vous de trouver comment abstraire le bytecode pour détecter les clones correctement. Si vous abstrayez trop le code, vous aurez plein de faux positifs, c'est à dire l'algorithme trouvera des clones mais un humain qui regarderait le code source ne serait pas d'accord. Ou, au contraire, vous aurez des faux négatifs, si vous n'abstrayez pas assez, c'est à dire des clones visibles pour un humain qui ne seront pas détectés par l'algorithme.
Vous êtes aussi libres de choisir comment doit fonctionner exactement l'algorithme qui calcule les valeurs de hash, pourvu que vous utilisiez le principe du rolling hash.

Si l'algorithme de détection de clone ne marche pas, vous serez sévèrement punis, être capable d'assurer que votre application détecte bien les clones est un objectifs important. Pour cela, on vous demande d'avoir au moins 20 exemples de codes qui sont des clones ou pas des clones, avec des tests unitaires (utilisant JUnit 5) automatiques qui montrent que votre algorithme fonctionne de façon satisfaisante.
On vous demande, de plus, sur Maven Central, de récupérer les artefacts ("main" et "source") des libraries junit 5, jackson, Google Guava, JAXB, Log4j 2, Slf4J et ASM et de tester votre projet avec.
Note : indexer des jars devrait être rapide, si votre code est lent, ou n'a pas assez de mémoire en indexant l'artefact Guava, ce n'est pas normal ! Vous devez veillez à ce que votre algorithme ne soit pas consommateur de temps ou de mémoire.

Pour lire le bytecode, vous allez utiliser la librairie ASM 9.4. Voici un exemple
     var finder = ModuleFinder.of(Path.of("name_of_your.jar"));
     var moduleReference = finder.findAll().stream().findFirst().orElseThrow();
     try(var reader = moduleReference.open()) {
       for(var filename: (Iterable<String>) reader.list()::iterator) {
         if (!filename.endsWith(".class")) {
           continue;
         }
         try(var inputStream = reader.open(filename).orElseThrow()) {
           var classReader = new ClassReader(inputStream);
           classReader.accept(new ClassVisitor(Opcodes.ASM9) {

               private static String modifier(int access) {
                 if (Modifier.isPublic(access)) {
                   return "public";
                 }
                 if (Modifier.isPrivate(access)) {
                   return "private";
                 }
                 if (Modifier.isProtected(access)) {
                   return "protected";
                 }
                 return "";
               }

               @Override
               public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                 System.err.println("class " + modifier(access) + " " + name + " " + superName + " " + (interfaces != null? Arrays.toString(interfaces): ""));
               }

               @Override
               public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) {
                 System.err.println("  component " + name + " " + ClassDesc.ofDescriptor(descriptor).displayName());
                 return null;
               }

               @Override
               public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
                 System.err.println("  field " + modifier(access) + " " + name + " " + ClassDesc.ofDescriptor(descriptor).displayName() + " " + signature);
                 return null;
               }

               @Override
               public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                 System.err.println("  method " + modifier(access) + " " + name + " " + MethodTypeDesc.ofDescriptor(descriptor).displayDescriptor() + " " + signature);
                 return new MethodVisitor(Opcodes.ASM9) {
                   @Override
                   public void visitInsn(int opcode) {
                     System.err.println("    opcode " + opcode);
                   }

                   @Override
                   public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
                     System.err.println("    opcode " + opcode + " " + owner+ "." + name + descriptor);
                   }

                   // + the other visit methods to get all the opcodes
                 };
               }
           }, 0);
         }
       }
     }
   

Technologies à utiliser
  • Vous devez utiliser Maven comme outil de build et IntelliJ comme IDE, et la version 19 de Java.
  • Pour tester vos services REST, vous pouvez utiliser Postman ou tout autres clients capables de faire des requêtes REST.
  • Les test unitaires Java doivent être effectués avec JUnit 5.9.1, vous pouvez vous référer au guide d'utilisation.
    Chaque classe classe Java doit avoir une classe de test correspondante (pour au moins 80%) du projet.
  • Pour lire le bytecode la librarie ASM 9.3. Vous pouvez vous référer au guide d'utilisation.
  • Pour la sérialisation/dé-sérialisation JSON des requêtes, vous utiliserez une API de parsing JSON Jackson 2.13.4.2
  • Pour l'extraction de "truc"(TM) dans le code source, vous utiliserez des expressions régulières avec le package java.util.regex.

Pour implanter les différents services REST, votre application doit utiliser une des technos (c'est moi qui choisi pas vous) ci-dessous.
Attention à bien faire la différence entre les versions AOT ou JVM et les versions synchrone et asynchrone (reactive) des APIs !
Pour l'API REST, les entrées et les sorties JSON doivent utiliser des records si possible.
Pour la mapping Object / Relational, c-a-d voir une ligne d'une table de BDD comme un objet Java. Il y a deux implantations, une à base d'Hibernate qui peut être utilisée directement soit par l'intermédiaire de la spécification JPA (Java Persistance API ou Jakarta Persistence API pour les versions plus récente). Et une à base de JDBI.
Pour le front-end web, vous avez besoin d'un framework graphique
Note : le front-end doit être "buildé" aussi en utilisant Maven (un seul POM pour front et le back), vous aurez peut-être besoin de plugin Maven specifique pour cela.
Note2: vous avez besoin de npm pour la partie build mais pas à l'exécution ! Vous pouvez de plus, utiliser une librairie spéciale pour la gestion des graphes pourvu quelle soit adaptée à votre framework (pas de react-graph si vous devez utiliser svelte).
Attention: lors du déploiement, vous ne devez pas utiliser nodejs comme serveur Web car vous avez déjà un serveur Web qui sert l'API donc il peut aussi servir les pages statiques de votre librarie JS. Bien sûr, pour le build, vous pouvez utiliser nodejs et npm pour construire votre application web.
Pour vous aidez à avoir de belles pages, vous allez aussi utiliser une bibliothèque qui vous aide pour la partie CSS
L'application a besoin d'une base de données mais vu le volume de données, pas forcément d'une "vrai" base de données, nous utiliserons donc des bases de données embedded.
L'intérêt d'une BDD embedded est qu'elle est prête à l'emploi directement à partir d'un jar.
Il y a deux façon d'accéder à une BDD en Java, en utilisant le Driver JDBC ou le DataSource JDBC. On vous demande d'utiliser le DataSource car la gestion des connections à la BD est automatique. Dans le cas où vous utilisez JPA / Hibernate, il vous faut aussi ajouter une dépendence sur Dialect correspondant à votre base de donnéees car chaque base de données parle un SQL un petit peu différent.
Attention, ces peuvent être utiliser aussi comme des BDDs classiques extérieurs à l'application, ce n'est pas ce qui nous intéresse ici, on veut la version embedded !
Et on ne veut pas que la base de donnée disparaisse quand on quitte l'application !

REST API documentation
Nous allons documenter l'API REST de votre back-end en utilisant le format Open API 3.
Note : il y a deux façon d'utiliser Open API, soit on l'utilise comme un générateur qui génère le squelette de l'API soit dans l'autre sens, on extrait les valeurs des classes Java. On va utiliser la seconde version, comme cela, la documentation de l'API sera toujours à jour avec le code.

Sécurité :
  • Pas de HTTPS pour ce projet (c'est mal) mais c'est pour vous aider à débugger !
  • Les entrées des services web au niveau de l'URL ou de la partie JSON doivent être validées et les sorties doivent être "escapées" pour éviter les injections de code.
  • Il n'y a aucune raison que le login/mdp de la BDD soit en dur dans votre code !

Binômes avec les technos qui doivent être utilisées
   Binome               | SERVER              | PERSISTENCE    | OPENAPI? | DB           | FRONT   | UI
   ABEELACK-REGUEME     | Helidon MP Reactive | JPA reactive   | OpenAPI  | Apache Derby | angular | bootstrap
   ABIB-SEDDAR	        | Spring MVC          | JPA	           | non      | H2           | vue     | bootstrap
   AGNETTI-SAIDI	    | Helidon SE Reactive | DBClient       | OpenAPI  | H2           | react   | tailwind
   AGONSE-ATTRAX	    | Spring Reactive     | JPA reactive   | OpenAPI  | SQLite       | react   | tailwind
   AHAMMAD-POMBO        | Spring MVC	      | JPA	           | non      | SQLite       | vue     | bootstrap
   ANCEL-CROHARE	    | Quarkus             | JPA            | non      | SQLite       | vue     | tailwind
   AOUNALLAH-BOURENNANE | Quarkus             | Hib. + panache | non      | SQLite       | vue     | bootstrap
   ARBAOUI-LYLY_IENG    | Quarkus             | JPA            | non      | SQLite       | angular | tailwind
   AVRON-HATHAT	Avron   | Quarkus Reactive    | Hib. + panache | OpenAPI  | SQLite       | react   | bootstrap
   BARDIN-LOUSADO       | Quarkus             | JPA            | non      | SQLite       | angular | tailwind
   BENAIBOUCHE-HAMADI	| Spring MVC          | JPA            | OpenAPI  | SQLite       | vue     | bootstrap
   BENDIAF-ENNOUCHI     | Spring MVC          | JPA            | non      | SQLite       | angular | bootstrap
   BENMALEK-DOS_SANTOS  | Spring 6 Reactive   | JPA (jakarta)  | OpenAPI  | SQLite       | vue     | bootstrap
   BENREGUIG-VILARINHO	| Spring MVC 6 + Loom | JPA (jakarta)  | OpenAPI  | Apache Derby | react   | tailwind
   BOUKA-SIMON          | Quarkus             | Hib. + panache | non      | H2           | react   | tailwind
   BOULET-DOS_SANTOS	| Helidon SE          | DBClient       | OpenAPI  | SQLite       | svelte  | bulma
   COLLET-EYNARD        | Helidon SE	      | Jdbi           | OpenAPI  | H2           | react   | tailwind
   COLLET-LE_DUFF       | Spring 6 Reactive   | JPA (jakarta)  | OpenAPI  | Apache Derby | svelte  | tailwind
   CROS-MAZYRAC	        | Spring Reactive     | JPA reactive   | OpenAPI  | HyperSQL     | react   | tailwind
   DA_COSTA-LIU	DA      | Micronaut	          | JPA            | non      | SQLite       | vue     | tailwind
   DE_JESUS-RICHARD	    | Helidon NIMA        | DBClient       | OpenAPI  | SQLite       | react   | bulma
   DEBATS-SIM           | Spring MVC          | JPA            | non      | H2           | vue     | bootstrap
   DOS_SANTOS-KAMDOM    | Quarkus Reactive    | JPA reactive   | OpenAPI  | HyperSQL     | vue     | tailwind
   DOUKI-LEDOUX         | Spring MVC 6 + Loom | JPA (jakarta)  | OpenAPI  | HyperSQL     | svelte  | bulma
   FOULON-LAGIER        | Helidon SE Reactive | DBClient       | OpenAPI  | HyperSQL     | svelte  | bulma
   FRICHE-ETIENNE       | Quarkus             | Jdbi           | non      | SQLite       | react   | tailwind
   FROIDURE-BENMECHICH	| Spring MVC          | JPA            | non      | H2           | angular | bootstrap
   GARCIA-KINGUE        | Micronaut Reactive  | JPA reactive   | OpenAPI  | HyperSQL     | react   | bulma
   GAUDET-JEAN	        | Helidon NIMA        | Jdbi           | OpenAPI  | H2           | svelte  | tailwind
   GENNEVOISE-THEPHARAT	| Quarkus             | JPA            | non      | H2           | angular | tailwind
   HAIDAMOUS-JAILLARD   | Helidon MP          | Jdbi           | non      | H2           | vue     | bootstrap
   JOUVENOT-MENAA       | Micronaut Reactive  | JPA reactive   | OpenAPI  | H2           | angular | tailwind
   KY-NODA_HODA	        | Quarkus             | JPA            | non      | H2           | vue     | tailwind
   LAGIER-NGUYEN        | Helidon MP Reactive | JPA reactive   | OpenAPI  | SQLite       | vue     | bootstrap
   LE_CHECH-VONG        | Helidon MP          | JPA            | non      | SQLite       | react   | tailwind
   MACKE-PEREZ          | Helidon MP          | JPA            | non      | SQLite       | react   | tailwind
   MONTEIRO-THORSTEINSSON Spring Reactive     | JPA reactive   | OpenAPI  | H2           | vue     | bulma
   OSMANI-WANG          | Spring MVC          | JPA            | non      | SQLite       | angular | bootstrap
   PHAM_DIEU-SOM        | Quarkus Reactive    | Hib. + panache | OpenAPI  | Apache Derby | angular | bulma
   QUACH-TOURE          | Micronaut           | Jdbi           | non      | H2           | react   | bootstrap
   RAMAROSON-TELLIER    | Spring 6 Reactive   | JPA (jakarta)  | OpenAPI  | SQLite       | react   | bulma
   STEPHANELY-VONG      | Micronaut           | JPA            | non      | H2           | react   | bootstrap
  

Calendrier des rendus.
Soutenance intermédiaire (bêta) :
Toutes les parties de l'architecture doivent être présentes et communiquer entre elles.
Il peut y avoir des bugs, c'est pas grave. Fonctionnellement, seule la partie incrémentale du calcul des métriques et des badges n'est pas demandée.
Description des tests (TODO !)
Soutenance finale :
TODO !

Pour vous aider, si vous ne respectez pas les indications de "sudden death" suivantes, votre projet sera considéré comme mort et noté 0.
Pour la partie Java, le programme doit être écrit en utilisant correctement les différents concepts vus lors du cours de Java Avancé (sous-typage, polymorphisme, lambdas, classes internes, exceptions, types paramétrés, annotations, collections, entrées/sorties).
  • Une des technologies que votre projet utilise n'est pas celle requise pour votre binome
  • Il ne doit pas y avoir de warnings lorsque l'on compile avec javac -Xlint:all.
  • Dans un module, les packages d'implantation ne doivent pas être exportés et requires transitive doit être utilisé là où c'est nécessaire.
  • Il ne doit pas y avoir de raw types, de @SuppressWarning non justifié, de cast non justifié.
  • Le principe d'encapsulation et la programmation par contrat doivent être respectées.
  • Il ne doit pas y avoir de champs ou méthodes protected.
  • Il ne doit pas y avoir d'instanceof/if...else sur des types là où il est possible d'utiliser le polymorphisme ou le pattern matching.
  • Chaque interface devra être nécessaire.
    Une interface possède 0 ou 1 méthode (sinon justifiée).
  • Aucune classe abstraite ne doit être publique ou utilisée comme un type.
  • Chaque méthode devra être appelée (pas de code mort).
  • Aucune méthode ne doit faire plus de 10 lignes sans une vraie justification.
  • Il est interdit d'utiliser des champs static typés par un objet (pas de variables globales), seules les constantes (static final) de type primitif sont autorisées (et utiliser l'injection de dépendence SVP).
  • Le fichier POM.xml ne doit pas contenir de dépendances non listées dans ce document où ayant une autre version que la version demandée (à part les dépendances de dépendances).