Le Mind Mapping appliqué aux dépendances des projets « mavénisés »

Comme vous l’avez peut-être constaté dans vos applications d’entreprise, les dépendances des modules sont parfois difficiles à appréhender, de par leur nombre et la complexité des relations.

Encore plus si vous vous retrouvez face à un chantier de refactoring important qui touche à votre application en sa globalité (refonte du modèle métier, éclatement de modules, restructuration des couches logicielles, migration de framework, externalisation des traitements communs en aspect, …) ; assurer la réussite d’un tel chantier s’avère généralement une tâche difficile : vous avez beau essayé de pousser l’étude d’impact au bout, les surprises sont toujours au rendez-vous.

Garder un œil sur l’application en phase de transformation implique systématiquement une prise de « snapshots » réguliers des dépendances entre modules ; cela vous permettra sans doute de savoir rapidement si vous êtes sur la bonne voie, ou si un « break – rethink to avoid worse » est nécessaire.

C’est dans cette optique que le Mind Mapping s’impose afin de fournir une vue documentaire visuelle facilement exploitable des dépendances des modules ; vous permettant ainsi de contrôler la tendance du projet et d’éviter toute apparition de dépendance cyclique directe ou indirecte (transitive), du moins la faire disparaître assez rapidement si elle est déjà là.

De ce fait, on s’aperçoit que cette approche a plus de valeur ajoutée pour le Lead technique ou encore l’architecte de l’équipe de développement que pour le développeur. Quant à ce dernier, et si besoin (on ne s’amuse pas à faire des « mvn dependency:tree » tous les jours), il pourrait rester fidèle aux outils qui viennent accompagner son IDE, que ce soit sous forme de plugin (Jdepend, byecycle, M2Eclipse avec Dependency graph view et Dependency Hierarchy view, …) ou encore en standalone (JDepend, mvn dependency:tree, …).

Dans cet article, on s’intéresse particulièrement aux projets « mavénisés ». La démarche consiste, pour un module donné, à produire un artefact pouvant être exploité par un outil open source de Mind Mapping. L’idée est donc de développer un simple plugin maven en charge de générer un fichier au format attendu pour l’outil. Il existe plusieurs logiciels open source de Mind Mapping à savoir FreeMind, FreePlane (un dérivé de FreeMind), Xmind, etc. Le choix est fait sur FreePlane qui traite des fichiers de type « .mm ».

La « Mind Map »: Un véritable couteau suisse de la pensée

La Mind Map se veut une représentation visuelle externe de ce qui se passe dans le cerveau. Ainsi, on peut s’en servir pour refléter la pensée, la réflexion, la connaissance, la mémoire et stimuler la créativité.
Les cartes mentales « Mind Maps », ou encore les cartes heuristiques, représentent par définition, une méthode graphique pour prendre des notes. Leur base visuelle permet de distinguer des mots ou des idées, souvent avec des couleurs et des symboles. Elles prennent généralement un format de niveau hiérarchique ou d’un arbre, avec des idées de ramification.
Les cartes mentales sont des collections de mots structurés par le contexte mental de l’auteur avec des mnémoniques visuels, des couleurs, des icônes et des liens.
Ces cartes diffèrent des cartes conceptuelles, dans l’esprit, du fait qu’elles se concentrent sur une seule idée, alors que les cartes conceptuelles en communiquent plusieurs. Elles ont également une finalité différente : elles aident à la mémoire et à l’organisation.

Ci-dessous une Mind Map concernant les usages possibles du Mind Mapping :

UsagesMindmappingLarge

Mind Map Maven Plugin

Les API nécessaires

  • maven-dependency-tree.jar: utilisé pour construire l’arbre des dépendances d’un module donné et en récupérer le « root node »
  • maven-plugin-api.jar: API de plugin Maven
  • velocity-tools.jar : utilisation du moteur de substitution Velocity (Velocity Templating Engine) pour la génération de l’artefact final à partir d’un template.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <packaging>maven-plugin</packaging>
    <groupId>fr.xebia.maven.plugins</groupId>
    <artifactId>mindmap-maven-plugin</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>Maven dependency :: ${project.artifactId} ${project.packaging}</name>
    <description>
        This plugin generates a MindMap from the project
        dependencies. The mindmap file (with the '.mm' extension)
        can be viewed with Freeplane (free tool).

        By default, the mindmap shows the full dependency tree.

        You can filter this tree at generation time
        by artifact's group id, by providing a parameter
         'groupIdsFilteringREGEXMatch'
        setted with a groupId name fragment (a startswith will be performed).

        Usage is (for exemple) :
        mvn fr.xebia.maven.plugins:mindmap-maven-plugin:1.0.0-SNAPSHOT:mindmap

        with filtering :
        mvn fr.xebia.maven.plugins:mindmap-maven-plugin:1.0.0-SNAPSHOT:mindmap -DgroupIdsFilteringREGEXMatch=fr.xebia

    </description>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven.shared</groupId>
            <artifactId>maven-dependency-tree</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>2.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-tools</artifactId>
            <version>2.0</version>
        </dependency>
    </dependencies>
</project>

MindmapMojo.java

package fr.xebia.maven.plugin.mindmap;

import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Properties;

import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.dependency.tree.DependencyNode;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.tools.generic.SortTool;

/**
 * Generate a mindmap from the pom dependencies.
 *
 * @goal mindmap
 *
 */
public class MindmapMojo extends AbstractMojo {
    /** @component */
    private org.apache.maven.artifact.factory.ArtifactFactory artifactFactory;
    /**
     * @component
     * @required
     * @readonly
     */
    private ArtifactMetadataSource artifactMetadataSource;
    /**
     * @component
     * @required
     * @readonly
     */
    private ArtifactCollector artifactCollector;
    /**
     * @component
     * @required
     * @readonly
     */
    private DependencyTreeBuilder treeBuilder;
    /** @parameter default-value="${localRepository}" */
    private ArtifactRepository localRepository;
    /**
     * @parameter default-value="${project}"
     */
    private MavenProject project;

    /**
     * @parameter expression="${groupIdsFilteringREGEXMatch}"
     */
    private String groupIdsFilteringREGEXMatch = null;

    public void execute() throws MojoExecutionException {
        try {
            ArtifactFilter artifactFilter = new ScopeArtifactFilter(null);
            // Build project dependency tree
            DependencyNode rootNode = treeBuilder.buildDependencyTree(project,
                    localRepository, artifactFactory, artifactMetadataSource,
                    artifactFilter, artifactCollector);

            generateMindMapXML(project, rootNode);

        } catch (DependencyTreeBuilderException e) {
            throw new MojoExecutionException("Unable to build project dependency tree.", e);
        }

    }

    private void generateMindMapXML(MavenProject mavenProject, DependencyNode rootNode) throws MojoExecutionException {
        FileWriter fw = null;

        try {
            Properties p = new Properties();
            p.setProperty("resource.loader", "class");
            p.setProperty("class.resource.loader.class",
            "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");

            //  first, get and initialize an engine
            VelocityEngine ve = new VelocityEngine();
            ve.init(p);

            //  next, get the Template
            Template freePlaneTemplate = ve.getTemplate("MindMapTemplate.vm");

            // create a context and add data
            VelocityContext context = new VelocityContext();

            context.put("artifactId", mavenProject.getArtifactId());
            context.put("sorter", new SortTool());
            context.put("rootNode", rootNode);
            context.put("date", (new SimpleDateFormat("dd/MM/yy HH:mm")).format(Calendar.getInstance().getTime()));

            context.put("groupIdsFilteringREGEXMatch", (groupIdsFilteringREGEXMatch != null)?groupIdsFilteringREGEXMatch:"");

            context.put("creationTS", Calendar.getInstance().getTimeInMillis());

            // now render the template
            fw = new FileWriter("./"+mavenProject.getGroupId()+"_"+mavenProject.getArtifactId()+"_"+mavenProject.getVersion()+".mm");

            // write the mindmap xml to disc
            freePlaneTemplate.merge(context, fw);
        } catch (Exception e) {
            throw new MojoExecutionException("Unable to generate mind map.", e);
        } finally {
            if (fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    getLog().warn("Unable to properly close stream.", e);
                }
            }
        }
    }
}

FreePlane Velocity Template: MindMapTemplate.vm

#macro( exploreNodes $node )
    #set( $nodeCount = $nodeCount +1 )
    #set( $color = $nodeCount % 255 )

    #if ($node.artifact.groupId.startsWith($groupIdsFilteringREGEXMatch))

        <node ID="ID_$nodeCount" CREATED="$creationTS" MODIFIED="$creationTS">
            <richcontent TYPE="NODE">
            <html>
              <head></head>
              <body>
                <p>
                  ## Mettre en gris clair le groupId
                  <font color="#cccccc">$node.artifact.groupId</font>
                </p>
                <p>
                 ## Mettre en gras l'artifactId
                  <b>$node.artifact.artifactId</b>
                </p>
                <p>
                  ## Mettre en gris clair la version
                  <small><font color="#cccccc">$node.artifact.version</font></small>
                </p>
              </body>
            </html>
            </richcontent>
            #* Pour le noeud parent (root node) on rajoute un noeud supplémentaire à gauche du noeud parent
               et contenant le texte suivant:Generated : ${date}
               avec une icône calendar
            *#
            #if (!$rootNodeExplored)
                <node TEXT="Generated : ${date}" POSITION="left" ID="ID_${nodeCount}000" CREATED="$creationTS" MODIFIED="$creationTS">
                    <icon BUILTIN="calendar"/>
                </node>
                #* Pour le noeud parent (root node), quand le filtre sur le groupeId est utilisé
                   on rajoute un noeud supplémentaire à gauche du noeud parent
                   et contenant le texte suivant:Only groups starting with [${groupIdsFilteringREGEXMatch}] are shown
                   avec une icône essagebox_warning
                *#
                #if( $groupIdsFilteringREGEXMatch.length() > 0)
                    <node TEXT="Only groups starting with [${groupIdsFilteringREGEXMatch}] are shown" POSITION="left" ID="ID_${nodeCount}001" CREATED="$creationTS" MODIFIED="$creationTS">
                        <icon BUILTIN="messagebox_warning"/>
                    </node>
                #end
            #end

            #if (!$rootNodeExplored)
                #set( $rootNodeExplored = true )
            #end

            ## Parcours des noeuds fils du noeud en cours et appel récursif de la méthode: exploreNodes
            #foreach( $dependencyNode in $sorter.sort($node.children,"artifact.artifactId") )
                #exploreNodes($dependencyNode)
            #end
        </node>
    #end
#end
#set( $nodeCount = 0 )
#set( $Integer = 0 )
#set( $rootNodeExplored = false )

<map version="0.9.0">
<!--To view this file, download free mind mapping software Freeplane from http://freeplane.sourceforge.net -->

#exploreNodes($rootNode)

</map>

Exécution du plugin

Sans le filtre:

mvn fr.xebia.maven.plugins:mindmap-maven-plugin:1.0.0-SNAPSHOT:mindmap

Avec le filtre:

mvn fr.xebia.maven.plugins:mindmap-maven-plugin:1.0.0-SNAPSHOT:mindmap -DgroupIdsFilteringREGEXMatch=fr.xebia

Attacher le goal « mindmap » à une phase spécifique du build Lifecycle

<build>
   <plugins>
      <plugin>
         <groupId>fr.xebia.maven.plugins</groupId>
         <artifactId>mindmap-maven-plugin</artifactId>
         <version>1.0.0-SNAPSHOT</version>
         <executions>
            <execution>
               <phase>package</phase>
               <goals>
                  <goal>mindmap</goal>
               </goals>
            </execution>
         </executions>
      </plugin>
   </plugins>
</build>

Il est possible de définir la phase par défaut du Mojo lors de sa déclaration. Le Mojo s’exécutera donc lors de cette phase à condition que l’on n’aie précisé aucune phase dans <execution>. Dans le cas contraire on parle de « rebinding » du Mojo avec une nouvelle phase du build Lifecycle.

/**
 * Generate a mindmap from the pom dependencies.
 *
 * @goal mindmap
 *
 * @phase package
 *
 */
public class MindmapMojo extends AbstractMojo {

}

Vous pourrez vous référer à la documentation de l’API Mojo pour en savoir plus sur l’ensemble des annotations possibles.

Raccourcissement de la ligne de commande

Vous avez sans doute remarqué que le préfixe d’exécution du plugin n’est pas très convivial aux utilisateurs ; le fait de taper

mvn ${groupId}:${artefactId}:${version}:${goal}

en l’occurrence:

mvn fr.xebia.maven.plugins:mindmap-maven-plugin:1.0.0-SNAPSHOT:mindmap

semble être parfois gênant.

Afin de donner aux utilisateurs un préfixe idéal ou encore un « shortened prefix » pour faire référence au plugin, il faut rajouter le groupId dans le fichier de configuration maven (settings.xml) comme suit :

<pluginGroups>
   <pluginGroup>fr.xebia.maven.plugins</pluginGroup>
</pluginGroups>

Désormais, vous pouvez exécuter le plugin en tapant:

mvn mindmap:mindmap

Pour plus d’informations sur la résolution des préfixes des plugins maven je vous invite à consulter la documentation officielle.

Exploitation des Fichiers .mm

On va appliquer le plugin sur lui-même, ce qui nous donnera l’aperçu suivant avec FreePlane :

Par la suite, vous avez la possibilité de naviguer dans les différents noeuds (déplier / plier), tout en ayant la souplesse de les éditer.
Par exemple, si vous dépliez le module velocity-tools, vous aurez l’ensemble de ses dépendances directes :

L’application du filtre groupIdsFilteringREGEXMatch=fr.xebia ne donnera que le module « mindmap-maven-plugin » lui même (comme il ne dispose d’aucune dépendance à un groupId commençant par fr.xebia):

Aller plus loin

Une fois que vous avez compris le mécanisme de fonctionnement du templating et avec un minimum de connaissances du langage VTL (Velocity Template Language), vous pouvez faire ce que vous voulez ; on pourrait imaginer par exemple de colorer les dépendances cycliques en insérant des flèches rouges liant les modules concernés (ajout des balises <arrowlink COLOR= »#ff3300″> attendu par FreePlane), ou encore de développer des filtres plus avancés en fonction de vos besoins ; je vois bien un filtre sur le scope de la dépendance (compile, runtime, test, …).

8 Responses

  • Très intéressant cet outil ! Je connaissais les dépendances maven et le mind mapping notamment avec FreeMind, mais je ne pensais pas à allier les deux. C’est vrai que les graphes des dépendances maven sont très souvent illisibles (et très très fournis) dès que le projet devient conséquent.

    J’aime beaucoup tes idées d’amélioration, est-ce que tu penses les développer ?

    Merci pour cet article et cet outil en tout cas. Je l’imagine bien en plugin pour Sonar pour visualiser facilement les dépendances des projets :)

  • À ce que je peux comprendre pour ce qui est du résultat il existe exactement la même chose intégré à Netbeans : une carte similaire au mind mapping des dépendances.

    Cela n’empêche pas que votre article est très interressant sur la problématique du refactoring, et comme exemple de plugiin maven.

    Cordialement.

  • Superbe idée,

    Ce plugin n’est pas disponible quelque part ?

  • Bonjour Issam,

    Le projet Tattletale de JBoss (http://www.jboss.org/tattletale) permet également d’avoir une vision intéressante des dépendances d’un projet (même s’il n’est pas mavenisé car il fait de l’introspection sur le runtime).

    Il est facile d’emploi (et ne demande du coup pas de programmer comme dans ton exemple).
    Il doit être assez facile d’en faire une tâche en Maven Site ou pour Sonar.

    Cdt

  • @ Tous: Désolé pour mes réponses tardives.

    @ Stéfanie: Certainement, je pense qu’il faudra continuer à implémenter les idées d’amélioration susvisées, surtout que le besoin y est.

    @ Franck: Il est prévu qu’il soit disponible sur http://code.google.com/p/xebia-france/

    @ Olivier: Certes le projet Tattletale de JBoss offre une vision intéressante des dépendances, mais le but recherché étant d’avoir un artefact sous forme d’une mind map (ce qui n’est pas le cas avec Tattletale); on aurait pu également penser à JDepend Maven Plugin (mvn jdepend:generate).

    Cordialement.

  • Bonjour,

    Le plugin est désormais disponible sur le repo maven:
    http://repo2.maven.org/maven2/fr/xebia/maven/plugins/mindmap-maven-plugin/1.0.0/
    Vous pouvez l’intégrer dans vos projets et l’exécuter dans la phase « package » en ajoutant la configuration suivante dans le pom:

    <build>
      <plugins>
        <plugin>
          <groupId>fr.xebia.maven.plugins</groupId>
          <artifactId>mindmap-maven-plugin</artifactId>
          <version>1.0.0</version>
          <executions>
            <execution>
              <phase>package</phase>
              <goals>
                <goal>mindmap</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
    

    Cordialement.

  • Le projet est désormais hébergé sur GitHub à l’adresse suivante: https://github.com/ielfatmi/mindmap-maven-plugin.
    Une nouvelle version vient d’être publiée (1.0.1) et apporte la prise en compte du scope de la dépendance dans la mind map.

Laisser un commentaire