Atol C&D met à disposition de la communauté Alfresco (sous licence GPL) une extension qui utilise à la fois le Framework Audit Trail et le Framework SURF, son nom de code : AuditSurf. Cet outil permet d’avoir une vue d’ensemble sur l’utilisation de l’entrepôt Alfresco. Il fournit une assistance à l’administration et un suivi de l’utilisation d’Alfresco.
# Fonctionnement général
AuditSurf est découpé en deux blocs :
- un module responsable de la récupération des données d’audit coté entrepôt (module AMP)
- une extension SURF qui assure la présentation / mise en forme des données (Application basée sur le Framework SURF)
Les échanges entre ces deux blocs s’effectuent au travers de connecteurs (HTTP), ce qui permet à notre application SURF d’appeler des WebScripts situés coté entrepôt (le format majoritairement utilisé lors de ces échanges est le JSON).
Voici une schématisation du fonctionnement d’AuditSurf :
Accès aux données :
Les informations d’AuditSurf sont issues du mécanisme d’audit trail d’Alfresco, de JMX (pour la version entreprise seulement) et de l’entrepôt lui-même.
Cette partie va traiter des mécanismes que nous avons utilisés pour extraire les données d’audit de la base de données Alfresco. Si vous désirez avoir des informations sur le mécanisme d’audit au sein d’Alfresco, je vous invite à lire l’article concernant l’audit sur ce blog.
J’illustrerai les mécanismes coté entrepôt Alfresco à l’aide d’un exemple simple (qui sera disponible au téléchargement).
- Accès aux données d’audit (Utilisation d’Hibernate)
La majorité des fonctionnalités de notre application utilisent des données qui sont stockées dans la base de données.
Pour récupérer les informations dans les tables d’audit, nous utilisons le mapping Hibernate mis en place dans Alfresco et plus particulièrement l’API que propose Hibernate : Criteria (c‘est une API d’interrogation par critères intuitive et extensible).
Pour commencer, nous avons créé un service afin de centraliser tous les accès à la base de données via Hibernate. Ce service hérite de la classe abstraite HibernateDaoSupport indispensable pour pouvoir « exécuter » des requêtes Hibernate.
Afin de réaliser des requêtes Criteria, il est utile de consulter le fichier de mapping Hibernate concernant l’audit qui se trouve dans le SDK Alfresco : Audit.hbm.xml.
Prenons un exemple simple : on désire récupérer la liste des documents créés lors des sept derniers jours :
Pour récupérer la liste des documents créés, il faut auditer le service « FileFolderService » et plus particulièrement sa méthode « create ».
Pour construire la requête Criteria, nous aurons besoins des trois tables d’audit :
- La table ALF_AUDIT_SOURCE pour pouvoir sélectionner le service « FileFolderService » et la méthode « create ».
- La table ALF_AUDIT_FACT pour récupérer les informations sur le document (sa date de création et son NodeRef).
- et la table ALF_AUDIT_DATE si l’on souhaite restreindre nos résultats sur une période précise.
Création du service : SimpleAuditService.java
1 2 3 4 5 6 7 8 |
package com.atolcd.alfresco.auditexample.cmr; import java.util.Calendar; import java.util.List; public interface SimpleAuditService{ public List<object[]> getCreatedDocuments(final Calendar start, final Calendar end); } |
Implémentation du service : SimpleAuditServiceImpl.java
L’implémentation de notre service hérite de la classe HibernateDaoSupport et implémente évidement l’interface SimpleAuditService que nous venons de créer.
- Construction de la requête Criteria
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package com.atolcd.alfresco.auditexample.repo; import java.sql.SQLException; import java.util.Calendar; import java.util.List; import org.alfresco.repo.audit.hibernate.AuditFact; import org.hibernate.Criteria; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.criterion.Expression; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.springframework.orm.hibernate3.HibernateCallback; import org.springframework.orm.hibernate3.support.HibernateDaoSupport; import com.atolcd.alfresco.auditexample.cmr.SimpleAuditService; public class SimpleAuditServiceImpl extends HibernateDaoSupport implements SimpleAuditService{ public List<object[]> getCreatedDocuments(final Calendar start, final Calendar end){ HibernateCallback callback = new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { Criteria crit = session.createCriteria(AuditFact.class, "af"); crit.createCriteria("auditDate", "ad"); crit.createCriteria("auditSource", "aso"); crit.add(Restrictions.eq("aso.service", "FileFolderService")) .add(Restrictions.eq("aso.method", "create")) .add(Restrictions.eq("af.arg3", "{http://www.alfresco.org/model/content/1.0}content")) .add(Restrictions.eq("af.fail", false)); if(start != null && end != null) crit.add(Expression.between("ad.date", start.getTime(), end.getTime())); //Sélection crit.setProjection(Projections.projectionList() .add(Projections.property("af.returnValue")) .add(Projections.property("af.date")) ); return crit.list(); } }; List<object[]> result = (List<object[]>)(getHibernateTemplate().execute(callback)); return result; } } |
Explication de la requête :
La requête ci-dessus est relativement simple et peut se décomposer comme une requête SQL classique.
Puisque l’on utilise Hibernate, on ne travail pas sur des tables mais sur des classes cependant le raisonnement est sensiblement similaire (bien que la construction n’est pas ordonnée de la même façon).
On commence par sélectionner les classes sur lesquelles on désire travailler (équivalent à clause FROM en SQL) en utilisant « createCriteria ». Ensuite, on définit des restrictions (clause WHERE) à l’aide de « Restrictions ». Puis on projette ce dont on a besoin (clause SELECT) à l’aide de « Projections ».
Cette requête Crtieria équivaut à la requête SQL suivante :
1 2 3 4 5 6 7 8 9 |
SELECT af.return_val, af.timestamp FROM alf_audit_fact af, alf_audit_date ad, alf_audit_source aso WHERE af.audit_source_id = aso.id AND af.audit_date_id = ad.id AND aso.service = 'FileFolderService' AND aso.method = 'create' AND af.arg_3 = '{http://www.alfresco.org/model/content/1.0}content' AND af.fail = 0 [AND ad.date_only between date_debut and date_fin]; |
Pour plus d’informations sur Criteria, je vous renvoie vers le site d’Hibernate.
Cet exemple n’est plus d’actualité dans Alfresco 3.2 Entreprise. En effet, dans la dernière monture d’Alfresco, il faut s’intéresser au service « ContentService » et à sa méthode « getWriter » pour avoir des informations sur la création de contenu.
Voici à quoi ressemblerait la requête criteria :
1 2 3 4 5 6 7 |
crit.add(Restrictions.eq("aso.service", "ContentService")) .add(Restrictions.eq("aso.method", "getWriter")) .add(Restrictions.eq("af.fail", false)); crit.setProjection(Projections.projectionList() .add(Projections.property("arg1")) .add(Projections.property("af.date")) |
- Injection du service via le Framework Spring
Afin de pouvoir utiliser notre service, il faut créer un bean Spring pour que l’on puisse facilement l’injecter dans nos WebScripts.
La déclaration du bean se fait dans le fichier module-context.xml du module :
- Création d’un WebScript
Maintenant que l’on a créé un service capable de récupérer des données d’audit, on peut créer un WebScript qui fera appel à ce dernier.
Les WebScripts d’AuditSurf (coté entrepôt) sont des WebScripts Java-Backed. Ces WebScripts permettent de faire des traitements plus évolués que des WebScripts « classiques ».
Ce genre de WebScript est composé :
- d’une classe Java (qui fera appel à notre service pour pouvoir récupérer les informations d’audit)
- d’un fichier de description qui indique les URLs par lesquelles le WebScript est accessible, son mode d’authentification, …
- d’un ou plusieurs templates (FreeMarker) correspondant aux représentations possibles (un template par format (HTML, JSON, Atom, etc.))
- et éventuellement d’un fichier JavaScript.
Poursuite de l’exemple : on créé un WebScript qui va faire appel à la méthode getCreatedDocuments du service que l’on a créé précédemment (SimpleAuditService).
# 1. Création de la classe JAVA
Ce WebScript sera de type « DeclarativeWebScript » du fait que les données qu’il renverra seront mises en forme par un template.
Ce WebScript aura besoin du NodeService et du service que nous avons créé. Ces services seront injectés à l’aide de Spring.
Afin de pour pouvoir réceptionner ces services dans la classe Java du WebScript, il faut dans un premier temps définir des « setters ».
Fonctionnement :
Le fonctionnement du WebScript est simple, il va faire appel à notre service puis il va traiter les données qui lui seront renvoyées. Les résultats seront ensuite insérés dans le « model » qui sera transmis au template.
CreatedDocumentsGet.java
(Dans cette exemple, la période est défini par défaut, le WebScript va récupérer les fichiers créés lors des 7 derniers jours)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
package com.atolcd.alfresco.auditexample.web.scripts; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.web.scripts.Cache; import org.alfresco.web.scripts.DeclarativeWebScript; import org.alfresco.web.scripts.Status; import org.alfresco.web.scripts.WebScriptException; import org.alfresco.web.scripts.WebScriptRequest; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import com.atolcd.alfresco.auditexample.cmr.SimpleAuditService; public class CreatedDocumentsGet extends DeclarativeWebScript implements InitializingBean { private SimpleAuditService auditService; private NodeService nodeService; // Setters public void setAuditService(SimpleAuditService auditService){ this.auditService = auditService; } public void setNodeService(NodeService nodeService){ this.nodeService = nodeService; } public void afterPropertiesSet() throws Exception { Assert.notNull(auditService); Assert.notNull(nodeService); } @Override protected Map<string, Object> executeImpl(WebScriptRequest req, Status status, Cache cache){ try { Map<string, Object> model = new HashMap<string, Object>(); GregorianCalendar start = new GregorianCalendar(); GregorianCalendar end = new GregorianCalendar(); start.setTime(new Date()); end.setTime(new Date()); start.add(Calendar.WEEK_OF_YEAR, -1); List <object[]> listres = auditService.getCreatedDocuments(start, end); Map<date, NodeRef> docs = new HashMap<date, NodeRef>(); for(int i=0;i<listres.size();i++){ //Exemple : FileInfo[name=AuditHbm.xml, isFolder=false, nodeRef=workspace://SpacesStore/06468917-9e0d-40ee-ba7d-d6125bcbd72f] String fileInfo = (String)listres.get(i)[0]; Pattern pNodeRef = Pattern.compile("nodeRef=([^,]*)]"); Matcher matcher = pNodeRef.matcher(fileInfo); //Test si le nœud existe if(matcher.find() && this.nodeService.exists(new NodeRef(matcher.group(1)))){ NodeRef node = new NodeRef(matcher.group(1)); //Récupération du nœud docs.put((Date)listres.get(i)[1], node); } } //Insertion dans le model model.put("createdDocs", docs.entrySet()); return model; } catch(Exception e){ throw new WebScriptException("[CreatedDocumentsGet] Error in executeImpl function"); } } } |
# 2. Création du fichier de description du WebScript
Nom du fichier : createddocuments.get.desc.xml
1 2 3 4 |
Created documents Created documents /auditexample/CreatedDocuments admin |
# 3. Création de templates
Ensuite, à l’aide de Freemarker, il possible d’effectuer le rendu que l’on souhaite (HTML, JSON, …). Voici deux exemples:
Rendu HTML : createddocuments.get.html.ftl
1 2 3 4 5 |
<#if createdDocs?exists> <#if createdDocs?size > 0> <#list createdDocs?sort_by("key")?reverse as data> <#assign node = companyhome.nodeByReference[data.value] /> ${node.properties.name} (${data.key?datetime?string.medium?capitalize}) |
Rendu JSON : createddocuments.get.json.ftl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<#if createdDocs?exists> <#if createdDocs?size !=0> { "node": [ <#list createdDocs?sort_by("key")?reverse as data> <#assign node = companyhome.nodeByReference[data.value] /> { "icon" : "${url.context}${node.icon16}", "name" : "${node.properties.name}", "downloadUrl" : "${url.context}${node.downloadUrl}?ticket=${session.ticket}", "date" : "${data.key?datetime?string.medium?capitalize}" } <#if data_has_next>, ] } |
# 4. Injection Spring du Web Script
Il est important de ne pas oublier de lier la classe java (CreatedDocumentsGet.java) au WebScript, pour cela, il est nécessaire de créer un bean Spring, auquel on injectera les deux services dont on a besoin.
On active l’audit :
On se limitera d’auditer la méthode « create » du service « FileFolderService ».
Cet exemple est disponible ici sous la forme d’un module Alfresco, les sources complètes étant disponibles ici.
Cet exemple ne sera pas fonctionnel en l’état sur Alfresco 3.2 Entreprise.
Adresse pour accéder au WebScript (rendu HTML) :
http://adresse-serveur:port/alfresco/service/auditexample/CreatedDocuments.html
Adresse pour accéder au WebScript (rendu JSON) :
http://adresse-serveur:port/alfresco/service/auditexample/CreatedDocuments.json
Mise en forme / Application SURF :
AuditSurf a été construit à l’aide du Framework SURF (Framework web léger) et est basé sur les WebScripts. (Je vous renvoie vers l’article sur les fondamentaux de SURF, rédigé par Thomas Broyer).
Comportement SURF
Toute notre application SURF est contenue dans une seule page (hormis la connexion et la déconnexion qui sont gérées par deux autres pages). La page principale est découpée en une multitude de régions. Chacune de ces régions étant associée à un composant et chaque composant est lié à un WebScript SURF.
Tous les WebScripts SURF font appels à des WebScripts situés coté entrepôt Alfresco. Pour cela, on utilise des « endpoint » et des connecteurs pour communiquer (Je vous renvoie une nouvel fois vers l’article de Thomas Broyer, §4 de la section « Architecture »).
Ces connecteurs sont définis dans le fichier webscript-framework-config.xml, ils permettent de définir le mode d’accès (mais pas seulement) à l’entrepôt Alfresco (ou une autre source de données).
Nous avons utilisé comme brique de départ, l’excellent tutoriel de Ben Hagan disponible à adresse suivante : http://www.benh.co.uk/alfresco/surf-part-1-getting-started/
WebScripts SURF, récupération des données
Dans AuditSurf, on distingue deux familles de WebScript : ceux destinés à une mise en forme sous forme de « dashlets » (rendu HTML) et ceux destinés pour l’affichage des graphiques (JSON pour Open Flash Chart).
Chaque WebScript de notre extension SURF fait appel à un WebScript qui se situe coté entrepôt Alfresco. Les données récupérées, à l’aide des connecteurs, sont majoritairement des données au format JSON (cependant, ces données peuvent être parfois textuelles).
Ces données sont par la suite traitées afin de pouvoir générer un rendu graphique.
Ces WebScripts sont composés :
- d’un fichier de description
- d’un fichier JavaScript (pour se connecter à un WebScript Alfresco)
- d’un template pour le rendu (HTML ou JSON pour les graphiques)
- des fichiers .properties pour l’internationalisation
Schématisation du fonctionnement des WebScripts coté SURF
Voici un exemple de connexion à un WebScript « Alfresco » via un connecteur (fichier JavaScript) :
1 2 3 4 5 |
//Création d'un connecteur var connector = remote.connect("alfresco"); //Récupération du JSON renvoyé par le Web Script Alfresco var json = connector.get("/auditexample/createdDocuments.json"); |
Il faut ensuite traiter les données (JSON dans le cas présent) afin de récupérer les informations qui nous intéressent.
Ces informations sont ensuite stockées dans le « model » afin que le template Freemarker du Web Script SURF puisse mettre en forme les données.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//"Evaluation" du JSON var data = eval( '(' + json + ')'); //On boucle sur les objets du JSON for(var i = 0 ; i<data["documents"].length ; i++) { var obj = { "icon" : data[i].icon, "name" : data[i].name, "url" : data[i].downloadUrl, "date" : data[i].date }; //On écrit dans le model model.items.push(obj); } |
Le fichier Freemarker se charge ensuite de faire le rendu (soit en renvoyant du code HTML soit en renvoyant du JSON pour l’affichage des graphiques).
Mise en page
La mise en page de notre application SURF est assurée essentiellement par deux choses : d’une part OpenFlashChart pour la génération des graphiques (histogrammes dans notre cas) et d’autre part YUI pour tout ce qui est lié à la mise en page.
Open Flash Chart
Afin de mettre en valeur les statistiques, nous avons utilisé un projet de rendu graphique Open Source : Open Flash Chart 2.
Cet outil propose une API JavaScript (il existe aussi des librairies PHP, Perl, Python, Ruby, …) pour produire des graphiques en Flash. Les graphiques sont soignés, et offre d’excellentes capacités d’interaction.
Pour générer les graphiques, Open Flash Chart prend en entrée des données au format JSON.
La syntaxe du JSON pour Open Flash Chart est scindée en deux parties : une partie définissant la mise ne page du graphique et une autre partie contenant les données à proprement dit.
http://teethgrinder.co.uk/open-flash-chart-2/json-format.php
L’utilisation de cet outil nécessite l’installation de Flash coté client.
YUI
YUI est une librairie JavaScript développée par Yahoo (Yahoo User Interface). Cette librairie permet de mettre en place relativement facilement des effets tels que des animations, des panels, la gestion des cookies, etc… Alfresco utilise très largement cette librairie pour son interface SURF Alfresco Share.
YUI a été utilisé, dans AuditSurf, notamment pour :
- créer des effets de transitions
- l’affichage des popups d’information
- le Drag & Drop des dashlets
- et la gestion de cookies (pour se souvenir du positionnement des « dashlets »)
Pour finir
AuditSurf est disponible dans son intégralité au téléchargement sur la forge Alfresco : http://forge.alfresco.com/projects/auditsurf/
AuditSurf est compatible avec
- les versions Entreprise : 3.0, 3.0.1, 3.1, 3.1.1 et 3.1.2 (cependant les informations JMX avec les versions 3.0.x sont limitées)
- la version Entreprise 3.2
- les versions Community 3.2 / 3.2r / 3.2r2
AuditSurf a été élu contribution du mois d’Août 2009 par Alfresco.
14 février 2012 at 21 h 28 min
Merci pour cette article, mais la reconstitution de l’objet à partir du JSON est incorrect:
// »Evaluation » du JSON
var data = eval( ‘(‘ + json + ‘)’);
//On boucle sur les objets du JSON
for(var i = 0 ; i<data["documents"].length ; i++) {
var obj = {
"icon" : data[i].icon,
"name" : data[i].name,
"url" : data[i].downloadUrl,
"date" : data[i].date
};
//On écrit dans le model
model.items.push(obj);
}
la variable 'data' référence "le tableau multidimensionnel" qui ne contient qu'une seul ligne représentant le tableau nommé "documents" qui lui contient les données pour reconstituer l'objet donc dans la boucle, data[i].icon est une référence null il faudrait plutôt faire data.documents[i].icon pour pointer sur la valeur icon du tableau documents.
Ci-dessous le code corrigé:
//"Evaluation" du JSON
var data = eval( '(' + json + ')');
//On boucle sur les objets du JSON
for(var i = 0 ; i<data["documents"].length ; i++) {
var obj = {
"icon" : data.documents[i].icon,
"name" : data.documents[i].name,
"url" : data.documents[i].downloadUrl,
"date" : data.documents[i]].date
};
//On écrit dans le model
model.items.push(obj);
}