Alfresco et Sharepoint

jlv mars 10th, 2009

Alfresco 3 implémente le protocole Sharepoint ce qui permet d’utiliser avec MS Office, les fonctions d’espaces partagés, le verrouillage des documents, les versions …

Alfresco est une alternative à Sharepoint et le tutoriel d’Alfresco en français alfresco-et-sharepoint vous donnera un aperçu de la solution.

SSO entre Share et Alfresco

Laurent Meunier novembre 27th, 2008

Si vous avez déjà installé Alfresco 3.0, vous vous êtes sûrement rendu compte que les deux applications Share et Alfresco gèrent leur authentification indépendamment l’une de l’autre. Si vous passez de Share à Alfresco (et inversement), vous devez de nouveau montrer patte blanche en donnant votre nom d’utilisateur et votre mot de passe … ce qui est particulièrement frustrant pour nos utilisateurs.

Que faire ? Mettre en place un système de SSO et CAS répond très bien à ce problème.

Lire la suite »

Alfresco - Protéger un document PDF avec PDFBox

Bertrand Magnier novembre 14th, 2008

Autre fonctionnalité liée au format PDF pouvant s’avérer intéressante : la protection d’un fichier par un mot de passe, ainsi que la restriction des actions autorisées (extraction de texte, impression, …).

L’implémentation va encore une fois se faire par le biais d’une action Alfresco, mais utilisera la librairie PDFBox, embarquée par Alfresco.

PDFBox [en] est une librairie Java Open-Source (licence BSD) permettant de travailler avec des fichiers PDF (génération, modification et extraction de contenu). Elle comporte également plusieurs programmes utilisables en ligne de commande.

Détails de l’action

Pour cette action de protection, il va nous falloir comme paramètres :

  • les mots de passe propriétaire et utilisateur du document,
  • le détail des actions autorisées.

Dans PDFBox, ce type de protection est représenté par les objets ProtectionPolicy (pour la partie authentification) et AccessPermission (pour la partie détail); plus précisément nous utiliserons une StandardProtectionPolicy, une protection par mot de passe (par opposition à la PublicKeyProtectionPolicy, protection par certificat).

Notre action va donc ouvrir le document PDF, vérifier si il n’est pas déjà protégé, puis créer la protection à partir des paramètres utilisateurs et enfin l’appliquer.

package com.atolcd.repo.action.executer;
 
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
 
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.util.TempFileProvider;
import org.pdfbox.exceptions.COSVisitorException;
import org.pdfbox.pdmodel.PDDocument;
import org.pdfbox.pdmodel.encryption.AccessPermission;
import org.pdfbox.pdmodel.encryption.BadSecurityHandlerException;
import org.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
 
public class PDFProtectActionExecuter extends ActionExecuterAbstractBase
	implements InitializingBean {
 
    public static final String NAME = "protect";
    public final static String PARAM_OWNER_PASSWORD = "owner_password";
    public final static String PARAM_USER_PASSWORD = "user_password";
    public final static String PARAM_ASSEMBLY = "assembly";
    public final static String PARAM_EXTRACT_CONTENT = "extract_content";
    public final static String PARAM_EXTRACT_ACCESSIBILITY = "extract_accessibility";
    public final static String PARAM_FILLING = "filling";
    public final static String PARAM_MODIFY = "modify";
    public final static String PARAM_MODIFY_ANNOTATIONS = "modify_annotations";
    public final static String PARAM_PRINTING = "printing";
    public final static String PARAM_DEGRADED_PRINTING = "degraded_printing";
 
    private ContentService contentService;
 
    public void afterPropertiesSet() throws Exception {
	Assert.notNull(contentService);
    }
 
    public void setContentService(ContentService contentService) {
	this.contentService = contentService;
    }
 
    @Override
    protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {
	ContentReader reader = contentService.getReader(actionedUponNodeRef,
		ContentModel.PROP_CONTENT);
	if (MimetypeMap.MIMETYPE_PDF.equals(reader.getMimetype())) {
	    PDDocument doc = null;
	    try {
		File newPdf = TempFileProvider.createTempFile("tmp", "protect");
		FileOutputStream fos = new FileOutputStream(newPdf);
 
		doc = PDDocument.load(reader.getContentInputStream());
 
		if (!doc.isEncrypted()) {
		    Boolean allowAssembly = (Boolean) action
			    .getParameterValue(PARAM_ASSEMBLY);
		    Boolean allowExtraction = (Boolean) action
			    .getParameterValue(PARAM_EXTRACT_CONTENT);
		    Boolean allowExtractionAccessibility = (Boolean) action
			    .getParameterValue(PARAM_EXTRACT_ACCESSIBILITY);
		    Boolean allowFillingInForm = (Boolean) action
			    .getParameterValue(PARAM_FILLING);
		    Boolean allowModifications = (Boolean) action
			    .getParameterValue(PARAM_MODIFY);
		    Boolean allowAnnotationModification = (Boolean) action
			    .getParameterValue(PARAM_MODIFY_ANNOTATIONS);
		    Boolean allowPrinting = (Boolean) action
			    .getParameterValue(PARAM_PRINTING);
		    Boolean allowDegradedPrinting = (Boolean) action
			    .getParameterValue(PARAM_DEGRADED_PRINTING);
 
		    AccessPermission ap = new AccessPermission();
		    ap
			    .setCanAssembleDocument((allowAssembly != null && allowAssembly
				    .booleanValue()));
		    ap
			    .setCanExtractContent((allowExtraction != null && allowExtraction
				    .booleanValue()));
		    ap
			    .setCanExtractForAccessibility((allowExtractionAccessibility != null && allowExtractionAccessibility
				    .booleanValue()));
		    ap
			    .setCanFillInForm((allowFillingInForm != null && allowFillingInForm
				    .booleanValue()));
		    ap
			    .setCanModify((allowModifications != null && allowModifications
				    .booleanValue()));
		    ap
			    .setCanModifyAnnotations((allowAnnotationModification != null && allowAnnotationModification
				    .booleanValue()));
		    ap.setCanPrint((allowPrinting != null && allowPrinting
			    .booleanValue()));
		    ap
			    .setCanPrintDegraded((allowDegradedPrinting != null && allowDegradedPrinting
				    .booleanValue()));
 
		    StandardProtectionPolicy policy = new StandardProtectionPolicy(
			    (String) action
				    .getParameterValue(PARAM_OWNER_PASSWORD),
			    (String) action
				    .getParameterValue(PARAM_USER_PASSWORD), ap);
		    doc.protect(policy);
		    doc.save(fos);
		    fos.close();
		    doc.close();
 
		    ContentWriter writer = contentService.getWriter(
			    actionedUponNodeRef, ContentModel.PROP_CONTENT,
			    true);
		    writer.putContent(newPdf);
		} else {
		    doc.close();
		    throw new RuntimeException(
			    "This document is already encrypted");
		}
	    } catch (FileNotFoundException fnfe) {
		fnfe.printStackTrace();
	    } catch (BadSecurityHandlerException bshe) {
		bshe.printStackTrace();
	    } catch (COSVisitorException cosve) {
		cosve.printStackTrace();
	    } catch (IOException ioe) {
		ioe.printStackTrace();
	    }
	}
    }
 
    @Override
    protected void addParameterDefinitions(List paramList) {
	paramList.add(new ParameterDefinitionImpl(PARAM_OWNER_PASSWORD,
		DataTypeDefinition.TEXT, true,
		getParamDisplayLabel(PARAM_OWNER_PASSWORD)));
	paramList.add(new ParameterDefinitionImpl(PARAM_USER_PASSWORD,
		DataTypeDefinition.TEXT, true,
		getParamDisplayLabel(PARAM_USER_PASSWORD)));
	paramList.add(new ParameterDefinitionImpl(PARAM_ASSEMBLY,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_ASSEMBLY)));
	paramList.add(new ParameterDefinitionImpl(PARAM_EXTRACT_CONTENT,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_EXTRACT_CONTENT)));
	paramList.add(new ParameterDefinitionImpl(PARAM_EXTRACT_ACCESSIBILITY,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_EXTRACT_ACCESSIBILITY)));
	paramList.add(new ParameterDefinitionImpl(PARAM_FILLING,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_FILLING)));
	paramList.add(new ParameterDefinitionImpl(PARAM_MODIFY,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_MODIFY)));
	paramList.add(new ParameterDefinitionImpl(PARAM_MODIFY_ANNOTATIONS,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_MODIFY_ANNOTATIONS)));
	paramList.add(new ParameterDefinitionImpl(PARAM_PRINTING,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_PRINTING)));
	paramList.add(new ParameterDefinitionImpl(PARAM_DEGRADED_PRINTING,
		DataTypeDefinition.BOOLEAN, false,
		getParamDisplayLabel(PARAM_DEGRADED_PRINTING)));
    }
 
}

Module

Encore une fois, nous vous proposons ici l’action sous la forme d’un module Alfresco, les sources complètes étant disponibles ici.

Personnalisation de Pentaho

Sylvain Decloix octobre 23rd, 2008

Dans le cadre d’un projet Pentaho pour un client du secteur médical, nous avons étendu et amélioré certaines fonctionnalités de base de la plate-forme, notamment en ce qui concerne la partie restitution Web.

Traduction française de la plate-forme

Atol CD a entièrement effectué la traduction de la plate-forme Pentaho 1.7 GA PCI (Pre-configured Installation).
L’ensemble des fichiers de traduction sont disponibles sur GoogleCode à cette adresse :
http://code.google.com/p/pentaho-french-translation

Un zip complet des fichiers traduits est téléchargeable dans l’onglet “Download”.

Merci à Sébastien, Nicolas et Bénédicte pour leur contribution… ;-)

Amélioration du rendu des Tableaux de bord

Depuis que nous mettons en place des dashboards et des rapports sur des projets BI avec Pentaho, nous avons souvent été frustrés par l’aspect graphique rudimentaire et “old school” proposé par JFree. ;-)

Bien heureusement, Pentaho est dans le domaine de l’Open Source, par définition on peut alors assez facilement se proposer d’enrichir l’outil.

C’est donc ce que nous avons décidé de faire en créant une nouvelle couche de restitution plus vivante et conviviale à l’aide d’un projet de rendu graphique Open Source: Open Flash Chart.

Voyons tout d’abord un exemple de tableau de bord réalisé nativement avec Pentaho (avec JFreeChart donc) :

Développement d’une couche de restitution web avec Open Flash Chart :

Puisque Pentaho est une plate-forme OSBI plutôt bien conçue, il est plutôt simple d’y ajouter un composant personnalisé (un “plugin” en quelque sorte…).

Ainsi, les graphiques OpenFlashChart que nous avons mis en place s’appellent de la même manière que les graphiques JFreeChart :

  • Un fichier xaction pour ramener les données
  • Un fichier xml pour décrire le graphique (le “widget”)
  • Une page jsp pour l’affichage du graphique

Une nouvelle classe java “OpenFlashChartHelper” a été développée afin que les données fournies par l’xaction puissent être chargées dans les graphes en Flash.

Le code de la page JSP ressemble à ceci :

<%
	        SimpleParameterProvider parameters = new SimpleParameterProvider();
	        parameters.setParameter( "drill-url", "SibDashboardFlash?gir={gir}" );
	        parameters.setParameter( "inner-param", "gir");
	        parameters.setParameter( "image-width", "500");
        	        parameters.setParameter( "image-height", "350");
	        StringBuffer content = new StringBuffer();
	        ArrayList messages = new ArrayList();
	        OpenFlashChartHelper.doFlashChart( "samples", "formation",
 		"synthese_gir.widgetFlash.xml", parameters,
		content, userSession, messages, null, 1 );
%>
		<%= content.toString() %>

La seule chose qui change par rapport au code standard de Pentaho appelant JFreeChart est la méthode d’appel. Ainsi, l’objet et la méthode appelante est OpenFlashChartHelper.doFlashChart().

Les attributs de cette méthode sont les mêmes que pour le code de JfreeChart, à part qu’un nouvel attribut a été ajouté : il s’agit du numéro de graphe dans la page ( il est utilisé seulement pour différencier les appels à la bibliothèques Flash )

Le widget xml ressemble à ceci :

<chart> 
	 <data> 
		<data-solution>samples</data-solution> 
		<data-path>formation</data-path> 
		<data-action>synthese_gir_data.xaction</data-action> 
		<data-output>rule_result</data-output> 
		<data-name>gir</data-name> 
		<data-value>nb_patients</data-value> 
		<data-orientation>columns</data-orientation> 
	</data> 	 
	<!-- 1 ou 2 suivant la version de OFC utilisé --> 
	<OFC-render>1</OFC-render> 
	<width>500</width> 
	<height>400</height> 
	<title> 
		<text>Nombre de patients par code GIR</text> 
		<style>{font-size: 20px; color:#736AFF}</style> 
	</title> 
	<bg-colour>#C548D1</bg-colour> 
	<elements> 
		<element> <!-- autre exemple de pie chart --> 
			<type>pie</type> 
	  		<colours> 
				<colour>#d01f3c</colour> 
				<colour>#356aa0</colour> 
				<colour>#C79810</colour> 
				<colour>#65a111</colour> 
				<colour>#f121f1</colour> 
			</colours> 
			<alpha>50</alpha> 
			<border>2</border> 
			<animate>0</animate> 
			<start-angle>45</start-angle> 
			<tip>GIR=%23x_label%23&lt;br&gt;%23val%23</tip> 
			<gradient-fill>true</gradient-fill> 
			<label-colour>#0000d0</label-colour> 
			<dynamic-tips>0</dynamic-tips> 
			<dynamic-links>0</dynamic-links> 
		</element> 
	</elements> 
</chart>

Au final, le tableau de bord en Open Flash Chart ressemble à ça :

Comme souvent les vidéos sont plus parlantes que des copies d’écrans.

Voici donc celle du dashboard Pentaho natif (Jfree) :

Et celle avec le dahsboard Open Flash Chart made by atolcd :

Atol, nouveau spot de Surf !

Thomas Broyer octobre 22nd, 2008

Mercredi dernier (15 octobre), j’étais à Munich pour apprendre à surfer !

Le cours était donné par la crème d’Alfresco : Michael “Uzi” Uzquiano (Director of WCM Products), soutenu par David “davidc” Caruana (Chief Architect) et Mike Farman (Director of ECM Products) ; et organisé par Nancy Garrity (Community Manager) en marge de la European Community Conference.

Architecture

Tout d’abord et contrairement à ce que je disais en mai dernier, Surf n’est pas une évolution des Web Scripts, mais une plate-forme de développement Web à part entière, qui tire parti des Web Scripts, mais pas uniquement (et pas même forcément). Au final, une application Surf reste globalement un ensemble de fichiers XML.

La plate-forme Surf est orientée page, composant et contenu, ce contenu pouvant provenir d’où bon vous semble : un entrepôt documentaire Alfresco bien entendu, mais là encore pas uniquement ni forcément (Alfresco Network par exemple est une application Surf qui n’utilise pas du tout Alfresco).

La notion de contenu ici est également particulière dans le sens où du contenu peut être fourni par les pages et les composants eux-même. Disons pour simplifier qu’un contenu provient d’une source de données externe à l’application. Oui oui, j’ai bien dit externe : une application Surf utilisant Alfresco –probablement le cas le plus courant– n’est pas tenue d’être déployée sur la même machine que l’entrepôt Alfresco, il y accède par des requêtes HTTP sur les Web Scripts mis à disposition et qui donnent accès à toutes les APIs disponibles en Java (ou presque), à savoir la gestion des utilisateurs, des catégories, des droits d’accès, des documents et espaces, etc. Et contrairement à ce qu’on pourrait croire, cette architecture a permis d’améliorer les performances de l’ensemble en forçant les développeurs à limiter le nombre de requêtes effectuées, et donc le nombre d’accès à la base de données et au file-system (et pour ceux qui testeraient l’application Share dans une Labs 3B, sachez que la version 3.0 Entreprise est quasi-identique en fonctionnalités mais avec de bien meilleures performances).

Les échanges avec les fournisseurs de contenus et/ou de services (endpoints) s’effectue au travers de connecteurs qui gèrent les interactions (HTTP, JDBC, SOAP, etc.) ainsi que le mode d’authentification (délégué à des authentifieurs). Des credential vaults s’occupent de gérer les différentes identités de l’utilisateur selon l’endpoint contacté, faisant office de Single-Sign On. Ainsi, même si ce n’est pas tout à fait opérationnel au moment où j’écris, Alfresco Network sert de point d’entrée unique, à authentification unique, pour les différentes applications d’Alfresco : forums, JIRA, support technique, wiki, etc.

Fonctionnement

Basiquement, dans Surf, une URL identifie une page, un type de page ou un contenu. Un type de page se résoud en une page selon le theme courant (par exemple, une page d’édition du profil utilisateur pourra être différente selon que l’utilisateur est un employé, un fournisseur ou un client). De même, un contenu se résoud en une page selon son type et les content associations (pour un contenu Alfresco, son type Alfresco : cm:content, cm:folder, etc. ce qui fait tout de suite penser au mécanisme de dispatch déjà présent dans le client JSF).

Nous avons donc une page. Celle-ci indique un niveau d’authentification requis (même principe que pour les Web Scripts) et référence un template instance. Ce template instance se contente la plupart du temps de donner le nom d’un template FreeMarker, dans lequel des directives particulières pourront être utilisées pour définir des régions dans lesquels viendront se loger les composants. Le rôle d’un template instance et de son template est de définir une présentation globale de la page (layout) et d’assembler des composants.

Chaque région est nommée et a une portée (globale, template instance ou page). C’est grâce à ces deux propriétés, complétées le cas échéant du nom du template instance ou de la page, que sont définis les component bindings, à savoir la configuration déterminant quel composant viendra se loger dans chaque région.

Le composant est la pièce maîtresse d’une application Surf, et correspond la plupart du temps simplement à un Web Script. L’API disponible pour ces Web Scripts est bien évidemment différente de celle des Web Scripts côté entrepôt Alfresco, et pour une fois est unifiée entre JavaScript et FreeMarker (et même Java d’ailleurs ; on parlera donc plutôt d’API Surf).

Enfin, il faut ajouter à cela les associations de pages qui permettent de définir de façon centralisée les liens entre les différentes pages de l’application (on construira donc les menus de navigation et autres “plan du site” de façon dynamique à partir de ces informations), les composants de chrome qui peuvent s’intercaler entre le template et le composant pour générer du code HTML complémentaire, à des fins de mise en forme principalement (dans un esprit de “thème graphique” donc), et les éléments de configuration, définissables à différents niveaux (global, composant, Web Script) et accessible par l’API Surf.

Tout ceci sans oublier les fonctionnalités nouvelles ou améliorées de dispatch sur URL (avec placeholders accessibles par l’API Surf, contrairement aux Web Scripts 2.1/2.2), internationalisation / régionalisation (I18N / L10N), négociation de contenu (voire de langue), le JavaScript d’un Web Script peut être différent selon le type de données envoyé par le client (pour les requêtes POST et PUT) avec en sus accès direct à ces données, éventuellement pré-parsées (cas de JSON ou XML par exemple), etc. et la possibilité de remplacer chaque composante du système : créateur de liens, page mapper, fabrique d’utilisateurs, etc.

Ah, j’oubliais encore : une application Surf étant un ensemble de fichiers XML, ils peuvent également être stockés dans un Alfresco, et plus particulièrement dans les AVM de WCM. Et Alfresco nous prépare un “studio” d’édition de sites WCM “à la volée” pour la version 3.1, ça promet !

Le mot de la fin

Et pour résumer tout ça, disons tout simplement que Surf est la brique qui nous manquait pour faciliter le développement d’applications métier gérant des documents : parapheur électronique, gestion de courrier, etc.

Alfresco - Insérer un filigrane sur les document PDF avec iText

Bertrand Magnier octobre 22nd, 2008

Après la conversion vers PDF/A, nous nous intéressons aujourd’hui à une autre fonctionnalité liée au format de fichier PDF implémentable dans Alfresco : l’insertion d’un texte en filigrane.

Cette insertion est rendue possible par un simple développement utilisant les API mises à disposition par Alfresco, la configuration du client web, et la librairie iText.

iText [en] est une API JAVA permettant de générer des documents PDF, ainsi que de modifier des documents PDF existants. Cette API est disponible gratuitement, sous licence MPL et LGPL.

Les actions dans Alfresco

Une action au sens Alfresco est une unité de travail, exécutée sur un document ou un espace, et que l’on peut actionner depuis l’interface standard (depuis la fiche de propriétés d’un élément, sélectionner « Lancer une action ») ou automatiser via les règles de gestion.

Il est possible, via une configuration, de mettre à disposition des utilisateurs une nouvelle action écrite en Java comme indiqué dans le wiki Alfresco [en]. Ce développement est facilité par les classes de base Alfresco, ce qui nous permet de nous consacrer pleinement à l’application du filigrane.

package com.atolcd.repo.action.executer;
 
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
 
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.util.TempFileProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
 
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;
 
public class PDFWatermarkActionExecuter extends ActionExecuterAbstractBase
	implements InitializingBean {
 
    public static final String NAME = "watermark";
    public static final String PARAM_TEXT = "watermark-text";
 
    private ContentService contentService;
 
    public void setContentService(ContentService contentService) {
	this.contentService = contentService;
    }
 
    public void afterPropertiesSet() throws Exception {
	Assert.notNull(contentService);
    }
 
    @Override
    protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {
	ContentReader reader = contentService.getReader(actionedUponNodeRef,
		ContentModel.PROP_CONTENT);
 
	if (MimetypeMap.MIMETYPE_PDF.equals(reader.getMimetype())) {
	    PdfReader pdfReader = null;
	    PdfStamper stamper = null;
	    try {
		pdfReader = new PdfReader(reader.getContentInputStream());
		File newPdf = TempFileProvider.createTempFile("tmp",
			"watermark");
		FileOutputStream fos = new FileOutputStream(newPdf);
		stamper = new PdfStamper(pdfReader, fos);
		BaseFont bf = BaseFont.createFont(BaseFont.HELVETICA,
			BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
		PdfContentByte under;
		int total = pdfReader.getNumberOfPages() + 1;
		for (int i = 1; i < total; i++) {
		    under = stamper.getUnderContent(i);
		    under.beginText();
		    under.setFontAndSize(bf, 18);
		    under.setTextMatrix(0, 0.5f, -0.5f, 0, 30, 30);
		    under.showText((String) action
			    .getParameterValue(PARAM_TEXT));
		    under.endText();
		}
		stamper.close();
 
		ContentWriter writer = contentService.getWriter(
			actionedUponNodeRef, ContentModel.PROP_CONTENT, true);
		writer.putContent(newPdf);
	    } catch (Exception e) {
		e.printStackTrace();
	    }
	}
    }
 
    @Override
    protected void addParameterDefinitions(List paramList) {
	paramList
		.add(new ParameterDefinitionImpl(PARAM_TEXT,
			DataTypeDefinition.TEXT, true,
			getParamDisplayLabel(PARAM_TEXT)));
    }
}

La classe PDFWatermarkActionExecuter est réduite à sa plus simple expression : une méthode addParameterDefinitions permettant de gérer les paramètres et une méthode executeImpl chargée de l’application de notre filigrane : un texte passé en paramètre de l’action, avec une position, une orientation, une police et une taille fixées.

Il serait possible d’améliorer cette action en permettant à l’utilisateur de paramétrer plus finement (police, taille, etc…) le filigrane, voire d’utiliser une image en complément (ou à la place) du texte.

Il est à noter qu’un autre développement est nécessaire afin d’obtenir une interface adaptée à la saisie dudit paramètre (encore une fois, direction le wiki[en]).

Module

L’action est ici proposée sous forme de module, facilement installable dans une instance Alfresco [en], comportant :

  • le code Java chargé de l’ajout du filigrane sur toutes les pages d’un document PDF,
  • le morceau d’interface permettant de passer en paramètre le texte que l’on souhaite insérer,
  • la configuration ajoutant l’action dans l’interface standard.

Télécharger le projet Eclipse du module.

Conversion PDF/A dans Alfresco

Thomas Broyer octobre 10th, 2008

Ce n’est pas un scoop, Alfresco permet de transformer les documents qu’il héberge de/vers différents formats. Pour les documents bureautiques, il s’appuie sur OpenOffice.org.

Depuis sa version 2.4 datant de mars dernier, OpenOffice.org permet non seulement d’exporter en PDF, mais supporte le format PDF/A-1. Cette variante de PDF 1.4 destinée à l’archivage à long terme, standardisé par l’ISO (ISO 19005-1), a été mis à jour en juillet afin d’apporter des précisions et corrections techniques, ce qui relance le buzz en cette période de rentrée.

Partant de ces 3 informations, on peut légitimement se demander :

Alfresco ne permettrait-il pas alors de générer des PDF/A-1 à partir de documents bureautiques ?

La réponse est oui, et ceci par une simple petite reconfiguration !

Il suffit – après mise en place de la version 2.4 ou supérieure d’OpenOffice, le cas échéant – d’éditer le fichier WEB-INF/classes/alfresco/mimetype/openoffice-document-formats.xml pour ajouter dans la section concernant le format PDF les 3 lignes suivantes (signalées en bleu) :

  <document-format><name>Portable Document Format</name>
    <mime-type>application/pdf</mime-type>
    <file-extension>pdf</file-extension>
    <export-filters>
      <entry><family>Presentation</family><string>impress_pdf_Export</string></entry>
      <entry><family>Spreadsheet</family><string>calc_pdf_Export</string></entry>
      <entry><family>Text</family><string>writer_pdf_Export</string></entry>
    </export-filters>
    <export-options>
      <entry><string>SelectPdfVersion</string><int>1</int></entry>
    </export-options>
  </document-format>

(Il est également possible de copier ce fichier pour le placer dans tomcat/shared/classes/alfresco/mimetype, et ainsi éviter tout écrasement lors de mises à jour d’Alfresco.)

Utilisé en coordination avec les règles de contenu, les comportements (behaviours ou policies dans le langage Alfresco), les scripts (scripts de l’entrepôt ou Web Scripts) et/ou les workflows, il devient donc possible d’automatiser l’archivage au format PDF/A-1 des documents bureautiques.

La contre-partie, bien évidemment, est que le format PDF/A-1 sera utilisé pour toutes les transformations vers le format PDF basées sur OpenOffice.

MVC Web

Thomas Broyer mai 13th, 2008

Un bon “MVC Web” doit garder à l’esprit que HTTP est partie prenante du contrôleur (au même titre que le navigateur Web également). La base est donc de faire du dispatch selon l’URL de la requête, ainsi bien entendu que la méthode HTTP utilisée (GET pour les requêtes safe, POST, PUT et/ou DELETE pour les actions, par définition unsafe). La seconde règle est de ne pas confondre redirection et vue: pour simplifier, si une action de suppression ne doit pas résulter en un écran “suppression effectuée”, la réponse HTTP devrait être une redirection (le navigateur va alors faire une requête GET sur cette autre URL, et ainsi récupérer par exemple une version “à jour” des données, ne contenant plus l’élément supprimé).

Quel que soit le framework employé, il ne faut pas perdre de vue que dans une application Web, la phase de conception est très importante, puisqu’elle doit définir les échanges client/serveur. Cette phase de conception est d’autant plus importante si on utilise de l’AJAX en complément des mécanismes HTML (liens et formulaires).

Le cas Ruby on Rails: une inspiration pour tous

Même si les développeurs de RoR n’ont rien inventé (beaucoup avaient déjà utilisé des patterns similaires), ils ont su imposer leur vision du MVC Web, totalement “Web style” (ce n’était pas le cas dans les premières versions de RoR cependant, où la méthode GET pouvait très bien être utilisée pour des actions unsafe, d’où de nombreux problèmes, notamment avec le Google Web Accelerator), et qui inspire quasiment tout le monde aujourd’hui: TurboGears et Django dans le monde Python, Symfony côté PHP, Phobos et Spring MVC en Java, et ASP.NET MVC pour la plateforme .NET.

Avec Rails, il est éventuellement possible d’utiliser le rendu d’une vue là où une redirection serait plus appropriée, mais ce fonctionnement demande au final plus de travail que de réaliser une redirection (dû au fait que les données –le modèle– doit être transmis à la vue –plus ou moins explicitement–).

À noter tout de même, RoR ne doit pas sa popularité qu’à son MVC, mais également à sa vision convention over configuration.

Le cas JSF: l’exemple à ne pas suivre

JSF est un mauvais MVC Web: les spécifications de JSF indiquent de toujours utiliser la méthode POST, et les outcomes n’utilisent jamais de redirection. En pratique, cela signifie que :

  • rien n’est jamais bookmarkable (utilisation de POST)
  • l’adresse apparaissant dans la barre d’adresse du navigateur est toujours “en retard” par rapport au contenu affiché (donc si on rafraîchit la page –F5, sans renvoyer les données POST–, le résultat sera différent).

Le problème est dans la conception même de JSF, et est en partie lié à son orientation composant: une URL est lié à une page JSP, dont les composants déterminent comment évaluer la requête POST, et appellent une voire plusieurs actions liées au composant ayant déclenché la requête ainsi qu’aux autres composants dont la valeur a été modifiée, la réponse du serveur étant le résultat d’une autre page JSP, qui n’a potentiellement rien à voir avec l’URL de la requête. Il n’y a donc pas de mapping URL/contrôleur, mais URL+requête-POST/action(s).

De plus, JSF est par essence totalement stateful, ce qui va à l’encontre des fondements du Web et de HTTP (pensez ReST).

Pour les curieux, le problème se situe dans le troisième bloc de la figure 4.2 du document Designing Enterprise Applications with the J2EE(TM) Platform, §4.4 Web-Tier Application Framework Design : la sélection de la vue “suivante”; et on nous indique que cette sélection dépend de la vue “courante”: il n’y a pas de “vue courante” en mode déconnecté comme avec HTTP, et la “vue” renvoyée n’est pas sensée être l’état de “l’application” (si tant est que ce concept existe) mais une représentation de la ressource identifiée par l’URL de la requête et/ou une représentation du résultat de l’action (avec le statut HTTP correct correspondant).

L’orientation “composants” de JSF fait que, de par sa conception, les vues vont chercher l’information nécessaire dans les composants (qui servent donc à la fois de modèle et/ou de contrôleur), alors que ça devrait être au contrôleur de donner l’ensemble des informations nécessaires (modèle) à la vue. Un autre gros souci de JSF est qu’il n’est vraiment pas aisé ni intuitif de faire des redirections en réponse à une requête, en tous cas lorsqu’on veut diriger vers un URL dynamique dépendant de l’action en cours.

Alfresco : de JSF aux Web Scripts

Alfresco a commencé avec un client JSF (qui par ailleurs reposait sur le pire de JSF), rapidement complémenté par quelques servlets (qu’on pourrait presque qualifier de hacks) permettant un accès direct à certains écrans.

Depuis la version 2.1, Alfresco nous a gratifié d’un nouveau mode de développement “léger” : les Web Scripts. Ce “framework” permet de développer des applications ReSTful (ou autre) dans Alfresco. Chaque Web Script se compose d’un fichier de description, d’un script (facultatif) et d’un ou plusieurs modèles de rendu.

  • Le fichier de description indique les URLs par lesquelles le Web Script est accessible (similaire à Routes de Ruby on Rails, mais avec une gestion décentralisée), le mode d’authentification (inexistant –guest–, n’importe quel utilisateur Alfresco, ou un administrateur uniquement), et depuis peu des options de cache HTTP de la réponse.
  • Le script (JavaScript) permet de valider les paramètres, modifier l’entrepôt de contenus, etc. et éventuellement changer le statut HTTP de la réponse (redirection, erreur, etc.)
  • Les templates (FreeMarker) correspondent aux représentations possibles, avec un template par format (HTML, JSON, Atom, etc.)

Alfresco utilise également le principe “convention over configuration” :

  • Tous ces fichiers ne sont liés entre eux que par leur nom. Celui-ci doit de plus respecter un format particulier et contenir la méthode HTTP gérée (un Web Script ne répond qu’à une méthode HTTP), ainsi que le format généré (pour les templates).
  • Le choix du format de la réponse (et donc du template utilisé) est réalisé par un paramètre format dans la query-string ou par un suffixe (à la façon des extensions de fichiers Windows).
  • Le script communique avec les templates à l’aide d’une variable globale model.
  • Si la réponse n’est pas 200 OK, le script peut demander à utiliser un autre template pour le rendu. Ce template doit faire partie des fichiers du Web Script mais contenir le code HTTP dans son nom. À défaut, un fichier nommé status.<code>.<format>.ftl sera utilisé, recherché également dans les répertoires parents. Au pire, un fichier status.<format>.ftl peut être placé à la racine des Web Scripts de façon à servir de catch-all.

Tout ceci est encore grandement perfectible (pas de génération d’URL pour un autre Web Script, à la façon des link_to de Ruby on Rails, matching des URLs simplistes) mais la base est là, et est remplaçable, comme tout composant d’Alfresco.

Alfresco migre d’ailleurs vers un client léger entièrement basé sur les Web Scripts pour sa future version 3.0, et a pour cela réécrit le framework (dont le matching des URLs) et l’a beaucoup enrichi par ailleurs (notion de composants, intégration de Yahoo! UI, etc.)

CMS : TYPO3 4.2

Fabrice Mouron mai 12th, 2008

Typo3

La nouvelle version de Typo3 est disponible en téléchargement.

Cette version majeure permet d’améliorer un certain nombre de points :

  • Révision de l’interface utilisateur de backend (menus, onglets dans les formulaires, etc)
  • Augmentation de la vitesse d’exécution des pages côté frontend
  • Suppression du type de page “Standard” au profit du type “Avancé”
  • Mise à jour de l’éditeur HTML (RTE)
  • etc …

Autre changement important, une uniformisation est apportée dans l’encodage de Typo3 qui devient désormais par défaut en UTF-8.

Coté développement, de nouvelles fonctions sont apparues (Typoscript, php). A noter, l’éditeur de gabarit permettant la complétion automatique ainsi que la coloration syntaxique du Typoscript.

Pour consulter la liste des modifications apportées, consulter le changelog.

Notre livre blanc “Les ETL Open Source”: quelques compléments

Sylvain Decloix avril 15th, 2008

La parution récente du livre blanc “Les ETL Open Source, une réelle alternative aux solutions propriétaires” sur notre site web a déjà suscité de nombreuses demandes d’informations, ainsi que quelques compliments sur le travail d’étude et de synthèse effectué par le pôle Business Intelligence d’Atol CD.

Les responsables des projets Pentaho Data Integration et Talend nous ont ainsi apporté plusieurs compléments et remarques très constructives sur leurs ETL respectifs, ainsi nous profitons de l’ouverture de notre blog technique pour diffuser ceux-ci.

Talend Open Studio :

Quelques précisions de la part de Fabrice Bonan, Directeur Général de Talend :

  • Depuis la version 2.3, la librairie de composants de Talend Open Studio inclut plus de 250 composants
  • Au travers du composant générique JDBC tJDBCSP, Talend peut appeler les procédures stockées de n’importe quel SGBD (comme Kettle)
  • Talend prend en charge complètement la norme JMS au travers des composants tMomInput et tMomOutput
  • Talend gère de façon native le transcodage par table de référence, les jointures hétérogènes ainsi que les jointures internes (left outer join mode), externe (right join) avec les modes “First Match”, “Last Match” et “All Matches” (produit cartésien). En mode ELT, T.O.S. supporte nativement toutes les jointures ANSI.
  • Des briques Open Source Commerciales permettent l’automatisation de la mise en production, la gestion de grappes de serveurs (grid computing, load balancing, gestion du fail over), la visualisation de l’historique et des stats de traitements des jobs (temps de traitement, erreurs, alertes).
  • Talend permet la définition de n’importe quel format de logs. Celles-ci peuvent être envoyées directement au composant tMap pour réorganiser les colonnes, les filtrer, etc…

-

Pentaho Data Integration (Kettle) :

Plusieurs échanges avec Matt Casters (le créateur de Kettle) ont permis d’affiner les tests comparatifs entre Kettle et Talend Open Studio, notamment les essais n° 4, 5 et 6 du livre blanc (pages 26 à 35).

En effet, dans ces tests, l’étape “Database Lookup” (qui sert à récupérer la description du produit dans la table [produits] à partir de son code) est loin d’être la plus performante.

Matt Casters suggère en effet d’utiliser un “Stream Lookup” qui permet d’effectuer la recherche directement sur les données montées en mémoire après un chargement via un “Table Output“.

Vous trouverez de plus amples informations sur les différentes étapes de lookup disponibles dans Kettle sur le wiki de pentaho.

Dans notre livre blanc, les temps de traitement obtenus avec Kettle pour le test 6 étaient très élevés en comparaison avec Talend Open Studio.

Ci-dessous, le graphe extrait de notre livre blanc indiquait un écart de performance très important pour 1 million de lignes traitées: 602 secondes pour Kettle contre 28 pour Talend.

En remplaçant l’étape “Database Lookup” par un “Stream Lookup”, les temps de traitement sont améliorés de façon très conséquente.

Pour 1 million de lignes, on passe ainsi de 602 à 88 secondes pour Kettle :

La transformation Kettle associée est la suivante :

-

Des gains de performance ont encore été obtenus avec la méthodologie suivante:

  • Augmentation de la taille de la JVM disponible (passage de 256Mo à 750Mo)
  • Utilisation de la dernière Release v3.0.3 de Kettle (merci à Matt pour la fourniture de cette version encore non disponible sur SourceForge)
  • Remplacement de l’étape JavaScript par 2 étapes successives finalement moins gourmandes en mémoire: ajout de constante + calcul

On obtient alors au final un temps de traitement de 42 secondes pour Kettle (toujours pour 1 million de lignes en entrée).

-

Pour conclure :

conclusion

42 secondes pour Kettle et 28 secondes pour Talend Open Studio pour réaliser un traitement d’intégration de données complexe sur un fichier CSV comprenant un million de lignes, voila qui devrait sans aucun doute interpeller les utilisateurs actuels d’ETL commerciaux… !

Comme nous l’avions déjà indiqué dans notre livre blanc, Kettle et Talend sont deux excellents ETL Open Source.

Maintenant, c’est à vous de vous faire votre idée en les testant pour vos propres besoins !

Pour vous guider, voici quelques liens pouvant vous être utiles :

Et bien sûr, l’équipe Business Intelligence d’Atol CD reste à votre disposition pour tout renseignement, conseil ou “Webex” sur ces 2 ETL Open Source.

Sylvain Decloix - 18 Avril 2008