Lecture résultats recherche avec Apache Commons
Cet article va présenter comment effectuer une lecture de résultats de recherche avec une simulation de construction de facettes. Le principe est de parcourir un ensemble de résultat et de construire des listes de valeurs distinctes.
Les classes d'exemple présentes sur la page d'accueil seront reprises.
Tout le code présenté est disponible ici : http://www.jouvinio.net/svn/study/trunk/apacheCommons
Sommaire
Votre avis
Nobody voted on this yet
|
|
Objectif
Le principe est de lire un ensemble d'instance de People
et de les convertir en tableau. Durant cette lecture, trois listes seront construites avec les valeurs distinctes et ordonnées des propriétés :
- Prénom
- Nom
- Gender
Utilitaires
Transformation
La première classe mise en place va permettre de transformer les instances de fr.ejn.tutorial.datas.People
en tableau de chaînes de caractères. Pour cela une implémentatire de l'interface Transformer
est utilisée.
package fr.ejn.tutorial.apache.commons.tool.results.transformer;
import fr.ejn.tutorial.datas.People;
import org.apache.commons.collections4.Transformer;
/**
* Transformer instance to build a String array from the People instance.
*
* @author Etienne Jouvin
*
*/
public final class PeopleToArrayTransformer implements Transformer<People, String[]> {
private static PeopleToArrayTransformer INSTANCE = new PeopleToArrayTransformer();
/**
* Get default instance.
*
* @return Default instance.
*/
public static PeopleToArrayTransformer getInstance() {
return INSTANCE;
}
/**
* Private constructor.
*/
private PeopleToArrayTransformer() {
}
/** {@inheritDoc} */
@Override
public String[] transform(People input) {
return new String[] { input.getName(), input.getSurname(), input.getGender().name() };
}
}
L'implémentation est très simple et seule la fonction transform
présente un intérêt. A partir de la variable de la fonction, un tableau est construit avec les données.
Comparateurs
Lors de la construction des valeurs distinctes, un ordre sera mis en place afin d'ordonner les valeurs. Deux implémentations sont construites en étendant la classe ComparableComparator
disponible dans le package org.apache.commons.collections4.comparators
Ordre alphabétique
Le premier comparateur mis en place va permettre d'effectuer une comparaison en ignorant la casse des chaînes de caractères. Pour cela, la fonction compareIgnoreCase
de la classe StringUtils
est utilisée.
package fr.ejn.tutorial.apache.commons.tool.results.comparator;
import org.apache.commons.collections4.comparators.ComparableComparator;
import org.apache.commons.lang3.StringUtils;
/**
* Comparator implementation on String by ignoring case.
*
* @author Etienne Jouvin
*
*/
public final class IgnoreCaseStringComparator extends ComparableComparator<String> {
private static final IgnoreCaseStringComparator INST = new IgnoreCaseStringComparator();
private static final long serialVersionUID = 752790054048664906L;
/**
* Get default instance.
*
* @return Default instance.
*/
public static IgnoreCaseStringComparator getInstance() {
return INST;
}
/**
* Private constructor.
*/
private IgnoreCaseStringComparator() {
}
/** {@inheritDoc} */
@Override
public int compare(String obj1, String obj2) {
return StringUtils.compareIgnoreCase(obj1, obj2);
}
}
Comparaison par ordre prédéfini
Le second comparateur sera utilisé pour ordonner les valeurs Gender
en fonction d'un ordre prédéfini. Le code mis en place est le uivant.
package fr.ejn.tutorial.apache.commons.tool.results.comparator;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.collections4.PredicateUtils;
import org.apache.commons.collections4.comparators.ComparableComparator;
import java.util.ArrayList;
import java.util.List;
/**
* Comparator implementation on value position in a referential.
*
* @author Etienne Jouvin
*
* @param <E> Type of object to compare.
*/
public final class SpecificOrderComparator<E extends Comparable<? super E>>
extends ComparableComparator<E> {
private static final long serialVersionUID = 2451589821688727842L;
private transient List<E> orderRef;
/**
* Default constructor with the referential used to sort.
*
* @param orderRef Ordered referential.
*/
public SpecificOrderComparator(List<E> orderRef) {
this.orderRef = new ArrayList<E>(orderRef);
}
/** {@inheritDoc} */
@Override
public int compare(E obj1, E obj2) {
int indx1 = ListUtils.indexOf(orderRef, PredicateUtils.equalPredicate(obj1));
int indx2 = ListUtils.indexOf(orderRef, PredicateUtils.equalPredicate(obj2));
ComparableComparator<Integer> comparator = ComparableComparator.comparableComparator();
return comparator.compare(indx1, indx2);
}
/** {@inheritDoc} */
@Override
public boolean equals(final Object object) {
boolean res = false;
if (super.equals(object) && object.getClass().equals(SpecificOrderComparator.class)) {
res = CollectionUtils.isEqualCollection(CollectionUtils.emptyIfNull(orderRef), CollectionUtils
.emptyIfNull(((SpecificOrderComparator<?>) object).orderRef));
}
return res;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return "SpecificOrderComparator".hashCode();
}
}
Au niveau du constructeur, une liste de valeurs est fournie afin de spécifier l'ordre souhaité. Celle-ci est ensuite utilisée au niveau de la fonction compare
pour trouver les positions des deux valeurs. La comparaison est ensuite effectuée sur ces positions dans la liste de référence. Ainsi, les données peuvent être triées en fonction de l'ordre souhaité.
Construction facettes
La construction de facettes sera réalisée à l'aide d'implémentation de Closure
. Le principe est d'extraire une valeur, prénom / nom / gender, et de la stockée dans une liste de valeur unique et trié selon un ordre souhaité.
Code commun
Beaucoup de code sera commun pour les différentes implémentations.
- Construction de la liste unique;
- Stockage d'une valeur;
- Récupération des valeurs ordonnées.
Le code est le suivant.
package fr.ejn.tutorial.apache.commons.tool.results.closure;
import fr.ejn.tutorial.datas.People;
import org.apache.commons.collections4.Closure;
import org.apache.commons.collections4.list.SetUniqueList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Common facet builder instance. Use an internal list to store value with unicity specified by the
* instance SetUniqueList.
*
* @author Etienne Jouvin
*
* @param <O> Type of object in the facet collection.
*/
public abstract class FacetAddClosure<O> implements Closure<People> {
private List<O> items;
/**
* Default constructor with the target list.
*/
public FacetAddClosure() {
this.items = SetUniqueList.setUniqueList(new ArrayList<O>());
}
/**
* Add item into the list.
*
* @param item item to add to the list.
*/
protected void addItem(O item) {
items.add(item);
}
/**
* Get the comparator instance to use in order to sort results.
*
* @return Comparator instance to use.
*/
abstract Comparator<O> getComparator();
/**
* Get the built list.
*
* @return Built list.
*/
public List<O> getItems() {
ArrayList<O> res = new ArrayList<>(items);
// Sort the list.
Collections.sort(res, getComparator());
// Return the list now sorted.
return res;
}
}
Au niveau du constructeur, la liste unique est construire à l'aide de la fonction setUniqueList
de la classe SetUniqueList
. Cette classe est fournie par commons-collections
et permet de constuire une liste qui s'assurera de l'unicité des valeurs insérées.
La fonction addItems
est uniquement mise en place pour ajouter une valeur dans cette liste, et s'assurer que la liste sera modifiée de façon uniforme.
Enfin la fonction getItems
permet de récupérer les valeurs ajoutées et triées à l'aide du comparateur retourné par la fonction getComparator
. Ainsi les sous classes ne devront se charger que de l'extraction des valeurs, et la construction des valeurs distinctes est centralisée et "sécurisée".
Facette prénom
La première implémentation doit permettre d'extraire les données unique des prénoms, récupérés à l'aide de la fonction getName
des instances People
. Le tri s'effectue par ordre alphabétique en ignorant la casse à l'aide de l'instance IgnoreCaseStringComparator
. Le code est très simple, grâce à la classe parente mise en place précédement.
package fr.ejn.tutorial.apache.commons.tool.results.closure;
import fr.ejn.tutorial.apache.commons.tool.results.comparator.IgnoreCaseStringComparator;
import fr.ejn.tutorial.datas.People;
import java.util.Comparator;
/**
* Facet closure for the people name value. Get value from the method {@link People#getName()}. Use
* a comparator on the value in upper case.
*
* @author Etienne Jouvin
*
*/
public class FacetPeopleName extends FacetAddClosure<String> {
/** {@inheritDoc} */
@Override
public void execute(People input) {
super.addItem(input.getName());
}
/** {@inheritDoc} */
@Override
Comparator<String> getComparator() {
return IgnoreCaseStringComparator.getInstance();
}
}
Facette nom
Le principe de cette implémentation est exactement le même que pour la précédente. La donnée est extraite depuis la fonction getSurname
.
package fr.ejn.tutorial.apache.commons.tool.results.closure;
import fr.ejn.tutorial.apache.commons.tool.results.comparator.IgnoreCaseStringComparator;
import fr.ejn.tutorial.datas.People;
import java.util.Comparator;
/**
* Facet closure for the people surname value. Get value from the method
* {@link People#getSurname()}. Use a comparator on the value in upper case.
*
* @author Etienne Jouvin
*
*/
public class FacetPeopleSurname extends FacetAddClosure<String> {
/** {@inheritDoc} */
@Override
public void execute(People input) {
super.addItem(input.getSurname());
}
/** {@inheritDoc} */
@Override
Comparator<String> getComparator() {
return IgnoreCaseStringComparator.getInstance();
}
}
Facette Gender
Ce constructeur est un peu plus compliqué, car l'ordre s'effectue depuis un ordre de valeur souhaité à savoir WOMAN
en premier puis MAN
. Cependant, grace à l'externalisation du comparateur et de la classe parente, l'implémentation tien en quelques lignes.
package fr.ejn.tutorial.apache.commons.tool.results.closure;
import fr.ejn.tutorial.apache.commons.tool.results.comparator.SpecificOrderComparator;
import fr.ejn.tutorial.datas.Gender;
import fr.ejn.tutorial.datas.People;
import java.util.Arrays;
import java.util.Comparator;
/**
* Facet closure for the people gender value. Get value from the method {@link People#getGender()}.
* Use a comparator on the value in upper case.
*
* @author Etienne Jouvin
*
*/
public class FacetPeopleGender extends FacetAddClosure<Gender> {
private Comparator<Gender> comparator;
/**
* Default constructor.
*/
public FacetPeopleGender() {
this.comparator = new SpecificOrderComparator<>(Arrays.asList(Gender.WOMAN, Gender.MAN));
}
/** {@inheritDoc} */
@Override
public void execute(People input) {
super.addItem(input.getGender());
}
/** {@inheritDoc} */
@Override
Comparator<Gender> getComparator() {
return this.comparator;
}
}
La valeur est donc extraite depuis la fonction getGender
, et le comparateur est initialisé dans le constructeur.
Exécution
Une fois tous les outils mis en place, l'exécution est codée dans une classe type Reader
qui va orchestrer l'ensemble de ceux-ci dans une fonction search
. Le code est le suivant.
package fr.ejn.tutorial.apache.commons.tool.results;
import fr.ejn.tutorial.apache.commons.tool.results.closure.FacetAddClosure;
import fr.ejn.tutorial.apache.commons.tool.results.closure.FacetPeopleGender;
import fr.ejn.tutorial.apache.commons.tool.results.closure.FacetPeopleName;
import fr.ejn.tutorial.apache.commons.tool.results.closure.FacetPeopleSurname;
import fr.ejn.tutorial.apache.commons.tool.results.model.Results;
import fr.ejn.tutorial.apache.commons.tool.results.transformer.PeopleToArrayTransformer;
import fr.ejn.tutorial.dao.PeopleDao;
import fr.ejn.tutorial.datas.Gender;
import fr.ejn.tutorial.datas.People;
import org.apache.commons.collections4.Closure;
import org.apache.commons.collections4.ClosureUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.TransformerUtils;
import java.util.Arrays;
import java.util.Collection;
/**
* Query results builder instance.
* <p>
* REad result from the PeopleDao instance and extract all possibles facets data.
* </p>
*
* @author Etienne Jouvin
*
*/
public class QueryResultsReader {
private PeopleDao peopleDao;
/**
* Do a search and retrieve results + built facets.
*
* @return Query results.
*/
public Results search() {
Collection<People> peoples = this.peopleDao.getPeopleByGender();
FacetAddClosure<Gender> facetPeopleGender = new FacetPeopleGender();
FacetAddClosure<String> facetPeopleName = new FacetPeopleName();
FacetAddClosure<String> facetPeopleSurname = new FacetPeopleSurname();
Closure<People> closures = ClosureUtils
.chainedClosure(Arrays.asList(facetPeopleGender, facetPeopleName, facetPeopleSurname));
Iterable<People> peoplesIterable = IterableUtils
.transformedIterable(peoples, TransformerUtils.asTransformer(closures));
Collection<String[]> searchResults = CollectionUtils
.collect(peoplesIterable, PeopleToArrayTransformer.getInstance());
Results res = new Results();
res.setGendersFacet(facetPeopleGender.getItems());
res.setNamesFacet(facetPeopleName.getItems());
res.setSurnamesFacet(facetPeopleSurname.getItems());
res.setResults(searchResults);
return res;
}
/**
* Set the people dao instance.
*
* @param peopleDao Instance to set.
*/
public void setPeopleDao(PeopleDao peopleDao) {
this.peopleDao = peopleDao;
}
}
A noter la fonction setPeopleDao
qui va permettre d'injecter une DAO
facilement, et utilisé dans le cadre des tests unitaires par exemple.
La problématique principale est de pouvoir exécuter l'ensemble des closures sur chaque valeur avant transformation, et d'évtier de multiple lecture de la liste des résultats. L'exéction de multiple closure est rendue possible en créant une instance "chaînée" depuis la fonction chainedClosure
de la classe ClosureUtils
. Ainsi les trois instances seront déclenchées.
FacetAddClosure<Gender> facetPeopleGender = new FacetPeopleGender();
FacetAddClosure<String> facetPeopleName = new FacetPeopleName();
FacetAddClosure<String> facetPeopleSurname = new FacetPeopleSurname();
Closure<People> closures = ClosureUtils
.chainedClosure(Arrays.asList(facetPeopleGender, facetPeopleName, facetPeopleSurname));
La deuxième astuce consiste à construire une instance de Transformer
à partir de cette nouvelle instance de closure, ce qui est possible avec la fonction asTransformer
de la classe TransformerUtils
. Ceci permet d'exécuter le "closure" et de retourner l'objet origine sans aucune transformation. Cette instance est utilisée pour construire une implémentation de Iterable
exécutant la transformation lors de l'appel de la fonction next
.
Iterable<People> peoplesIterable = IterableUtils
.transformedIterable(peoples, TransformerUtils.asTransformer(closures));
Enfin la fonction collect
de CollectionUtils
est exécutée sur cet iterable
avec le transformer construisant le tableau depuis les instances de People
.
Collection<String[]> searchResults = CollectionUtils
.collect(peoplesIterable, PeopleToArrayTransformer.getInstance());
En résumé, lors du parcour de l'iteration, les exécutions sont les suivantes.
- 1. Récupération d'un élément de l'itération peoplesIterable.
- 1.1 Appel d'un transformer exécutant la fonction execute d'un closure.
- 1.2 Appel de tous les closures encapsulé par
closures
. - 2. Transformation de People en String[].
- 3. Ajout dans la collection résultat.
Enfin, les résultats et les facettes sont placées dans une classe pour encapsuler le tout.