Personnaliser Jenkins

Jenkins est un serveur d'intégration continue qui a la particularité d'être extensible, permettant une grande liberté dans la mise en place de cycles de déploiement adaptés à chaque besoin : ajout de phases lors des builds, création de rapports personnalisés...

Je vais donc présenter un exemple de plugin montrant comment il est possible d'ajouter d'une part une action globale à un projet et d'autre part un rapport personnalisé pour chaque construction (build).

Avant tout, un peu de concret, voici un aperçu de ce que cela donne :

Action du projetRapport personnalisé de build

Qu'est-ce qu'un plugin Jenkins ?

Packagé sous la forme d'un fichier *.hpi, il contient essentiellement :

  • Les classes Java d'extension
  • Les vues (tout ou parties de pages, fichiers *.jelly)
  • Les ressources (images...)

En mode développement, il est possible de créer un fichier *.hpl qui simule un lien direct vers l'environnement de développement, permettant les modifications à chaud sans avoir à redémarrer le serveur.

Jelly...?

Pour faire court, ce langage d'Apache est à mi-chemin entre XML et JSP : il rend possible, en XML, de décrire une page web (à base de HTML) et d'y insérer des appels à des beans Java.

Premier aperçu

Comme les choses sont bien faites, il est possible d'utiliser Maven pour générer un artefact contenant la structure par défaut d'un plugin. Rien de plus simple, en trois étapes (décrites en détails ici) :

  • Configurer Maven pour accéder aux repositories de Jenkins (pour récupérer les goals)
  • utiliser mvn hpi:create
  • "eclipsifier" le projet

Survolons de la structure du projet :

  • src/main/java : classique, les sources du plugin
  • src/main/ressources : contiendra essentiellement les fichiers *.jelly de nos vues
  • src/main/webapp : est le répertoire des ressources (images...)

La mise en place d'un plugin se base essentiellement sur quelques normes de codage :

  1. La classe principale du plugin doit être annotée avec @Extension. Elle servira de point d'entrée.
  2. Une page Jelly associée à une action com.mypackage.MyAction doit se situer dans le répertoire src/main/ressources/com/mypackage/MyAction
  3. Les noms des pages Jelly ont une incidence sur le interprétation : index.jelly sera considérée comme la page d'accueil de l'action, summary.jelly sera utilisée dans la page de présentation d'un build.
  4. L'API de Jenkins fournit une série de classes facilement extensibles qui déterminent à elles seules le rôle du code ajouté :
    • Etendre hudson.model.Action signifie mettre en place en classe agissant comme un Controlleur lors du clic sur un lien dans une page
    • Etendre hudson.tasks.BuildStepDescriptor permet d'ajouter des étapes aux builds
    • Implémenter hudson.model.RootAction ajoute un lien dans le menu statique de gauche...

En mode développement, pour tester votre plugin, il suffit de lancer la commande suivante sur votre projet :

mvn clean hpi:run

Maven va recompiler les sources et créer le fichier *.hpl qui sera déployé sur l'instance locale de Jenkins après son démarrage. Rien d'autre à faire! (Il arrivera parfois que toutes les sources ne soient pas actualisées, il faut donc supprimer manuellement ce fichier dans les répertoires de Jenkins)

En avant!

Mettons maintenant en oeuvre ces éléments :

Créer une action sur un projet

Le menu contextuel à un projet est créé à partir de hudson.model.TransientProjectActionFactory. Nous allons donc créer une nouvelle extension à partir de cette classe afin d'ajouter notre action :


import hudson.Extension;
import hudson.model.Action;
import hudson.model.TransientProjectActionFactory;
import hudson.model.AbstractProject;

import java.util.Collection;
import java.util.Collections;

@Extension
public class MyProjectsReportActionFactory extends TransientProjectActionFactory {

 @Override
 public Collection createFor(AbstractProject arg0) {
  return Collections.singleton(new MyProjectAction(arg0));
 }

}

Tout simplement! Notez la présence de la fameuse annotation @Extension.

Et maintenant, le code de l'action :


package com.mypackage; 

import hudson.model.Action;
import hudson.model.AbstractProject;

import java.text.SimpleDateFormat;

public class MyProjectAction implements Action {
 
 private AbstractProject project;

 private SimpleDateFormat sdf;

 public MyProjectAction(AbstractProject p) {
  this.setProject(p);
  sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 }

 public String getDisplayName() {
  return "Rapport perso";
 }

 public String getIconFileName() {
  return "/plugin/<artifact_id>/icon.jpg";
 }

 public String getUrlName() {
  return "MyProjectAction";
 }
 

 public AbstractProject getProject() {
  return project;
 }

 public void setProject(AbstractProject project) {
  this.project = project;
 }

 public String getLastBuildDate() {
  return sdf.format(project.getLastBuild().getTime());
 }
 
 public String getProjectURL() {
  return Jenkins.getInstance().getRootUrl() + project.getUrl();
 }
}

Petite description de cette classe :

  • getDisplayName() : est le libellé du lien hypertexte de l'action
  • getIconFileName() : est le chemin vers l'icône associée au lien. Important : comme spécifié plus haut, les ressources sont placées dans le répertoire webapp, mais une fois le plugin déployé, tout ce qui est sous ce répertoire se retrouve dans le dossier plugin de Jenkins, dans un répertoire nommé avec de l'artifact_id Maven du projet!
  • Enfin, les méthodes getProject(), getProjectURL() et getLastBuildDate() vont être utilisées dans la page Jelly.

Pour terminer, la vue Jelly de notre action (située à src/main/resources/com/mypackage/MyProjectAction/index.jelly) :


<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">

    <l:layout>
        <l:side-panel>
            <p>
            <a href="${it.projectURL}"><img src="/plugin/<artifactid>/up.png"/> Retour au Projet</a>
            </p>
        </l:side-panel>
        <!--<st:include it="${it.project}" page="sidepanel.jelly"/>-->
        <l:main-panel>
            <h1><img src="/plugin/<artifactid>/icon.jpg"/>Rapport perso</h1>
            <h2>Projet ${it.project.displayName}</h2>
            <p>Résultat : ${it.project.buildHealth.description}</p>
            <h3>Dernier build</h3>
            <p>Date : ${it.lastBuildDate}</p>
            <p>Etat : ${it.project.lastBuild.buildStatusSummary.message}</p>
        </l:main-panel>
    </l:layout>

</j:jelly>

Voici donc le résultat :

Personnaliser le détail d'un build

Imaginons que le build de votre projet produise un fichier personnalisé dont il serait bon d'afficher une présentation sur la page du résumé. Pour permettre l'analyse de ce fichier au moment imparti, il faut ajouter une étape au build pour exécuter notre action de parcours du rapport. Cette action sera alors disponible dans la configuration du projet :

Voici comment ajouter cette étape :


import hudson.Extension;
import hudson.model.AbstractProject;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;

@Extension
public class MyPluginDescriptor extends BuildStepDescriptor<Publisher> {
 
 public MyPluginDescriptor() {
  super(MyPluginPublisher.class);
 }

 @Override
 public boolean isApplicable(Class<? extends AbstractProject> arg0) {
  return true;
 }

 @Override
 public String getDisplayName() {
  return "My Plugin Descriptor";
 }

}

Cette classe permet simplement d'indquer une référence vers l'objet Publisher permettant le déclenchement d'une action particulière lors de la publication du résultat du build. Ci-dessous le publisher :


import org.kohsuke.stapler.DataBoundConstructor;

import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Recorder;

public class MyPluginPublisher extends Recorder {
 
 @DataBoundConstructor
 public MyPluginPublisher() {
 }

 public BuildStepMonitor getRequiredMonitorService() {
  return BuildStepMonitor.BUILD;
 }
 
 @Override
 public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) 
   throws InterruptedException, IOException {
  MyBuildAction action = new MyBuildAction(build);
  build.addAction(action);
  return true;
 }

}

On voit donc ici l'ajout de l'action qui va réaliser notre traitement :


import hudson.model.Action;
import hudson.model.AbstractBuild;

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

public class MyBuildAction implements Action {

 private AbstractBuild<?, ?> build;

 public MyBuildAction(AbstractBuild<?, ?> p) {
  this.setBuild(p);
 }

 /**
  * Returns null to not display action link
  */
 public String getDisplayName() {
  return null;
 }

 /**
  * Returns null to not display action link
  */
 public String getIconFileName() {
  return null;
 }

 /**
  * Returns null to not display action link
  */
 public String getUrlName() {
  return null;
 }

 public AbstractBuild<?, ?> getBuild() {
  return build;
 }

 public void setBuild(AbstractBuild<?, ?> build) {
  this.build = build;
 }

 public List<String[]> getBuildRapport() throws Exception {
  List<String[]> rapports = new ArrayList<String[]>();
  File rapport = new File(build.getProject().getWorkspace().child(sdf2.format(build.getTime()) + "rapport-jenkins.txt").toURI());
  ...
        return rapports;
 }

}

Pour entrer dans le détail :

  • Cette fois-ci, les méthodes getDisplayName(), getIconFileName() et getUrlName() renvoient vide afin de ne pas faire apparaitre de lien dans le menu de la page.
  • getBuildRapport() renvoie, après analyse du fichier produit, un tableau utilisé dans la page Jelly ci-dessous

Il est important de noter ici que toutes ressources produites par les builds sont situées dans le répertoire workspace de l'instance de Jenkins :

Pour terminer, la vue Jelly de notre résumé (située à src/main/resources/com/mypackage/MyBuildAction/summary.jelly) :


<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">

     <table style="width:100%" border="1">
        <h1><img src="/plugin/artifact_id/icon.jpg"/> Résumé :</h1>
        <tr>
        <th>Enquête</th>
        <th>Résultat</th>
        <th>Erreur</th>
        </tr>
        <j:forEach items="${it.buildRapport}" var="data">
        <tr>
         <td style="vertical-align:middle"><b>${data[0]}</b></td>
       <j:if test="${empty(data[2])}">
        <j:set var="color" value="#00AA00" />
        <j:set var="image" value="icon_cool.jpg" />
        </j:if>
        <j:if test="${!empty(data[2])}">
            <j:set var="color" value="#AA0000" />
            <j:set var="image" value="icon_angry.jpg" />
        </j:if>
         <td style="text-align:center"><img height="150px" src="/plugin/artifact_id/${image}" /><br/>
            <font style="color:${color};font-size:2em"><b>${data[1]}</b></font>
         </td>
         <td style="vertical-align:middle"><j:if test="${!empty(data[2])}">${data[2]}</j:if></td>
        </tr>
       </j:forEach>
     </table>
     <hr/>
</j:jelly>

L'aperçu de cette page est présenté plus haut.

C'est terminé! En quelques classes et presque encore moins de lignes de code, voici un beau plugin pour maîtriser Jenkins!

P.S.: Merci à Michaël Pailloncy pour sa présentation au Toulouse JUG.

Sources :


Fichier(s) joint(s) :



Valider des fichiers XML en Java

La vérification de fichiers XML en fonction de schémas XSD peut vite devenir plus complexe qu'elle n'en a l'air...

Imaginons une application Java qui utilise des schémas imbriqués situés dans un certain répertoire pour valider des fichiers situer dans un autre répertoire :

Dans C:\schemas\ :

schema-parent.xsd


<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="3.0">
 <xs:include schemaLocation="schema-fils.xsd"/>
 <xs:element name="OPS">
  <xs:annotation>
   <xs:documentation>bla bla.</xs:documentation>
  </xs:annotation>
  <xs:complexType>
   <xs:choice>
    ...
</xs:schema>

schema-fils.xsd


<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="3.0">
 <xs:include schemaLocation="schema-petit-fils.xsd"/>
 <xs:element name="TOT">
  <xs:annotation>
   <xs:documentation>bla bla.</xs:documentation>
  </xs:annotation>
  <xs:complexType>
   <xs:choice>
    ...
</xs:schema>

schema-petit-fils.xsd


<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="3.0">
 <xs:element name="XDF">
  <xs:annotation>
   <xs:documentation>bla bla.</xs:documentation>
  </xs:annotation>
  <xs:complexType>
   <xs:choice>
    ...
</xs:schema>

Dans C:\xml\ :

avalider.xml


<?xml version="1.0" encoding="UTF-8"?>
<OPS xmlns="..." xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="schema-parent.xsd" AD="FRA" FR="AA"
 ON="540" OD="2012-11-12" OT="15:08">
 <DAT TM="CU">
  ...
 </DAT>
</OPS>

Et ci-dessous le code permettant d'exécuter la validation :


...
import org.xml.sax.helpers.DefaultHandler;
...

XSDHandler handler = new XSDHandler();

SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setValidating(true);
SAXParser sp = spf.newSAXParser();

sp.getParser().setLocale(Locale.ENGLISH);

sp.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");

InputStream schemaIs = getClass().getResourceAsStream("schema-parent.xsd");
sp.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource", schemaIs);
sp.parse(fichierAvalider, handler);

...

class XSDHandler extends DefaultHandler {

 private final static Logger logger = Logger.getLogger(XSDHandler.class);

 @Override
 public void fatalError(SAXParseException e) {
  logger.error("Erreur fatale :" + message);
 }

 @Override
 public void error(SAXParseException e) {
  logger.error("Erreur :" + message);
 }

 @Override
 public void warning(SAXParseException e) {
  logger.warn("Warning :" + message);
 }
}

Lors de l'exécution, l'erreur suivante va être levée :

Echec de la lecture du document de schéma 'schema-fils.xsd' pour les raisons suivantes : 1) Le document est introuvable ; 2) Le document n'a pas pu être lu ; 3) L'élément racine du document n'est pas .

Ceci est du à la présence de l'attribut schemaLocation au sein même du fichier XML, qui prévaut sur toute autre déclaration schemaLocation dans les XSD. Le SAXParser va donc tenter de trouver le schéma relativement au fichier en cours de vérification (dans C:\xml\)... Il est donc préférable de supprimer cet attribut.

Cependant, même après cette correction, l'erreur persiste... En effet, dans ce cas de schémas imbriqués, le SAXParser utilise l'attribut schemaLocation de schema-parent.xsd contenant normalement le chemin relatif vers le schéma fils, mais recherche toujours de manière relative au fichier XML!

Pour régler ce problème, sans avoir à indiquer de chemin absolu dans les schémas, il est possible d'indiquer au SAXParser comment retrouver ses petits. Pour ce faire, il faut modifier le XSDHandler qui lui est passé pour surcharger la méthode resolveEntity :


class XSDHandler extends DefaultHandler {

 private final static Logger logger = Logger.getLogger(XSDHandler.class);

 @Override
 public void fatalError(SAXParseException e) {
  logger.error("Erreur fatale :" + message);
 }

 @Override
 public void error(SAXParseException e) {
  logger.error("Erreur :" + message);
 }

 @Override
 public void warning(SAXParseException e) {
  logger.warn("Warning :" + message);
 }

 @Override
 public InputSource resolveEntity(String arg0, String arg1)
   throws IOException, SAXException {
  File tmp = new File(arg1);
  return new InputSource(new FileInputStream(new File(getClass().getResource("/v3/"+tmp.getName()).toURI())));
 }
}

Le deuxième argument arg1 contient le chemin complet où le parser cherche le schéma imbriqué. Il suffit donc d'un petit tour de passe-passe pour renvoyer le bon fichier!

Hope this helps!


Fichier(s) joint(s) :



Développeur président!

Dans la série des articles "Fier d'être développeur", en voici un très bon expliquant les rôles et enjeux de notre si beau métier :

We Need a Programmer for President

Et si on ne devait retenir qu'une phrase :

There is data indicating that we are automating jobs faster than we can create them. This is, in large part, due to software. It's the software developers who are now in demand and that industry may be where many of the jobs are shifting

Prenez le temps de suivre les liens indiqués au fil de l'article, ils sont tout aussi intéressants.


Fichier(s) joint(s) :



Un IDE pour développer en XML

Pour faire suite à mon article décrivant des outils pour maîtriser contenu XML, schémas et transformations , je vais vous présenter un véritable IDE aux fonctionnalités complètes pour le développement et la gestion de fichiers XML et schémas XSD : OxygenXML.

Le point fort de ce logiciel est de présenter la structure d'un schéma sous forme d'arbre graphique : clair, exhaustif et facile à manipuler, il permet une lecture aisée même pour les schémas complexes.

Son interface condense les outils récurrents et indispensables (parcours XPath, debuggage de validation/transformation...) très agréables à utiliser puisque toujours à portée de clic et bien réalisés.

La génération de documentation est également très au point, et produit par exemple des fichiers HTML très utiles :

On retrouve ainsi tous les éléments nécessaires à la compréhension du schéma :

  • Namespace
  • Extrait du diagramme graphique
  • Eléments utilisant
  • Eléments fils
  • Présentation détaillée des attributs
  • Extrait de la source du schéma correspondant

Autre fonctionnalité indispensable, aperçue dans la documentation, est la génération d'instances : elle permet de créer, à partir du schéma en question, un exemple de fichier XML valide contenant quelques valeurs par défaut :

Tout est configurable à souhait :

  • Ajout de différents namespaces par élément
  • Créer ou non les éléments facultatifs
  • Règles à appliquer aux valeurs par défaut
  • Possibilité de définir certaines valeurs par défaut

Cet IDE met donc tout à disposition pour créer et manipuler des données XML.


Fichier(s) joint(s) :

Observer une JVM sous toutes ses coutures

J'ai déjà écrit quelques articles sur les différents moyens de monitorer une JVM, notamment avec la JConsole, mais il existe bien d'autres outils permettant d'observer le déroulement d'une application dont VisualVM, qui est sûrement le plus complet et le plus agréable à utiliser.

Comme il existe déjà de nombreux blogs décrivant son fonctionnement, je ne vais pas les paraphraser et je vous laisse les découvrir via les liens plus bas.

Je vais ici plutôt m'attarder sur la description du plugin Eclipse qui permet d'ajouter un lanceur d'application basé sur VisualVM afin d'accéder directement au profilage dès le démarrage.

Après avoir téléchargé et installé le plugin sur le site officiel, direction les préférences du workspace :

Il s'agit ici de simplement configurer :

  • Dans la section "VisualVM configuration", le chemin d'exécution de VisualVM et du JDK.
  • Dans la section "Default launchers", le nouveau lanceur à utiliser (qui n'est finalement qu'un hook avant le démarrage de l'application)

Ainsi, à la prochaine exécution dans l'IDE, VisualVM sera lancé en parallèle et automatiquement câblé dessus :

Il ne reste plus qu'à profiter de toutes les fonctionnalités de l'outil, toutes plus indispensables les unes que les autres... Enjoy!

Sources :


Fichier(s) joint(s) :



Eclipse : régler les problèmes de master password

Une fois n'est pas coutume, cet article est un simple mémo pour conserver à portée de main deux liens utiles pour résoudre les problèmes de "secure storage" d'Eclipse (corrompu, manquant, non sauvegardé...) :

Si vous avez rencontré d'autres problèmes (et trouvé des solutions!), n'hésitez pas à compléter...

Voilà tout!


Fichier(s) joint(s) :



Nouveau jargon informatique

A l'heure de la pause, il est toujours bon de se cultiver un peu.

Voici donc un lien vers le génial site CodingHorror qui nous présente une liste des nouveaux termes liés aux plus ou moins bonnes pratiques en informatique. Enjoy!

http://www.codinghorror.com/blog/2012/07/new-programming-jargon.html


Fichier(s) joint(s) :

Maitrisez vos plugins RCP : traces et fuites SWT

Dans cet article je vais simplement partager, pour ne pas paraphraser, quelques liens intéressants pour permettre de debugger avec précisions tous les niveaux d'une application RCP.

Fuites de mémoires SWT

Il arrive assez vite d'être confronté à des problèmes de mémoire importants lors de la création d'interfaces graphiques en SWT. Afin de vérifier la bonne utilisation des composants et surtout leur nettoyage (via la méthode dispose), l'outil Sleak permet de comparer à deux instants donnés le nombre d'éléments graphiques présents dans la mémoire de l'application. Très utile pour localiser les électrons libres!. Pour en savoir plus :

Activer les traces dans les plugins

De la même manière, il peut être utile d'activer un mode verbeux de l'activité des plugins. Voici deux méthodes pour activer ces traces :

Avec tout ceci, plus rien ne vous échappera!


Fichier(s) joint(s) :

Créer un splash-screen dynamique

Le but de cet article est d'illustrer de manière concise la mise en place d'un écran de démarrage (splash screen) personnalisé pour une application Eclipse RCP.

Pour commencer, il faut indiquer, dans l'onglet "Splash" de la définition du produit, le plugin contenant la configuration de l'écran d'accueil (point d'extension) :

Ensuite, dans le fichier plugin.xml du plugin en question, ajouter le point d'extension org.eclipse.ui.splashHandlers associé d'une part au produit et d'autre part à la classe gérant la composition de l'écran :

Il ne reste plus, dans la classe mentionnée, qu'à composer l'interface du splash screen, à l'aide de composants SWT classiques (progressbar, text...), comme démontré dans ce thread : http://stackoverflow.com/questions/8345927/eclipse-rcp-application-custom-splash-screen

HTH!


Fichier(s) joint(s) :



Exporter des plugins Eclipse en ligne de commande

Pour faire suite à mon article sur le déploiement de produit headless, voici un exemple illustrant comment il est également possible de n'exporter que des plugins, afin de pouvoir les mettre à jour par simple copie dans la distribution finale sans avoir à exporter tout le produit.

Export classique via IHM

L'IDE fournit nativement la possibilité d'exporter (via Ant) un ou des plugins. L'option intéressante de cet outil est la sauvegarde du script Ant appelé, qui sera utilisé plus loin en ligne de commande.

Ainsi, dans le menu File > Export se trouve l'action "Deployable plug-ins and fragments" :

Dans le panneau suivant, voici l'option à utiliser :

Le script produit ressemble à :

<project default="plugin_export" name="build">
 <target name="plugin_export">
  <pde.exportPlugins destination="C:\test" exportSource="false" 
   exportType="directory" 
   plugins="com.developpef.plugin1,com.developpef.plugin2" 
   qualifier="v20120627" useJARFormat="true"/>
 </target>
</project>

Export via ligne de commande

Pour utiliser ce script Ant en ligne de commande, voici la procédure à utiliser :

start /wait eclipse.exe -noSplash -application org.eclipse.ant.core.antRunner 
 -data ..\..\workspace -buildfile buildPatch.xml

  • l'instruction "wait" permet à la console d'attendre la fin de l'exécution
  • "eclipse.exe -noSplash" permet de lancer une instance du moteur d'Eclipse sans splashScreen
  • le paramètre "application" indique au moteur de l'IDE de n'exécuter que le antRunner et non pas l'ouverture de l'interface
  • le paramètre "data" indique le chemin vers le workspace
  • le paramètre "buildfile " indique le chemin vers le fichier Ant à exécuter

Il est tout de même nécessaire de lancer Eclipse car le script Ant contient une tâche "pde.exportPlugins" qui requiert la mise en place de variables et autres scripts internes à l'IDE.

Avec ces simples éléments, il est alors possible de produire à la volée les plugins à partir d'un environnement de développement, en mode "silencieux" (sans interface).

Sources :


Fichier(s) joint(s) :



Les bons outils pour démarrer avec Android

Comme je l'ai évoqué dans mon précédent article sur le sujet, j'ai dernièrement "essuyé les plâtres du débutant" quant à la mise en place d'un environnement de développement "optimal" pour Android.

Après avoir fait de nombreuses et vaines recherches pour essayer de trouver un retour d'expérience sur le sujet, j'ai décidé de publier le mien. Cet article étant tout à fait subjectif, je vous invite donc à critiquer, commenter, suggérer des idées sur votre expérience!

Le bon IDE

Etant un fervent adepte d'Eclipse, j'ai cependant vite été déçu par les lenteurs et pertes de performances dues aux plugins Android. Je me suis donc orienté vers IntelliJ qui supporte nativement les outils nécessaires et présente donc une fluidité d'utilisation bien meilleure.

Optimiser le déploiement

Pour tester l'application sur le terminal, il est indispensable de régulièrement re-construire le fichier APK pour le publier à nouveau. L'IDE propose cette fonctionnalité mais relance à chaque déploiement la compilation intégrale des sources, la génération des ressources (classe R.java ...) et passe généralement par l'étape de signature du fichier (ce qui implique la saisie du mot de passe etc). Pour contourner cette gêne, il est préférable de déployer à partir de ANT. En effet, les sources contiennent par défaut le fichier build.xml nécessaire. Il suffit donc d'utiliser la ligne de commande :

ant -Dsdk.dir=(pathToAndroidSDK) clean debug

La commande debug va automatiquement signer le fichier avec des paramètres par défaut pour accélérer l'opération.

Améliorer l'émulateur

L'émulateur fourni par défaut dans le SDK rempli certes très bien son devoir, mais est malheureusement très lent. Et les quelques configurations de tuning possibles n'augmentent guère les performances.

L'idéal est de s'orienter vers la mise en place d'une machine virtuelle VirtualBox exécutant le système d'exploitation Android-x86. Pour installer cet outillage, voici un très bon tutoriel sur cette page.

Pour publier le nouveau fichier APK dans la machine virtuelle, il suffit d'exécuter :

// Connexion à la VM
adb connect (IP_DE_LA_VM)
// installation
adb install -r (CHEMIN_DU_APK)

Avec cet outillage, il est possible de tester une nouvelle version de l'application en quelques secondes, là où les outils intégrés aux IDE trainent lors de la re-compilation et publication.

Les commandes utiles

En plus des commandes citées précédemment, il est assez pratique d'en garder quelques unes sous la main, parmi les plus utilisées :

  • adb logcat > C:\log.txt : pour récupérer les logs distants dans un fichier local
  • sqlite3 /data/data/my.package/databases/[nom_bdd] : pour accéder en ligne de commande à la base de données sqlite (sqlite3 est dans le répertoire "tools" du SDK)
  • ddms : toujours dans le répertoire "tools" se trouve l'outil de debug Dalvik, très complet, contenant notamment l'explorateur de fichiers mais également beaucoup d'utilitaires :
    the Dalvik Debug Monitor Server (DDMS), which provides port-forwarding services, screen capture on the device, thread and heap information on the device, logcat, process, and radio state information, incoming call and SMS spoofing, location data spoofing, and more

Vous voilà bien armés, ne reste plus qu'à coder!


Fichier(s) joint(s) :



Piloter Camel avec Quartz et JMX

Cette fois-ci, le besoin est d'ordonnancer l'exécution d'une application Camel via le gestionnaire de tâches Quartz tout en laissant la possibilité de démarrer manuellement les routes à la demande.

Commençons par l'ordonnancement. Admettons que le programme ne doit se lancer qu'à partir de 5 heures du matin en fin de semaine. La syntaxe CRON correspondante est :

* * 5 ? * SAT-SUN

Pour configurer la route initiale avec cette règle, il faut lui spécifier une routePolicy :

@Override
public void configure() throws Exception {
  
 // Quartz
 CronScheduledRoutePolicy startPolicy = new CronScheduledRoutePolicy();
 // ne démarre que le weekend à partir de 5h du matin
 startPolicy.setRouteStartTime("* * 5 ? * SAT-SUN");
 ...
 from("file://...").routePolicy(startPolicy).noAutoStartup().routeId("firstRoute")...
}

Ainsi donc, la route n'est pas démarrée au lancement du contexte mais délègue cette tâche à l'ordonnanceur. (Ne pas oublier d'ajouter la dépendance camel-quartz)

Passons à la mise en place de JMX pour piloter à distance Camel.

Au démarrage de l'application, il faut référencer un bean particulier ayant connaissance du contexte Camel auprès du server JMX de la JVM :

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
MyAgentManagerMXBean mbean = new MyCamelMBean(camelContext);
ObjectName name = new ObjectName("my.jmx.domain.MyDomain:type=MyCamelMBean,name=myCamelMBean");
server.registerMBean(mbean, name);

La paramètre camelContext du bean est une instance du context Camel, par exemple de org.apache.camel.main.Main. Je précise au passage qu'afin de rendre disponible l'interface à JMX, le nom de celle-ci doit être suffixé par MXBean (il s'agit d'une convention JMX). Et voici comment sont écrites l'interface et l'instance du bean mbean

public interface MyAgentManagerMXBean {
 
 /**
  * Start Camel manually, overriding quartz schedule
  * @throws Exception
  */
 public void camelManualStart() throws Exception;

}
public class MyCamelMBean implements MyAgentManagerMXBean {

 private Main camel;
 
 public MyCamelMBean(Main context) {
  camel = context;
 }

 @Override
 public void camelManualStart() throws Exception {
  camel.getRouteBuilders().get(0).getContext().startRoute("firstRoute");
 }
}

Avec cette configuration, il est alors possible, avec n'importe quelle console JMX (ici la JConsole) de démarrer manuellement la route configurée avec Quartz :

J'en profite pour ajouter un exemple permettant de récupérer des informations directement de Camel, par exemple ici le nombre de message traités sur une route :

public long getSourceFilesManagedNb() {
 long result = 0;
 try {
  JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1616/jmxrmi");
  JMXConnector jmxc = JMXConnectorFactory.connect(url);
  MBeanServerConnection server = jmxc.getMBeanServerConnection();
  ObjectName objName = new ObjectName("org.apache.camel:type=routes,name=\"firstRoute\",*");
  List<ObjectName> cacheList = new LinkedList<ObjectName>(server.queryNames(objName, null));
  for (ObjectName objName2 : cacheList) {
   result = (Long) server.invoke(objName2, "getExchangesCompleted", null, null);
  }
 } catch (Exception e) {
  e.printStackTrace();
 }
 return result;
}

Voilà tout!


Fichier(s) joint(s) :



Comment je me suis lancé dans le développement mobile

J'ai décidé d'expliquer dans cet article comment, après quelques semaines de recherches, je me suis lancé dans le développement d'une application pour appareils mobiles. Comme le processus de décision a été plutôt long et le fruit de nombreux questionnements, je le rend public afin d'aider ceux qui seraient dans la même perspective à prendre une bonne décision.

Le contexte du projet

L'idée originelle était de développer un petit "assistant" dans la pratique d'une activité de loisir. Etant moi-même pratiquant, je me suis rendu à l'évidence qu'il fallait que l'outil soit toujours à portée de main de l'utilisateur, donc sur un appareil mobile! (je garde pour le moment le mystère que le sujet exact du projet :-) )

Le choix de la technologie

Il existe aujourd'hui pas mal de moyens pour réaliser une application nomade. Ayant une bonne expérience de Java, j'ai d'abord essayé de créer une simple application Swing emballée dans un Jar, puisque la majorité des terminaux savent lire ce genre de paquet. Mais là premier écueil : les Manifest des applications Java sont en réalité totalement différents de ceux des applications mobiles... Qu'à cela ne tienne, utilisons J2ME... Dès la première tentative de compilation du projet, c'est le drame : J2ME requiert un compilateur compatible 1.3, donc adieu Swing, adieu encore les listes paramétrées et pire, adieu les listes tout court, à remplacer par des Vecteurs. C'en est trop, il faut trouver autre chose!

Les premières recherches sur le net concernant le développement mobile et la compatibilité inter-plateformes font apparaître qu'il est nécessaire d'utiliser HTML5, CSS3 et Javascript... Je n'ai jamais été très fan du développement web... Mais cela pourrait faire une bonne raison de s'y mettre! En regardant de plus près, il existe déjà pas mal de solutions "in cloud" pour déployer un projet web sur toutes les plateformes mobiles. Super! Maintenant, il faut encore rattraper le retard accumulé auprès des technos citées précédemment pour mettre en place quelque chose de correct. Au fond je ne suis pas très motivé pour ça.

Une solution pour faire le pont Java-Web? GWT, me souffle-t-on! Bon sang mais c'est bien sûr! Superbe occasion de se lancer! Et là, pour faire court : la présentation sur le site officiel est alléchante quoique plutôt sommaire, conceptuellement cela semble complexe mais bien organisé mais alors pour la réalisation... Ils ont sorti l'artillerie lourde! A grand renfort de lignes de commandes, panels graphiques et objets ésotériques, on est vite perdus!

Donc c'est le moment de se décider pour un développement spécifique à une plateforme. Restreignons le choix aux plus répandues : iOS et Android. N'ayant jamais fait de C, il ne reste plus qu'Android (qui semble prendre de plus en plus de parts de marché, donc intéressant). Et là, enfin, le Graal! Un site officiel qui présente plus clairement ce que permet le SDK, une documentation qui semble plus accessible et plus claire et surtout du code basé uniquement sur JAVA, du XML maison et seulement quatre ou cinq grands concepts techniques et/ou généraux sur le fonctionnement d'une appli... Ouf! De plus, ayant une vieillissante expérience en Flex, tout se ressemble.

Les débuts

Alors c'est parti, je me lance! Etant habitué à Eclipse, je récupère le SDK Android et la batterie de plugins nécessaire, je génère le premier projet "Hello world" et après la configuration de l'émulateur c'est la désillusion : ça rame! Eclipse est connu pour vite monter en charge lors de lourdes opérations, mais le mode Debug sur l'émulateur Android bat des records, mon PC est submergé, difficile d'avancer.

En se renseignant un peu, on comprend vite qu'il est préférable d'utiliser l'émulateur de manière indépendante de l'IDE, en ligne de commande (simple). Faisons ça. Reste l'IDE lui même qui malgré tout semble peiné par la surcharge des plugins Android. Il faudra donc lui faire des infidélités cette fois-ci : je me tourne vers IntelliJ IDEA. Et là effectivement, on respire un peu! D'autant plus que le développement Android semble grandement amélioré par une intégration à l'IDE beaucoup plus importante : auto-complétion des informations textuelles par lien direct avec les fichiers de ressources XML, meilleure organisation du projet par reconnaissance des éléments spécifiques à l'architecture Android (dossier "res") etc.

Enfin cette fois, c'est parti! C'est assez plaisant (pour un débutant) de voir une appli fonctionner sur un appareil mobile. Je profiterai dans l'avenir de cette nouvelle expérience pour publier quelques articles sur le développement Android.

A suivre donc...


Fichier(s) joint(s) :



Eclipse : réutiliser les icônes JDT

Aujourd'hui un petit pense-bête, pour faire suite à mon vieil article sur les icônes Eclipse, voici le lien vers la page contenant tous les icônes utilisés par JDT : Help Eclipse. Et vive RCP


Fichier(s) joint(s) :



Eclipse : extraire une feature et ses dépendances

Lors de la création d'applications RCP, il peut être utile de récupérer quelques features indépendantes afin des les intégrer à la distribution (pour ajouter des nouveaux plugins). Quand ces features ne sont pas livrées de manière autonome, il devient vite assez fastidieux de suivre à la main toutes les dépendances pour récupérer les plugins utiles.

Pour faciliter ce travail à tous ceux qui en auront besoin, voici une petite moulinette prête à l'emploi pour copier toutes les dépendances d'une feature :

public class EclipseFeatureExtractor {
 
 private static Set<String> requiredFeatures = new HashSet<String>();
 private static Set<String> requiredPlugins = new HashSet<String>();
 private static DocumentBuilder builder;
 private static XPath xpath;
 private static File featuresFolder;
 private static File pluginsFolder;

 /**
  * @param args
  * @throws Exception 
  */
 public static void main(String[] args) throws Exception {
  if(args.length != 2 || (args.length>=1 && args[0].equals("-help"))) {
   System.out.println("*********** USAGE ***********");
   System.out.println("-help affiche aide.\n");
   System.out.println("Paramètres nécessaires :");
   System.out.println("1- Dossier de la feature à examiner");
   System.out.println("2- Dossier de destination pour la copie des features et plugins");
   return;
  }
  
  String mainFeature = args[0];
  String destination = args[1];
  
  File destFolder = new File(destination);
  File mainFeatureFolder = new File(mainFeature);
  featuresFolder = mainFeatureFolder.getParentFile();
  pluginsFolder = new File(featuresFolder.getParentFile(), "plugins");
  
  DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  factory.setNamespaceAware(true);
  builder = factory.newDocumentBuilder();
  
  XPathFactory xpathfactory = XPathFactory.newInstance();
  xpath = xpathfactory.newXPath();
  
  parseFeature(mainFeatureFolder);
  
  File destFeaturesFolder = new File(destFolder,"features");
  if(!destFeaturesFolder.exists()) {
   destFeaturesFolder.mkdirs();
  }
  copyFeatures(destFeaturesFolder);
  
  File destPluginsFolder = new File(destFolder,"plugins");
  if(!destPluginsFolder.exists()) {
   destPluginsFolder.mkdirs();
  }
  copyPlugins(destPluginsFolder);
 }

 private static void copyPlugins(File destPluginsFolder) throws Exception {
  for(String required : requiredPlugins) {
   for(File other : pluginsFolder.listFiles()) {
    if(other.getName().startsWith(required)) {
     System.out.println("Copying plugin : "+other.getName());
     if(other.isDirectory()) {
      File specificDest = new File(destPluginsFolder, other.getName());
      specificDest.mkdirs();
      FileUtils.copyDirectory(other, specificDest);
     } else {
      File copiedPlugin = new File(destPluginsFolder, other.getName());
      FileUtils.copyFile(other, copiedPlugin);
     }
    }
   }
  }
 }

 private static void copyFeatures(File destFeaturesFolder) throws Exception {
  for(String required : requiredFeatures) {
   for(File other : featuresFolder.listFiles()) {
    if(other.isDirectory() && other.getName().startsWith(required)) {
     File specificDest = new File(destFeaturesFolder, other.getName());
     specificDest.mkdirs();
     System.out.println("Copying feature : "+other.getName());
     FileUtils.copyDirectory(other, specificDest);
    }
   }
  }
 }

 public static void parseFeature(File featureFolder)
   throws SAXException, IOException, XPathExpressionException {
  File featureDescriptor = new File(featureFolder, "feature.xml");
  System.out.println("Parsing : " + featureDescriptor.getAbsolutePath());
  Document doc = builder.parse(featureDescriptor);
  // Required features
  XPathExpression xRequiredFeatures = xpath.compile("//requires/import/@feature|//includes/@id");
  NodeList nodes = (NodeList)xRequiredFeatures.evaluate(doc, XPathConstants.NODESET);
  String feature = "";
  for (int i = 0; i < nodes.getLength(); i++) {
   feature = nodes.item(i).getNodeValue()+"_";
   requiredFeatures.add(feature);
   for(File other : featuresFolder.listFiles()) {
    if(other.isDirectory() && other.getName().startsWith(feature)) {
     parseFeature(other);
    }
   }
  }
  // Required plugins
  XPathExpression xRequiredPlugins = xpath.compile("//plugin/@id");
  nodes = (NodeList)xRequiredPlugins.evaluate(doc, XPathConstants.NODESET);
  String plugin = "";
  for (int i = 0; i < nodes.getLength(); i++) {
   plugin = nodes.item(i).getNodeValue()+"_";
   requiredPlugins.add(plugin);
  }
 }

}

Ce code peut être amélioré pour gérer de manière plus fine les versions des features/plugins, ici ignorées.


Fichier(s) joint(s) :



Créer une console personnalisée avec JTextPane

Voici la problématique à résoudre : un outil a été développé en prévision de n'être exécuté qu'en ligne de commande. Toutes les informations ressorties le sont donc via System.out ou System.err. Or il se trouve maintenant qu'il est nécessaire d'intégrer cet outil dans une application comportant une interface graphique et donc de diriger les sorties vers un composant graphique en y ajoutant un formatage selon le type de message. Il est bien sûr impossible de modifier le code de l'outil... Voici un aperçu du résultat attendu :

Pour résoudre ceci, il faut commencer par redéfinir la sortie des messages. Java offre cette possibilité à l'aide de :

System.setOut(...);
System.setErr(...);

Ces deux méthodes attendent en entrée un objet héritant de java.io.PrintStream. Mais avant de voir comment mettre en place ces éléments, regardons comment est créée l'interface :

public static void buildGUI() throws Exception {
 final CustomConsole out = new CustomConsole();
 JScrollPane sp = new JScrollPane(out.getOutComponent());
 JButton btn = new JButton("Run!");
 btn.addActionListener(new ActionListener() {
  @Override
  public void actionPerformed(ActionEvent e) {
   out.clear();
   Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
     doJob();
    }
   });
   t.start();
  }
 });
 JPanel p = new JPanel(new BorderLayout());
 p.add(btn, BorderLayout.NORTH);
 p.add(sp, BorderLayout.CENTER);
 
 JFrame f = new JFrame("Test console");
 f.setSize(800, 850);
 f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 f.add(p);
 f.setVisible(true);
}

Cette première méthode utilise des objets Swing courants pour construire l'interface, et introduit CustomConsole : c'est dans cette classe que réside toute notre customisation. Voici son contenu :

public class CustomConsole {

 private JTextPane outComponent;

 public CustomConsole() throws Exception {
  this.outComponent = new JTextPane() {
   private static final long serialVersionUID = 8575888440178628669L;
   // to remove line wrap
   public boolean getScrollableTracksViewportWidth() {
    return getUI().getPreferredSize(this).width <= getParent().getSize().width;
   }
   @Override
   public void setEditable(boolean b) {
    super.setEditable(false);
   }
  };
  // build error style
  Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
  Style s = outComponent.getStyledDocument().addStyle("error", def);
  StyleConstants.setForeground(s, Color.RED);
  StyleConstants.setItalic(s, true);
  // define standard out
  System.setOut(new CustomOutStream(outComponent));
  System.setErr(new CustomErrStream(outComponent));
 }

 public JTextPane getOutComponent() {
  return outComponent;
 }

 public void clear() {
  outComponent.setText("");
 }
}

Comme vous pouvez le constater, le code est assez simple. La définition du style de police pour les messages d'erreurs se fait à partir du style standard de police du JTextPane auquel on ajoute nos caractéristiques. Notez la subtilité lors de la définition de cet objet : pour empêcher les retours à la ligne lors de l'affichage, on surcharge la méthode getScrollableTracksViewportWidth(). Enfin, la suite de notre personnalisation réside dans les objets CustomOutStream et CustomErrStream : mais là encore, rien de très compliqué, les deux classes sont quasiment identiques :

public class CustomOutStream extends CustomPrintStream {

 private JTextPane outComponent;

 public CustomOutStream(JTextPane area) throws Exception {
  super();
  outComponent = area;
 }

 @Override
 public void write(byte buf[], int off, int len) {
  addToConsole(new String(buf, off, len));
 }

 @Override
 public void write(int b) {
  addToConsole(new String(new char[] { (char) b }));
 }

 @Override
 public void println(String x) {
  addToConsole(x);
 }

 private void addToConsole(String s) {
  StyledDocument doc = outComponent.getStyledDocument();
  try {
   if (!s.endsWith("\n")) {
    println();
   }
   doc.insertString(doc.getLength(), s, doc.getStyle(StyleContext.DEFAULT_STYLE));
  } catch (BadLocationException e) {
   e.printStackTrace();
  }
  outComponent.setCaretPosition(doc.getLength() - s.length());
 }

}

Elles redéfinissent les méthodes d'écriture les plus courantes afin de nous permettre de gérer les styles : dans cet exemple, pour la sortie standard, on choisi d'utiliser la police par défaut, correspondant au style StyleContext.DEFAULT_STYLE.

Pour ce qui est de la sortie d'erreur, on utilise le même code en remplaçant simplement le style par celui défini dans la console (italique rouge) : doc.getStyle("error")

L'instruction setCaretPosition permet de créer l'effet d'autoscroll sur la console.

Pour unifier la création du flux de sortie, j'ai créé la classe ci-dessous, mère des classes précédentes :

public class CustomPrintStream extends PrintStream {

 public CustomPrintStream() throws FileNotFoundException, UnsupportedEncodingException {
  // on indique un fichier texte parce que requis mais non utilisé ici
  // et on indique l'encodage désiré pour cette console
  super(new FileOutputStream("C:\\empty.txt"), true, "ISO-8859-1");
 }

}

Cette console peut alors être testée avec ce simple code :

private static void doJob() {
 FileInputStream fstream = null;
 try {
  System.out.println("Sortie console standard");
  if(true) {
   throw new Exception("premiere erreur!!");
  }
 } catch (Exception e) {
  e.printStackTrace();
  System.out.println("info après premiere erreur");
 } finally {
  try {
   fstream.close();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }
}

Nous avons donc pu créer cette interface graphique sans modifier le code de l'outil de base.

Hope this helps!

Sources :


Fichier(s) joint(s) :

Eclipse : déployer un produit headless

Une étape importante du développement d'une application basée sur Eclipse RCP est l'automatisation du déploiement du produit. Pour ce faire, l'IDE met à disposition une série de tâches ANT permettant d'externaliser cette tâche.

Il existe cependant différents "niveaux d'automatisation" :

  • Ecriture d'un script ANT utilisable seulement depuis l'IDE
  • Utilisation d'un batch s'appuyant sur le moteur de l'IDE (Equinox)
  • Insertion dans un cycle d'intégration continue

Je vais donc ici essayer de présenter les différentes ressources pour mettre en place chacune de ces méthodes.

Ecriture d'un script ANT utilisable seulement depuis l'IDE

Pour démarrer l'élaboration du script ANT principal, la meilleure source est cet article de Lars Vogel. Il contient les premiers éléments indispensables. Contenu du script :

<project default="main">
 <property file="build.properties"/>
 <target name="main">
  <property name="baseLocation" value="${eclipse.home}"/>
  <!-- by default, check for deltapack co-located with eclipse -->
  <property name="deltapack" value="${eclipse.home}/deltapack/eclipse"/>

  <!-- Check that we have a deltapack -->
  <available property="haveDeltaPack" file="${deltapack}"/>
  <fail unless="haveDeltaPack" message="The deltapack is required to build this product.  Please edit buildProduct.xml or set the &quot;deltapack&quot; property." />
   
  <property name="builder" value="${basedir}" />
  <property name="buildDirectory" value="${basedir}/buildDirectory"/>
  <property name="pluginPath" value="${basedir}/..${path.separator}${deltapack}" />
  <property name="buildTempFolder" value="${buildDirectory}" />
   
  <ant antfile="${eclipse.pdebuild.scripts}/productBuild/productBuild.xml" />

  <move todir="${basedir}">
   <fileset dir="${buildDirectory}/${buildLabel}" includes="*.zip"/>
  </move>

  <!-- refresh the workspace -->
  <eclipse.convertPath fileSystemPath="${basedir}" property="resourcePath"/>
  <eclipse.refreshLocal resource="${resourcePath}" depth="infinite"/>
 </target>
</project>

Comme on le voit, ce script utilise principalement certaines tâches internes d'Eclipse (eclipse.convertPath...) ainsi qu'un autre script faisant l'essentiel du travail : ${eclipse.pdebuild.scripts}/productBuild/productBuild.xml. Situé dans le plugin plugins\org.eclipse.pde.build_[version] il défini notamment la tâche eclipse.generateFeature, point de départ du déploiement. Le fichier de propriétés quant à lui contient essentiellement la configuration habituellement valorisée par l'IDE lors de l'utilisation de la fonctionnalité d'export de produit classique.

Il peut être nécessaire lors d'une livraison d'ajouter certains répertoires à la racine de l'archive créée, pour des besoins métier, par exemple le JRE. Pour ce faire, il faut définir, dans les features, les ressources "root". Par exemple, dans le fichier build.properties d'une feature, ajouter la ligne :

root.folder.jre = absolute:${eclipse.home}/jre

Ainsi sera créé un répertoire nommé "jre" contenant la version du JRE présente à l'emplacement spécifié.

Utilisation d'un batch s'appuyant sur le moteur de l'IDE (Equinox)

Le script ANT tel que décrit ci-dessus nécessite l'ouverture d'Eclipse pour s'exécuter dans la même JRE. Il est cependant possible de simuler la présence de l'IDE sans pour autant lancer l'IHM grâce à l'exécution directe de son moteur : Equinox. Voici un exemple de batch :

java -jar ../../plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar -application org.eclipse.ant.core.antRunner -buildfile buildProduct.xml -Dfile.encoding=UTF-8

Le lanceur Equinox est alors utilisé pour démarrer le plugin de lancement de script ANT. Le déploiement devient donc totalement indépendant de l'IDE (ou du moins de sa partie graphique!).

Il existe tout de même un bémol avec cette méthode : puisque le batch ne fait appel qu'au plugin ANT, toute configuration apportée par le workspace est perdue, principalement la gestion des encodages de fichiers. Il est certes possible de définir la propriété -Dfile.encoding mais qui devient alors unique pour toute la phase de déploiement... Si certains fichiers nécessitent une compilation (ou une simple sauvegarde) dans un autre encodage, il sera corrompu. Si je me trompe, je suis preneur de toute solution sur ce point! :-)

Insertion dans un cycle d'intégration continue

Je n'ai personnellement pas testé ce système mais Ralf Ebert a écrit un tutoriel très complet sur la mise en place de Buckminster et Hudson.

Bon courage!

Sources :


Fichier(s) joint(s) :



Un système de template avec Groovy et Java

La génération de rapports au format texte brut (TXT) peut parfois devenir un vrai casse-tête, surtout s'il contient une mise en forme particulière quelles que soient les données.

Pour se faciliter la tâche, il est possible d'utiliser un moteur de templates basé sur Groovy, exécutable en Java, qui assurera la stabilité du rendu avec un effort réduit.

Commençons par le code Java générant notre "rapport" :

import groovy.text.SimpleTemplateEngine;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.codehaus.groovy.control.CompilationFailedException;

public class ReportUtil {

 public static void main(String[] args) {
  Map<String, Object> data = new HashMap<String, Object>();

  Map<String, String> context = new HashMap<String, String>();
  context.put("mascotte", "Duke");
  context.put("auteur", "DevellOpPeF");
  context.put("tool", "GRoOvY");
  context.put("date", "01042012");

  data.put("context", context);
  
  List<String[]> remerciements = new ArrayList<String[]>();
  remerciements.add(new String[]{"Rodolphe","Pour avoir retrouvé ces sources"});
  remerciements.add(new String[]{"Olivier","Pour ses questions pertinentes sur Groovy/Java"});
  
  data.put("remerciements", remerciements);

  try {
   buildFileFromGroovyTemplate(ReportUtil.class.getClassLoader()
     .getResource("TestReport.tmpl").toURI(), "txt", data);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 public static String buildFileFromGroovyTemplate(URI templateFilePath,
   String outputType, Map<String, Object> dataProvider)
   throws CompilationFailedException, IOException {

  String outputFileName = "out_" + System.currentTimeMillis() + "."
    + outputType;
  String outputFilePath = "C:/test/" + outputFileName;

  File outputFile = new File(outputFilePath);
  if (outputFile.exists()) {
   outputFile.delete();
  }

  File tmplFile = new File(templateFilePath);
  if (tmplFile.exists()) {
   FileWriter fileWriter = new FileWriter(outputFilePath, false);

   new SimpleTemplateEngine().createTemplate(new FileReader(tmplFile))
     .make(dataProvider).writeTo(fileWriter);

   fileWriter.close();
  }

  return outputFileName;
 }

}

Son contenu est assez simple :

  • La méthode main constitue le jeu de données à utiliser,
  • La méthode buildFileFromGroovyTemplate lance le moteur Groovy pour générer le fichier final à partir du template TestReport.tmpl et par l'intermédiaire du SimpleTemplateEngine

Il suffit d'utiliser la dépendance suivante pour compiler cette classe :

<dependency>
 <groupId>org.codehaus.groovy</groupId>
 <artifactId>groovy</artifactId>
 <version>1.8.6</version>
</dependency>

Voici maintenant le plus important, le contenu du template :

<%
/////// FUNCTIONS

// Method to write string on given char number
def format(format, text) {
 String.format(format,text)
}

def writePageHeader()
{
%>Hi! from ${context["mascotte"]}!

I was generated with Groovy, run by Java and built by ${context["auteur"]}...


Remerciements :
<%}

def writeFirstQuarter() {
%>


7aJeS6viEuWwWk0hX3Bj8twFQi5q7VQ6xZXE
l2Oe8tA3F7OW2AcPEGahtIffLSf6UugDZ3yX
QNL31XLtRuB5pabFbMsFjFsueIIEm1I9ET35
cd4XIYyRrOIsOM5HIdMoqv9t71NMoJDUcAl6
MfbMIxEW8dyC7RChMz3YUWlbDPP23R3G5kTD
bT2iH4cWbhgkcpZGf2tDUt8neMWFdVdzgy8c
EsSDawQv02JeUb7SsZCwhrpfj7B99RvE2FHT
zJZAvF7bF7F2B87JSvuarvksYckRrLN6Vd6I
w6UzumNNBjYOsw1y3pY0WO3BF5hGPGf9A8zu
fxGivMreSlo5lp6JFsbnUK8SH1HqguzSRo0F
q02oIAaQqFvBXFqOKUikYpvXUeQPNx7vd0Ww
xcspJ5BaLPMJCbAZp1MsSEaxTUnvYNbJAZmK
cnlMsCian5gfFgDbbZpZ5pMsdbRFAbmvgzAQ
OVyz7IPPNQE7DlYD82qhMDAJsAJ8np6E6ay2
oNxpWXquVZ${context["tool"]}3PwACcLc0F6984kiKrGi
<%}

def writeSecondQuarter() {
%>EaGQM4WH5VEf2OH9x8hnLc8fWJc5NLPSsWpb
nKqvv77q3LcJrXyDMb8ICPI5ZX5oX6WME7A2
8T5HOUtK21EPEJMEBHCt5Mw2nXUuNsLL6GSJ
DnDbk2LCZYfspil2jJhArVYCE4b2ylmpicXP
KBapEyy3XAsGpJ34ZoyjfedT9FlZPQtZS58X
HA8mU8nDXEU2tdbr9O7QDzI8yKcusJCmKAry
cVx5UQFGvk${context["auteur"]}qZze5ybzbHNK2It
xfGLFoGsXiZtxpW6DuJgeFyNrnnxFRQ16WCA
RIpvWCdMvkrEoYaqYVmAgYBznssLORhuZOTx
Oq5PBx0d4E6ssT06FI0CNtrXkT4ti2Bxe4At
rrv70yQvAvjBpJ8qfjzxVYcd5ImXo9jCet9T
UBDHf8PWWbWC0bunHyFOyJAyc4tAyKhnLl7n
lf5mJlhflLbYBAH8nbKB2Lp8C4qmalyJVz2t
bFEXDHTf0wWW3kbcM8VV1FSI0PipDmr1biP2
HozZjRhO1l7Uqc2nhiYKDkC45dg5ZURu2l4p
YhnzdwDQjrs89rk5Dli0p1dcxK0yXedxIeMw
<%}

def writeThirdQuarter() {
%>A1DdfnctUK475FZI0dZ1iJtZ2s9ixQmxIcYc
Ae8y20H1X2IHgQ6DCuOTepuW1nhR7sH8EFt4
oUAFj16WJ27Q1BVV8S4cdalDp6ZwKRLmdLbS
YlMGccBLiLrl6F3mY4rDIFhqpTYZ5tLTNYZ9
KX107aJWnE1AYEZbDpPPQ23had8kwNr6qUwo
K0TDymjtzak6USOF199vq8pLWzMiWc9udSqI
ROF1azSd9hY8LBRcJUCxbmFNwl11My8rmDUJ
H1DNFv57hHrfqNk5XLpJh1UHWi0f364Bc9O1
CgOXcq6dNbuKnSKDK8M9zrSpVbXIkXymCmHb
zTZw2woP3PdY9gHGeeI3A91lhIsZqrVBcxcg
<%}

def writeLastQuarter() {
%>iYnZHyol6IMPK8${format("%-16s",context["date"])}8Xrbcq
s9mfwL0aUjySD8H0oaL1ghXIT0f64jcijuLa
xieIhx9Tj5Aerj8hGjj7kYUCJhrxpWoV3Atp
hRg9TbhvpPJHqDIFf6puW9aR3BKelwlXBXXV
yTi5qa7w3htdDu9z1My98bR3lf5kd2PHLcDx
Y3yhvNTl65sJCv4GpJGFPvH7ighmCqaat5GR
EiYLt50Kh3wFUOAayJ0yMMHYquiEynhqWEx0
BHTJy3hD8L2fxPliGlJHYh2It33wZb1a5Kpz
<%}


/////// MAIN

writePageHeader()

remerciements.each {
%>- ${format("%-15s",it[0])} : ${format("%-100s",it[1])}
<%}

writeFirstQuarter()
writeSecondQuarter()
writeThirdQuarter()
writeLastQuarter()
%>

Pour ceux qui ne connaissent pas Groovy, sa syntaxe est très proche de celle de Java et/ou JSF. Les instructions Groovy sont placées dans des balises <% (...) %>. Tout ce qui est en dehors correspond au contenu du template, ce qui sera écrit dans le fichier final. Dans ce contenu, l'accès aux variables se fait pas l'usage de ${...}

Ce que l'on trouve ici :

  • La méthode format est utilisée pour écrire une donnée selon un format particulier, en complétant si besoin est par des espaces afin d'assurer la mise en page.
  • Les méthodes writePageHeader, writeFirstQuarter,writeSecondQuarter, writeThirdQuarter et writeLastQuarter permettent de segmenter la génération du contenu, une manière parfois plus lisible de répartir le code.
  • En dernière partie, un bloc intitulé "Main" qui est constitué des appels aux méthodes précédentes et qui va donc entrainer la génération du rapport.
  • Une boucle each parcourant les éléments de la liste remerciements passée en paramètre

Voici donc le résultat (avec un formattage HTML pour la présentation sur cette page) :

Hi! from Duke

I was generated with Groovy, run by Java and built by DevellOpPeF...

Remerciements :

Rodolphe        : Pour avoir retrouvé ces sources
Olivier         : Pour ses questions pertinentes sur Groovy/Java


7aJeS6viEuWwWk0hX3Bj8twFQi5q7VQ6xZXE
l2Oe8tA3F7OW2AcPEGahtIffLSf6UugDZ3yX
QNL31XLtRuB5pabFbMsFjFsueIIEm1I9ET35
cd4XIYyRrOIsOM5HIdMoqv9t71NMoJDUcAl6
MfbMIxEW8dyC7RChMz3YUWlbDPP23R3G5kTD
bT2iH4cWbhgkcpZGf2tDUt8neMWFdVdzgy8c
EsSDawQv02JeUb7SsZCwhrpfj7B99RvE2FHT
zJZAvF7bF7F2B87JSvuarvksYckRrLN6Vd6I
w6UzumNNBjYOsw1y3pY0WO3BF5hGPGf9A8zu
fxGivMreSlo5lp6JFsbnUK8SH1HqguzSRo0F
q02oIAaQqFvBXFqOKUikYpvXUeQPNx7vd0Ww
xcspJ5BaLPMJCbAZp1MsSEaxTUnvYNbJAZmK
cnlMsCian5gfFgDbbZpZ5pMsdbRFAbmvgzAQ
OVyz7IPPNQE7DlYD82qhMDAJsAJ8np6E6ay2
oNxpWXquVZGRoOvY3PwACcLc0F6984kiKrGi
EaGQM4WH5VEf2OH9x8hnLc8fWJc5NLPSsWpb
nKqvv77q3LcJrXyDMb8ICPI5ZX5oX6WME7A2
8T5HOUtK21EPEJMEBHCt5Mw2nXUuNsLL6GSJ
DnDbk2LCZYfspil2jJhArVYCE4b2ylmpicXP
KBapEyy3XAsGpJ34ZoyjfedT9FlZPQtZS58X
HA8mU8nDXEU2tdbr9O7QDzI8yKcusJCmKAry
cVx5UQFGvkDevellOpPeFqZze5ybzbHNK2It
xfGLFoGsXiZtxpW6DuJgeFyNrnnxFRQ16WCA
RIpvWCdMvkrEoYaqYVmAgYBznssLORhuZOTx
Oq5PBx0d4E6ssT06FI0CNtrXkT4ti2Bxe4At
rrv70yQvAvjBpJ8qfjzxVYcd5ImXo9jCet9T
UBDHf8PWWbWC0bunHyFOyJAyc4tAyKhnLl7n
lf5mJlhflLbYBAH8nbKB2Lp8C4qmalyJVz2t
bFEXDHTf0wWW3kbcM8VV1FSI0PipDmr1biP2
HozZjRhO1l7Uqc2nhiYKDkC45dg5ZURu2l4p
YhnzdwDQjrs89rk5Dli0p1dcxK0yXedxIeMw
A1DdfnctUK475FZI0dZ1iJtZ2s9ixQmxIcYc
Ae8y20H1X2IHgQ6DCuOTepuW1nhR7sH8EFt4
oUAFj16WJ27Q1BVV8S4cdalDp6ZwKRLmdLbS
YlMGccBLiLrl6F3mY4rDIFhqpTYZ5tLTNYZ9
KX107aJWnE1AYEZbDpPPQ23had8kwNr6qUwo
K0TDymjtzak6USOF199vq8pLWzMiWc9udSqI
ROF1azSd9hY8LBRcJUCxbmFNwl11My8rmDUJ
H1DNFv57hHrfqNk5XLpJh1UHWi0f364Bc9O1
CgOXcq6dNbuKnSKDK8M9zrSpVbXIkXymCmHb
zTZw2woP3PdY9gHGeeI3A91lhIsZqrVBcxcg
iYnZHyol6IMPK801042012        8Xrbcq
s9mfwL0aUjySD8H0oaL1ghXIT0f64jcijuLa
xieIhx9Tj5Aerj8hGjj7kYUCJhrxpWoV3Atp
hRg9TbhvpPJHqDIFf6puW9aR3BKelwlXBXXV
yTi5qa7w3htdDu9z1My98bR3lf5kd2PHLcDx
Y3yhvNTl65sJCv4GpJGFPvH7ighmCqaat5GR
EiYLt50Kh3wFUOAayJ0yMMHYquiEynhqWEx0
BHTJy3hD8L2fxPliGlJHYh2It33wZb1a5Kpz

Les paramètres passés au template sont bien contenus dans le rapport final (si, en cherchant bien!)

Voilà tout, au final il est assez simple de disposer de la puissance de Groovy, conjointement à un projet Java classique. Bon courage!


Fichier(s) joint(s) :