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.

vendredi 27 juillet 2018

Types de paramètres pour un contrôleur

Types de paramètres pour un contrôleur


Les exemples utilisent spring boot 2.

Nous allons voir comment effectuer une sauvegarde de donnée à l'aide de Spring.

Nous allons aborder l'approche avec un modeleAttribute et un RequestBody

Avec un requestBody

Tous les paramètres passent en JSon ou xml. Il est préférable d'employé un request body lors d'un envoi en ajax, même s'il est possible de le faire avec un modelAttribute.

Côté Client

$("#colorsForm").submit(function (e) {
    e.preventDefault();
    var colorsId= $('#colorsForm input[name="id"]').val();
    var form = transForm.serialize('#colorsForm');
    form = JSON.stringify(form);

    $.ajax({
        type: "post",
        url: "/template/edit/colors/" + colorsId,
        data: form,
        contentType: "application/json",
        dataType : "json",
        success: function (data) {
            $('#colorsForm input[name="id"]').val(data.id);
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert("Erreur");
        }
    });


Côté serveur

    @PostMapping("template/edit/colors/{id}")
    @ResponseBody
    public ColorsDto edit(Model model, @RequestBody ColorsDto dto, @PathVariable("id") Integer id)  {
        return colorsService.save(dto);
    }



Avec un modeleAttribute

Tous les paramètres sont envoyé via l'url, cette façon de faire est à opter lorsqu'on désire lier un formulaire à un object du côté serveur.

Côté Client
   
$("#colorsForm").submit(function (e) {
     e.preventDefault();
    
     var colorsId = $('#colorsForm input[name="id"]').val();
    
     $.ajax({
         type: "post",
         url: "/template/edit/colors/" + colorsId,
         data: $("#colorsForm").serialize(),
         success: function (data) {
             $('#colorsForm input[name="id"]').val(data.id);
         },
         error: function (XMLHttpRequest, textStatus, errorThrown) {
             alert("Erreur");
         }
     });

 });

});



Côté serveur

    @PostMapping("template/edit/colors/{id}")
    @ResponseBody
    public ColorsDto edit(Model model, @ModelAttribute ColorsDto dto, @PathVariable("id") Integer id) {
        returncolorsService.save(dto);
    }

   
    Les deux façons de faire permettent d'arriver au même résultat.

vendredi 15 juin 2018

Datatable et la pagination de Spring Data

Datatable est un composant JQuery qui fournit divers fonctionalités: pagination, trie, filtre concernant un tableau (grid).  C'est un des plus évolué dans son domaine.

Voici l'initialisation du datatable en Javascript. La partie en gras concernant le passage des paramètres côté client au côté serveur.

var samplingsTable = $('#samplingsTable').DataTable({
    'bLengthChange': false, //hide 'show entries dropdown
    'processing': true,
    'serverSide': true,
    'pagingType': 'simple_numbers',
    'dom': 'Bfrtip',
    'ajax': {
        'type': 'get',
        'url': url,
        'data': function(d) {
            var current = $('#samplingsTable').DataTable();
            d.page = (current != undefined) ? current.page.info().page : 0;
            d.size = (current != undefined) ? current.page.info().length : 5;
            d.sort = d.columns[d.order[0].column].data + ',' + d.order[0].dir;
            d.search = d.search.value;
        }
    },
    'columns': [
      {'data': 'compositeId'}, 
      {'data': 'buildDate'}, 
      {'data': 'productTypesName'}, 
      {'data': 'productsName'}, 
      {'data': 'machineName'}
     ]
});

Niveau serveur, il faut faire le pont du côté serveur à client. Le paramètre draw est un champ utilisé par Datatable.

@GetMapping("samplings")
@ResponseBody
public Map<String, Object> getSamplings(Pageable pageable,
            @RequestParam("draw") Integer draw,

            @RequestParam(value = "search", defaultValue = "") String search) {

             Map<String, Object> data = new HashMap<>();

             Page<SamplingsDto> newPage  = samplingsService.get(pageable);

             data.put("data", newPage.getContent());
             data.put("draw", draw);
             data.put("recordsTotal", newPage.getTotalElements());
             data.put("recordsFiltered", newPage.getTotalElements());

             return data;
}

Il y a d'autre façon de procéder. tel que tout faire sur le serveur ou le client. La façon présenté ici est hybride.

Triage de clé composite avec Spring Data

Une clé primaire est très souvent lié à un champs. Il est possible d'en composer une à l'aide de plusieurs champs. Dans notre sous nous utliserons spring boot 2, jpa avec l'implémentation hibernate et postgres sql. Il est possible que ça fonctionne avec d'autre base de donnée et d'autre implémentation de JPA. La clé primaire sera composé de deux champs. Soit un Id issue d'une séquence et de l'année en cours.

@Entity
@IdClass(SamplingsPK.class)
public class Samplings {

    @Id
    private Integer year;

    @Id
    @GeneratedValue
    private Integer id;
}

public class SamplingsPK implements Serializable {

    private int year;
    private Integer id;

    public SamplingsPK(int year, Integer id) {
        this.id = id;
        this.year=year;
    }

    private SamplingsPK(){
        
    } 

    @PrePersist
    public void prePersist() {
        year = LocalDate.now().getYear();
    }

}

Puisque nous utilisons une clé composé, au niveau du repository, il faut spécifié le type.

@Repository
public interface SamplingsRepository extends JpaRepository<Samplings, SamplingsPK> {
}

Lorsque vous utiliser le paging dans Spring Data, il faut mentionner les champs de la clé composé.

Pageable pageable = PageRequest.of(1, 1, Sort.Direction.ASC, "year","id");

samplingsRepository.finAll(pageable);

Nous avons vue comment utiliser une clé composé  lors de l'utilisation de la pagination avec Spring data.