mardi 12 octobre 2021

Lenovo mini5i et Linux

J'ai récemment fait l'acquisiton d'un Lenovo mini 5i au lieu d'un ordinateur portable qui ont de plus en plus de composant soudé.

Un petit intel Core i3-10100, qui est au final autrement plus puissant que le i5-5300U de mon T450S

Tous les composants sont accessible: ssd, disque dur, carte wifi, mémoire vive. J'ai mis 32 gig de ram. Le ssd a été remplacé par un WD_BLACK  SN850 compatible pcie 4.Ordinateur portable IdeaCentre mini 5i | Lenovo CanadaTous les composants de la machines sont bien supportés par linux. Suse tumbleweed y a été installé sans aucune anomalie.

La machine possède plusieurs ports USB 3.2. Au niveau graphique, il y a un port display port et hdmi.

Au démarrage, il y a quelques erreur dans les logs

CPI BIOS Error (bug): Failure creating named object [\_SB.PCI0.XHC.RHUB.GPLD], AE_ALREADY_EXISTS (20210331/dswload2-326) 

ACPI Error: AE_ALREADY_EXISTS, During name lookup/catalog (20210331/psobject-220)
ACPI: Skipping parse of AML opcode: Method (0x0014)
ACPI BIOS Error (bug): Failure creating named object [\_SB.PCI0.XHC.RHUB.TPLD], AE_ALREADY_EXISTS (20210331/dswload2-326)
ACPI Error: AE_ALREADY_EXISTS, During name lookup/catalog (20210331/psobject-220) 

La mise en veille est effectué avec succès. Je n'ai pas remarqué de différence en terme de performance que je démarre sous Linux ou Windows.

Afin d'améliorer les performances, thermald peut être installé, ce n'est pas fait par défaut sous tumbleweed.

J'arrive sans peine à faire du montage vidéo, développement informatique et faire du traitement sur base de données.

Un point négatif est l'absence de port pour les cartes mémoire. J'ai résolue en acheté un lecteur Unitek usb 3.1.

Au final une petite machine professionel, bien supporté sous Linux et qui est silentieuse.


jeudi 31 décembre 2020

Architecture hexagonale

Introduction

Dans une application conventionnel, il ne sera pas rare d'avoir une couche de contrôleur,  de service et de dao. Pour chacune de nos entités, une classe existera sous chacune de ces couches. Il est possible de se retrouver rapidement avec des centaines de classes. Ce type d'architecture utilise un modèle d'objet anémique, c'est-à-dire qu'il ne contient pas de logique métier.

Il n'est pas rare de voir un appel à la couche de persistance directement de la première couche.

Les entités, les domains object n'ont pas de règle métier. Elle est alors mise dans les classes de service, utilitaire. Ces classes chargent des entités, modifient leurs états et persistent le tout. Les services commencent à utiliser d'autres services et il n'est pas rare de se retrouver avec des classes de plusieurs milliers de lignes. Pour une petite application, de type CRUD, ça peut être plus rapide à développer, mais imaginer maintenant une application avec 200K, 300K... lignes de code.

C'est une architecture souvent basée sur le design de la base de données. Une modification au sein de celle-ci et des modifications à chaque couche est nécessaire.

Plus l'application prendra de l'expansion, plus il deviendra difficile de la modifier sans causer de multiples anomalies.

Une bonne architecture doit pouvoir s'adapter à de nouvelles exigences sans tout chambouler.

Une architecture hexagonale pourrait être une approche à songer pour débuter sans trop devoir tout changer si on désirait passer à des microservices par la suite.

Architecture hexagonale

Le but de ce type d'architecture est d'avoir un noyau relié à la logique métier. Tout le reste est externalisé tel que la sauvegarde de donnée ou bien la réception de donnée. Cette conception est priorisée en DDD: domain-driven design.  

 


Avantages

  • Indépendance de la couche métier aux autres couches, technologie
  • Test plus facile à effectuer

 Désavantage

  • Plus de code nécessaire
  • Structure plus complexe

 

Couche

Cette architecte implique de diviser l'application en trois couches:

  1. Application
  2. Domaine
  3. Infrastructure

Application

Cette couche est le point d’interaction de l’application. Il peut s'agir de contrôleur rest, de service de messages. C'est la porte d'entrée pour exécuter le domaine métier.  Cette portion est parfois appelée espace utilisateur.

Domaine

C'est l'implémentation de la couche métier, c'est le noyau central de l'application. Il se trouve aussi les interfaces de communication avec les deux autres couches. Il est possible de rencontrer cette couche sous l’appellation règle d'affaires.

Infrastructure

Cette couche possède l'implémentation pour sauvegarder dans une base de données, fichier, web service externe. C'est le point de sortie du domaine métier. Un synonyme pour cette couche est espace serveur.

Principe

Le domaine n'a pas connaissance de l'application et de l'infrastructure. C'est là qu'entrent en jeu les adapteurs et les ports. Les ports sont des interfaces que les adapteurs implémenteront. Chaque couche peut être testée individuellement, de plus l'utilisation d'interface ajoute un niveau d'indépendance au niveau de l'implémentation. Pour un test, les données pourraient être sauvegardées dans un fichier alors qu'en production la sauvegarde se ferait dans une base de données.

Contexte, limite d'un domaine

Contrairement à une application monolithique, l'architecture hexagonale nécessite de découper selon le contexte (bounded context). On se retrouve avec plusieurs petits modèles au lieu d'un modèle unique. 

Un contexte peut être vu comme les limites d'un sous-domaine dans une application auquel une certaine logique métier peut être appliqué.

Si on prend une application telle qu'amazon: il en existe plusieurs inventaire, prix, produit, commentaire.

 
L'interface graphique accède une multitude de microservice délimité par leur domaine. Si les instances du service de commentaire sont en arrêt, un client pourra tout de même acheter un livre.
 

Objet valeur (Value Object)

C'est un objet immuable qui décrit un aspect du domaine mais qui n'a pas d'identité. De la logique métier est fortement conseillé d'y être présent. Un âge ou bien une adresse pourrait être présenté de la sorte. Il faut considérer que deux VO sont égales si l'ensemble de ses champs sont identiques.

public final class Age{
     private final int value;
 
     public Age(int value){
        if(age < 0 || age > 120){
            throw new InvalidAgeException()
        }
        this.value=value;
     }

     public Age change(int value){
         return new Age(value);
     }
}            

Il serait tout à fait possible de ne pas utiliser un objet dédié, mais il faudrait externaliser la logique métier.  D'ailleurs c'est souvent cette façon de faire qu'il est possible de voir, on se retrouve avec des validations à maint endroit et le risque d'erreur de tout genre s'accroît.

Agrégat

C'est un groupe d'objet métier: objet de valeur, entité qu'on manipule comme si ça serait qu'une seule entité métier. Pour une facture, il y a différents éléments qui peuvent la composer: émetteur, destinataire, date, liste des produits ou service, coût, taxe. Toute modification devrait se faire au sein de notre agrégat afin de garantir sa cohérence. Si la facture est déjà payée, il ne devrait pas être possible d'ajouter d'élément dedans. Si un agrégat a besoin des informations d'un autre agrégat, son identifiant peut-être utilisé. Dans une transaction, il devrait avoir qu'une sauvegarde pour un agrégat.

Il faut prendre en considération que pour une situation donnée, un objet pourrait être un objet de valeur et dans un autre cas une entité ou même un agrégat.

Est-ce qu'on se soucie des valeurs de l'objet ou bien de l'identification d'une instance? Ce questionnement peut nous aider à choisir entre une value objet ou bien un agrégat. Pour spécifier l'identité d'un agrégat, une bonne pratique est d'utiliser une VO. 

Au lieu de faire

public class Invoice{
    int id;
}
 
Passer par un VO dans l'agrégat 

public class Invoice{
    InvoiceId id;
}

public class InvoiceId{
    int id; 
    InvoiceId(int id){
        if(id < 1){
            throw new ArgumentException("Id must have a minimum value of 1");
        } 
        this.id=id; 
    }
}
 
Dans le cas de notre InvoiceId, le id pourrait être la date du jour: 20201229 suivit d'un chiffre  qui s'incrémente et se réinitialise chaque jour. 

Il est plus aisé de changer le type de donnée par la suite, il y a moins d'endroits à effectuer les changements. Il est possible d'ajouter de la logique pour en autre effectuer des validations. Il est plus difficile de passer une mauvaise valeur que si on utilisait le type primitif.

Mise en pratique

Notre exemple consistera à une application de facturation, il pourrait avoir un contexte de facturation, client, paiement.... nous développerons celui de facturation. Plus précisément le scénario d'aller chercher une Invoice selon son Id. Notre application sera monolithique mais sous-divisée. Chacun des modules devrait utiliser une architecture hexagonale.

Notre objet Invoice va représenter toutce qui doit être facturé à un customer. LineInvoice représenterait chaque item produit ou service acheté. Invoice est un bon candidat pour un agrégat, mais LineInvoice?

public class Invoice {

    private final InvoiceId id;
    private final InvoiceStatus status;
    private final Long customerId;
    private final LocalDate paidDate;
    private final LocalDate dueDate;
    private final Flux<InvoiceItem> invoiceItems;
    
    private Invoice(InvoiceId id, InvoiceStatus status, Long customerId,
            LocalDate paidDate, LocalDate dueDate,
            Flux<InvoiceItem> invoiceItems) {
        this.id = id;
        this.status = status;
        this.customerId = customerId;
        this.paidDate = paidDate;
        this.dueDate = dueDate;
        this.invoiceItems = invoiceItems;
    }
    
    public static Invoice withoutId(InvoiceStatus status, Long customerId,
            LocalDate paidDate, LocalDate dueDate,
            Flux<InvoiceItem> invoiceItems) {
        return new Invoice(null, status, customerId, paidDate, dueDate, invoiceItems);
    }
    
    public BigDecimal calculateTotalPrice() {
        if (invoiceItems == null) {
            return BigDecimal.ZERO;
        }

        return invoiceItems.map(ii -> ii.calculateTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add).block();
        
    }
}

LineInvoice pourrait être aussi un bon candidat pour un agrégat,  d'autres entités auraient besoin d'un identifiant. Il pourrait être possible d'appliquer une taxe, un rabais ou un frais d'envois spécifiques.

 public class InvoiceItem {
    private String description;
    private int quantity;
    private BigDecimal unitPrice;
    
    public InvoiceItem(String description, int quantity, BigDecimal unitPrice){
        if(quantity < 1){
            //quantity must be over 0
        }
        
        if(unitPrice.compareTo(BigDecimal.ZERO)!=1){
            //unit price must be over 0
        }
        this.description=description;
        this.quantity=quantity;
        this.unitPrice=unitPrice;
    }
    
    public BigDecimal calculateTotalPrice(){
      return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
    
}

 L'adapteur web

@RestController
@RequestMapping("/invoices")
public class GetInvoiceController {

    LoadInvoiceQuery loadInvoiceQuery;
    
    GetInvoiceController(LoadInvoiceQuery loadInvoiceQuery){
        this.loadInvoiceQuery=loadInvoiceQuery;
    }
    
    @GetMapping("/{id}")
    public Mono<Invoice> getInvoicesById(@PathVariable Long id){
        return loadInvoiceQuery.getInvoice(new InvoiceId(id));
    }
}

Le port pour la partie web    

public interface LoadInvoiceQuery {
    Mono<Invoice> getInvoice(InvoiceId invoiceId);
}

Le service au niveau du domaine

@Service
public class GetInvoiceService implements LoadInvoiceQuery{

    private final LoadInvoicePort loadInvoicePort;

    public GetInvoiceService(final LoadInvoicePort loadInvoicePort) {
        this.loadInvoicePort = loadInvoicePort;
    }
    
    @Override
    public Mono<Invoice> getInvoice(InvoiceId invoiceId){
        return loadInvoicePort.loadInvoice(invoiceId);
    }

}

L'adapteur de persistence

@Component
public class InvoicePersistenceAdapter implements LoadInvoicePort {

    @Autowired
    InvoiceMapper invoiceMapper;

    SpringPostgresInvoiceRepository invoiceRepository;
    SpringPostgresInvoiceLineRepository invoiceLineRepository;
    
    public InvoicePersistenceAdapter(SpringPostgresInvoiceRepository invoiceRepository,
            SpringPostgresInvoiceLineRepository invoiceLineRepository) {
        this.invoiceRepository = invoiceRepository;
        this.invoiceLineRepository=invoiceLineRepository;
    }


    @Override
    public Mono<Invoice> loadInvoice(InvoiceId invoiceId) {

        Mono<InvoiceEntity> invoiceEntity = invoiceRepository.findById(invoiceId.getValue());
        Flux<InvoiceLineEntity> invoiceLineEntity = invoiceLineRepository.findByInvoiceId(invoiceId.getValue());
        return invoiceMapper.mapToDomainEntity(invoiceEntity, invoiceLineEntity);
    }

}

Pour la persistence, il est possible d'utiliser une base de données ou un schéma par contexte ou bien dans utiliser un global à l'application. Seul un contexte a été développé, cependant même en optant pour une seule base de données pour l'ensemble des contextes, si on a besoin d'une information d'un autre contexte, en utilisant le id tel que customerId dans notre exemple,  pour aller effectuer la requête. Ce qui est une bonne approche pour bien séparer les domaines.

Si vous avez besoin d'augmenter les performances, vous pouvez utiliser un load balancer ou un reverse proxy pour démarrer de multiple instance de votre application avant d'opter pour des microservice. 

 Les packages sont organisés par couche. Certains préfèrent une organisation par fonctionnalité.

Responsabilité de l'adapteur pour la bd

  • Prendre les données en entrée
  • Les convertir dans le format de la bd
  • Les envoyer à la bd
  • Retourner les donner de sortie de la bd

Si vous désirez utiliser un ORM,  il faut faire le pont entre la structure objet et ceux utilisés pour le domaine, car tel que déjà dit, la couche du domaine ne devrait pas être imprégnée d'un framework. Cependant, j'ai pu constater à quelques reprises que ce n'était pas toujours respecté. 

Dans une architecture hexagonale, on se retrouve avec un petit domaine, je trouve alors l'emploi d'un ORM moins intéressant. Si vous optez pour cette solution, MapStruct peut grandement simplifier la conversion en vos objets métier.

Dans notre exemple, nous avons utilisé r2dbc, afin que l'application soit de type reactive. Contrairement au ORM, les relations ne sont pas géré, il faut le faire manuellement.

Autrement, Spring Data JDBC peut être une autre solution.

À partir d'un Id, nous avons été chercher un objet de type InvoiceEntity, il a été ensuite convertir en Invoice pour la couche domaine, nous aurions pu le convertir dans un autre format selon les besoins au niveau du web. Certains pourraient trouver que le passage entre ces couches est inutile dans ce cas. Un appel direct à partir du web vers la bd pourrait être effectué. Je suis d'accord, à moins de gros problème de performance, je continuerais d'utiliser cette approche, une fois que des raccourcies sont mis en place, ils deviennent rapidement la norme et on se trouve avec une architecture loin des attentes initiales.


Communication / synchronisation

Vous voudriez développer un autre module tel que le Customer. Comment les différents modules pourraient communiquer entre eux?

Dépendant si le type de communication: synchrone, asynchrone, vous pouvez optez pour des requêtes HTTP, service d'agent de message (RabbitMQ, JMS...)

Conclusion

Vous êtes une startup, vous voyez en grand. Vous regardez ce qu'utilise Netflix et vous voulez faire pareil. Vous n'avez aucun produit, aucune ligne de code de fait, mais vous pensez que la seule solution pour gérer votre hypothétique centaine de millions de requêtes est les microservices.

Il ne faut pas négliger la complexité niveau infrastructure: une base de données par microservice, la gestion des multiples instances des microservices, la communication entre eux et leur surveillance. Ces particularités amènent à avoir une équipe devops très efficace.

L'utilisation d'une architecture hexagonale pourrait grandement simplifier la mise en place de votre application avant qu'un passage au microservice soit réellement nécessaire. Le but de l'article était de vous montrer une façon différente de faire une architecture logiciel, libre à vous de l'adapter selon vos besoins.


 
 

jeudi 30 avril 2020

Comment mapper des objets avec MapStruct

Il n'est pas rare dans une application de devoir copier les valeurs d'un type d'objet à un autre. Par exemple les objets de la couche métier à ceux de la présentation.

Une multitude de librairie existe en Java pour effectuer cette opération.
Dans cet article nous utiliserons un des plus rapides soit MapStruct. Une comparaison est disponible à cette url.

Mappage avec les mêmes noms de champs

public class Person{
    private String name;
    private String lastname;
    private int age;
}

public class PersonDto{
    private String name;
    private String lastname;

}

Il suffit de créer une interface

@Mapper
public interface PersonMapper {
    PersonDto personToPersonDTO(Person entity);
    Person epersonDTOtoPerson(PersonDto dto);
}

MapStruct générera une classe d'implémentation. Ne surtout pas hésiter à la regarder pour voir le code généré pour éviter des surprises.

Mappage avec des noms différents de champs

public class Person{
    private String firstname;
    private String lastname;
    private int age;
}

public class PersonDto{
    private String name;
    private String lastname;

}

Il suffit de créer une interface et de spécifier les noms de source et de destination.

@Mapper
public interface PersonMapper {
    @Mapping(target="lastname", source"firstname")
    PersonDto personToPersonDTO(Person entity);

    @Mapping(target="firstname", source="name")
    Person epersonDTOtoPerson(PersonDto dto);
}

Mappage d'une valeur par défaut

 Si un champ est null, il est possible d'assigner une valeur par défaut.


@Mapper
public interface PersonMapper {
    @Mapping(target="lastname", source"firstname", defaultValue="bob")
    PersonDto personToPersonDTO(Person entity);
 }

Mettre à jour un objet existant

@Mapper
public interface PersonMapper {

    void updatePersonFromDto(
PersonDto dto, @MappingTarget Person entity);
}


Il suffit d'ajouter l'annotation MappingTarget à l'objet existant (l'entité devant être mis à jour).

Une multitude de possibilités existe, regardez la documentation

J'ai longtemps hésité à utiliser ce type de produit, car pour des cas plus complexes il peut être plus rapide de programmer soit même les affectations que de bien configurer l'outil et vérifier par la suite si le code généré correspondant à nos attentes.

mercredi 1 janvier 2020

Générer un rapport grâce à Thymeleaf et Open HTML to PDF

Une multitude de produit existe pour générer des rapports dans différents formats. Jaspert Report, iText. Certain sont plus bas niveau tel que PDFbox. Le moteur de template Thymeleaf permet de générer des pages web. Ensuite il est possible d'utiliser une librarie tierce tel que Open Html To pdf pour générer un pdf.

C'est la manière la plus simple et rapide que j'ai trouvé pour générer un pdf.

Spring boot sera utilisé, par défaut lorsqu'il est utilisé avec Thymeleaf, une configuration est généré afin de pouvoir généré des pages web.

Une autre configuration doit être créé pour être indépendant de celle-ci.

@Component
public class PdfGeneratorUtil<T> {

    @Autowired
    private TemplateEngine templateEngine;

    public byte[] process(String templateName, String templateExtension, List<T> listT, String contextVariableName) throws Exception {
        Context ctx = new Context();
        ctx.setVariable(contextVariableName, listT);
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        String processedHtml = templateEngine.process("fragments/html-reports/" + templateName + "." + templateExtension, ctx);

        PdfRendererBuilder builder = new PdfRendererBuilder();
        builder.useFastMode();
        builder.withHtmlContent(processedHtml, "");

        builder.toStream(output);
        builder.run();
        return output.toByteArray();
    }
}

Au niveau du contrôleur

public ResponseEntity<byte[]> getPdfReport(Model model) throws Exception {

        List<User> users = new ArrayList<>();

        users.add(new User("Yvan", "Dubois"));
        users.add(new User("Yvon", "Couler"));
        users.add(new User("Ytord", "Lamope"));

        byte[] content = pdfGeneratorUtil.process("usersReport", "html", users, "users");

        return preparePdfReport(content);
    }


Cette méthode permet de télécharger le pdf généré.

private ResponseEntity<byte[]> preparePdfReport(byte[] content) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("application/pdf"));
        String fileName = "report.pdf";

        headers.add("Content-Disposition", "inline;filename=" + fileName);
        headers.setCacheControl("no-cache, must-revalidate, post-check=0, pre-check=0");
        ResponseEntity<byte[]> response = new ResponseEntity<>(content, headers, HttpStatus.OK);
        return response;
    }


Cette portion de code permet d'assigner une liste d'objets à une variable dans un template Thymeleaf.  La classe PdfRendererBuilder prendra cette page et génèrera un pdf.

La mise en page peut être définie dans le template Thymeleaf dans la section css.

@page
{
    size: letter portrait;
    margin-left: 10px;
    margin-right:15px;
}


.new-page{
    page-break-after:always;
}


Avec des possibilités de condition dans Thymeleaf et le css, il est aisé d'arriver d'allure professionel.

lundi 30 décembre 2019

Chargement d'un fragment Thymeleaf en Ajax

Même si le moteur de template Thymeleaf est exécuté du côté serveur, il est possible de charger un fragment à la suite d'un évènement.

Dans notre page, nous avons une section avec l'id main

<div id="main" class="container-fluid">

</div>


Le fragment sera inséré à cet endroit après un appel sur le serveur.

 var XHR = new XMLHttpRequest();
 XHR.open('get', '/ajaxfragment');
 XHR.send();

 XHR.addEventListener('readystatechange', function() {
     if (XHR.readyState === XMLHttpRequest.DONE && XHR.status === 200) {
         document.getElementById("main").innerHTML = XHR.responseText;
     }

});

Le code du contrôleur qui nous retourne notre fragment Thymeleaf.

@GetMapping(value = {"/ajaxfragment"})
public String getAjaxFragment(Model model) {

    List<User> users = new ArrayList<>();

    users.add(new User("Yvan", "Dubois"));
    users.add(new User("Yvon", "Couler"));
    users.add(new User("Ytord", "Lamope"));

    model.addAttribute("users", users);

    return "fragments/ajax::Ajax";

}

Le fragment Thymeleaf

<div th:fragment="Ajax">

    <h3>Ajax fragment</h3>
    <table class="table">
        <tr th:each="user : ${users}">
            <td th:text="${user.firstname}" />
            <td th:text="${user.lastname}" />
        </tr>

    </table>
</div>


Cette stratégie peut être  utilisé afin de rendre plus dynamique certaine partie de votre applicaiton web.

Les sources du projets: https://github.com/marccollin/thymeleaf 

dimanche 29 décembre 2019

Les formulaires avec thymeleaf

Dans un précédent tutoriel, le moteur de thymeleaf a été présenté rapidement. Nous allons ici présenté comment utiliser un formulaire avec ce moteur de template.

Nous verrons deux façon d'appréhender les formulaires.

La sauvegarde est complètement différentes.

Le formulaire est utilisé pour la sauvegarde d'un utilisateur.

Le modèle est

public class User {

    private String firstname;
    private String lastname;

    private boolean enabled;
   
    private String userTypeId;

    ...
}

public class UserType {

    private Long id;
    private String type;

    ...
}

Le code pour afficher le formulaire est

@GetMapping(value = {"/userform"})
    public String getUserForm(Model model) {

        List<UserType> userTypes = new ArrayList<>();

        UserType userType1 = new UserType();
        userType1.setId(1l);
        userType1.setType("Admin");

        UserType userType2 = new UserType();
        userType2.setId(2l);
        userType2.setType("Standard");

        UserType userType3 = new UserType();
        userType3.setId(3l);
        userType3.setType("Invité");









        userTypes.add(userType1);
        userTypes.add(userType2);
        userTypes.add(userType3);


        model.addAttribute("user", new User());
        model.addAttribute("userTypes", userTypes);


        return "formuser";
    }


La page html ne fait qu'ajouter quelques marqueurs

<!DOCTYPE html>
<html>
    <head th:include="fragments/head :: HeadCss"/>
    <body>
        <div th:replace="fragments/top-menu :: TopMenu('user')"/>

        <h3>Form User</h3>

        <div id="main" class="container-fluid">

            <form action="#" th:action="@{/userform}" th:object="${user}" method="post">

                <div class="form-group row">
                    <label th:for="firstname" class="col-sm-2 col-form-label" >Prénom</label>
                    <div class="col-sm-10">
                        <input type="text" class="form-control" id="firstname" th:placeholder="#{user.firstname.placeholder}" th:field="*{firstname}" />
                    </div>
                </div>

                <div class="form-group row">
                    <label th:for="lastname" class="col-sm-2 col-form-label" >Nom</label>
                    <div class="col-sm-10">
                        <input type="text" class="form-control" id="lastname" th:placeholder="#{user.lastname.placeholder}" th:field="*{lastname}" />
                    </div>
                </div>

                <div class="form-group row">
                    <label th:for="userTypeId" class="col-sm-2 col-form-label" >Type</label>
                    <div class="col-sm-10">
                        <select id="type" th:field="*{userTypeId}">
                            <option th:each="userType : ${userTypes}" th:value="${userType.id}" th:text="${userType.type}"></option>
                        </select>
                    </div>
                </div>

                <div class="form-group row">
                    <label th:for="enabled" class="col-sm-2 col-form-label">Actif</label>
                    <div class="col-sm-10">
                        <input type="checkbox" th:field="*{enabled}" />
                    </div>
                </div>

                <div class="form-group">
                    <div class="col-sm-12">
                        <div class="float-right">
                            <button type="submit" class="btn btn-primary">Sauvegarder</button>
                            <button type="button" id="resetButton" class="btn btn-primary">Reset</button>
                        </div>
                    </div>
                </div>

            </form>

        </div>

    </body>
</html>


Dans le controller, deux variables sont assignés aux modèles via ces lignes

     model.addAttribute("user", new User());
     model.addAttribute("userTypes", userTypes);


Dans le formulaire,

th:action sert à spécifier l'url où sera soumis le formulaire
th:object sert à spécifier l'objet où sera lié les données  soumises au formulaire

Les attributs de l'objet sont accessible th:field



La sauvegarde du système

   @PostMapping(value = {"/userform"})
    public String savUserForm(@ModelAttribute User user) {

        System.out.println(user.toString());

        //do want you want with value
        return "savingok";
    }


La deuxiême façon nécessite un peu de javascript, car le mapping devra être fait manuellement.

<form id="form">
    <div class="form-group row">
        <label for="firstname" class="col-sm-2 col-form-label" >Prénom</label>
        <div class="col-sm-10">
            <input type="text" class="form-control" id="firstname" th:placeholder="#{user.firstname.placeholder}"  />
        </div>
    </div>

    <div class="form-group row">
        <label for="lastname" class="col-sm-2 col-form-label" >Nom</label>
        <div class="col-sm-10">
            <input type="text" class="form-control" id="lastname" th:placeholder="#{user.lastname.placeholder}"  />
        </div>
    </div>

    <div class="form-group">
        <div class="col-sm-12">
            <div class="float-right">
                <button type="button" class="btn btn-primary" onclick="saveForm()" >Sauvegarder</button>
                <button type="button" class="btn btn-primary">Reset</button>
            </div>
        </div>
    </div>
</form>


Pour le javascript



<script type="text/javascript">
    function toJSONString( form ) {
        var obj = {};
        var elements = form.querySelectorAll( "input, select, textarea" );
        for( var i = 0; i < elements.length; ++i ) {
                    var element = elements[i];
                    var id = element.id;
                    var value = element.value;

                    if( id ) {
                        obj[ id ] = value;
                    }
        }

        return JSON.stringify( obj );
    }

    function saveForm() {
                var XHR = new XMLHttpRequest();
                XHR.open('POST', 'http://localhost:8080/userformrest');
                XHR.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
                //JSON.stringify();

                var formElement = document.querySelector("form");
               
                var json = toJSONString(formElement);
               
                var formData = new FormData(formElement);

                XHR.send(json);

    }
</script> 

Le paramètre pour la sauvegarde est différent. Ce n'est plus l'annotation ModelAttribute qui est utilisé mais bien RequestBody.

    @PostMapping(value = {"/userformrest"})
    @ResponseBody
    public ResponseEntity savUserFormRest(@RequestBody User user) {

        System.out.println(user.toString());

        //do want you want with value
        return new ResponseEntity<>(HttpStatus.OK);
    }


Le code est disponible à https://github.com/marccollin/thymeleaf.

dimanche 1 décembre 2019

Débuter rapidement avec Thymeleaf

Débuter rapidement avec Thymeleaf

Thymeleaf est un moteur de template côté serveur. Tel que JSP, JSF, GWT, les pages sont généré sur le serveur. Ce moteur est entièrement supporté par Spring Boot.

Ce moteur est majoritairement utilisé dans des applications, mais il peut être utilisé dans la construction de page pdf.

Thymeleaf utilise des pages html auquel des marqueurs (commande) sont ajouté pour aider le moteur à faire certain traitement.

Les exemples utilisés sont basé sur un projet en spring boot 2.2

public class User {

 private String firstname;

 public User(){
   
  public String getFirstname() {
        return firstname;
    }
    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

}

@Controller
public class UserController {

    private final UserService userService;
   
    public UserController(final UserService userService){
        this.userService=userService;
    }
   

    @GetMapping(value="/userlist")
    public String userList(Model model) {

        List<User> usersList = userService.findAll();

        model.addAttribute("users", usersList);

        return "userList";
    }
}


Le fichier html

<!DOCTYPE html>
<html>
    <head>
        <title>User list</title>
    </head>
    <body>
        <table>
            <tr th:each="user : ${users}">

                <td th:text="#{firstname}"></td>
                <td th:text="${user.firstname}"></td>
            </tr>
        </table>
    </body>
</html>



th:each  et th:text sont des marqueurs. Le premier permet de boucler sur la listes de users alors que le second permet d'afficher.

Au niveau du contrôleur, la variable usersList est mise dans le modèle avec le nom users.
Dans le fichier html, on y accède en bouclant sur la même variable avec la syntaxe ${nom de ma variable}.

L'usage #{...} permet d'avoir accès au fichier d'internalisation.

Dans le répertoire resources les fichiers
  • messages.properties
  • messages_en.properties
permettent de passer d'une langue à une autre.

Dans le fichier messages.properties

firstname=Prénom

et dans le fichier messages_en.properties

firstname=Firstname.

Condition

L'opérateur Elvis est disponible avec la même notation.

Condition ? true : false

Si la valeur du firstname est Paul alors son fond de couleur sera blanc.

Afin que ça puisse fonctionner, il faut utiliser le marqueur th:style.

<!DOCTYPE html>
<html>
    <head>
        <title>User list</title>
    </head>
    <body>
        <table>
            <tr th:each="user : ${users}">

                <td th:text="#{firstname}"></td>
                <td th:text="${user.firstname}" th:style="${user.firstname=='Paul'} ? 'background: #ffffff' : ''"></td>
            </tr>
        </table>
    </body>
</html>


Un projet basique est disponible.