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


Camel Cheatsheet

Dans cet article j'ai décidé de regrouper un petit ensemble de trucs et astuces à connaître lors du développement d'applications Camel.

Et pour une fois, je vais rédiger ce contenu en anglais afin de le rendre accessible au plus grand nombre.

Edit : This article is viewable at (thanks to them) :

Java Code Geeks

Polling an empty directory (and send an empty message with null body) :
from("file://temp?sendEmptyMessageWhenIdle=true")
Stop a route :
.process(new Processor() {
 public void process(Exchange exchange) throws Exception {
  getContext().stopRoute("ROUTE_ID");
 }
})
Access a property of the object in the body :
admitting the object has a method named "getMydata()" :
new ValueBuilder(simple("${body.mydata}")).isEqualTo(...)
Define an aggregator :
.aggregate(simple("${header.id}.substring(0,15)"), 
       genericAggregationStrategy)
.completionPredicate(header(Exchange.BATCH_COMPLETE) .isEqualTo(Boolean.TRUE))
  • "${header.id}.substring(0,15)" : flag to differenciate messages (here, the returned string is common to all messages, we aggregate them all)
  • Exchange.BATCH_COMPLETE : predicate indicating the end of polling (all files parsed for example)
  • genericAggregationStrategy : above, an example of an aggregator grouping all messages' contents in a list :
public class GenericAggregationStrategy implements AggregationStrategy {
  @SuppressWarnings("unchecked")
  public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
    if (oldExchange == null) {
      ArrayList<Object> list = new ArrayList<Object>();
      list.add(newExchange.getIn().getBody());
      newExchange.getIn().setBody(list);
      return newExchange;
    } else {
      Object oldIn = oldExchange.getIn().getBody();
      ArrayList<Object> list = null;
      if(oldIn instanceof ArrayList) {
        list = (ArrayList<Object>) oldIn;
      } else {
        list = new ArrayList<Object>();
        list.add(oldIn);
      }
      list.add(newExchange.getIn().getBody());
      newExchange.getIn().setBody(list);
      return newExchange;
    }
  }
}
Manually trigger an aggregation's completion (whatever it is) :

Send a message with the header Exchange.AGGREGATION_COMPLETE_ALL_GROUPS = true
It is possible to do from("bean:..."), knowing that the bean will be polled permanently (like with "file") and re-instanciated each time.
Modify the message's body on a route, using :
.transform(myExpression)
with myExpression :
public class MyExpression implements Expression {
  public <T> T evaluate(Exchange exchange, Class<T> type) {
    MyBean newData = ...;
    return exchange.getContext().getTypeConverter()
      .convertTo(type, newData);
  }
}
Using JaxB :
  • on a route :
    .[un]marshal().jaxb("my.business_classes.package")
  • with a configurable DataFormat :
    .[un]marshal(jaxbDataFormat)
    
    with :
// indicate to Jaxb to not write XML prolog :
JaxbDataFormat jaxbDataFormat = 
   new JaxbDataFormat("my.business_classes.package");
jaxb.setFragment(true);
General concepts for threads management :
  • a from(...) = a thread
  • except for from("direct:...") wich creates a "named route" with a unique identifier only callable by another route (in the same thread than the caller).
  • The component .resequence().batch() creates a new thread to rethrow the messages.

Define a shutdown strategy :

getContext().setShutdownStrategy(new MyShutdownStrategy(getContext()));
With :
public class MyShutdownStrategy extends DefaultShutdownStrategy {
   protected CamelContext camelContext;
   private long timeout = 1;
   private TimeUnit timeUnit = TimeUnit.SECONDS;
   public SpiralShutdownStrategy(CamelContext camelContext) {
      this.camelContext = camelContext;
   }

   @Override
   public long getTimeout() {
      return this.timeout;
   }

   @Override
   public TimeUnit getTimeUnit() {
      return this.timeUnit;
   }

   @Override
   public CamelContext getCamelContext() {
      return this.camelContext;
   }

   /**
   * To ensure shutdown
   *
   */
   @Override
   public void suspend(CamelContext context, 
        List<RouteStartupOrder> routes) throws Exception {
      doShutdown(context, routes, getTimeout(), 
          getTimeUnit(), false, false, false);
   }

   /**
   * To ensure shutdown
   *
   */
   @Override
   public void shutdown(CamelContext context, 
        List<RouteStartupOrder> routes, long timeout, 
        TimeUnit timeUnit) throws Exception {
      doShutdown(context, routes, this.timeout, 
           this.timeUnit, false, false, false);
   }

   /**
   * To ensure shutdown
   *
   */
   @Override
   public boolean shutdown(CamelContext context, RouteStartupOrder route, 
      long timeout, TimeUnit timeUnit, boolean abortAfterTimeout)
         throws Exception {
      super.shutdown(context, route, this.timeout, 
          this.timeUnit, false);
      return true;
   }
}

Stop a batch :

.process(new Processor() {
   public void process(Exchange exchange) throws Exception {
      context.stop();
   }
});
Calling a method of a bean from a route :
  1. method's return is always affected to message's body. For example :
    • public void myMethod(Exchange e) :
      Will not modify the body
    • public boolean myMethod(Exchange e) :
      the boolean (or whatever primitive type) will be set in the body
    • public Object myMethod(Exchange e) :
      the Object will be placed in the body (even if null)
    • public Message myMethod(Exchange e) :
      the Message will be placed in the body (better avoid this)
    • public List<Object> myMethod(Exchange e) :
      the list will be set in the body : useful to use with .split(), each object will be sent in a new message
    • public List<Message> myMethod(Exchange e) :
      the list will be set in the body : a .split() will create a new message for each element (better avoid, see upper)
  2. configurable method's parameters :
    • public void myMethod(Exchange e) :
      the complete Exchange will be passed
    • public void myMethod(Object o) :
      Camel will try to convert the body in the required parameter's class
    • public void myMethod(@Body File o, @Header("myHeader") String myParamHeader) :
      Camel will inject each parameter as specified
Exceptions management on routes :
  • in a global way (to be declared before all routes) :
    onException(MyException.class, RuntimeCamelException.class).to(...)...
  • to truly handle Exception and not bubble it in routes (and logs) :
    onException(...).handled(true).to(...)...
  • to continue process in a route after an Exception :
    onException(...).continued(true).to(...)...
  • An exception is "handled" or "continued"
  • local way (in a route) :
    from(...)
    .onException(...).to("manage_error").log("FAIL !!").end()
    .to("continue_route")... 
    
For writing file, only the header Exchange.FILE_NAME is necessary.
Reorder messages with component .resequence :
  • uses an expression to compute the new order of messages, from a unique Comparable "key" (number, String or custom Comparator)
  • two ways :
    • .batch() : batch mode. Waits the reception of ALL the messages befor reorder them. ATTENTION : a new thread is created to rethrow messages.
    • .stream() : streaming mode. Uses a gap detection between the messages' keys to re-send them. It is possible to configure a maximal capacity and a timeout.
Split the body with a token :
.split(body().tokenize("TOKEN"))
Knowing that the TOKEN will be deleted from content. For example, if receiving a message containing : "data1TOKENdata2TOKENdata3", messages created will be : "data1", "data2, "data3".

So avoid this when treating XML data, prefer "tokenizeXML()".

Dynamic access to body's data :
Sending mails :
from("direct:mail")
 .setHeader("To", constant(mailTo))
 .setHeader("From", constant(mailFrom)) 
 .setHeader("Subject", constant(mailSubject)) 
 .to("smtp://${user}@${server}:${port}?password=${password}");
With attachment :
.beanRef(MAIL_ATTACHER, "attachLog");
//with
public class MailAttacher {
   public void attachLog(Exchange exc) throws Exception {
      File toAttach = ...;   
      exc.getIn().addAttachment(toAttach.getName(), 
               new DataHandler(new FileDataSource(toAttach)));
      // if needed
      exc.setProperty(Exchange.CHARSET_NAME, "UTF-8");
   }
}

Useful Exchange's properties :

  • Exchange.AGGREGATED_* : aggregations management
  • Exchange.BATCH_* : treated messages management
  • Exchange.FILE_* : File messages management
  • Exchange.HTTP_* : web requests management
  • Exchange.LOOP_* : loops management
  • Exchange.REDELIVERY_* : exceptions management
  • Exchange.SPLIT_* : splitted contents management
  • Exchange.EXCEPTION_CAUGHT : thrown exception on route
Loop a route :
from("direct:...")
.loop(countExpression)
.to("direct:insideLoop")
.end()

Where "countExpression" is an Expression used to dynamically compute the loop count (evaluated entering the loop)

It is preferable to move the loop's code in a new route if the process is complex.

Headers management :

Message's headers are defined at its creation. When using a ".split()", all subsequent messages will have the same headers from the original message (so be careful when managing files). In an aggregation, custom headers will have to be managed manually to be preserved in the rest of the route.

Intercept a message and execute a route parallely (to be declared before routes) :
interceptSendToEndpoint("ENDPOINT_TO_INTERSEPT").to(...)...
Send an exchange from a Java class to a route :
  1. Create a ProducerTemplate from Camel context :
    ProducerTemplate template = camelRouteBuilder
           .getContext().createProducerTemplate();
    
  2. Define its target endpoint :
    template.setDefaultEndpointUri("direct:start");
    
  3. Create a message and Exchange from template :
    Message message = new DefaultMessage();
    message.setBody("...");
    message.setHeader(key,value);
    
    Exchange exchange = template.getDefaultEndpoint().createExchange();
    exchange.setIn(message);
    
  4. Send message :
    template.send(exchange);
    
Definie a maximum inflight messages with a route policy :
ThrottlingInflightRoutePolicy policy = new ThrottlingInflightRoutePolicy();
policy.setMaxInflightExchanges(999);
policy.setScope(ThrottlingInflightRoutePolicy.ThrottlingScope.Route);

from("seda:route").routePolicy(policy)
Best pratices to combine Split and aggregator :

It is possible to use :
.split(body())
(...)
.aggregate(simple("${header.id}.substring(0,15)"), simpleAggregationStrategy)
.completionPredicate(header(Exchange.SPLIT_COMPLETE).isEqualTo(Boolean.TRUE))
(... end of route ...)
But with an empty list (null body or list size to 0), split behavior must be confusing and might shortcut the end of the route. Safer use is :
.split(body(), simpleAggregationStrategy)
(...)
.end()
The end() instruction clearly isolates the split instructions block and simpleAggregationStrategy is still triggered at SPLIT_COMPLETE

To be continued, don't hesitate to participate!


 
P.S. : un grand merci à l'équipe de "Camel masters", sans qui rien de tout cela n'aurait été possible! Daminou, Gaël et Gros ;-) kiss, love.

Fichier(s) joint(s) :



Piloter Camel avec Quartz et JMX

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

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

* * 5 ? * SAT-SUN

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

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

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

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

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

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

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

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

}
public class MyCamelMBean implements MyAgentManagerMXBean {

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

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

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

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

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

Voilà tout!


Fichier(s) joint(s) :



Taming the Camel

Le but de cet article est de pousser un peu plus la compréhension des mécanismes cachés de Camel, "under the hood"! D'où ce titre "racoleur" : Dompter Camel, en référence au grand nombre d'articles de présentation de Camel que l'on peut trouver sur le net intitulés "Riding the Camel"... :)

La documentation du site officielle, quoique plutôt claire sur les fonctionnalités offertes par l'outil, occulte plus généralement l'aspect technique de leur utilisation. Je veux plus particulièrement parler de ce qui se passe dans la JVM selon les routes mises en place.

Après avoir développé une première application sur les seules bases de la documentation et du livre "Camel in action", de nombreux problèmes sont survenus liés à l'existence de multiples threads dans la JVM, issus des différentes routes créées. En effet, même si lors de la première conception il était apparu assez évident de créer ces différentes routes (effectuant des opérations bien particulières), il est devenu rapidement compliqué de les orchestrer pour réaliser le flux de traitements métier souhaité.

En cherchant un peu plus précisément dans la documentation autour de cette problématique, on tombe rapidement sur la description de plusieurs mécanismes permettant de mieux gérer l'enchainement des routes :

  • le plus simple est l'ajout sur une route de l'instruction .noAutoStartup() pour indiquer qu'elle ne doit pas se lancer au démarrage de l'application. Elle pourra être démarrée par la suite avec : getContext().startRoute("myRoute")
  • l'utilisation de l'instruction ".onCompletion()" qui permet d'écrire la suite de la route à exécuter uniquement à la fin de la route précédente. Il est alors possible de conditionner le déroulement avec par exemple ".onFailureOnly()" pour prendre en compte les cas d'erreurs ou encore le test d'un prédicat ".onWhen()" pour conditionner la suite des opérations. Pour plus de détails voir : http://camel.apache.org/oncompletion.html
  • l'injection au sein des routes d'un système de synchronisation, basé sur le concept de "UnitOfWork" : son principe est de regrouper un ensemble de tâches à exécuter lors d'un échange. Ceci offre la possibilité d'ajouter (ou de supprimer) l'appel à une ou plusieurs actions exécutées séquentiellement à la fin de l'échange. Voici donc le modèle objet entrant en jeu :
    L'objet "Synchronization" possède deux méthodes utilisées comme callback par "UnitOfWork" dans la méthode "done()" (invoquée à la fin de l'Exchange). L'objet "UnitOfWork" est propre à chaque échange (injecté par un processeur particulier).
    Exemple d'utilisation :
    from("direct:download")
    .process(new Processor() {
     @Override
     public void process(Exchange e) throws Exception {
      e.getUnitOfWork().addSynchronization(new MySynchro());
     }
    }).to("file:...");
    
    ...
    
    public class MySynchro implements Synchronization {
     public void onComplete(Exchange e) {
      ...
     }
     
     public void onFailure(Exchange e) {
      ...
     }
    }
    

La différence fondamentale entre "onCompletion" et Synchronization est le modèle de thread utilisé. Synchronization implique l'usage du même thread pour réaliser les tâches spécifiées, qui sera donc bloqué jusqu'à leur fin. Au contraire, "onCompletion" transfert l'échange (une copie en réalité) à un thread indépendant (celui de la route suivante).

Mais ces différentes options, même si elles apportent la possibilité de garder la main sur le séquencement des opérations, ne résolvent toujours pas le problème de l'apparition de multiples threads dans la JVM... Pour palier à ceci, et c'est à partir de ce moment que l'on se rend compte des lacunes de la documentation, il existe une syntaxe précise qui permettra d'enchainer les traitement au sein d'un même thread :

from("file://C:/test").to("direct:sameThread");

from("direct:sameThread").to("...");

L'utilisation de "direct:" spécifie une exécution continue des opérations, tandis que "seda:" permet de réaliser des traitements asynchrones. Mais le point commun - essentiel ! - à cet appel par système d'ID sur les routes est, vous l'aurez compris, l'utilisation d'un seul thread pour la totalité des instructions.

Voici un tableau comparatif de ce qui se passe dans la JMV (via la JConsole) dans tous ces cas de figures :

from("file://C:/test").to("...");
from("file://C:/test/unzipped").to("...");
from("direct:sameThread").to("...");
Deux threads créés à partir des routes utilisant "file://"
from("file://C:/test").to("...").process(new Processor() {
 @Override
 public void process(Exchange exchange) throws Exception {
   getContext().startRoute("idleRoute");
  }
 });
from("file://C:/test/unzipped").routeId("idleRoute").noAutoStartup().to("...");
from("direct:sameThread").to("...");
Un seul thread au démarrage de l'application Un deuxième thread démarré dès l'appel à la deuxième route
from("file://C:/test").to("direct:sameThread");
from("direct:sameThread").to("...");
Présence du seul thread de l'application

Pour conclure, vous pouvez voir qu'il est très important de connaître le fonctionnement interne de Camel afin d'optimiser la composition de l'application. Ceci permet d'une part de limiter les problèmes de concurrence lors du déroulement des routes et d'autre part de créer un système plus "léger" qui facilitera notamment l'extinction du contexte Camel de par le nombre limité de threads.

Sur ce, en selle!


Fichier(s) joint(s) :



Apache Camel et performance

Après avoir bien pris en main Camel, je dois dire que je suis tout à fait satisfait des possibilités offertes (voir mon précédent article). Cependant, j'ai remarqué quelques comportements peu adaptés au traitement des volumétries importantes de données.

En effet, il semblerait que certains composants et/ou outils proposés par défaut causent des pics de mémoire voir même systématiquement des crash OutOfMemory. Je vais donc essayer ici de les décrire afin de permettre aux futurs développeurs de les prendre en compte dès le départ. Je précise tout de même que j'utilise actuellement la version 2.8.3.

Marshalers et OutOfMemory

Voici un exemple de code simpliste situant le problème :

from("file://...").unmarshal().zip().to("file://...");

Ici, le but est de dezipper un fichier pour écrire son contenu vers la destination spécifiée. Utilisez un fichier de 500Mo par exemple et l'application devrait planter assez rapidement!

En effet, la méthode unmarshall().zip() va charger en mémoire (!) un DeflaterOutputStream correspondant au contenu total du fichier AVANT de l'écrire. Qui plus est, un fichier plus modeste (quelques Mo), ne causant pas de crash, restera chargé en mémoire jusqu'à l'arrivé du garbage collector.

L'intérêt de conserver en mémoire ces informations peut être compréhensible dans le cas du traitement de petits fichiers, mais dès lors que l'on pousse l'implémentation vers des volumétries importantes, il vaut mieux privilégier une autre stratégie (Camel ne prévoyant à priori pas la possibilité d'écrire le flux de données au fur et à mesure de se décompression). Un code plus robuste ressemblerait à :

from("file://...").convertBodyTo(File.class).to("bean://com.MyDezipperBean");

Qui se contente de rediriger le fichier vers un bean personnalisé qui permettra de mieux gérer les flux de données, notamment grâce au classique java.util.zip.*

Notez qu'il en va de même pour le CSV. L'appel à la méthode unmarshal().csv() produira le même comportement. Préférez donc l'utilisation de org.apache.commons.csv.CSVParser par l'intermédiaire d'un bean, beaucoup plus à-même de gérer les fichiers CSV volumineux.

Split et montées en charge

Imaginons la nécessité de découper un gros fichier CSV entrant en plusieurs plus petits (quelques soient les conditions et méthodes employées). Une implémentation évidente ressemblerait à :

from("file://...").convertBodyTo(File.class)
  .split().method("com.services.CsvService","splitDatas")
  .to("file://...");

Où com.services.CsvService est un bean recevant le fichier CSV initial, le parcourant pour découper le contenu selon X règles métier puis retournant une liste de contenus CSV destinés à être écrits dans des fichiers séparés.

En observant de près le comportement de l'application, on aura vite fait de se rendre compte de la rapide montée en charge due essentiellement au stockage en mémoire des informations CSV (lors de leur transfert sous forme de message du bean vers l'endpoint fichier). Une fois de plus donc, le cas du traitement de fichiers nombreux et/ou conséquents peut vite devenir problématique. Une solution de contournement peut consister à faire écrire directement au bean les fichiers (pour ne pas avoir à les re-transporter sur Camel) puis éventuellement de faire transiter simplement leur chemin si besoin.

Voici donc les problématiques que j'ai rencontrées à ce jour. Après avoir été plusieurs fois agréablement surpris par l'intelligence des composants et des diverses implémentations de EIP proposées par Camel, je dois avouer que j'ai quelque peu été dérouté par ces comportements.

Hope this helps!


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