Affichage des articles dont le libellé est Plugin Maven. Afficher tous les articles
Affichage des articles dont le libellé est Plugin Maven. Afficher tous les articles

vendredi 1 novembre 2013

Tester ses plugins Maven

Lorsqu'il y a développement, il y a généralement test.
Mais tester un plugin (Mojo) Maven n'est pas évident. Il existe plusieurs solutions, plus ou moins pratique.
1 - Tester sans framework dédié.
Pour tester un mojo Maven (une classe Java associée à un goal), le plus simple consiste a déclarer tous les attributs exposés à Maven en protected ou en package private.
Par exemple, un Mojo affichant une chaine de caractère.
public class ShowMessageMojo extends AbstractMojo {
 /**
  * @parameter
  * @required
  */
 private String message ;
    
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info(message) ;
    }
}

Pour tester ce mojo, il est nécessaire de changer private String message ; en protected String message ;. Ainsi, il est possible de créer un classe de test qui hérite de ce mojo et qui ajoutera un getter/setter.
Il est aussi tout a fait possible, dans le mojo du départ de mettre des getter/setter. Ces getter/setter seront en plus utilisé par Maven pour modifier l'attribut.

Ensuite, une simple classe JUnit permet de tester.
Le gros inconvénient viendra lorsque le mojo voudra récupérer le projet Maven, les repositories...
2 - Tester avec maven-plugin-testing-harness
Pour pouvoir exécuter un mojo dans un contexte Maven, il existe le plugin maven-plugin-testing-harness.
Dans l'exemple suivant, voici la dépendance utilisée :
<dependency>
  <groupId>org.apache.maven.plugin-testing</groupId>
  <artifactId>maven-plugin-testing-harness</artifactId>
  <version>1.3</version>
  <scope>test</scope>
</dependency>

Reprise de l'exemple précédent, mais avec l'ajout du projet maven :
/**
 * @goal showMessage
 * @requiresProject true
 * 
 * @author Emeric MARTINEAU
 */
public class ShowMessageMojo extends AbstractMojo {
 /**
  * Le POM.
  * 
  * @parameter expression="${project}"
  * @required
  */
 protected MavenProject project;
 
 /**
  * @parameter
  * @required
  */
 private String message ;
    
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info(project.getBasedir()) ;
    }
}

Comment alors créer un projet maven dans le cadre d'un test ?
Pour cela, une classe de test dérivant de AbstractMojoTestCase doit être créée.

Cette classe met a disposition un ensemble de méthode qui facilite le test.

Il faut au préalable, créer un fichier pom.xml qui appel le plugin, exemple :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                      http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>group-id-bidon</groupId>
    <artifactId>artifact-id-bidon</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>Test plugin</name>

    <build>
        <plugins>
            <plugin>
                <groupId>org.mycompany.plugin</groupId>
                <artifactId>maven-toto-plugin</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <configuration>
                    <message>SRVF_TestMaven</message>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Ce fichier de pom permet de récupérer une instance du plugin/mojo.
Voici comment instancier le mojo et l'exécuter :
// Par convention, le classe de test porte le nom de la
// classe du mojo, suffixé avec "Test".
public class ShowMessageMojoTest extends AbstractMojoTestCase {
    /**
     * @throws Exception if any
     */
    public void testMojo() throws Exception
    {
        // Lecture du fichier pom pour initiliser le context Plexus
        File pom = getTestFile( "src/test/resources/pom.xml" );
        assertNotNull( pom );
        assertTrue( pom.exists() ) ;
        
        // Recuperation du mojo par le nom du goal
        ShowMessageMojo myMojo = (ShowMessageMojo) lookupMojo( "showMessage", pom );
        assertNotNull( myMojo );
                
        // Execution du mojo
        myMojo.execute();
    }
}

Le résultat ?
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.469 sec <<< FAILURE!
testMojo(ShowMessageMojoTest)  Time elapsed: 0.283 sec  <<< ERROR!
java.lang.NullPointerException: null
 at ShowMessageMojo.execute(ShowMessageMojo.java:31)
 at ShowMessageMojoTest.testMojo(ShowMessageMojoTest.java:30)

NullPointerException à la ligne getLog().info(project.getBaseDir()) ;. Pourquoi ?
Parce que expression="${project}" n'est pas interprété.

Il faudra donc, manuellement affecter une valeur, juste avant l'exécution du mojo, avec :
public class ShowMessageMojoTest extends AbstractMojoTestCase {
    /**
     * @throws Exception if any
     */
    public void testMojo() throws Exception
    {
        // Lecture du fichier pom pour initiliser le context Plexus
        File pom = getTestFile( "src/test/resources/pom.xml" );
        assertNotNull( pom );
        assertTrue( pom.exists() ) ;

        // Recuperation du mojo par le nom du goal
        ShowMessageMojo myMojo = (ShowMessageMojo) lookupMojo( "showMessage", pom );
        assertNotNull( myMojo );

        // Creation du projet maven
        final MavenProject mvnProject = new MavenProject() ;
        mvnProject.setBasedir(new File("/")) ;
        setVariableValueToObject(myMojo, "project", mvnProject) ;
  
        // Execution du mojo
        myMojo.execute();
    }
}

Il en sera ainsi pour toute les proriétés.

Evidement, cela reste encore un peu manuel. Mais d'autre méthode existe pour configurer le mojo.

Il est possible d'utiliser la ligne configureMojo(myMojo, "maven-toto-plugin", pom) ;.
public class ShowMessageMojoTest extends AbstractMojoTestCase {
    /**
     * @throws Exception if any
     */
    public void testMojo() throws Exception
    {
        // Lecture du fichier pom pour initiliser le context Plexus
        File pom = getTestFile( "src/test/resources/pom.xml" );
        assertNotNull( pom );
        assertTrue( pom.exists() ) ;

        // Recuperation du mojo par le nom du goal
        ShowMessageMojo myMojo = (ShowMessageMojo) lookupMojo( "showMessage", pom );
        assertNotNull( myMojo );

        configureMojo(myMojo, "maven-toto-plugin", pom) ;
  
        // Execution du mojo
        myMojo.execute();
    }
}

Le seul inconvénient de ce système, c'est qu'il ne gère pas la possiblilité d'avoir dans le même fichier de configuration, plusieurs goal avec la baslise <execution />.
Il faut donc pour chaque goal créer un pom.

lundi 29 juillet 2013

Utilisation de la balise <configuration> dans un plugin maven

Lorsque dans un pom, on active un plugin maven, il arrive de le configurer via une balise configuration.
Prenons l'exemple de maven war plugin :
<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <webResources>
            <resource>
              <!-- this is relative to the pom.xml directory -->
              <directory>resource2</directory>
            </resource>
          </webResources>
        </configuration>
      </plugin>
    </plugins>
  </build>
  ...
</project>

Cette balise est très intéressante.
Imaginons que nous souhaitons créer un plugin qui exécute des commandes shell, et en plus dans un certain ordre, comment faire ?

Partons du postulat que notre plugin se configure comme suit :
<plugin>
 <groupId>org.test.plugins</groupId>
 <artifactId>maven-montest-plugin</artifactId>
 <version>1.0.0-SNAPSHOT</version>
 <executions>
  <execution>
   <phase>generate-sources</phase>
   <goals>
    <goal>shell-exec</goal>
   </goals>
   <configuration>
    <script name="toto">
     <run name="coucou1" exec="commande ${project.version}"></run>
     <run name="coucou2">nom du script ${project.version}</run>
    </script>
   </configuration>
  </execution>
 </executions>
</plugin> 

Pour pourvoir reproduire le comportement de la balise webResources. Il faut utiliser un objet du type org.codehaus.plexus.configuration.PlexusConfiguration.
Ici, nous aurons donc un attribut script :
/**
 * @goal shell-exec
 * @threadSafe true
 */
public class ShellExecutionMojo extends AbstractMojo {
    /**
     * The script to execute
     *
     * @parameter
     */
    private PlexusConfiguration script;
 
 ...
}

Il est important de noter, qu'il n'est pas possible d'avoir un tableau ou une liste à ce niveau.

La classe PlexusConfiguration contient une méthode getName() qui retourne le nom de la balise XML. Dans notre cas run.
Pour récupérer un attribut de la balise, il suffit d'appeler la méthode getAttribute(String). Cette méthode renvoit null si la balise n'est pas présente.
Pour récupérer le contenu de la balise (exemple coucou2), il faut utiliser la méthode getValue().
S'il y a des balises enfants, il faudra utiliser les méthodes getChild().
Si on souhaite récupérer la liste des enfants, il faudra utiliser la méthode getChildren(), qui renvoit un tableau et getChildCount() pour savoir le nombre d'enfants.

Voici l'exemple complet :
package org.test.plugins;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.configuration.PlexusConfiguration;

import org.test.plugin.util.PlexusConfigurationComparator;

/**
 * @goal shell-exec
 * @threadSafe true
 */
public class ShellExecutionMojo extends AbstractMojo {
    /**
     * The script to execute
     *
     * @parameter
     */
    private PlexusConfiguration script;
 
    /**
     * List of script to run
     */
    private List listScriptToRun ;
    
 @Override
 public void execute() throws MojoExecutionException, MojoFailureException {
  PlexusConfiguration[] children = script.getChildren() ;
  
  getLog().info("Running ShellExecutorMojo") ;
  
  listScriptToRun = new ArrayList(children.length) ;
  
  for (PlexusConfiguration currentNode : children) {
   if ("run".equals(currentNode.getName())) {
    listScriptToRun.add(currentNode) ;
   }
  }
  
  Collections.sort(listScriptToRun, new PlexusConfigurationComparator(getLog())) ;
  
  for (PlexusConfiguration currentNode : listScriptToRun) {
   getLog().info("> " + currentNode.getAttribute("name")) ;
   
   execScript(currentNode) ;
  }  
  
  getLog().info("Fin") ;
 }
 
 private void execScript(final PlexusConfiguration currentNode) {
  String cmd = currentNode.getAttribute("exec") ;
  
  if (StringUtils.trimToNull(cmd) == null) {   
   cmd = currentNode.getValue() ;
   
   getLog().info("Balise = " + cmd) ;
  } else {
   getLog().info("Balise exec = " + cmd) ;
  }
 }
}

La classe de tri :
/**
 * 
 */
package org.test.plugin.util;

import java.util.Comparator;

import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.logging.Log ;

/**
 * Sort PlexusConfiguration object
 * @author Emeric MARTINEAU
 */
public class PlexusConfigurationComparator implements Comparator {
 /**
  * Logger
  */
 private Log logger ;
 
 public PlexusConfigurationComparator(final Log log) {
  this.logger = log ;
 }
 
 @Override
 public int compare(PlexusConfiguration object1, PlexusConfiguration object2) {
  final String orderString1 = object1.getAttribute("order") ;
  final String orderString2 = object2.getAttribute("order") ;
  
  int order1 = convertOrder(orderString1) ;
  int order2 = convertOrder(orderString2) ;
  
  
  return order1 - order2 ;
 }
 
 /**
  * Converti l'order
  * @param orderString
  * @return
  */
 private int convertOrder(final String orderString) {
  int order = 0 ;
  
  // If string is null, no order attribut found
  if (StringUtils.trimToNull(orderString) != null) {  
   try {  
    order = Integer.valueOf(orderString) ; 
   } catch(final NumberFormatException e) { 
    logger.warn(String.format(
         "Invalide 'order' tag value = '%s'", orderString)) ;
   } 
  }
  
  return order ;
 }

}

lundi 22 avril 2013

Vérifier qu'un artifact maven existe v2

Dans le post précédent, un première solution a été donnée pour savoir si un artifact existe ou nom.
Le problème, c'est que cette vérification se base sur le pom.xml, or il est possible d'avoir un pom sans jar par exemple.

Le nouvel exemple, va être plus restrictif.
Il va indiquer si un artifact est présent sur un repository distant. Il peut être très facilement adapté pour supporter le repository local (il y a une log dans le code pour ce cas).

Le code ne supporte que les versions release (1.2.0), les versions SNPASHOT, RELEASE, LASTEST ne sont pas supportées.
Il faudra au préalable résoudre la version.

Le principe est donc :
  • récupérer le descriptor,
  • regarder s'il y a un repository,
  • s'il y a un repository, construire un requête HTTP et lire le status (réponse).

voici le code :
package org.test.maven.plugin;

import java.io.File;
import java.util.List;

import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.resolution.ArtifactDescriptorException;
import org.sonatype.aether.resolution.ArtifactDescriptorRequest;
import org.sonatype.aether.resolution.ArtifactDescriptorResult;
import org.sonatype.aether.util.artifact.DefaultArtifact;

/**
 * Ce plugin indique si l'artefact existe dans un repository distant pour une version RELEASE uniquement
 * 
 * @goal check-exists-artifact
 */
public class VerifierArtefactExistant2 extends AbstractMojo {
    /**
     * @parameter default-value="${localRepository}" 
     */
    private ArtifactRepository localRepository;    
    
    /**
     * The entry point to Aether, i.e. the component doing all the work.
     * 
     * @component
     */
    private RepositorySystem repoSystem;

    /**
     * The current repository/network configuration of Maven.
     * 
     * @parameter default-value="${repositorySystemSession}"
     * @readonly
     */
    private RepositorySystemSession repoSession;

    /**
     * List of Remote Repositories used by the resolver
     * 
     * @parameter expression="${project.remoteArtifactRepositories}"
     * @readonly
     * @required
     */
    protected List<ArtifactRepository> remoteRepos;

    /**
     * Group Id of Artifact 
     * 
     * @parameter default-value="${project.groupId}"
     * @required
     */
    private String groupId;

    /**
     * Name of Artifact
     * 
     * @parameter default-value="${project.artifactId}"
     * @required
     */
    private String artifactId;

    /**
     * Version of Artifact
     * 
     * @parameter default-value="${project.version}"
     */
    private String version;

    /**
     * Type of Artifact (War,Jar,etc)
     * 
     * @parameter default-value="${project.packaging}"
     * @required
     */
    private String type;

    /**
     * Classifier for Artifact (tests,sources,etc)
     * 
     * @parameter default-value="${project.classifier}"
     */
    private String classifier;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            ArtifactDescriptorResult artefactFile = getDescriptor() ;
            
            boolean exists = checkIfRemoteFileExists(artefactFile) ;
            
            if (!exists)
            {
                // Si l'artefact n'existe pas, il faut supprimer le repertoire car maven
                // 'note' le fait qu'il n'a pas trouve et pour une version RELEASE, il ne
                // tentera pas un nouveau telechargement.
                //
                // Attention, dans le cas d'un artifact present uniquement en local, il ne 
                // faut pas le supprimer
                if (artefactFile.getRepository() == null)
                {
                    getLog().debug("Suppression des informations dans le repository local") ;
                    cleanArtifactLocalRepositoryData(artefactFile) ;
                }
            }
        } catch (ArtifactDescriptorException e) {
            throw new MojoExecutionException("Impossible de recuperer les metadata de l'artefact", e);
        }
    }

    /**
     * Renvoi le descriptor de l'artefact.
     * 
     * @return le descriptor
     * @throws ArtifactDescriptorException
     *             en cas d'erreur lors de la recherche de l'artefact
     */
    private ArtifactDescriptorResult getDescriptor() throws ArtifactDescriptorException {
        DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId,
                classifier, type, version);
        
        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest(artifact,
                RepositoryUtils.toRepos(remoteRepos), null) ;
        ArtifactDescriptorResult descriptor = repoSystem.readArtifactDescriptor(repoSession, request) ;        

        return descriptor ;
    }
    
    /**
     * Suppression du repertoire
     * 
     * @param result resultat de la recherche de l'artefact descriptor
     */
    private void cleanArtifactLocalRepositoryData(ArtifactDescriptorResult result)
    {
        // Le chemin d'acces au repository local + construction du chemin vers 
        // les données
        final File dirToClean = new File(localRepository.getBasedir(),
                String.format("%s/%s/%s",
                        result.getArtifact().getGroupId().replace(".", "/"),
                        result.getArtifact().getArtifactId(),
                        result.getArtifact().getVersion())) ;
        
        if (!dirToClean.exists()) {
            getLog().warn(
                    String.format("Le repertoire '%s' n'existe pas.",
                            dirToClean.getAbsolutePath())) ;
        } else {
            if (getLog().isDebugEnabled())
            {
                getLog().debug(
                        String.format("Suppression du repertoire '%s'.",
                                dirToClean.getAbsolutePath())) ;                    
            }
            
            deleteDirectoryOnExit(dirToClean) ;
        }
    }
    
    /**
     * Supprime un repertoire et son contenu au shutdown de la VM car maven lock les fichiers.
     * 
     * @param directory repertoire a supprimer
     */
    private void deleteDirectoryOnExit(File directory)
    {
        if (directory.exists())
        {
            File[] listFile = directory.listFiles() ;
            
            for(File currentFile : listFile)
            {
                if (currentFile.exists())
                {
                    currentFile.deleteOnExit() ;
                }
            }
            
            directory.deleteOnExit() ;
        }
    }    
    
    /**
     * Verifie qu'une version release existe bien sur le serveur.
     * NE FONCTIONNE PAS POUR version = 'SNAPSHOT', 'LASTEST', 'RELEASE'
     * Il faudra resoudre la version d'abord.
     * 
     * @param artefactFile
     * @return
     */
    private boolean checkIfRemoteFileExists(final ArtifactDescriptorResult artefactFile)
    {
        boolean fileExits = false ;
        
        org.sonatype.aether.repository.ArtifactRepository repository = artefactFile.getRepository() ;
        
        if (repository != null)
        {        
            ArtifactRepository selectedRepository = rechercherRemoteRepository(repository) ;
                        
            if (selectedRepository == null)
            {
                // La variable repository est un repository local
                // Cela veut dire que l'artifact n'existe pas a distance, mais seulement en local,
                // c'est une erreur !
                getLog().warn("L'artifact est present en local mais pas sur le serveur depot distant !") ;
            }
            else
            {
                fileExits = verifierExistanceArtifactDistant(artefactFile, selectedRepository) ;
            }

        }
        
        return fileExits ;
    }
    
    /**
     * Recherche le repository distant dont l'ID correspond a celui en entree
     * 
     * @param repository repository Aether
     * 
     * @return repository maven
     */
    private ArtifactRepository rechercherRemoteRepository(org.sonatype.aether.repository.ArtifactRepository repository)
    {
        ArtifactRepository selectedRepository = null ;
        
        // Recherche le repository distant d'ou le l'artifact a ete recuperer
        for(ArtifactRepository currentRepository : remoteRepos)
        {
            if (repository.getId().equals(currentRepository.getId()))
            {
                selectedRepository = currentRepository ;
            }
        }
        
        return selectedRepository ;
    }
    
    /**
     * Verifie que dans le repository distant, la matiere existe bien.
     * 
     * @param artefactFile artifact
     * @param remoteRepository repository disant
     * @return
     */
    private boolean verifierExistanceArtifactDistant(ArtifactDescriptorResult artefactFile, ArtifactRepository remoteRepository)
    {
        boolean remoteFileExits = false ;
        
        Artifact artifact = artefactFile.getArtifact() ;
        
        // Creer le nom du fichier avec les informations
        String fileName ;
        
        if (StringUtils.isBlank(artifact.getClassifier()))
        {
            fileName = String.format("%s-%s.%s", artifact.getArtifactId(), artifact.getVersion(), artifact.getExtension()) ;
        }
        else
        {
            fileName = String.format("%s-%s-%s.%s", artifact.getArtifactId(), artifact.getVersion(), artifact.getClassifier(), artifact.getExtension()) ;
        }            
        
        // Construit l'URL
        String url = String.format("%s%s/%s/%s/%s", remoteRepository.getUrl(), artifact.getGroupId().replace(".", "/"),
                artifact.getArtifactId(), artifact.getVersion(), fileName) ;
        
        // Ouvre une connexion vers le fichier
        try {
            getLog().info(String.format("Verification de l'existance de l'artifact '%s'", url)) ;
            
            URL u = new URL(url) ;

            HttpURLConnection http = (HttpURLConnection) u.openConnection() ;
            int status = http.getResponseCode() ;

            if (status >= 200 && status < 300)
            {
                remoteFileExits = true;
                getLog().info("Artifact trouve") ;
            }
            else
            {
                getLog().warn("Les informations de description (pom.xml) de l'artifact est present sur le serveur depot distant, "
                        + "mais l'artifact est introuvable sur ce serveur !") ;
            }
            
            http.disconnect() ;
        } catch (MalformedURLException e) {
            getLog().error(
                    String.format("Probleme de creation de l'url '%s'", url), e) ;
        } catch (IOException e) {
            getLog().error(
                    String.format("Impossible d'ouvrir une connexion reseau vers '%s'", url), e) ;
        }        
        
        return remoteFileExits ;
    }    
}

Le code commence à devenir conséquent, mais il répond bien à la problématique.

lundi 15 avril 2013

Vérifier qu'un artifact maven existe

Précédemment, un exemple de récupération de version d'un artifact maven a été présenté.
Maintenant, une demande qui n'est pas nouvelle est "comment savoir si un artifact existe sur le repository local ou distant ?".
En cherchant sur internet, il faut bien avouer que les seules réponses trouvées sont :
  • tenter de télécharger l'artifact (http://stackoverflow.com/questions/4809238/maven-how-to-check-if-an-artifact-exists),
  • faire une requête HTTP sur un serveur type Nexus (http://www.jarradmbattaglia.com/2012/02/08/maven-how-to-check-if-an-artifact-in-nexus-using-http/)

Rien de satisfaisant, surtout si le but est de savoir si l'artifact existe sans télécharger l'artifact en question.

Le principe est le même que l'exemple précédent pour connaitre la version. Mais, l'astuce va être récupérer le descriptor. Et ensuite ?
Est-ce comme pour la résolution de version, si l'artifact n'existe pas, il n'y a pas d'erreur levée, ou un paramètre indiquant une erreur ?

La réponse est oui mais !
Lorsque les informations sur le descriptor est récupéré, il est possible de savoir sur quel repository il a été récupéré.
De sorte, que si il n'y a pas de repository indiqué, cela signifie que l'artifact n'a pas été trouvé !

Toutefois, ce n'est pas encore fini.
En effet, si l'artefact n'existe pas, une entrée est quand même créé dans le repository local et dans ce cas, s'il s'agit d'une version release, et qu'elle existe ensuite, maven n'essaiera pas de un re-téléchargement, car il note que la version n'existe pas.
Il faut alors supprimer le répertoire du repository local mais seulement à l'arrêt de la JVM car maven verrouille le répertoire.

Voici alors le code :
package org.test.maven.plugin;

import java.io.File;
import java.util.List;

import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.resolution.ArtifactDescriptorException;
import org.sonatype.aether.resolution.ArtifactDescriptorRequest;
import org.sonatype.aether.resolution.ArtifactDescriptorResult;
import org.sonatype.aether.util.artifact.DefaultArtifact;

/**
 * Ce plugin indique si l'artefact existe dans un repository (local ou distant)
 * 
 * @goal check-exists-artifact
 */
public class VerifierArtefactExistant extends AbstractMojo {
    /**
     * @parameter default-value="${localRepository}" 
     */
    private ArtifactRepository localRepository;    
    
    /**
     * The entry point to Aether, i.e. the component doing all the work.
     * 
     * @component
     */
    private RepositorySystem repoSystem;

    /**
     * The current repository/network configuration of Maven.
     * 
     * @parameter default-value="${repositorySystemSession}"
     * @readonly
     */
    private RepositorySystemSession repoSession;

    /**
     * List of Remote Repositories used by the resolver
     * 
     * @parameter expression="${project.remoteArtifactRepositories}"
     * @readonly
     * @required
     */
    protected List<ArtifactRepository> remoteRepos;

    /**
     * Group Id of Artifact 
     * 
     * @parameter default-value="${project.groupId}"
     * @required
     */
    private String groupId;

    /**
     * Name of Artifact
     * 
     * @parameter default-value="${project.artifactId}"
     * @required
     */
    private String artifactId;

    /**
     * Version of Artifact
     * 
     * @parameter default-value="${project.version}"
     */
    private String version;

    /**
     * Type of Artifact (War,Jar,etc)
     * 
     * @parameter default-value="${project.packaging}"
     * @required
     */
    private String type;

    /**
     * Classifier for Artifact (tests,sources,etc)
     * 
     * @parameter default-value="${project.classifier}"
     */
    private String classifier;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            ArtifactDescriptorResult artefactFile = getDescriptor() ;
            
            if (artefactFile.getRepository() == null)
            {                                
                getLog().error("Artifact inexistant") ;    
                
                // Si l'artefact n'existe pas, il faut supprimer le repertoire car maven
                // 'note' le fait qu'il n'a pas trouve et pour une version RELEASE, il ne
                // tentera pas un nouveau telechargement
                cleanArtifactLocalRepositoryData(artefactFile) ;
            }            
            else
            {
                getLog().info("Artefect trouve");
            }
        } catch (ArtifactDescriptorException e) {
            throw new MojoExecutionException("Impossible de recuperer les metadata de l'artefact", e);
        }
    }

    /**
     * Renvoi le descriptor de l'artefact.
     * 
     * @return le descriptor
     * @throws ArtifactDescriptorException
     *             en cas d'erreur lors de la recherche de l'artefact
     */
    private ArtifactDescriptorResult getDescriptor() throws ArtifactDescriptorException {
        DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId,
                classifier, type, version);
        
        ArtifactDescriptorRequest request = new ArtifactDescriptorRequest(artifact,
                RepositoryUtils.toRepos(remoteRepos), null) ;
        ArtifactDescriptorResult descriptor = repoSystem.readArtifactDescriptor(repoSession, request) ;        

        return descriptor ;
    }
    
    /**
     * Suppression du repertoire
     * 
     * @param result resultat de la recherche de l'artefact descriptor
     */
    private void cleanArtifactLocalRepositoryData(ArtifactDescriptorResult result)
    {
        // Le chemin d'acces au repository local + construction du chemin vers 
        // les données
        final File dirToClean = new File(localRepository.getBasedir(),
                String.format("%s/%s/%s",
                        result.getArtifact().getGroupId().replace(".", "/"),
                        result.getArtifact().getArtifactId(),
                        result.getArtifact().getVersion())) ;
        
        if (!dirToClean.exists()) {
            getLog().warn(
                    String.format("Le repertoire '%s' n'existe pas.",
                            dirToClean.getAbsolutePath())) ;
        } else {
            if (getLog().isDebugEnabled())
            {
                getLog().debug(
                        String.format("Suppression du repertoire '%s'.",
                                dirToClean.getAbsolutePath())) ;                    
            }
            
            deleteDirectoryOnExit(dirToClean) ;
        }
    }
    
    /**
     * Supprime un repertoire et son contenu au shutdown de la VM car maven lock les fichiers.
     * 
     * @param directory repertoire a supprimer
     */
    private void deleteDirectoryOnExit(File directory)
    {
        if (directory.exists())
        {
            File[] listFile = directory.listFiles() ;
            
            for(File currentFile : listFile)
            {
                if (currentFile.exists())
                {
                    currentFile.deleteOnExit() ;
                }
            }
            
            directory.deleteOnExit() ;
        }
    }    
}

Le pom qui lance le goal :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test.truc</groupId>
    <artifactId>kiki</artifactId>
    <version>1.2.1</version>
    <packaging>jar</packaging>
    <name>test-maven</name>

    <build>
        <plugins>
            <plugin>
                <groupId>rg.test.maven.plugin</groupId>
                <artifactId>maven-test-plugin</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <executions>
                    <execution>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>check-exists-artifact</goal>
                        </goals>
                        <configuration>
                            <groupId>fr.truc.bidule</groupId>
                            <artifactId>coco</artifactId>
                            <version>1.0.11</version>
                            <type>jar</type>

                            <propertyResult>totoVariable</propertyResult>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <tasks>
                                <echo>Hello world! ${totoVariable}</echo>
                            </tasks>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>


Une réflexion vient à l'esprit, le contrôle se fait sur le pom.xml, mais il est possible que celui-ci soit présent, sans pour autant que le jar par exemple, exite lui.
La solution sera donné la prochaine fois.

lundi 8 avril 2013

Résoudre la version d'un artefact Maven avec Maven 3

Il peut être utile, dans le cadre d'un projet de résoudre la version d'un artefact lorsqu'une version générique est passée en paramètre (RELEASE, LATEST, x.x.x-SNAPSHOT).
Pour ce faire, il n'y a pas d'autre choix que de créer un plugin maison, pour enchaîner avec un traitement particularité.
Pour ce faire, il est nécessaire de se plonger dans la documentation volumineuse des API maven. De plus, l'API a évolué en version 3 (même si l'API v2 est toujours disponible).

Dans un premier temps, il faut créer le pom adéquat :
package org.test.maven.plugin;

import java.util.List;

import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.resolution.ArtifactResolutionException;
import org.sonatype.aether.resolution.VersionRequest;
import org.sonatype.aether.resolution.VersionResolutionException;
import org.sonatype.aether.resolution.VersionResult;
import org.sonatype.aether.util.artifact.DefaultArtifact;

/**
 * Ce plugin affiche la version resolue d'un artefact
 * 
 * @goal version-artifact
 */
public class VersionArtefact extends AbstractMojo {
 /**
  * The entry point to Aether, i.e. the component doing all the work.
  * 
  * @component
  */
 private RepositorySystem repoSystem;

 /**
  * The current repository/network configuration of Maven.
  * 
  * @parameter default-value="${repositorySystemSession}"
  * @readonly
  */
 private RepositorySystemSession repoSession;

 /**
  * List of Remote Repositories used by the resolver
  * 
  * @parameter expression="${project.remoteArtifactRepositories}"
  * @readonly
  * @required
  */
 protected List<ArtifactRepository> remoteRepos;

 /**
  * Group Id of Artifact 
  * 
  * @parameter default-value="${project.groupId}"
  * @required
  */
 private String groupId;

 /**
  * Name of Artifact
  * 
  * @parameter default-value="${project.artifactId}"
  * @required
  */
 private String artifactId;

 /**
  * Version of Artifact
  * 
  * @parameter default-value="${project.version}"
  */
 private String version;

 /**
  * Type of Artifact (War,Jar,etc)
  * 
  * @parameter default-value="${project.packaging}"
  * @required
  */
 private String type;

 /**
  * Classifier for Artifact (tests,sources,etc)
  * 
  * @parameter default-value="${project.classifier}"
  */
 private String classifier;

 @Override
 public void execute() throws MojoExecutionException, MojoFailureException {
  try {   
   VersionResult version = getVersionArtifact() ;
   
   getLog().info(version.toString());
  } catch (VersionResolutionException e) {
   throw new MojoExecutionException("Impossible de recuperer la version de l'artefact", e);
  }
 }
 
 /**
  * Renvoi le fichier correspondant à l'artefact.
  * Prend en charge le téléchargement dans le repo local.
  *
  * @return le fichier demandé
  * @throws ArtifactResolutionException en cas d'erreur lors de la recherche de l'artefact
  */
    private VersionResult getVersionArtifact() throws VersionResolutionException {
  DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId,
    classifier, type, version);
  
        VersionRequest request = new VersionRequest(artifact, RepositoryUtils.toRepos(remoteRepos), null);
        VersionResult version = repoSystem.resolveVersion(repoSession, request) ; 
  
        return version;
    } 
}

Il est nécessaire de récupérer les repositories Maven et l'interface permettant de les piloter.
L'interface s’appelle Aether.
Ensuite, le système fonctionne avec un principe de requête et de réponse.
Une requête avec l'artifactId, groupId... est créé, l'interface est appelé est le résultat est récupéré.

Attention ! Si l'artefact n'existe pas et qu'une version générique est passée en paramètre, un exception est levé. Sinon, la version renvoyée est celle donnée en paramètre.
Il n'y a donc pas moyen de savoir si l'artefact existe. En réalité si, mais vous ne trouverez rien sur ce sujet sur internet.
Pour cela, rendez-vous la semaine prochaine pour connaitre l'astuce.

Voici alors le code (très simple) :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.test.maven.plugin</groupId>
    <artifactId>test-plugin-maven</artifactId>
    <packaging>maven-plugin</packaging>
    <version>1.0.0-SNAPSHOT</version>
 
 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-plugin-plugin</artifactId>
    <version>3.0.4</version>
   </plugin>
  </plugins>
 </build>

 <dependencies> 
  <dependency>
   <groupId>org.apache.maven</groupId>
   <artifactId>maven-aether-provider</artifactId>
   <version>3.0.4</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.apache.maven</groupId>
   <artifactId>maven-core</artifactId>
   <version>3.0.3</version>
   <scope>compile</scope>
  </dependency>
 </dependencies>

</project>

Ainsi, une version SNAPSHOT aura comme retour :
1.2.1-20121227.145212-1 @ nexus-libraries (http://xxxxxxx:8081/nxus/content/groups/public/, releases+snapshots)

lundi 4 mars 2013

Ajouter un propriété dynamiquement dans un plugin maven

Il peut être parfois utile dans un plugin maven, développé par vos soins, d'ajouter des properties qui pourront être ensuite utilisée dans le pom.xml.

Pour ce faire, rien de bien compliqué.
Il est nécessaire de récupérer l'objet projet de Maven et d'ajouter un propriété :
// injection de l'objet project
/** @parameter default-value="${project}" */
private org.apache.maven.project.MavenProject project ;

// et dans la méthode execute(), ajouter :
project.getProperties().setProperty("maPropriete", maValeur) ;

vendredi 11 janvier 2013

Création de plugin maven [2/2]

Dans l'article précédent, il a été vu comment créer un plugin maven très simple.

Il va être abordé un plugin pas plus difficile, mais avec quelques différences (nécessite un projet, paramètre obligatoire...).

Deuxième plugin maven


Dans le même projet, une classe qui liste un répertoire va être créé :
package org.test.maven.plugin;

import java.io.File;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;

/**
 * 
 * @author Emeric MARTINEAU
 * @goal listDir
 * @requiresProject true
 */
public class ListDirectory extends AbstractMojo {

 /**
  * @parameter default-value="${project}"
  * @required
  * @readonly
  */
 private MavenProject project; 
 
 /**
  * @required
  * @parameter expression="${dir}"
  */
 private String dir; 
 
 /* (non-Javadoc)
  * @see org.apache.maven.plugin.Mojo#execute()
  */
 @Override
 public void execute() throws MojoExecutionException, MojoFailureException {
  final File directory = new File(dir) ;
  
  if (!directory.exists()) {
   getLog().error(dir.concat(" doesn't exists !")) ;
   
   return ;
  }  
  
  // List file of directory
  final File[] fileList = directory.listFiles() ;
  
  File currentFile ;
  
  // For each file
  for(int index = 0; index < fileList.length; index++) {
   currentFile = fileList[index] ;
   
   if (currentFile.isDirectory()) {
    // Current file is directory
    getLog().info("<DIR> ".concat(currentFile.getName())) ;
   } else {
    getLog().info("      ".concat(currentFile.getName())) ;
   }
  }
  
  getLog().info("Basedir = " + project.getBasedir().getAbsolutePath()) ;
 }
}
Le goal est donc listDir et la classe nécessite un projet @requiresProject true.

Le paramètre dir est rendu obligatoire par la présence dans la java doc de @required.

Le paramètre project représente le projet maven (pom.xml).

Pour fonctionner, la dépendence suivante doit être ajoutée :
<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-core</artifactId>
    <version>3.0.3</version>
    <scope>compile</scope>
</dependency>

Voici le résutat si lancé sans pom :
mvn org.test.maven.plugin:test-plugin-maven:listDir

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.766s
[INFO] Finished at: Thu Dec 13 11:06:23 CET 2012
[INFO] Final Memory: 4M/245M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.test.maven.plugin:test-plugin-maven:1.0.0-SNAPSHOT:listDir (default-cli): Goal requires a project to execute but there is no POM in this directory (D:\Logiciels\Apache\apache-maven-3.0.3\bin). Please verify you invoked Maven from the correct directory. -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MissingProjectException
Voici le résultat si lancé avec un pom mais sans le paramètre :
mvn org.test.maven.plugin:test-plugin-maven:listDir

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building toto 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- test-plugin-maven:1.0.0-SNAPSHOT:listDir (default-cli) @ toto ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.359s
[INFO] Finished at: Thu Dec 13 11:17:21 CET 2012
[INFO] Final Memory: 4M/245M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.test.maven.plugin:test-plugin-maven:1.0.0-SNAPSHOT:listDir (default-cli) on project toto: The parameters 'dir' for goal org.test.maven.plugin:test-plugin-maven:1.0.0-SNAPSHOT:listDir are missing or invalid -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginParameterException
Voici le résultat lancé correctement :
mvn org.test.maven.plugin:test-plugin-maven:listDir -Ddir=/temp/

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building toto 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- test-plugin-maven:1.0.0-SNAPSHOT:listDir (default-cli) @ toto ---
[INFO]       1.jn1
[INFO]       intro.jn1
[INFO] <DIR> jill-binaries
[INFO] <DIR> jill-tiles
[INFO] <DIR> MAVEN
[INFO]       test.png
[INFO] Basedir = /temp
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.578s
[INFO] Finished at: Thu Dec 13 11:17:54 CET 2012
[INFO] Final Memory: 6M/309M
[INFO] ------------------------------------------------------------------------

L'objet MavenProject


La partie de code de la classe :
/**
 * @parameter default-value="${project}"
 * @required
 * @readonly
 */
private MavenProject project;
permet de récupéré via la propriété maven ${project} le projet maven.
Il est donc possible d’accéder à toute les propriétés du projet.
L'objet MavenProject est disponible dans la librairie maven-core.

Il ne reste plus qu'à se reporter à la documentation officiele : Maven - Guide to Developing Java Plugins.

lundi 7 janvier 2013

Création de plugin maven [1/2]

Maven permet de facilement (une fois son fonctionnement compris) de construire, livrer, installer un projet.

Maven fonctionne sur la base de plugin. Toutefois, il n'est pas rare qu'un besoin particulier s'exprime et qu'aucun plugin n'y réponde.
Il faudra alors écrire soit même un plugin.

L'écriture de plugin est relativement aisée. C'est plutôt ce que doit faire le plugin qui n'est pas trivial.

Il existe deux types de plugin. Celui qui nécessite un projet pour s'exécuter (donc un pom.xml), comme par exemple maven-compiler-plugin.
Et celui qu'il est possible de lancer sans projet.

Premier plugin maven

Pouc commencer, un plugin sans projet pas être créé. Il affichera un message dans les logs. Ce message sera paramètrable (donc facultatif).

Voici la classe du plugin :
package org.test.maven.plugin;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

/**
 * 
 * @author Emeric MARTINEAU
 * @goal hello
 * @requiresProject false
 */
public class HelloWorld extends AbstractMojo {

 /**
  * @parameter expression="${hello.str}" default-value="Hello World!"
  */
 private String helloStr;  
  
 @Override
 public void execute() throws MojoExecutionException, MojoFailureException {
  getLog().info(helloStr) ;
 }

}
Elle hérite nécessairement de la classe org.apache.maven.plugin.AbstractMojo.
Sera implémenté la méthode execute().

La configuration du plugin se fait via la JavaDoc et certaines balises @.
Ici pour la classe il y a le goal définit par @goal hello et si un projet est requis @requiresProject false.
Il est possible d'indiquer la phase avec @phase.

La classe accèpte un paramètre hello.str configuré avec @parameter expression="${hello.str}" default-value="Hello World!".
Il peut être valoriser en ligne de commande via -Dhello.str=xxxx ou dans un pom avec :
<configuration>
 <hello.str>Bonjour le monde !</hello.str>
</configuration>
Si aucun paramètre n'est communiqué, le paramètre prendra la valeur Hello World!.

Et son pom associé :
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.test.maven.plugin</groupId>
    <artifactId>test-plugin-maven</artifactId>
    <packaging>maven-plugin</packaging>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.0.4</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugin</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <goalPrefix>testPluginMaven</goalPrefix>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Exemple d'exécution sans paramètre :
mvn org.test.maven.plugin:test-plugin-maven:hello

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- test-plugin-maven:1.0.0-SNAPSHOT:hello (default-cli) @ standalone-pom ---
[INFO] Hello World!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.297s
[INFO] Finished at: Thu Dec 13 11:01:56 CET 2012
[INFO] Final Memory: 4M/245M
[INFO] ------------------------------------------------------------------------
Exemple d'exécution avec paramètre :
mvn org.test.maven.plugin:test-plugin-maven:hello -Dhello.str="bonjour ca va ?"

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- test-plugin-maven:1.0.0-SNAPSHOT:hello (default-cli) @ standalone-pom ---
[INFO] bonjour ca va ?
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.234s
[INFO] Finished at: Thu Dec 13 11:55:40 CET 2012
[INFO] Final Memory: 6M/309M
[INFO] ------------------------------------------------------------------------
Le fait que le plugin est exécuté sans pom se remarque à standalone-pom juste au dessus du texte affiché par le plugin.