JCMS fournit en standard de nombreux contrôles lors de la validation d'un formulaire. Néanmoins, pour certains usages, il est nécessaire d'ajouter des contrôles particuliers sur les champs d'une publication, d'aiguiller automatiquement un contenu dans le workflow selon la valeur de ses champs, d'enrichir un objet juste avant son enregistrement, de déclencher des séries d'écritures en cascade, etc. Tous ces contrôles doivent être effectués non seulement sur les formulaires HTML mais aussi sur tout enregistrement déclenché via l'API JCMS.
JCMS 5 a introduit DCM (Data Control Model), un nouveau modèle de contrôle et d'action centralisé sur les objets du store. DCM permet de contrôler l'intégrité des données, de vérifier si une écriture (création, mise à jour, suppression, …) peut être réalisée et, le cas échéant, de déclencher cette écriture ainsi que toutes les traitements annexes. Dans les précédentes version de JCMS, ces traitements étaient pris en charge par les FormHandler mais ne pouvaient être utilisés que dans le cadre de requêtes HTTP. La centralisation de ces traitements simplifie la tâche du développeur qui n'a plus à ce soucier des traitements annexes lorsqu'il effectue des écritures sur les données du store.
DCM est un modèle extensible. L'interface DataController permet de développer facilement de nouveaux contrôles d'intégrité, de validation et d'exécution des écritures sur n'importe quel type de données géré dans le store.
A partir de JCMS 5.7, il est recommandé d'organiser les développements spécifiques dans des modules. Chaque module peut déclarer un ou plusieurs DataController.
1. Simplification des écritures dans le store
DCM simplifie l'API d'écriture dans le store. Il se charge de traiter à la fois la validation de l'opération (l'objet est-il correctement rempli, l'utilisateur a-t-il le droit de déclencher ce type d'opération, …) et l'exécution du traitement.
Par exemple, lors de la suppression d'un membre, JCMS se charge au préalable de :
- supprimer les données propres au membre ;
- réassigner ses publications aux administrateurs des espaces de travail ;
- mettre à jour les droits d'accès des publications ;
- supprimer l'extension sur le membre si elle existe.
Le programmeur n'a alors plus à ce soucier de savoir tout ce qui doit être validé et exécuté pour réaliser une opération d'écriture. Par exemple, pour mettre à jour le login d'un membre, avec JCMS 4.1.1, il fallait écrire :
void updateMember(Member mbr, String login, Member opAuthor) {
if (!loggedMember.isAdmin()) {
System.out.println("Forbidden") ;
return;
}
Member updated = new Member(mbr) ;
if (Util.isEmpty(updated.getLogin())) {
System.out.println("Login is empty") ;
return;
}
if (myMethodToVerifyLoginNotUsed(mbr, login)) {
System.out.println("Login already used") ;
return;
}
updated.setLogin(login) ;
channel.updateData(mbr, update, opAuthor) ;
}
A partir de JCMS 5, il suffit d'écrire :
void updateMember(Member mbr, String login, Member opAuthor, String userLang) {
Member updated = new Member(mbr) ;
updated.setLogin(login) ;
ControllerStatus status = updated.checkUpdate(opAuthor); if (status.hasFailed()) {
System.out.println(status.getMessage(userLang)) ;
return;
}
updated.performUpdate(opAuthor;) }
Le code est plus simple mais surtout il décharge le développeur des contrôles qui doivent être fait sur l'objet avant et après son écriture. Lors d'un changement de version de JCMS, il n'y a donc plus besoins de revoir l'ensemble des développements spécifiques pour prendre en compte les nouveaux contrôles internes de JCMS.
2. Contrôles sur les écritures dans le store
2.1 Validation et contrôle d'intégrité
DCM introduit l'interface DataController
qui permet d'ajouter des contrôles lors de la validation des formulaires. Les contrôles peuvent porter sur tous les types de données gérés par JCMS (Member
, Category
, Publication
, ...)
Quelques exemples d'usage :
- Sur des publications :
- Le champ
légende
doit être rempli si le champphoto
est rempli et vice-versa. - Le champ
date de début
doit être antérieur au champdate de fin
. - Le champ
résumé
doit comporter au moins 100 caractères lorsque le contenu est soumis à validation. - Les champs
titre
etrésumé
doivent être remplis dans toutes les langues. - Autant de champs multivalués
TitreSection
que de champsContenuSection
.
- Le champ
- Sur des catégories :
- Le champ
nom
doit être inférieur à 30 caractères si cette catégorie appartient à la brancheRubriques
. - Pas plus de 3 niveaux de profondeur dans la branche
Rubriques
.
- Le champ
- Sur des membres :
- Le champs
password
doit contenir au moins 8 caractères et au moins 1 chiffre. - Interdire qu'un membre appartienne simultanément aux groupes G1 et G2.
- Le champs
Au delà de la validation des formulaires, DCM généralise le contrôle d'intégrité des données. Depuis la version 5, JCMS fournit une nouvelle fonctionnalité dans l'espace d'administration : le contrôle d'intégrité des données. Cette interface liste l'ensemble des données qui ne respectent pas leurs contraintes d'intégrités (internes à JCMS et spécifique à l'application). Cet outil est par exemple utile lorsqu'on opère des changements sur un type de publication et qu'il faut vérifier l'intégrité des données existantes.

Fig. 1. Le contrôleur d'intégrité.
2.2 Contrôle de l'exécution des écritures
Les DataController permettent aussi d'agir sur l'objet avant et après qu'une opération d'écriture soit exécutée. Ceci permet au développeur d'ajouter des traitements spécifiques sur les données juste avant qu'elles ne soient écrites mais aussi de déclencher des traitements posterieur à l'écriture.
Quelques exemples d'usage :
- Signature électronique des publications
- Aiguillage automatique des messages suspects du forum dans un état spécial du workflow
- Envoi de mail à l'administrateur lorsqu'un contenu très volumineux est enregistré dans le store.
- Catégorisation automatique des contenus
- Résumé automatique
3. L'API DCM
3.1 La classe ControllerStatus
Toutes les méthodes de validation et de contrôle d'intégrité renvoient une instance de la classe ControllerStatus
qui possède les méthodes suivantes :
hasFailed()
indique si le contrôle a échoué.isForbidden()
précise si l'échec est lié aux droits du membre.getMessage(userLang)
renvoie un message décrivant les raisons de l'échec
3.2 Contrôle d'intégrité
Le contrôle d'intégrité d'un objets JCMS dérivant de la classe Data
se fait en invoquant la méthode checkIntegrity()
. Celle-ci retourne un ControllerStatus
.
Exemple :
Publication pub = channel.getPublication("c_1234");
ControllerStatus status = pub.checkIntegrity(); If (status.hasFailed()) {
System.out.println("Integrity error: " + status.getMessage("fr"));
}
3.2 Validation des écritures
Tous les objets JCMS dérivant de la classe Data
disposent des méthodes de validation suivantes :
checkCreate(Member mbr)
checkUpdate(Member mbr)
checkDelete(Member mbr)
Ces trois méthodes vérifient si les écritures sont autorisées et si le membre (mbr
) a le droit de faire ce type d'opération sur cet objet. Les méthodes checkCreate()
et checkUpdate()
vérifient en plus l'intégrité de l'objet. Le comportement de la méthode checkDelete()
dépend de la classe d'objet mais, d'une manière générale, elle vérifie si l'objet n'est pas référencé par d'autres objets.
Selon la classe de l'objet, il peut exister d'autres méthodes de validation. Par exemple, dans la classe Publication, la méthode checkMerge()
vérifie si une copie de travail peut-être fusionnée avec la publication originale.
Toutes les méthodes de validation commencent par invoquer leur super-méthode, jusqu'aux méthodes de la classe Data
qui invoquent la méthode générale :checkWrite(int op, Member mbr, boolean checkIntegrity, Map context)
Cette méthode vérifie que :
- les écritures sont actives sur le site ;
- l'objet est intègre (si
checkIntegrity
est true) ; - le membre possède les droits nécessaires pour l'exécution de l'action.
Elle délègue ensuite la validation aux méthodes checkWrite()
des DataController spécifiques.
La variable context
(de type Map
) permet d'obtenir des informations sur le contexte des validations et des exécutions. Par exemple, si la méthode est invoquée depuis un FormHandler, celui-ci ajoute la request
et la response
dans context
. Ceci permet de contrôler la valeur de l'un des paramètres de la request
ou de placer des informations dans la response
(p. ex. un cookie).
3.3 Exécution des écritures
Les méthodes performCreate()
, performUpdate()
et performDelete()
exécutent l'opération quelles représentent. Elles doivent être maintenant utilisées systématiquement à la place des méthodes createData()
, updateData()
et deleteData()
de la classe Channel
car l'invocation de ces dernières ne déclenche aucun DataController
.
Exemple :
// 1. Create
Category cat = new Category();
cat.setParent(channel.getRootCategory());
cat.setName("My category");
ControllerStatus status = cat.checkCreate(loggedMember); if (status.hasFailed()) {
System.out.printlnt("Cannot create " + cat + ": " + status.getMessage(userLang));
return;
}
cat.performCreate(loggedMember); // 2. Update
Updated updated = new Category(cat);
updated.setDescription("My description");
ControllerStatus status = updated.checkUpdate(loggedMember); if (status.hasFailed()) {
System.out.printlnt("Cannot update " + cat + ": " + status.getMessage(userLang));
return;
}
updated.performUpdate(loggedMember); // 3. Delete
ControllerStatus status = cat.checkDelete(loggedMember); if (status.hasFailed()) {
System.out.printlnt("Cannot delete " + cat + ": " + status.getMessage(userLang));
return;
}
cat.performDelete(loggedMember);
Au delà des 3 méthodes performCreate()
, performUpdate()
et performDelete()
, chaque classe peut implémenter ses propres méthodes d'éxécution. Par exemple, la classe Category
implémente la méthode performDeepCopy()
qui réalise la copie en profondeur d'une branche de catégorie. La classe Publication
implémente la méthode performMerge()
qui réalise la fusions d'une copie de travail avec la publication originale.
4. Développement de DataController spécifiques
4.1 Le composant DataController
L'ajout de contrôles spécifiques sur un type de donnée, se fait en écrivant une classe qui implémente l'interface DataController
. Cette classe doit ensuite être déclarée au sein du fichier plugin.xml
d'un module via la balise <datacontroller>
dans la section <plugincomponents>
. Cette balise comporte l'attribut types
qui permet de préciser les types de données sur lesquels portera le DataController. Les types doivent être séparés par des pipes (|) et peuvent être déclaré avec leur nom court. Le DataController opérera sur les types déclarés et sur tous les sous-type. Pour associer un DataController à toutes les publications, il suffit donc de l'associer à Publication.class
.
Exemple de déclaration :
<plugin>
...
<plugincomponents>
<datacontroller class="custom.MyDataController" types="Member|Article|PortletSearch" /> ... </plugincomponents> ... </plugin>
4.1 L'interface DataController
L'ajout de contrôles spécifiques sur une classe donnée, se fait en écrivant une classe qui implémente l'interface DataController
qui impose l'implémentation des méthodes suivantes :
checkIntegrity(Data data)
checkWrite(Data data, int op, Member mbr, boolean checkIntegrity, Map context)
beforeWrite(Data data, int op, Member mbr, Map context)
afterWrite(Data data, int op, Member mbr, Map context)
La méthodes checkIntegrity()
permet d'ajotuer des contrôles d'intégrité sur la donnée
La méthode checkWrite()
permet d'ajouter des contrôler de validités lors d'une écriture.
Les méthodes beforeWrite()
et afterWrite()
sont respectivement invoquées avant et après que l'opération soit effectuée.
4.2 La classe BasicDataController
En pratique, il est recommandé de faire dériver les DataController de la classe
BasicDataController
. Il s'agit d'une implémentation neutre de l'interface DataController
(et de JcmsConstants
). Les méthodes checkIntegrity()
et checkWrite()
renvoient ControllerStatus.OK
. Les méthodes beforeWrite()
et afterWrite()
ne font aucun traitement. Cette classe permet donc de n'implémenter que les méthodes véritablement utilisées.
Exemple : ce DataController vérifie que le champ résumé contient au moins 300 caractères.
public class SummaryController extends BasicDataController
implements PluginComponent {
ControllerStatus checkIntegrity(Data data) {
Publication pub = (Publication)data ;
If (Util.isEmpty(pub.getAbstract()) || pub.getAbstract().length() < 300) {
return new ControllerStatus("Le résumé doit contenir au moins 300 caractères");
}
return ControllerStatus.OK ;
}
...
}
Déclaration du DataController :
<datacontroller class="custom.SummaryController" types="Publication" />
4.3 Construction du ControllerStatus
La classe ControllerStatus
contient deux objets prédéfinis :
ControllerStatus.OK
: doit être retourné lorsque l'opération est valideControllerStatus.FORBIDDEN
: doit être retourné lorsque l'echec de la validation est liée aux droits du membre.
Le message du ControllerStatus
peut être fournit soit directement à la construction, soit par les méthodes setProp()
pour les message multilingues.
Exemples :
ControllerStatus status = new ControllerStatus("Un message en français");
ControllerStatus statusML = new ControllerStatus();
statusML.setProp("jcmsplugin.dcmdemo.msg", "param1", "param2");
Avec dans les fichiers de propriétés de langue du module :
// fr.prop
jcmsplugin.dcmdemo.msg: Exemple de message localisé avec 2 paramètres : {0} et {1}
// en.prop
jcmsplugin.dcmdemo.msg: Example of localized message with 2 parameters: {0} and {1}
5. Exemples de DataController
Les exemples suivants illustrent différents usages des DataController. Les implémentations des codes de contrôle sont volontairement naïves pour garder le code de l'exemple simple et focalisé sur l'API contrôle.
Pour illustrer certains exemples, le type ArticlePhoto est utilisé. Sa structure est décrite ci-dessous :

Fig. 2. Le type Article Photo.
5.1 Champs obligatoires conditionnels
Ce DataController impose que, dans les objets ArticlePhoto, le champ legende
soit rempli si le champ photo est rempli. L'ensemble du contrôle est réalisé dans la méthode checkIntegrity()
.
package com.jalios.jcmsplugin.demodcm;
import generated.ArticlePhoto;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.ControllerStatus;
import com.jalios.jcms.Data;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
import com.jalios.util.Util;
public class PhotoLegendeController extends BasicDataController
implements PluginComponent {
public boolean init(Plugin plugin) {
return true;
}
public ControllerStatus checkIntegrity(Data data) {
ArticlePhoto pub = (ArticlePhoto)data;
if (Util.notEmpty(pub.getPhoto()) && Util.isEmpty(pub.getLegende())) {
ControllerStatus status = new ControllerStatus();
status.setProp("msg.edit.empty-field", "Legende"); return status; } if (Util.isEmpty(pub.getPhoto()) && Util.notEmpty(pub.getLegende())) { ControllerStatus status = new ControllerStatus(); status.setProp("msg.edit.empty-field", "Photo"); return status; } return ControllerStatus.OK;
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.PhotoLegendeController" types="ArticlePhoto" />
5.2 Date de début antérieur à la date de fin
Ce DataController impose que les champs startDate
et endDate
de la classe CalendarEvent
soient correctement ordonnés. L'ensemble du contrôle est réalisé dans la méthode checkIntegrity().
package com.jalios.jcmsplugin.demodcm;
import generated.CalendarEvent;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.ControllerStatus;
import com.jalios.jcms.Data;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
public class StartEndDateController extends BasicDataController
implements PluginComponent {
public boolean init(Plugin plugin) {
return true;
}
public ControllerStatus checkIntegrity(Data data) {
CalendarEvent pub = (CalendarEvent)data;
// check that end date is after the start date
if (pub.getStartDate() != null &&
pub.getEndDate() != null &&
pub.getEndDate().before(pub.getStartDate())) {
return new ControllerStatus("La date de fin doit être postérieure à la date de début.");
}
return ControllerStatus.OK;
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.StartEndDateController" types="CalendarEvent" />
5.3 Contrôle de la profondeur d'une branche de catégorie
Ce DataController limite la profondeur d'une branche de catégorie. La branche à contrôler est définie par l'identifant virtuel $id.jcmsplugin.demodcm.catdepth.root
. La propriété jcmsplugin.demodcm.catdepth.depth
permet de préciser la profondeur maximum autorisée.
package com.jalios.jcmsplugin.demodcm;
import java.util.List;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.Category;
import com.jalios.jcms.Channel;
import com.jalios.jcms.ControllerStatus;
import com.jalios.jcms.Data;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
public class CatDepthController extends BasicDataController
implements PluginComponent {
int depth;
Category root;
public boolean init(Plugin plugin) {
Channel channel = Channel.getChannel();
depth = channel.getIntegerProperty("jcmsplugin.demodcm.catdepth.depth", 3); root = channel.getCategory("$id.jcmsplugin.demodcm.catdepth.root"); return true; } public ControllerStatus checkIntegrity(Data data) {
Category cat = (Category)data;
List ancestorList = cat.getAncestorList(root, true);
if (cat.hasAncestor(root) && ancestorList.size() > 3) {
return new ControllerStatus(root + " must not have more than " + depth + " sub-levels.");
}
return ControllerStatus.OK;
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.CatDepthController" types="Category" />
Propriétés du DataController :
jcmsplugin.demodcm.catdepth: 3
$id.jcmsplugin.demodcm.catdepth.root: c_1234
5.4 Contrôle du format des mots de passe
Ce DataController impose un format sur les mots de passe des membres : entre 8 et 16 caractères avec au moins un chiffre. Toute la difficulté de ce DataController provient du fait que le champ password
de l'objet mbr
est crypté et qu'il n'est donc pas possible de faire le contrôle du format sur ce champ. Le mot de passe en clair peut être obtenu par le paramètre de requête password1
. Pour cela, le DataController récupère la requête via la méthode channel.getCurrentServletRequest()
.
package com.jalios.jcmsplugin.demodcm;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.Channel;
import com.jalios.jcms.ControllerStatus;
import com.jalios.jcms.Data;
import com.jalios.jcms.Member;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
import com.jalios.util.Util;
public class PasswordController extends BasicDataController
implements PluginComponent {
public boolean init(Plugin plugin) {
return true;
}
public ControllerStatus checkWrite(Data data, int op, Member mbr, boolean checkIntegrity, Map context) {
// Control only creation and update
if (op != OP_CREATE && op != OP_UPDATE && op != Member.OP_UPDATE_PROFILE) {
return ControllerStatus.OK;
}
// Get the request to extract the uncrypted password
HttpServletRequest request = (HttpServletRequest)Channel.getChannel().getCurrentServletRequest();
if (request == null) {
return ControllerStatus.OK;
}
// Check if the password has been provided
String password = request.getParameter("password1");
if (Util.isEmpty(password)) {
return ControllerStatus.OK;
}
// Check the password
if (checkPassword(password)) {
return ControllerStatus.OK;
}
return new ControllerStatus("The password must contains between 8 and 16 characters and at least 1 digit.");
}
boolean checkPassword(String password) {
// At least 8 characters (max 16)
if (password.length() < 8 || password.length() > 16) {
return false;
}
// At least 1 digit
for(int i = 0; i < password.length(); i++) {
if (Character.isDigit(password.charAt(i))) {
return true;
}
}
return false;
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.PasswordController" types="Member" />
5.5 Contrôle des messages du forum
Ce DataController vérifie le contenu des messages postés dans le forum. Si un message contient des mots suspects, il est aiguillé dans un état spécial du workflow. Le contrôle se fait dans la méthode beforeWrite()
pour pouvoir modifier le champs pstatus
du message le cas échéant. Le DataController est associé à la classe AbstractForumMessage
qui est la classe mère des objet ForumMessage
et ForumDiscussion
.
package com.jalios.jcmsplugin.demodcm;
import java.util.Map;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.Data;
import com.jalios.jcms.Member;
import com.jalios.jcms.forum.AbstractForumMessage;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
public class ForumMessageController extends BasicDataController
implements PluginComponent {
static final int CHECK_TEXT_PSTATUS = -80;
public boolean init(Plugin plugin) {
return true;
}
public void beforeWrite(Data data, int op, Member mbr, Map context) {
if (op != OP_CREATE && op != OP_UPDATE) {
return;
}
AbstractForumMessage msg = (AbstractForumMessage)data;
if (!checkMessage(msg)) {
msg.setPstatus(CHECK_TEXT_PSTATUS);
}
}
boolean checkMessage(AbstractForumMessage msg) {
// Get all the text of the msg
String text = msg.getTitle() + " " + msg.getText();
text = text.toLowerCase();
// Filter with a very naive algorithm
String[] blackList = new String[] {"loan", "viagra", "cialis", "porn"};
for(int i = 0; i < blackList.length; i++) {
if (text.indexOf(blackList[i]) > 0) {
return false;
}
}
return true;
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.ForumMessageController" types="AbstractForumMessage" />
5.6 Signature électronique d'un contenu
Ce DataController signe un contenu avec l'algorithme MD5. Le contrôle est réalisé dans la méthode beforeWrite()
. La signature est calculée sur toutes les chaînes de caractères recherchables de l'objet puis enregistrée dans le champ digest
de la publication.
package com.jalios.jcmsplugin.demodcm;
import generated.ArticlePhoto;
import java.security.MessageDigest;
import java.util.Map;
import org.apache.log4j.Logger;
import sun.misc.BASE64Encoder;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.Data;
import com.jalios.jcms.Member;
import com.jalios.jcms.Publication;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
public class DigestController extends BasicDataController
implements PluginComponent {
public boolean init(Plugin plugin) {
return true;
}
public void beforeWrite(Data data, int op, Member mbr, Map context) {
if (op != OP_CREATE && op != OP_UPDATE) {
return;
}
ArticlePhoto pub = (ArticlePhoto)data;
pub.setDigest(getDigest(pub));
}
String getDigest(Publication pub) {
// Append all the (searchable) text of this publication
String[] strings = pub.getSearchStrings();
StringBuffer sb= new StringBuffer(1024);
for(int i = 0; i < strings.length; i++) {
sb.append(strings[i]);
}
String text = sb.toString();
// Compute the digest
return base64Encoder.encodeBuffer(md5.digest(text.getBytes())).trim();
}
static BASE64Encoder base64Encoder;
static MessageDigest md5;
private static final Logger logger = Logger.getLogger(DigestController.class);
static {
base64Encoder = new BASE64Encoder();
try {
md5 = MessageDigest.getInstance("MD5");
} catch(java.security.NoSuchAlgorithmException ex) {
logger.warn("An exception occured while retrieving MD5 MessageDigest", ex);
}
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.DigetController" types="ArticlePhoto" />
5.7 Alerte sur les contenus volumineux
Ce DataController envoie un mail à l'administrateur si une publication contient plus d'une certaine quantité de texte. Le contrôle est réalisé dans la méthode afterWrite()
.
package com.jalios.jcmsplugin.demodcm;
import java.util.Map;
import com.jalios.jcms.BasicDataController;
import com.jalios.jcms.Channel;
import com.jalios.jcms.Data;
import com.jalios.jcms.Member;
import com.jalios.jcms.Publication;
import com.jalios.jcms.plugin.Plugin;
import com.jalios.jcms.plugin.PluginComponent;
import com.jalios.util.MailUtil;
public class HugeContentController extends BasicDataController
implements PluginComponent {
int maxSize;
public boolean init(Plugin plugin) {
maxSize = Channel.getChannel().getIntegerProperty("jcmsplugin.dcmdemo.huge.max", 10000); return true; } public void afterWrite(Data data, int op, Member mbr, Map context) {
if (op != OP_CREATE && op != OP_UPDATE) {
return;
}
Publication pub = (Publication)data;
// Check publication size
if (getSize(pub) < maxSize) {
return;
}
// Alert the administrator
alertAdmin(pub, mbr);
}
int getSize(Publication pub) {
// Compute the size of the (searchable) text of this publication
int size = 0;
String[] strings = pub.getSearchStrings();
for(int i = 0; i < strings.length; i++) {
size += strings[i].length();
}
return size;
}
void alertAdmin(Publication pub, Member mbr) {
// Build the mail
Channel channel = Channel.getChannel();
String subject = "[" + channel.getName() + "] Huge Content Alert";
String content =
"A huge content has been saved by " + mbr + ".\n" +
"Check it out at the following URL:\n" +
channel.getUrl() + pub.getDisplayUrl(null);
String sender = channel.getDefaultAdmin().getEmail();
Member wsAdmin = (Member)pub.getWorkspace().getAdministrators().get(0);
String recipient = wsAdmin.getEmail();
// Send the mail
try {
MailUtil.sendMail(sender,
recipient,
subject,
content);
} catch (Exception ex) {
System.out.println("An exception occured while sending mail alert");
}
}
}
Déclaration du DataController :
<datacontroller class="com.jalios.jcmsplugin.demodcm.HugeContentController" types="Content" />
Propriétés du DataController :
jcmsplugin.demodcm.huge.max: 1000000
6. Détails sur l'implémentation des DataController
Depuis JCMS 5, l'ensemble du code de gestion des objets de JCMS exploite DCM. Pour les classes internes (p. ex. Category
, Member
, …) et les classes générées (p. ex. Article
, Faq
, Glossaire
, …) le code de contrôle est encapsulé dans la classe.
DCM repose sur un modèle de délégation. La classe Data
(classe mère de tous les objets JCMS) définit les principales méthodes de contrôles :
checkIntegrity()
déclenche l'appel à la méthodecheckIntegrity()
des DataController spécifiquescheckWrite()
contrôle l'intégrité de l'objet (optionnel), l'activation des écriture, invoque la méthodecheckMember()
et déclenche l'appel à la méthodecheckWrite()
des DataController spécifiques.checkMember()
contrôle les droits du membre à exécuter l'opérationcheckCreate()
invoque la méthodecheckWrite()
checkUpdate()
invoque la méthodecheckWrite()
checkDelete()
invoque la méthodecheckWrite()
performCreate()
etperformUpdate()
invoquent la méthodebeforeWrite()
des DataController spécifiques, exécute la création ou la mise-à-jour de l'objet et invoque la méthodeafterWrite()
des DataController.performDelete()
exécute la suppression de l'extension si elle existe et fait un traitement similaire àperformCreate()
etperformUpdate()
.
Selon leurs besoins, les sous-classes surchargent les méthodes checkIntegrity()
, checkCreate()
, checkUpdate()
, checkDelete()
, performCreate()
, performUpdate()
et performDelete()
. Les méthodes de validation (checkXXX()
) commencent par invoquer la méthode correspondante de leur superclasse puis, si celle-ci valide l'opération, effectuent leur propre vérification.
Les méthodes d'exécution (performXXX()
) appellent leur super méthode selon leurs besoins. Par exemple, la version surchargée de performDelete()
effectuera d'abord ses traitements de nettoyage puis invoquera à la fin super.performDelete()
. La version surchargée de performUpdate()
invoquera d'abord super.performUpdate()
puis exécutera son code spécifique ensuite.
Les méthodes de contrôle d'intégrité et de validation des DataController sont donc invoquées avant les méthodes de contrôle interne des objets.
7. Recommandations d'usage
7.1 Choisir la bonne méthode
Lors de l'écriture d'un DataController, il convient de choisir de façon pertinente où doit être placé le code de contrôle. Pour les validations, il est recommandé de placer le code dans la méthode checkIntegrity()
si ce contrôle ne dépend pas du type d'écriture ni de son contexte. Dans le cas contraire, on placera le code dans la méthode checkWrite()
. Pour les contrôles portant sur l'exécution, on les placera dans la méthode beforeWrite()
s'ils doivent modifier l'objet et dans afterWrite()
dans les autres cas.
7.2 Granularité des DataController
D'une manière générale il est préférable de développer de petits DataController qui effectuent un contrôle précis. Dans le cas où l'enchaînement des contrôles doit être maîtrisé, il convient de les regrouper dans un même DataController.
7.3 DataController vs. StoreListener
La méthode afterWrite()
de l'interface DataController
permet d'être alerté après chaque opération d'écriture. Ce comportement est assez semblable aux StoreListener. Cependant, les StoreListener permettent de capturer plus d'évènements :
- Un StoreListener peut être notifié lors du chargement du store. Ceci permet par exemple de maintenir des index sur les données.
- Un StoreListener peut être notifié des écritures réalisées sur d'autres réplicas. Ceci permet de déclencher des opérations sur chaque réplica. Par exemple, la suppression du fichier référencé par un FileDocument est réalisée par un StoreListener.
Il est conseiller d'utiliser les DataController lorsque l'action doit être déclenchée une et une seule fois sur l'ensemble des réplicas ; comme par exemple l'envoi d'un mail ou l'enchaînement d'une autre écriture. Les StoreListener doivent être réservés aux seuls cas où un DataController ne peut pas répondre aux besoins.
7.4 DataController vs. Custom FormHandler
Les FormHandler sont les classes Java chargées du traitement des formulaires de création, mise à jour et suppression. Ils utilisent maintenant les méthodes de validation et d'exécution de DCM. Néanmoins, ils ont aussi leurs propres méthodes de validations du format des paramètres reçus (contrôle des valeurs des champs numériques, des dates, des liens, ...)
Lors de la définition d'un type de publication, il est possible de préciser la classe du FormHandler. Cette classe doit dériver de la classe du FormHandler générée pour le type. On parle alors de Custom FormHandler.
L'introduction des DataController restreint l'usage des Custom FormHandler au contrôle des actions spécifiques du formulaire. C'est par exemple le cas pour le type Faq. Une Faq affiche un ensemble de FaqEntry qui référencent la Faq. Les FaqEntry sont listées selon la valeur de leur champ order. Des petites flèches haut/bas présentes, sur le gabarit d'affichage de la Faq, permettent de manipuler l'ordre des FaqEntry. Le traitement des actions opUp
et opDown
, correspondant aux requêtes émises par les petites flèches, est réalisé dans CustomFaqEntryFormHandler
.