Manipuler des données géographiques avec GeoTools

Dans le monde des Systèmes d'Informations Géographique (SIG), il est assez courant de devoir manipuler des données au format Shapefile (ensemble de fichiers standardisés pour la représentation de cartographie) afin de présenter différents types d'informations.

Pour ce faire, le plus simple est encore d'utiliser comme intermédiaire une base Postgre dédiée, autrement nommée PostGIS, spécifique au traitement de ce type de données.

La solution la plus courante est d'utiliser les outils en ligne de commande shp2pgsql ou ogr2ogr qui permettent de créer un fichier SQL à partir des Shapefile et éventuellement de le jouer directement en base. Cependant, le but de ce billet est de présenter comment il est possible en Java d'extraire des informations et/ou ré-organiser un lot de fichiers Shapefile. L'utilisation d'une base de données intermédiaire a pour vocation de résoudre des problèmes de performance liés à la manipulation directe des fichiers.

Voici un exemple de code à utiliser :

public class GeoToPostGISClient {

 private static final String POSTGIS_TABLENAME = "MY_TABLE";

 private static GeoProperties props = GeoProperties.getInstance();

 private static ShapefileDataStoreFactory shpFactory = new ShapefileDataStoreFactory();

 private static FeatureTypeFactoryImpl factory = new FeatureTypeFactoryImpl();

 private static JDBCDataStore pgStore;

 private SimpleFeatureType schema;

 public GeoToPostGISClient() throws IOException {
  // Ouvrir une connexion vers la base PostGIS
  if (pgStore == null) {
   PostgisNGDataStoreFactory pgFactory = new PostgisNGDataStoreFactory();
   Map<String, String> jdbcparams = new HashMap<String, String>();
   jdbcparams.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
   jdbcparams.put(PostgisNGDataStoreFactory.HOST.key, props.getProperty(GeoProperties.DB_HOST));
   jdbcparams.put(PostgisNGDataStoreFactory.PORT.key, props.getProperty(GeoProperties.DB_PORT));
   jdbcparams.put(PostgisNGDataStoreFactory.SCHEMA.key, props.getProperty(GeoProperties.DB_SCHEMA));
   jdbcparams.put(PostgisNGDataStoreFactory.DATABASE.key, props.getProperty(GeoProperties.DB_NAME));
   jdbcparams.put(PostgisNGDataStoreFactory.USER.key, props.getProperty(GeoProperties.DB_USER));
   jdbcparams.put(PostgisNGDataStoreFactory.PASSWD.key, props.getProperty(GeoProperties.DB_PWD));
   pgStore = pgFactory.createDataStore(jdbcparams);
  }
 }

 /**
  * Insert all specified shapefiles in Postgre
  * 
  * @param shapefilePaths files
  * @throws IOException all
  */
 public void insertShpIntoDb(List<String> shapefilePaths) throws IOException {
  Iterator<String> iterator = shapefilePaths.iterator();
  String path = null;
  while (iterator.hasNext()) {
   path = iterator.next();

   Map<String, Object> shpparams = new HashMap<String, Object>();
   shpparams.put("url", "file://" + path);
   // create indexes only for last file (performance issue)
   FileDataStore shpStore = (FileDataStore) shpFactory.createDataStore(shpparams);
   SimpleFeatureCollection features = shpStore.getFeatureSource().getFeatures();

   if (schema == null) {
    // Copy schema and change name in order to refer to the same
    // global schema for all files
    SimpleFeatureType originalSchema = shpStore.getSchema();
    Name originalName = originalSchema.getName();
    NameImpl theName = new NameImpl(originalName.getNamespaceURI(), originalName.getSeparator(), POSTGIS_TABLENAME);
    schema = factory.createSimpleFeatureType(theName, originalSchema.getAttributeDescriptors(), originalSchema.getGeometryDescriptor(),
      originalSchema.isAbstract(), originalSchema.getRestrictions(), originalSchema.getSuper(), originalSchema.getDescription());
    pgStore.createSchema(schema);
   }
   SimpleFeatureStore featureStore = (SimpleFeatureStore) pgStore.getFeatureSource(POSTGIS_TABLENAME);

   // Ajout des objets du shapefile dans la table PostGIS
   DefaultTransaction transaction = new DefaultTransaction("create");
   featureStore.setTransaction(transaction);
   try {
    featureStore.addFeatures(features);
    transaction.commit();
   } catch (Exception problem) {
    LOGGER.error(problem.getMessage(), problem);
    transaction.rollback();
   } finally {
    transaction.close();
   }
   shpStore.dispose();
  }
  extractFromDb();
 }

 /**
  * Extracts local data from postgis DB
  * 
  * @throws IOException all
  */
 public void extractFromDb() throws IOException {
  // Faire une requête spatiale dans la base
  ContentFeatureCollection filteredFeatures = null;

  String destFolder = "/shp/";

  for (Object dep : ReferentielDepartement.getDepartements()) {
   try {
    filteredFeatures = pgStore.getFeatureSource(POSTGIS_TABLENAME).getFeatures(CQL.toFilter("DPT_NUM = '" + dep + "'"));
   } catch (CQLException e) {
    LOGGER.error(e.getMessage(), e);
   }
   if (filteredFeatures != null && filteredFeatures.size() > 0) {
    // Écrire le résultat dans un fichier shapefile
    Map<String, String> destshpparams = new HashMap<String, String>();
    SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
    String destinationSchemaName = "MySchema_" + dep;
    destshpparams.put("url", "file://" + destFolder + destinationSchemaName + "_" + formatter.format(new Date()) + ".shp");
    DataStore destShpStore = shpFactory.createNewDataStore(destshpparams);

    // duplicate existing schema to create destination's one
    Name originalName = schema.getName();
    NameImpl theName = new NameImpl(originalName.getNamespaceURI(), originalName.getSeparator(), destinationSchemaName);
    SimpleFeatureType destschema = factory.createSimpleFeatureType(theName, schema.getAttributeDescriptors(),
      schema.getGeometryDescriptor(), schema.isAbstract(), schema.getRestrictions(), schema.getSuper(), schema.getDescription());
    destShpStore.createSchema(destschema);

    SimpleFeatureStore destFeatureStore = (SimpleFeatureStore) destShpStore.getFeatureSource(destinationSchemaName);
    destFeatureStore.addFeatures(filteredFeatures);

    // Fermer les connections et les fichiers
    destShpStore.dispose();
   }
  }
 }
}

Avec ce type de code, il est possible d'extraire une nouvelle cartographie spécifique (découpage selon la variable DPT_NUM) à partir d'un lot de données source.

Pour une mise en place plus rapide, voici les dépendances nécessaires (pom.xml) :

...
<repositories>
 <repository>
  <id>osgeo</id>
  <name>Open Source Geospatial Foundation Repository</name>
  <url>http://download.osgeo.org/webdav/geotools/</url>
 </repository>
</repositories>
...
<dependencies>
 <!-- Geo Tools -->
 <dependency>
  <groupId>org.geotools</groupId>
  <artifactId>gt-shapefile</artifactId>
  <version>8.0-M4</version>
 </dependency>
 <dependency>
  <groupId>org.geotools.jdbc</groupId>
  <artifactId>gt-jdbc-postgis</artifactId>
  <version>8.0-M4</version>
 </dependency>
 <dependency>
  <groupId>org.geotools</groupId>
  <artifactId>gt-cql</artifactId>
  <version>8.0-M4</version>
 </dependency>
</dependencies>

Voilà tout, bon courage!
HTH


Fichier(s) joint(s) :



Apache Camel par l'exemple

Avec cet article j'ai décidé d'entrer directement dans le vif du sujet...

S'il fallait présenter rapidement Camel, on pourrait dire qu'il s'agit d'une plateforme d'intégration d'application, basée sur un système d'échange de messages et dont le but est de fournir une implémentation des grands patrons d'intégrations en entreprise (facilitant la communication inter-applications). Pour ne pas plagier ou paraphraser, voici deux articles intéressants présentant ces patrons : le premier sur le site de Novedia et le second, plus détaillé chez Soat. Pour continuer sur une présentation plus spécifique de Camel, voici un premier billet écrit sur le blog d'Octo en enfin une présentation complète par un des co-auteurs du livre "Camel In Action", Jonathan Anstey.

Mon but ici est donc de fournir un exemple de mise en place d'un "bus" Camel pour créer un flux applicatif.

Le scénario est le suivant : on doit récupérer une archive zippée sur un serveur FTP distant, la décompresser et traiter son contenu en fonction de son type : les fichiers CSV doivent être segmentés selon une règle métier puis re-zippés unitairement et les autres types de fichiers sont envoyés à un script shell. Entre-temps, les données sont triées et validées. Celles qui sont invalides sont déposées séparément dans un répertoire spécifique.

De manière plus illustrée :

En jaune sont représentés les composants intégrés à Camel : FTP, ZIP, EXEC et CSV
En rouge sont illustrés les patrons d'intégration implémentés : Split, Enricher, Router, Filter, Sort, Recipient list, Validate.
En blanc sont indiqués les beans/services personnalisés ajoutés.

Et maintenant le plus intéressant, le code pour la mise en place des routes (les commentaires décrivent tout son fonctionnement) :

import java.util.Comparator;
import java.util.List;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.dataformat.csv.CsvDataFormat;
import org.apache.camel.language.bean.BeanLanguage;
import org.apache.camel.processor.validation.PredicateValidationException;

import CamelProperties;
import ReferentielDepartement;

/**
 * Main class to creates Camel routes
 * 
 * @author pe.faidherbe
 *
 */
public class MyRoutesBuilder extends RouteBuilder {
 
 // Stores archive's file name retrieved from remote server
 private static String downloadedFileName;
 
 // Indicates Csv column's name used to sort datas
 private static String csvIdentityColumn;
 
 // Indicates Csv column's number used to sort datas
 private static Integer csvIdentityColumnNumber;
 
 // Indicates length of sorting data used as data identifier
 private static int csvIdentityLength;
 
 // General properties
 private static final CamelProperties camelProps = CamelProperties.getInstance();
 
 // Csv data delimiter
 private static final String csvDelimiter = camelProps.getProperty(CamelProperties.CAMEL_DATA_SEP);
 
 // Prefix of data file (used for routing)
 private static final String NAT_FILE_PREFIX = camelProps.getProperty(CamelProperties.CAMEL_DATA_FILE_NAT_PREFIX);
 
 // Prefix of data file (used for routing)
 private static final String S2_FILE_PREFIX = camelProps.getProperty(CamelProperties.CAMEL_DATA_FILE_S2_PREFIX);
 
 // Prefix of data file (used for routing)
 private static final String ILOT_FILE_PREFIX = camelProps.getProperty(CamelProperties.CAMEL_DATA_FILE_ILOT_PREFIX);
 
 /**
  * Getter used by Camel
  * @return remote File Name
  */
 public String getDownloadedFileName() {
  return downloadedFileName;
 }
 
 /**
  * Getter used by Camel
  * @return csv identity column
  */
 public String getCsvIdentColumn() {
  return csvIdentityColumn;
 }
 
 /**
  * Getter used by Camel
  * @return csv identity column num
  */
 public Integer getCsvIdentityColumnNumber() {
  return csvIdentityColumnNumber;
 }
 
 /**
  * Getter used by Camel
  * @return csv identity data length
  */
 public int getCsvIdentLength() {
  return csvIdentityLength;
 }
 
 /**
  * Getter used by Camel
  * @return csv delimiter to use
  */
 public String getCsvDelimiter() {
  return csvDelimiter;
 }
 
 /**
  * Used by first Camel route to "persist" informations about remote file
  * later passed as parameters for second route
  */
 private void setMyContext(String downloadedArchive) {
  downloadedFileName = downloadedArchive.substring(0, downloadedArchive.indexOf("."));
  if(downloadedFileName.startsWith(S2_FILE_PREFIX)) {
   csvIdentityColumn = camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_COL_S2);
   csvIdentityLength = Integer.parseInt(camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_S2_LENGTH));
   csvIdentityColumnNumber = Integer.parseInt(camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_COL_S2_NUM));
  } else if(downloadedFileName.startsWith(NAT_FILE_PREFIX)) {
   csvIdentityColumn = camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_COL_NAT);
   csvIdentityLength = Integer.parseInt(camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_NAT_LENGTH));
   csvIdentityColumnNumber = Integer.parseInt(camelProps.getProperty(CamelProperties.CAMEL_DATA_IDENT_COL_NAT_NUM));
  }
 }

 /**
  * @see org.apache.camel.builder.RouteBuilder#configure()
  */
 @Override
 public void configure() throws Exception {
  // Props
  String camelWorkDir = camelProps.getProperty(CamelProperties.CAMEL_WORK_DIR);
  
  // Csv comparator
  CsvSorter sorter = new CsvSorter();
  
  // dead Letter Channel
  errorHandler(deadLetterChannel("log:camel"));
  
  // not validated messages go to particular error folder
  onException(PredicateValidationException.class).handled(true)
   .to("file://C:/test2/csverror?fileName=${header:downloadedFileName}_${header:territoire}_${date:now:yyyyMMddHHmmss}.csv")
   .log("Validation Error : ${exception.message}").end(); 
  
  /*
   * Download
   */
  from("ftp://"+camelProps.getProperty(CamelProperties.FTP_USER_PROP)
    + "@"
    + camelProps.getProperty(CamelProperties.FTP_HOST)
    + "?password="
    + camelProps.getProperty(CamelProperties.FTP_USER_PWD)
    + "&binary=true&noop=true&disconnect=true"
    // Poll every X sec
    + "&consumer.delay=" + camelProps.getProperty(CamelProperties.FTP_POLL_TIME_MS)
    // Specify temp destination for performance issue (not loaded in memory)
    + "&localWorkDirectory="+camelWorkDir)
   .log("Unzipping : ${file:name}")
   // Read as zip file
   .marshal().zip()
   // Unzip in memory 
   .unmarshal().zip()
   // Send to bean to extract entries
   .split().method("ZipService","unzipFile")
    .log("Extracted : ${header:entryName}")
   // Write each file
   .to("file://"+camelWorkDir+"?fileName=${header:entryName}")
   .process(new Processor() {
    @Override
    public void process(Exchange e) throws Exception {
     // Set informations on how to treat latter data
     setMyContext((String) e.getIn().getHeader("CamelFileName"));
    }
   })
  .end();
  
  /*
   *  ROUTER
   */
  from("file://"+camelWorkDir).id("routerRoute").log("Routing start")
   // No autostart to avoid polling "undesired" file (not previously retrieved from ftp)
   //.noAutoStartup()
   // Enrich file polling with context informations
   .enrich("direct:contextEnricher")
   .choice()
    .when(header("downloadedFileName").startsWith(S2_FILE_PREFIX))
     // CSV data, going to split
     .to("direct:surfaces")
    .when(header("downloadedFileName").startsWith(ILOT_FILE_PREFIX))
     // Geo data, go to DB
     .to("direct:ilots")
    .when(header("downloadedFileName").startsWith(NAT_FILE_PREFIX))
     // CSV data, going to split
     .to("direct:national")
    .otherwise()
     .log("Fichier ${file:name} non pris en charge!")
    .end();
  
  /*
   * Content enricher
   */
  from("direct:contextEnricher")
   .setHeader("downloadedFileName", BeanLanguage.bean(getClass(), "getDownloadedFileName"))
   .setHeader("csvIdentityColumn", BeanLanguage.bean(getClass(), "getCsvIdentColumn"))
   .setHeader("csvIdentityLength", BeanLanguage.bean(getClass(), "getCsvIdentLength"))
   .setHeader("csvDelimiter", BeanLanguage.bean(getClass(), "getCsvDelimiter"));
  
  /*
   * Manage geo data
   */
  from("direct:ilots")
   // Manage only SHP files
   .filter(header("entryName").endsWith("shp"))
   .to("file://C:/test2?fileName=${header:entryName}")
   // RecipientList is needed because route is computed at runtime (because of dynamic parameters)
   .recipientList(simple("exec:C:/test2/cmd/ogr2ogr.bat?args=${header:entryName}&workingDir=C:/test2/cmd/&useStderrOnEmptyStdout=true"))
    // Convert to String because cmd return is InputStream
    .convertBodyTo(String.class)
    .log("Command return : ${body} , error : ${header:exec_stderr}")
   .end();
  
  /*
   * Split CSV
   */

  // Used to customized CSV separator
  CsvDataFormat csvFormat = new CsvDataFormat();
  csvFormat.setDelimiter(csvDelimiter);
  
  from("direct:surfaces").convertBodyTo(String.class).unmarshal(csvFormat)
   .sort(body(), sorter)
   // Split CSV content
   // Bean uses StringBuilder and writes CSV content in order to get better performance than
   // creating a lot of List<Map<String, Object>> handled by camel's csv marshaler
   .split().method("CsvService","splitDatas")
   // Business check : is "territoire" a valid data?
   .validate(header("territoire").in(ReferentielDepartement.getDepartements()))
   // Write each data to a proper file
   .to("file://C:/test2/splitted?fileName=Territorial_${header:territoire}_${date:now:yyyyMMdd}.csv")
   .log("Written CSV file for : ${header:territoire}");
  
  from("direct:national").convertBodyTo(String.class).unmarshal(csvFormat)
   .sort(body(), sorter)
   // Split CSV content
   .split().method("CsvService","splitDatas")
   .validate(header("territoire").in(ReferentielDepartement.getDepartements()))
   // Write each data to a proper file
   .to("file://C:/test2/splitted?fileName=${file:onlyname.noext}_${header:territoire}_${date:now:yyyyMMddHHmmss}.csv")
   .log("Written CSV file for : ${header:territoire}");
  
  
  /*
   * Zip final files
   */
  // To handle transformation from camel's DeflaterOutputStream to traditional ZipOutputStream
  CustomizedZipDataFormat zipFormat = new CustomizedZipDataFormat();
  from("file://C:/test2/splitted").marshal(zipFormat)
   .to("file://C:/test2?fileName=${file:onlyname.noext}.zip")
   .log("Zipped : ${file:onlyname.noext}");
 }
 
 class CsvSorter implements Comparator<List<String>> {
  @Override
  public int compare(List<String> o1, List<String> o2) {
   int result = 0;
   // Do not treat first line (headers)
   if(!o1.contains(csvIdentityColumn) && !o2.contains(csvIdentityColumn)) {
    String ccom1 = o1.get(csvIdentityColumnNumber).substring(0, csvIdentityLength);
    String ccom2 = o2.get(csvIdentityColumnNumber).substring(0, csvIdentityLength);
    result = ccom1.compareTo(ccom2);
   }
   return result;
  }
 }
}

Pour ce qui est du service permettant de segmenter les informations CSV, voici son squelette :

public class CsvService {
 
 /**
  * Receives csv informations and split it out to multiple messages
  * Received datas are pre-ordered
  * @param headers in-message headers
  * @param body in-message body (unmarshalled csv content)
  * @return messages containing csv info to be written
  */
 public List<Message> splitDatas(@Headers Map<String, Object> headers,
   @Body List<List<String>> body) {
  List<Message> answer = new ArrayList<Message>();
  
  // headers
  List<String> csvHeaders = body.get(0);
  
  String csvDelimiter = (String) headers.get("csvDelimiter");
  
  // data
  List<List<String>> datas = body.subList(1, body.size());

  (... sort routine ...)

  return answer;
 }
}

Pour ce qui est du service permettant de dézipper l'archive :

public class ZipService {

 /**
  * Splits in message to multiple messages for entries
  * @param headers in headers
  * @param body in body
  * @return one message per zip entry
  */
 public List<Message> unzipFile(@Headers Map<String, Object> headers,
   @Body Object body) {
  List<Message> answer = new ArrayList<Message>();
  try {
   ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(
     (byte[]) body));
   ZipEntry ze = null;
   String entryName = "";
   String unzippedFiles = "";
   while ((ze = zis.getNextEntry()) != null) {
    entryName = ze.getName();
    unzippedFiles += entryName + ",";
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    for (int c = zis.read(); c != -1; c = zis.read()) {
     out.write(c);
    }
    zis.closeEntry();
    out.close();
    DefaultMessage message = new DefaultMessage();
    Map<String, Object> newHeaders = new CaseInsensitiveMap(headers);
    newHeaders.put("entryName", entryName);
    newHeaders.put("unzippedFiles", unzippedFiles);
    message.setHeaders(newHeaders);
    message.setBody(out.toByteArray());
    answer.add(message);
   }
   zis.close();
  } catch (Throwable e) {
   e.printStackTrace();
  }
  return answer;
 }
}

Enfin, le code utilisé pour créer des archives zip utilisables (via CustomizedZipDataFormat) provient de cette page.

J'espère que tout ce code ne paraît pas trop indigeste, mais en y regardant de plus près, on s'aperçoit que Camel permet assez facilement de mettre en place ce genre de flux de données, en peu de lignes de code et surtout de manière plutôt lisible. La documentation est d'ailleurs incroyablement bien faite pour ce qui concerne la description des patrons et des composants natifs.

Le seul bémol que j'ajouterai est la gestion des formats ZIP. En effet, par défaut, Camel crée des DeflaterOutputStream : je ne sais pas trop d'où provient ce format, mais en tout cas il ne permet pas directement de créer des archives lisibles. Il faut donc explicitement les convertir en ZipOutputStream classique.

N'hésitez pas à m'indiquer si vous avez déjà utilisé cet outil et surtout si vous voyez des façons d'améliorer ce que j'ai présenté!

Sources :


Fichier(s) joint(s) :



XSLT et modification de namespace

Cet article a pour but d'éclaircir l'utilisation des namespaces au sein des feuilles de transformations XSL.

Imaginons qu'il faille changer le namespace global du xml suivant :

<?xml version="1.0" encoding="utf-8"?>
<pef:OPS xmlns:pef="http://www.fooldomain.fr">
  <pef:DAT TM="CU">
    ...
  </pef:DAT>
</pef:OPS>

En celui-ci :

<?xml version="1.0" encoding="utf-8"?>
<pef:OPS xmlns:pef="http://developpef.blogspot.com">
  <pef:DAT TM="CU">
    ...
  </pef:DAT>
</pef:OPS>

A première vue, le template suivant devrait faire l'affaire :

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:pef="http://www.fooldomain.fr"
  version="1.0">
  
 <xsl:template match="pef:OPS">
  <pef:OPS xmlns:pef="http://developpef.blogspot.com">
   <pef:DAT TM="CU">
   ...
   </pef:DAT>
  </pef:OPS>
 </xsl:template>
  
</xsl:stylesheet>

Or, voici ce qui sera produit :

<?xml version="1.0" encoding="utf-8"?>
<pef:OPS xmlns:pef="http://developpef.blogspot.com">
  <pef:DAT xmlns:pef="http://www.fooldomain.fr" TM="CU">
    ...
  </pef:DAT>
</pef:OPS>

Le processeur XSL ajoutera l'ancien namespace au premier élément qui n'en porte pas... Pourquoi donc? Tout d'abord, la déclaration du namespace telle que faite sur l'élément OPS n'affecte en aucun cas les namespaces utilisés par le processeur. Lui ne connait que ceux qui sont déclarés en en-tête du document. Il va donc naturellement rajouter le namespace qu'il utilise au premier élément considéré. En réalité, dans ce cas de figure, il faut explicitement indiquer au processeur les deux namespace à utiliser : le premier lui permettant de lire le XML entrant et le second décrivant le XML sortant. Voici donc à quoi doit ressemble notre XSLT :

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:peffool="http://www.fooldomain.fr"
  xmlns:pef="http://developpef.blogspot.com"
  exclude-result-prefixes="peffool"
  version="1.0">
  
 <xsl:template match="peffool:OPS">
  <pef:OPS>
   <pef:DAT TM="CU">
   ...
   </pef:DAT>
  </pef:OPS>
 </xsl:template>
  
</xsl:stylesheet>

Ainsi, le template travaillera bien sur les éléments basés sur l'ancien namespace peffool:OPS pour produire le nouveau XML basé sur le bon namespace xmlns:pef="http://developpef.blogspot.com"

Hope this helps! :)


Fichier(s) joint(s) :

XSLT et transcodage

Lors de la transformation d'un fichier XML, il peut être nécessaire d'effectuer un transcodage sur certains éléments (migration d'une ancienne valeur vers une nouvelle). Les choses se compliquent si en plus ceci doit se faire à partir d'un référentiel externe contenant le mapping des données à utiliser.

Concrètement, voici un exemple de fichier XML initial :

<?xml version="1.0"?>
<OPS AD="FRA" FR="FRA">
      <LOG RC="FHZW" MA="VIAUD">
          <SPE SN="ANE" WT="5000.00">
            <PRO FF="A" PS="FRE" PR="WHL" TY="BOX" CF="1.00"></PRO>
            <RAS SR="23E6"></RAS>
          </SPE>
          <SPE SN="ANE" WT="5000.00">
            <PRO FF="A" PS="FRE" PR="WHL" TY="BOX" CF="1.00"></PRO>
            <RAS SR="24F8"></RAS>
         </SPE>
     </LOG>
</OPS>

Qui doit être convertit pour obtenir ceci (on change la valeur de l'attribut SR de l'élement RAS) :

<?xml version="1.0"?>
<OPS AD="FRA" FR="FRA">
      <LOG RC="FHZW" MA="VIAUD">
          <SPE SN="ANE" WT="5000.00">
            <PRO FF="A" PS="FRE" PR="WHL" TY="BOX" CF="1.00"></PRO>
            <RAS SR="FR23E6"></RAS>
          </SPE>
          <SPE SN="ANE" WT="5000.00">
            <PRO FF="A" PS="FRE" PR="WHL" TY="BOX" CF="1.00"></PRO>
            <RAS SR="FR24F8"></RAS>
         </SPE>
     </LOG>
</OPS>

La table de transcodage utilisée est située dans le document 'transcodage.xml' :

<mapping>
    <data>
        <item v1="23E6" v3="FR23E6" />
        <item v1="24F8" v3="FR24F8" />
    </data>
</mapping>

Pour arriver à nos fins, XSLT met à notre disposition l'élément xsl:key qui permet de définir une clé de recherche dans un document. Voici comment l'utiliser :

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
<xsl:variable name="map" select="document('transcodage.xml')"/>
<xsl:key name="mapPorts" match="/mapping/data/item" use="@v1"/>
 
<xsl:template match="@* | node()">
    <xsl:copy>
        <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
</xsl:template>
 
<xsl:template match="@SR[parent::RAS]">
    <xsl:variable name="codeV1" select="."/>
    <xsl:for-each select="$map">
        <xsl:attribute name="SR">
            <xsl:value-of select="key('mapPorts', $codeV1)/@v3"/>
        </xsl:attribute>
    </xsl:for-each>
</xsl:template>
 
</xsl:stylesheet>

Voici en détail le déroulement des opérations :

  • La variable map contient la table de transcodage.
  • La clé mapPorts permet d'indiquer : "je recherche tous les éléments correspondant à /mapping/data/item et je teste leur attribut v1
  • Dans le template gérant l'attribut SR du noeud RAS, on parcourt les éléments de la table de transcodage (en changeant temporairement de noeud racine grâce à xsl:for-each) afin de récupérer la valeur de l'attribut v3 de l'élément dont l'attribut v1 porte la valeur actuelle. En effet, l'appel à la fonction key() retourne l'élément qui a répondu aux conditions passées en paramètres (chemin décrit par la clé et valeur d'attribut)

Ainsi, chaque ancienne valeur sera remplacée par la valeur correspondante issue du document de mapping. Une autre syntaxe est possible, sans utiliser de clé :

<xsl:template match="@SR[parent::RAS]">
    <xsl:attribute name="SR">
       <xsl:value-of select="$map/mapping/data/item[@v1 = current()]/@v3"/>
    </xsl:attribute>
</xsl:template>

Cette solution est tout de même à modérer dans le cas de volume important de données.

Mon prochain article sera dédié au changement de namespace lors d'une transformation XSL.

Bon courage!


Fichier(s) joint(s) :

Exporter un document Calc en XML

Encore une fois, les choses ne sont pas si simples qu'il n'y paraît...

Ici le besoin consiste à exporter un classeur OpenOffice Calc en XML, selon une structure personnalisée. Le logiciel offre déjà la possibilité d'exporter sous forme d'XML à la sauce Micro$oft : autant dire que le résultat n'est pas glorieux :

Le document Calc :

Le XML produit (Fichier > Enregistrer sous... > Microsoft Excel 2003 XML) :

<?xml version="1.0" encoding="utf-8"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x2="http://schemas.microsoft.com/office/excel/2003/xml" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:x="urn:schemas-microsoft-com:office:excel">
  <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
    <Colors>
      <Color>
        <Index>3</Index>
        <RGB>#c0c0c0</RGB>
      </Color>
      <Color>
        <Index>4</Index>
        <RGB>#ff0000</RGB>
      </Color>
    </Colors>
  </OfficeDocumentSettings>
  <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
    <WindowHeight>9000</WindowHeight>
    <WindowWidth>13860</WindowWidth>
    <WindowTopX>240</WindowTopX>
    <WindowTopY>75</WindowTopY>
    <ProtectStructure>False</ProtectStructure>
    <ProtectWindows>False</ProtectWindows>
  </ExcelWorkbook>
  <Styles>
    <Style ss:ID="Default" ss:Name="Default" />
    <Style ss:ID="Result" ss:Name="Result">
      <Font ss:Bold="1" ss:Italic="1" ss:Underline="Single" />
    </Style>
    <Style ss:ID="Result2" ss:Name="Result2">
      <Font ss:Bold="1" ss:Italic="1" ss:Underline="Single" />
      <NumberFormat ss:Format="Euro Currency" />
    </Style>
    <Style ss:ID="Heading" ss:Name="Heading">
      <Alignment ss:Horizontal="Center" />
      <Font ss:Bold="1" ss:Italic="1" ss:Size="16" />
    </Style>
    <Style ss:ID="Heading1" ss:Name="Heading1">
      <Alignment ss:Horizontal="Center" ss:Rotate="90" />
      <Font ss:Bold="1" ss:Italic="1" ss:Size="16" />
    </Style>
    <Style ss:ID="co1" />
    <Style ss:ID="ta1" />
    <Style ss:ID="ta_extref" />
  </Styles>
  <ss:Worksheet ss:Name="Exemple1">
    <Table ss:StyleID="ta1">
      <Column ss:Span="1" ss:Width="64.2614" />
      <Row ss:Height="12.8409">
        <Cell>
          <Data ss:Type="String">Donnée 1</Data>
        </Cell>
        <Cell>
          <Data ss:Type="String">Donnée 1.1</Data>
        </Cell>
      </Row>
      <Row ss:Height="12.8409">
        <Cell>
          <Data ss:Type="String">Donnée 2</Data>
        </Cell>
        <Cell>
          <Data ss:Type="String">Donnée 1.2</Data>
        </Cell>
      </Row>
    </Table>
    <x:WorksheetOptions />
  </ss:Worksheet>
  <ss:Worksheet ss:Name="Exemple2">
    <Table ss:StyleID="ta1">
      <Column ss:Span="1" ss:Width="64.2614" />
      <Row ss:Height="12.8409">
        <Cell>
          <Data ss:Type="String">Feuille 2 test</Data>
        </Cell>
        <Cell>
          <Data ss:Type="String">Autre valeur</Data>
        </Cell>
      </Row>
    </Table>
    <x:WorksheetOptions />
  </ss:Worksheet>
</Workbook>

Pas très lisible tout ça...

Avant de continuer, il faut savoir que les fichiers Calc (.ods) ne sont en fait que des fichiers zip contenant des XML. La structure de base du classeur, tel que lu par OpenOffice est la suivante :

  • content.xml
  • meta.xml
  • mimetype.xml
  • settings.xml
  • styles.xml

Tous ces fichiers décrivent le contenu, la présentation et les métas informations constituant le classeur. Le plus intéressant est le premier. En voici un extrait :

  <office:body>
    <office:spreadsheet>
      <table:table table:name="Exemple1" table:style-name="ta1" table:print="false">
        <office:forms form:automatic-focus="false" form:apply-design-mode="false" />
        <table:table-column table:style-name="co1" table:number-columns-repeated="2" table:default-cell-style-name="Default" />
        <table:table-row table:style-name="ro1">
          <table:table-cell office:value-type="string">
            <text:p>Donnée 1</text:p>
          </table:table-cell>
          <table:table-cell office:value-type="string">
            <text:p>Donnée 1.1</text:p>
          </table:table-cell>
        </table:table-row>
        <table:table-row table:style-name="ro1">
          <table:table-cell office:value-type="string">
            <text:p>Donnée 2</text:p>
          </table:table-cell>
          <table:table-cell office:value-type="string">
            <text:p>Donnée 1.2</text:p>
          </table:table-cell>
        </table:table-row>
      </table:table>
      <table:table table:name="Exemple2" table:style-name="ta1" table:print="false">
        <table:table-column table:style-name="co1" table:number-columns-repeated="2" table:default-cell-style-name="Default" />
        <table:table-row table:style-name="ro1">
          <table:table-cell office:value-type="string">
            <text:p>Feuille 2 test</text:p>
          </table:table-cell>
          <table:table-cell office:value-type="string">
            <text:p>Autre valeur</text:p>
          </table:table-cell>
        </table:table-row>
      </table:table>
    </office:spreadsheet>
  </office:body>

On retrouve ici les informations contenues dans les cellules de chaque feuille.

Ainsi, puisque la structure des documents Calc est basée sur du XML, si l'on désire l'exporter sous un autre format, il faudra passer par... XSLT!

Admettons que nous voulions exporter le document présenté plus haut sous la forme :

<mapping>
 <Exemple1>
  <data col1="Donnée 1" col2="Donnée 1.1" />
  <data col1="Donnée 2" col2="Donnée 1.2" />
 </Exemple1>
 <Exemple2>
 ...
</mapping>

Il faudra donc trouver la feuille de transformation XSL convenable. Par exemple, celle-ci (le code est perfectible mais est présenté à titre d'exemple) :

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
 xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
 xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
 exclude-result-prefixes="office table">
 
<xsl:output method="xml" indent="yes" encoding="UTF-8" omit-xml-declaration="yes"/>
 
<!-- To avoid annoying header data -->
<xsl:template match="office:meta" />
<xsl:template match="office:settings" />
<xsl:template match="office:styles" />
<xsl:template match="office:master-styles" />
<xsl:template match="office:automatic-styles" />
<xsl:template match="office:font-face-decls" />
<xsl:template match="office:scripts" />
<xsl:template match="office:font-face-decls" />
 
<!-- parse document -->
<xsl:template match="office:spreadsheet">
 <xsl:element name="mapping">
  <xsl:for-each select="table:table">
   <!-- take only sheets that have real data (multiple columns) -->
   <xsl:if test="table:table-column[@table:number-columns-repeated]">
    <xsl:variable name="tablename" select="@table:name" />
    <xsl:element name="{$tablename}">
     <xsl:for-each select="table:table-row">
      <xsl:element name="item">
       <xsl:for-each select="table:table-cell">
        <xsl:variable name="attrNameV1" select="'col1'" />
        <xsl:variable name="attrNameV3" select="'col2'" />
        <xsl:if test="position() = 1">
         <xsl:attribute name="{$attrNameV1}">
          <xsl:value-of select="text:p" />
         </xsl:attribute>
        </xsl:if>
        <xsl:if test="position() = 2">
         <xsl:attribute name="{$attrNameV3}">
          <xsl:value-of select="text:p" />
         </xsl:attribute>
        </xsl:if>
       </xsl:for-each>
      </xsl:element>
     </xsl:for-each>
    </xsl:element>
   </xsl:if>
  </xsl:for-each>
 </xsl:element>
</xsl:template>
</xsl:stylesheet>

En détails, voici comment elle fonctionne :

  • Les namespaces déclarés correspondent à ceux attendus par OpenOffice pour présenter ses propres balises.
  • Le premier bloc de templates supprime le traitement de tout ce qui est méta-infos, styles et autres scripts éventuellement contenus dans le classeur.
  • L'élement table:table décrit chaque feuille du classeur
  • Le test xsl:if permet de ne pas traiter les feuilles ne contenant aucune donnée (chaque feuille contenant par défault toujours au moins une cellule vide)
  • Enfin, on parcour chaque ligne (table:table-row) et chaque cellule (table:table-cell) pour en extraire le contenu (text:p)

Pour rendre cet export disponible dans OpenOffice, il faut le configurer. Pour ce faire : Outils > Paramétrages du filtre XML... > Nouveau... Et renseigner le chemin vers notre XSLT dans la section "XSLT pour export"

Notre export sera finalement réalisable depuis le menu Fichier > Exporter...

Pourquoi faire simple quand on peut faire compliqué!


Fichier(s) joint(s) :



Pourquoi Scala ne m'a pas séduit...

Il y a quelques temps, j'écrivais une série d'articles sur Scala et la programmation fonctionnelle, en tant que débutant et pour partager ce que j'ai pu découvrir de ces concepts.

Même si l'aspect fonctionnel est intéressant, après plusieurs tentatives de m'y remettre, je dois avouer que je n'ai pas vraiment accroché. Et pour expliquer les raisons de mon changement d'avis, je m'en remets à ce post de Stephen Colebourne qui exprime plutôt clairement ce que j'ai ressenti. Si je devais en extraire les points les plus marquants :

By the way, if you're looking at Scala, you may come across conference presentations, blog posts and forums that show small snippets of code in Java and Scala and show how much less code you have to write in Scala, and how much simpler it appears to be. This is a clever trick. The real complexity occurs when you start mixing each of the seemingly simple features together in a large codebase, on a big team, over a period of time, where code is read far more than it is written. That is when you start to appreciate that some of Java's restrictions are there for a reason.
The language is a well-meaning attempt to create something with a higher abstraction level. But what got created is a language that has huge inherent complexity and doesn't really address the true issues that developers face today, yet is being pitched as being a suitable replacement for Java, something which I find to be bonkers.

Tout ca pour dire qu'au final, même si Scala m'avais semblé être une alternative séduisante à Java, le plongeon rapide dans un niveau de complexité élevée m'a quelque peu rebuté... Mon but n'était pas d'apprendre un nouveau langage, mais plutôt de trouver quelque chose de complémentaire à Java.

Comme "alternative/complément" à Java, je préfère Groovy. Comme "Java sugar", je préfère Xtend.


Fichier(s) joint(s) :

Déployer Java dans le cloud avec Jelastic

Depuis quelques temps, les systèmes de gestion d'applications Cloud (PaaS - Plateforme as a Service) en tout genre se multiplient. Mais certains à mon sens sortent vraiment du lot en proposant de réels "services". C'est le cas notamment de Jelastic qui permet de créer des environnements de déploiement d'applications Java très facilement.

Voici un graphique résumant les fonctionnalités offertes :

Avec seulement un compte sur ce site, il est possible de récupérer le code source depuis un serveur de gestion de configuration, packager le tout grâce à Maven, déployer sur un serveur Tomcat et gérer une base de données. Le tout éventuellement de manière sécurisée (SSL) et ajustable selon le niveau de performances requis (load balancing). Qui dit mieux??!!

Le but étant de faciliter la mise en place de multiples processus de déploiements d'un application sur différents environnements (développement, test, recette...) sans avoir à se soucier de l'aspect matériel (technique). En effet, la liste des outils mis à disposition est déjà assez impressionnante :

Java developers have access to support for any JVM-based application including pure Java 6 or 7, JRuby, Scala and Groovy. In addition, Jelastic provides support for SQL databases that include MariaDB, MySQL, and PostgreSQL. Non-SQL database support is provided for MongoDB and CouchDB. Supported application servers include Tomcat 6 and 7, GlassFish and Jetty. Load balancing and caching is done though integrated NGINX, and developer tools integration via Maven and Ant plug-ins.

L'idée est ingénieuse, la mise en oeuvre semble bien aboutit puisque le site est très facile à utiliser, reste donc à savoir si cela séduira les développeurs et managers. La plateforme est encore en version beta mais commence à faire ses preuves comme on peut le voir au travers de son blog et sa communauté semble déjà bien active.

Un outil de plus à suivre de près!


Fichier(s) joint(s) :



Des outils pour maîtriser contenu XML, schémas et transformations

La problématique à l'origine de cet article est la suivante : quels sont les outils (simples et visuels) existants pour déduire la feuille de transformation (XSL) nécessaire à la conversion de fichiers XML entre deux versions de schémas (XSD).

De prime abord, il existe beaucoup de logiciels sur le web répondant à ce besoin. Or il se trouve qu'aucun ne possède réellement l'ensemble des fonctionnalités désirées. Je vais donc ici compiler les petites astuces pour se munir de tout le nécessaire afin de se faciliter la vie.

La première étape est de réussir à comparer les schémas XSD pour essayer de lier les correspondances existantes entre les deux versions. Pour ce faire, le logiciel DiffDog de la société Altova, leader dans le domaine de la manipulation de fichiers XML, me semble de loin le plus simple et précis. Voici par exemple ce qu'il est capable de faire en ouvrant simplement les deux schémas :

Rien de plus aisé donc que de mapper les éléments, même dans des schémas longs et complexes, grâce à son interface accessible. Puis, à partir de cette construction, il est possible de générer le fichier XSL nécessaire à la transformation entre les deux versions.

Petit bémol cependant, comme vous pouvez le voir sur l'aperçu, DiffDog ne tient pas compte des éléments non mappés : ceux-ci n'apparaitront donc jamais dans le XSL généré... Plutôt gênant s'il s'agit d'attributs obligatoires! Dans le même principe, les contraintes spécifiées (format des données, caractère obligatoire...) ne sont pas gérées.

Il faut donc prévoir dans ce scénario de modifier à la main la feuille de transformation. Comment donc consulter le contenu du schéma de manière la plus simple possible afin de retrouver ces informations?

Le projet WTP de la fondation Eclipse dispose d'un éditeur de schémas XSD très complet permettant notamment d'en visualiser le contenu sous la forme de diagramme de classe :

Or là encore, pas moyen de visualiser rapidement l'arborescence des éléments ainsi que les contraintes... L'idéal serait une présentation sous forme de graphe. Altova propose le logiciel XMLSpy qui dispose d'une fonction de visualisation sous cette forme. Il est très complet et contient une palette d'outils très intéressante mais petit détail : il est payant...

En cherchant un peu sur le net, on peut voir qu'il existe des alternatives : WSVT, l'ancêtre de WTP. Mais là encore le bât blesse : la visualisation en graphe n'est plus disponible depuis la version 1.5 (juillet 2006 intégré dans la version 3.1 d'Eclipse!) comme l'indique ce bug!!!

Heureusement, la distribution en question est toujours téléchargeable à l'adresse suivante. Avec ceci, il est enfin possible de lire le contenu d'un schéma sous forme de graphe de manière claire, concise et complète :

Ici apparaissent bien plus explicitement les contraintes et types de relations entre les éléments.

Je ne sais pas trop pourquoi ce formalisme a été abandonné par WTP (peut-être à cause du lobbying des éditeurs de logiciels spécialisés?).

Dernier outil utile : la génération d'un XML basique à partir d'un schéma. Cette fois-ci, la dernière version de WTP contient la fonction "generate > XML file" disponible sur les fichiers XSD. Elle permet d'obtenir un fichier type répondant à l'ensemble des exigences du schéma. L'outil d'édition s'appuie ensuite sur ses spécifications pour aider la saisie du contenu :

Pour terminer, voici un petit bout de code Java permettant d'exécuter la transformation des fichiers XML :

import java.io.File;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public class Test {

 public static void main(String[] args) {
  try {
   File xmlFile = new File("OOB20110921007705.xml");
   File xsltFile = new File("ttt.xsl");
   File htmlFile = new File("result.xml");
   Source xmlSource = new StreamSource(xmlFile);
   Source xsltSource = new StreamSource(xsltFile);
   Result xmlResult = new StreamResult(htmlFile);
   TransformerFactory transFact = TransformerFactory.newInstance();
   Transformer trans = transFact.newTransformer(xsltSource);
   trans.transform(xmlSource, xmlResult);
  } catch (TransformerException e) {
   e.printStackTrace();
  }

 }

}

Voici donc une compilation des outils les plus simples (et si possibles gratuits) pour gérer du contenu XML. J'espère vous avoir fait économiser quelques heures de Googling!


Fichier(s) joint(s) :



Développeur artisan


We are tired of writing crap.


"Learning. Caring. Practicing. Sharing." The Four Pillars Of Software Craftsmanship

J'ai décidé dans cet article de me faire l'écho d'une mouvance qui refait surface récemment, le "software craftsmanship". Elle a pour but de valoriser le métier du développement logiciel comme l'artisanat au sens puriste du terme : un travail nécessitant savoir-faire, maîtrise et respect.

Après m'être renseigné, j'ai pu constaté que les débats autour de ce thème étaient déjà très avancés puisque l'idée n'est pas nouvelle (initiée au tout début des années 2000 notamment par le livre The Pragmatic Programmer: From Journeyman to Master).

Mon but ici n'est donc pas de lancer un n-ième débat mais plutôt de partager ces valeurs qui à mon sens peuvent apporter beaucoup de choses dans la façon de voir et de fonctionner dans ce métier. Pour illustrer ceci, je ne citerai que Robert Martin et son article sur le sujet qui résume en quelques points ces idées :

What we are not doing:

  • We are not putting code at the center of everything.
  • We are not turning inward and ignoring the business and the customer.
  • We are not inspecting our navels.
  • We are not offering cheap certifications. 
  • We are not forgetting that our job is to delight our customers. 

What we will not do anymore:

  • We will not make messes in order to meet a schedule.
  • We will not accept the stupid old lie about cleaning things up later.  
  • We will not believe the claim that quick means dirty.
  • We will not accept the option to do it wrong.
  • We will not allow anyone to force us to behave unprofessionally. 

What we will do from now on:

  • We will meet our schedules by knowing that the only way to go fast is to go well.
  • We will delight our customers by writing the best code we can.
  • We will honor our employers by creating the best designs we can.
  • We will honor our team by testing everything that can be tested.
  • We will be humble enough to write those tests first.
  • We will practice so that we become better at our craft.  

We will remember what our grandmothers and grandfathers told us:

  • Anything worth doing is worth doing well.
  • Slow and steady wins the race.
  • Measure twice cut once.
  • Practice, Practice, Practice.

Je vous invite donc à lire attentivement les différentes opinions listées dans les sources, elles apportent toutes un point de vue instructif et qui laisse quelques champs de réflexion...

Sources :


Fichier(s) joint(s) :



Côte de popularité des langages

La société Tiobe, société renommée dans l'audit de code et d'application, vient de publier les nouveaux résultats de son étude des tendances d'utilisation des différents langages de programmation :

Le premier graphique est sûrement des plus intéressants et révélateurs :

Comme on peut le voir, Java est toujours en tête. Certes C est devenu un concurrent important en terme de nombre de développeurs, mais un détail peut cependant faire pencher la balance en termes de "valeur" pour Java. Si on regarde bien ce graphique, on observe une forte chute de l'utilisation de Java durant l'année 2004/2005 : ceci est très probablement dû à la crise économico-financière de l'époque et donc à la baisse du nombre de projets réalisés sur le marché. De là à dire que Java est préférentiellement utilisé au niveau industriel...

Enfin, un dernier point pour évoquer le renouveau du dynamisme de Java amorcé cette année notamment avec la sortie de la version 7, voici un bref résumé du chemin parcouru et des perspectives d'amélioration.

Choisissez votre camp et à vos claviers!


Fichier(s) joint(s) :



SWTBot, tests fonctionnels pour développeurs

A l'heure de l'avènement de Jubula, plateforme de tests fonctionnels pour Eclipse RCP distribué avec Indigo, j'ai décidé de plutôt étudier le fonctionnement de SWTBot et ce pour plusieurs raisons :

  • Le "fonctionnel" ne doit pas être exclusif à la maîtrise d'ouvrage surtout quand il s'agit de test : c'est le reproche principal que je fais à Jubula. Il est à mon sens un peu trop orienté vers cette idée : de prime abord, il ressemble à une grosse usine à gaz prévue pour que les "acteurs du fonctionnel" puisse s'amuser à assembler les pièces du puzzle de leur scénario de test. Utiliser des "bibliothèques" d'actions (clic souris, drag and drop...) ou de comportements de composants (sélection de checkbox...) et les glisser-déposer sur le jeu de test pour créer le scénario me semble un peu moins pertinent qu'une suite d'instructions claires et atomiques écrites noir sur blanc...
  • Faciliter le mariage "intégration continue-tests fonctionnels" parce que chacun d'eux peut s'avérer être une peine en soi, si en plus il faut passer par des manipulations quelque peu capilotractées... Pourquoi faire compliqué quand on peut faire simple, avec juste une suite de tests JUnit!
  • Assurer la maintenance des tests car c'est bien souvent là que sont sollicités les développeurs. Même si certains tests fonctionnels sont parfois livrés dès le départ du projet, joints aux spécifications, dès que celles-ci évoluent (et c'est toujours le cas), ce sont bien les développeurs qui sont chargés des les maintenir. Donc fournir toute une batterie d'outils graphiques pour la réalisation des tests s'avère bien plus lourd et inconfortable que quelques lignes de codes à modifier simultanément à l'application.

Je tiens tout de même à préciser que ces remarques sont entièrement personnelles et subjectives et n'ont en aucun cas pour but de dévaloriser l'outil. Cet article a seulement vocation de mettre en avant certaines facilités offertes par SWTBot du point de vue développeur. Jubula a surement fait le choix d'autres cibles puisque moins particulièrement orienté SWT/Eclipse RCP.

Fonctionnement de SWTBot

Rentrons maintenant dans le vif du sujet. Le principe de base de ce plugin est de fournir un moyen d'exécuter une application basée sur SWT (et plus particulièrement Eclipse RCP), dérouler un scénario d'utilisation et contrôler son comportement. Techniquement, ceci est utilisable très facilement en créant une classe basique JUnit avec un runner fournit : SWTBotJunit4ClassRunner. Pour le reste, l'API parle d'elle-même, exemple :

@Test
public void testCompilationJava() throws Exception {
	// Préparation de la perspective
	bot.menu("Fenêtre").menu("Afficher la vue").menu("Autre...").click();
	bot.shell("Afficher Vue").activate();
	bot.tree().expandNode("Général").select("Progression");
	bot.button("OK").click();
	bot.menu("Fenêtre").menu("Afficher la vue").menu("Autre...").click();
	bot.shell("Afficher Vue").activate();
	bot.tree().expandNode("Général").select("Marqueurs");
	bot.button("OK").click();
	...
	// Pour supprimer les warnings de la vue 
	SWTBotView markersView = bot.viewByTitle("Marqueurs");
	markersView.show();
	markersView.setFocus();
	markersView.menu("Configurer le contenu...").click();
	bot.shell("Configurer le contenu").activate();
	bot.button("Nouveau").click();
	bot.checkBox("Afficher les gravités :").click();
	bot.checkBox("Avertissement").click();
	bot.checkBox("Info").click();
	bot.button("OK").click();
		
	/*
	 * Normallement, les éléments de la vue sont réduits et on
	 * teste qu'il n'y ait pas la catégorie "Incidents Java".
	 */
	SWTBotTree markersTree = tree(markersView);
	assertEquals(markersTree.visibleRowCount(),0);
}

private SWTBotTree tree(SWTBotView view) {
	List controls = new ChildrenControlFinder(view.getWidget()).findControls(widgetOfType(Tree.class));
	if (controls.isEmpty())
		throw new WidgetNotFoundException("Could not find any tree");
	SWTBotTree tree = new SWTBotTree(controls.get(0));
	return tree;
}

Le but de ce test est de vérifier qu'un projet java a bien été compilé et donc qu'il n'y a pas de marqueurs d'erreur dans la vue "Marqueurs". Simple, directif et concis. Comme vous pouvez le voir, il est possible d'utiliser directement les méthodes "assertXXX" de JUnit pour vérifier le comportement/état des éléments graphiques. La navigation dans l'interface est facilitée par la possibilité d'enchainer les instructions pointées comme lors de la sélection de sous-menus.

SWTBot recèle également quelques fonctionnalités très intéressantes notamment lors d'une utilisation dans un contexte d'intégration continue : système de log avancé et très précis, création de captures d'écran à chaque problème pour connaitre l'état de l'application... Et puisque chaque classe de test relève d'un simple outillage JUnit, une application Eclipse RCP peut être exécutée à partir d'un script ANT en ligne de commande! Que demande le peuple :) Si ce n'est pas encore fait, voici de quoi vous convaincre en moins de 5 min.

Un must-have donc pour tout développeur RCP!

Sources


Fichier(s) joint(s) :



Debugger comme un dieu...

Un des pires scénario (et donc des plus récurrents) dans l'apparition d'un bug est sûrement celui-ci :

Le client vient de nous remonter un bug qui semble ne se produire que sur son environnement de production - donc inaccessible et top-secret - et semble-t-il de manière assez aléatoire. Il faut que tu nous corriges ça au plus vite!

Passées les premières sueurs froides, il apparait que sous cet angle, seule une force divine pourrait vous aider à résoudre ce problème : pouvoir débugger l'application sans point d'arrêt (car elle ne peut pas tourner en mode debug sur l'environnement de prod), sans y avoir accès (puisque l'environnement est protégé) et qui plus est pile au moment où intervient le bug...

Rassurez-vous, vous pouvez maintenant invoquer la toute puissance de Chronos Chronon, un plugin Eclipse se définissant comme "time-traveling debugger".

Présentation

Chronon est disponible en version finale depuis le 25 avril dernier (fin de la bêta). Ses principales fonctionnalités sont assez simples mais révolutionnent les usages du debugger classique, comme le résume justement son fondateur Prashant Deva :

* A 'flight data recorder' for Java programs which can record every line of code executed inside a program and save it to a file on the disk. The file can be shared among developers and played back in our special time travelling debugger to instantly find the root cause of an issue. This also means that no bugs ever need to be reproduced!

* A Time Travelling Debugger, with a novel UI that plugs seamlessly into Eclipse, which allows you to playback the recordings. It not only step back and forward but to any point in the execution of your program.

Dans les détails...

La première étape lors de l'utilisation de Chronon est l'enregistrement du déroulement de l'application. Il est possible d'indiquer les packages à surveiller pour que chaque classe mise en jeu soit instrumentalisée en mémoire afin de pouvoir enregistrer tout ce qui s'y passe, ce qui signifie certes une perte de performance (quoique très bien gérée et limitée), mais surtout que seules ces classes-ci sont surveillées (et non les librairies externes éventuellement appelées). Au final, toutes ces informations sont compressées dans un fichier utilisable sur n'importe quelle machine.

La seconde étape, la plus fun intéressante, est la manipulation de ces enregistrements et le debug étape par étape avec la possibilité de remonter le temps. Voici comment se présente l'interface sous Eclipse :

Comme vous pouvez le voir, l'interface parle d'elle-même : beaucoup d'outils très prometteurs! Et tout ceci est très simple d'utilisation. Par exemple, pour trouver la cause de l'erreur levée, il suffit de cliquer sur la ligne en question dans la vue "Thrown exceptions". On peut donc savoir à quel moment du déroulement du scénario elle est apparue et quelles étaient les valeurs de toutes les variables à cet instant précis :

Et il en va de même pour les sorties dans la console : un simple clic sur une ligne renvoie au code exécuté :

Il est donc possible de naviguer en toute tranquillité dans le code qui a été exécuté afin de trouver à quel moment et pourquoi l'application s'est mal comportée. En effet, contrairement à d'autres debuggers "omniscients" focalisés sur l'état des objets, Chronon se concentre sur la notion de temps et permet de retracer le fil de l'histoire. C'est ce qui en fait un concept novateur voire révolutionnaire dans la façon d'envisager la résolution de bugs. Plus de pertes de temps à essayer de le reproduire sur place ou à l'aide de toute une batterie de tests/données créés pour l'occasion. Les équipes peuvent même se transmettre les enregistrements et enquêter indépendamment.

... Et bien plus encore...

Chronon recèle encore bien plus de fonctionnalités toutes plus attirantes les unes que les autres. En vrac :

  • L'enregistrement peut être stoppé/redémarré à tout moment pendant l'exécution de l'application
  • Prévu pour pouvoir supporter des enregistrements sur plusieurs heures, jours...
  • Mise en place d'un serveur d'enregistrement capable de gérer et d'être géré par plusieurs clients
  • Création de logs post-execution (l'explication du fondateur vaut toutes les autres) :
    Since Chronon records the entire execution of your program and we have this very powerful 'query' engine on top of it, we know the exact state of your program at all points in time. We even know exactly which statements were executed and in which order they were executed.

    If you combine those two facts, the order of execution of each statement and the knowledge of program state at all times, you will see that what we are essentially doing can be generalized as a form of a database query. A very complex and custom query but a query nonetheless.
  • ...

Pour conclure, on peut sans hésiter affirmer que Chronon est l'outil à connaître pour enfin avoir la main-mise sur une application et son comportement dans le temps (Marty et Doc' vont pouvoir remiser leur Doloréane!)

Sources


Fichier(s) joint(s) :



Un nouveau magazine pour les hommes...

... et les femmes qui désirent être toujours plus à l'affût des dernières nouveautés de la communauté Java : Oracle Java Magazine.

Tout fraîchement sortit des éditions Oracle, ce bi-mensuel a été créé "par et pour la communauté". Le premier numéro est déjà très prometteur : belle présentation, contenu clair, précis et pertinent. On notera la participation des grands noms des différents projets et de quelques Java Champions.

Un must-have donc à ne pas rater pour être toujours plus aware! Bonne lecture!!


P.S.: Oui un petit titre racoleur de temps en temps ça ne fait pas de mal...


Fichier(s) joint(s) :



Du nouveau pour le projet Code Recommenders

Un petit peu de promotion pour le projet sur lequel je travaille quelque peu aux cotés de Marcel Bruch. Sur le blog officiel sont présentées les nouveautés : refactoring de l'ordonnancement des méthodes et auto-completion "intelligente" (fonctionnalité à laquelle j'ai contribué).

N'hésitez pas à commenter, apporter des idées, dénigrer, louer... Enfin donner votre avis!


Fichier(s) joint(s) :

Intégrer Mantis à Eclipse grâce à Mylyn

Eclipse devient un environnement de développement -de plus en plus- intégré. Il est clair que depuis longtemps maintenant, le but de la fondation est de créer un outil capable d'être utilisé dans toutes les étapes de l'ALM (Application Lifecycle Management). Exemples :

  • Modeling : UML avec Papyrus
  • Configuration Management : SVN avec Subclipse
  • Build management : Maven avec m2eclipse
  • Software Testing : Test unitaires avec le support de JUnit et fonctionnels avec Jubula/SWTBot
  • Release Management : Maven avec m2eclipse
  • Workflow : implémentations avec MWE

Mais cette liste peut encore être complétée par la prise en compte de "Change Management" et "Issue Management" grâce à Mylyn. En effet, ce projet a pour but d'ajouter à Eclipse la capacité de gérer de manière plus approfondie les changements réalisés sur un projet, en s'interfaçant avec les outils les plus courants dans ce domaine : Jira, Mantis... Il est intégré de base dans la majorité des distributions.

Cet article a donc pour but de présenter Mylyn et tente de montrer comment être plus efficace avec sa nouvelle manière de gérer les tickets Mantis.

Tout d'abord, à quoi sert Mylyn? Sa vocation principale est d'apporter une nouvelle façon de traiter les modifications : au sein de l'IDE, un ticket est représenté sous la forme d'une tâche, planifiable dans le temps (l'avancement du travail effectué est donc matérialisé) mais aussi contextualisable (il est possible de lier directement des modifications de code à un ticket). L'IDE est en lien direct et permanent avec le serveur Mantis afin de récupérer toutes les anomalies saisies et de les mettre à jour sans avoir à ouvrir un navigateur. La présentation complète des fonctionnalités est disponible sur cette page.

Voyons donc, avec un exemple, comment traiter un ticket uniquement depuis Eclipse.

Tout commence par la configuration de l'accès au serveur. Dans la vue "Task Repositories", il faut créer un nouveau serveur :

Mantis fournit par défaut un webservice SOAP utilisable pour interagir avec ses données, généralement à l'adresse "http:///api/soap/mantisconnect.php". Il faut donc indiquer à Mylyn l'adresse de ce webservice pour se connecter :

Le serveur apparaît alors dans la vue. Pendant la configuration, Mylyn a récupéré la liste des projets et des utilisateurs créés dans Mantis. Il est donc possible de récupérer les tickets en indiquant la requête à utiliser, autrement dit le filtre enregistré sur le serveur. Par exemple, j'ai créé un filtre dans Mantis pour ne voir que les tickets qui me sont affectés, appelé "All Mine" :

Dans Eclipse, il faut indiquer la requête à utiliser pour télécharger les tickets, avec la fonction "New Query" :

Après avoir sélectionné le projet, il est possible de choisir le filtre "All Mine" :

Pour finaliser la récupération des tâches, il faut se rendre dans la vue "Task List" et synchroniser avec le serveur :

On peut donc voir les tickets rapatriés sous forme de tâches, comprenant l'ensemble des informations saisie sous Mantis. A partir de ce moment, la tâche est consultable hors-ligne et modifiable à volonté. Il est par la suite possible de mettre à jour Mantis avec le bouton "Submit" dans l'éditeur :

Voyons donc comment traiter le bug illustré ici et contextualiser dans Mantis la tâche avec le code mis en jeu au moment du commit. Tout d'abord, il possible de planifier la réalisation dans le temps afin de connaitre le travail effectué dans la journée. Pour ceci, il est possible de définir la date attendue et le temps nécessaire estimé pour la réalisation :

La progression sera ainsi visible en survolant la catégorie regroupant les tâches :

Afin de lier le contexte à la tâche, il faut d'abord l'activer, autrement dit indiquer qu'elle est la tâche courante (point gris à gauche de la tâche sur l'image plus haut). Dès lors, de nouvelles actions sont disponibles comme l'accès direct au code depuis la description de la tâche, grâce à la trace de l'exception :

Ensuite, une fois la correction effectuée, c'est lors de la synchronisation avec le serveur SVN (avant le commit) que l'on indique le contexte (série de modifications) de la tâche courante :

Ainsi, dans l'éditeur de la tâche, sous l'onglet "Context", apparaissent désormais les classes mises en jeu :

Revenons maintenant au commit. Mylyn ajoute par défaut dans le commentaire la référence au Mantis de la tâche. Il est vivement conseillé de conserver au moins ces informations dans le commentaire, nous verrons par la suite pourquoi :

Il faut maintenant mettre à jour le ticket Mantis pour indiquer sa résolution. Pour cela, il suffit de modifier les champs de la section "Action" dans l'éditeur de la tâche et soumettre au serveur :

La mise à jour terminée, la progression du travail est actualisée et le bug est maintenant clos dans Mantis :

Un fichier ZIP a été lié, contenant les informations nécessaires à Mylyn pour récupérer le contexte de la tâche dans l'IDE si nécessaire. A propos de contexte, qu'est devenu le commentaire si important lors du commit? Pour le savoir, il faut éditer la classe qui a été modifiée puis afficher les annotations serveur grâce à "Team"->"Show annotation" :

Affichées en mod "diff", il est possible de voir le commentaire spécifié lors du commit qui a modifié la ligne en question, et donc de retrouver directement le lien vers le ticket Mantis traité :

Comme on peut le voir, le traitement des bugs devient donc bien plus pratique et pragmatique avec Mylyn : le déroulement des corrections se fait entièrement depuis l'IDE, sans avoir à manipuler un navigateur externe (il est même possible de pousser le vice l'outil jusqu'à ouvrir les tickets Mantis correspondants aux tâches dans le navigateur intégré d'Eclipse...).

Il existe encore bien d'autres possibilités d'utilisation, mais j'espère que cette présentation aura éveillé les curiosités!

Sources :


Fichier(s) joint(s) :



Précisions sur le support de Java 7 sous Eclipse

Voici quelques nouvelles pistes pour les plus pressés de découvrir et tester les nouveautés du langage :

La dernière version de l'IDE, Indigo (3.7), ne gérant pas encore le compileur de Java 7, il faudra attendre le mois de septembre et les builds d'intégration de la version 3.7.1 pour trouver les premiers supports, officiellement intégrés dans la version suivante 3.8M1 (nommée Juno, téléchargeable ici).

Il est d'ores et déjà possible d'avoir un aperçu de l'intégration du nouveau compileur, grâce à quelques images dévoilées pour la version 3.8 de JDT et à cette vidéo de démonstration "live".

On your marks... ready... compile!


Fichier(s) joint(s) :



Configuration en temps réel grâce à Java NIO

Oracle ayant récemment publié la version 7 du JRE, j'ai décidé de commencer à tester quelque peu les nouveautés disponibles.

Dans cet article, je vais présenter les possibilités offertes par la nouvelle gestion du système de fichiers, avec java.nio.file.WatchService, qui permet de mettre en place un outil de "surveillance" des ressources.

Imaginons donc une fonctionnalité de mise à jour en temps réel de la configuration d'une application lors de la modification d'un fichier properties.

Voici le code mis en place :

package com.developpef;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Properties;

public class PropertiesSpreader {

	private static final String basePath = "C:\\java\\ides\\eclipse-jee-helios-SR2\\workspace\\Test\\resources";
	private static Path propDir;

	public static void main(String[] args) {
		try {
			loadProperties();

			FileSystem fs = FileSystems.getDefault();
			final WatchService watcher = fs.newWatchService();
			propDir = fs.getPath(basePath);
			propDir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);

			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					watch(watcher);
				}
			});
			t.start();

			System.out.println("Le watcher a été démarré...");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static void watch(WatchService watcher) {
		WatchKey key = null;
		do {
			try {
				key = watcher.take();
				List<WatchEvent<?>> events = key.pollEvents();
				for (WatchEvent<?> event : events) {
					if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
						Path propFile = (Path) event.context();
						if (propFile.endsWith("test.properties")) {

							BasicFileAttributes bfa = Files.readAttributes(
									propDir.resolve(propFile),
									BasicFileAttributes.class);
							System.out
									.println("\n** Fichier propriétés modifié! Rechargement... (modifications du "
											+ bfa.lastModifiedTime() + ")**");
							loadProperties();
						}
					}
				}
			} catch (InterruptedException|IOException e) {
				e.printStackTrace();
			}
		} while (key.reset());
	}

	private static void loadProperties() throws IOException {
		InputStream stream = new FileInputStream(new File(basePath
				+ "\\test.properties"));
		Properties props = new Properties();
		props.load(stream);
		stream.close();
		System.out.println("Titre de l'application : "
				+ props.getProperty("App.title"));
	}

}

L'intérêt de cette implémentation est de gérer la configuration dans un thread autonome (comme un daemon) qui actualisera les propriétés de l'application.

La gestion du système de fichiers a été "allégée", notamment grâce à un nouveau "super objet" java.nio.file.FileSystem qui facilite la manipulation des ressources de manière logique. Ainsi, par exemple, plus besoin d'instancier des java.io.File à tour de bras : la méthode java.nio.file.FileSystem.getPath("Path") permet de récupérer n'importe quel type de chemin, qu'il s'agisse d'un fichier ou d'un dossier, sous la forme d'un java.nio.file.Path.

Ensuite, l'essentiel de la fonction de mise à jour réside dans la méthode watch qui relance le chargement du fichier de configuration dès que celui-ci est modifié. Exemple de trace lors de l'utilisation :

Titre de l'application : Java 7 Watch Service
Le watcher a été démarré...

** Fichier propriétés modifié! Rechargement... (modifications du 2011-08-10T09:22:37.183499Z)**
Titre de l'application : Java 7 Watch Service rocks!!

Pour les plus observateurs, vous aurez également pu remarquer une nouvelle syntaxe de catch multiple : catch (InterruptedException|IOException e) apportée par le projet Coin dont j'ai parlé dans cet article.

Note :

Ce code fonctionne correctement lors de la modification du fichier avec Notepad (de Windows). Mais dès que l'on essaie de modifier le fichier avec un autre éditeur (Notepad++ ou Eclipse), le WatchService réagit à deux évènements de modification. Je n'ai pas encore d'explication quant à ce comportement (comme expliqué ici), mais si quelqu'un a déjà pu approfondir les tests et trouvé une solution, tout commentaire est le bienvenu!


Fichier(s) joint(s) :