We apologize for untranslated text, you can use the Google Translation button to get an automatic translation of the web page in the language of your choice.

JCMS 5.7: Develop with DCM and the DataController (french)

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 :

  1. supprimer les données propres au membre ;
  2. réassigner ses publications aux administrateurs des espaces de travail ;
  3. mettre à jour les droits d'accès des publications ;
  4. 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 champ photo est rempli et vice-versa.
    • Le champ date de début doit être antérieur au champ date de fin.
    • Le champ résumé doit comporter au moins 100 caractères lorsque le contenu est soumis à validation.
    • Les champs titre et résumé doivent être remplis dans toutes les langues.
    • Autant de champs multivalués TitreSection que de champs ContenuSection.
  • Sur des catégories :
    • Le champ nom doit être inférieur à 30 caractères si cette catégorie appartient à la branche Rubriques.
    • Pas plus de 3 niveaux de profondeur dans la branche Rubriques.
  • 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.

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.

Screenshot Integrity Report

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 :

  1. les écritures sont actives sur le site ;
  2. l'objet est intègre (si checkIntegrity est true) ;
  3. 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 valide
  • ControllerStatus.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 :

Screenshot Article Photo Type

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éthode checkIntegrity() des DataController spécifiques
  • checkWrite() contrôle l'intégrité de l'objet (optionnel), l'activation des écriture, invoque la méthode checkMember() et déclenche l'appel à la méthode checkWrite() des DataController spécifiques.
  • checkMember() contrôle les droits du membre à exécuter l'opération
  • checkCreate() invoque la méthode checkWrite()
  • checkUpdate() invoque la méthode checkWrite()
  • checkDelete() invoque la méthode checkWrite()
  • performCreate() et performUpdate() invoquent la méthode beforeWrite() des DataController spécifiques, exécute la création ou la mise-à-jour de l'objet et invoque la méthode afterWrite() des DataController.
  • performDelete() exécute la suppression de l'extension si elle existe et fait un traitement similaire à performCreate() et performUpdate().

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.