jeudi 19 juin 2025

Automatisation d'interface utilisateur

La solution qui est présentée se base sur thymeleaf et boostrap 3. 

Interface utilisateur

Depuis le tout début de ma carrière, à chaque emploi j'ai eu à créer de nombreuses interfaces web pour des applications de gestion. Ce n'est pas nécessairement très intéressant, c'est redonnant et c'est de plus en plus complexe pour au final prendre des données, les afficher à l'utilisateur, lui permettre de les modifier et finalement les retourner au serveur.

Après avoir fait du jsp, jsf, asp, php, struts, gwt, gxt, gwt-ext, jquery, thymeleaf j'avais de grosse attente envers les nouveaux framework / librairie frontend. J'ai été malheureusement fortement déçu après avoir fait quelques nouvelles applications from scratch en angular / react.

Encore plus complexe qu'auparavant, mais plus découpé. Courbe d'apprentissage assez élevé et maintenance coûteuse. Le temps et argent pris pour constamment mettre à jour des libraires c'est du temps, argent en moins pour ajouter des nouvelles fonctionnalités à l'utilisateur.

Sur un nouveau projet qui allait demander la création d'une centaine d'écran avec la majeure partie du temps que deux développeurs, il fallait trouver une solution efficace et rapide. Une preuve de concept au tout début avec  react / angular / vue afin de voir si c'était viable de les utiliser fût créé.

Personne à l'interne n'avait de l'expérience avec ces outils. De plus la centaine d'écrans à réaliser n'était qu'une petite partie insignifiante du système à créer. Il fût rapidement décidé qu'avec le temps et le budget alloué que ces outils ne pouvaient assurer une réussite au projet.

Idée

Une centaine d'écrans à réaliser en peu de temps, comment y parvenir?
J'ai alors commencé à penser à des critères que les interfaces avaient en commun. Est-ce qu'il y aurait un moyen d'automatiser les interfaces utilisateur d'une application de gestion?
Qu'est-ce qui peut changer? Les types de données. Est-ce que le champ est obligatoire? L'ordre d'affichage des composants. Est-ce qu'il doit être formaté? L'affichage du champs est nécessaire? Est-ce qu'il doit avoir un style particulier à appliquer au champ?

Une annoation à été créé pour ces différents critères. Certaine aurait pu être combiné et avoir des attributs.

Reflection

Afin d'éviter le plus possible d'écrire du code, dans un esprit de généricité avec les différents critères récurrents d'un écran à l'autre, j'ai regardé du côté de la réflection en java. J'ai mis en place des annotations pour différent critère qui serait utilisé dans un dto (représentant les données d'une interface utilisateur). Par la suite, via la réflection ces dto  sont parcouru et un  objet générique est utilisé pour construire l'interface utilisateur via thymeleaf.

public class UsersDto {

    @Order(value = 1)
    private Long id

    @Mandatory
    @Order(value = 2)
    private String firstname;

    @Order(value = 3)
    private String lastname;

    @Order(value = 4)
    private String username;

    @TextArea(value=true)
    @OuterCssClass(value="col-md-12")
    @Order(value = 17)
    private String comment;

    @MandatoryField(value=true)
    @Formatter(value="'mask' : '999 999-9999'")
    private String phone;

    private LocalDate birthDate;

    private List<Ville> villesId

}


Structure de données où les informations étaient mises

@Data
public class UIReflection {

    private String field;
    
    private Object value;

    private String formatter;
    private String sortable;
    private boolean mandatory;
    
    private String i18n;
    
    private String dataType;
    private String uiComponent;
}

Ensuite une opération analysait le dto et pour chacun des champs du dto, un objet de ce type était créer.
Si le type de donnée était de type Date, un DateTimePicker serait inséré dans l'attribut uiComponent Ceci est une version simplifié de ce qui a été fait.

Class<?> myUserDto = UsersDto.class;
Field[] allFields = myClass.getFields();

List<UIReflection> uiReflections = new ArrayList<>();

for (Field field : allFields) {

    UIReflection uiReflection = new UIReflection();
    uiReflection.setField(field.getName());
    uiReflection.setI18n(field.getName());
    uiReflection.setMandatory(mandatoryValue(field));
    uiReflection.setFormatter(formatter(field));
    
    if (field.getType().equals(LocalDate.class)) {
        uiReflection.setDataType("localdate");
    } else if (field.getType().equals(boolean.class)) {
        uiReflection.setDataType("boolean");
    } else if (ReflectionUtils.isNumeric(field.getType()) && !field.getType().getName().contains("Id")){
        uiReflection.setDataType("number");
    } else if (field.getType().equals(String.class)) {
        if(textArea(field)){
            uiReflection.setDataType("textArea");
        }
    }
    uiReflections.add(uiReflection);
}
    

La création d'une nouvelle écran se résumait majoritairement à créer un dto et mettre les annotations sur chacun des champs.

Template / fragment Thymeleaf

Par la suite, un template  parcourait les objets UIReflection  affectaient les valeurs css, attribut aux composants html.

 <div th:fragment="Generic">
            <input id="genericFormId" name="id" type="hidden"/>
            <div th:each="uiGenericForm, iterStat : ${uiReflectionsForm}" th:remove="tag">
                
                <div class="col-sm-6">
                    
                    <div th:class="${uiGenericForm.mandatory ? 'form-group required' : 'form-group' }" class="form-group">
                        <label th:attr="for='genericForm'+${uiGenericForm.field}" class="col-sm-3 control-label" th:text="#{${uiGenericForm.i18n}}"></label>
                        <div class="col-sm-9">
                            <div th:switch="${uiGenericForm.dataType}" th:remove="tag">

                                <div th:case="localDateTime" class="input-group date" th:id="'genericForm'+${uiGenericForm.field}+'DatePicker'">
                                    <input type="text" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}" class="form-control"/>
                                    <span class="input-group-addon">
                                        <span class="glyphicon glyphicon-calendar"></span>
                                    </span>
                                </div>

                                <div th:case="selection" th:remove="tag">
                                    
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        
                                        <select  class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:name="${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}" >
                                            <option th:value="NULL" selected="selected" th:text="#{form.select.empty}"></option>
                                            <option th:each="generic : ${__${uiGenericForm.field}__}" th:attr="data-id=${generic.id}" th:value="${generic.id}" th:text="${generic.name}"></option>
                                         </select>
                                        
                                    </div>
                                    
                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        
                                        <select  class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:name="${uiGenericForm.field}" >
                                            <option th:value="NULL" selected="selected" th:text="#{form.select.empty}"></option>
                                            <option th:each="generic : ${__${uiGenericForm.field}__}" th:attr="data-id=${generic.id}" th:value="${generic.id}" th:text="${generic.name}"></option>
                                         </select>
                                        
                                    </div>
                                    
                                </div>

                                <input th:case="boolean" type="checkbox" th:attr="name=${uiGenericForm.field}"/>

                                <div th:case="map">
                                   <div th:include="fragments/map-table :: map-table" th:remove="tag"></div>
                                </div>

                                <div th:case="number" th:remove="tag">
                                   
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="number" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}, name=${uiGenericForm.field}" required/>
                                    </div>  

                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="number" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}"/>
                                    </div> 
                                    
                                </div>
                                
                                <div th:case="*" th:remove="tag">
                                
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="text" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}, name=${uiGenericForm.field}" required/>
                                    </div>  

                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="text" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}"/>
                                    </div> 
                                
                                </div>

                             </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>


Une fois que le template est traité.


Possibilité


Préférez-vous les formulaires horizontaux au formulaires verticaux?  Appréciez-vous davantage Tailwind css que bootstrap? Quelques modification dans les classes css du template et le tour est joué.

Il est tout à fait possible de changer ce script thymeleaf par un autre moteur de template orienté serveur tel que freemaker, groovy, pebble... Htmx pourrait être utilisé afin de ne pas rafraichir la page au complet.

Dans notre cas, htmx n'existait pas. On chargait les templates via fetch afin d'éviter de recharger la page au complet. Un api rest était utiliser pour afficher, modifier et sauvegarer une ressource. Donc au final du MVC et REST.

Il serait aussi envisageable de retourner la structure de UIReflection en JSON. VueJs, React, Angular... pourrait alors être utilisé pour générer le UI au lieu d'un template serveur. 




lundi 16 juin 2025

Kde 6 vs Windows 11

Nous allons comparer Kde 6 à l'environnement graphique de Windows 11.

Le précédent comparatif date de 2015 


Voici une liste d'ancien comparatif.




Menu

Le menu sous Kde est toujours divisé par des catégories. Prenez note que sous une autre distribution, cela pourrait être différent.







Calculatrice


Un mode simple, scientifique, statistique et numérique est disponible 



Configuration

Le panneau de configuration s'est simplifié, un menu par catégorie s'est ajouté.




Explorateur de fichier





Bureau





Navigateur internet




Konqueror est le navigateur par défaut, cependant, il y a aussi maintenant aussi Falkon. Konquer gère toujours la gestion des fichiers et supporte de multiple protocole réseau.



Ressource système


Cette application a été revu. La vue principal donne une bonne idée de l'état actuel du système. Pour plus de détail, les autres sections sont disponible.





lundi 30 septembre 2024

Multimplémentation et l'injection de dépendance.

Dans cet article,  nous montrons une approche afin d'avoir un seul endpoint lorsque vous avez différent type de donnée en entrée ou sortie. Cela permet aussi de faire du traitement différent pour chacun des types.


Définition des types de données

C'est une des nombreuses possibilités de la librairie Jackson qui est montrée ici.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Square.class, name = "SQUARE"),
    @JsonSubTypes.Type(value = Circle.class, name = "CIRCLE"),
})

@Data
public abstract class Shape {
    public String type;
}


@JsonTypeName("SQUARE")
@Data
public class Square extends Shape {
   public double length;
}

@JsonTypeName("CIRCLE")
@Data
public class Circle extends Shape {
   public double radius;
}


Définition des services pour les types de données

Puisque le traitement peut être différent pour chaque type de données,  un service est créé pour chacun d'eux.

public interface IShapeService<T extends Shape> {
    T saveShape(T t);
}

@Service("SQUARE_SERVICE")
public class SquareShapeService  implements IShapeService<Shape> {
    public Square save(Square square){
         return square;
    }

}

@Service("CIRCLE_SERVICE")
public class CircleShapeService  implements IShapeService<Circle> {
    public Circle save(Circle circle){
         return circle;
    }
}

S'il y a du code commun, les classes implémentant IShapeService pourrait hériter d'une autre classe. Sinon, la composition pourrait être utilisée.

En affectant une valeur pour les différents services et en voyant qu'ils implémentent IShapeService, Spring sera qu'il doit mettre le nom du service comme clé et les instances comme valeur dans la Map.

Il faut faire le pont entre le type de donnée entrée et le service à appeler. Il serait possible d'utiliser une enum. 

Donc si on passe le type SQUARE, on concatène "_SERVICE" afin d'obtenir l'instance de SquareShapeService dans la map

@Service
public class ShapeService{
   private final Map<String, IShapeService> shapeServiceMap;

     public Shape save(Shape shape) {
        IShapeService service = shapeServiceMap.get(shape.getType() + _"SERVICE");
        return service.save(shape);
    }  
}


@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/shapes")
public class ShapeController {

   private ShapeService shapeService;

    @PostMapping("")
    public ResponseEntity<Shape> save(@RequestBody Shape shape) {
        return new ResponseEntity<>(shapeService.save(shape), HttpStatus.OK);

    }
}


Il est donc possible de Passer un objet de type square ou circle à ce contrôleur

vendredi 26 mai 2023

Thymeleaf et htmx

Thymeleaf est un moteur de template serveur régulièrement utilisé le monde Java, principalement avec le framework Spring. Dans cet article, nous présentation quelques architectures possibles avec ce framework ainsi que l'utilisation de Htmx afin de rendre les applications thymeleaf plus dynamiques.

 

 Architecture d'application

Voici un ensemble d'architecture varié qu'il est possible de faire avec Thymeleaf

 
La première étant la plus simple et la plus utilisée dans les exemples montrés sur la toile.
J'ai pu expérimenter la cinquième. Le mvc était utilisé pour construire les interfaces utilisateurs, par contre toutes les opérations de suppression, de mise à jour ou de création de données utilisaient une architecture rest. Le javascript était utilisé afin de reloader la page.

C'est une solution simple et rapide à mettre en place face à une solution SPA avec Angular, React ou bien Vue.

HDA

L'augmentation du temps de développement, la complexité des frameworks, courbe d'apprentissage élevé, maintenance onéreuse ont fait reculé certains.

React à htmx

 

Hypermedia Driven Application combine la facilité des multiples pages application (mpa) avec l'expérience utilisateur des single page application (spa). 

Une panoplie de librairie existe pour apporter l'expérience utilisateur à des frameworks orientés serveur.

unpoly, htmx, alpine, hyperscript... permettent via l'ajout de mot clé dans le html de rendre des applications utilisant des framework serveurs: jsf, jsp, thymeleaf... plus dynamique, d'éviter de charger la page au complet..

Htmx

Htmx est une petite librairie qui donne accès à ajax, css, sse et websocket via des attributs en html.

Cette librairie  htmx-spring-boot-thymeleaf facilite l'utilisation de htmx avec spring boot et thymeleaf. Elle n'est pas obligatoire.

Vidéo d'une présentation de thymeleaf avec htmx

https://youtu.be/okCdaBTQsik

Une autre présentation

https://www.youtube.com/watch?v=38WAVRfxPxI

Une liste d'exemple est disponible via ce lien.

Éviter le rafraîchissement de la page 

Notre exemple est un template ayant un fragment pour les css, un autre pour le menu et un autre pour le javascript.
 
index.html
<!DOCTYPE html>
<html lang="en">
<head th:replace="~{fragments/css :: headcss}">
<meta charset="UTF-8">
<title>test</title>
</head>
<body class="container">
<div th:replace="~{fragments/navbarmenu :: menu}"/>

<section id="main" class="fade-me-out">

<div id="modals-here"></div>
</section>

<div th:replace="~{fragments/jsscripts :: jsscripts}"/>

</body>
</html>

 

 Fragment pour le css

<!DOCTYPE html>
<html lang="en">
<head th:fragment="headcss">

<title>Generic form UI</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" th:href="@{/css/datepicker-bs5.min.css}" href="/css/datepicker-bs5.min.css" />
</head>
<body>

</body>
</html>


Fragment pour le javascript

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js scripts</title>
</head>
<body>
<div th:fragment="jsscripts">

<script type="text/javascript" th:src="@{/js/genericui.js}" src="/js/genericui.js"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.bundle.min.js}" src="/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" th:src="@{/js/datepicker-full.min.js}" src="/js/datepicker-full.min.js"></script>

<script type="text/javascript" th:src="@{/js/htmx.min.js}" src="/js/htmx.min.js"></script>
<script type="text/javascript" th:src="@{/js/inputmask.min.js}" src="/js/inputmask.min.js"></script>

<script type="text/javascript" th:src="@{/js/bootstrap-validation.js}" src="/js/bootstrap-validation.js"></script>

<script type="text/javascript">
document.addEventListener('htmx:afterRequest', function(evt) {

Inputmask().mask(document.querySelectorAll("input"));

const matchesDate = document.getElementsByClassName("date1");

for (let i = 0; i < matchesDate.length; i++) {
const datepicker = new Datepicker(matchesDate[i], {
buttonClass: 'btn',
});
}

});

</script>
</div>
</body>
</html>

Fragment pour le menu

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NavBar</title>
</head>
<body>

<nav th:fragment="menu" class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" th:href="@{/index2}">Norenda</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" th:href="@{/index}">Index</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" hx-push-url="true" hx:get="@{/modal}" hx-target="#main" hx-swap="innerHTML" hx-trigger="click">Modal</a>
</li>
<li class="nav-item">
<a class="nav-link" hx-push-url="true" hx:get="@{/success}" hx-target="#main" hx-swap="innerHTML swap:1s" hx-trigger="click">Succes</a>
</li>
<li class="nav-item">
<a class="nav-link" hx-push-url="true" hx:get="@{/editor}" hx-target="#main" hx-swap="innerHTML swap:1s" hx-trigger="click">Editeur</a>
</li>
</ul>
<form class="d-flex" role="search" hx:post="@{/search}" hx-target="#main" hx-swap="innerHTML">
<input class="form-control me-2" type="search" placeholder="Recherche" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Recherche</button>
</form>
</div>
</div>
</nav>

</body>
</html>

 

Les deux premiers liens font un chargement complet de la page. 
Le troisième lien affiche une modale
Le quatrième lien affiche un message 
Le cinquième lien affiche un formulaire.

Mis à part les deux premiers liens, il n'y a aucun chargement complet de la page. Il y a remplacement d'une zone par le fragment retourné du serveur.

Un des liens propose une animation.
 
Pour éviter de faire un chargement complet de la page, il faut utiliser certains mots clés.

On mentionne ici que get est fait sur le url mentionnée
hx:get="@{/editor}"

On mentionne ici quel sera la cible du fragment retourné, où ira le contenu
hx-target="#main" 

On mentionne ici qu'on remplace le contenu de la cible par celui du fragment, il serait possible de l'ajouter avant, après
hx-swap="innerHTML"

 

Animation

Quelques animations sont disponibles tels que des fade in, fade out. Il est aussi possible d’utiliser des css pour créer ses propres animations.

Conclusion

Htmx peut vous permettre de rendre encore plus dynamique votre application qui utilise un framework orienté serveur à une fraction du prix en termes de temps, argent et apprentissage. Pour encore plus de fonctionnalité, il faut regarder unpoly.


Vous pouvez télécharger les sources du projet ici.

mercredi 17 mai 2023

Spring data jpa et Hibernate’s Warning “firstResult/maxResults specified with collection fetch”

Nous verrons dans cet article comment corrigé le problème 
Hibernate’s Warning “firstResult/maxResults specified with collection fetch” qui peut survenir lorsqu'il y a un fetch d'effectuer et qu'il y a du paging. Nous utiliserons spring data jpa.

Détail structure

Imaginons une relation 1 à plusieurs


Lorsqu'on obtient un parent, on désire obtenir aussi sa liste d'enfants. En sql, il y aurait une jointure à effectuer. Il est question d'une recherche avancé où on pourrait recherche par plusieurs critères.

Spécification

Création d'une spécification pour construire une requête dynamique

@Component
public class ParentSpecification {

    public Specification<Parent> recherche(RechercheParent recherche) {
        return (root, cq, cb) -> {
            Join<Parent, enfant> enfantJoin;

             List<Predicate> rootPredicates = new ArrayList<>();

             if (Long.class != cq.getResultType()) {
                enfantJoin = (Join) root.fetch("enfants", JoinType.LEFT);
             } else {
                enfantJoin = (Join) root.join("enfants", JoinType.LEFT);
             }

             sqlLikeCondition("nom", recherche.nomParent(), root, cb, rootPredicates);
             sqlEqualCondition("age", recherche.age(), root, cb, rootPredicates);
             return  cb.and(rootPredicates.toArray(new Predicate[rootPredicates.size()]));

    }

}

Repository

Spring data Jpa a une méthode findAll qui accepte en paramètre une spécification. Il y a même une version de la méthode qui accepte aussi un objet Pageable. Afin de pouvoir l'utiliser, le repository dois hérité de JpaSpecificationExecutor.

@Repository
@Transactional(readOnly = true)
public interface ParentRepository extends JpaRepository<Parent, Long>, JpaSpecificationExecutor<Parent> {
}


Il sera alors possible d'effectuer un appel comme celui çi

 Page<Parent> pageParent = parentRepository.findAll(parentSpecification.search(parentRecherche), pageable);

Détail du problème

Par contre, dans les logs il y aura le message org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory


Dans la base de donnée avec une requête sql le résultat serait par exemple
Chaque enfant est relié à son parent.

Habituellement, Hibernate, le orm par défaut utilisé par spring boot, lorsque le paging est utilisé il va prendre x première valeurs jusqu'à y. Par exemple si on mentionne qu'on veut les 5 premières résultats on obtiendrait


Ce qui est pas vraiment ce qu'on voudrait, mais bien les 5 premiers parents.

Hibernate donne le message “firstResult/maxResults specified with collection fetch” car il applique alors la pagination en mémoire sans appliquer la pagination et ensuite retourne le résult voulue.
Si votre table à énormément de donnée, vous pouvez obtenir rapidement des problèmes de performance, surtout si vous effectuer des rêquetes sans aucun critères de recherche.

Solution

Afin de palier à ce problème, nous allons redéfinir une méthode spring data jpa

Dans la classe de spécification, il faudra ajouter

public Specification<Parent> idIn(Set<Long> parentIds) {
        return (root, cq, cb) -> {

            if (parentIds == null || parentIds.isEmpty()) {
                return null;
            }

            return root.get("parentId").in(parentIds);

        };
    }

Il faut créer une nouvelle interface avec la méthode findEntityIds

@NoRepositoryBean
public interface CustomJpaSpecificationExecutor<E, ID extends Serializable> extends JpaSpecificationExecutor<E> {

    Page<ID> findEntityIds(Specification<E> specification, Pageable pageable);
}

Par la suite il faut créer une classe implémentant cette interface


public class CustomBaseSimpleJpaRepository<E, ID extends Serializable> extends SimpleJpaRepository<E, ID> implements CustomJpaSpecificationExecutor<E, ID> {

    private final EntityManager entityManager;
    private final JpaEntityInformation<E, ID> entityInformation;

    public CustomBaseSimpleJpaRepository(JpaEntityInformation<E, ID> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = entityInformation;
    }

    private static long executeCountQuery(TypedQuery<Long> query) {

        Assert.notNull(query, "TypedQuery must not be null");

        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }

        return total;
    }

    @Override
    public Page<ID> findEntityIds(Specification<E> specification, Pageable pageable) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<ID> criteriaQuery = criteriaBuilder.createQuery(entityInformation.getIdType());
        Root<E> root = criteriaQuery.from(getDomainClass());

        // Get the entities ID only
        criteriaQuery.select((Path<ID>) root.get(entityInformation.getIdAttribute()));

        // Apply specification(s) to restrict results or to write dynamic queries
        Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);

        if (predicate != null) {
            criteriaQuery.where(predicate);
        }

        // Update Sorting
        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        if (sort.isSorted()) {
            criteriaQuery.orderBy(toOrders(sort, root, criteriaBuilder));
        }
        TypedQuery<ID> typedQuery = entityManager.createQuery(criteriaQuery);

        // Update Pagination attributes
        if (pageable.isPaged()) {
            typedQuery.setFirstResult((int) pageable.getOffset());
            typedQuery.setMaxResults(pageable.getPageSize());
        }

        return PageableExecutionUtils.getPage(typedQuery.getResultList(), pageable,
                () -> this.executeCountQuery(getCountQuery(specification, getDomainClass())));

    }

}

Il faudra faire deux appels

Page<Long> pageParentId = parentRepository.findEntityIds(parentSpecification.search(parentRecherche), pageable);

List<Parent> parents = parentRepository.findAll(parentSpecification.search(parentRecherche).and(parentSpecification.idIn(Set.copyOf(pageParentId.getContent()))));

return new PageImpl<>(parents, pageable, pageParentId.getTotalElements());

Spring data jpa lancera 3 requêtes.

Une première requête pour obtenir les id des parents.
Une autre requête pour obtenir le nombre total de donnée 
Une troisième requête nécessitant les id de la première requête sera effectué