Le StoreMerge est un outil fourni avec JCMS qui permet de fusionner deux fichiers store.xml.
Le StoreMerge est typiquement utilisé pour fusionner les données d’un environnement de développement avec les données d’un site en production.
Avec le script deploy.sh, le StoreMerge est le composant coeur du système de développement et de déploiement de JCMS.
La version du StoreMerge décrite dans cet article est celle fournie avec JCMS 8 SP1. Cette version peut aussi fusionner les stores JCMS 7.0 et JCMS 7.1.
1. Principes
Le StoreMerge prend deux fichiers store.xml, un ensemble de paramètres, des règles de fusion et produit un nouveau fichier store.xml contenant la fusion des deux premiers.
1.1 Fonctionnement général
Lorsque le StoreMerge est lancé, trois cas peuvent se présenter :
- Cas 1 : Aucun conflit n’a été détecté
- La fusion a lieu
- Cas 2 : L'application des règles de fusion résout tous les conflits détectés
- La fusion a lieu
- Les éventuelles nouvelles opérations consécutives à la résolution des conflits sont ajoutées au store fusionné
- Cas 3 : L'application de ces règles ne résout pas tous les conflits
- La fusion n'a pas lieu
- La liste des conflits est indiquée
- Un fichier de résolution spécifique est alimenté avec des règles par défaut pour chaque conflit détecté
- Une fois qu’une règle de résolution a été choisie pour tous les conflits, il faut relancer le StoreMerge avec ce fichier
1.2 Identification des divergences
La fusion ne porte que sur les parties divergentes des deux stores. Pour cela, le StoreMerge doit connaître la plus grande estampille commune aux deux stores (GCS : Greatest Common Stamp). Toutes les opérations antérieures ou égales à cette estampille constituent le prefixe commun des deux stores. Toutes les opérations postérieures à cette estampille constituent le suffixe divergent. La fusion des deux store consiste donc à recopier dans le nouveau store le préfixe commun et à intégrer la fusion des deux suffixes divergents.
Le StoreMerge détermine automatiquement la GCS. Cependant, pour réduire le temps de traitement, notamment pour les stores de taille importante (plusieurs dizaines de Mo), il est recommandé d’indiquer au StoreMerge cette estampille (via le paramètre -gcs).
Note : Dans le cas où les préfixes précédant le GCS seraient différents, c'est le préfixe du store2 qui est utilisé pour créer le store fusionné.
1.3 Traitement des conflits
Les suffixes divergents de deux stores peuvent comporter des opérations conflictuelles. Par exemple, la même donnée (catégorie, groupe, publication, …) a été modifiée sur les deux stores.
Le StoreMerge résout automatiquement les conflits de mise à jour sur une même donnée lorsqu’il s’agit d’attributs différents. Par exemple, sur le store 1 le nom d’une catégorie a été modifié et le parent de la même catégorie a été modifié sur le store 2. Le store fusionné comportera ces deux modifications.
Lorsque le conflit porte sur le même attribut, le mode de résolution doit être indiqué au StoreMerge dans des fichiers de règles. De même, les conflits entre la mise à jour d’une donnée et sa suppression doivent aussi être renseignés dans ces fichiers.
Le StoreMerge reçoit deux fichiers de règles :
- Un fichier de règles générales que l’on constituera au fur et à mesure des conflits rencontrés. Ce fichier est optionnel.
- Un fichier de règles spécifiques à cette fusion qui contient tous les conflits qui n’ont pas été résolus par le fichier de règles générales. Ce fichier est obligatoire si des conflits ont été détectés.
Lorsque le StoreMerge identifie un conflit pour lequel aucune règle de résolution n’a été définie, il l’indique et, si l’option a été activée, alimente un fichier de règles avec ce conflit. Il suffit alors d’éditer ce fichier, de modifier éventuellement la résolution proposée et de relancer la fusion avec ce fichier.
2. Lancement du StoreMerge
Le StoreMerge est fourni dans jcms.jar. Il faut donc le lancer avec une JVM.
2.1 Lancement depuis Eclipse
Si vous disposez d’un environnement Eclipse, vous pouvez ajouter un lanceur pour le StoreMerge.
- Menu Run > Run Configuration...
- Sélectionnez Java Application
- Cliquez sur l’icône New
- Saisissez “StoreMerge” dans Name
- Remplissez l’onglet Main (cf. infra)
- Remplissez l’onglet Arguments avec vos options de lancement(cf. infra)
- Cliquez sur le bouton Apply
Pour lancer le StoreMerge, sélectionnez l’application StoreMerge et cliquez sur le bouton Run.
2.2 Lancement avec un script shell
Si vous êtes sur Unix/Linux, écrivez le script suivant et enregistrez le dans un fichier storemerge.sh.
#!/bin/sh
export JAVA_HOME=...
export JCMS_HOME=...
export CLASSPATH=$JCMS_HOME/WEB-INF/classes:$JCMS_HOME/WEB-INF/lib/*
$JAVA_HOME/bin/java -cp $CLASSPATH com.jalios.jcms.tools.StoreMerge $*
2.3 Options de lancement
La syntaxe minimale de lancement du StoreMerge pour fusionner le fichier store1.xml avec le fichier store2.xml et produire un nouveau fichier storeMerged.xml est la suivante :
storemerge.sh -output storeMerged.xml store1.xml store2.xml
La fusion échouera si un conflit est détecté. Pour produire le fichier de résolution, il faut ajouter l’option -prepareResolutionFile avec le chemin du fichier de résolution à créer.
storemerge.sh -prepareResolutionFile resolution.txt -output storeMerged.xml store1.xml store2.xml
Une fois le fichier de résolution défini il faut relancer le StoreMerge avec l’option -resolutionFile et le chemin du fichier de résolution.
storemerge.sh -resolutionFile resolution.txt -output storeMerged.xml store1.xml store2.xml
Lorsque vous aurez fait plusieurs fusions avec des résolutions de conflits, si vous identifiez des règles récurrentes, vous pouvez les enregistrer dans un fichier de règles générales et l’indiquez avec l’option -ruleFile.
storemerge.sh -ruleFile rules.txt -prepareResolutionFile resolution.txt -output storeMerged.xml store1.xml store2.xml
Si votre store est gros pensez à indiquer la plus grande estampille commune avec l’option -gcs. Celle-ci peut être obtenue par un premier lancement du StoreMerge, en consultant la sortie.
StoreMerge -gcs c_123456789 -ruleFile rules.txt -prepareResolutionFile resolution.txt -output storeMerged.xml store1.xml store2.xml
Note : Dans le cas où les préfixes précédant le GCS seraient différents, c'est le préfixe du store2 qui est utilisé pour créer le store fusionné.
D’autres options sont disponibles. Voici la syntaxe générale de lancement du StoreMerge :
usage: StoreMerge [options] -output storeMerged.xml store1.xml store2.xml
options:
-classicIO use the classic I/O (slower but consume less memory).
-gcs <arg> provide the Greatest Common Stamp (exclusive with -useJStoreSyncOp option).
-ignoreConflicts ignore the conflicts.
-output <arg> the output file.
-prepareResolutionFile <arg> a file that will be filled with each conflicting ID.
-resolutionFile <arg> a file that contains a resolution for each conflicting ID.
-ruleFile <arg> a file that contains rules for conflict resolution.
-strictUpdateConflicts warn for each update/update conflict whatever the attribute values are.
-useJStoreSyncOp use the <JSTORE:SYNC> mark to find out the store divergence (exclusive with -gcs option).
exit code:
0: Merge done (or no merge to perform)
1: Bad parameter
2: Error during the merge
3: Conflicts have been detected
2.4 Exemple de lancement
Voici un exemple de sortie suite à un lancement du StoreMerge sur deux stores ayant des conflits :
INFO - store 1 : C:\Temp\store1.xml
INFO - store 2 : C:\Temp\store2.xml
INFO - ignoreConflicts : false
INFO - strictUpdateConflicts : false
INFO - useJStoreSyncOp : false
INFO - useClassicIO : false
INFO - gcs : (no gcs)
INFO - output : C:\Temp\storemerged.xml
INFO -ruleFile :
INFO -resolutionFile :
INFO -prepareResolutionFile : C:\Temp\resolution.txt
INFO - Start merging...
INFO - Load conflict resolution rules...
INFO - Compute store diff
INFO - Greatest Common Stamp: r_100
INFO - Load divergent suffixes...
INFO - Store 1 suffix: 11 operations on 11 data
INFO - Store 2 suffix: 4 operations on 4 data
INFO - Process conflicts...
WARN - Conflict update/update on data "r_11" (member) for attribute "declaredGroups":
# ============================================================
# Store 1 value: "@|r_1|r_2"
# ------------------------------------------------------------
# Store 2 value: "@|r_1|r_3|r_4"
# ------------------------------------------------------------
# Conflict details:
# Store 1 new items: r_2
# Store 2 new items: r_3, r_4
# ============================================================
WARN - Conflict update/update on data "r_1" (group) for attribute "name":
# ============================================================
# Store 1 value: "Group R1 - Updated by R"
# ------------------------------------------------------------
# Store 2 value: "Group R1 - Updated by C"
# ============================================================
WARN - 2 conflicts detected on 2 data. Stop merge.
WARN - The stores have not been merged (status: 3 - Conflicts).
INFO - Total time: 438 ms.
3. Résolution des conflits
A partir des deux stores à fusionner, le StoreMerge détermine les deux suffixes divergents.
Un conflit apparaît lorsqu'il existe pour une même donnée une liste d'opérations dans les deux suffixes divergents.
3.1 Types de conflits détectés
Les types de conflits sont nommés en fonction du type d'opération qui à eu lieu sur la donnée conflictuelle.
3.1.2 Conflits Update / Update
Conflit de mise à jour sur une même donnée.
Mode de résolution :
- Fusion des attributs distincts.
- Plusieurs règles sont proposées pour traiter les divergences sur un même attribut (cf. infra).
- Des opérations additionnelles peuvent être ajoutées au store fusionné pour résoudre les conflits sur les attributs (cf. infra).
3.1.3 Conflits Update / Delete et Conflits Delete / Update
Conflit entre une mise à jour d'une donnée et la suppression de cette même donnée.
Mode de résolution :
- Une seule des deux listes d'opérations est retenue.
- Plusieurs règles sont proposées pour choisir quelle liste d'opérations retenir (cf. infra)
3.1.4 Conflits Create / Create
Conflit entre deux création d'une donnée. Chaque suffixe divergent comporte une opération de création sur une donnée avec le même identifiant.
Attention ! Ce type de conflit ne devrait pas apparaître. Il est révélateur d'une fusion manuelle du store (avec un éditeur de texte) ou d'un enchainement de fusion un peu particulier. Cela peut par exemple apparaitre si on essai de fusionner trois store s1, s2 et s3. On fusionne s1 et s2, ce qui produit s12. Puis s2 et s3, ce qui produit s23. Si on fusionne s12 et s23 alors ce type de conflit peut apparaitre car les suffix divergents peuvent comporter des opérations de s2.
Mode de résolution :
- Une seule des deux listes d'opérations est retenue.
- Plusieurs règles sont proposées pour choisir quelle liste d'opérations retenir (cf. infra)
3.1.5 Conflits Delete / Delete
Il ne s'agit pas véritablement d'un conflit puisque la donnée doit être supprimée. Cependant, on ne retient qu'une seule des deux listes d'opérations pour éviter d'avoir des messages d’erreur au chargement du store dans le cas où il y aurait des opérations de mise à jour postérieures à la première des deux opérations de suppression.
Mode de résolution :
- Seule la liste d'opérations du premier store fourni en paramètre est ajoutée au store fusionné.
3.1.5 Conflits non traités
Conflits inter-attributs
Les conflits portant sur des attributs distincts ne sont pas traités.
Exemple : Contrainte d'intégrité entre deux attributs (p. ex. : dans le type Article, champ photoCaption obligatoire si l'attribut photo n'est pas vide)
Conflits inter-données
Les conflits portant sur plusieurs données ne sont pas traités.
Exemple : Création d'un contenu avec la catégorie c1 vs. Suppression de la catégorie c1.
Recommandation
Après une fusion de store, il est recommandé de démarrer le site avec le store fusionné et de lancer le contrôle d'intégrité (Espace d'administration > Supervision > Contrôle d'intégrité des données).
3.2 Règles générales de résolution de conflits
Les règles de résolution de conflits sont de la forme :
Target: Action
Target : représente un moyen d’identifier une donnée conflictuelle
Action : représente le mode de résolution à appliquer.
3.2.1 Exemples de targets
generated.Article@categories // l'attribut categories des Articles generated.Portlet.*@skins // l'attribut skin de toutes les classes Portlet workspace@administrators // l'attribut administrators des Workspace .*@titleML // l'attribut titleML de toutes les classes generated.Portlet.*@.* // Tous les attributs de toutes les portlet $j_1@name: // l'attribut name de l'objet j_1 $j_2@.*: // tous les attributs de l'objet j_2
3.2.2 Exemples de règles
generated.Portlet.*@author: ignore // ignorer les conflits portant sur l'attribut author des Portlet
workspace.*@administrators: ignore-order // ignorer l'ordre des éléments du champ administrators des workspace
$j_1@name: store1 // pour les conflits portant sur le nom du groupe j_1, prendre les modification du store 1
$j_2@.*: store2 // pour tous les conflits portant sur le membre j_2, prendre les modification du store 2
Article@description: prefer-not-empty // pour les conflits portant sur le champ description des Article, prendre celui qui est rempli.
Article@description: store1 // si la règle précédente n'a pas résolu le conflit, prendre la valeur du store1
generated.Portlet.*@update-delete: store2 // pour tous les conflits U/D sur les portlet, prendre l'opération du store 2
.*@update-delete: update // pour tous les conflits U/D, privilégier le update
3.2.3 Syntaxe des règles
SyntaxeBNF des règles.
rule ::= uuRule | udRule | ccRule uuRule ::= uuTarget ":" uuAction uuTarget ::= selector "@" attributSelector selector ::= classSelector | idSelector idSelector ::= "$" (regex | string) classSelector ::= regex | string attributSelector ::= regex | string uuAction ::= "ignore" | "ignore-order" | "ignore-empty-item" | "merge" | "store1" | "store2" | "prefer-not-empty" | quoted-string udRule ::= udTarget ":" udAction udTarget ::= selector "@update-delete" udAction ::= "delete" | "update" | "store1" | "store2" ccRule ::= ccTarget ":" ccAction ccTarget ::= selector "@create-create" ccAction ::= "store1" | "store2" quoted-string ::= "\"" string "\""
3.2.4 Ordre des targets
Plusieurs targets peuvent cibler un même attribut. C'est l'ordre déclaré dans le fichier de résolution qui l'emporte. En cas de séquence d'action, elles sont jouées dans l'ordre de la séquence.
Aussi, il convient d'écrire les règles de la plus précise à la plus large.
Exemple
Règles :
generated.PortletRSS@displayTitle: store2
generated.PortletRSS@.*: store1
generated.Portlet.*@.*[tT]itle.*: prefer-not-empty store2
generated.Portlet.*@.*: store1
Résolutions :
Classe |
Attribut en conflit |
Targets |
Actions |
PortletRSS |
displayTitle |
generated.PortletRSS@displayTitle |
store2 |
PortletRSS |
title |
generated.Portlet.*@.*[tT]itle.* |
prefer-not-empty |
PortletRSS |
title |
generated.Portlet.*@.*[tT]itle.* |
store2 |
PortletRSS |
author |
generated.PortletRSS@.* |
store1 |
PortletWorkflow |
displayTitle |
generated.Portlet.*@.*[tT]itle.* generated.Portlet.*@displayTitle |
prefer-not-empty |
PortletWorkflow |
displayTitle |
generated.Portlet.*@.*[tT]itle.* , generated.Portlet.*@displayTitle |
store2 |
PortletWorkflow |
author |
generated.Portlet.*@.* |
store1 |
3.3 Actions sur les conflits Update/Update
3.3.1 ignore
Action : la fusion a lieu sans tenir compte des conflits sur cet attribut. Le résultat après fusion n’est pas prévisible. Il est lié à l’ordonnancement des opérations.
Génération d'opérations : non
Syntaxe : target: ignore
Exemple :
generated.Portlet.*@author: ignore // ignorer tout conflit sur l'attribut author des portlets
.*@opAuthor: ignore // ignorer tout conflit sur l'attribut opAuthor de toute donnée
3.3.2 ignore-order
Action : la fusion a lieu sans tenir compte des conflits d'ordre sur cet attribut. Le résultat après fusion n’est pas prévisible. Il est lié à l’ordonnancement des opérations.
Génération d'opérations : non
Syntaxe : target: ignore-order
Exemple :
generated.Portlet.*@templates: ignore-order
generated.Portlet.*@skins: ignore-order
workspace.*@administrators: ignore-order
workspace.*@typeMap: ignore-order
workspace.*@roleMap: ignore-order
3.3.3 ignore-empty-item
Action : la fusion a lieu sans tenir compte des valeurs vides dans cet attribut de type collection. Le résultat après fusion n’est pas prévisible. Il est lié à l’ordonnancement des opérations.
Génération d'opérations : non
Syntaxe : target: ignore-empty-item
Exemple :
generated.Portlet.*@childrenBindings: ignore-empty-item
generated.Portlet.*@children: ignore-empty-item
3.3.4 store1
Action : la valeur du store1 est retenue pour cet attribut.
Génération d'opérations : oui
Syntaxe : target: store1
Exemple :
generated.Portlet@.*: store1
group@(workspace|parentSet): store1
3.3.5 store2
Action : la valeur du store2 est retenue pour cet attribut.
Génération d'opérations : oui
Syntaxe : target: store2
Exemple :
wkrole@.*: store2
generated.Article@.*: store2
generated.Portlet@displayTitle: store2
3.3.6 prefer-not-empty
Action : si le conflit porte entre une valeur vide et une valeur non vide alors c'est la valeur non vide qui l'emporte. Si ce n'est pas le cas, c'est la règle suivante qui s'applique.
Génération d'opérations : oui
Syntaxe : target: prefer-not-empty
Exemple :
.*@titleML: prefer-not-empty
3.3.7 merge
Action : les 2 jeux d'items de l'attribut conflictuel sont fusionnés. Cette règle s'applique uniquement aux attributs de type tableau, Collection et Map. Dans tous les cas, les valeurs null et les doublons sont éliminés. L'ordre est aléatoire. Pour les collections et les tableaux, cette action doit donc être réservée à des champs multivalués dont l'ordre n'est pas explicite.
Génération d'opérations : oui
Syntaxe : target: merge
Exemple :
Portlet.*@skins: merge
Exemple de fusion :
Store1 : skins="@skin1||skin2"
Store2 : skins="@skin2|skin3"
Après fusion : skins="@skin1|skin2|skin3"
3.3.8 quoted-string
Action : la valeur de l'attribut conflictuel est forcée à la valeur indiquée entre double quotes. Ce type de sélecteur est plutôt destiné au fichier de règles spécifiques.
Génération d'opérations : oui
Syntaxe : target: "new value"
Exemple :
Portlet.*@opAuthor: "j_2"
3.4 Actions sur les conflits Update/Delete
3.4.1 update
Action : lors d'un conflit update/delete sur une donnée, seules les opérations provenant du store ayant fait l'update sont retenues.
Génération d'opération : non
Syntaxe : target: update
Exemple :
Article.*@update-delete: update
3.4.2 delete
Action : lors d'un conflit update/delete sur une donnée, seules les opérations provenant du store ayant fait le delete sont retenues.
Génération d'opération : non
Syntaxe : target: delete
Exemple :
Portlet.*@update-delete: delete
3.4.3 store1
Action : lors d'un conflit update/delete, seules les opérations du store1 sont retenues
Génération d'opération : non
Syntaxe : target: store1
Exemple :
Portlet.*@update-delete: store1
3.4.4 store2
Action : lors d'un conflit update/delete, seules les opérations du store2 sont retenues
Génération d'opération : non
Syntaxe : target: store2
Exemple :
Article.*@update-delete: store2
3.5 Actions sur les conflits Create/Create
3.5.1 store1
Action : lors d'un conflit create/create, seules les opérations du store1 sont retenues
Génération d'opération : non
Syntaxe : target: store1
Exemple :
Portlet.*@create-create: store1
3.5.2 store2
Action : lors d'un conflit create/create, seules les opérations du store2 sont retenues
Génération d'opération : non
Syntaxe : target: store2
Exemple :
Article.*@create-create: store2
3.6 Opérations de résolution des conflits
Pour les lignes comportant des conflits sur des attributs et pour lesquelles une résolution où la valeur de l'un des deux stores est privilégiée (i.e. store1, store2 ou prefer-not-empty) ainsi que pour l'action merge, il est nécessaire d'effectuer une fusion des attributs.
Ceci est fait en ajoutant à la fin du store fusionné des opérations représentant ces fusions.
L'URID storemerge est utilisé pour la construction des estampilles de ces nouvelles opérations.
Les opérations de fusion ne modifient pas l'attribut mdate.
Exemple
Store 1 :
<generated.PortletRSS stamp="s1_10000" op="update" id="s1_6000" displayTitle="My Title" cacheType="None" skins="@skin1|skin2" mdate="123456789000" />
Store 2 :
<generated.PortletRSS stamp="s2_10000" op="update" id="s1_6000" displayTitle="" cacheType="Session" skins="@skin2||skin1|" mdate="123456789001" />
Rules :
generated.Portlet.*@displayTitle: store1
generated.Portlet.*@cacheType: store2 generated.Portlet.*@skins: ignore-order
Fusion :
<generated.PortletRSS stamp="s1_10000" op="update" id="s1_6000" displayTitle="My Title" cacheType="None" skins="@skin1|skin2" mdate="123456789000" />
<generated.PortletRSS stamp="s2_10000" op="update" id="s1_6000" displayTitle="" cacheType="Session" skins="@skin2||skin1|" mdate="123456789001" />
<generated.PortletRSS stamp="storemerge_10001" op="update" id="s1_6000" displayTitle="My Title" cacheType="Session" />
3.7 Exemple de règles par défaut
Voici un exemple de fichier de règles générales de résolution de conflit.
# Group
group@name: store1
group@lastLdapSynchro: store1
group@parentSet: merge
group@order: store1
group@rightFromClassMap: merge
# Workspace
workspace@typeMap: merge
# Portlet
generated.Portlet.*@displayTitle: prefer-not-empty
generated.Portlet.*@displayTitle: store1
generated.Portlet.*@invalidClass: merge
generated.Portlet.*@templates: merge
generated.Portlet.*@skins: merge
generated.Portlet.*@condition: merge
generated.Portlet.*@.*: store2
# Publication
.*@pdate: store1
.*@edate: store1
.*@adate: store1
.*@sdate: store1
.*@opAuthor: store1