jeudi 26 juin 2025

Concevoir un système d’annotations pour l’automatisation

À la suite de l'article concernant Automatisation d'interface utilisateur, j'ai reçu différents commentaires relatifs au choix qui avait ét fait. J'ai décidé d'écrire un autre article présentant différentes conceptions, avec leurs avantages et inconvénieunts respectifs

Annotation et convention over configuration

L'approche qui avait été adoptée était basée sur une annotation par responsabilité, avec le moins de choix possible. Un maximum d'automatisation avait été mis en place.


@Order(value = 9)
@HideField(value = SectionEnum.TABLE)
@Mandatory
private Integer groundElementOwnerId;

Les annotations indiquent 

  • que le champs serait affiché à la neuvième position.
  • que le champ serait caché dans la section table de la page.
  • que le champ est obligatoire

@Order(value = 7)
@TableStruture
private Map<String,String> properties;

Ici il y avait détection de la map et un tableau était affiché.




Différentes conventions avaient été mises en place.

Concernant les select, lorsque le nom de l'attribut contenait "id" et comportait plus de deux caractères, une tentative était faite pour rechercher un service au nom du dto et de trouver une méthode s'appelant findAll. Le résultat était mis dans le modèle et pouvait être utilisé dans thymeleaf.

Lorsqu'un traitement spécifique était nécessaire pour un champ, ce dernier pouvait être masqué via l'annotation et une gestion personnalisée était alors implémentée dans un template thymeleaf.

Cette première approche avait été simple en mettre place, car il y avait peu de cas spécifique à gérer. Le développeur pouvait aisément ajouter de nouveau dto.

La gestion de la construction de la structure dynamique était assez simple malgré les quelques annotations à gérer. Peu d'attribut était disponible dans chaune des annotations.


Annotation générique

Avec cette approche, une seule annotation est définie avec l'ensemble des attributs possibles. C'est possiblement la plus simple, même si le développeur doit fournir davantage d'informations. Le principal défaut est qu'il est possible que le développeur assigne des valeurs inexistantes  pour le type spécifié. Deplus, l'annotation a plusieurs responsabilités.

@FormField(type = FormFieldType.PHONE, order = 9, pattern = "999 999-9999")
private String phone;
    
@FormField(type = FormFieldType.SELECT, optionsProvider = "getCities", order = 5 )
private String city;
    
@FormField(type = FormFieldType.CHECKBOX, options = {"sportCheck:Sport", "musiqueCheck:Musique", "lectureCheck:Lecture"}, order = 15 )
private List<String> interest;

Une seule annotation facilite la construction de la structure dynamique. Seuls les attributs existants pour un type donnée sont pris en compte.

Annotation par responsabilité

Les responsabilités sont définies via différentes annotations. Le dto devient plus verbeux, et la gestion plus complexe pour construire la structure dynamique. C'est une version avancé que la première approche.

@FormField(type = FieldType.SELECT)
@SelectConfig(valueField = "id", labelField = "name")
private List<Ville> villes;

@FormField(type = FieldType.INPUT, placeholder = "+1 (555) 123-4567")
@FormStyle(cssClass="form-control")
@FormValidation(required=true, readonly=true, pattern="'mask' : '999 999-9999'")
@Order(8)
private String phone;

Annotation générique avec avec type spécifique

Chaque type dispose de sa propre annotation avec ses propres attributs. Ainsi, le développeur ne peut pas se tromber sur les propriétés applicables.

@HiddenInput(order=1)
private Long idEditeur;

@TextInput(minLength = 2, maxLength = 20, order=2)
private String name;

@TextInput(readonly=true, order=3)
private String age;

@SelectInput(optionsProvider = "getCities", required = true, order=4)
private String city

Approche assez simple à comprendre, cependant l'attribut order bien qu'utile pour la position d'affichage n'étant pas un attribut HTML, il vient briser le cocept de séparation des responsabilités. Il pourrait être mis dans une autre annotation.


Quel approche au niveau des annotations préférez-vous?


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.