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) :