Comprendre le fonctionnement du ClassLoader

Une fois n'est pas coutume, cet article a surtout une vocation d'aide-mémoire, mais j'espère qu'il permettra à certains de comprendre ces détails de la JVM.

Lors de l'utilisation d'un JAR exécutable, il est important de connaître l'influence de la tactique de lancement de l'application sur son comportement.

En effet, il existe deux lignes de commande pour exécuter un JAR :

java -jar thefile.jar

ou

java -cp ../conf/;package2;thefile.jar com.my.MainClass

Voici un exemple de leur influence.

Prenons une application structurée ainsi :

rootFolder/
|- conf/
  |- test.properties
|- script/
  |- thefile.jar! 
    |- test.properties

Le JAR contient, à sa racine, le fichier test.properties ainsi qu'un MANIFEST ajoutant au classpath le répertoire "../conf/". Le fichier test.properties situé dans ce dernier diffère de quelques ligne par rapport à celui du JAR.

Le code utilisé pour charger les propriétés est le suivant :

InputStream propStream = getClass().getClassLoader().getResourceAsStream("test.properties");

En lançant l'application avec la première ligne de commande, les propriétés chargées sont celles situées dans le JAR. En revanche, avec la seconde ligne de commande, ce sont celles situées dans le répertoire "conf"...

Pourquoi donc? Il faut se référer à la documentation du ClassLoader pour comprendre l'ordre de chargement des ressources (extrait) :

How the Java Launcher Finds User Classes
(...)
The user class path is specified as a string, with a colon (:) separating the class path entries on Solaris, and a semi-colon (;) separating entries on Microsoft Windows systems. The java launcher puts the user class path string in the java.class.path system property. The possible sources of this value are:
  • The default value, ".", meaning that user class files are all the class files in the current directory (or under it, if in a package).
  • The value of the CLASSPATH environment variable, which overrides the default value.
  • The value of the -cp or -classpath command line option, which overrides both the default value and the CLASSPATH value.
  • The JAR archive specified by the -jar option, which overrides all other values. If this option is used, all user classes must come from the specified archive.

Voici donc ce que l'on apprend :

  • la commande "-cp" surcharge l'utilisation de tout autre définition de classpath
  • la commande "-jar" surcharge tout autre commande de lancement. Ce n'est également que dans ce contexte qu'est utilisé le MANIFEST

Voilà donc pourquoi la seconde ligne de commande ne prend en compte que les propriétés situées dans le répertoire "conf". Information supplémentaire (lue entre les lignes) : dans le cas de l'utilisation d'un JAR avec la première ligne de commande, le ClassLoader suppose que toutes les ressources utiles sont situées dans le JAR... Ce n'est qu'après l'avoir entièrement chargé qu'il utilisera le MANIFEST pour récupérer les ressources manquantes. C'est la raison pour laquelle, dans ce scénario, seules les propriétés situées dans le jar sont utilisées!

Hope this helps...

Sources :


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

Retour d'expérience sur Nuxeo Studio

Nuxeo Studio est une plateforme en ligne fournie par Nuxeo qui a pour vocation d'aider à la personnalisation d'une instance de la GED.

Cet article est donc dédié à un retour d'expérience sur l'outil afin d'aider à son utilisation en prévenant de certains écueils et donner quelques pistes d'améliorations pour toute personne concernée qui me lira ;-)

Création de types de documents personnalisés

Il est relativement aisé de mette en place des nouveaux documents, portant des attributs spécifiques. Il suffit pour cela de déclarer le nouveau document comme étendant par exemple "File", décrire son nouveau schéma (nouveaux attributs) puis redéfinir les vues d'affichage et de création pour accéder à ces nouvelles valeurs :

Pour afficher les nouveaux attributs dans la vue "liste" d'un dossier, il faut créer une nouvelle vue de contenu (ContentView) qui sera appliqué à un nouveau type de Dossier :

La valorisation de cet attribut se fera lors de la création du fichier, avec par exemple le code suivant :

PropertyMap pMap = new PropertyMap();
pMap.set("dc:title", dataFileName);
pMap.set("MyFile:extractionDate", extractionDate);
documentService.createDocument(deptFolder, "MyFile", dataFileName, pMap);

Le seul reproche à faire lors de la création de la ContentView, est l'impossibilité de surcharger une vue existante, par exemple la vue "liste" par défaut d'un dossier, pour y ajouter seulement les colonnes voulues. La mise en place d'une nouvelle vue de contenu impose la définition de toutes les colonnes qui la compose, quitte, comme dans cet exemple, à reproduire à l'identique la vue liste du dossier.

Création d'arborescences personnalisées

Après la création des documents, vient naturellement la question de la mise en place d'une arborescence type pour certains objets, par exemple un Domaine, qui peut être constitué d'une structure prédéfinie lors de son initialisation. Problème majeur ici, il est impossible de définir via Studio plusieurs niveaux de documents!!

La solution de contournement est donc de découper chaque niveau en un nouveau type de document avec des fils directs uniquement. Mais si en plus la structure d'un dossier doit être conditionnée selon là où il est créé (workspace, section...), il vaut mieux s'en remettre à l'API pour créer un client capable de gérer les arborescences "manuellement".

Création de groupes

Une fonctionnalité intéressante de Studio est la création de groupes d'utilisateurs, qui seront ensuite accessibles par défaut sur l'instance personnalisée.

Cependant, comme on le voit sur cette image, même s'il est possible dès cette étape de définir les utilisateurs qui appartiendront à ce groupe, il est impossible de définir des sous-groupes. Or cet aspect est pourtant très important pour représenter les différents niveaux organisationnels d'une entreprise. Une intervention manuelle sera donc indispensable pour hiérarchiser ces groupes, plutôt fastidieux lorsque l'on en dénombre plusieurs dizaines... Et de ce côté, inutile de compter sur l'API Java qui ne propose pas la gestion des groupes (pour des raisons de sécurité, ce qui peut se comprendre).

Enfin, pour aller de mal en pis, après avoir pris la peine de créer tous les groupes métier, on peut s'apercevoir que lors du déploiement de l'extension, qu'il devient impossible pour les utilisateurs de l'entreprise d'accéder à l'application : en effet, Nuxeo ne cherche plus à les authentifier depuis le LDAP de l'entreprise, mais depuis une source SQL locale... Ceci provient du fait que l'extension Studio proposant la création des groupes n'a qu'une vocation de test (!! selon les dire même des développeurs Nuxeo) et a écrasé la configuration initiale de l'instance. Ainsi donc, il n'est pas possible à l'heure actuelle de créer dans Studio une extension de groupes basée sur LDAP...

Définition des droits d'accès

Une des fonctionnalités basique importante d'une GED est l'organisation des accès aux différentes ressources selon les rôles des utilisateurs. Ici encore, un bug (signalé) dans Studio cause la non prise en compte des règles définies sur les éléments de bas niveau dans les arborescences :

Qui plus est, après avoir pris le soin de créer tous les groupes utiles comme vu au point précédent, il n'est pourtant pas possible d'y accéder directement dans l'interface d'attribution des droits d'accès comme on le voit sur l'image précédente. Seul un champ texte attend la saisie du nom du groupe à utiliser... Ne serait-il pas possible de créer une liste déroulante à partir des groupes précédemment définis?? Idem pour les droits : comment savoir quel droit utiliser? Certes, on connait habituellement "Read", "Write" mais le premier piège : "All", qui généralement correspond au regroupement des deux précédents, est en fait dans Nuxeo "Everything"... Et qu'en est-il des autres droits existant ("Manage workflow"...)? Une simple erreur de frappe et la configuration est bancale! Pourquoi ne pas avoir une liste déroulante basée sur l'énumération utilisée dans le code SecurityConstants (seul endroit où l'on trouve la liste de tous les droits d'accès!!!)

Pour conclure, on peut dire avec ce bref tour d'horizon que Nuxeo Studio est né d'une bonne (voire nécessaire) idée, c'est-à-dire l'élaboration d'une plateforme d'aide à la personnalisation d'une instance de la GED, mais il reste encore quelques progrès à faire pour faciliter l'utilisation aux néophytes comme moi qui n'ont aucune autre connaissance de l'aspect technique du système. L'espoir de pour une fois ne pas avoir à mettre les doigts dans le code s'évapore assez vite!


Fichier(s) joint(s) :



Maven, tour d'horizon du plugin Assembly

Parce que la création d'un JAR peut ne pas suffire lors de la livraison d'une application, le plugin maven-assembly permet de réaliser des paquets plus complexes.

Prenons l'exemple de la création d'une archive ZIP structurée ainsi :

- racine/
   - conf/
      - *.properties
   - lib/
      - *.jar
   - scripts/
      - application.jar
      - run.sh

Tout commence par l'ajout du plugin essentiel au POM du projet :

<plugin>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.3</version>
 <configuration>
   <descriptors>
  <descriptor>assemble/assembly.xml</descriptor>
   </descriptors>
 </configuration>
  </plugin>

Comme on le voit, cet extrait fait appel à assembly.xml qui est la description des opérations à effectuer. Voici un exemple de sa structure :

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
  <id>mon-zip-final</id>
  <formats>
    <format>zip</format>
  </formats>
  <fileSets>
      <!-- Récupération du JAR livrable -->
  <fileSet>
   <directory>target</directory>
   <outputDirectory>scripts</outputDirectory>
   <includes>
    <include>*.jar</include>
   </includes>
  </fileSet>
  <!-- Récupération des scripts shell de lancement -->
  <fileSet>
   <directory>deployment</directory>
   <outputDirectory>scripts</outputDirectory>
   <includes>
    <include>*.sh</include>
   </includes>
  </fileSet>
  <fileSet>
   <directory>src/main/resources/env/${env}</directory>
   <outputDirectory>conf</outputDirectory>
   <includes>
    <include>*.properties</include>
   </includes>
  </fileSet>
  </fileSets>
</assembly>

La première balise formats permet de définir la liste des formats attendus pour la distribution finale.

Chaque balise fileSet permet de créer des règles d'inclusion et/ou d'exclusion de fichiers à partir d'un répertoire :

  • On récupère le jar créé à partir du POM dans le répertoire "target" de maven
  • On regroupe les scripts shell de lancement issus du répertoire "deployment" du projet
  • On cherche les fichiers de propriétés à déplacer dans le répertoire "conf" en fonction de l'environnement cible, défini par la ligne de commande du build, par exemple : -Denv=production

Ces environnements sont configurés dans le POM par l'intermédiaire des balises "profile" :

<profiles>
 <!-- Profil de l'environnement de developpement -->
 <profile>
  <id>env.developpement</id>
  <activation>
   <property>
    <name>env</name>
    <value>developpement</value>
   </property>
   <activeByDefault>true</activeByDefault>
  </activation>
  <properties>
   <env>developpement</env>
  </properties>
  <build>
   ...

Reste maintenant à s'occuper du classpath de l'application et du jar exécutable.

Pour créer le répertoire "lib" contenant toutes les dépendances du projet, il faut ajouter à notre assembly les balises :

<!-- Pour mettre les dépendances du projet dans le répertoire lib -->
<dependencySets>
 <dependencySet>
  <outputDirectory>/lib</outputDirectory>
 </dependencySet>
</dependencySets>

La constitution du MANIFEST est dédiée au plugin JAR de maven, configuré ainsi :

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-jar-plugin</artifactId>
 <version>2.2</version>
 <configuration>
  <archive>
   <manifestEntries>
    <Class-Path>../conf/</Class-Path>
   </manifestEntries>
   <manifest>
    <addClasspath>true</addClasspath>
    <mainClass>com.developpef.CamelStartup</mainClass>
    <classpathPrefix>../lib/</classpathPrefix>
   </manifest>
  </archive>
 </configuration>
</plugin>

Où :

  • manifestEntries permet d'indiquer des éléments personnalisés à ajouter au classpath, ici notre répertoire contenant les fichiers de propriétés
  • mainClass est la classe principale du jar à exécuter
  • classpathPrefix spécifie le préfixe à ajouter à chaque entrée du classpath, le but étant dans notre exemple de pointer vers le répertoire parent "lib"

A ce niveau, maven est d'ores-et-déjà capable de produire une distribution proche de ce que nous voulions, structurée ainsi :

distribution-mon-zip-final.zip :
   - <artifact-id>-<version>/
      - conf/
         - *.properties
      - lib/
         - *.jar
      - scripts/
         - application-<version>.jar
         - run.sh

Il reste cependant quelques détails à régler.

Par défaut, le nom de l'archive finale est suffixé par l'identifiant de l'assembly. Pour éviter cela, il est possible d'ajouter la balise suivante dans le POM au niveau de la configuration du plugin "assembly" :

<appendAssemblyId>false</appendAssemblyId>

De la même façon, le répertoire racine de la distribution est suffixé par la version de l'artifact. Pour l'enlever, il faut ajouter cette balise dans l'assembly :

<baseDirectory>${artifactId}</baseDirectory>

Enfin, puisque le jar de l'application est exécuté par le script shell, pour ne pas avoir à réécrire ce dernier à chaque changement du version du jar, il est possible d'indiquer à maven le nom du jar à produire. Pour cela, ajoutons cette balise à la configuration du plugin "JAR" dans le POM :

<finalName>${artifactId}</finalName>

Nous y voilà! Il ne reste plus qu'à décompresser l'archive produite et lancer le script shell pour démarrer notre application :

distribution.zip :
   - <artifact-id>/
      - conf/
         - *.properties
      - lib/
         - *.jar
      - scripts/
         - application.jar
         - run.sh

Pour perfectionner notre système, il est possible de configurer l'appel à l'assemblage de manière implicite lors d'une phase du build maven. Par exemple, si nous voulons que notre distribution soit produite chaque fois que le jar est déployé, il suffit d'intégrer l'assemblage au moment de la phase "package" du build, comme ceci :

<plugin>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.2</version>
 <executions>
  <execution>
   <phase>package</phase>
   <goals>
    <goal>single</goal>
   </goals>
   <configuration>
    <descriptors>
     <descriptor>assemble/assembly.xml</descriptor>
    </descriptors>
    <appendAssemblyId>false</appendAssemblyId>
   </configuration>
  </execution>
 </executions>
</plugin>

Note :

Certaines versions de maven et/ou du plugin "assembly" peuvent provoquer l'erreur suivante lors du build :

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD ERROR
[INFO] ------------------------------------------------------------------------
[INFO] Failed to create assembly: Error adding file '(...)' to archive: /(...)/target/classes isn't a file.

Ceci est du à la présence de la balise dependencySets dans l'assembly ainsi qu'à un conflit lors de l'exécution des différentes phases du build (dans un projet multimodules par exemple).

La solution de contournement la plus rapide consiste à copier manuellement les dépendances du projet dans un répertoire temporaire et les ajouter ensuite dans la distribution.

Il faut donc configurer dans le POM le plugin "maven-dependency-plugin" pour appeler la copie :

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-dependency-plugin</artifactId>
 <executions>
  <execution>
   <id>copy-dependencies</id>
   <phase>package</phase>
   <goals>
    <goal>copy-dependencies</goal>
   </goals>
   <configuration>
    <outputDirectory>dependencies</outputDirectory>
    <overWriteReleases>false</overWriteReleases>
    <overWriteSnapshots>false</overWriteSnapshots>
    <overWriteIfNewer>true</overWriteIfNewer>
   </configuration>
  </execution>
 </executions>
</plugin>

Puis récupérer par l'assembly tous les jar produits dans notre zip final (ce qui remplace l'appel à dependencySets) :

<fileSet>
 <directory>dependencies</directory>
 <outputDirectory>lib</outputDirectory>
 <includes>
  <include>*.jar</include>
 </includes>
</fileSet>

Enfin, puisqu'il s'agit d'un répertoire temporaire, ne pas oublier de l'inclure dans la phase de "clean" du projet. Dans le POM :

<plugin>
 <artifactId>maven-clean-plugin</artifactId>
 <configuration>
  <filesets>
   <fileset>
    <directory>dependencies</directory>
   </fileset>
  </filesets>
 </configuration>
</plugin>

Bon déploiement!


Fichier(s) joint(s) :



Un client Java pour Nuxeo

Nuxeo est une application de Gestion Électronique de Documents (GED) qui permet, notamment, de manipuler son contenu via un client HTTP (REST) par l'usage du Nuxeo Automation Client.

La documentation étant très succincte à ce sujet et après de longues recherches pour finaliser un petit client capable de gérer les quelques opérations de base (upload d'un document, publication, création de relations), j'ai décidé de publier le résultat de mes trouvailles. Voici donc la classe du client en question :

import java.io.File;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.client.Session;
import org.nuxeo.ecm.automation.client.adapters.DocumentService;
import org.nuxeo.ecm.automation.client.jaxrs.impl.HttpAutomationClient;
import org.nuxeo.ecm.automation.client.model.Document;
import org.nuxeo.ecm.automation.client.model.FileBlob;
import org.nuxeo.ecm.automation.client.model.PropertyMap;

/**
 * Used to upload files to Nuxeo
 * 
 */
public class NuxeoAutomationClient {

 // logger
 private static Log LOGGER = LogFactory.getLog("camel");

 // Nuxeo's automation client
 private HttpAutomationClient client;

 // Connection's session
 private Session session;

 private DocumentService dc;

 public NuxeoAutomationClient(String login, String mdp) throws Exception {
  client = new HttpAutomationClient(SERVER_URL);
  session = client.getSession(login, mdp);
  dc = new DocumentService(session);
 }

 public Session getSession() {
  return session;
 }

 /**
  * Upload specified file into specified folder in Nuxeo
  * 
  * @param file the file
  * @param folderName the folder
  * @param publishFile publish File?
  * @throws NuxeoServiceException all
  */
 public void upload(File file, String folderName, boolean publishFile) throws Exception {
  // First get the root document and create a new File document at
  PropertyMap pMap = new PropertyMap();
  pMap.set("dc:title", folderName);
  String uploadFolder = CAMEL_NUXEO_WS_URL + "/" + folderName;
  Document deptFolder = null;
  LOGGER.info("Looking for folder : " + uploadFolder);
  deptFolder = dc.getDocument(uploadFolder);
  LOGGER.info("Uploading to folder : " + deptFolder.getPath());

  // upload file
  // check if file exists
  Document deptDoc = null;
  String dataFileName = file.getName();
  try {
   deptDoc = dc.getDocument(uploadFolder + "/" + dataFileName);
   // If exists, delete it
   if (deptDoc != null) {
    dc.remove(deptDoc);
   }
  } catch (Exception e) { // Document not found
   // foolish exception, do nothing
  }

  pMap = new PropertyMap();
  pMap.set("dc:title", dataFileName);
  pMap.set("file:filename", dataFileName);
  deptDoc = dc.createDocument(deptFolder, "File", dataFileName, pMap);
  LOGGER.info("Uploading file : " + deptDoc.getPath());

  // upload blob
  FileBlob fb = new FileBlob(file);
  fb.setFileName(dataFileName);
  String extension = dataFileName.substring(dataFileName.indexOf(".") + 1, dataFileName.length());
  String mime = "";
  if ("txt".equals(extension)) {
   mime = "text/plain";
  } else {
   mime = "application/" + extension;
  }
  fb.setMimeType(mime);
  dc.setBlob(deptDoc, fb);

  // publish file
  if (publishFile) {
   String pubPath = NUXEO_PUB + "/" + folderName;
   LOGGER.info("Publishing to : " + pubPath);
   deptFolder = dc.getDocument(pubPath);
   // if already published, remove it
   try {
    Document old = dc.getDocument(pubPath + "/" + dataFileName);
    if (old != null) {
     dc.remove(old);
    }
   } catch (Exception e) {
    // Document not found
    // foolish exception, do nothing
   }
   dc.publish(deptDoc, deptFolder, true);
  }

  // manage metadata relations
  String metadataFile = "META_FILE.doc";
  // Create relation
  Document metaDoc = dc.getDocument(CAMEL_NUXEO_WS_URL + "/Métadonnées/" + metadataFile);
  String predicate = "http://purl.org/dc/terms/ConformsTo";
  dc.createRelation(deptDoc, predicate, metaDoc);
  LOGGER.info("Created relation : " + deptDoc.getPath() + " " + predicate + " " + metaDoc.getPath());
 }
}

Son exécution est très simple : création d'un document (avec effacement de l'existant), envoi du blob correspondant (données), publication, liaison à un autre document.

Cela peut paraître simple, mais l'usage du DocumentService n'est pas très explicite dans les documentations.

Le principal écueil ici réside dans la création de la relation entre les documents. Ici le but est de créer les liens du type (basé sur la norme DCMI) :

  • Doc1 "Est conforme à" Doc2
  • Doc2 "A pour conformité" Doc1

où, conceptuellement, "Est conforme à" et "A pour conformité" sont des liens inverses issus du même prédicat de la relation.

Il faut savoir que Nuxeo se base sur un système de vocabulaire permettant, lors de la création de la relation, de gérer automatiquement les libellés des liens inverses issus des prédicats. Ainsi, dans cet exemple, ces liens sont calculés à partir du prédicat (normé selon DCMI) : http://purl.org/dc/terms/ConformsTo

J'espère vous avoir fait économiser quelques heures de recherche!

Sources :


Fichier(s) joint(s) :