Dreamisle.net Blog

Java, Rails, Security and so many ...

Hadopi Est Passée …

| Comments

Hier était un très mauvais jour pour la France et tout ce qui touche à l’internet et aux créations : Hadopi a été votée par les députés. Je ne vais pas refaire un long texte pour décrire pourquoi cette loi est anti-liberté et à mes yeux contraire à la démocratie, mais je tiens tout de même à exprimer publiquement mon avis à ce propos qui est que l’état commence sincèrement à partir dans le décor en imposant une loi dont ils ne comprennent absolument pas les implications techniques. Quand on repense au “Firewall OpenOffice” décrit par Albannel dans une interview … ça fait peur.

C’est beau l’incompétence.

Encore une loi mise en place par une bureaucratie sans aucune compétence pratique du domaine. Et cette fois dans le simple but de faire plaisir aux grands majors.

Ou est passé le liberté de la devise de la France ?

Quelques liens intéressant :

http://www.developpez.net/forums/d718546/bienvenue-club-developpeurs/taverne-privee-club-humour-divers/politique/etes-contre-nouvelle-loi-anti-piratage-hadopi-adoptee-12-05-a/

http://www.numerama.com/magazine/9854-10-bonnes-raisons-de-dire-NON-a-la-loi-Hadopi.html

http://www.developpez.net/forums/d576576/bienvenue-club-developpeurs/taverne-privee-club-humour-divers/politique/parlement-europeen-contredit-hadopi-vote-lamendement-bono/

Intégration d’Hibernate Search a Une Application Seam 2.1

| Comments

Introduction

De nombreuses applications Web nécessitent un moteur de recherche pour faciliter la recherche d’informations à vos utilisateurs.
Cependant, ce type d’outil peut vite être complexe a développer si l’on souhaite le rendre vraiment Full Text.

Qu’entends-t-on par Full Text?

Ils ‘agit en fait d’une technique de recherche dans document ou une base de donnée, nécessitant d’éxaminer tous les mots du texte. Néanmoins en raison des ambiguïtés du langage naturel une recherche Full Text aboutit souvent à des résultats peu pertinents.

Heuresement pour nous des solutions existent. Entre autre le bien connlu Lucene. http://lucene.apache.org/java/docs/.
Lucene est un moteur de recherche libre développé par Apache écrit en java qui permet d’indexer et de recherche dans le texte. Les indexs vont alors être gérés sous formes de fichiers très optimisés qui vont grandement accellérer vos recherches.
Enfin Lucene gère nativement la syntaxe de recherche type google, vous n’aurez pas donc pas à parser ce qui est entré par vos utilisateurs.

Hibernate Search est donc en quelque sorte la couche qui va vous permettre d’intégrer Lucene à votre application Seam, dont le contenu dynamique est géré par Hibernate (vers votre base de donée donc).

Je vous conseil de télécharger l’outil Luke (http://www.getopt.org/luke/) qui vas vous permettre de visualiser les indexs que nous allons créer plus tard dans ce tutoriel.

Configuration de votre application

Comme vous vous en doutez il ne suffit pas d’ajouter un jar dans un coin de votre application pour faire fonctionner ce système.
Il faut donc commencer par configurer Hibernate

Vous devez donc ajouter quelques éléments à votre persistence.xml.
Ajouter les lignes suivantes à la persistence-unit déjà configurée de votre application, en prenant soin de modifier le repertoire ou seront stockés les Indexs.

<!-- use a file system based index -->
<property name="hibernate.search.default.directory_provider"   
value="org.hibernate.search.store.FSDirectoryProvider"/>
<!-- directory where the indexes will be stored -->
<property name="hibernate.search.default.indexBase"  
value="/Users/leakim/Documents/Projects/JBoss/hibernateSearchIndex"/>
<property name="hibernate.ejb.event.post-insert" 
value="org.hibernate.search.event.FullTextIndexEventListener"/>
<property name="hibernate.ejb.event.post-update" 
value="org.hibernate.search.event.FullTextIndexEventListener"/>
<property name="hibernate.ejb.event.post-delete" 
value="org.hibernate.search.event.FullTextIndexEventListener"/>

Dependances

Nous allons maintenant ajouter les dépendances nécessaires, attention je vous précise ici la version d’hibernate pour une raison simple : il faut que les versions d’Hibernate et Hibernate Search soient compatibles entres elles ou vous aurez des ennuis.

Dans votre pom principal :

     <!-- Hibernate search -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-commons-annotations</artifactId>
       <version>3.0.0.ga</version>
     </dependency>
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-search</artifactId>
       <version>3.0.1.GA</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-core</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-analyzers</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-highlighter</artifactId>
       <version>2.3.0</version>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-snowball</artifactId>
       <version>2.3.0</version>
     </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>3.3.2.GA</version>
        <exclusions>
          <exclusion>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
          </exclusion>
        </exclusions>
      </dependency>

Dans le projet qui va implémenter le moteur de recherche :

    <!-- Hibernate Search dependencies -->
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-commons-annotations</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-search</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-core</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-analyzers</artifactId>
       <scope>provided</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-snowball</artifactId>
       <scope>provided</scope>
     </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <scope>provided</scope>
      </dependency>

Ici vous avez des dépendances supplémentaires, je les ai précisés pour ceux d’entre vous qui connaitraient déjà et souhaite utiliser Lucene SnowBall ou les Analyzer personnalisés. Nous ne rentrerons pas ici en détails sur ces éléments, je vous renvoi à la doc de Lucene pour cela


Déclaration d’entités pour l’indexation

Maintenant qu’Hibernate Search est installé, il va falloir indexer nos entités. Vous allez voir avec l’exemple que c’est très simple grâce aux annotations disponibles

/**
 * Entity which represent a user text entry.
 * @author leakim
 *
 */
@Entity
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Indexed
@Name("textEntry")
public class TextEntry implements Serializable {

    /**
     * uid.
     */
    private static final long serialVersionUID = 792694656532097709L;

    /**
     * Unique Id.
     */
    @Id @GeneratedValue
    @DocumentId
    private Long id;

    /**
     * Text Entry title.
     */
    @NotNull @Length(max = 70)
    @Field(index = Index.TOKENIZED)
    private String title;

    /**
     * Text Content.
     */
    @NotNull
    @Field(index = Index.TOKENIZED)
    @Basic(fetch = FetchType.LAZY)
    @Length(max = 10000)
    private String content;


    /**
     *
     */
    @NotNull
    private Date date = new Date();


    /**
     * The category.
     */

    @ManyToOne
    @IndexedEmbedded
    private Category category;

    /**
     * The status.
     */
    private TextEntryStatus status;
    
    /** Code retiré par soucis de lisibilité*/
/**
 * Entity intended to represent a category.
 * @author leakim
 *
 */
@Entity
@Table(name = "Category", uniqueConstraints = {
        @UniqueConstraint(columnNames = "name") })
@Indexed
public class Category implements Serializable {

    /**
     * ID.
     */
    @Id @GeneratedValue
    @DocumentId
    private Long id;


    /**
     * The name of the category.
     */
    @Length(max = 25)
    @Field(index = Index.TOKENIZED)
    private String name;
     /** Code retiré par soucis de lisibilité*/

Explications

  • @Indexed Indique à Hibernate que l’entité est Indexée.
  • @DocumentId Indique à Hibernate l’ID utilisé comme Document ID, autant Indexed que DocumentId sont strictement nécessaire si vous voulez indexer, sans cela ça ne marchera pas.
  • @Field(index = Index.TOKENIZED) Indique à Hibernate Search qu’il s’agit d’un champ indexé de type Tokenized, ce qui signifie en quelque sorte un champ multi-éléments.(Pensez au StringTokenizer …)
  • @IndexedEmbedded Ici l’annotaions porte bien son nom, il s’agit d’embarquer l’index de l’entité annotée
  • Si vous souhaitez faire une recherche dans une liste ou un set, il faudra utiliser @ContainedIn

Indexation

Voilà vos entités sont prêtes à être indéxés, pour cela on peut créer une petite classe utilitaire.
Ici je le fais à chaque démarrage de l’application car celle-ci est en cours de développement, mais ce pas forcémment nécessaire. Par exemple :

/**
 * Index Blog entry at startup.
 *
 * @author Mikael Robert
 */
@Name("indexerService")
@Scope(ScopeType.APPLICATION)
@Startup
public class IndexerService {

    /**
     * The logger.
     */
    @Logger
    private Log logger;

    /**
     * Hibernate Search Entity Manager.
     */
    @In
    private FullTextEntityManager entityManager;

    /**
     * Creation method called on application startup.
     */
    @SuppressWarnings("unchecked")
    @Create
    public void indexTextEntry() {
        List blogEntries = entityManager.createQuery("select be from TextEntry be").getResultList();
        entityManager.purgeAll(TextEntry.class);
        logger.debug("Indexing textEntries ....");
        for (Object be : blogEntries) {
            entityManager.index(be);
        }

        entityManager.purgeAll(Category.class);
        List categoryList = entityManager.createQuery("select category From Category category").getResultList();
        logger.debug("Indexing Categories....");
        for (Object category : categoryList) {
            entityManager.index(category);
        }

    }
}

Explications

Vous vouyez c’est relativement simple : on sélectionne tous les éléments de la table correspondant à l’entité à indexer, et on demander à Hibernate de les indexer.
Le purgeAll utilisé en début de chaque indexation indique de supprimer les indexs éxistants?

Recherche

Et maintenat l’élément attendu, la partie moteur de recherche

/**
 * @author leakim
 *
 */
@Name("searchService")
@Scope(ScopeType.PAGE)
public class SearchService {

    /**
     * logger.
     */
    @Logger
    private Log logger;

    /**
     * Hibernate fullTextEntityManager.
     */
    @In
    private FullTextEntityManager entityManager;

    /**
     * The search pattern.
     */
    private String searchPattern;

    /**
     * The searchs results.
     */
    private List<TextEntry> searchResults;


    /**
     * Construct the search results.
     *
     */
    public void  search() {
        if (searchResults != null) {
            logger.debug("Old result list size : #0", searchResults.size());
        }
        if (searchPattern == null || "".equals(searchPattern)) {
            searchPattern = null;
            searchResults =  null;
        } else {
            Map<String, Float> boostPerField = new HashMap<String, Float>();
            boostPerField.put("title", 3f);
            boostPerField.put("category.name", 2f);
            boostPerField.put("content", 1f);
            String[] productFields =  {"title", "content", "category.name"};

            QueryParser parser = new MultiFieldQueryParser(productFields, new StandardAnalyzer(), boostPerField);
            parser.setAllowLeadingWildcard(true);
            org.apache.lucene.search.Query luceneQuery = null;
            try {
                luceneQuery = parser.parse("*" + searchPattern + "*");
            } catch (ParseException e) {
                searchResults =   null;
            }

            searchResults =   entityManager.createFullTextQuery(luceneQuery, TextEntry.class)
                .setMaxResults(100).getResultList();
            logger.debug("Search count #0", searchResults.size());
        }
    }
}

Explications

  • Le champ de texte de recherche de la page web est mappé au champ searchPattern.
  • La partie correspondant à la construction du map boostPerField permet, lors d’une recherche multi champs, d’indiquer un “booster” à Hibernate Search. Cela va agir sur la “pertinence” du champ. On attribut une “note” indiquant l’importance du champ dans la recherche.
  • Les productFields sont donc les champs dans les quels Hibernate Search devra chercher.
  • Ici vous voyez donc que quand la luceneQuery parse le champ on ajoute “*” de chaque coté du searchPattern, syntaxe bien connue :)
  • StandardAnalyzer : je ne vais pas entrer dans les détails sur les Analyzer dans ce tutorial, mais lucene utilise des objets Analyzer pour analyser votre texte, cela vous permet avec les snowBall de grandement affiner la pertinence de vos résultats.
  • setAllowLeadingWildcard permet l’utilisation de * et ?.
La logique est donc :
  • Vérifier le pattern entré par l’utilisateur.
  • Instancier la recherche multi champs et les booster.
  • Parser le champ.
  • Lancer la recherche avec le fullTextEntityManager. Attention ! pas l’entityManager classique.
  • Récupérer la liste en retour de la fonction et l’afficher.

Conclusion

Et voilà vous avez votre moteur de recherche. Vous pourrez rapidement le constater, celui-ci est déjà très pertinent. Mais le gros apport est surtout au niveau de la performance, les indexs lucene garantissent une rapidité de parcours et de recherche assez ahurissante.
De plus vos requètes mettront systèmatiquement le même temps, quelque soit le nombre de ligne dans la ou les tables parcourues.

Changement De Serveur

| Comments

Le site a changé de serveur d’hébergement, si vous constatez des problèmes de liens, n’hésitez pas à me le signaler ! merci.

Developpez.com

| Comments

Un domaine vient de m’être ouvert sur developpez.com, je suis en attente de validation pour publier mes articles sur Seam sur le site. Donc prochainement vous pourrez avoir les articles au format developpez et pdf. Je continuerais bien sur à publier sur Dreamisle aussi.

Vos Idées

| Comments

Si vous avez des idées de tutoriaux qui vous paraîtraient pertinents, n’hésitez pas à me les soumettre, si c’est dans mes compétences je ferais des tutoriaux !

Système De Pagination en Lazy Loading Avec Seam, JPA Et Richfaces.

| Comments

Lorsqu’on gère de gros volumes de données, il devient vite essentiel de monter un système de pagination digne de ce nom. D’une part pour faciliter la visualisation par l’utilisateur, et d’une autre pour éviter l’explosion de la mémoire au chargement de vos données.

Nous allons ici apprendre à mettre en place un système de pagination qui va automatiquement gérer un cache mémoire correspondant uniquement aux données de la page courante visualisée.

Nous allons aussi, plutôt que de réinventer la roue utiliser le data:scroller de richfaces, pour gérer la navigation entre les pages.

Vous allez le voir ce système est basé sur la simplicité, j’ai toujours pensé que les systèmes les plus simples étaient les plus efficaces.

Dans le cas présent nous paginerons une liste de Text Entry (une entité simple pour persister du contenu utilisateur dans un de mes projets), mais ce système est applicable à n’importe quel type d’entité correctement écrite.

J’ai souvent vu des systèmes de pagination, mais dans beaucoup de cas, on ne fait que gérer l’affichage page par page côté utilisateur avec un simple dataScroller, et aucun code derrière pour gérer la persistance des données en mémoire

JSF

Commençons par voir la JSF qui affiche notre liste : Vous le voyez ici c’est très simple; on a une dataTable pour afficher la liste des données, et on y a adjoint un rich:dataScroller pour paginer. La subtilité dans cette JSF reside seulement dans la mise en place de la variable page pour le datascroller.

<h:form>
<rich:datascroller for="textEntryListTable" page="#{textEntryList.currentPage}"/>
<rich:dataTable id="textEntryListTable" value="#{textEntries}"  var="entry" style="border-style: none;"  rows="5">
    <rich:column style="border-bottom: 0px; border-right: 0px;">
     <rich:panel>
      <f:facet name="header"><h:outputText value="#{entry.getFormatedTitle()}"  /></f:facet>
      <h:outputText value="#{entry.content}" escape="false" />
     </rich:panel>
     <rich:spacer height="25"/>
     </rich:column>
  </rich:dataTable>
</h:form>

Lazy List

On a donc besoin d’un composant qui permet de gérer un cache mémoire, de taille variable (le nombre d’élément voulu par page) et qui ne charge QUE les données de la page courante. Cet objet devra donc travailler conjointement avec une liste classique pour faire son travail de manière quasi transparente. Voyons un peu le code de cet objet.

/**
 * @author mikael.robert
 * This list load data with lazy method to avoid problem of memory for big data list.
 * @param <T> the object type.
 */
public class LazyList<T> extends AbstractList<T> {

    /**
     * logger.
     */
    @Logger
    private Log logger;

    /**
     * The query.
     *
     * */
    private Query query;

    /**
     * cache for loaded datas.
     * */
    private Map<Integer, T> loaded;

    /**
     *  total number of results.
     *  */
    private long numResults;

    /**
     * currentPage.
     */
    private int currentPage;


    /**
     * the page size.
     * */
    private int pageSize;

    /**
     * constructor.
     * */
    public LazyList() {
        loaded = new HashMap<Integer, T>();
    }

    /**
     * Create a LazyList backed by the given query, using pageSize results
     * per page, and expecting numResults from the query.
     * @param query the query to get the datas.
     * @param pageSize the page size wanted.
     * @param numResults the number of results of the dataset.
     * @param currentPage the currentPage.
     */
    public LazyList(Query query, int pageSize, long numResults, int currentPage) {
        this();
        this.query = query;
        this.pageSize = pageSize;
        this.numResults = numResults;
        this.currentPage = currentPage;
    }

    /**
     * Fetch an item, loading it from the query results if it hasn't already
     * been.
     * @param i the first result.
     * @return object the object.
     */
    @SuppressWarnings("unchecked")
    public T get(int i) {
          if (!loaded.containsKey(i)) {
            List<T> results = (List<T>) query.setFirstResult(i).setMaxResults(pageSize).getResultList();
            for (int j = 0; j < results.size(); j++) {
                loaded.put(i + j, results.get(j));
            }
        }
        return loaded.get(i);
    }

    /**
     * Return the total number of items in the list. This is done by
     * using an equivalent COUNT query for the backed query.
     * @return size the size.
     */
    public int size() {
        return (int) numResults;
    }

     /**
      * update the number of results expected in this list.
      * @param numResults the number of results total.
      * */
    public void setNumResults(long numResults) {
        this.numResults = numResults;
    }

    /**
     * get the loaded cache size.
     * @return size the size.
     */
    public int getLoadedSize() {
        return loaded.size();
    }
}

Comme vous pouvez le voir cet objet dérive de AbstractList : au lieu d’utiliser un ArrayList ou autre, on utiliser celui-ci, mais de la même manière qu’une autre list. Ainsi le développement est transparent. Cet objet possède quelques attributs :

  • Query query; La requète de récupération des données passée à l’objet lors de la construction.

  • Map<Integer, T> loaded; Le cache mémoire.

  • long numResults; Le nombre total de données présentes en base.

  • int currentPage; La page courante.

  • private int pageSize; La taille d’une page.

Tout d’abord certains puristes dirons que ce n’est pas génial de conserver la requête JPA dans l’objet. Personnellement je pense que ce n’est pas un problème car il sera accédé par un objet interface entre lui et le client ce qui limite fortement les risques. C’est le code de la méthode surchargée get qui est important ici. Il est relativement simple, mais c’est lui qui va permettre de ne charger que ce qui est nécessaire, et permettre l’utilisation du cache pour ne pas recharger ce qui l’est déjà.

Grosso modo, lorsque le composant rich:dataTable utilisé dans la partie JSF de ce billet, va faire appel au get d’un élément, si celui-ci n’est pas déjà chargé alors on le récupère. Cette récupération est faite un utilisant la requête a la quelle on fixe comme premier résultat, l’indice courant. On fixe à la requête un nombre maximum de résultat correspondant à la taille de page. Ainsi on ne récupère que les éléments de la page courante. Cependant, il y a un seul petit hic à ce fonctionnement du à priori au rich:dataScroller. Je n’ai pas vraiment compris pourquoi, mais j’ai constaté, que j’avais toujours deux pages chargées dans mon cache. Je suppose fortement que c’est le composant rich:dataScroller qui joint à une dataTable, précharge deux pages de données pour accélérer la navigation. En effet j’ai refait un dataScroller pour des tests et là je n’avais qu’une page en mémoire. Si quelqu’un a plus d’infos la dessus je suis interessé ! Néanmoins ce n’est pas bien génant d’avoir deux pages en cache.

Enfin, ceux qui ont bien compris le fonctionnement vont se dire qu’on accumule tout ce qu’on charge au fur et à mesure dans le cache. En fait c’est dans l’utilisation de la lazy list qu’on va gérer ça, je vous propose de passer à la partie suivante pour mieux comprendre.

Gestion de la liste

Vous allez donc pouvoir utiliser votre lazy list. Dans un précédent article j’avais expliqué le fonctionnement de la Factory, de DataModel et de DataModelSelection. Nous allons ici les réutiliser, mais conjointement à un autre composant que nous verrons un peu après, qui sera lui chargé de gérer la mémoire.

On a donc ici une gestion de liste quasiment identique à qu’on peut faire d’habitude avec Seam. Comme d’habitude j’ai enlevé les getters/setters pour ne pas surcharger le code, mais ils sont nécessaires. Jetons donc un oeil à ce code :

/**
* dreamisle-cms
* Copyright (C) 2009 Mikael Robert
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package org.dreamisle.services.blog;

import java.util.List;

import javax.ejb.Remove;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;

import org.dreamisle.entities.Comment;
import org.dreamisle.entities.TextEntry;
import org.dreamisle.services.utils.LazyList;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.End;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.annotations.datamodel.DataModelSelection;
import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.international.StatusMessage.Severity;
import org.jboss.seam.log.Log;

/**
 * text Entry list. Seam Query Components.
 * @author leakim
 *
 */
@Name("textEntryList")
@Scope(ScopeType.CONVERSATION)
public class TextEntryList {

    /**
     * The page Size.
     */
    public static final int PAGE_SIZE = 5;

    /**
     * The currentPage.
     */
    private int currentPage  = 1;

    /**
     * The logger.
     */
    @Logger
    private Log logger;

    /**
     * Entity manager.
     */
    @In
    private EntityManager entityManager;

    /**
     * The result list.
     */
    @DataModel
    private List<TextEntry> textEntries;

    /**
     * The current text entry.
     */
    @Out(required = false)
    @DataModelSelection
    private TextEntry textEntry;

    /**
     * Init method.
     */
    @Create
    @Begin
    public void init() {
        logger.debug("Create Text entry list manager.");
    }

    /**
     * return the result list.
     *
     *
     */
    @Factory("textEntries")
    public void initTextEntries() {
        logger.debug("REINITIALIZE LIST");
        String ejbQl =  "from TextEntry textEntry order by textEntry.date asc";
        StringBuilder ejbQLStd = new StringBuilder("select textEntry ");
        StringBuilder ejbQLCount = new StringBuilder("select count(textEntry) ");
        ejbQLCount.append(ejbQl);
        ejbQLStd.append(ejbQl);
        try {
            Query query = entityManager.createQuery(ejbQLStd.toString());
            long count = (Long) entityManager.createQuery(ejbQLCount.toString()).getSingleResult();
            textEntries = new LazyList<TextEntry>(query, PAGE_SIZE, count, currentPage);
        } catch (NoResultException ex) {
            logger.error("No results found in text entry");
            FacesMessages.instance().add(Severity.WARN, "No results found in text entry");
        }
    }

    /**
     * Destroy method.
     */
    @End
    @Destroy
    public void destroy() {

    }
}

Comme vous avez pu le voir : - Cet objet est maintenu en conversation, afin de permettre de conserver l’état de l’objet ( ici la page courante et le cache mémoire). - J’ai mis en place une méthode annotée @Create, pour pouvoir forcer le début de la conversation lors de la création de l’objet. Idem pour le @Destroy + @End, ainsi on est sur et certain que la conversation commence et termine quand on le souhaite, sans perte de l’état. - Nous avons une constante qui correspond à la taille de la page, et une variable currentPage qui correspond à la page courante visualisée par l’utilisateur. - La lazy list est reconstruite à chaque utilisation de la Factory : à chaque changement de page. Pour initializer le nombre maximum de résultats (le numResults de la LazyList) on réalise la mère requète mais avec un count, beaucoup plus rapide à éxécuter qu’une recupération de données. On passe à la Lazy List la taille de la page, et la page courante. Ici on n’utilise pas la page courante, mais je l’ai laissée car j’ai un projet ou a la lazylist a des méthodes utilaitres qui en ont besoins. A vous d’améliorer la Lazylist selon votre besoin.

Comme vous le voyez ce système n’est pas très compliqué, mais est efficace. Ce n’est probablement pas le plus efficace ni la meilleure façon de faire, mais c’est très rapide à mettre en place, et très simple à réutiliser.

Voilà j’espère que ce billet vous aura été utile.

Création D’un Composant Facelet Personnalisé Avec Facelets JSF Et Richfaces

| Comments

Exemple de création de composant facelets

Si vous utilisez JSF, vous avez du remarquer qu’on fait souvent des assemblages similaires de composants.

Parfois l’idée vous passe par la tête de créer votre propre composant JSF pour remplacer ces assemblages redondants, mais créer un composant JSF est long et difficile . La difficulté réside notamment dans la gestion du cycle de vie JSF du composant que vous créer. Bien heureusement il existe une solution alternative grâce à Facelets En effet vous pouvez créer un assemblage de composant JSF qui deviendra alors un composant en soit, mais un composant Facelet cette fois. Ce qui ne changera rien dans son utilisation.

Le but ici va donc être de créer une list box qui peut recevoir n’importe quoi dans la liste déroulante, dans mon cas j’avais besoin de listes ou d’arbre de checkbox avec label. Donc d’utiliser dans ma liste déroulante, soit une rich:dataTable soit un rich:tree.

On souhaite arriver à un composant générique qui permettrait de faire ce genre de chose : [caption id=”attachment_209” align=”alignnone” width=”300” caption=”Le composant avec une rich:dataTable”]Le composant avec une rich:dataTable[/caption]

[caption id=”attachment_210” align=”alignnone” width=”300” caption=”Le composant avec un rich:tree”]Le composant avec un rich:tree[/caption]

Les étapes

Créer un composant Facelets requiert trois étapes :

  • Création d’un fichier de taglib pour pouvoir le déclarer à facelets et l’utiliser.
  • Déclaration du fichier de taglib à facelets dans le web.xml
  • Création du composants dans un fichier xhtml

Configuration

Le plus simple pour intégrer les composants que vous créer à votre WAR est de les placer dans le repertoire WEB-INF Par exemple j’ai créer un repertoire components dans WEB-INF,
Voyons maintenant la configuration du composant, tout d’abord dans web.xml : On indique à Facelets ou alors chercher les composants additionnels dans WEB-INF/facescomponents.taglib.xml :

    <context-param>
    <param-name>facelets.LIBRARIES</param-name>
    <param-value>/WEB-INF/facescomponents.taglib.xml</param-value>
    </context-param> 

Taglib

Ensuite, comme vous pouvez le voir précédemment on créer un fichier taglib pour notre composants : voici le fichier facescomponents.taglib.xml :

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://www.dreamisle.net/facescomponents</namespace>
  <tag>
    <tag-name>selectListCheckBox</tag-name>
    <source>components/selectListCheckBox.xhtml</source>
  </tag>
</facelet-taglib>

Création du composant

On peut maintenant créer le composant : J’ai laissé la partie CSS/Javascript volontairement au cas ou vous souhaitiez réutiliser ce composant. Tout ce qui correspond au position et au z-index est néanmoins important pour permettre la création de la pseudo liste dédroulatne. Le javascript (ici fait avec jQuery) est utilisé pour enrouler/dérouler la liste, et pour compter les checkbox comptés dans le contenu. Il s’agit d’un exemple de comportement javascript que l’on peut attribuer à un composant, à vous d’adadapter à votre choix. J’ai choisi d’utiliser jQuery pour plus de portabilité. De plus la librairie permet d’éviter de polluer le code jsf d’appels javascript sur les “onclick”, “onchange” etc … Vous pouvez bien sur définir avec ce que vous voulez le comportement de votre composant. Voir même avec un objet Seam appellé en ajax, mais cela le rend inutilisable en dehors de votre contexte Seam.

Le système d’id est un peu compliqué, en fait on créer nos id à partir de l’id du composant crée dans la JSF, pour éviter les erreurs de duplicate ID. Donc on a des id avec des EL la dedans, mais ne vous formalisez pas de cela. Cela complexifie la lecture du code jQuery et JSF mais c’est une façon simple d’éviter le duplicate ID si vous inserez plusieurs fois le composant dans la même page.

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:s="http://jboss.com/products/seam/taglib"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:rich="http://richfaces.org/rich"
    xmlns:a4j="http://richfaces.org/a4j">
    
    <!-- Style-->
          <style>

            /* on met le composant en position relative, pour pouvoir placer la liste déroulante au dessus en absolute après*/
            .divLayoutSelectListCheckBox {
              position: relative;
            }
            /* la largeur totale du composant*/
            .dataGridLayoutSelectListCheckBox {
              width:350px
            }
            /* le champ input simulant une select box */
            .idropDownSelectListCheckBoxInput {
                width:332px;
                margin:0px;
                z-index: 2;
            }
            /* uniquement pour que la rich:dataTable n'ai pas de bordures. */
            .dataTableDropDownList {
              width: 350px;
              border-style: none;
              background-color: #EEF5FE;
            }
            /* uniquement pour que les colonnes de la rich:dataTable n'aient pas de bordures. */
            .dataTableDropDownListColumn {
              border-bottom: 0px;
              border-right: 0px;
            }
            /* le div de la liste en elle meme. */          
            .dropDownSelectListCheckBox {
              height: 202px;
              overflow: auto;
              background-color: #EEF5FE;
              border: 1px solid #CAC5BE;
            }
            /* on fixe en position absolue et avec un z-index important  le div entourant la liste deroulante pour qu'elle passe au dessus du reste*/           
            .fullListDropDownCheckBox {
              position: absolute;
              z-index: 5;
            }
            /* Le pied de la liste deroulante*/         
            .dropDownSelectListCheckBoxFooter {
              background-color: #BED6F8;
              border: 1px solid #CAC5BE;
            }

            </style>
            <!-- Javascript for the component behaviour-->
            <a4j:loadScript src="resource://jquery.js"/>
            <script type="text/javascript">
                    jQuery(document).ready(function() {
                        /* Par défaut on cache la liste deroulante */
                        jQuery("##{id}magicBoxList").hide();


                        /* Si le parametre footer est a faux on cache le footer*/
                        if (!jQuery(#{footer})) {
                            jQuery("##{id}footerListBox").hide();
                        }
                        /* On remplis la valeur du champ input par defaut */
                        jQuery("##{formId}\\:#{id}statesinput").attr("value", "0" + " " + '#{selectedLabel}');
                        
                        /* Un click dans l'input cache ou montre la liste*/
                        jQuery("##{formId}\\:#{id}statesinput").click(function() {
                            if (jQuery("##{id}magicBoxList").is(":hidden")) {
                                jQuery("##{id}magicBoxList").show();
                              } else {
                                  jQuery("##{id}magicBoxList").hide();
                              }
                        });
                        /* Un click sur la fleche cache ou montre la liste.*/
                        jQuery("##{formId}\\:#{id}showandhidelinkid").click(function() {
                            if (jQuery("##{id}magicBoxList").is(":hidden")) {
                                jQuery("##{id}magicBoxList").show();
                              } else {
                                  jQuery("##{id}magicBoxList").hide();
                              }
                        });


                        /* Deselectionne toutes les checkbox lorsqu'on clique sur le lien correspondant*/
                        jQuery('##{formId}\\:#{id}deselectAllButtonFooter').click(function(){
                            jQuery("##{id}dropListContainer input[@type='checkbox']").attr('checked', false);
                            jQuery("##{formId}\\:#{id}statesinput").attr("value", "0 " + '#{selectedLabel}');
                         });

                        /*  Met  à jour le compteur de cases cochés à chaque click sur une checkbox  */
                         jQuery("##{id}dropListContainer input[@type='checkbox']").click(function() {
                             count = jQuery("##{id}dropListContainer  input[@type='checkbox']:checked").length;
                             if (count > #{maxCheck}) {
                                jQuery("##{formId}\\:#{id}statesinput").attr("value", "#{maxSelectMessage}" + "  " + "#{maxCheck}" + " éléments");
                                jQuery(this).attr('checked', false);
                             } else {
                                 jQuery("##{formId}\\:#{id}statesinput").attr("value", count + " " + '#{selectedLabel}');
                             }
                         });
                    });
            </script>

<!-- le code proprement dit du composant -->
        <h:panelGrid columns="2">
            <s:div styleClass="divLayoutSelectListCheckBox" id="#{id}idPanelGridForFc">
                <h:panelGrid columns="2" border="0" cellpadding="0" cellspacing="0" 
                  styleClass="dataGridLayoutSelectListCheckBox" >
                    <!-- on immite une select box avec un inputText desactivé et une image de fleche -->
                    <h:inputText id="#{id}statesinput" readonly="true" 
                       styleClass="idropDownSelectListCheckBoxInput" />
                    <a4j:commandLink id="#{id}showandhidelinkid">
                      <h:graphicImage value="/img/arrow.png" alt="&gt;"/>
                    </a4j:commandLink>
                  </h:panelGrid>
                  <h:panelGroup>
                  <!-- la liste deroulante-->
                  <div class="fullListDropDownCheckBox" id="#{id}magicBoxList">
                  <div class="dropDownSelectListCheckBox" id="#{id}dropListContainer">
                  <!-- Le contenu de la liste deroulante sera inséré ici-->
                    <ui:insert />
                  </div>
                  <!-- On ajoute un footer a la liste déroulante avec un lien  "tout deselectionner"-->
                  <div class="dropDownSelectListCheckBoxFooter" id="#{id}footerListBox">
                    <a4j:commandLink id="#{id}deselectAllButtonFooter">
                      Tout désélectionner
                    </a4j:commandLink>
                  </div>
                  </div>
                </h:panelGroup>
            </s:div>
         </h:panelGrid>

</ui:composition>

Comme vous pouvez le voir, on a créer le composant comme on ferait une page jsf classique, la différence réside uniquement dans le fait qu’on créer une composition.

L’élément important ici est le <ui:insert />
C’est ici que sera placé le contenu de la liste déroulante, remplie par l’utilisateur du composant dans une page JSF.

Autre élément à noter: les EL utilisés dans le composant. Par exemple ici #{id} , il s’agit en fait de récuperer les paramètres, passés au composant de manière classique dans la JSF, pour mieux comprendre lisez la partie “utilisation du composant” vous allez tout de suite voir que les paramètres déclarés au composant, sont récupérable sous forme d’el avec la même casse dans l’EL, ainsi votre composant est facilement paramétrable.

{formId}, #{maxSelectMessage}, #{maxCheck}, #{footer}

et #{selectedLabel} ne sont utilisés que dans les fonctions de comportement jQuery du composant. Vous voyez que vous pouvez même utilisez les paramètres pour définir le comportement du composant avec javascript.

J’ai choisi pour l’exemple d’utiliser des a4j:commandLink lié à des appels javascript (déclarés dans la partie javascript) mais il y a d’autres façon de faire.

Utilisation du composant

Et voici maintenant un exemple d’utilisation qui permet d’arriver aux captures au début de cet article :

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:s="http://jboss.com/products/seam/taglib"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:rich="http://richfaces.org/rich"
    xmlns:a4j="http://richfaces.org/a4j"
    xmlns:fc="http://www.meteojob.com/facescomponents"
    template="layout/template.xhtml">

  <ui:define name="body">
    <rich:panel>
    <f:facet name="header"> select test </f:facet>
    <h:form id="formRichListId">
    <!-- ici une liste avec en label une chaine de caracteres, et devant le label une checkbox -->
      <fc:selectListCheckBox id="fcSelectList" formId="formRichListId" selectedLabel="éléments choisis" 
         maxCheck="4" maxSelectMessage="Vous devez choisir au maximum:" footer="true">
              <rich:dataTable value="#{dreamSelectBox.theList}" var="item" 
                 styleClass="dataTableDropDownList" >
                 <rich:column styleClass="dataTableDropDownListColumn">
                    <h:selectBooleanCheckbox value="false" name="checkboxRichList" />
                 </rich:column>
                 <rich:column styleClass="dataTableDropDownListColumn">
                    <h:outputText value="#{item}" escape="false" />
                  </rich:column>
              </rich:dataTable>
      </fc:selectListCheckBox>

      <rich:spacer height="50" />
    <!-- ici un arbre avec aussi des checkbox, j'ai 
        repris l'arbre donné en exemple dans la démo en ligne de richfaces -->
      <fc:selectListCheckBox id="fcSelectTree" formId="formRichListId" 
         selectedLabel="éléments cochés" 
         maxCheck="4" maxSelectMessage="Vous devez choisir au maximum:" footer="true">
              <rich:tree style="width:350px;" nodeSelectListener="#{dreamSelectBox.theTree.processSelection}"
                reRender="selectedNode" ajaxSubmitSelection="true"  switchType="client"
                value="#{dreamSelectBox.theTree.getTreeNode()}" var="item" ajaxKeys="#{null}" >
              <rich:treeNode>
                <f:facet name="iconLeaf"><h:selectBooleanCheckbox value="false"/></f:facet>
                <h:outputText value="#{item}" />
              </rich:treeNode>
              </rich:tree>
      </fc:selectListCheckBox>

      </h:form>
    </rich:panel>
</ui:define>

</ui:composition>

Et voilà, vous avez votre composant, j’espère que cet article aura été utile. N’hésitez pas à commenter si vous avez des questions ou des remarques constructives.

Système D’authentification Simple Avec Seam.

| Comments

Beaucoup d’applications Web nécessitent une partie privée et sécurisée. Pour cela il faut donc une page d’authentification et le code qui va derrière pour la gérer. Ce billet va donc vous permettre de développer un système d’authentification simple, basé sur les composants mis en place par Seam à cet effet, que vous pourrez complexifier par la suite.

On va donc utiliser deux composants Seam à cet égard : Identity et Credentials. Ce que vous devez retenir c’est que :

  • Credentials contient les données de l’utilisateur.
  • Identity contient les méthodes pour l’authentification et connaître l’état de l’utilisateur (loggé ou non etc …)

Ces deux composants sont stockées en session, ce qui leur donne une visibilité large dans l’application.

Configuration

Tout d’abord il faut mettre en place un tout petit peu de configuration.

Premièrement dans le fichier components.xml de votre projet voici les lignes nécessaires pour utiliser les composants Seam liés à la sécurité et à l’authentification.

  
<drools:rule-base name="securityRules">
<drools:rule-files>  
       <value>/security.drl</value>  
</drools:rule-files>  
</drools:rule-base>  
<!-- <security:rule-based-permission-resolver security-rules="#{securityRules}"/> -->  
<security:identity authenticate-method="#{authenticator.authenticate}" remember-me="true"/> 

La première partie : drools:rule-base name="securityRules" correspond à la déclaration à Drools du fichier de configuration Security.drl, il est utilisé si vous utilisez des règles type Drools pour l’authentification, autrement laissez le presque vide :

package Permissions;

import java.security.Principal;

import org.jboss.seam.security.permission.PermissionCheck;
import org.jboss.seam.security.Role;

On peut voir que security:rule-based-permission-resolver est commenté car nous n’utiliserons pas drools ici pour gérer la sécurité. Enfin la dernière ligne est la plus intéressante : elle permet de déclarer la méthode d’authentification au composant Identity de Seam. On devra donc ici avoir un Objet nommé authenticator (annoté @Name("authenticator"))

Voilà pour la configuration.

JSF

Passons maintenant aux choses sérieuses : le code. Commençons par le côte vue : la jsf. Voici ce que ça donne (soit dit en passant il s’agit de la page générée par le seam gen.)

 
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<ui:composition xmlns="http://www.w3.org/1999/xhtml" 
xmlns:s="http://jboss.com/products/seam/taglib" 
xmlns:ui="http://java.sun.com/jsf/facelets" 
xmlns:f="http://java.sun.com/jsf/core" 
xmlns:h="http://java.sun.com/jsf/html" 
xmlns:rich="http://richfaces.org/rich" 
template="layout/template.xhtml"> 
<ui:define name="body">  
  <h:form id="login"> 
    <rich:panel>  
    <f:facet name="header">Login</f:facet> 
    <p>Veuillez vous identifier.</p>  
     <div class="dialog">  
       <h:panelGrid columns="2" rowClasses="prop" columnClasses="name,value"> 
          <h:outputLabel for="username">Nom d'utilisateur:</h:outputLabel>  
          <h:inputText id="username" value="#{credentials.username}"/>  
          <h:outputLabel for="password">Password</h:outputLabel>  
          <h:inputSecret id="password" value="#{credentials.password}"/>  
       </h:panelGrid>  
     </div>  
   </rich:panel> 
   <div class="actionButtons">  
      <h:commandButton value="Login" action="#{identity.login}"/>  
   </div>  
   </h:form> 
 </ui:define> 
</ui:composition> 

Ici il n’est pas nécessaire de rentrer dans les détails … Il s’agit d’un formulaire tout à fait classique avec richfaces histoire de le rendre un peu joli. Simplement nous mappons notre champ login a credentials.username et notre champ password à credentials.password. Puis le bouton de submit déclenche l’action identity.login que nous avons - rappellez vous- configuré dans le components.xml.

Et n’oublions pas : il faut définir une “règle de redirection”, lorsqu’on va submiter le formulaire, il faut savoir comment rediriger l’utilisateur en fonction du retour de la fonction. Ici on décide de mettre une règle dans le cas ou identity.login retourne true, c’est le cas ou l’utilisateur est loggé et on le redirige sur une page de notre choix. On ne met aucune règle sur le cas “false” ce qui permet à la page de login d’être rechargée tant que l’utilisateur n’est pas loggé. On créer donc un fichier login.page.xml si votre page de login s’appelle login.xhtml

Ejb Entity

Passons maintenant au code côté serveur.

On va donc avoir besoins de deux ejbs entity : l’utilisateur et son rôle, et d’un composant service l’authenticator.

On va commencer par déclarer l’EJB Entity annexe : le Role. En effet Seam permet de très simplement mettre en place une gestion des rôles dans les applications, ici on va simplement l’implémenter pour que ce soit en place, mais nous ne rentrerons pas dans les détails de l’utilisation des rôles. Voici un exemple de rôle : ( j’ai enlevè les getters et setters, ils sont bien entendu nécessaires.)

/**
 * @author leakim
* Chaque role doit être unique c'est pourquoi on ajoute la contrainte d'intégrité sur le rolename.
 * */
@Name("role")
@Entity
@Table(name = "Role", uniqueConstraints = @UniqueConstraint(columnNames = "rolename"))
public class Role implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;

    /** id. */
    @Id @GeneratedValue
    private Long roleId;

    /** the role rolename;. */
    @RoleName // Cette annotation permet de declarer a Seam que cet attribut sera le nom du role.
    private String rolename;
}

Voilà jusqu’ici rien de compliqué, passons à un peu plus sérieux maintenant l’utilisateur : Comme vous pourrez le voir Seam fournit un ensemble d’annotations pour permettre de déclarer/mapper vos attributs d’objets aux composants de sécurité. ( j’ai enlevè les getters et setters, ils sont bien entendu nécessaires.)

/** User Entity.
 * @author leakim
* Ici on a decide de rendre unique l'userName qui estannote en @userPrincipal et l'email.
*
 * */
@Name("user")
@Entity
@Table(name = "User", uniqueConstraints = {
        @UniqueConstraint(columnNames = "userName"),
        @UniqueConstraint(columnNames = "email") })
public class User implements Serializable {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    /** id. */
    @Id @GeneratedValue
    private Long id;

    /** name. */
    @NotNull
    @UserPrincipal // user principal permet de definir le "login" .
    private String userName;

    /** password. */
    @NotNull
    @UserPassword(hash = "MD5") // définit ce champ comme étant le mot de passe.
    private String passwordHash;

    /** email. */
    @NotNull
    @Email // pour la validation
    private String email;

    /** the user firstname. */
    @NotNull @Length(max = 40)
    @UserFirstName
    private String firstName;

    /** the user lastname. */
    @NotNull @Length(max = 40)
    @UserLastName
    private String lastName;

    /** the user roles.
    * Ici ça se complique un peu, on définit que ce Set définit les rôles, et on annote
     * l'attribut correctement pour les relations ( un rôle peut être affectée a plusieurs utilisateurs, un utilisateur peut avoir plusieurs rôles.)
     */
    @UserRoles
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "UserRoles",
            joinColumns = @JoinColumn(name = "UserId"),
            inverseJoinColumns = @JoinColumn(name = "RoleId"))
    private Set userRoles;
}

Composant d’authentification

Voilà on a notre modèle de données, maintenant le composant d’authentification, c’est lui qui definit la méthode d’authentification, appellée par le composant Identity. Il récupère les données de l’utilisateur dans le composant Credentials.

/** authenticate an user. */
@Name("authenticator")
public class Authenticator {
    /** the logger. */
    @Logger
    private Log log;

    /** entityManager. */
    @In
    private EntityManager entityManager;

    /** the identity. */
    @In
    private Identity identity;

    /** credentials. */
    @In
    private Credentials credentials;

    /** Authenticate an user.
     * @return true if the user is correctly authenticated.
     * */
    public boolean authenticate() {
        try {
            log.info("authenticating {0}", credentials.getUsername());
            Query query = entityManager.createQuery("from User where username = :username "
                    + "and passwordHash = :password");
            query.setParameter("password", credentials.getPassword());
            query.setParameter("username", credentials.getUsername());

            User user = (User) query.getSingleResult();

            if (user.getUserRoles() != null) {
                for (Role mr : user.getUserRoles()) {
                    identity.addRole(mr.getRoleName());
                }
            }
            return true;
        } catch (NoResultException ex) {
            return false;
        }
    }
}

Comme vous pouvez le voir c’est très simple, on récupère ce qu’a entré l’utilisateur dans le formulaire, on regarde s’il existe en base un utilisateur qui a ce “username” et ce “password”, si oui c’est que l’utilisateur a correctement entré les données dans le formulaire : on peut l’authentifier donc on ajoute a identity le ou les rôles de l’utilisateur puis on retourne TRUE a Identity : votre utilisateurs est loggé !

Et voilà vous avez toutes les clefs en main pour la gestion de vos utilisateurs, à vous d’adapter et de personnalisez tout ça pour votre application …

Prochainement, l’utilisation des Converter et des Validator dans Seam.

Version iPhone

| Comments

Le site possède maintenant une version iPhone / iPod Touch. Il basculera automatiquement sur le format adapté si vous le constulez avec un de ces fabuleux appareils ;)

Events Et Observer : Gestion Des événements Asynchrone Avec Seam.

| Comments

Parmis les nombreux apports de Seam il y en a un qui est à mes yeux particulièrement utile : la gestion des évènements (Events). Et là ou cela devient vraiment interessant, c’est dans la simplicité de leur utilisation.

Tout d’abord quel est l’intérêt ? Admettons que vous avez un composant qui réalise une tâche interne à votre application (ajout de donnée, modification, visualisation d’une page … tout et n’importe quoi en fait).

Or suite à cette action vous avez un ou plusieurs process à effectuer après, par exemple envoi d’un mail, enregistrement d’un status en base etc … bref votre action première, doit être suivi d’autres.

Dans un développement classique vous allez enchaîner les appels de méthode pour pouvoir réaliser votre process complet.

Or si ce process change, cela deviens vite laborieux de modifier votre enchevêtrement de méthodes et/ou de tests.

Et bien c’est là qu’est l’intérêt des évènements : vous allez pouvoir pour une action donnée déclencher un évènement, une sorte de message qui sera reçu par un ou plusieurs composants de l’applications, complètement indépendant de votre emetteur d’évènements.

Tout d’abord ce modèle présente un avantage majeur : vous developpez vraiment des composants indépendants : vous devez juste spécifier quels messages ils attendent. Cela vous évite donc les modèles classiques d’objets interdépendants, ou la gestion des dépendance devient laborieuse dans une grosse application.

Tout ce que vous avez à faire dans l’action de base c’est déclencher un évènement sans vous préoccuper de quel ou combien de composants seront appelés après, ou de quel appel sera réalisé après.

Ensuite à vous de mettre en place les “Observeurs’ de l’évènement en question.

Mettons en place un exemple simple : Ici un objet héritant d’EntityHome<TextEntry> ou TextEntry est un EJB entity déjà défini, mais peu importe l’entity lorsqu’on utilise une EntityHome de Seam pour le mettre en relation CRUD avec la base.

@Name("textEntryHome")
public class TextEntryHome extends EntityHome {

    private static final long serialVersionUID = 3141975971653011468L;

    @RequestParameter
    private Long textEntryId;

    @Override
    public Object getId() {
        if (textEntryId == null) {
            return super.getId();
        } else {
            return textEntryId;
        }
    }

    @Override @Begin
    public void create() {
        super.create();
    }

    @Override
    public String persist() {
        Events.instance().raiseEvent("postAdded", this.getInstance().getId());
        return super.persist();
    }

}

La ligne interessante est donc celle ci :

Events.instance().raiseEvent("postAdded", this.getInstance().getId()); Simplement, lorsqu’une Entité de type TextEntry est persistée, on déclenche l’évenèmenets “postAdded” avec comme paramètre l’Id de l’entity. Vous pouvez passer ce que vous voulez en paramètre et le récupérer plus tard lors du traitement de l’évènements. Vous pouvez aussi déclencher un évènement sur l’appel d’une méthode grâce à l’annotation @RaiseEvent(“nomEvent”) au lieu de faire un appel dans la méthode. Cela vous permet de complètement sortir l’événement du code métier. Enfin vous pouvez aussi décider d’envoyer l’événement de manière Asynchrone en utilisant raiseAsynchronousEvent au lieu de raiseEvent. Imaginez alors les possibilité d’un tel fonctionnement. Si vous déclenchez un événement de manière asynchrone celui-ci sera géré par le service timer d’EJB3.

Maintenant occupons nous de l’observeur :

public class TextEntryManager {

    @Logger
    private Log logger;

    @In
    private EntityManager entityManager;

    @Observer("postAdded")
    @Asynchronous
    public void processEvent(Long id) {
        TextEntry textEntry = entityManager.find(TextEntry.class, id);
        if (textEntry != null) {
            logger.info("Text entry with title : #0 has been added ",  textEntry.getTitle());
        }

    }
}

Voilà, comme vous pouvez le voir ce qu’on fait ici est simple : grâce à l’id passé en paramètre, on récupère l’entity insérée précédemment, puis on met un message de log. Ce cas n’a pas vraiment d’intérêt il est juste là pour illustrer le fonctionnement. On vois que l’annotation @Observer sur une méthode, avec en paramètre le nom de l’événement suffit à récupérer celui-ci. Les paramètres de la méthode doivent avoir le même ordre que ceux passés à l’événement pour pouvoir les récupérer. Seule une annotation suffit donc pour récupérer l’évènement et ses paramètres, et vous pouvez mettre autant d’Observers que vous le souhaitez en écoute du même événement.

Enfin ici la méthode a été annoté @Asynchronous pour rendre le processus de traitement complètement asynchrone, afin d’optimiser le traitement des événements.

Et voilà, cette approche était une initiation,mais si vous maitrisez le concept, et commencez à le mettre en oeuvre avec JMS ou Quartz, vous pouvez arrivez à une gestion vraiment très fine et optimisée de vos processus tout en développant des objets qui deviennent réelement des composants.

Attention cependant à ne pas abuser des événements, ce mécanisme n’est pas fait pour être utilisé à tout bout de champ et vous risquez la confusion si vous en utilisez trop dans votre application : ciblez les cas qui nécessitent vraiment un traitement semblable.