8. Casestudy
8.1. Introduction
We willen een webapplicatie voor het maken van afspraken voor een medische praktijk schrijven. Dit probleem is behandeld in het document 'Tutorial AngularJS / Spring 4' op de URL [http://tahe.developpez.com/angularjs-spring4/]. De architectuur van deze applicatie was als volgt:
![]() |
- in [1] levert een webserver statische pagina’s aan een browser. Deze pagina’s bevatten een applicatie AngularJS die is opgebouwd volgens het MVC-model (Model – View – Controller). Het model is hier zowel dat van de weergaven als dat van het domein, hier vertegenwoordigd door de laag [Services];
- de gebruiker zal interactie hebben met de weergaven die hem in de browser worden getoond. Zijn acties zullen soms vereisen dat de Spring 4-server [2] wordt geraadpleegd. Deze verwerkt het verzoek en retourneert een antwoord jSON (JavaScript Object Notation) [3]. Dit antwoord wordt gebruikt om de aan de gebruiker getoonde weergave bij te werken.
We stellen voor om deze applicatie over te nemen en deze van begin tot eind te implementeren met Spring MVC. De architectuur ziet er dan als volgt uit:
![]() |
De browser maakt verbinding met een applicatie [Web 1], geïmplementeerd met Spring MVC, die haar gegevens ophaalt bij een webservice [Web 2], eveneens geïmplementeerd met Spring MVC.
8.2. Functies van de applicatie
De lezer wordt uitgenodigd om de functionaliteiten van de applicatie te ontdekken door deze te testen. We laden in STS de Maven-projecten uit de map [etude-de-cas]:
![]() | ![]() |
Allereerst gaan we de database MySQL 5 [dbrdvmedecins] aanmaken met de tool [Wamp Server] (zie paragraaf 9.5):
![]() |
- in [1] selecteren we de tool [phpMyAdmin] van WampServer;
- in [2] kiest men de optie [Importer];
![]() |
- in [3] selecteer je het bestand [database/dbrdvmedecins.sql];
- bij [4] voert u het uit;
- in [5] wordt de database aangemaakt.
Vervolgens moeten we de server starten die met de database is verbonden. Dit is het project [rdvmedecins-webjson-server]
![]() |
De server zal beschikbaar zijn via URL en [http://localhost:8080]. Dit kan worden gewijzigd in het bestand [application.properties] van het project:
![]() |
server.port=8080
De instellingen voor de toegang tot de database zijn opgeslagen in de klasse [DomainAndPersistenceConfig] van het project [rdvmedecins-metier-dao]:
![]() |
// de gegevensbron MySQL
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
Als u SGBD MySQL met andere inloggegevens opent, gebeurt het hier.
Vervolgens starten we, op dezelfde manier als bij de vorige server, de server [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Deze server is standaard beschikbaar op URL [http://localhost:8081]. Ook dit is configureerbaar in het projectbestand [application.properties]:
server.port=8081
Bovendien moet deze server de URL kennen van de server die met de database is verbonden. Deze configuratie is te vinden in de bovenstaande klasse [AppConfig]:
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// webservice-root / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// time-out in milliseconden
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
Als de eerste server op een andere poort dan 8080 is gestart, moet regel 5 worden aangepast.
Vervolgens roept u met een browser de URL en [http://localhost:8081/boot.html] op:
![]() |
- naar [1], de startpagina van de applicatie;
- in [2] en [3], de gebruikersnaam en het wachtwoord van degene die de applicatie wil gebruiken. Er zijn twee gebruikers: admin/admin (login/wachtwoord) met een rol (ADMIN) en user/user met een rol (USER). Alleen de rol ADMIN heeft het recht om de applicatie te gebruiken. De rol USER is er alleen om te laten zien wat de server in dit gebruiksscenario antwoordt;
- in [4], de knop waarmee je verbinding kunt maken met de server;
- in [5], de taal van de applicatie. Er zijn er twee: standaard Frans en Engels;
- in [6], de URL van de server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1] log je in;
![]() |
- zodra je bent ingelogd, kun je de arts kiezen bij wie je een afspraak wilt maken [2] en de dag waarop die plaatsvindt [3]. Zodra een arts en een dag zijn ingevuld, wordt de agenda automatisch weergegeven:
![]() |
- Zodra de agenda van de arts is opgevraagd, kun je een tijdvak reserveren [5];
![]() |
- in [6] kiest men de patiënt voor de afspraak en bevestigt men deze keuze in [7];
![]() |
Zodra de afspraak is bevestigd, keert men automatisch terug naar de agenda, waar de nieuwe afspraak nu is opgenomen. Deze afspraak kan later worden verwijderd met [8].
De belangrijkste functies zijn nu beschreven. Ze zijn eenvoudig. Laten we afsluiten met het taalbeheer:
1

- in [1] schakelt u over van het Frans naar het Engels;
![]() |
- in [2] is de weergave overgeschakeld naar het Engels, inclusief de kalender;
8.3. De database
![]() |
De database die hierna [dbrdvmedecins] wordt genoemd, is een database MySQL5 met de volgende tabellen:
![]() |
De afspraken worden beheerd door de volgende tabellen:
- [medecins]: bevat de lijst met artsen van de praktijk;
- [clients]: bevat de lijst met patiënten van de praktijk;
- [creneaux]: bevat de beschikbare tijdvakken van elke arts;
- [rv]: bevat de lijst met afspraken van de artsen.
De tabellen [roles], [users] en [users_roles] zijn tabellen die verband houden met de authenticatie. Daar gaan we ons in eerste instantie niet mee bezighouden. De relaties tussen de tabellen die de afspraken beheren, zijn als volgt:
![]() |
- een tijdslot behoort toe aan een arts – een arts heeft 0 of meerdere tijdslots;
- een afspraak brengt zowel een klant als een arts samen via een tijdslot van die arts;
- een klant heeft 0 of meerdere afspraken;
- aan een tijdslot zijn 0 of meerdere afspraken gekoppeld (op verschillende dagen).
8.3.1. De tabel [MEDECINS]
Deze bevat informatie over de artsen die worden beheerd door de applicatie [RdvMedecins].
![]() | ![]() |
- ID: identificatienummer van de arts – primaire sleutel van de tabel
- VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
- NOM: de achternaam van de arts
- PRENOM: zijn of haar voornaam
- TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)
8.3.2. De tabel [CLIENTS]
De cliënten van de verschillende artsen worden opgeslagen in de tabel [CLIENTS]:
![]() | ![]() |
- ID: identificatienummer van de klant – primaire sleutel van de tabel
- VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
- NOM: de naam van de klant
- PRENOM: de voornaam
- TITRE: zijn/haar aanspreektitel (mevrouw, meisje, heer)
8.3.3. De tabel [CRENEAUX]
Deze tabel geeft een overzicht van de tijdvakken waarin de RV mogelijk zijn:
![]() |
![]() |
- ID: nummer dat het tijdvak identificeert – primaire sleutel van de tabel (regel 8)
- VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt met 1 verhoogd telkens wanneer er een wijziging in de rij wordt aangebracht.
- ID_MEDECIN: nummer dat de arts identificeert aan wie dit tijdslot toebehoort – vreemde sleutel op de kolom MEDECINS (ID).
- HDEBUT: starttijd van het tijdvak
- MDEBUT: minuten begin van het tijdslot
- HFIN: einduur van het tijdvak
- MFIN: minuten einde tijdslot
De tweede regel van de tabel [CRENEAUX] (zie [1] hierboven) geeft bijvoorbeeld aan dat tijdvak nr. 2 om 8.20 uur begint en om 8.40 uur eindigt en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER).
8.3.4. De tabel [RV]
Deze tabel geeft een overzicht van de RV die voor elke arts zijn vastgelegd:
![]() |
- ID: nummer dat de RV op unieke wijze identificeert – primaire sleutel
- JOUR: dag van de RV
- ID_CRENEAU: tijdvak van RV – externe sleutel op het veld [ID] van de tabel [CRENEAUX] – bepaalt zowel het tijdvak als de betreffende arts.
- ID_CLIENT: nummer van de klant voor wie de reservering is gemaakt – externe sleutel op het veld [ID] van de tabel [CLIENTS]
Deze tabel heeft een uniekheidsbeperking op de waarden van de gekoppelde kolommen (JOUR, ID_CRENEAU):
Als een rij in de tabel [RV] de waarde (JOUR1, ID_CRENEAU1) heeft voor de kolommen (JOUR, ID_CRENEAU), dan mag deze waarde nergens anders voorkomen. Anders zou dit betekenen dat er twee RV’en tegelijkertijd voor dezelfde arts zijn vastgelegd. Vanuit het oogpunt van Java-programmering start de driver JDBC van de database een SQLException wanneer dit zich voordoet.
De regel met id gelijk aan 3 (zie [1] hierboven) betekent dat er op 23/08/2006 een RV is geboekt voor tijdvak nr. 20 en klant nr. 4. Uit de tabel [CRENEAUX] blijkt dat tijdvak nr. 20 overeenkomt met het tijdvak 16.20 - 16.40 uur en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER). Uit de tabel [CLIENTS] blijkt dat klant nr. 4 mevrouw Brigitte BISTROU is.
8.3.5. Het aanmaken van de database
Om de database [dbrdvmedecins] aan te maken, wordt een script [dbrdvmedecins.sql] meegeleverd met de voorbeelden in dit document [1-3]:
![]() |
We gebruiken de tool [PhpMyAdmin] van WampServer:
![]() |
- in [1] selecteren we de tool [phpMyAdmin] van WampServer;
- in [2] kiest men de optie [Importer];
![]() |
- in [3] selecteer je het bestand [database/dbrdvmedecins.sql];
- bij [4] voert u het uit;
- in [5] wordt de database aangemaakt.
8.4. De webservice / jSON
![]() |
In de bovenstaande architectuur gaan we nu in op de opbouw van de webservice / jSON, gebouwd met het Spring-framework MVC. We zullen dit in verschillende stappen schrijven:
- eerst de lagen [métier] en [DAO] (Data Access Object). Hier gebruiken we Spring Data;
- vervolgens de webservice jSON zonder authenticatie. Hier gebruiken we Spring MVC;
- daarna voegen we het authenticatiegedeelte toe met Spring Security.
Het volgende is een kopie van het document [http://tahe.developpez.com/angularjs-spring4/], maar met enkele wijzigingen.
8.4.1. Inleiding tot Spring Data
We gaan de laag [DAO] van het project implementeren met Spring Data, een onderdeel van het Spring-ecosysteem.
![]() |
Op de website van Spring zijn talrijke tutorials te vinden om aan de slag te gaan met Spring [http://spring.io/guides]. We gaan er een gebruiken om Spring Data te introduceren. Hiervoor gebruiken we de Spring Tool Suite (STS).
![]() |
- in [1] importeren we een van de tutorials uit [spring.io/guides];
![]() |
- in [2] kiezen we de tutorial [Accessing Data Jpa] die laat zien hoe je met Spring Data toegang krijgt tot een database;
- in [3] kiezen we een project dat door Maven is geconfigureerd;
- in [4] kan de tutorial in twee vormen worden aangeboden: [initial], een lege versie die je vult terwijl je de tutorial volgt, of [complete], de definitieve versie van de tutorial. We kiezen voor deze laatste;
- in [5] kun je ervoor kiezen om de tutorial in een browser te bekijken;
- in [6], het uiteindelijke project.
8.4.1.1. De Maven-configuratie van het project
De Maven-afhankelijkheden van het project worden geconfigureerd in het bestand [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- gebruik UTF-8 voor alles -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- regels 5-9: definiëren een bovenliggend Maven-project. Dit project bepaalt het grootste deel van de afhankelijkheden van het project. Deze kunnen voldoende zijn, in welk geval er niets wordt toegevoegd, of onvoldoende, in welk geval de ontbrekende afhankelijkheden worden toegevoegd;
- regels 12-15: definiëren een afhankelijkheid van [spring-boot-starter-data-jpa]. Dit artefact bevat de klassen van Spring Data;
- regels 16-19: definiëren een afhankelijkheid van SGBD en H2, waarmee in-memory-databases kunnen worden aangemaakt en beheerd.
Laten we eens kijken naar de klassen die door deze afhankelijkheden worden geleverd:
![]() | ![]() | ![]() |
Het zijn er heel veel:
- sommige behoren tot het Spring-ecosysteem (die welke beginnen met spring);
- andere behoren tot het Hibernate-ecosysteem (hibernate, jboss), waarvan we hier de implementatie JPA gebruiken;
- weer andere zijn testbibliotheken (junit, hamcrest);
- weer andere zijn logboekbibliotheken (log4j, logback, slf4j);
We zullen ze allemaal behouden. Voor een applicatie in productie zouden we alleen die moeten behouden die noodzakelijk zijn.
Op regel 26 van het bestand [pom.xml] staat de volgende regel:
<start-class>hello.Application</start-class>
Deze regel is gekoppeld aan de volgende regels:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Regels 6-9: met de plug-in [spring-boot-maven-plugin] kan het uitvoerbare JAR-bestand van de applicatie worden gegenereerd. Regel 26 van het bestand [pom.xml] verwijst dan naar de uitvoerbare klasse van dit JAR-bestand.
8.4.1.2. De laag [JPA]
Toegang tot de database verloopt via een laag [JPA], Java Persistence API:
![]() |
![]() |
De applicatie is eenvoudig en beheert klanten [Customer]. De klasse [Customer] maakt deel uit van de laag [JPA] en ziet er als volgt uit:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Een klant heeft een ID [id], een voornaam [firstName] en een achternaam [lastName]. Elk exemplaar [Customer] vertegenwoordigt een rij in een databasetabel.
- regel 8: annotatie JPA, waardoor de persistentie van de instanties [Customer] (Create, Read, Update, Delete) wordt beheerd door een implementatie JPA. Uit de Maven-afhankelijkheden blijkt dat de implementatie JPA / Hibernate wordt gebruikt;
- regels 11-12: annotaties JPA die het veld [id] koppelen aan de primaire sleutel van de tabel [Customer]. Regel 12 geeft aan dat de implementatie JPA de methode voor het genereren van de primaire sleutel zal gebruiken die eigen is aan de gebruikte SGBD, in dit geval H2;
Er zijn geen andere annotaties voor JPA. Er worden dan standaardwaarden gebruikt:
- de tabel van [Customer] krijgt de naam van de klasse, d.w.z. [Customer];
- de kolommen van deze tabel krijgen de naam van de velden van de klasse: [id, firstName, lastName], waarbij hoofdletters en kleine letters in de naam van een tabelkolom niet van belang zijn;
Opgemerkt moet worden dat de gebruikte implementatie JPA op geen enkel moment bij naam wordt genoemd.
8.4.1.3. De laag [DAO]
![]() |
![]() |
De klasse [CustomerRepository] implementeert de laag [DAO]. De code ervan is als volgt:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Het gaat dus om een interface en niet om een klasse (regel 7). Deze interface is een uitbreiding van de interface [CrudRepository], een interface van Spring Data (regel 5). Deze interface wordt gedefinieerd door twee typen: het eerste is het type van de beheerde elementen, in dit geval het type [Customer], het tweede is het type van de primaire sleutel van de beheerde elementen, in dit geval het type [Long]. De interface [CrudRepository] ziet er als volgt uit:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Deze interface definieert de bewerkingen CRUD (Create – Read – Update – Delete) die kunnen worden uitgevoerd op een type JPA T:
- regel 8: met de methode `save` kan een entiteit van type T in de database worden opgeslagen. Hiermee wordt de entiteit opgeslagen met de primaire sleutel die door de methode SGBD is toegekend. Deze methode maakt het ook mogelijk om een entiteit van type T bij te werken die wordt geïdentificeerd door haar primaire sleutel id. De keuze voor de ene of de andere actie hangt af van de waarde van de primaire sleutel id: als deze null is, vindt de opslagbewerking plaats, anders de bijwerkingsbewerking;
- regel 10: idem, maar dan voor een lijst met entiteiten;
- regel 12: met de methode findOne kan een entiteit T worden opgehaald die wordt geïdentificeerd door de primaire sleutel id;
- regel 22: met de methode `delete` kan een entiteit T worden verwijderd die wordt geïdentificeerd door zijn primaire sleutel `id`;
- regels 24-28: varianten van de methode [delete];
- regel 16: met de methode [findAll] kunnen alle opgeslagen entiteiten T worden opgehaald;
- regel 18: idem, maar beperkt tot de entiteiten waarvan de lijst met identificatiecodes is doorgegeven;
Laten we teruggaan naar de interface [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- met regel 9 kan een [Customer] worden opgezocht op basis van de naam [lastName];
En dat is alles voor de laag [DAO]. Er is geen implementatieklasse voor de vorige interface. Deze wordt tijdens de uitvoering gegenereerd door [Spring Data]. De methoden van de interface [CrudRepository] worden automatisch geïmplementeerd. Voor de methoden die aan de interface [CustomerRepository] zijn toegevoegd, hangt het ervan af. Laten we teruggaan naar de definitie van [Customer]:
private long id;
private String firstName;
private String lastName;
De methode op regel 9 wordt automatisch geïmplementeerd door [Spring Data], omdat deze verwijst naar het veld [lastName] (regel 3) van [Customer]. Wanneer Spring Data een methode [findBySomething] tegenkomt in de te implementeren interface, implementeert het deze via de volgende JPQL-query (Java Persistence Query Language):
Het type T moet dus een veld hebben met de naam [something]. Zo wordt de methode
zal worden geïmplementeerd met code die er ongeveer als volgt uitziet:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
waarbij [em] verwijst naar de persistentiecontext JPA. Dit is alleen mogelijk als de klasse [Customer] een veld heeft met de naam [lastName], wat het geval is.
Kortom, in eenvoudige gevallen stelt Spring Data ons in staat om de laag [DAO] te implementeren met een eenvoudige interface.
8.4.1.4. De laag [console]
![]() |
![]() |
De klasse [Application] ziet er als volgt uit:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// een paar klanten opslaan
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// alle klanten ophalen
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// haal een individuele klant op via ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// klanten op achternaam ophalen
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- regel 10: geeft aan dat de klasse dient om Spring te configureren. Recente versies van Spring kunnen namelijk in Java worden geconfigureerd in plaats van in XML. Beide methoden kunnen tegelijkertijd worden gebruikt. In de code van een klasse met de annotatie [Configuration] vinden we normaal gesproken Spring-beans, d.w.z. definities van klassen die moeten worden geïnstantieerd. Hier is geen enkele bean gedefinieerd. We moeten hier nogmaals benadrukken dat wanneer we met een SGBD werken, diverse Spring-beans moeten worden gedefinieerd:
- een [EntityManagerFactory] die de te gebruiken implementatie JPA definieert,
- een [DataSource] die de te gebruiken gegevensbron definieert,
- een [TransactionManager] die de te gebruiken transactiebeheerder definieert;
Hier is geen van deze beans gedefinieerd.
- regel 11: de annotatie [EnableAutoConfiguration] is een annotatie afkomstig uit het project [Spring Boot] (regels 5-6). Deze annotatie vraagt Spring Boot via de klasse [SpringApplication] (regel 16) om de applicatie te configureren op basis van de bibliotheken die in het Classpath worden aangetroffen. Omdat de Hibernate-bibliotheken in het Classpath staan, zal de bean [entityManagerFactory] met Hibernate worden geïmplementeerd. Omdat de bibliotheek SGBD H2 in het classpath staat, wordt de bean [dataSource] geïmplementeerd met H2. In de bean [dataSource] moeten ook de gebruiker en het wachtwoord worden gedefinieerd. Hier zal Spring Boot de standaardbeheerder van H2 gebruiken, die geen wachtwoord heeft. Omdat de bibliotheek [spring-tx] in het classpath staat, wordt de transactiebeheerder van Spring gebruikt.
Bovendien wordt de map waarin de klasse [Application] zich bevindt, gescand op zoek naar beans die impliciet door Spring worden herkend of expliciet zijn gedefinieerd door middel van Spring-annotaties. Zo worden de klassen [Customer] en [CustomerRepository] geïnspecteerd. Omdat de eerste de annotatie [@Entity] heeft, wordt deze gecatalogiseerd als een entiteit die door Hibernate moet worden beheerd. Omdat de tweede de interface [CrudRepository] uitbreidt, wordt deze geregistreerd als een Spring-bean.
Laten we de regels 16-17 van de code eens bekijken:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- regel 16: de statische methode [run] van de klasse [SpringApplication] uit het Spring Boot-project wordt uitgevoerd. De parameter hiervan is de klasse die een annotatie [Configuration] of [EnableAutoConfiguration] heeft. Alles wat hierboven is uitgelegd, zal dan plaatsvinden. Het resultaat is een Spring-applicatiecontext, d.w.z. een verzameling beans die door Spring worden beheerd;
- regel 17: we vragen deze Spring-context om een bean die de interface [CustomerRepository] implementeert. We halen hier de klasse op die door Spring Data is gegenereerd om deze interface te implementeren.
De volgende bewerkingen maken uitsluitend gebruik van de methoden van de bean die de interface [CustomerRepository] implementeert. Let op regel 50: de context wordt afgesloten. De uitvoer op de console is als volgt:
- regels 1-8: het logo van het Spring Boot-project;
- regel 9: de klasse [hello.Application] wordt uitgevoerd;
- regel 10: [AnnotationConfigApplicationContext] is een klasse die de Spring-interface [ApplicationContext] implementeert. Het is een bean-container;
- regel 11: de bean [entityManagerFactory] wordt geïmplementeerd met de klasse [LocalContainerEntityManagerFactory], een Spring-klasse;
- regel 15: [Hibernate] verschijnt. Er is gekozen voor deze implementatie, JPA;
- regel 19: een Hibernate-dialect is de variant SQL, die moet worden gebruikt in combinatie met SGBD. Hier geeft het dialect [H2Dialect] aan dat Hibernate gaat werken met de SGBD en H2;
- regels 21-22: de database wordt aangemaakt. De tabel [CUSTOMER] wordt aangemaakt. Dit betekent dat Hibernate is geconfigureerd om de tabellen te genereren op basis van de definities JPA, in dit geval de definitie JPA van de klasse [Customer];
- regels 27-31: de vijf ingevoerde klanten;
- regels 33635: resultaat van de methode [findOne] van de interface;
- regels 37-40: resultaten van de methode [findByLastName];
- regels 41 en volgende: logboeken van het afsluiten van de Spring-context.
8.4.1.5. Handmatige configuratie van het Spring Data-project
We dupliceren het vorige project in het project [gs-accessing-data-jpa-2]:
![]() |
In dit nieuwe project gaan we niet vertrouwen op de automatische configuratie door Spring Boot. We gaan dit handmatig doen. Dit kan handig zijn als de standaardconfiguraties niet aan onze eisen voldoen.
Allereerst gaan we de benodigde afhankelijkheden expliciet opgeven in het bestand [pom.xml]:
...
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring-transacties -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
...
</project>
- regels 2-18: de basisbibliotheken van Spring;
- regels 19-29: de Spring-bibliotheken voor het beheren van transacties met een database;
- regels 30-35: de Spring-bibliotheek voor het werken met een ORM (Object Relational Mapper);
- regels 36-41: Spring Data, gebruikt om toegang te krijgen tot de database;
- regels 42-47: Spring Boot om de applicatie te starten;
- regels 54-59: de SGBD H2;
- regels 60-70: databases worden vaak gebruikt met pools van open verbindingen, waardoor herhaaldelijk openen en sluiten van verbindingen wordt voorkomen. Hier wordt de implementatie van [commons-dbcp] gebruikt;
Nog steeds in [pom.xml] wordt de naam van de uitvoerbare klasse gewijzigd:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
In het nieuwe project blijven de entiteit [Customer] en de interface [CustomerRepository] ongewijzigd. We gaan de klasse [Application] wijzigen, die in twee klassen wordt opgesplitst:
- [Config], de configuratieklasse:
- [Main], de uitvoerbare klasse;
![]() |
De uitvoerbare klasse [Main] is dezelfde als voorheen, maar dan zonder de configuratie-annotaties:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- regel 12: de klasse [Main] heeft geen configuratie-annotaties meer;
- regel 16: de applicatie wordt gestart met Spring Boot. De parameter [Config.class] is de nieuwe configuratieklasse van het project;
De klasse [Config] die het project configureert, is als volgt:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// de gegevensbron H2
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// de provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transactiemanager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- regel 22: de annotatie [@Configuration] maakt van de klasse [Config] een Spring-configuratieklasse;
- regel 21: de annotatie [@EnableJpaRepositories] maakt het mogelijk om de mappen aan te wijzen waarin de Spring Data-interfaces [CrudRepository] zich bevinden. Deze interfaces worden Spring-componenten en zijn beschikbaar in de Spring-context;
- regel 20: de annotatie [@EnableTransactionManagement] geeft aan dat de methoden van de interfaces [CrudRepository] binnen een transactie moeten plaatsvinden;
- regel 19: met de annotatie [@EntityScan] kunnen de mappen worden opgegeven waarin naar de entiteiten JPA moet worden gezocht. Hier is deze annotatie uitgecommentarieerd, omdat deze informatie al expliciet in regel 50 is vermeld. Deze annotatie zou aanwezig moeten zijn als de modus [@EnableAutoConfiguration] wordt gebruikt en de entiteiten JPA zich niet in dezelfde map bevinden als de configuratieklasse;
- regel 18: met de annotatie [@ComponentScan] kunnen de mappen worden opgegeven waarin naar Spring-componenten moet worden gezocht. Spring-componenten zijn klassen die zijn gemarkeerd met Spring-annotaties zoals @Service, @Component, @Controller, ... Hier zijn er geen andere dan die welke zijn gedefinieerd binnen de klasse [Config], dus is de annotatie uitgecommentarieerd;
- regels 25-33: definiëren de gegevensbron, de database H2. Het is de annotatie @Bean op regel 25 die ervoor zorgt dat het door deze methode aangemaakte object een door Spring beheerde component wordt. De naam van de methode kan hier willekeurig zijn. Deze moet echter [dataSource] heten als EntityManagerFactory op regel 47 ontbreekt en via autoconfiguratie wordt gedefinieerd;
- regel 29: de database krijgt de naam [demo] en wordt gegenereerd in de projectmap;
- regels 36-43: definiëren de gebruikte implementatie JPA, in dit geval een Hibernate-implementatie. De naam van de methode kan hier willekeurig zijn;
- regel 39: geen logbestanden voor SQL;
- regel 30: de database wordt aangemaakt als deze nog niet bestaat;
- regels 46-54: definiëren de EntityManagerFactory die de persistentie van JPA zal beheren. De methode moet verplicht [entityManagerFactory] heten;
- regel 47: de methode ontvangt twee parameters van het type van de twee eerder gedefinieerde beans. Deze worden vervolgens geconstrueerd en door Spring als parameters van de methode geïnjecteerd;
- regel 49: stelt de gebruikte implementatie JPA vast;
- regel 50: hier worden de mappen opgegeven waarin de entiteiten JPA te vinden zijn;
- regel 51: stelt de te beheren gegevensbron vast;
- regels 57-62: de transactiebeheerder. De methode moet verplicht [transactionManager] heten. Deze ontvangt als parameter de bean uit de regels 46-54;
- regel 60: de transactiebeheerder wordt gekoppeld aan EntityManagerFactory;
De bovenstaande methoden kunnen in willekeurige volgorde worden gedefinieerd.
Het uitvoeren van het project levert dezelfde resultaten op. Er verschijnt een nieuw bestand in de projectmap, namelijk het databasebestand H2:
![]() |
Ten slotte kunnen we Spring Boot achterwege laten. We maken een tweede uitvoerbare klasse aan, [Main2]:
![]() |
De klasse [Main2] bevat de volgende code:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- regel 15: de configuratieklasse [Config] wordt nu gebruikt door de Spring-klasse [AnnotationConfigApplicationContext]. Op regel 5 is te zien dat er nu geen afhankelijkheden meer zijn ten opzichte van Spring Boot.
De uitvoering levert dezelfde resultaten op als voorheen.
8.4.1.6. Een uitvoerbaar archief maken
Om een uitvoerbaar archief van het project te maken, kun je als volgt te werk gaan:
![]() |
- in [1]: maak een uitvoerconfiguratie aan;
- in [2]: van het type [Java Application]
- in [3]: geef het uit te voeren project aan (gebruik de knop Browse);
- in [4]: geeft de uit te voeren klasse aan;
- in [5]: de naam van de uitvoeringsconfiguratie – kan willekeurig zijn;
![]() |
- in [6]: het project wordt geëxporteerd;
- in [7]: in de vorm van een uitvoerbaar JAR-archief;
- in [8]: geeft het pad en de naam aan van het uit te voeren bestand dat moet worden aangemaakt;
- in [9]: de naam van de uitvoerconfiguratie die in [5] is aangemaakt;
Zodra dit is gebeurd, openen we een console in de map die het uitvoerbare archief bevat:
Het archief wordt als volgt uitgevoerd:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
De resultaten die in de console worden weergegeven, zijn als volgt:
8.4.1.7. Een nieuw Spring Data-project aanmaken
Om een Spring Data-projectsjabloon aan te maken, kun je als volgt te werk gaan:
![]() |
- in [1] maakt u een nieuw project aan;
- in [2]: van het type [Spring Starter Project];
- het gegenereerde project wordt een Maven-project. In [3] geef je de naam van de projectgroep op;
- in [4]: geef je de naam op van het artefact (hier een jar) dat bij het bouwen van het project wordt aangemaakt;
- in [5]: hier wordt het pakket van de uitvoerbare klasse opgegeven die in het project zal worden aangemaakt;
- in [6]: de Eclipse-naam van het project – deze kan willekeurig zijn (hoeft niet identiek te zijn aan [4]);
- in [7]: hier wordt aangegeven dat er een project wordt aangemaakt met een laag [JPA]. De benodigde afhankelijkheden voor een dergelijk project worden vervolgens opgenomen in het bestand [pom.xml];
![]() |
- in [8]: het aangemaakte project;
Het bestand [pom.xml] bevat de benodigde afhankelijkheden voor een project JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- bovenliggende entiteit opzoeken in de repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- regels 9-12: de benodigde afhankelijkheden voor JPA – zullen [Spring Data] bevatten;
- regels 13-17: de benodigde afhankelijkheden voor de JUnit-tests die met Spring zijn geïntegreerd;
De uitvoerbare klasse [Application] doet niets, maar is vooraf geconfigureerd:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
De testklasse [ApplicationTests] doet niets, maar is vooraf geconfigureerd:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- regel 9: de annotatie [@SpringApplicationConfiguration] maakt het mogelijk om het configuratiebestand [Application] te gebruiken. De testklasse zal zo profiteren van alle beans die in dit bestand worden gedefinieerd;
- regel 8: de annotatie [@RunWith] maakt de integratie van Spring met JUnit mogelijk: de klasse kan worden uitgevoerd als een JUnit-test. [@RunWith] is een annotatie JUnit (regel 4), terwijl de klasse [SpringJUnit4ClassRunner] een Spring-klasse is (regel 6);
Nu we een applicatieskelet JPA hebben, kunnen we dit aanvullen om de persistentielag van de server voor onze afspraakbeheerapplicatie te implementeren.
8.4.2. Het Eclipse-project van de server
![]() |
![]() |
De belangrijkste onderdelen van het project zijn:
- [pom.xml]: het Maven-configuratiebestand van het project;
- [rdvmedecins.entities]: de entiteiten JPA;
- [rdvmedecins.repositories]: de Spring Data-interfaces voor toegang tot de entiteiten JPA;
- [rdvmedecins.metier]: de laag [métier];
- [rdvmedecins.domain]: de entiteiten die door de laag worden beheerd [métier];
- [rdvmdecins.config]: de configuratieklassen van de persistentielaag;
- [rdvmedecins.boot]: een eenvoudige console-applicatie;
8.4.3. De Maven-configuratie
![]() | ![]() | ![]() |
Het bestand [pom.xml] van het project is als volgt:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- piloot JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- mapper jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- gebruik UTF-8 voor alles -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- regels 8-12: het project is gebaseerd op het bovenliggende project [spring-boot-starter-parent]. Voor afhankelijkheden die al in het bovenliggende project aanwezig zijn, wordt geen versie opgegeven. De versie die in het bovenliggende project is gedefinieerd, wordt gebruikt. Andere afhankelijkheden worden op de gebruikelijke manier gedeclareerd;
- regels 15-18: voor Spring Data;
- regels 20-24: voor de tests JUnit;
- regels 26-29: voor de Spring Security-bibliotheek, waarvan de laag [DAO] gebruikmaakt van een van de klassen voor wachtwoordversleuteling;
- regels 31-34: stuurprogramma JDBC van SGBD MySQL5;
- regels 36-39: Tomcat-verbindingspool JDBC. Een verbindingspool bundelt openstaande verbindingen met een database. Wanneer de code een verbinding wil openen, wordt deze aangevraagd bij de pool. Wanneer de code de verbinding sluit, wordt deze niet gesloten maar teruggegeven aan de pool. Dit alles gebeurt op transparante wijze vanuit de code. Dit levert prestatiewinst op, omdat het herhaaldelijk openen en sluiten van een verbinding tijd kost. Hier brengt de verbindingspool bij het instantiëren een bepaald aantal verbindingen met de database tot stand. Vervolgens worden er geen verbindingen meer geopend of gesloten, tenzij het aantal in de pool opgeslagen verbindingen ontoereikend blijkt te zijn. In dat geval maakt de pool automatisch nieuwe verbindingen aan;
- regels 41-44: Jackson-bibliotheek voor het beheer van jSON;
- regels 46-50: Google-bibliotheek voor het beheer van collecties;
8.4.4. De entiteiten JPA
![]() |
De entiteiten JPA zijn de objecten die de rijen van de databasetabellen zullen inkapselen.
![]() |
De klasse [AbstractEntity] is de bovenliggende klasse van de entiteiten [Personne, Creneau, Rv]. De definitie ervan is als volgt:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialisatie
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// getters en setters
..
}
- regel 11: de annotatie [@MappedSuperclass] geeft aan dat de geannoteerde klasse de bovenliggende klasse is van de entiteiten JPA en [@Entity];
- regels 15-17: definiëren de primaire sleutel [id] van elke entiteit. Het is de annotatie [@Id] die het veld [id] tot primaire sleutel maakt. De annotatie [@GeneratedValue(strategy = GenerationType.IDENTITY)] geeft aan dat de waarde van deze primaire sleutel wordt gegenereerd door SGBD en dat de generatiemodus [IDENTITY] wordt opgelegd. Voor de SGBD MySQL betekent dit dat de primaire sleutels worden gegenereerd door de SGBD met het attribuut [AUTO_INCREMENT]
- regels 18-19: definiëren de versie van elke entiteit. De implementatie JPA zal dit versienummer verhogen telkens wanneer de entiteit wordt gewijzigd. Dit nummer dient om te voorkomen dat de entiteit gelijktijdig door twee verschillende gebruikers wordt bijgewerkt: twee gebruikers, U1 en U2, lezen de entiteit E met een versienummer gelijk aan V1. U1 wijzigt E en slaat deze wijziging op in de database: het versienummer verandert dan in V1+1. U2 wijzigt op zijn beurt E en slaat deze wijziging op in de database: er wordt een uitzondering gegenereerd omdat het een andere versie (V1) heeft dan die in de database (V1+1);
- regels 29-33: met de methode [build] kunnen de twee velden van [AbstractEntity] worden geïnitialiseerd. Deze methode retourneert de referentie van de aldus geïnitialiseerde instantie [AbstractEntity];
- regels 36-44: de methode [equals] van de klasse wordt opnieuw gedefinieerd: twee entiteiten worden als gelijk beschouwd als ze dezelfde klassenaam en dezelfde id-identificatie hebben;
- regels 21-26: wanneer de methode [equals] van een klasse opnieuw wordt gedefinieerd, moet ook de bijbehorende methode [hashCode] opnieuw worden gedefinieerd (regels 21-26). De regel is dat twee entiteiten die volgens de methode [equals] gelijk zijn, ook dezelfde [hashCode] moeten hebben. Hier is de [hashCode] van een entiteit gelijk aan de primaire sleutel [id]. De [hashCode] van een klasse wordt met name gebruikt bij het beheer van woordenboeken waarvan de waarden instanties van de klasse zijn;
De entiteit [Personne] is de bovenliggende klasse van de entiteiten [Medecin] en [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributen van een persoon
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// standaardconstructor
public Personne() {
}
// constructor met parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters en setters
...
}
- regel 6: de annotatie [@MappedSuperclass] geeft aan dat de geannoteerde klasse de bovenliggende klasse is van de entiteiten JPA en [@Entity];
- regels 10-15: een persoon heeft een aanspreektitel (Melle), een voornaam (Jacqueline) en een achternaam (Tatou). Er wordt geen informatie gegeven over de kolommen van de tabel. Deze zullen daarom standaard dezelfde namen dragen als de velden;
De entiteit [Medecin] is als volgt:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// standaardconstructor
public Medecin() {
}
// constructor met parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- regel 6: de klasse is een entiteit JPA;
- regel 7: gekoppeld aan de tabel [MEDECINS] in de database;
- regel 8: de entiteit [Medecin] is afgeleid van de entiteit [Personne];
Een arts kan als volgt worden aangemaakt:
Als men bovendien een identificatiecode en een versie wil toewijzen, kan men het volgende schrijven:
waarbij de methode [build] dezelfde is als die gedefinieerd in [AbstractEntity].
De entiteit [Client] is als volgt:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// standaardconstructor
public Client() {
}
// constructor met parameters
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identiteit
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- regel 6: de klasse is een entiteit JPA;
- regel 7: gekoppeld aan de tabel [CLIENTS] in de database;
- regel 8: de entiteit [Client] is afgeleid van de entiteit [Personne];
De entiteit [Creneau] is als volgt:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// kenmerken van een tijdvak van RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// een tijdvak is gekoppeld aan een arts
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// vreemde sleutel
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// standaardconstructeur
public Creneau() {
}
// fabrikant met parameters
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// vreemde sleutel
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- regel 10: de klasse is een entiteit JPA;
- regel 11: gekoppeld aan de tabel [CRENEAUX] in de database;
- regel 12: de entiteit [Creneau] is afgeleid van de entiteit [AbstractEntity] en erft dus de identificatiecode [id] en de versie [version];
- regel 16: starttijd van het tijdslot (14);
- regel 17: startminuten van het tijdvak (20);
- regel 18: eindtijd van het tijdvak (14);
- regel 19: eindminuten van het tijdvak (40);
- regels 22-24: de arts die eigenaar is van het tijdslot. De tabel [CRENEAUX] heeft een vreemde sleutel naar de tabel [MEDECINS]. Deze relatie wordt weergegeven door de regels 22-24;
- regel 22: de annotatie [@ManyToOne] geeft aan dat er een relatie is van meerdere (tijdvakken) naar één (arts). Het attribuut [fetch=FetchType.LAZY] geeft aan dat wanneer een entiteit [Creneau] wordt opgevraagd bij de persistentie-context en deze in de database moet worden opgezocht, de entiteit [Medecin] niet mee wordt teruggegeven. Het voordeel van deze modus is dat de entiteit [Medecin] alleen wordt opgezocht als de ontwikkelaar daarom vraagt. Zo wordt geheugen bespaard en worden de prestaties verbeterd;
- regel 23: geeft de naam aan van de kolom met de externe sleutel in de tabel [CRENEAUX];
- regels 27-28: de vreemde sleutel in de tabel [MEDECINS];
- regel 27: de kolom [ID_MEDECIN] is al gebruikt in regel 23. Dit betekent dat deze op twee verschillende manieren kan worden gewijzigd, wat niet is toegestaan volgens de norm JPA. We voegen daarom de attributen [insertable = false, updatable = false] toe, waardoor de kolom alleen nog maar kan worden gelezen;
De entiteit [Rv] is als volgt:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// kenmerken van een Rv
@Temporal(TemporalType.DATE)
private Date jour;
// een rv is gekoppeld aan een klant
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// een RV is gekoppeld aan een tijdslot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// vreemde sleutels
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// standaardfabrikant
public Rv() {
}
// met parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// vreemde sleutels
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters en setters
...
}
- regel 14: de klasse is een entiteit JPA;
- regel 15: gekoppeld aan de tabel [RV] in de database;
- regel 16: de entiteit [Rv] is afgeleid van de entiteit [AbstractEntity] en erft dus de identificatiecode [id] en de versie [version];
- regel 21: de datum van de afspraak;
- regel 20: het Java-type [Date] bevat zowel een datum als een tijdstip. Hier wordt aangegeven dat alleen de datum wordt gebruikt;
- regels 24-26: de klant voor wie deze afspraak is gemaakt. De tabel [RV] heeft een vreemde sleutel naar de tabel [CLIENTS]. Deze relatie wordt weergegeven door de regels 24-26;
- regels 29-31: het tijdvak van de afspraak. De tabel [RV] heeft een vreemde sleutel naar de tabel [CRENEAUX]. Deze relatie wordt weergegeven door de regels 29-31;
- regels 34-35: de vreemde sleutel [idClient];
- regels 36-37: de vreemde sleutel [idCreneau];
8.4.5. De laag [DAO]
![]() |
We gaan de laag [DAO] implementeren met Spring Data:
![]() |
De laag [DAO] wordt geïmplementeerd met vier Spring Data-interfaces:
- [ClientRepository]: biedt toegang tot de entiteiten JPA en [Client];
- [CreneauRepository]: biedt toegang tot de entiteiten JPA en [Creneau];
- [MedecinRepository]: geeft toegang tot de entiteiten JPA en [Medecin];
- [RvRepository]: geeft toegang tot de entiteiten JPA en [Rv];
De interface [MedecinRepository] is als volgt:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- regel 7: de interface [MedecinRepository] neemt alleen de methoden van de interface [CrudRepository] over, zonder er andere aan toe te voegen;
De interface [ClientRepository] is als volgt:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- regel 7: de interface [ClientRepository] neemt alleen de methoden van de interface [CrudRepository] over, zonder er andere aan toe te voegen;
De interface [CreneauRepository] is als volgt:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// lijst met spreekuren van een arts
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- regel 8: de interface [CreneauRepository] erft de methoden van de interface [CrudRepository];
- regels 10-11: met de methode [getAllCreneaux] kunnen de beschikbare tijdvakken van een arts worden opgevraagd;
- regel 11: de parameter is de identificatiecode van de arts. Het resultaat is een lijst met beschikbare tijdvakken in de vorm van een object [Iterable<Creneau>];
- regel 10: met de annotatie [@Query] kan de query JPQL (Java Persistence Query Language) worden gespecificeerd die de methode implementeert. De parameter [?1] wordt vervangen door de parameter [idMedecin] van de methode;
De interface [RvRepository] is als volgt:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- regel 10: de interface [RvRepository] erft de methoden van de interface [CrudRepository];
- regels 12-13: met de methode [getRvMedecinJour] kunnen de afspraken van een arts voor een bepaalde dag worden opgehaald;
- regel 13: de parameters zijn de ID van de arts en de dag. Het resultaat is een lijst met afspraken in de vorm van een [Iterable<Rv>]-object;
- regel 12: met de annotatie [@Query] kan de query JPQL worden gespecificeerd die de methode implementeert. De parameter [?1] wordt vervangen door de parameter [idMedecin] van de methode en de parameter [?2] wordt vervangen door de parameter [jour] van de methode. De volgende query JPQL volstaat niet:
omdat de velden van de klasse Rv, van het type [Client] en [Creneau], worden verkregen in de modus [FetchType.LAZY], wat betekent dat ze expliciet moeten worden opgevraagd om te worden verkregen. Dit gebeurt in de query JPQL met de syntaxis [left join fetch entité], die vraagt om een join uit te voeren met de tabel waarnaar de vreemde sleutel verwijst, om de entiteit waarnaar wordt verwezen op te halen;
8.4.6. De laag [métier]
![]() |
![]() |
- [IMetier] is de interface van de laag [métier] en [Metier] is de implementatie ervan;
- [AgendaMedecinJour] en [CreneauMedecinJour] zijn twee bedrijfsentiteiten;
8.4.6.1. De entiteiten
De entiteit [CreneauMedecinJour] koppelt een tijdslot aan de eventuele afspraak die binnen dat tijdslot is gemaakt:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// velden
private Creneau creneau;
private Rv rv;
// constructors
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters en setters
...
}
- regel 12: het tijdvak;
- regel 13: de eventuele afspraak – anders null;
De entiteit [AgendaMedecinJour] is de agenda van een arts voor een bepaalde dag, d.w.z. de lijst met zijn afspraken:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// velden
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// constructors
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters en setters
...
}
- regel 13: de arts;
- regel 14: de dag in de agenda;
- lijn 15: de openingstijden, met of zonder afspraak;
8.4.6.2. De dienst
De interface van de laag [métier] is als volgt:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// klantenlijst
public List<Client> getAllClients();
// lijst met artsen
public List<Medecin> getAllMedecins();
// lijst met tijdvakken van een arts
public List<Creneau> getAllCreneaux(long idMedecin);
// lijst met afspraken van een arts op een bepaalde dag
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// een klant zoeken op basis van zijn ID
public Client getClientById(long id);
// een klant zoeken op basis van zijn ID
public Medecin getMedecinById(long id);
// een afspraak zoeken op basis van de id
public Rv getRvById(long id);
// een tijdslot zoeken op basis van de id
public Creneau getCreneauById(long id);
// een RV toevoegen
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// een RV verwijderen
public void supprimerRv(Rv rv);
// beroep
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
De opmerkingen leggen de functie van elke methode uit.
De implementatie van de interface [IMetier] is de volgende klasse [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// implementatie interface
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- regel 24: de annotatie [@Service] is een Spring-annotatie die de geannoteerde klasse tot een door Spring beheerde component maakt. Een component kan al dan niet een naam krijgen. Deze heeft de naam [métier];
- regel 25: de klasse [Metier] implementeert de interface [IMetier];
- regel 28: de annotatie [@Autowired] is een Spring-annotatie. De waarde van het veld waarop deze annotatie is aangebracht, wordt door Spring geïnitialiseerd (geïnjecteerd) met de referentie van een Spring-component van het opgegeven type of met de opgegeven naam. Hier specificeert de annotatie [@Autowired] geen naam. Er zal dus een injectie op basis van het type plaatsvinden;
- regel 29: het veld [medecinRepository] wordt geïnitialiseerd met de referentie van een Spring-component van het type [MedecinRepository]. Dit is de referentie van de klasse die door Spring Data wordt gegenereerd om de interface [MedecinRepository] te implementeren, die we al eerder hebben besproken;
- regels 30-35: dit proces wordt herhaald voor de drie andere besproken interfaces;
- regels 39-41: implementatie van de methode [getAllClients];
- regel 40: we gebruiken de methode [findAll] van de interface [ClientRepository]. Deze methode retourneert een type [Iterable<Client>] dat we met de statische methode [Lists.newArrayList] omzetten naar [List<Client>]. De klasse [Lists] is gedefinieerd in de Google Guava-bibliotheek. In [pom.xml] is deze afhankelijkheid geïmporteerd:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- regels 38-86: de methoden van de interface [IMetier] worden geïmplementeerd met behulp van de klassen van de laag [DAO];
Alleen de methode op regel 88 is specifiek voor de laag [métier]. Deze is hier geplaatst omdat deze een bedrijfsspecifieke verwerking uitvoert die meer is dan alleen toegang tot gegevens. Zonder deze methode was er geen reden om een laag [métier] aan te maken. De methode [getAgendaMedecinJour] is als volgt:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// lijst met beschikbare tijdvakken van de arts
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// lijst met afspraken van dezelfde arts voor dezelfde dag
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// er wordt een woordenboek aangemaakt op basis van de gemaakte afspraken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// de agenda voor de gevraagde dag wordt aangemaakt
AgendaMedecinJour agenda = new AgendaMedecinJour();
// de arts
agenda.setMedecin(getMedecinById(idMedecin));
// de dag
agenda.setJour(jour);
// de reserveringsslots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// invullen van de reserveringsslots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// regel i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// tijdvak
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// is het tijdvak vrij of gereserveerd?
if (hReservations.containsKey(idCreneau)) {
// het tijdvak is bezet – de reservering wordt genoteerd
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// het resultaat wordt weergegeven
return agenda;
}
De lezer wordt verzocht de commentaren te lezen. Het algoritme is als volgt:
- alle tijdvakken van de aangegeven arts worden opgehaald;
- we halen al zijn afspraken voor de aangegeven dag op;
- met deze twee gegevens kan worden vastgesteld of een tijdslot vrij of bezet is;
8.4.7. De configuratie van het Spring-project
![]() |
De klasse [DomainAndPersistenceConfig] configureert het gehele project:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
// entiteitspakketten JPA
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// de gegevensbron MySQL
@Bean
public DataSource dataSource() {
// gegevensbron TomcatJdbc
DataSource dataSource = new DataSource();
// configuratie JDBC
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// aanvankelijk geopende verbindingen
dataSource.setInitialSize(5);
// resultaat
return dataSource;
}
// de provider JPA is Hibernate
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transactiemanager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- regel 17: de klasse is een Spring-configuratieklasse;
- regel 18: de pakketten waarin de [CrudRepository]-interfaces van Spring Data zich bevinden. Deze worden toegevoegd aan de Spring-context;
- regel 19: voegt alle klassen uit het pakket [rdvmedecins] en de daarvan afgeleide klassen met een Spring-annotatie toe aan de Spring-context. In het pakket [rdvmdecins.metier] wordt de klasse [Metier] met de bijbehorende annotatie [@Service] gevonden en toegevoegd aan de Spring-context;
- regels 26-39: configureren de Tomcat-verbindingspool JDBC (regel 5);
- regel 36: de verbindingspool heeft standaard 5 open verbindingen. Deze regel wordt ter illustratie weergegeven. In ons geval zou 1 verbinding voldoende zijn. Indien de laag [DAO] door meerdere threads zou worden gebruikt, zou deze regel nodig zijn. Dit zal later het geval zijn, wanneer de laag [DAO] als ondersteuning dient voor een webapplicatie die van nature meerdere gebruikers ondersteunt die tegelijkertijd worden bediend;
- regels 42-49: de gebruikte implementatie JPA is een Hibernate-implementatie;
- regel 45: geen SQL-logs;
- regel 46: geen regeneratie van tabellen;
- regel 47: de gebruikte SGBD is MySQL;
- regels 53-61: definiëren de EntityManagerFactory van de laag JPA. Op basis van dit object verkrijgt men het object [EntityManager], waarmee de bewerkingen JPA kunnen worden uitgevoerd;
- regel 57: hier worden het pakket of de pakketten aangegeven waarin de entiteiten JPA zich bevinden;
- regel 58: hier wordt de gegevensbron aangegeven die moet worden gekoppeld aan de laag JPA;
- regels 64-69: de transactiebeheerder die gekoppeld is aan de voorgaande EntityManagerFactory. Standaard worden de methoden van de [CrudRepository]-interfaces van Spring Data binnen een transactie uitgevoerd. De transactie wordt gestart voordat de methode wordt aangeroepen en wordt beëindigd (door een commit of rollback) nadat deze is verlaten;
8.4.8. De tests van de [métier]-laag
De klasse [rdvmedecins.tests.Metier] is een Spring-testklasse / JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// klantenweergave
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// Weergave van artsen
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// Weergave van de tijdvakken van een arts
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// Lijst met afspraken van een arts op een bepaalde dag
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// een RV toevoegen
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// controle
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// een RV toevoegen in hetzelfde tijdvak op dezelfde dag
// moet een uitzondering veroorzaken
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// de fout wordt genoteerd
erreur = true;
}
// er wordt gecontroleerd of er een fout is opgetreden
Assert.assertTrue(erreur);
// lijst met RV
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// agenda weergeven
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// een RV verwijderen
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// controle
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// hulpprogramma - toont de items in een verzameling
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- regel 22: met de annotatie [@SpringApplicationConfiguration] kan het eerder besproken configuratiebestand [DomainAndPersistenceConfig] worden gebruikt. De testklasse profiteert zo van alle beans die in dit bestand zijn gedefinieerd;
- regel 23: de annotatie [@RunWith] maakt de integratie van Spring met JUnit mogelijk: de klasse kan nu worden uitgevoerd als een JUnit-test. [@RunWith] is een annotatie JUnit (regel 9), terwijl de klasse [SpringJUnit4ClassRunner] een Spring-klasse is (regel 12);
- regels 26-27: injectie in de testklasse van een verwijzing naar de laag [métier];
- veel tests zijn slechts eenvoudige visuele tests:
- regels 32-33: lijst met klanten;
- regels 35-36: lijst met artsen;
- regels 39-40: lijst met afspraken van een arts;
- regel 43: lijst met afspraken van een arts;
- regel 50: toevoegen van een nieuwe afspraak. De methode [ajouterRv] geeft de afspraak weer met aanvullende informatie, namelijk de primaire sleutel id;
- regel 53: deze primaire sleutel wordt gebruikt om de afspraak in de database op te zoeken;
- regel 54: er wordt gecontroleerd of de gezochte afspraak en de gevonden afspraak dezelfde zijn. Ter herinnering: de methode [equals] van de entiteit [Rv] is opnieuw gedefinieerd: twee afspraken zijn gelijk als ze dezelfde id hebben. Dit toont aan dat de toegevoegde afspraak inderdaad in de database is opgeslagen;
- regels 61-73: er wordt geprobeerd dezelfde afspraak een tweede keer toe te voegen. Dit moet door de SGBD worden afgewezen, omdat er een uniekheidsvoorwaarde geldt:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
Regel 8 hierboven geeft aan dat de combinatie [JOUR, ID_CRENEAU] uniek moet zijn, waardoor het niet mogelijk is om twee afspraken op dezelfde dag in hetzelfde tijdslot te plaatsen.
- regel 73: er wordt gecontroleerd of er daadwerkelijk een uitzondering is opgetreden;
- regel 77: de agenda wordt opgevraagd van de arts voor wie zojuist een afspraak is toegevoegd;
- regel 79: er wordt gecontroleerd of de toegevoegde afspraak daadwerkelijk in zijn agenda staat;
- regel 82: de toegevoegde afspraak wordt verwijderd;
- regel 84: we zoeken de verwijderde afspraak op in de database;
- regel 85: we controleren of we een pointer null hebben opgehaald, wat aangeeft dat de gezochte afspraak niet bestaat;
De test is geslaagd:
![]() |
8.4.9. Het consoleprogramma
![]() |
Het consoleprogramma is eenvoudig. Het laat zien hoe je een externe sleutel kunt ophalen:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// opstarten
public static void main(String[] args) {
// de configuratie wordt voorbereid
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// de configuratie wordt gestart
ConfigurableApplicationContext context = app.run(args);
// kerntaak
IMetier métier = context.getBean(IMetier.class);
try {
// een RV toevoegen
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// controle
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// de Spring-context sluiten
context.close();
}
// hulpprogramma - toont de elementen van een verzameling
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
Het programma voegt een afspraak toe en controleert vervolgens of deze is toegevoegd.
- regel 19: de klasse [SpringApplication] maakt gebruik van de configuratieklasse [DomainAndPersistenceConfig];
- regel 20: verwijdering van de opstartlogs van de applicatie;
- regel 22: de klasse [SpringApplication] wordt uitgevoerd. Deze retourneert een Spring-context, d.w.z. de lijst met geregistreerde beans;
- regel 24: er wordt een verwijzing opgehaald naar de bean die de interface [IMetier] implementeert. Het gaat dus om een verwijzing naar de laag [métier];
- regels 27-31: toevoeging van een nieuwe afspraak voor vandaag, voor klant nr. 1 in tijdvak nr. 1. De klant en het tijdvak zijn volledig verzonnen om aan te tonen dat alleen de identificatiecodes worden gebruikt. We hebben hier de versie geïnitialiseerd, maar we hadden er ook zomaar iets kunnen invullen. Deze wordt hier niet gebruikt;
- regel 34: we willen weten welke arts tijdvak nr. 1 heeft. Daarvoor moeten we in de database tijdvak nr. 1 opzoeken. Omdat we in de modus [FetchType.LAZY] zitten, wordt de arts niet samen met het tijdvak opgehaald. We hebben er echter voor gezorgd dat er een veld [idMedecin] in de entiteit [Creneau] is opgenomen om de primaire sleutel van de arts op te halen;
- regel 35: we halen de primaire sleutel van de arts op;
- regel 36: de lijst met afspraken van de arts wordt weergegeven;
De console-uitvoer is als volgt:
8.4.10. Logboekbeheer
De logbestanden van de console worden geconfigureerd via twee bestanden: [application.properties] en [logback.xml] [1]:
![]() |
Het bestand [application.properties] wordt gebruikt door het Spring Boot-framework. Hierin kunnen talrijke parameters worden gedefinieerd om de standaardwaarden van Spring Boot te wijzigen (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Hieronder volgt de inhoud ervan:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- regel 1: regelt het logniveau van Hibernate – hier geen logs
- regel 2: regelt de weergave van de Spring Boot-banner – hier geen banner
Het bestand [logback.xml] is het configuratiebestand van het logboekframework [logback] [2]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders krijgen standaard het type ch.qos.logback.classic.encoder.PatternLayoutEncoder toegewezen -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- logniveau instellen -->
<root level="info"> <!-- uit, info, debug, waarschuwing -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- het algemene logniveau wordt bepaald door regel 9 – hier logs van niveau [info];
Dit levert het volgende resultaat op:
Als we het logniveau van Hibernate wijzigen naar [info] (zonder verder iets te veranderen):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
dan levert dat het volgende resultaat op:
Als we het logniveau wijzigen naar [debug] (zonder verder iets te wijzigen):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
dit levert het volgende resultaat op:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. De laag [web / jSON]
![]() |
![]() |
We gaan de laag [web / jSON] in verschillende stappen opbouwen:
- stap 1: een operationele weblaag zonder authenticatie;
- stap 2: authenticatie instellen met Spring Security;
- stap 3: implementatie van CORS en [Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain the resource originated from. (Wikipedia)]. De client van onze webservice zal een Angular-webclient zijn die niet noodzakelijkerwijs tot hetzelfde domein behoort als onze webservice. Standaard heeft deze dan geen toegang, tenzij de webservice dit toestaat. We zullen zien hoe;
8.4.11.1. Maven-configuratie
Het bestand [pom.xml] van het project ziet er als volgt uit:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring MVC-weblaag -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- testlaag -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- laag DAO -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- regels 12-15: het bovenliggende Maven-project;
- regels 19-22: de afhankelijkheden voor een Spring-project MVC;
- regels 24-28: de afhankelijkheden voor de JUnit / Spring-tests;
- regels 30-34: de afhankelijkheden van het project van de lagen [métier, DAO, JPA];
8.4.11.2. De interface van de webservice
![]() |
- in [1], hierboven, kan de browser slechts een beperkt aantal URL-verzoeken met een specifieke syntaxis indienen;
- in [4] ontvangt hij een antwoord jSON;
De antwoorden van onze webservice zullen allemaal dezelfde vorm hebben, die overeenkomt met de transformatie jSON van een object van het type [Response], namelijk:
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// eventuele foutmeldingen
private List<String> messages;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters en setters
...
}
- regel 7: foutcode van het antwoord 0: OK, anders: KO;
- regel 11: een lijst met foutmeldingen, indien er een fout is;
- regel 13: de inhoud van het antwoord;
Hieronder vindt u de schermafbeeldingen die de interface van de webservice / jSON illustreren:
Lijst van alle patiënten van de artsenpraktijk [/getAllClients]
![]() |
Lijst van alle artsen van de medische praktijk [/getAllMedecins]
![]() |
Lijst met de spreekuurmomenten van een arts [/getAllCreneaux/{idMedecin}]
![]() |
Lijst met afspraken van een arts [/getRvMedecinJour/{idMedecin}/{jjjj-mm-dd}
![]() |
Agenda van een arts [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-jj}]
![]() |
Om een afspraak toe te voegen of te verwijderen, gebruiken we de Chrome-extensie [Advanced Rest Client], omdat deze handelingen met een POST worden uitgevoerd.
Een afspraak toevoegen [/ajouterRv]
![]() |
- in [0], de URL van de webservice;
- in [1] wordt de methode POST gebruikt;
- in [2], de tekst jSON van de informatie die in de vorm {dag, idClient, idCreneau} aan de webservice wordt verzonden;
- in [3] geeft de klant aan de webservice aan dat hij informatie in het formaat jSON verstuurt;
Het antwoord is dan als volgt:
![]() |
- in [4]: de client stuurt de header waarmee hij aangeeft dat de gegevens die hij verstuurt in het formaat jSON zijn;
- in [5]: de webservice antwoordt dat hij ook jSON verstuurt;
- in [6]: het antwoord jSON van de webservice. Het veld [body] bevat de vorm jSON van de toegevoegde afspraak;
De aanwezigheid van de nieuwe afspraak kan worden gecontroleerd:
![]() |
Let op de id [50] van de afspraak. We gaan deze verwijderen.
Een afspraak verwijderen [/supprimerRv]
![]() |
- in [1], de URL van de webservice;
- in [2] wordt de methode POST gebruikt;
- in [3], de tekst jSON van de informatie die in de vorm {idRv} naar de webservice wordt verzonden;
- in [4] geeft de client aan de webservice aan dat hij informatie jSON verstuurt;
Het antwoord luidt dan als volgt:
![]() |
- in [5]: het veld [status] is 0, wat aangeeft dat de bewerking is geslaagd;
Het verwijderen van de afspraak kan worden gecontroleerd:
![]() |
Hierboven is de afspraak van patiënt [Mme GERMAIN] niet meer aanwezig.
Via de webservice kunnen entiteiten ook op basis van hun ID worden opgehaald:
![]() |
![]() |
![]() |
![]() |
Al deze URL-entiteiten worden verwerkt door de controller [RdvMedecinsController], die we binnenkort zullen presenteren.
8.4.11.3. Configuratie van de webservice
![]() |
De configuratieklasse [AppConfig] is als volgt:
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- regel 12: de klasse [AppConfig] configureert de gehele applicatie;
- regel 9: de klasse [AppConfig] is een Spring-configuratieklasse;
- regel 10: er wordt aangegeven dat de Spring-componenten moeten worden gezocht in het pakket [rdvmedecins.web] en de onderliggende pakketten. Op deze manier worden de volgende componenten gevonden:
- [@RestController RdvMedecinsController] in het pakket [rdvmedecins.web.controllers];
- [@Component ApplicationModel] in het pakket [rdvmedecins.web.models];
- regel 11: we importeren de klasse [DomainAndPersistenceConfig] die het project [rdvmedecins-metier-dao] configureert om toegang te krijgen tot de beans van dit project;
- regel 11: de klasse [SecurityConfig] configureert de beveiliging van de webapplicatie. We laten deze voorlopig buiten beschouwing;
- regel 11: de klasse [WebConfig] configureert de laag [web / jSON];
De klasse [WebConfig] ziet er als volgt uit:
package rdvmedecins.web.config;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
// configuratie van de DispatcherServlet voor de headers CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- regels 20-25: definiëren de bean [dispatcherServlet]. De klasse [DispatcherServlet] is de servlet van het Spring-framework MVC. Deze vervult de rol van [FrontController]: hij onderschept de verzoeken die naar de Spring-site MVC worden gestuurd en laat deze verwerken door een van de controllers van de site;
- regel 22: instantiëren van de klasse;
- regel 23: deze regel kan voorlopig worden genegeerd;
- regels 27-30: de servlet [dispatcherServlet] verwerkt alle URL;
- regels 27-30: activeren de ingebouwde Tomcat-server in de afhankelijkheden van het project. Deze draait op poort 8080;
- regels 38-67: vier jSON-mappers geconfigureerd met verschillende jSON-filters;
- regels 38-41: een mapper jSON zonder filters;
- regels 43-49: de mapper jSON [jsonMapperShortCreneau] serialiseert/deserialiseert een object [Creneau] waarbij het veld [Creneau.medecin] wordt genegeerd;
- regels 51-59: de mapper jSON [jsonMapperLongRv] serialiseert/deserialiseert een object [Rv] waarbij het veld [Rv.creneau.medecin] wordt genegeerd;
- regels 61-67: de mapper jSON [jsonMapperShortRv] serialiseert / deserialiseert een object [Rv] waarbij de velden [Rv.creneau] en [Rv.client] worden genegeerd;
8.4.11.4. De klasse [ApplicationModel]
![]() |
De klasse [ApplicationModel] zal voor twee doeleinden worden gebruikt:
- als cache om de lijsten met artsen en patiënten (klanten) op te slaan;
- als enige interface voor de controllers;
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@Component
public class ApplicationModel implements IMetier {
// de laag [métier]
@Autowired
private IMetier métier;
// gegevens afkomstig van de laag [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuratiegegevens
private boolean CORSneeded = false;
private boolean secured = false;
@PostConstruct
public void init() {
// artsen en klanten worden opgehaald
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- interface van de laag [métier]
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// getters en setters
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- regel 19: de annotatie [@Component] maakt van de klasse [ApplicationModel] een Spring-component. Net als alle Spring-componenten die we tot nu toe hebben gezien (met uitzondering van @Controller), wordt er slechts één object van dit type geïnstantieerd (singleton);
- regel 20: de klasse [ApplicationModel] implementeert de interface [IMetier];
- regels 23-24: er wordt door Spring een verwijzing naar de laag [métier] geïnjecteerd;
- regel 34: de annotatie [@PostConstruct] zorgt ervoor dat de methode [init] direct na het instantiëren van de klasse [ApplicationModel] wordt uitgevoerd;
- regels 38-39: de lijsten met artsen en klanten worden opgehaald uit de laag [métier];
- regel 41: als er een uitzondering optreedt, worden de berichten uit de uitzonderingsstack opgeslagen in het veld op regel 17;
De architectuur van de weblaag ontwikkelt zich als volgt:
![]() |
- in [2b] communiceren de methoden van de controller(s) met het singleton [ApplicationModel];
Deze strategie biedt flexibiliteit bij het beheer van de cache. Momenteel worden de spreekuren van de artsen niet in de cache opgeslagen. Om dit wel te doen, volstaat het om de klasse [ApplicationModel] aan te passen. Dit heeft geen invloed op de controller, die de methode [List<Creneau> getAllCreneaux(long idMedecin)] zal blijven gebruiken zoals voorheen. Het is de implementatie van deze methode in [ApplicationModel] die zal worden gewijzigd.
8.4.11.5. De klasse Static
De klasse [Static] bevat een reeks statische hulpprogramma's die geen „bedrijfsspecifieke” of „webgerichte” aspecten hebben:
![]() |
De code ervan is als volgt:
package rdvmedecins.web.helpers;
import java.util.ArrayList;
import java.util.List;
public class Static {
public Static() {
}
// lijst met foutmeldingen van een uitzondering
public static List<String> getErreursForException(Exception exception) {
// de lijst met foutmeldingen van de uitzondering wordt opgehaald
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
}
- regel 12: de methode [Static.getErreursForException] die (op regel 8 hieronder) is gebruikt in de methode [init] van de klasse [ApplicationModel]:
@PostConstruct
public void init() {
// de artsen en klanten ophalen
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
De methode construeert een object [List<String>] met de foutmeldingen [exception.getMessage()] van een uitzondering [exception] en van de uitzonderingen die deze bevat, [exception.getCause()].
8.4.11.6. Het skelet van de controller [RdvMedecinsController]
![]() |
We gaan nu de verwerking van de URL van de webservice in detail bespreken. Bij deze verwerking zijn drie hoofdklassen betrokken:
- de controller [RdvMedecinsController];
- de klasse met hulpprogramma's [Static];
- de cacheklasse [ApplicationModel];
![]() |
De controller [RdvMedecinsController] ziet er als volgt uit:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// lijst met berichten
private List<String> messages;
// mappers jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// foutmeldingen van de applicatie
messages = application.getMessages();
}
// lijst met artsen
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// lijst met klanten
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// overzicht van de spreekuren van een arts
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// overzicht van de afspraken van een arts
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- regel 35: de annotatie [@Controller] maakt van de klasse [RdvMedecinsController] een Spring-controller, de C van MVC;
- regels 38-39: hier wordt door Spring een object van het type [ApplicationModel] geïnjecteerd. We hebben dit al besproken;
- regels 41-42: hier wordt door Spring een object van het type [RdvMedecinsCorsController] geïnjecteerd. Dit object zullen we pas later introduceren;
- regels 48-58: de mappers jSON die zijn gedefinieerd in de configuratieklasse [WebConfig];
- regel 60: de annotatie [@PostConstruct] markeert een methode die direct na het instantiëren van de klasse moet worden uitgevoerd. Wanneer deze wordt uitgevoerd, zijn de door Spring geïnjecteerde objecten beschikbaar;
- regel 63: eventuele foutmeldingen worden opgehaald uit het object [ApplicationModel]. Dit object is bij het opstarten van de applicatie geïnstantieerd en heeft geprobeerd de artsen en klanten in de cache op te slaan. Als dit is mislukt, dan hebben we [messages!=null]. Hierdoor kunnen de methoden van de controller vaststellen of de applicatie correct is geïnitialiseerd;
- regels 67-118: de URL-methoden die door de service [web / jSON] worden blootgesteld. Alle methoden retourneren de string jSON van een object van het type [Response<T>], namelijk:
![]() |
package rdvmedecins.web.models;
import java.util.List;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// eventuele foutmeldingen
private List<String> messages;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters en setters
...
}
- regel 9: een foutcode: 0 betekent geen fout;
- regel 11: als [status!=0], dan is [messages] een lijst met foutmeldingen;
- regel 13: een T-object dat in het antwoord is ingekapseld. T is gelijk aan null in geval van een fout;
Dit object wordt geserialiseerd tot jSON voordat het naar de clientbrowser wordt verzonden;
- regel 67: de weergegeven URL is [/getAllMedecins]. De client moet een methode [GET] gebruiken om zijn verzoek in te dienen (method = RequestMethod.GET). Als deze URL zou worden aangevraagd door een POST, zou deze worden geweigerd en zou Spring MVC een foutcode HTTP naar de webclient sturen. De methode stuurt zelf het antwoord terug naar de client (regel 68). Dit is een tekenreeks (regel 67). De header HTTP [Content-type : application/json; charset=UTF-8] wordt naar de client verzonden om aan te geven dat deze een tekenreeks jSON zal ontvangen (regel 67);
- regel 77: de parameter URL wordt ingesteld door {idMedecin}. Deze parameter wordt opgehaald met de annotatie [@PathVariable] op regel 79;
- regel 79: de parameter [long idMedecin] krijgt zijn waarde van de parameter {idMedecin} van de URL [@PathVariable("idMedecin")]. De parameter in de URL en die van de methode kunnen verschillende namen hebben. Hierbij moet worden opgemerkt dat [@PathVariable("idMedecin")] van het type String is (de gehele URL is een String), terwijl de parameter [long idMedecin] van het type [long] is. De typewijziging gebeurt automatisch. Er wordt een foutcode HTTP geretourneerd als deze typewijziging mislukt;
- regel 105: de annotatie [@RequestBody] verwijst naar de body van de aanvraag. In een verzoek van het type GET is er vrijwel nooit een hoofdtekst (maar het is mogelijk er een op te nemen). In een verzoek van het type POST is er meestal wel een (maar het is mogelijk er geen op te nemen). Voor de URL [ajouterRv] verstuurt de webclient in zijn POST de volgende tekenreeks jSON:
De syntaxis [@RequestBody PostAjouterRv post] (regel 105) in combinatie met het feit dat de methode de jSON [consumes = "application/json; charset=UTF-8"] op regel 103 verwacht, zorgt ervoor dat de door de webclient verzonden tekenreeks jSON wordt gedeserialiseerd tot een object van het type [PostAjouterRv]. Dit is als volgt:
package rdvmedecins.web.models;
public class PostAjouterRv {
// postgegevens
private String jour;
private long idClient;
private long idCreneau;
// getters en setters
...
}
Ook hier vinden de benodigde typewijzigingen automatisch plaats;
- in de regels 107-109 vinden we een soortgelijk mechanisme voor de URL en [/supprimerRv]. De verzonden tekenreeks jSON is als volgt:
en het type [PostSupprimerRv] is als volgt:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// postgegevens
private long idRv;
// getters en setters
...
}
8.4.11.7. De URL [/getAllMedecins]
De URL en [/getAllMedecins] worden verwerkt via de volgende methode van de controller [RdvMedecinsController]:
// lijst met artsen
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// het antwoord
Response<List<Medecin>> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// lijst met artsen
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// antwoord
return jsonMapper.writeValueAsString(response);
}
- regels 9-10: er wordt gecontroleerd of de applicatie correct is geïnitialiseerd (messages==null). Als dat niet het geval is, wordt een antwoord teruggestuurd met status=-1 en body=messages;
- regel 13: anders wordt de lijst met artsen opgevraagd bij de klasse [ApplicationModel];
- regel 19: de tekenreeks jSON uit het antwoord wordt verzonden met de mapper jSON [jsonMapper], omdat de klasse [Medecin]geen filter jSON heeft. Het antwoord kan foutloos zijn (regel 14) of een fout bevatten (regel 16). De methode [application.getAllMedecins()] genereert geen uitzondering, omdat deze zich beperkt tot het retourneren van een lijst die in de cache staat. Toch behouden we deze uitzonderingsafhandeling voor het geval dat de artsen niet langer in de cache worden opgeslagen;
We hebben het geval waarin de applicatie niet correct is geïnitialiseerd nog niet geïllustreerd. Laten we SGBD en MySQL5 stoppen, de webservice starten en vervolgens URL en [/getAllMedecins] opvragen:

We krijgen inderdaad een foutmelding. In een normale situatie krijgen we het volgende scherm te zien:
![]() |
8.4.11.8. De URL [/getAllClients]
De URL [/getAllClients] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:
// lijst met klanten
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// het antwoord
Response<List<Client>> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// lijst met klanten
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// antwoord
return jsonMapper.writeValueAsString(response);
}
Deze methode is vergelijkbaar met de eerder besproken methode [getAllMedecins]. De verkregen resultaten zijn als volgt:
![]() |
8.4.11.9. De URL [/getAllCreneaux/{idMedecin}]
De URL [/getAllCreneaux/{idMedecin}] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:
// lijst met spreekuren van een arts
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// het antwoord
Response<List<Creneau>> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// de arts wordt opgehaald
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// afspraken van de arts
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// antwoord
return jsonMapperShortCreneau.writeValueAsString(response);
}
- regel 12: de arts die wordt geïdentificeerd door de parameter [id] wordt opgevraagd via een lokale methode:
private Response<Medecin> getMedecin(long id) {
// de arts ophalen
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// Bestaande arts?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
We keren terug van deze methode met een status in [0,1,2]. Laten we teruggaan naar de code van de methode [getAllCreneaux]:
- regels 13-14: als status!=0, wordt een antwoord met foutmelding opgebouwd;
- regel 16: de arts wordt opgehaald;
- regel 19: de beschikbare tijdvakken van deze arts worden opgehaald;
- regel 25: er wordt een object [List<Creneau>] als antwoord verzonden. Laten we de definitie van de klasse [Creneau] nog eens bekijken:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// kenmerken van een tijdvak van RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// een tijdvak is gekoppeld aan een arts
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// vreemde sleutel
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- regel 13: de arts wordt opgezocht in de modus [FetchType.LAZY];
Laten we nog eens kijken naar de aanvraag JPQL die de methode [getAllCreneaux] implementeert in de laag [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
De notatie [c.medecin.id] dwingt de koppeling tussen de tabellen [CRENEAUX] en [MEDECINS] af. Daardoor levert de query alle afspraken van de arts op, waarbij in elk van deze afspraken de arts wordt vermeld. Wanneer we deze afspraken serialiseren naar jSON, verschijnt in elk daarvan de tekenreeks jSON van de arts. Dit is overbodig. Om de serialisatie te controleren, hebben we twee dingen nodig:
- toegang hebben tot het object dat wordt geserialiseerd;
- het te serialiseren object configureren;
Punt 1 wordt gecontroleerd door de juiste converter jSON voor het object in de controller te injecteren:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
Punt 2 wordt gerealiseerd door een annotatie toe te voegen aan de klasse [Creneau] die is gedefinieerd in het project [rdvmedecins-metier-dao]:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- regel 3: een annotatie uit de bibliotheek jSON Jackson. Deze creëert een filter met de naam [creneauFilter]. Met behulp van dit filter kunnen we programmatisch bepalen welke velden al dan niet moeten worden geserialiseerd;
De serialisatie van het object [Creneau] vindt plaats in de volgende regel van de methode [getAllCreneaux]:
// antwoord
return jsonMapperShortCreneau.writeValueAsString(response);
De mapper jSON [jsonMapperShortCreneau] is in de klasse [WebConfig] als volgt gedefinieerd:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- regel 5: het filter met de naam [creneauFilter] is gekoppeld aan het filter [creneauFilter] uit regel 4. Dit filter serialiseert het object [Creneau] zonder het veld [medecin];
Het resultaat van de methode [getAllCreneaux] is de tekenreeks jSON van het type [Response<List<Creneau>].
De verkregen resultaten zijn als volgt:
![]() |
of deze, als het tijdslot niet bestaat:
![]() |
Uit dit voorbeeld kunnen we de volgende regel afleiden:
- de methoden van de webserver / jSON retourneren een object van het type [Response<T>] dat wordt geserialiseerd naar jSON;
- als het type T één of meerdere filters jSON heeft, wordt voor de serialisatie een mapper met dezelfde filters gebruikt;
8.4.11.10. De URL [/getRvMedecinJour/{idMedecin}/{jour}]
De URL [/getRvMedecinJour/{idMedecin}/{jour}] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:
// lijst met afspraken van een arts
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// het antwoord
Response<List<Rv>> response=null;
boolean erreur = false;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// de datum wordt gecontroleerd
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// de arts wordt opgehaald
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// lijst met zijn afspraken
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// antwoord
return jsonMapperLongRv.writeValueAsString(response);
}
- moet de tekenreeks jSON worden omgezet naar het type [Response<List<Rv>>]. De klasse [Rv] heeft een veld [Rv.creneau]. Als dit veld wordt geserialiseerd, komt men het filter jSON [creneauFilter] tegen;
- regel 47: het object van het type [Response<List<Rv>>] uit regel 7 wordt geserialiseerd tot jSON;
Laten we eens kijken naar het geval waarin de lijst met afspraken in regel 42 is verkregen. De klasse [Rv] in het project [rdvmedecins-metier-dao] is als volgt gedefinieerd:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// kenmerken van een afspraak
@Temporal(TemporalType.DATE)
private Date jour;
// een afspraak is gekoppeld aan een klant
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// een afspraak is gekoppeld aan een tijdslot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// vreemde sleutels
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- regel 11: de klant wordt opgezocht met de modus [FetchType.LAZY];
- regel 18: het tijdvak wordt opgezocht met de modus [FetchType.LAZY];
Laten we nog eens kijken naar de query JPQL, die de afspraken opzoekt:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Er worden expliciet join-bewerkingen uitgevoerd om de velden [client] en [creneau] op te halen. Bovendien krijgen we door de join [cr.medecin.id=?1] ook de arts mee. De arts zal dus in de reeks jSON van elke afspraak verschijnen. Deze dubbele informatie is echter overbodig. We hebben gezien hoe we dit probleem kunnen oplossen met behulp van een filter jSON op het object [Creneau]. Vanwege de modi [FetchType.LAZY] van de velden [client] en [creneau] van de klasse [Rv], zullen we binnenkort merken dat het nodig is om een filter jSON toe te passen op de klasse [RV] van het project [rdvmedecins-metier-dao]:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
We zullen de serialisatie van het object [Rv] controleren met het filter [rvFilter]. Blijkbaar hoeven we hier niet te filteren, omdat we alle velden van het object van het type [Rv] nodig hebben. Omdat we echter hebben aangegeven dat de klasse een filter jSON heeft, moeten we dit filter definiëren voor elke serialisatie van een object van het type [Rv], anders krijgen we een uitzondering. Hiervoor gebruiken we de volgende mapper jSON, gedefinieerd in de klasse [rdvMedecinsController]:
@Autowired
private ObjectMapper jsonMapperLongRv;
Deze mapper is als volgt gedefinieerd in de configuratieklasse [WebConfig]:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
- regel 4: we geven aan dat alle velden van het object [Rv] moeten worden geserialiseerd;
- regel 5: we geven aan dat in het object [Creneau] het veld [medecin] niet moet worden geserialiseerd;
- regel 6: we voegen de twee filters [rvFilter] en [creneauFilter] toe aan de filters jSON van het object [jsonMapperLongRv];
De verkregen resultaten zijn als volgt:
![]() |
of deze resultaten met een dag zonder afspraak:
![]() |
of deze met een onjuiste dag:
![]() |
of deze met een onjuiste arts:
![]() |
8.4.11.11. De URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
De URL [/getAgendaMedecinJour/{idMedecin}/{jour}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// het antwoord
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// de datum wordt gecontroleerd
Date jourAgenda = null;
if (!erreur) {
// de datum wordt gecontroleerd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// de arts wordt opgehaald
Medecin médecin = null;
if (!erreur) {
// de arts wordt opgehaald
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// zijn agenda ophalen
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// antwoord
return jsonMapperLongRv.writeValueAsString(response);
}
- regels 6, 49: de tekenreeks jSON wordt omgezet naar het type [AgendaMedecinJour], ingekapseld in een object [Response];
Het type [AgendaMedecinJour] is als volgt:
public class AgendaMedecinJour implements Serializable {
// velden
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
Het type [CreneauMedecinJour] is als volgt:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// velden
private Creneau creneau;
private Rv rv;
De velden [creneau] en [rv] hebben filters jSON die moeten worden geconfigureerd. Dit gebeurt in regel 49 van de methode [getAgendaMedecinJour], die gebruikmaakt van de mapper jSON [jsonMapperLongRv] die we al eerder zijn tegengekomen:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
De verkregen resultaten zijn als volgt:
![]() |
Hierboven zien we dat dokter PELISSIER op 28/01/2015 om 8.20 uur een afspraak heeft met mevrouw Brigitte BISTROU;
of deze, als de datum onjuist is:
![]() |
of deze, als het nummer van de arts ongeldig is:
![]() |
8.4.11.12. De URL [/getMedecinById/{id}]
De URL [/getMedecinById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// het antwoord
Response<Medecin> response;
// status van de applicatie
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// antwoord
return jsonMapper.writeValueAsString(response);
}
- regels 5, 13: de methode zet de tekenreeks jSON om naar het type [Medecin]. Dit type heeft geen filterannotatie jSON. Daarom wordt op regel 14 de mapper jSON zonder filters gebruikt;
Op regel 10 is de methode [getMedecin] als volgt:
private Response<Medecin> getMedecin(long id) {
// de arts wordt opgehaald
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// bestaande arts?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
De verkregen resultaten zijn als volgt:
![]() |
of deze, als het artsnummer onjuist is:
![]() |
8.4.11.13. De URL [/getClientById/{id}]
De URL [/getClientById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// het antwoord
Response<Client> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// antwoord
return jsonMapper.writeValueAsString(response);
}
- regels 5, 13: de methode zet de tekenreeks jSON om naar het type [Client]. Dit type heeft geen filterannotatie jSON. Daarom wordt in regel 13 de mapper jSON zonder filters gebruikt;
Op regel 11 is de methode [getClient] als volgt:
private Response<Client> getClient(long id) {
// de klant wordt opgehaald
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// bestaande klant?
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// ok
return new Response<Client>(0, null, client);
}
De verkregen resultaten zijn als volgt:
![]() |
of deze, als het klantnummer onjuist is:
![]() |
8.4.11.14. De URL [/getCreneauById/{id}]
De URL [/getCreneauById/{id}] wordt verwerkt via de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// het antwoord
Response<Creneau> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// het tijdvak wordt teruggegeven
response = getCreneau(id);
}
// antwoord
return jsonMapperShortCreneau.writeValueAsString(response);
}
- regels 5, 14: de methode zet de tekenreeks jSON om naar het type [Response<Creneau>];
Regel 8, de methode [getCreneau] is als volgt:
private Response<Creneau> getCreneau(long id) {
// het tijdvak wordt opgehaald
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// bestaat het tijdvak?
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// ok
return new Response<Creneau>(0, null, créneau);
}
Laten we de code van de entiteit [Creneau] nog eens bekijken:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// kenmerken van een tijdvak van RV
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// een tijdvak is gekoppeld aan een arts
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// vreemde sleutel
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- regels 14-16: omdat het veld [medecin] in de modus [fetch = FetchType.LAZY] staat, wordt het niet meegenomen wanneer een tijdslot wordt opgehaald via zijn [id]. Het is daarom noodzakelijk om het uit te sluiten van de serialisatie. Zonder deze uitsluiting treedt er een uitzondering op. Dit komt doordat het serialisatieobject [mapper] de methode [getMedecin] aanroept om het veld [medecin] op te halen. Maar bij een implementatie met JPA / Hibernate heeft de modus [fetch = FetchType.LAZY] van het veld [medecin] een object [Creneau] geretourneerd, waarvan de methode [getMedecin] is geprogrammeerd om de arts op te halen in de context JPA. Dit wordt een object [proxy] genoemd. Laten we echter even terugkijken naar de architectuur van de webapplicatie:
![]() |
De controller bevindt zich in het blok [Contrôleurs / Actions]. Wanneer men zich in dit blok bevindt, bestaat het begrip context JPA niet meer. Deze context wordt aangemaakt tijdens de bewerkingen van de laag [DAO]. Hij blijft daarna niet bestaan. Wanneer de controller dus probeert toegang te krijgen tot de context JPA, treedt er een uitzondering op die aangeeft dat deze is gesloten. Om deze uitzondering te voorkomen, moet de serialisatie van het veld [medecin] van de klasse [Rv] worden verhinderd. Dit is wat de mapper jSON [jsonMapperShortCreneau] doet:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
De verkregen resultaten zijn als volgt:
![]() |
of deze, als het slotnummer onjuist is:
![]() |
8.4.11.15. De URL [/getRvById/{id}]
De URL [/getRvById/{id}] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// het antwoord
Response<Rv> response;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// de afspraak wordt opgehaald
response = getRv(id);
}
// antwoord
return jsonMapperShortRv.writeValueAsString(response);
}
- regels 5, 14: de methode retourneert de tekenreeks jSON van het type [Response<Rv>];
Regel 11: de methode [getRv] is als volgt:
private Response<Rv> getRv(long id) {
// Rv wordt opgehaald
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Bestaat rv?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// ok
return new Response<Rv>(0, null, rv);
}
De klasse [Rv] heeft twee velden met de annotatie [fetch = FetchType.LAZY], namelijk de velden [creneau] en [client]. Deze velden worden dus niet meegenomen wanneer men een [Rv] opzoekt via de primaire sleutel. Om dezelfde redenen als hierboven moeten ze dus van de serialisatie worden uitgesloten. Dit is wat de volgende mapper [jsonMapperShortRv] doet, gedefinieerd in de klasse [WebConfig]:
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
De verkregen resultaten zijn als volgt:
![]() |
of deze, als het afsprachnummer onjuist is:
![]() |
8.4.11.16. De URL [/ajouterRv]
De URL [/ajouterRv] wordt verwerkt volgens de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
// het antwoord
Response<Rv> response = null;
boolean erreur = false;
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// de verzonden waarden worden opgehaald
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// de verzonden waarden worden opgehaald
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// we controleren de datum
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// het tijdvak wordt opgehaald
Response<Creneau> responseCréneau = null;
if (!erreur) {
// het tijdvak wordt opgehaald
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// de klant wordt opgehaald
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// de klant wordt opgehaald
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// de afspraak wordt toegevoegd
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// antwoord
return jsonMapperLongRv.writeValueAsString(response);
}
- regels 5, 67: de methode moet de tekenreeks jSON van het type [Response<Rv>] retourneren;
- regel 3: de annotatie [@RequestBody PostAjouterRv post] haalt de body van POST op en plaatst deze in de parameter [PostAjouterRv post]. Deze inhoud is afkomstig van jSON [consumes = "application/json; charset=UTF-8"] en wordt automatisch gedeserialiseerd naar het volgende type [PostAjouterRv]:
public class PostAjouterRv {
// gegevens van het bericht
private String jour;
private long idClient;
private long idCreneau;
...
- vervolgens is er code die al in een of andere vorm is aangetroffen;
- regel 67: het instellen van de filters jSON, [creneauFilter] en [rvFilter]. De methode zet de tekenreeks jSON om in een type [Response<Rv>], waarbij Rv is verkregen in regel 61. Het object [Rv] omvat een object [Creneau] en een object [Client]. Het object [Creneau] heeft een afhankelijkheid [FetchType.LAZY] van een object [Medecin] en is verkregen in de regels 36-44. Het is opgezocht in de context JPA via zijn primaire sleutel en is opgehaald zonder zijn afhankelijkheid [FetchType.LAZY]. Uiteindelijk
- heeft het object [Rv] al zijn afhankelijkheden. Deze kunnen worden geserialiseerd;
- het object [Creneau] mist zijn afhankelijkheid [medecin]. Deze mag dus niet worden geserialiseerd;
De mapper jSON [jsonMapperLongRv], gedefinieerd in de klasse [WebConfig], voldoet aan deze voorwaarden:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
De verkregen resultaten zien er als volgt uit met de client [Advanced Rest Client]:
![]() |
- in [1], de URL van de POST;
- in [2], de POST;
- in [3], de geplaatste waarde;
- in [4a] is deze geboekte waarde afkomstig van jSON;
![]() |
- in [4b] geeft de klant aan dat hij jSON verstuurt;
- in [5] geeft de server aan dat hij jSON terugstuurt;
![]() |
- in [6], het antwoord jSON van de server dat de toegevoegde afspraak weergeeft. Hierin is de ID [id] van de toegevoegde afspraak te zien;
Met een niet-bestaand tijdslot krijgen we het volgende:
![]() |
8.4.11.17. De URL [/supprimerRv]
De URL [/supprimerRv] wordt verwerkt door de volgende methode van de controller [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// het antwoord
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// status van de applicatie
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// de verzonden waarden worden opgehaald
long idRv = post.getIdRv();
// de rv wordt opgehaald
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// verwijdering van de rv
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// antwoord
return jsonMapper.writeValueAsString(response);
}
- regel 5: het type [Void] is de klasse die overeenkomt met het primitieve type [void];
- regels 5, 34: de methode retourneert de tekenreeks jSON van het type [Response<Void>], dat geen filters jSON heeft. Daarom wordt in regel 34 de mapper jSON zonder filters gebruikt;
- regel 3: de methode heeft als parameter de body van de POST, d.w.z. de verzonden waarde. Deze wordt ontvangen in de vorm jSON [consumes = "application/json; charset=UTF-8"] en automatisch gedeserialiseerd naar het volgende type [PostSupprimerRv]:
public class PostSupprimerRv {
// gegevens van de post
private long idRv;
- regel 28: wanneer het verwijderen is gelukt, wordt een antwoord verzonden met [status=0];
De verkregen resultaten zijn als volgt:
![]() |
![]() |
- in [5] geeft het veld [status=0] aan dat het verwijderen is gelukt;
Bij een afsprachnummer dat niet bestaat, krijgt men het volgende:
![]() |
We zijn klaar met de controller. We gaan nu bekijken hoe we het project kunnen uitvoeren.
8.4.11.18. De uitvoerbare klasse van de webservice
![]() |
De klasse [Boot] [1] ziet er als volgt uit:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
Op regel 10 wordt de statische methode [SpringApplication.run] uitgevoerd met als eerste parameter de projectconfiguratieklasse [AppConfig]. Deze methode voert de automatische configuratie van het project uit, start de in de afhankelijkheden ingebouwde Tomcat-server en implementeert daar de controller [RdvMedecinsController].
De logbestanden worden beheerd door de volgende bestanden [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- aan encoders wordt standaard het type ch.qos.logback.classic.encoder.PatternLayoutEncoder toegewezen -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- logniveau-instelling -->
<root level="info"> <!-- uit, info, debug, waarschuwing -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- regel 9: het algemene logniveau is ingesteld op [info];
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
Regels 1-2 maken een specifiek logniveau mogelijk voor bepaalde onderdelen van de applicatie:
- regel 1: we willen de logboeken van de laag [web];
- regel 2: we willen geen logboeken van de laag [JPA];
- regel 3: geen Spring Boot-banner;
De logs tijdens de uitvoering zijn als volgt:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- regel 18: de Tomcat-server is actief;
- regel 21: de Spring-context wordt geïnitialiseerd;
- regels 27-38: de door de webservice blootgestelde URL worden gedetecteerd;
- regel 44: de Tomcat-server is gereed en wacht op verzoeken op poort 8080;
Als we het bestand [application.properties] als volgt wijzigen:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
krijgen we de volgende logbestanden:
Als we bovendien het bestand [logback.xml] als volgt wijzigen:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders krijgen standaard het type ch.qos.logback.classic.encoder.PatternLayoutEncoder toegewezen -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- logniveau-controle -->
<root level="off"> <!-- uit, info, debug, waarschuwing -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
krijgt men de volgende logbestanden:
We zien dus dat we enige controle hebben over de logberichten die in de console verschijnen. Het niveau [info] is vaak het juiste logniveau.
We hebben nu een operationele webservice die via een webclient kan worden opgevraagd. We gaan nu de beveiliging van deze service aanpakken: we willen dat alleen bepaalde personen de afspraken van de artsen kunnen beheren. Hiervoor gaan we het Spring Security-framework gebruiken, een onderdeel van het Spring-ecosysteem.
8.4.12. Inleiding tot Spring Security
We gaan opnieuw een Spring-gids importeren door de onderstaande stappen 1 tot en met 3 te volgen:
![]() |
![]() |
Het project bestaat uit de volgende onderdelen:
- in de map [templates] bevinden zich de pagina's HTML van het project;
- [Application]: is de uitvoerbare klasse van het project;
- [MvcConfig]: is de configuratieklasse van Spring MVC;
- [WebSecurityConfig]: is de configuratieklasse voor Spring Security;
8.4.12.1. Maven-configuratie
Het project [3] is een Maven-project. Laten we het bestand [pom.xml] bekijken om de afhankelijkheden te zien:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- tag::security[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- regels 10-14: het project is een Spring Boot-project;
- regels 17-20: afhankelijkheid van het [Thymeleaf]-framework;
- regels 22-25: afhankelijkheid van het Spring Security-framework;
8.4.12.2. De Thymeleaf-weergaven
![]() |
De weergave [home.html] ziet er als volgt uit:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- regel 12: het attribuut [th:href="@{/hello}"] genereert het attribuut [href] van de tag <a>. De waarde [@{/hello}] genereert het pad [<context>/hello], waarbij [context] de context van de webapplicatie is;
De gegenereerde code HTML is als volgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
De weergave [hello.html] is als volgt:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- regel 9: Het attribuut [th:inline="text"] genereert de tekst van de tag <h1>. Deze tekst bevat een $-uitdrukking die moet worden geëvalueerd. Het element [[${#httpServletRequest.remoteUser}]] is de waarde van het attribuut [RemoteUser] van de huidige aanvraag HTTP. Dit is de naam van de aangemelde gebruiker;
- regel 10: een formulier HTML. Het attribuut [th:action="@{/logout}"] genereert het attribuut [action] van de tag [form]. De waarde [@{/logout}] genereert het pad [<context>/logout], waarbij [context] de context van de webapplicatie is;
De gegenereerde code HTML is als volgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- regel 8: de vertaling van Hello [[${#httpServletRequest.remoteUser}]]!;
- regel 9: de vertaling van @{/logout};
- regel 11: een verborgen veld met de naam (attribuut name) _csrf;
De laatste weergave [login.html] is als volgt:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- regel 9: het attribuut [th:if="${param.error}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL die de inlogpagina weergeeft, de parameter [error] (http://context/login?error) bevat;
- regel 10: het attribuut [th:if="${param.logout}"] zorgt ervoor dat de tag <div> alleen wordt gegenereerd als de URL, die de inlogpagina weergeeft, de parameter [logout] bevat (http://context/login?logout);
- regels 11-23: een formulier HTML;
- regel 11: het formulier wordt verzonden naar de URL [<context>/login], waarbij <context> de context van de webapplicatie is;
- regel 13: een invoerveld met de naam [username];
- regel 17: een invoerveld met de naam [password];
De gegenereerde code HTML is als volgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<div>
<label>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
Op regel 28 is te zien dat Thymeleaf een verborgen veld met de naam [_csrf] heeft toegevoegd.
8.4.12.3. Spring-configuratie MVC
![]() |
De klasse [MvcConfig] configureert het Spring-framework MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- regel 7: de annotatie [@Configuration] maakt van de klasse [MvcConfig] een configuratieklasse;
- regel 8: de klasse [MvcConfig] breidt de klasse [WebMvcConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
- regel 10: herdefinitie van een methode van de bovenliggende klasse;
- regels 11-16: met de methode [addViewControllers] kunnen URL worden gekoppeld aan weergaven HTML. De volgende koppelingen worden hier gemaakt:
URL | weergave |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
De extensie [html] en de map [templates] zijn de standaardwaarden die door Thymeleaf worden gebruikt. Deze kunnen via de configuratie worden gewijzigd. De map [templates] moet zich in de root van het classpath van het project bevinden:
![]() |
Boven [1] zijn de mappen [java] en [resources] beide bronmappen (source folders). Dit betekent dat hun inhoud zich in de root van het classpath van het project bevindt. Dus in [2] zullen de mappen [hello] en [templates] zich in de root van het classpath bevinden.
8.4.12.4. Configuratie van Spring Security
![]() |
De klasse [WebSecurityConfig] configureert het Spring Security-framework:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- regel 9: de annotatie [@Configuration] maakt van de klasse [WebSecurityConfig] een configuratieklasse;
- regel 10: de annotatie [@EnableWebSecurity] maakt van de klasse [WebSecurityConfig] een Spring Security-configuratieklasse;
- regel 11: de klasse [WebSecurity] breidt de klasse [WebSecurityConfigurerAdapter] uit om bepaalde methoden ervan opnieuw te definiëren;
- regel 12: herdefinitie van een methode van de bovenliggende klasse;
- regels 13-16: de methode [configure(HttpSecurity http)] wordt opnieuw gedefinieerd om de toegangsrechten voor de verschillende URL-klassen van de applicatie vast te leggen;
- regel 14: met de methode [http.authorizeRequests()] kunnen URL'en aan toegangsrechten worden gekoppeld. Hierin worden de volgende koppelingen gemaakt:
URL | regel | code |
toegang zonder authenticatie | | |
alleen toegang na authenticatie |
- regel 15: definieert de authenticatiemethode. De authenticatie vindt plaats via een formulier URL [/login] dat voor iedereen toegankelijk is [http.formLogin().loginPage("/login").permitAll()]. Het uitloggen (logout) is eveneens voor iedereen toegankelijk;
- regels 19-21: herdefiniëren de methode [configure(AuthenticationManagerBuilder auth)] die de gebruikers beheert;
- regel 20: de authenticatie vindt plaats met ‘vast’ gedefinieerde gebruikers [auth.inMemoryAuthentication()]. Een gebruiker wordt hier gedefinieerd met de login [user], het wachtwoord [password] en de rol [USER]. Aan gebruikers met dezelfde rol kunnen dezelfde rechten worden toegekend;
8.4.12.5. Uitvoerbare klasse
![]() |
De klasse [Application] is als volgt:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- regel 8: de annotatie [@EnableAutoConfiguration] vraagt Spring Boot (regel 3) om de configuratie uit te voeren die de ontwikkelaar niet expliciet heeft uitgevoerd;
- regel 9: maakt van de klasse [Application] een Spring-configuratieklasse;
- regel 10: vraagt om de map van de klasse [Application] te scannen op zoek naar Spring-componenten. De twee klassen [MvcConfig] en [WebSecurityConfig] worden op deze manier gedetecteerd omdat ze de annotatie [@Configuration] hebben;
- regel 13: de methode [main] van de uitvoerbare klasse;
- regel 14: de statische methode [SpringApplication.run] wordt uitgevoerd met de configuratieklasse [Application] als parameter. We zijn dit proces al eerder tegengekomen en weten dat de Tomcat-server die in de Maven-afhankelijkheden van het project is opgenomen, zal worden gestart en dat het project daarop zal worden geïmplementeerd. We hebben gezien dat vier URL-bestanden werden beheerd door [/, /home, /login, /hello] en dat sommige werden beschermd door toegangsrechten.
8.4.12.6. Testen van de applicatie
Laten we beginnen met het opvragen van de URL [/], een van de vier geaccepteerde URL-bestanden. Deze is gekoppeld aan de weergave [/templates/home.html]:
![]() |
De aangevraagde URL [/] is voor iedereen toegankelijk. Daarom hebben we deze verkregen. De link [here] is als volgt:
De URL [/hello] wordt opgevraagd wanneer je op de link klikt. Deze is beveiligd:
URL | regel | code |
toegang zonder authenticatie | | |
alleen toegang na authenticatie |
U moet geauthenticeerd zijn om deze te verkrijgen. Spring Security zal de browser van de klant dan doorverwijzen naar de authenticatiepagina. Volgens de getoonde configuratie is dit de pagina URL [/login]. Deze is voor iedereen toegankelijk:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
We krijgen dus [1]:
![]() |
De broncode van de verkregen pagina is als volgt:
- op regel 7 verschijnt een verborgen veld dat niet in de oorspronkelijke pagina [login.html] voorkomt. Dit is door Thymeleaf toegevoegd. Deze code, genaamd CSRF (Cross Site Request Forgery), is bedoeld om een beveiligingslek te verhelpen. Dit token moet samen met de authenticatie naar Spring Security worden teruggestuurd, zodat deze wordt geaccepteerd;
We herinneren ons dat alleen de gebruiker user/password door Spring Security wordt herkend. Als we iets anders invoeren in [2], krijgen we dezelfde pagina te zien met een foutmelding in [3]. Spring Security heeft de browser doorgestuurd naar de pagina URL [http://localhost:8080/login?error]. De aanwezigheid van de parameter [error] heeft ervoor gezorgd dat de tag werd weergegeven:
<div th:if="${param.error}">Invalid username and password.</div>
Laten we nu de verwachte waarden voor user/password [4] invoeren:
![]() |
- in [4] melden we ons aan;
- in [5] leidt Spring Security ons om naar URL [/hello], omdat dit de URL is die we opvroegen toen we werden doorgestuurd naar de inlogpagina. De identiteit van de gebruiker werd weergegeven in de volgende regel van [hello.html]:
De pagina [5] toont het volgende formulier:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Wanneer u op de knop [Sign Out] klikt, wordt er een POST uitgevoerd op de URL [/logout]. Deze is, net als de URL en [/login], voor iedereen toegankelijk:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
In onze associatie URL / views hebben we niets gedefinieerd voor de URL en [/logout]. Wat gaat er gebeuren? Laten we het eens proberen:
![]() |
- in [6] klikken we op de knop [Sign Out];
- in [7] zien we dat we zijn doorgestuurd naar de URL [http://localhost:8080/login?logout]. Het is Spring Security die om deze omleiding heeft gevraagd. Door de aanwezigheid van de parameter [logout] in URL werd de volgende regel in de weergave getoond:
<div th:if="${param.logout}">You have been logged out.</div>
8.4.12.7. Conclusion
In het vorige voorbeeld hadden we eerst de webapplicatie kunnen schrijven en deze daarna beveiligen. Spring Security is niet ingrijpend. Het is mogelijk om de beveiliging van een reeds geschreven webapplicatie in te stellen. Daarnaast hebben we het volgende ontdekt:
- het is mogelijk om een authenticatiepagina te definiëren;
- de authenticatie moet vergezeld gaan van het door Spring Security verstrekte token CSRF;
- als de authenticatie mislukt, wordt men doorgestuurd naar de authenticatiepagina, met bovendien een parameter ‘error’ in het token URL;
- als de authenticatie slaagt, wordt men doorgestuurd naar de pagina die werd opgevraagd op het moment dat de authenticatie plaatsvond. Als de authenticatiepagina rechtstreeks wordt opgevraagd zonder tussenpagina, dan leidt Spring Security ons om naar de URL [/] (dit geval is niet behandeld);
- je meldt je af door de pagina URL [/logout] op te vragen met een POST. Spring Security leidt ons vervolgens door naar de authenticatiepagina met de parameter logout in de URL;
Al deze conclusies zijn gebaseerd op het standaardgedrag van Spring Security. Dit gedrag kan via de configuratie worden gewijzigd door bepaalde methoden van de klasse [WebSecurityConfigurerAdapter] opnieuw te definiëren.
De vorige tutorial zal ons verderop weinig helpen. We gaan namelijk gebruikmaken van:
- een database om gebruikers, hun wachtwoorden en hun rollen op te slaan;
- authenticatie via headers HTTP;
Er zijn vrij weinig tutorials te vinden voor wat we hier willen doen. De oplossing die wordt voorgesteld, is een samenvoeging van code die hier en daar is gevonden.
8.4.13. Beveiliging instellen voor de webdienst voor afspraken
8.4.13.1. De database
De database [rdvmedecins] wordt aangepast om rekening te houden met gebruikers, hun wachtwoorden en hun rollen. Er komen drie nieuwe tabellen bij:

Tabel [USERS]: de gebruikers
- ID: primaire sleutel;
- VERSION: versiekolom van de rij;
- IDENTITY: een beschrijvende identiteit van de gebruiker;
- LOGIN: de gebruikersnaam van de gebruiker;
- PASSWORD: zijn wachtwoord;
In de tabel USERS worden wachtwoorden niet in leesbare vorm opgeslagen:
![]() |
Het algoritme dat de wachtwoorden versleutelt, is het algoritme BCRYPT.
Tabel [ROLES]: de rollen
- ID: primaire sleutel;
- VERSION: versiekolom van de rij;
- NAME: rolnaam. Standaard verwacht Spring Security namen in de vorm ROLE_XX, bijvoorbeeld ROLE_ADMIN of ROLE_GUEST;
![]() |
Tabel [USERS_ROLES]: koppelingstabel USERS / ROLES
Een gebruiker kan meerdere rollen hebben, en een rol kan meerdere gebruikers omvatten. Er is sprake van een veel-op-veel-relatie, die wordt weergegeven door de tabel [USERS_ROLES].
- ID: primaire sleutel;
- VERSION: versiekolom van de rij;
- USER_ID: gebruikers-ID;
- ROLE_ID: rol-ID;
![]() |
Omdat we de database aanpassen, moeten alle lagen van het project [métier, DAO, JPA] worden aangepast:
![]() |
8.4.13.2. Het nieuwe project STS van [métier, DAO, JPA]
Het project [rdvmedecins-metier-dao] ontwikkelt zich als volgt:
![]() |
- naar [1]: het nieuwe project;
- naar [2]: de wijzigingen die voortvloeien uit de inachtneming van de beveiliging zijn gebundeld in één enkel pakket, [rdvmedecins.security]. Deze nieuwe elementen behoren tot de lagen [JPA] en [DAO], maar zijn voor het gemak in één pakket samengebracht.
8.4.13.3. De nieuwe entiteiten [JPA]
![]() |
De laag JPA definieert drie nieuwe entiteiten:
![]() |
De klasse [User] is de afspiegeling van de tabel [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// eigenschappen
private String identity;
private String login;
private String password;
// fabrikant
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identiteit
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters en setters
....
}
- regel 9: de klasse is een uitbreiding van de klasse [AbstractEntity] die al voor de andere entiteiten wordt gebruikt;
- regels 13-15: er worden geen kolomnamen opgegeven omdat ze dezelfde naam hebben als de velden waaraan ze zijn gekoppeld;
De klasse [Role] is een afspiegeling van de tabel [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// eigenschappen
private String name;
// constructors
public Role() {
}
public Role(String name) {
this.name = name;
}
// identiteit
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters en setters
...
}
De klasse [UserRole] is de weergave van de tabel [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// een UserRole verwijst naar een User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// een UserRole verwijst naar een rol
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters en setters
...
}
- regels 15-17: geven de externe sleutel weer van de tabel [USERS_ROLES] naar de tabel [USERS];
- regels 19-21: definiëren de vreemde sleutel van de tabel [USERS_ROLES] naar de tabel [ROLES];
8.4.13.4. Wijzigingen in de laag [DAO]
![]() |
De laag [DAO] wordt uitgebreid met drie nieuwe [Repository]:
![]() |
De interface [UserRepository] beheert de toegang tot de entiteiten [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// lijst met rollen van een gebruiker die wordt geïdentificeerd aan de hand van zijn id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// lijst met rollen van een gebruiker die wordt geïdentificeerd aan de hand van zijn login en wachtwoord
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// zoeken naar een gebruiker op basis van zijn login
User findUserByLogin(String login);
}
- regel 9: de interface [UserRepository] breidt de interface [CrudRepository] van Spring Data uit (regel 4);
- regels 12-13: met de methode [getRoles(User user)] kunnen alle rollen worden opgehaald van een gebruiker die is geïdentificeerd aan de hand van zijn [id]
- regels 16-17: idem, maar dan voor een gebruiker die wordt geïdentificeerd aan de hand van zijn login en wachtwoord;
- regel 20: om een gebruiker op te zoeken via zijn gebruikersnaam;
De interface [RoleRepository] beheert de toegang tot de entiteiten [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// zoeken naar een rol op naam
Role findRoleByName(String name);
}
- regel 5: de interface [RoleRepository] breidt de interface [CrudRepository] uit;
- regel 8: men kan een rol op naam zoeken;
De interface [userRoleRepository] beheert de toegang tot de entiteiten [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- regel 5: de interface [UserRoleRepository] breidt alleen de interface [CrudRepository] uit zonder er nieuwe methoden aan toe te voegen;
8.4.13.5. De klassen voor het beheer van gebruikers en rollen
![]() |
Spring Security vereist het aanmaken van een klasse die de volgende interface [UsersDetail] implementeert:
![]() |
Deze interface wordt hier geïmplementeerd door de klasse [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// eigenschappen
private User user;
private UserRepository userRepository;
// constructors
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters en setters
...
}
- regel 10: de klasse [AppUserDetails] implementeert de interface [UserDetails];
- regels 15-16: de klasse kapselt een gebruiker in (regel 15) en de repository waarmee de details van deze gebruiker kunnen worden opgehaald (regel 16);
- regels 22-25: de constructor die de klasse instantiëert met een gebruiker en diens repository;
- regels 28-35: implementatie van de methode [getAuthorities] van de interface [UserDetails]. Deze methode moet een verzameling elementen van het type [GrantedAuthority] of een afgeleid type samenstellen. Hier gebruiken we het afgeleide type [SimpleGrantedAuthority] (regel 32) dat de naam van een van de rollen van de gebruiker uit regel 15 omvat;
- regels 31-33: we doorlopen de lijst met rollen van de gebruiker uit regel 15 om een lijst met elementen van het type [SimpleGrantedAuthority] samen te stellen;
- regels 38-40: implementeren de methode [getPassword] van de interface [UserDetails]. Het wachtwoord van de gebruiker uit regel 15 wordt weergegeven;
- regels 38-40: implementeren de methode [getUserName] van de interface [UserDetails]. De gebruikersnaam uit regel 15 wordt geretourneerd;
- regels 47-50: het account van de gebruiker verloopt nooit;
- regels 52-55: het account van de gebruiker wordt nooit geblokkeerd;
- regels 57-60: de inloggegevens van de gebruiker verlopen nooit;
- regels 62-65: het account van de gebruiker is altijd actief;
Spring Security vereist ook dat er een klasse bestaat die de interface [AppUserDetailsService] implementeert:
![]() |
Deze interface wordt geïmplementeerd door de volgende klasse [AppUserDetailsService]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// de gebruiker zoeken op basis van zijn login
User user = userRepository.findUserByLogin(login);
// gevonden?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// de gegevens van de gebruiker weergeven
return new AppUserDetails(user, userRepository);
}
}
- regel 9: de klasse wordt een Spring-component en is dus beschikbaar in de context;
- regels 12-13: de component [UserRepository] wordt hier geïnjecteerd;
- regels 16-25: implementatie van de methode [loadUserByUsername] van de interface [UserDetailsService] (regel 10). De parameter is de gebruikersnaam;
- regel 18: er wordt gezocht naar de gebruiker op basis van zijn login;
- regels 20-22: als hij niet wordt gevonden, wordt er een uitzondering gegenereerd;
- regel 24: er wordt een object [AppUserDetails] aangemaakt en weergegeven. Dit is inderdaad van het type [UserDetails] (regel 16);
8.4.13.6. Tests van de laag [DAO]
![]() |
Allereerst maken we een uitvoerbare klasse [CreateUser] die een gebruiker met een rol kan aanmaken:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// syntaxis: login wachtwoord roleName
// er zijn drie parameters nodig
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// de parameters worden opgehaald
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// Spring-context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// bestaat de rol al?
Role role = roleRepository.findRoleByName(roleName);
// als deze niet bestaat, maken we deze aan
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// Bestaat de gebruiker al?
User user = userRepository.findUserByLogin(login);
// als deze niet bestaat, maken we deze aan
if (user == null) {
// het wachtwoord wordt gehasht met bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// we slaan de gebruiker op
user = userRepository.save(new User(login, login, crypt));
// we leggen de koppeling met de rol aan
userRoleRepository.save(new UserRole(user, role));
} else {
// de gebruiker bestaat al – heeft hij de gevraagde rol?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// indien niet gevonden, wordt de koppeling met de rol aangemaakt
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// Spring-context afsluiten
context.close();
}
}
- regel 17: de klasse verwacht drie argumenten die een gebruiker definiëren: zijn login, zijn wachtwoord en zijn rol;
- regels 25-27: de drie parameters worden opgehaald;
- regel 29: de Spring-context wordt opgebouwd op basis van de configuratieklasse [DomainAndPersistenceConfig]. Deze klasse bestond al in het oorspronkelijke project. Deze moet als volgt worden aangepast:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- regel 1: er moet worden aangegeven dat er nu [Repository]-componenten in het pakket [rdvmedecins.security] zitten;
- regel 4: er moet worden aangegeven dat er nu entiteiten JPA in het pakket [rdvmedecins.security] zitten;
Laten we teruggaan naar de code voor het aanmaken van een gebruiker:
- regels 30-32: we halen de referenties op van de drie [Repository] die we kunnen gebruiken om de gebruiker aan te maken;
- regel 34: we controleren of de rol al bestaat;
- regels 36-38: als dat niet het geval is, maken we deze aan in de database. De naam zal er als volgt uitzien: [ROLE_XX];
- regel 40: we controleren of de login al bestaat;
- regels 42-49: als de login niet bestaat, maken we deze aan in de database;
- regel 44: het wachtwoord wordt versleuteld. Hiervoor wordt de klasse [BCrypt] van Spring Security gebruikt (regel 4). We hebben dus de bibliotheken van dit framework nodig. Het bestand [pom.xml] bevat een nieuwe afhankelijkheid:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- regel 46: de gebruiker wordt in de database opgeslagen;
- regel 48: evenals de relatie die hem aan zijn rol koppelt;
- regels 51-57: als de gebruiker al bestaat, wordt gekeken of de rol die we hem willen toewijzen al bij zijn rollen hoort;
- regel 59-61: als de gezochte rol niet is gevonden, wordt er een rij aangemaakt in de tabel [USERS_ROLES] om de gebruiker aan zijn rol te koppelen;
- er is geen bescherming ingebouwd tegen mogelijke uitzonderingen. Dit is een ondersteunende klasse om snel een gebruiker met een rol aan te maken.
Wanneer de klasse wordt uitgevoerd met de argumenten [x x guest], krijgt men in de database de volgende resultaten:
Tabel [USERS]
![]() Tabel |
Tabel [ROLES]
![]() |
Tabel [USERS_ROLES]
![]() |
Laten we nu eens kijken naar de tweede klasse [UsersTest], die een test is van JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// de gebruiker wordt opgehaald: [admin]
User user = userRepository.findUserByLogin("admin");
// we controleren of zijn wachtwoord [admin] is
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// de rol van admin / admin wordt gecontroleerd
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// we halen de gebruiker [admin] op
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we controleren of het wachtwoord [admin] is
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// de rol van admin / admin wordt gecontroleerd
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// hulpprogramma - toont de elementen van een verzameling
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- regels 27-34: visuele test. Alle gebruikers worden met hun rollen weergegeven;
- regels 36-46: we controleren of de gebruiker [admin] het wachtwoord [admin] en de rol [ROLE_ADMIN] heeft, met behulp van de repository [UserRepository];
- regel 41: [admin] is het wachtwoord in leesbare tekst. In de database is het versleuteld volgens het algoritme BCrypt. Met de methode [BCrypt.checkpw] kan worden gecontroleerd of het versleutelde wachtwoord in de database inderdaad overeenkomt met het wachtwoord in de database;
- regels 48-59: er wordt gecontroleerd of de gebruiker [admin] het wachtwoord [admin] en de rol [ROLE_ADMIN] heeft, met behulp van de service [appUserDetailsService];
De tests worden met succes uitgevoerd met de volgende logbestanden:
8.4.13.7. Tussentijdse conclusie
De toevoeging van de benodigde klassen aan Spring Security kon worden gerealiseerd met slechts enkele aanpassingen aan het oorspronkelijke project. Laten we deze nog eens op een rijtje zetten:
- toevoeging van een afhankelijkheid van Spring Security in het bestand [pom.xml];
- het aanmaken van drie extra tabellen in de database;
- het aanmaken van entiteiten JPA en Spring-componenten in het pakket [rdvmedecins.security];
Dit zeer gunstige scenario vloeit voort uit het feit dat de drie aan de database toegevoegde tabellen onafhankelijk zijn van de bestaande tabellen. We hadden ze zelfs in een aparte database kunnen plaatsen. Dit was mogelijk omdat we hadden besloten dat een gebruiker een bestaan had dat onafhankelijk was van artsen en klanten. Als deze laatste potentiële gebruikers waren geweest, zouden er koppelingen moeten worden gemaakt tussen de tabel [USERS] en de tabellen [MEDECINS] en [CLIENTS]. Dit zou dan een aanzienlijke impact hebben gehad op het bestaande project.
8.4.13.8. Het project STS van de laag [web]
![]() |
Het project [rdvmedecins-webjson] ontwikkelt zich als volgt vanuit [1]:
![]() |
De belangrijkste wijzigingen moeten worden aangebracht in het pakket [rdvmedecins.web.config], waar Spring Security moet worden geconfigureerd. Er zijn nog andere, minder ingrijpende wijzigingen in de klassen [AppConfig] en [ApplicationModel]. We zijn al een configuratieklasse van Spring Security tegengekomen:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
We volgen dezelfde aanpak:
- regel 11: een klasse definiëren die de klasse [WebSecurityConfigurerAdapter] uitbreidt;
- regel 13: definieer een methode [configure(HttpSecurity http)] die de toegangsrechten tot de verschillende URL van de webservice vastlegt;
- regel 19: een methode [configure(AuthenticationManagerBuilder auth)] definiëren die de gebruikers en hun rollen vastlegt;
De configuratie van Spring Security wordt verzorgd door de klasse [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// de authenticatie wordt uitgevoerd door de bean [appUserDetailsService]
// het wachtwoord wordt versleuteld met het hash-algoritme BCrypt
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// beveiligde applicatie?
if (application.isSecured()) {
// het wachtwoord wordt verzonden via de header Authorization: Basic xxxx
http.httpBasic();
// de methode HTTP OPTIONS moet voor iedereen worden toegestaan
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// alleen de rol ADMIN mag de applicatie gebruiken
http.authorizeRequests() //
.antMatchers("/", "/**") // alle URL
.hasRole("ADMIN");
// geen sessie
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- regel 15: de klasse [SecurityConfig] is een Spring-configuratieklasse;
- regel 16: om de beveiliging van het project in te stellen;
- regels 19-20: de klasse [AppUserDetails], die gebruikers toegang geeft tot de applicatie, wordt geïnjecteerd;
- regels 21-22: de klasse [ApplicationModel], die als cache voor de webapplicatie dient, wordt geïnjecteerd. We besluiten deze hier ook te gebruiken om de webapplicatie op één plek te configureren. Deze klasse definieert de booleaanse waarde [isSecured] in regel 36. Deze booleaanse waarde beveiligt (true) of juist niet (false) de webapplicatie;
- regels 25-29: de methode [configure(HttpSecurity http)] definieert de gebruikers en hun rollen. Deze methode ontvangt als parameter een type [AuthenticationManagerBuilder]. Deze parameter wordt aangevuld met twee gegevens (regel 28):
- een verwijzing naar de service [appUserDetailsService] uit regel 20, die toegang geeft tot de geregistreerde gebruikers. Hierbij moet worden opgemerkt dat het feit dat ze in een database zijn opgeslagen, niet wordt vermeld. Ze zouden dus in een cache kunnen staan, geleverd door een webservice, ...
- het type versleuteling dat voor het wachtwoord wordt gebruikt. Ter herinnering: we hebben hier het algoritme BCrypt gebruikt;
- regels 38-47: de methode [configure(HttpSecurity http)] definieert de toegangsrechten voor de URL van de webservice;
- regel 34: we hebben in het inleidende project gezien dat Spring Security standaard een CSRF-token (Cross Site Request Forgery) beheert dat de gebruiker die zich wilde authenticeren, naar de server moest terugsturen. Hier is dit mechanisme uitgeschakeld. In combinatie met de booleaanse waarde (isSecured=false) maakt dit het mogelijk om de webapplicatie zonder beveiliging te gebruiken;
- regel 38: we schakelen de authenticatiemodus via de header HTTP in. De client moet de volgende header HTTP verzenden:
waarbij code de Base64-codering is van de tekenreeks **login:password**. De Base64-codering van de tekenreeks *admin:admin is bijvoorbeeld YWRtaW46YWRtaW4=*. De gebruiker met de login [admin] en het wachtwoord [admin] zal dus de volgende header HTTP verzenden om zich te authenticeren:
- regels 40-42: geven aan dat alle URL van de webservice toegankelijk zijn voor gebruikers met de rol [ROLE_ADMIN]. Dit betekent dat een gebruiker die deze rol niet heeft, geen toegang heeft tot de webservice;
- regel 47: het wachtwoord van de gebruiker kan al dan niet in een sessie worden opgeslagen. Als het wordt opgeslagen, hoeft de gebruiker zich slechts de eerste keer te authenticeren. De volgende keren worden zijn inloggegevens niet meer gevraagd. Hier is gekozen voor een modus zonder sessie. Elk verzoek moet vergezeld gaan van de beveiligingsgegevens;
De klasse [AppConfig], die de gehele applicatie configureert, verandert als volgt:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- De wijziging vindt plaats op regel 11: de configuratieklasse [SecurityConfig] wordt toegevoegd;
Ten slotte wordt de klasse [ApplicationModel] uitgebreid met een booleaanse waarde:
@Component
public class ApplicationModel implements IMetier {
...
// configuratiegegevens
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- regel 6: de booleaanse waarde [secured] wordt ingesteld op [true / false], afhankelijk van of de beveiliging al dan niet moet worden geactiveerd.
8.4.13.9. Testen van de webservice
We gaan de webservice testen met de Chrome-client [Advanced Rest Client]. We moeten de authenticatie-header HTTP specificeren:
waarbij [code] de Base64-code is van de tekenreeks [login:password]. Om deze code te genereren, kun je het volgende programma gebruiken:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// er worden twee argumenten verwacht: login en wachtwoord
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// de twee argumenten worden opgehaald
String chaîne = String.format("%s:%s", args[0], args[1]);
// de tekenreeks wordt gecodeerd
byte[] data = Base64.encode(chaîne.getBytes());
// de Base64-codering ervan wordt weergegeven
System.out.println(new String(data));
}
}
Als we dit programma uitvoeren met de twee argumenten [admin admin]:
![]() |
krijgen we het volgende resultaat:
Nu we weten hoe we de authenticatie-header HTTP kunnen genereren, starten we de nu beveiligde webservice:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Vervolgens vragen we met de Chrome-client [Advanced Rest Client] de lijst met alle artsen op:
![]() |
- in [1] vragen we de URL van de artsen op;
- in [2], met een methode GET;
- in [3] geven we de header HTTP van de authenticatie; de code [YWRtaW46YWRtaW4=] is de Base64-codering van de tekenreeks [admin:admin];
- in [4] verzenden we het commando HTTP;
Het antwoord van de server is als volgt:
![]() |
- in [1], de authenticatie-header HTTP;
- in [2] stuurt de server een antwoord terug: jSON;
- in [3], een lijst met headers HTTP die betrekking hebben op de beveiliging van de webapplicatie;
We krijgen inderdaad de lijst met artsen:
![]() |
Laten we nu een verzoek HTTP proberen met een onjuiste authenticatieheader. Het antwoord is dan als volgt:
![]() |
- in [1] en [3]: de authenticatieheader HTTP;
- in [2]: het antwoord van de webservice;
Laten we nu de gebruiker user / user proberen. Deze bestaat wel, maar heeft geen toegang tot de webservice. Als we het Base64-encoderingsprogramma uitvoeren met de twee argumenten [user user]:
![]() |
krijgen we het volgende resultaat:
![]() |
- in [1] en [3]: de authenticatie-header HTTP;
- in [2]: het antwoord van de webservice. Dit verschilt van het vorige, dat [401 Unauthorized] was. Deze keer heeft de gebruiker zich correct geauthenticeerd, maar beschikt hij niet over voldoende rechten om toegang te krijgen tot URL;
Een beveiligde webservice is nu operationeel. We gaan deze uitbreiden zodat hij verzoeken tussen domeinen toestaat. Deze behoefte kwam naar voren in het document [Tutoriel AngularJS / Spring 4] en hoewel deze behoefte hier niet bestaat, gaan we er toch aan voldoen.
8.4.14. Implementatie van verzoeken tussen domeinen
Laten we eens kijken naar het probleem van domeinoverschrijdende verzoeken. In het document [Tutoriel AngularJS / Spring 4] wordt een client/server-toepassing ontwikkeld waarbij de client een toepassing is met de naam AngularJS:
![]() |
- de pagina’s HTML / CSS / JS van de Angular-toepassing zijn afkomstig van de server [1];
- in [2] doet de service [dao] een verzoek aan een andere server, namelijk de server [2]. Welnu, dit wordt verboden door de browser die de Angular-applicatie uitvoert, omdat het een beveiligingslek is. De applicatie mag alleen de server opvragen waarvan ze afkomstig is, d.w.z. de server [1];
Eigenlijk is het niet helemaal juist om te zeggen dat de browser de Angular-applicatie verbiedt om de server [2] te benaderen. De applicatie benadert deze server juist om te vragen of hij een client toestaat die niet van zijn eigen domein afkomstig is om hem te benaderen. Deze techniek voor het delen van gegevens wordt CORS (Cross-Origin Resource Sharing) genoemd. De server [2] geeft toestemming door specifieke HTTP-headers te verzenden.
Om de problemen te illustreren die zich kunnen voordoen, gaan we een client-server-applicatie maken waarbij:
- de server onze webserver / jSON is;
- de client een eenvoudige pagina HTML is, voorzien van JavaScript-code die verzoeken naar de webserver / jSON verstuurt;
8.4.14.1. Het clientproject
![]() |
Het project is een Maven-project met het volgende bestand: [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- opzoeken van bovenliggend element in de repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- regels 14-19: dit is een Spring Boot-project;
- regels 29-32: er wordt gebruikgemaakt van de afhankelijkheid [spring-boot-starter-web], die een Tomcat-server en Spring MVC meebrengt;
De pagina HTML ziet er als volgt uit:
![]() |
Deze wordt gegenereerd door de volgende code:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- methode HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- geplaatste waarde -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- bevestigingsknop -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- regel 6: we importeren de bibliotheek jQuery;
- regel 7: er wordt code geïmporteerd die we zelf gaan schrijven;
De code [client.js] is als volgt:
// algemene gegevens
var url;
var posted;
var response;
var method;
function requestServer() {
// de gegevens van het formulier worden opgehaald
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// er wordt handmatig een Ajax-verzoek gedaan
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// tekstresultaat
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// systeemfout
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// we voeren handmatig een Ajax-aanroep uit
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// tekstresultaat
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// systeemfout
response.text(jqXHR.responseText);
}
})
}
// bij het laden van het document
$(document).ready(function() {
// de referenties van de componenten van de pagina worden opgehaald
url = $("#url");
posted = $("#posted");
response = $("#response");
});
We laten het aan de lezer over om deze code te begrijpen. Alles is al eens eerder aan bod gekomen. Sommige regels verdienen echter een toelichting:
- regel 11:
- [document] verwijst naar het document dat door de browser is geladen, ook wel het DOM (Document Object Model) genoemd,
- [document.forms[0]] verwijst naar het eerste formulier van het document; een document kan er meerdere bevatten. Hier is er slechts één,
- [document.forms[0].elements['method']] verwijst naar het element van het formulier dat het attribuut [name='method'] heeft. Er zijn er twee:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- regel 11:
- [document.forms[0].elements['method'].value] is de waarde die wordt verzonden voor de component met het attribuut [name='method']. We weten dat de verzonden waarde de waarde is van het attribuut [value] van de aangevinkte keuzeknop. In dit geval zal dat dus een van de tekenreeksen ['get', 'post'] zijn;
- regels 23-25: we nemen contact op met een server die een header HTTP [Authorization: Basic code] vereist. We maken deze header aan voor de gebruiker [admin / admin], die als enige de server mag benaderen;
- regel 26: de gebruiker voert URL in van het type [/getAllMedecins, /supprimerRv, ...]. Deze URL moeten dus worden aangevuld;
- regel 28: de server stuurt jSON terug, wat een tekstvorm is. We geven het type [text/plain] op als resultaattype om het weer te geven zoals het is ontvangen;
- regel 33: weergave van het tekstantwoord van de server;
- regel 39: weergave van een eventuele foutmelding in tekstformaat;
- regel 52: om aan te geven dat de client jSON verstuurt;
In de gebouwde client/server-applicatie:
- is de client een webapplicatie die beschikbaar is op URL [http://localhost:8081]. Dit is de applicatie die we momenteel aan het bouwen zijn;
- de server is een webapplicatie die bereikbaar is via URL [http://localhost:8080]. Dit is onze webserver / jSON;
Omdat de client niet via dezelfde poort als de server wordt bereikt, doet zich het probleem van cross-domain-verzoeken voor. [http://localhost:8080] en [http://localhost:8081] zijn twee verschillende domeinen.
De Spring Boot-applicatie is een console-applicatie die wordt gestart door de volgende uitvoerbare klasse [Client]:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
// statische pagina's
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// configuratie dispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// ingebouwde Tomcat-server
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- regel 14: de klasse [Client] is een Spring-configuratieklasse;
- regel 15: er wordt een Spring-applicatie MVC geconfigureerd. Deze annotatie zorgt voor een aantal automatische configuraties;
- regel 16: om bepaalde standaardwaarden van het Spring-framework MVC te herdefiniëren, moet de klasse [WebMvcConfigurerAdapter] worden uitgebreid;
- regels 23-26: met de methode [addResourceHandlers] kunnen de mappen worden opgegeven waarin de statische bronnen (html, css, js, ...) van de applicatie zich bevinden. Hier wordt de map [static] aangegeven, die zich in het classpath van het project bevindt:
![]() |
- regels 29-37: configuratie van de bean [dispatcherServlet] die verwijst naar de Spring-servlet MVC;
- regels 40-43: de ingebouwde Tomcat-server draait op poort 8081;
8.4.14.2. De URL [/getAllMedecins]
We starten:
- de web-/json-server op poort 8080;
- de client van deze server op poort 8081;
vervolgens vragen we de URL [http://localhost:8081/client.html] [1] op:
![]() |
- in [2], voeren we een GET uit op de URL [http://localhost:8080/getAllMedecins];
We krijgen geen antwoord van de server. Als we naar de ontwikkelconsole (Ctrl-Shift-I) kijken, zien we een foutmelding:
![]() |
- in [1], we bevinden ons in het tabblad [Network];
- In [2] zien we dat het verzoek HTTP dat is gedaan niet [GET] is, maar [OPTIONS]. Bij een verzoek tussen domeinen controleert de browser bij de server of aan een aantal voorwaarden is voldaan door een verzoek HTTP [OPTIONS] te verzenden. In dit geval zijn de verzoeken die worden aangeduid door de stippen [5-6];
- in [5] vraagt de browser of het doel URL bereikbaar is via een GET. De header van het verzoek [Access-Control-Request-Method] vraagt om een antwoord met een header HTTP [Access-Control-Allow-Methods] waarin wordt aangegeven dat de gevraagde methode wordt geaccepteerd;
- in [5] verstuurt de browser de header HTTP [Origin: http://localhost:8081]. Deze header vraagt om een antwoord in de vorm van een header HTTP [Access-Control-Allow-Origin] waarin wordt aangegeven dat de opgegeven bron wordt geaccepteerd;
- in [6] vraagt de browser of de headers HTTP, [accept] en [authorization] worden geaccepteerd. De header van het verzoek [Access-Control-Request-Headers] verwacht een antwoord met een header HTTP [Access-Control-Allow-Headers] waarin wordt aangegeven dat de gevraagde headers worden geaccepteerd;
- er treedt een fout op in [3]. Als je op het pictogram klikt, krijg je de foutmelding [4];
- in [4] geeft het bericht aan dat de server de header HTTP [Access-Control-Allow-Origin] niet heeft verzonden, die aangeeft of de herkomst van het verzoek wordt geaccepteerd;
- in [7] is te zien dat de server deze header inderdaad niet heeft verzonden. Daardoor heeft de browser geweigerd het aanvankelijk gevraagde verzoek HTTP GET uit te voeren;
We moeten de webserver / jSON aanpassen. We brengen een eerste wijziging aan in [ApplicationModel], een van de configuratie-elementen van de webservice:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// configuratiegegevens
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- regel 6: we maken een booleaanse variabele aan die aangeeft of we klanten van buiten het domein van de server al dan niet accepteren;
- regels 10-12: de methode om toegang te krijgen tot deze informatie;
Vervolgens maken we een nieuwe Spring-controller MVC:
![]() |
De klasse [RdvMedecinsCorsController] ziet er als volgt uit:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// opties naar de client verzenden
public void sendOptions(String origin, HttpServletResponse response) {
// CORS toegestaan?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// de header wordt ingesteld CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// bepaalde headers toestaan
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// GET wordt toegestaan
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// lijst met artsen
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- regels 12-13: de klasse [RdvMedecinsCorsController] is een Spring-controller;
- regels 33-36: definiëren een actie die de URL [/getAllMedecins] verwerkt wanneer deze wordt aangevraagd met het commando HTTP [OPTIONS];
- regel 34: de methode [getAllMedecins] accepteert als parameters:
- het object [@RequestHeader(value = "Origin", required = false)], dat de header HTTP [Origin] van de aanvraag ophaalt. Deze header is verzonden door de afzender van de aanvraag:
Er wordt aangegeven dat de header HTTP [Origin] optioneel is [required = false]. In dit geval, als de header ontbreekt, krijgt de parameter [String origin] de waarde null. Aangezien [required = true] de standaardwaarde is, wordt er een uitzondering gegenereerd als de header ontbreekt. We wilden dit scenario vermijden;
- regel 34:
- het object [HttpServletResponse response] dat naar de klant wordt verzonden die het verzoek heeft ingediend;
Deze twee parameters worden door Spring ingevuld;
- regel 35: de verwerking van het verzoek wordt gedelegeerd aan de methode op de regels 19-30;
- regels 15-16: het object [ApplicationModel] wordt geïnjecteerd;
- regels 21-23: als de applicatie is geconfigureerd om verzoeken van andere domeinen te accepteren en als de afzender de header HTTP [Origin] heeft verzonden en als deze herkomst begint met [http://localhost], dan wordt het verzoek van een ander domein geaccepteerd, anders wordt het afgewezen;
- regel 25: als de client zich in het domein [http://localhost:port] bevindt, sturen we de header HTTP:
wat betekent dat de server de herkomst van de client accepteert;
- regel 25: we hebben twee specifieke HTTP-headers gemeld in het verzoek HTTP [OPTIONS]:
Op de header HTTP [Access-Control-Request-X] reageert de server met een header HTTP [Access-Control-Allow-X] waarin wordt aangegeven wat is toegestaan. De regels 23-26 herhalen alleen het verzoek van de klant om aan te geven dat het is geaccepteerd;
We zijn nu klaar voor nieuwe tests. We lanceren de nieuwe versie van de webservice en ontdekken dat het probleem nog steeds bestaat. Er is niets veranderd. Als we in regel 35 hierboven een console-uitvoer plaatsen, wordt deze nooit weergegeven, wat aantoont dat de methode [getAllMedecins] uit regel 34 nooit wordt aangeroepen.
Na wat onderzoek ontdekken we dat Spring MVC de opdrachten HTTP en [OPTIONS] zelf verwerkt met een standaardverwerking. Het is dus altijd Spring die reageert en nooit de methode [getAllMedecins] op regel 34. Dit standaardgedrag van Spring MVC kan worden gewijzigd. We passen de bestaande klasse [WebConfig] aan:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
// configuratie van de dispatcherservlet voor de headers CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// toewijzing jSON
...
- regels 10-11: de bean [dispatcherServlet] wordt gebruikt om de servlet te definiëren die de verzoeken van de clients afhandelt. Deze is hier van het type [DispatcherServlet], de servlet van het Spring-framework MVC;
- regel 12: er wordt een instantie van het type [DispatcherServlet] aangemaakt;
- regel 13: er wordt aangegeven dat de servlet de commando's HTTP en [OPTIONS] naar de applicatie moet doorsturen;
- regel 14: de servlet wordt op deze manier geconfigureerd;
We voeren de tests opnieuw uit met deze nieuwe configuratie. We krijgen het volgende resultaat:
![]() |
- in [1] zien we dat er twee verzoeken HTTP zijn naar URL en [http://localhost:8080/getAllMedecins];
- in [2], de verzoek [OPTIONS];
- in [3], de drie headers HTTP die we zojuist in het antwoord van de server hebben geconfigureerd;
Laten we nu het tweede verzoek bekijken:
![]() |
- in [1], het onderzochte verzoek;
- in [2], dit is het verzoek GET. Dankzij het eerste verzoek [OPTIONS] heeft de browser de gevraagde informatie ontvangen. Hij voert nu het aanvankelijk gevraagde verzoek [GET] uit;
- in [3], het antwoord van de server;
- in [4] stuurt de server jSON;
- in [5] is er een fout opgetreden;
- in [6], de foutmelding;
Het is moeilijker uit te leggen wat hier is gebeurd. Het antwoord [3] van de server is normaal [HTTP/1.1 200 OK]. We zouden dus het gevraagde document moeten hebben. Het is mogelijk dat de server het document wel heeft verzonden, maar dat de browser het gebruik ervan verhindert omdat hij wil dat het antwoord voor het verzoek GET ook de header HTTP [Access-Control-Allow-Origin:http://localhost:8081] bevat.
We passen de controller [RdvMedecinsController] als volgt aan:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// lijst met artsen
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// het antwoord
Response<List<Medecin>> response;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// status van de applicatie
...
- regels 1-2: de controller [RdvMedecinsCorsController] wordt ingevoegd;
- regels 7-8: in de parameters van de methode [getAllMedecins] wordt het object HttpServletResponse geïnjecteerd, dat het antwoord aan de client en de header HTTP [Origin] omvat;
- regel 12: de methode [sendOptions] van de controller [RdvMedecinsCorsController] wordt aangeroepen, dezelfde methode die werd aangeroepen om het verzoek HTTP [OPTIONS] te verwerken. Deze zal dus dezelfde headers HTTP verzenden als voor dat verzoek;
Na deze wijziging zijn de resultaten als volgt:
![]() |
We hebben inderdaad de lijst met artsen ontvangen.
8.4.14.3. De andere URL [GET]
We tonen nu de andere URL-verzoeken die via een GET zijn opgevraagd. In de controllers volgt de code van de acties die deze verwerken het patroon van de acties die eerder de URL en [/getAllMedecins] hebben verwerkt. De lezer kan de code controleren in de voorbeelden die bij dit document zijn geleverd. Hier is een voorbeeld:
in [RdvMedecinsCorsController]
// lijst met afspraken van een arts
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
in [RdvMedecinsController]
// lijst met afspraken van een arts
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// het antwoord
Response<List<Rv>> response = null;
boolean erreur = false;
// kopteksten CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// status van de applicatie
...
Hieronder volgen enkele schermafbeeldingen van de uitvoering:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. De URL [POST]
Laten we het volgende geval eens bekijken:
![]() |
- we maken een POST [1] naar de URL [2];
- in [3], de geboekte waarde. Dit is een reeks jSON;
- in totaal willen we de afspraak met de waarde [id] 100 verwijderen;
We wijzigen voorlopig geen code. Het verkregen resultaat is dan als volgt:
![]() |
- in [1]; net als bij de verzoeken [GET] wordt er door de browser een verzoek [OPTIONS] gedaan;
- bij [2] vraagt de browser om toestemming voor een verzoek naar [POST]. Voorheen was dit [GET];
- in [3] vraagt hij toestemming om de headers HTTP en [accept, authorization, content-type] te verzenden. Voorheen waren er alleen de eerste twee headers;
We passen de methode [RdvMedecinsCorsController.sendOptions] als volgt aan:
public void sendOptions(String origin, HttpServletResponse response) {
// Cors toegestaan?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// de header wordt ingesteld CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// bepaalde headers worden toegestaan
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// GET wordt toegestaan
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- regel 9: we hebben de headers HTTP en [Content-Type] toegevoegd (hoofdletters en kleine letters maken niet uit);
- regel 11: we hebben de methode HTTP [POST] toegevoegd;
Hierdoor worden de methoden [POST] op dezelfde manier verwerkt als de verzoeken [GET]. Hier volgt een voorbeeld van URL [/supprimerRv]:
in [RdvMedecinsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// het antwoord
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// status van de applicatie
if (messages != null) {
...
in [RdvMedecinsCorsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
Het verkregen resultaat is als volgt:
![]() |
Voor URL en [/ajouterRv] krijgen we het volgende resultaat:
![]() |
8.4.14.5. Conclusion
Onze applicatie ondersteunt nu verzoeken tussen domeinen. Deze kunnen al dan niet worden toegestaan via de configuratie in de klasse [ApplicationModel]:
// configuratiegegevens
private boolean corsAllowed = false;
8.5. Geprogrammeerde client van de webservice / jSON
Laten we terugkeren naar de algemene architectuur van de applicatie die we willen schrijven:
![]() |
Het bovenste deel van het schema is al geschreven. Dat is de webserver / jSON. We gaan nu aan de slag met het onderste deel, en om te beginnen met de laag [DAO]. We gaan deze schrijven en vervolgens testen met een console-client. De testarchitectuur ziet er als volgt uit:
![]() |
8.5.1. Het project van de console-client
Het project STS van de console-client ziet er als volgt uit:
![]() |
8.5.2. Maven-configuratie
Het bestand [pom.xml] van de console-client is als volgt:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- bovenliggende entiteit opzoeken in de repository -->
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- bibliotheek jSON gebruikt door Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- door Spring gebruikte component RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>
- regels 15-20: het bovenliggende Spring Boot-project;
- regels 24-27: de console-client van de webserver / jSON is gebaseerd op een component genaamd [RestTemplate], geleverd door de afhankelijkheid [spring-web];
- regels 29-36: voor het serialiseren en deserialiseren van jSON-objecten is de bibliotheek jSON nodig. We gebruiken een variant van de Jackson-bibliotheek die door Spring Web wordt gebruikt;
- regels 38-41: op het laagste niveau communiceert de component [RestTemplate] met de server via de sockets TCP/IP. We willen de parameter [timeout] hiervan instellen, d.w.z. de maximale wachttijd voor een antwoord van de server. Met de component [RestTemplate] kunnen we deze parameter niet instellen. Om dit te doen, gaan we de constructor [RestTemplate] een laag-niveau-component doorgeven die wordt geleverd door de afhankelijkheid [org.apache.httpcomponents.httpclient]. Het is deze afhankelijkheid die ons in staat stelt om de [timeout] van de communicatie in te stellen;
8.5.3. Het pakket [rdvmedecins.client.entities]
![]() |
Het pakket [rdvmedecins.client.entities] bevat alle entiteiten die de webservice / jSON via zijn verschillende URL verstuurt. We zullen deze niet opnieuw in detail bespreken. We volstaan met te vermelden dat de entiteiten JPA en [Client, Creneau, Medecin, Rv, Personne] zijn ontdaan van al hun annotaties JPA en jSON. Hier is bijvoorbeeld de klasse [Rv]:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// dag van de afspraak
private Date jour;
// een afspraak is gekoppeld aan een klant
private Client client;
// een afspraak is gekoppeld aan een tijdslot
private Creneau creneau;
// externe sleutels
private long idClient;
private long idCreneau;
// standaardfabrikant
public Rv() {
}
// met parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// getters en setters
...
}
8.5.4. Het pakket [rdvmedecins.client.requests]
![]() |
Het pakket [rdvmedecins.client.requests] bevat de twee klassen waarvan de waarde jSON is toegewezen aan URL, [/ajouterRv] en [supprimerRv]. Ze zijn identiek aan hun tegenhangers aan de serverzijde.
8.5.5. Het pakket [rdvmedecins.client.responses]
![]() |
[Response] is het type van alle antwoorden van de webservice / jSON. Dit is een generiek type:
package rdvmedecins.client.responses;
import java.util.List;
public class Response<T> {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// eventuele foutmeldingen
private List<String> messages;
// de inhoud van het antwoord
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters en setters
...
}
- regel 5: het type [T] varieert afhankelijk van de URL van de webservice / jSON;
8.5.6. Het pakket [rdvmedecins.client.dao]
![]() |
- [IDao] is de interface van de laag [DAO] en [Dao] is de implementatie ervan. We komen later terug op deze implementatie;
8.5.7. Het pakket [rdvmedecins.client.config]
![]() |
De klasse [DaoConfig] configureert de applicatie. De code ervan is als volgt:
package rdvmedecins.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// Aanmaken van de component RestTemplate
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// resultaat
return restTemplate;
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- regel 13: de klasse [DaoConfig] is een Spring-configuratieklasse;
- regel 14: het pakket [rdvmedecins.client.dao] wordt doorzocht op Spring-componenten. Daarin bevindt zich de component [Dao];
- regels 17-24: definiëren een Spring-singleton met de naam [restTemplate] (de naam van de methode). Deze methode retourneert een instantie van [RestTemplate], het basisgereedschap dat Spring biedt om te communiceren met een webservice / jSON;
- regel 21: we zouden [RestTemplate restTemplate = new RestTemplate() ;] kunnen schrijven. In de meeste gevallen is dat voldoende. Maar hier willen we de [timeout] van de client vastleggen. Hiervoor injecteren we in de component [RestTemplate] een laag-niveau-component van het type [HttpComponentsClientHttpRequestFactory] (regel 20), waarmee we deze [timeout] kunnen instellen. De benodigde Maven-afhankelijkheid is al gepresenteerd;
- regels 28-57: definiëren jSON-mappers. Dit zijn de jSON-mappers die aan de serverzijde worden gebruikt (zie paragraaf 8.4.11.3) om het type T van het antwoord [Response<T>] te serialiseren. Dezelfde converters zullen nu aan de clientzijde worden gebruikt om het type T te deserialiseren;
8.5.8. De interface [IDao]
Laten we terugkeren naar de architectuur van de applicatie:
![]() |
De laag [DAO] is een adapter tussen de laag [console] en de URL die door de webservice / jSON worden blootgesteld. De interface [IDao] ziet er als volgt uit:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// URL van de webservice
public void setUrlServiceWebJson(String url);
// time-out
public void setTimeout(int timeout);
// authenticatie
public void authenticate(User user);
// lijst met klanten
public List<Client> getAllClients(User user);
// lijst met artsen
public List<Medecin> getAllMedecins(User user);
// lijst met tijdvakken van een arts
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// een klant zoeken op basis van zijn ID
public Client getClientById(User user, long id);
// een klant zoeken op basis van zijn ID
public Medecin getMedecinById(User user, long id);
// een afspraak zoeken op basis van de id
public Rv getRvById(User user, long id);
// een tijdvak zoeken op basis van de id
public Creneau getCreneauById(User user, long id);
// een RV toevoegen
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// een RV verwijderen
public void supprimerRv(User user, long idRv);
// lijst met afspraken van een arts op een bepaalde dag
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// agenda
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- regel 14: de methode waarmee de root van de webservice /jSON kan worden ingesteld, bijvoorbeeld [http://localhost:8080];
- regel 17: de methode waarmee de [timeout] aan de clientzijde kan worden ingesteld. We willen deze parameter controleren omdat sommige HTTP-clients soms erg lang wachten op een antwoord dat niet zal komen;
- regel 20: de methode waarmee een gebruiker kan worden geïdentificeerd. Genereert een uitzondering als de gebruiker niet wordt herkend;
- regels 22-53: aan elke URL die door de webservice wordt blootgesteld / jSON is een methode van de interface gekoppeld waarvan de handtekening is afgeleid van de handtekening van de server-side methode die de blootgestelde URL verwerkt. Laten we bijvoorbeeld de volgende server-URL nemen:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
- regel 1: we zien dat [idMedecin] en [jour] de parameters zijn van de URL. Dit zullen de invoerparameters zijn van de methode die is gekoppeld aan deze client-side URL;
- regel 2: we zien dat de servermethode een type [Response<String>] retourneert. Dit type [String] is het type van de waarde jSON van het type [AgendaMedecinJour]. Het type van het resultaat van de methode die aan deze URL aan de clientzijde is gekoppeld, zal [AgendaMedecinJour] zijn;
Aan de clientzijde wordt de volgende methode gedeclareerd:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Deze handtekening is geschikt wanneer de server een antwoord [int status, List<String> messages, String body] verstuurt met [status==0]. In dit geval hebben we [messages==null && body!=null]. Ze is niet geschikt wanneer [status!=0]. In dat geval hebben we [messages!=null && body==null]. We moeten op de een of andere manier aangeven dat er een fout is opgetreden. Hiervoor genereren we een uitzondering van het type [RdvMedecinsException] als volgt:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// foutcode
private int status;
// lijst met foutmeldingen
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// getters en setters
...
}
- regels 9 en 11: de uitzondering neemt de waarden over van de velden [status, messages] van het object [Response<T>] dat door de server is verzonden;
- regel 5: de klasse [RdvMedecinsException] is een uitbreiding van de klasse [RuntimeException]. Het betreft dus een ongecontroleerde uitzondering, d.w.z. dat het niet verplicht is deze af te vangen met een try/catch-constructie en deze te vermelden in de signatuur van de methoden van de interface;
Bovendien hebben alle methoden van de interface [IDao] die de webservice / jSON opvragen, het volgende type [User] als parameter:
package rdvmedecins.client.entities;
public class User {
// gegevens
private String login;
private String passwd;
// constructors
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// getters en setters
...
}
Elke uitwisseling met de webservice / jSON moet namelijk vergezeld gaan van een authenticatieheader HTTP.
8.5.9. Het pakket [rdvmedecins.clients.console]
Nu we de interface van de laag [DAO] kennen, kunnen we de console-applicatie presenteren.
![]() |
De klasse [Main] ziet er als volgt uit:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// time-out van verbindingen in milliseconden
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we halen een referentie op uit de laag [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// de URL van de webservice / json wordt ingesteld
dao.setUrlServiceWebJson("http://localhost:8080");
// de time-outs worden ingesteld in milliseconden
dao.setTimeout(TIMEOUT);
// Authenticatie
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lijst met klanten
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lijst met artsen
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lijst met beschikbare tijdvakken van arts 2
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// klant nr. 1
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// arts nr. 2
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// afspraak nr. 3
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// afspraak nr. 4
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// een afspraak toevoegen
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lijst met afspraken van arts 1 op 8 januari 2015
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// agenda van arts 1 op 8 januari 2015
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// verwijdering van de toegevoegde afspraak
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// lijst met afspraken van arts 1 op 8 januari 2015
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// context sluiten
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- regel 19: de serializer jSON waarmee we het antwoord van de server kunnen weergeven, regel 184;
- regel 25: de component [AnnotationConfigApplicationContext] is een Spring-component die de configuratie-annotaties van een Spring-applicatie kan verwerken. We geven de klasse [AppConfig], die de applicatie configureert, door aan de constructor;
- regel 26: we halen een verwijzing op naar de laag [DAO];
- regels 27-30: deze wordt geconfigureerd;
- regels 32-169: we testen alle methoden van de interface [IDao];
De verkregen resultaten zijn als volgt:
09:20:56.935 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
We laten het aan de lezer over om de resultaten aan de code te koppelen. Deze code laat zien hoe elke methode van de laag [DAO] kan worden aangeroepen. Laten we slechts enkele punten opmerken:
- regels 2-14: hieruit blijkt dat de server bij een authenticatiefout een status HTTP, [403 Forbidden] of [401 Unauthorized] terugstuurt, afhankelijk van het geval;
- regels 30-31: er wordt een afspraak toegevoegd aan arts nr. 1;
- regels 32-33: deze afspraak is zichtbaar. Het is de enige afspraak van die dag;
- regels 34-35: deze afspraak is ook zichtbaar in de agenda van de arts;
- regels 36-37: de afspraak is verdwenen. De code heeft deze inmiddels verwijderd;
De logbestanden van de console worden beheerd door de volgende bestanden:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- aan encoders wordt standaard het type ch.qos.logback.classic.encoder.PatternLayoutEncoder toegewezen -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- logniveau-controle -->
<root level="info"> <!-- uit, info, debug, waarschuwing -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. Implementatie van de laag [DAO]
Nu rest ons nog de kern van de laag [DAO] te presenteren, namelijk de implementatie van de bijbehorende interface [IDao]. We zullen dit stap voor stap doen.
![]() |
De interface [IDao] wordt geïmplementeerd door de abstracte klasse [AbstractDao] en de daarvan afgeleide klasse [Dao].
De bovenliggende klasse [AbstractDao] is als volgt:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// gegevens
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL webservice / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// de time-out voor verzoeken van de webclient instellen
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// de gebruiker en het wachtwoord worden in Base64 gecodeerd - vereist
// Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// generieke verzoek
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- regel 20: de klasse is abstract, waardoor we deze niet als een Spring-component kunnen aanmerken. Het is de onderliggende klasse die als zodanig zal worden aangemerkt;
- regels 23-24: we injecteren de bean [restTemplate] die we hebben gedefinieerd in de configuratieklasse [AppConfig];
- regel 25: de URL is de root van de webservice / jSON;
- regels 32-38: stellen de time-out in voor de client wanneer deze op een antwoord van de server wacht;
- regel 34: we halen de component [HttpComponentsClientHttpRequestFactory] op die we bij het aanmaken van de bean [restTemplate] in deze bean hadden geïnjecteerd (zie [AppConfig]);
- regel 36: we stellen de maximale wachttijd van de client in wanneer deze een verbinding met de server tot stand brengt;
- regel 37: we stellen de maximale wachttijd in voor de client wanneer deze wacht op een antwoord op een van zijn verzoeken;
De implementatie van de communicatiemethoden met de server wordt ondergebracht in de volgende generieke methode:
// algemeen verzoek
protected String getResponse(User user, String url, String jsonPost) {
...
}
- regel 2: de parameters van [getResponse] zijn als volgt:
- [User user]: de gebruiker die verbinding maakt;
- [String url]: de URL die moet worden opgevraagd. Dit is het einde van de URL, waarbij het eerste deel wordt geleverd door het veld [urlServiceWebJson] van de klasse,
- [String jsonPost]: de tekenreeks jSON die moet worden verzonden. Als deze waarde aanwezig is, wordt de URL opgevraagd met een POST, anders met een GET;
Laten we verdergaan:
// algemeen verzoek
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL (contact opnemen)
// jsonPost: de waarde jSON moet worden verzonden
try {
// verzoek uitvoeren
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// de aanvraag wordt uitgevoerd
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- regels 23-24: de instructie die het verzoek naar de server stuurt en het antwoord ontvangt. De component [RestTemplate] biedt een groot aantal methoden voor communicatie met de server. We hadden ook een andere methode dan [exchange] kunnen kiezen. De tweede parameter van de aanroep bepaalt het type van het verwachte antwoord, in dit geval een tekenreeks van het type jSON. De eerste parameter is het verzoek van het type [RequestEntity] (regel 7). Het resultaat van de methode [exchange] is van het type [ResponseEntity<String>]. Het type [ResponseEntity] omvat het volledige antwoord van de server, inclusief de headers HTTP en het door de server verzonden document. Evenzo omvat het type [RequestEntity] het volledige verzoek van de client, inclusief de headers HTTP en de eventuele verzonden waarde;
- regel 23: dit is de body van het object [ResponseEntity<String>] die wordt teruggegeven aan de aanroepende methode, d.w.z. de string jSON die door de server is verzonden;
- regels 9-21: we moeten het verzoek van het type [RequestEntity] samenstellen. Dit verschilt naargelang we een GET of een POST gebruiken om het verzoek te doen;
- regel 9: de aanvraag voor een GET. De klasse [RequestEntity] biedt statische methoden om de verzoeken GET, POST, HEAD, ... aan te maken Met de methode [RequestEntity.get] kan een verzoek GET worden aangemaakt door de verschillende methoden die dit verzoek opbouwen aan elkaar te koppelen:
- de methode [RequestEntity.get] accepteert als parameter de doel-URL in de vorm van een URI-instantie,
- met de methode [accept] kunnen de elementen van de header HTTP [Accept] worden gedefinieerd. Hier geven we aan dat we het type [application/json] accepteren dat de server zal verzenden;
- het resultaat van deze reeks methoden is een type [HeadersBuilder];
- regels 10-12: als de parameter [User user] niet null is, nemen we de header HTTP [Authorization] op in het verzoek;
- regel 13: de methode [HeadersBuilder.build] gebruikt deze verschillende gegevens om het type [RequestEntity] van de aanvraag samen te stellen;
- regel 15: de aanvraag voor een POST. Met de methode [RequestEntity.post] kan een aanvraag van het type POST worden aangemaakt door de verschillende methoden die deze aanvraag samenstellen achter elkaar te plaatsen:
- de methode [RequestEntity.post] accepteert als parameter de doel-URL in de vorm van een URI-instantie,
- met de methode [header] kunnen de HTTP-headers worden gedefinieerd die men wil gebruiken, in dit geval die van de autorisatie,
- de daaropvolgende methode [header] neemt de header [Content-Type: application/json] op in het verzoek om aan te geven dat de verzonden waarde in de vorm van een tekenreeks jSON zal worden ontvangen;
- met de methode [accept] kunnen we aangeven dat we het type [application/json] accepteren dat de server zal verzenden;
- regels 17-19: als de parameter [User user] niet gelijk is aan null, wordt de header HTTP [Authorization] in het verzoek opgenomen;
- regel 20: de methode [BodyBuilder.body] stelt de geboekte waarde vast. Dit is de tweede parameter van de generieke methode [getResponse] (regel 2);
- regels 25-28: als er een fout optreedt, wordt er een uitzondering van het type [RdvMedecinsException] gegenereerd;
De methode [getMessagesForException] in de regels 26 en 28 is als volgt:
// lijst met foutmeldingen van een uitzondering
protected static List<String> getMessagesForException(Exception exception) {
// de lijst met foutmeldingen van de uitzondering wordt opgehaald
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// het bericht wordt alleen opgehaald als het !=null is en niet leeg
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// volgende oorzaak
cause = cause.getCause();
}
return erreurs;
}
De privémethode [getBase64] levert de Base64-code van de tekenreeks 'login:passwd' voor de authenticatieheader HTTP:
private String getBase64(User user) {
// de gebruiker en het wachtwoord worden in base64 gecodeerd – vereist Java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
De klasse [Dao] breidt de klasse [AbstractDao] als volgt uit:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// mappers jSON
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- regel 22: de klasse [Dao] is een Spring-component. Hier is de annotatie [@Service] gebruikt. We hadden ook de annotatie [@Component] kunnen blijven gebruiken die tot nu toe werd gebruikt;
- regels 26-36: injectie van de vier mappers jSON die zijn gedefinieerd in de configuratieklasse [DaoConfig];
De methoden van de klasse [Dao] volgen allemaal hetzelfde patroon. We zullen een bewerking GET en een bewerking POST in detail bespreken.
Allereerst een aanvraag [GET]:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// het antwoord
Response<AgendaMedecinJour> response;
// de agenda
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// de agenda AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// analyse van het antwoord
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- regel 5: de generieke methode [getResponse] wordt aangeroepen. De daadwerkelijk gebruikte parameters zijn de volgende:
- 1: de gebruiker;
- 2: het doel van URL;
- 3: de te verzenden waarde. Hier is er geen;
- regel 5: de aanroep is niet omgeven door een try/catch-blok. De methode [getResponse] kan een uitzondering van het type [RdvMedecinsException] genereren. Als deze uitzondering wordt gegenereerd, wordt deze doorgegeven aan de methode die de hierboven genoemde methode [getAgendaMedecinJour] heeft aangeroepen;
- regel 8: deURL [/getAgendaMedecinJour] stuurt een type [Response<AgendaMedecinJour>] dat aan de serverzijde door de mapper jSON jSON is geserialiseerd;W2HTMLP005748ZQX. Dezelfde mapper wordt gebruikt om de ontvangen tekenreeks jSON te deserialiseren;
- regels 10-13: als er een fout optreedt in regel 9, wordt een type [RdvMedecinsException] gegenereerd;
- regels 16-21: het door de server verzonden antwoord wordt geanalyseerd;
- regels 17-18: als de server een fout heeft gemeld, wordt er een uitzondering gegenereerd met de door de server doorgegeven informatie;
- regels 19-21: anders wordt de agenda van de arts weergegeven;
De te onderzoeken aanvraag POST ziet er als volgt uit:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// het antwoord
Response<Rv> response;
try {
// de afspraak
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// de afspraak afspraak
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// analyse van het antwoord
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- regel 6: de methode [getResponse] wordt aangeroepen met de volgende parameters:
- 1: de gebruiker;
- 2: het doel URL,
- 3: de verzonden waarde: we geven de waarde jSON door van het type [PostAjouter], samengesteld uit de informatie die de methode als parameters heeft ontvangen. We gebruiken een mapper jSON zonder filters;
- regel 9: aan de serverzijde is het de mapper jSON [jsonMapperLongRv] die het antwoord van de server heeft geserialiseerd. Aan de clientzijde wordt dezelfde mapper gebruikt om het te deserialiseren;
- regel 6: de URL [/ajouterRv] retourneert de waarde jSON van het type [Response<Rv>];
- regels 4-11: hier is de methode [getResponse] in een try/catch-blok geplaatst, omdat het serialiseren van de verzonden waarde een uitzondering kan veroorzaken. De methode [getResponse] kan een uitzondering van het type [RdvMedecinsException] genereren. In dat geval wordt deze gewoon opnieuw aangeroepen (regels 11-12);
De volgende code (regels 13-24) is vergelijkbaar met de code die zojuist is besproken. Het enige verschil met een bewerking GET is dus de tweede parameter van de methode [getResponse], die de waarde jSON van de te posten waarde moet zijn.
De overige methoden zijn volgens hetzelfde patroon opgebouwd.
8.5.11. Afwijking
Bij het uitvoeren van diverse tests is een afwijking geconstateerd die is samengevat in de volgende klasse [Anomalie]:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// time-out van verbindingen in milliseconden
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we halen een referentie op uit de laag [DAO]
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// de URL van de webservice / json wordt ingesteld
dao.setUrlServiceWebJson("http://localhost:8080");
// de time-outs worden ingesteld in milliseconden
dao.setTimeout(TIMEOUT);
// Authenticatie
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authenticatie
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authenticatie
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// context afsluiten
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- regels 31-38: de gebruiker [admin, admin] wordt geauthenticeerd;
- regels 40-47: de gebruiker [admin, x] wordt geauthenticeerd, die dus een onjuist wachtwoord heeft;
- regels 49-56: de gebruiker [user, user] wordt geauthenticeerd; dit is een bestaande gebruiker, maar hij heeft geen toestemming;
Dit zijn de resultaten:
- regel 2: tegen alle verwachtingen in is de gebruiker [admin, x] geaccepteerd;
Als we de regels 33-38 van de code uitcommentariëren, krijgen we het volgende resultaat:
wat het verwachte resultaat is. Het lijkt alsof, toen de gebruiker [admin, admin] zich de eerste keer met succes had aangemeld, zijn wachtwoord voor de volgende keren niet meer nodig was. Dat is inderdaad het geval. Spring Security maakt standaard gebruik van een sessie, waardoor een gebruiker, zodra hij zich eenmaal heeft geauthenticeerd, dit bij volgende verzoeken niet meer hoeft te doen. We kunnen de configuratie van [Spring Security] in de webserver / jSON aanpassen zodat dit niet langer het geval is:
![]() |
Het bestand [SecurityConfig] moet als volgt worden aangepast:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// geen sessie
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- op regel 5 wordt aangegeven dat er geen beveiligingssessie mag zijn;
Hiermee is het probleem met de fout opgelost.
8.6. Codering van de Spring/Thymeleaf-server
8.6.1. Inleiding
Laten we terugkeren naar de architectuur van de te bouwen client/server-applicatie:
![]() |
- de webserver [Web2] / jSON is gebouwd;
- de clientlaag [DAO] van de client [Web1] is gebouwd;
De relatie tussen de server [Web1] en de clientbrowsers is een client-serverrelatie waarbij de server een webserver / jSON is. [Web1] levert namelijk HTML-streams af die zijn ingekapseld in een jSON-string. De client/server-architectuur is als volgt:
![]() |
- we hebben een client-architectuur [2] / server-architectuur [1], waarbij de client en de server communiceren via jSON;
- in [1] levert de Spring-weblaag MVC / Thymeleaf weergaven, weergavefragmenten en gegevens in jSON. De server is dus een webserver / jSON, net als de server [Web1]. Ook deze is stateless;
- in [2]: de JavaScript-code die is ingebed in de weergave die bij het opstarten van de applicatie wordt geladen, is gelaagd opgebouwd:
- de laag [présentation] zorgt voor de interacties met de gebruiker,
- de laag [DAO] zorgt voor de toegang tot de gegevens via de server [Web2];
- de client [2] zal bepaalde weergaven in de cache opslaan om de server te ontlasten;
We gaan de webserver / jSON [Web1], geïmplementeerd met Spring MVC / Thymeleaf, in verschillende stappen bouwen:
- kennismaking met het Bootstrap-framework;
- het schrijven van de weergaven;
- het schrijven van de controller;
Vervolgens zullen we, los daarvan, de client JS van de server [Web1] bouwen. Om duidelijk te maken dat deze client een zekere onafhankelijkheid heeft ten opzichte van de server [Web1], zullen we deze bouwen met de tool [Webstorm] in plaats van met STS.
In het vervolg zullen bepaalde details worden weggelaten, omdat ze ons zouden kunnen afleiden van het belangrijkste: de organisatie van de code. De geïnteresseerde lezer kan de volledige code vinden op de website van dit document.
8.6.2. Het project STS
![]() |
- in [1], de Java-codes;
- in [2], de weergaven;
De Maven-configuratie in [pom.xml] is als volgt:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- regels 16-19: het project is een Thymeleaf-project;
- regels 20-24: dat is gebaseerd op de laag [DAO] die we zojuist hebben opgebouwd;
De Java-configuratie wordt verzorgd door twee bestanden:
![]() |
De laag [web] wordt geconfigureerd door het volgende bestand [WebConfig]:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- configuratie van de QZXW2HTML-laag BW3dlYl0ZQX
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// configuratie van de dispatcherservlet voor de headers CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
We zijn op een bepaald moment al met alle elementen van deze configuratie in aanraking gekomen. Laten we er even aan herinneren dat de regels 42-47 nodig zijn wanneer men de server wil kunnen benaderen met domeinoverschrijdende verzoeken (CORS). Dat zal hier het geval zijn.
De klasse [AppConfig] configureert de gehele applicatie:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// hoofdmap webservice / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// time-out in milliseconden
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- regel 11: [AppConfig] importeert de configuratie van de laag [DAO] en de laag [web];
- regels 15-16: de inloggegevens waarmee de applicatie toegang krijgt tot de opstartfase van de applicatie om de artsen en klanten in de cache op te slaan;
- regel 18: de URL van de webservice / jSON [Web1];
- regel 20: de timeout voor de HTTP-aanroepen van de applicatie;
- regel 22: een booleaanse waarde om domeinoverschrijdende oproepen al dan niet toe te staan;
Ten slotte is in [application.properties] de Tomcat-server geconfigureerd om op poort 8081 te werken:
![]() |
server.port=8081
8.6.3. De functionaliteiten van de applicatie
Deze zijn beschreven in paragraaf 8.2. We zetten ze hier nogmaals op een rijtje. Met een browser roept men de URL [http://localhost:8081/boot.html] op:
![]() |
- naar [1], de startpagina van de applicatie;
- in [2] en [3], de gebruikersnaam en het wachtwoord van degene die de applicatie wil gebruiken. Er zijn twee gebruikers: admin/admin (login/wachtwoord) met een rol (ADMIN) en user/user met een rol (USER). Alleen de rol ADMIN heeft het recht om de applicatie te gebruiken. De rol USER is er alleen om te laten zien wat de server in dit gebruiksscenario antwoordt;
- in [4], de knop waarmee je verbinding kunt maken met de server;
- in [5], de taal van de applicatie. Er zijn er twee: standaard Frans en Engels;
- in [6], de URL van de server [rdvmedecins-springthymeleaf-server];
![]() |
- in [1] log je in;
![]() |
- zodra je bent ingelogd, kun je de arts kiezen bij wie je een afspraak wilt maken [2] en de dag waarop die plaatsvindt [3]. Zodra een arts en een dag zijn ingevuld, wordt de agenda automatisch weergegeven:
![]() |
- Zodra de agenda van de arts is opgevraagd, kun je een tijdvak reserveren [5];
![]() |
- in [6] kiest men de patiënt voor de afspraak en bevestigt men deze keuze in [7];
![]() |
Zodra de afspraak is bevestigd, keert men automatisch terug naar de agenda, waar de nieuwe afspraak nu is opgenomen. Deze afspraak kan later worden verwijderd via [8].
De belangrijkste functies zijn nu beschreven. Ze zijn eenvoudig. Laten we afsluiten met het taalbeheer:
![]() |
- in [1] schakelt men over van het Frans naar het Engels;
![]() |
- in [2] wordt de weergave in het Engels weergegeven, inclusief de kalender;
8.6.4. Stap 1: inleiding tot het CSS Bootstrap-framework
![]() |
In de bovenstaande webclient zullen de pagina’s HTML gebruikmaken van het Bootstrap-framework CSS [http://getbootstrap.com/] dat we nu gaan presenteren.
8.6.4.1. Het voorbeeldproject
Het ontwerp van de voorbeelden ziet er als volgt uit:
![]() |
- in [1]: het project in zijn geheel;
- in [2]: de Java-codes;
- in [3]: de JavaScript-scripts;
![]() |
- in [4]: de JavaScript-bibliotheken;
- in [5]: de Thymeleaf-weergaven;
- in [6]: de stylesheets;
8.6.4.1.1. Maven-configuratie
Het bestand [pom.xml] is dat van een Thymeleaf-Maven-project:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-bootstrap</name>
<description>Démos Bootstrap</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath /> <!-- bovenliggende entiteit opzoeken in repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. Java-configuratie
![]() |
De klasse [BootstrapDemo] configureert de Spring/Thymeleaf-applicatie:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(BootstrapDemo.class, args);
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}
We zijn dit soort code al eerder tegengekomen.
8.6.4.1.3. De Spring-controller
![]() |
De controller [BootstrapController] ziet er als volgt uit:
package istia.st.rdvmedecins;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BootstrapController {
@RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bso1() {
return "bs-01";
}
@RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs02() {
return "bs-02";
}
@RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs03() {
return "bs-03";
}
@RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs04() {
return "bs-04";
}
@RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs05() {
return "bs-05";
}
@RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs06() {
return "bs-06";
}
@RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs07() {
return "bs-07";
}
@RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs08() {
return "bs-08";
}
}
De acties zijn alleen bedoeld om door Thymeleaf verwerkte weergaven weer te geven.
8.6.4.1.4. Het bestand [application.properties]
Het bestand [application.properties] configureert de ingebouwde Tomcat-server:
server.port=8082
8.6.4.2. Voorbeeld nr. 1: het jumbotron
De actie [/bs-01] geeft de volgende weergave [bs-01.xml] weer:
![]() |
De weergave [bs-01.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- fout -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- regel 7: het bestand CSS van het Bootstrap-framework;
- regel 8: een lokaal bestand CSS;
- regel 13: geeft [1] weer;
- regels 19-21: geven [2] weer;
- regel 11: de klasse CSS [container] definieert een weergavegebied binnen de browser;
- regel 19: de klasse CSS [alert] geeft een gekleurd gebied weer. De klasse [alert-danger] gebruikt een vooraf gedefinieerde kleur. Er zijn er meerdere: [alert-info, alert-warning,...];
Het jumbotron [1] wordt gegenereerd door de volgende weergave [jumbotron.xml]:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- regel 4: het gebied heeft de klasse CSS [jumbotron];
- regel 5: de klasse [row] definieert een regel met 12 kolommen;
- regel 6: de klasse [col-md-2] definieert een gebied met twee kolommen in de regel;
- regel 7: in deze twee kolommen wordt een afbeelding geplaatst;
- regels 9-15: in de overige 10 kolommen wordt de tekst geplaatst;
8.6.4.3. Voorbeeld nr. 2: de navigatiebalk
De actie [/bs-02] geeft de volgende weergave [bs-02.xml] weer:
![]() |
Nieuw is de navigatiebalk [1] met het invoerformulier en de knoppen:
De weergave [bs-02.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- scripts JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar1"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- informatie -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 10: we importeren jQuery;
- regel 11: een lokaal script JS;
- regel 16: de navigatiebalk;
De navigatiebalk wordt gegenereerd door de volgende weergave [navbar1.xml]:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identificatieformulier -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- regel 3: de klasse [navbar] bepaalt de opmaak van de navigatiebalk. De klasse [navbar-inverse] geeft de navigatiebalk een zwarte achtergrond. De klasse [navbar-fixed-top] zorgt ervoor dat wanneer je door de pagina scrolt die door de browser wordt weergegeven, de navigatiebalk bovenaan het scherm blijft staan;
- regels 5-13: definiëren het gebied [1]. Dit is typisch een reeks klassen die ik niet begrijp. Ik gebruik de component zoals hij is;
- regels 14-26: definiëren een 'responsief' gebied van de bedieningsbalk. Op een smartphone verdwijnt dit gebied in een menu;
- regel 15: een afbeelding die momenteel verborgen is;
- regels 17-25: de klasse [navbar-form] geeft een formulier in de bedieningsbalk een opmaak. De klasse [navbar-right] plaatst het rechts daarvan;
- regels 21-23: de twee invoervelden van het formulier uit regel 17, [2]. Ze bevinden zich binnen een klasse [form-group] die de elementen van een formulier opmaakt en elk van hen heeft de klasse [form-control];
- regel 24: de klasse [btn] die een knop definieert, aangevuld met de klasse [btn-success] die de knop zijn groene kleur geeft;
- regel 24: wanneer er op de knop [Connexion] wordt geklikt, wordt de volgende functie JS uitgevoerd:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
Hier volgt een voorbeeld:

8.6.4.4. Voorbeeld nr. 3: de knop met lijst
De actie [/bs-03] geeft de volgende weergave [bs-03.xml] weer:
![]() |
- Nieuw is de lijst-boutron [1], ook wel 'dropdown' genoemd;
De code van de weergave [bs-03.xml] is als volgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar2"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 11: de dropdown-knop vereist het Bootstrap-bestand JS;
- regel 18: de nieuwe navigatiebalk;
De weergave [navbar2.xml] is als volgt:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- inlogformulier -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- talen -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- startpagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initNavBar2();
/*]]>*/
</script>
</section>
- regels 25-40: definiëren de lijstknop;
- regel 27: de klasse [btn-danger] geeft de knop een rode kleur;
- regels 32-39: de elementen van de lijst. Dit zijn links die elk gekoppeld zijn aan een functie JS;
- regels 46-51: een script JS dat wordt uitgevoerd nadat het document is geladen;
Het script JS [bs-03.js] is als volgt:
function initNavBar2() {
// dropdown met talen
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- regels 1-4: de functie die [dropdown] initialiseert. [$('.dropdown-toggle')] lokaliseert het element met de klasse [dropdown-toggle]. Dit is de lijstknop (regel 28 van de weergave). Hierop wordt de functie JS [dropdown()] toegepast, die is gedefinieerd in het bestand JS [bootstrap.js]. Pas na deze bewerking gedraagt de knop zich als een lijstknop;
- regels 10-21: de functie die wordt uitgevoerd bij het kiezen van een taal;
Hier volgt een voorbeeld:

8.6.4.5. Voorbeeld nr. 4: een menu
De actie [/bs-04] geeft de volgende weergave [bs-04.xml] weer:
![]() |
Er is een menu [1] toegevoegd.
De weergave [bs-04.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 18: er wordt een nieuwe navigatiebalk ingevoegd;
De weergave [navbar3.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- knoppen rechts -->
<div class="navbar-form navbar-right" role="form">
<!-- uitloggen -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- talen -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- startpagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// pagina wordt geladen
initNavBar3();
/*]]>*/
</script>
</section>
- regels 16-29: hiermee wordt het menu met vier opties aangemaakt, die elk zijn gekoppeld aan een script JS;
- regels 55-60: een script dat wordt uitgevoerd bij het laden van de pagina;
Het script JS [bs-04.js] is als volgt:
...
function initNavBar3() {
// dropdown met talen
$('.dropdown-toggle').dropdown();
// de geanimeerde afbeelding
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// de menu-links
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// we voegen ze toe aan een woordenboek
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// alle links worden verborgen
for ( var key in options) {
options[key].hide();
}
// we tonen de links die worden opgevraagd
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- regels 2-18: de initialisatiefunctie van de pagina;
- regel 4: om de knop met de talenlijst weer te geven;
- regels 6-7: de geanimeerde afbeelding wordt verborgen;
- regels 26-48: een functie [setMenu] waarmee kan worden aangegeven welke opties zichtbaar moeten zijn;
Ga naar de ontwikkelconsole (Ctrl-Shift-I) en voer de volgende code in: [1]:
![]() |
Laten we vervolgens teruggaan naar de browser. Het menu is gewijzigd [2]:
8.6.4.6. Voorbeeld nr. 5: een vervolgkeuzelijst
De actie [/bs-05] geeft de volgende weergave [bs-05.xml] weer:
![]() |
De nieuwigheid zit in [1]. We gebruiken hier een component die niet uit Bootstrap afkomstig is, [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
De code van de weergave [bs-05.xml] is als volgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content" th:include="choixmedecin">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 8: de CSS die nodig is voor de vervolgkeuzelijst;
- regel 13: het bestand JS dat nodig is voor de vervolgkeuzelijst;
- regel 24: de vervolgkeuzelijst;
De weergave [choixmedecin.xml] ziet er als volgt uit:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- lokaal script -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initChoixMedecin();
/*]]>*/
</script>
</section>
- regel 7-12: hier zien we een klassieke [select]-tag, maar dan met een specifieke klasse [combobox]. Het attribuut [data-style="btn-primary"] geeft de component zijn blauwe kleur;
- regels 16-21: een script dat wordt uitgevoerd bij het laden van de pagina;
Het bestand JS [bs-05.js] ziet er als volgt uit:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// de keuzelijst met artsen
$('#idMedecin').selectpicker();
// het menu
setMenu([ "lnkAfficherAgenda" ]);
}
- regels 7-12: de functie die wordt uitgevoerd bij het laden van de pagina;
- regel 9: de instructie die de [select] van de pagina omzet in een Bootstrap-dropdownlijst. [$('#idMedecin')] verwijst naar [select] (regel 7 van de weergave [choixmedecin]) en de functie JS [selectpicker] is afkomstig uit het bestand JS [bootstrap-select.js];
- regel 11: er wordt slechts één van de menuopties weergegeven;
- regels 2-5: de functie JS wordt uitgevoerd wanneer op de menuoptie [Agenda] wordt geklikt;
- regel 3: de waarde van de geselecteerde optie in de vervolgkeuzelijst wordt opgehaald: [$('#idMedecin option:selected')] zoekt eerst de component [id=idMedecin] en vervolgens in die component de geselecteerde optie. De bewerking [..].val() haalt vervolgens de waarde op van het gevonden element, d.w.z. het attribuut [value] van de geselecteerde optie;
Hier volgt een voorbeeld van de keuze van een arts:
![]() |
8.6.4.7. Voorbeeld nr. 6: een kalender
De actie [/bs-06] geeft de volgende weergave [bs-06.xml] weer:

De keuze van een arts of een datum activeert een functie JS die zowel de gekozen arts als de gekozen datum weergeeft. Hier is een voorbeeld:
![]() |
Met de knop ‘Talenlijst’ kun je de kalender (en alleen de kalender) in het Engels weergeven:

Dit is het meest complexe voorbeeld uit de reeks. De kalender is een component [bootstrap-datepicker] [http://eternicode.github.io/bootstrap-datepicker].
De weergave [bs-06.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- inhoud -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 8: het bestand CSS van de component [bootstrap-datepicker];
- regel 16: het bestand JS van de component [bootstrap-datepicker];
- regel 17: het bestand JS voor het beheer van een Franse kalender. Standaard is deze in het Engels;
- regel 15: het bestand JS uit een bibliotheek met de naam [moment], die toegang biedt tot talrijke functies voor tijdberekeningen [http://momentjs.com/];
- regel 28: de weergave van de kalender;
De weergave [choixmedecinjour.xml] ziet er als volgt uit:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- lokaal script -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- regels 17-23: de kalender;
- regel 18: de klasse [btn-primary] geeft de kalender zijn blauwe kleur;
- regel 18: het attribuut [disabled="true"] zorgt ervoor dat de datum niet handmatig kan worden ingevoerd. Men moet noodzakelijkerwijs de kalender gebruiken;
- regel 16: de kalender is in een sectie [id="calendar_container"] geplaatst. Om de taal van de kalender te wijzigen, moet deze eerst worden verwijderd en vervolgens opnieuw worden gegenereerd. We verwijderen dus de inhoud van de component [id="calendar_container"] en plaatsen daar vervolgens de nieuwe kalender met de nieuwe taal;
- regels 28-33: de initialisatiecode van de pagina;
Het bestand JS [bs-06.js] ziet er als volgt uit:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// kalender
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// kalender aanmaken
updateCalendar();
// de keuzelijst met artsen
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// het menu
setMenu([]);
}
- regel 2: de kalender wordt beheerd door verschillende functies JS. De variabele [calendar_infos] verzamelt informatie over de kalender. Deze is globaal, zodat hij door de verschillende functies kan worden gezien;
- regel 6: de container van de kalender wordt geïdentificeerd;
- regels 7-12: de opgeslagen informatie voor de kalender;
- regel 8: een verwijzing naar de container,
- regel 9: de code HTML van de kalender. Met deze twee gegevens kunnen we de kalender verwijderen en opnieuw genereren,
- regel 10: de datum van vandaag in het formaat [aaaa-mm-jj],
- regel 11: de taal van de kalender;
- regel 14: aanmaken van de kalender;
- regel 16: de keuzelijst met artsen;
- regels 17-19: telkens wanneer de geselecteerde waarde in deze keuzelijst verandert, wordt de methode [afficherAgenda] uitgevoerd;
- regel 21: geen menu in de navigatiebalk;
De functie [updateCalendar] is als volgt:
function updateCalendar(renew) {
if (renew) {
// de huidige kalender opnieuw genereren
calendar_infos.container.html(calendar_infos.html);
}
// initialisatie van de kalender
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// selectie van de huidige datum
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// gebeurtenissen
calendar.datepicker().on('hide', function(e) {
// weergave van de geselecteerde dag
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// de nieuwe datum wordt genoteerd
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// agenda-informatie weergeven
afficherAgenda();
// weergave van de geselecteerde dag
displayJour();
});
// weergave geselecteerde dag
displayJour();
}
- regel 1: de functie [updateCalendar] accepteert een parameter die al dan niet aanwezig kan zijn. Als deze aanwezig is, wordt de kalender opnieuw gegenereerd (regel 4) op basis van de informatie in [calendar_infos];
- regel 7: er wordt naar de kalender verwezen;
- regels 8-12: de initialisatieparameters;
- regel 9: het formaat van de beheerde datums [aaaa-mm-jj],
- regel 10: de eerste datum die in de kalender kan worden geselecteerd. In dit geval de datum van vandaag. Eerdere datums kunnen niet worden geselecteerd,
- regel 11: de taal van de kalender. Er zijn er twee: ['en'] en ['fr'];
- regel 13: de kalender is geconfigureerd;
- regels 15-17: als de datum van [calendar_infos] is ingesteld, wordt deze datum als huidige datum van de kalender weergegeven;
- regels 19-22: telkens wanneer de kalender wordt gesloten, wordt de geselecteerde datum weergegeven;
- regels 23-30: telkens wanneer er een datumwijziging plaatsvindt in de kalender:
- regel 25: wordt de geselecteerde datum opgeslagen in [calendar_infos],
- regel 27: er wordt informatie over de agenda weergegeven,
- regel 29: de geselecteerde dag wordt weergegeven;
- regel 32: we geven de geselecteerde dag weer, indien er een is;
De methode [displayJour] die de geselecteerde dag weergeeft, is als volgt:
// de geselecteerde dag weergeven
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- regel 3: als er al een datum is geselecteerd (in het begin is er geen datum geselecteerd in de kalender);
- regel 4: we lokaliseren het onderdeel waar we de datum gaan invoeren;
- regel 5: deze datum kan in het Engels of Frans worden geschreven. We stellen de taal van de bibliotheek [moment] in;
- regel 6: de geselecteerde datum wordt weergegeven in de gekozen taal en in het lange formaat;
- regel 7: deze datum wordt weergegeven;
Hier volgen twee voorbeelden:
![]() | ![]() |
Bij een wijziging van arts of datum wordt de methode [afficherAgenda] uitgevoerd:
function afficherAgenda() {
// dokter en datum weergeven
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Voorbeeld nr. 7: een 'responsive' tabel HTML
Opmerking: 'responsive' is een Engelse term die aangeeft dat een component zich kan aanpassen aan de grootte van het scherm waarop deze wordt weergegeven. We zullen hiervan een voorbeeld laten zien.
De actie [/bs-07] geeft de volgende weergave [bs-07.xml] weer (volledig scherm):
![]() |
Nieuw is de tabel HTML [1]. Deze tabel wordt beheerd door de bibliotheek JS [footable]: [https://github.com/fooplugins/FooTable].
Als je het browservenster verkleint, krijg je het volgende te zien:
![]() |
- de tabel HTML heeft zich aangepast aan de schermgrootte;
- in [1] moet je, om de link [Réserver] te zien, op het teken [+] klikken;
- in [2] is dit wat je ziet als je op het teken [+] klikt;
De weergave [bs-07.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- inhoud -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 10: de CSS uit de bibliotheek [footable];
- regel 19: de JS uit de bibliotheek [footable];
- regel 31: de tabel HTML van een agenda;
De weergave [agenda.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- startpagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initAgenda();
/*]]>*/
</script>
</body>
</html>
- regel 4: plaatst de tabel in een regel [row] en een gekleurd kader [alert alert-danger];
- regel 5: de tabel beslaat 6 kolommen [col-md-6];
- regel 6: de tabel HTML wordt opgemaakt met Bootstrap [class='table'];
- regel 9: het attribuut [data-toggle] geeft aan in welke kolom het symbool [+/-] staat waarmee de regel kan worden uit- en ingeklapt;
- regel 15: het attribuut [data-hide='phone'] geeft aan dat de kolom verborgen moet worden als het scherm de afmetingen van een telefoonscherm heeft. Men kan ook de waarde 'tablet' gebruiken;
- regel 31: er wordt een functie JS gekoppeld aan de link [Réserver];
- regel 46: er wordt een functie JS gekoppeld aan de link [Supprimer];
- regels 56-61: initialisatie van de pagina;
Een aantal van de hierboven gebruikte klassen CSS zijn afkomstig uit het bestand CSS [bootstrapDemo.css]:
@CHARSET "UTF-8";
#tijdvakken th {
text-align: center;
}
#td-cellen {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
De stijlen [status-*] zijn afkomstig uit een voorbeeld van het gebruik van de tabel [footable] dat op de website van de bibliotheek te vinden is.
In het bestand JS [bs-07.js] wordt de pagina als volgt geïnitialiseerd:
function initAgenda() {
// de tabel met tijdvakken
$("#creneaux").footable();
}
Dat is alles. [$("#creneaux")] verwijst naar de tabel HTML die we ‘responsief’ willen maken. Daarnaast zijn er de functies JS die verband houden met de twee koppelingen [Réserver] en [Supprimer]:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Voorbeeld nr. 8: een modaal venster
De actie [/bs-08] geeft de volgende weergave [bs-08.xml] weer:

Terwijl eerder het klikken op de link [Réserver] informatie in het informatievenster weergeeft, verschijnt hier een modaal venster om een klant te selecteren voor de RV:

De gebruikte component is de component [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].
De weergave [bs-08.xml] ziet er als volgt uit:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- lokaal script -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigatiebalk -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- inhoud -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- regel 19: het bestand JS dat nodig is voor de modale vensters;
- regel 32: de weergave [agenda-modal] is identiek aan de weergave [agenda], op één detail na: de functie JS die de koppeling [Réserver] beheert:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
De functie [showDialogResa] zorgt ervoor dat het modale selectievenster voor een klant wordt weergegeven;
- regel 33: de weergave [resa.xml] is het modale selectievenster voor een klant:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal title</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- startpagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geïnitialiseerd
initResa();
/*]]>*/
</script>
</section>
- regels 3-37: het modaal venster;
- regels 13-30: de inhoud van dit venster (wat er wordt weergegeven);
- regels 31-34: de knoppen van het dialoogvenster;
- regel 32: een knop [Annuler] die wordt beheerd door de functie JS [cancelDialogResa];
- regel 33: een knop [Valider] die wordt beheerd door de functie JS [validateResa];
- regels 39-44: het initialisatiescript van het modaalvenster;
Dit levert de volgende weergave op:
![]() |
Let op: het modaalvenster wordt standaard niet weergegeven. Daarom is het niet zichtbaar bij het opstarten van de applicatie, hoewel de code HTML wel in het document aanwezig is.
Het bestand JS [bs-08.js] ziet er als volgt uit:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// het tijdvak-id wordt opgeslagen
this.idCreneau = idCreneau;
// het reserveringsvenster wordt weergegeven
var resa = $("#resa");
resa.modal('show');
// log
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// het dialoogvenster wordt verborgen
resa.modal('hide');
}
// reservering bevestigen
function validateResa() {
// we halen de gegevens op
var idClient = $('#idClient option:selected').val();
// het dialoogvenster wordt verborgen
resa.modal('hide');
// informatie
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// de keuzelijst met klanten
$('#idClient').selectpicker();
// modaal venster
resa = $("#resa");
resa.modal({});
}
- regels 30-36: de initialisatiefunctie van het modaalvenster;
- regel 32: het modaalvenster bevat een vervolgkeuzelijst die moet worden geïnitialiseerd;
- regels 34-35: initialisatie van het modale venster zelf;
- regels 5-13: de functie JS die is gekoppeld aan de link [Réserver];
- regel 7: de parameter van de functie wordt opgeslagen in de globale variabele van regel 1;
- regels 9-10: het modaalvenster wordt zichtbaar gemaakt;
- regel 12: er wordt informatie in het informatievenster weergegeven;
- regels 15-18: afhandeling van de knop [Annuler]. We verbergen alleen het modale venster (regel 17);
- regels 21-31: de functie JS die aan de knop [Valider] is gekoppeld;
- regel 23: we halen het attribuut [value] op van de geselecteerde klant;
- regel 25: het dialoogvenster wordt verborgen;
- regel 27: de twee gegevens worden geregistreerd: het nummer van het gereserveerde tijdvak en voor welke klant;
8.6.5. Stap 2: het schrijven van de weergaven
We gaan nu de weergaven beschrijven die door de server [Web1] worden geleverd, evenals hun sjablonen.
![]() |
8.6.5.1. De weergave [navbar-start]
Deze geeft de navigatiebalk van de startpagina weer:

De code van [navbar-start.xml] is als volgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- aanmeldingsformulier -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- talen -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- startpagina -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initNavBarStart();
/*]]>*/
</script>
</section>
Deze weergave heeft geen sjabloon. Ze heeft de volgende gebeurtenishandlers:
évt | handler |
klik op de inlogknop | |
klik op de link [Français] | |
klik op de link [English] |
8.6.5.2. De weergave [jumbotron]
Dit is de weergave die onder de navigatiebalk [navbar-start] op de startpagina wordt getoond:

De code [jumbotron.xml] is als volgt:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
De weergave [jumbotron] heeft geen sjabloon en geen gebeurtenissen.
8.6.5.3. De weergave [login]
Dit is de weergave die onder het jumbotron op de startpagina wordt getoond:

De code [login.xml] is als volgt:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
De weergave heeft geen sjabloon en geen gebeurtenissen.
8.6.5.4. De weergave [navbar-run]
Dit is de navigatiebalk die wordt weergegeven wanneer de verbinding tot stand is gebracht:

De code [navbar-run.xml] is als volgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- knoppen aan de rechterkant -->
<form class="navbar-form navbar-right" role="form">
<!-- uitloggen -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- talen -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- pagina initialiseren -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initNavBarRun();
/*]]>*/
</script>
</section>
Deze weergave heeft geen sjabloon. Ze heeft de volgende gebeurtenishandlers:
évt | handler |
klik op de knop 'Afmelden' | |
klik op de link [Français] | |
klik op de link [English] |
8.6.5.5. Het scherm [accueil]
Dit is de weergave die direct onder de navigatiebalk wordt getoond: [navbar-run]:

De code [accueil.html] is als volgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- agenda -->
<div id="agenda"></div>
<!-- lokaal script -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geladen
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Het sjabloon ziet er als volgt uit:
- [rdvmedecins.medecinItems] (regel 8): de lijst met artsen;
In zijn huidige vorm lijkt de weergave geen gebeurtenishandlers te hebben. In werkelijkheid worden deze gedefinieerd in de functie [initChoixMedecinJour]. Deze functie is beschreven in paragraaf 8.6.4.7, op pagina 466 en met name op pagina 469. Daarin zijn de volgende gebeurtenishandlers te vinden:
event | handler |
keuze van een arts | |
keuze van een datum |
8.6.5.6. Het scherm [agenda]
Het scherm [agenda] toont een dag uit de agenda van een arts:

De code [agenda.xml] is als volgt:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- reservering -->
<section th:include="resa" />
</th:block>
<!-- pagina initialiseren -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geïnitialiseerd
initAgenda();
/*]]>*/
</script>
</body>
</html>
Het sjabloon van deze weergave bevat slechts één element:
- [agenda] (regel 4): een enigszins complex sjabloon dat speciaal is ontworpen voor de weergave van de agenda;
Het heeft de volgende gebeurtenishandlers:
event | handler |
klik op de knop [Supprimer] | |
klik op de link [Réserver] |
De weergave [resa] van regel 47 is de weergave die wordt getoond wanneer de gebruiker op een link [Réserver] klikt:

De bijbehorende code [resa.xml] is als volgt:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modale titel</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- pagina initialiseren -->
<script th:inline="javascript">
/*<![CDATA[*/
// de pagina wordt geïnitialiseerd
initResa();
/*]]>*/
</script>
</body>
</html>
Zijn model bestaat uit slechts één element:
- [clientItems] (regel 24): de klantenlijst;
Het beschikt over de volgende gebeurtenisverwerkers:
event | handler |
klik op de knop [Annuler] | |
klik op de knop [Valider] |
8.6.5.7. Het scherm [erreurs]
Dit is het scherm dat wordt weergegeven als de door de gebruiker gevraagde actie niet kon worden uitgevoerd:

De code [erreurs.xml] is als volgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-danger">
<h4>
<span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
</h4>
<ul>
<li th:each="message : ${erreurs}" th:text="${message}" />
</ul>
</div>
</section>
Het sjabloon bevat slechts één element:
- [erreurs] (regel 8): de lijst met weer te geven fouten;
De weergave heeft geen gebeurtenishandler.
8.6.5.8. Résumé
De volgende tabel geeft een overzicht van de weergaven en hun modellen:
weergave | sjabloon | gebeurtenisverwerkers |
navbar-start | ||
jumbotron | ||
login | ||
navbar-run | ||
home | ||
agenda | ||
reservering | ||
fouten |
8.6.6. Stap 3: acties schrijven
Laten we teruggaan naar de architectuur van de webservice [Web1]:
![]() |
We gaan nu bekijken welke URL-acties door [Web1] worden blootgesteld en hoe deze zijn geïmplementeerd:
8.6.6.1. De URL-acties die door de service [Web1] worden blootgesteld
Dit zijn de volgende:
- een URL voor elk van de voorgaande weergaven of een combinatie daarvan;
- een URL om een RV toe te voegen;
- een URL om een RV te verwijderen;
Ze geven allemaal een reactie van het type [Reponse], zoals hieronder weergegeven:
public class Reponse {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// de navigatiebalk
private String navbar;
// het jumbotron
private String jumbotron;
// de hoofdtekst van de pagina
private String content;
// de agenda
private String agenda;
...
}
- regel 5: een status van het antwoord: 1 (OK), 2 (fout);
- regel 7: de stroom HTML van de weergaven [navbar-start] of [navbar-run], al naar gelang het geval;
- regel 9: de feed HTML van de weergave [jumbotron];
- regel 13: de stroom HTML van de weergave [agenda];
- regel 9: de feed HTML van de weergaven [accueil], [erreurs], [login], al naar gelang het geval;
De weergegeven URL zijn de volgende
plaatst de weergave [navbar-start] in [Reponse.navbar] | |
plaatst de weergave [navbar-run] in [Reponse.navbar] | |
plaatst de weergave [accueil] in [Reponse.content] | |
plaatst de weergave [jumbotron] in [Reponse.jumbotron] | |
zet de weergave [agenda] in [Reponse.agenda] | |
plaatst de weergave [login] in [Reponse.content] | |
| |
zet de weergave [navbar-run] in [Reponse.navbar], de weergave [jumbotron] in [Reponse.jumbotron], de weergave [accueil] in [Reponse.content], de weergave [agenda] in [Reponse.agenda] | |
voegt de geselecteerde afspraak toe en plaatst de nieuwe agenda in [Reponse.agenda] | |
verwijdert de geselecteerde afspraak en plaatst de nieuwe agenda in [Reponse.agenda] |
8.6.6.2. Het singleton [ApplicationModel]
![]() |
Van de klasse [ApplicationModel] wordt één enkel exemplaar geïnstantieerd en in de controller van de applicatie geïnjecteerd. De code ervan is als volgt:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- regel 6: [ApplicationModel] is een Spring-component;
- regel 7: die de interface van de laag [DAO] implementeert. We doen dit zodat de acties de laag [DAO] niet hoeven te kennen, maar alleen het singleton [ApplicationModel]. De architectuur van [Web1] ziet er dan als volgt uit:
![]() |
Laten we nog eens kijken naar de code van de klasse [ApplicationModel]:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// de laag [DAO]
@Autowired
private IDao dao;
// de configuratie
@Autowired
private AppConfig appConfig;
// gegevens afkomstig van de laag [DAO]
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// configuratiegegevens
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// uitzondering
private RdvMedecinsException rdvMedecinsException;
// fabrikant
public ApplicationModel() {
}
@PostConstruct
public void init() {
// configuratie
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// de vervolgkeuzelijsten voor artsen en klanten worden in de cache opgeslagen
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// de items van de keuzelijsten worden aangemaakt
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// getters en setters
...
// implementatie van de interface [IDao]
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- regel 11: injectie van de referentie van de implementatie van de laag [DAO]. Vervolgens wordt deze referentie gebruikt om de interface [IDao] te implementeren (regels 64-80);
- regel 14: invoeren van de configuratie van de applicatie;
- regels 33-37: gebruik van deze configuratie om diverse elementen van de applicatiearchitectuur in te stellen;
- regels 38-46: we slaan de informatie op in de cache die de vervolgkeuzelijsten van artsen en klanten zal vullen. We gaan er dus vanuit dat als een arts of een klant verandert, de applicatie opnieuw moet worden opgestart. Het idee hier is om te laten zien dat een Spring-singleton als cache voor de webapplicatie kan dienen;
De klassen [MedecinItem] en [ClientItem] zijn beide afgeleid van de volgende klasse [PersonneItem]:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// element van een lijst
private Long id;
private String texte;
// constructor
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// getters en setters
...
}
- regel 8: het veld [id] wordt de waarde van het attribuut [value] van een optie in de vervolgkeuzelijst;
- regel 9: het veld [texte] is de tekst die wordt weergegeven bij een optie in de vervolgkeuzelijst;
8.6.6.3. De klasse [BaseController]
![]() |
De klasse [BaseController] is de bovenliggende klasse van de controllers [RdvMedecinsController] en [RdvMedecinsCorsController]. Het was niet verplicht om deze bovenliggende klasse aan te maken. Hierin zijn hulpprogramma-methoden van de klasse [RdvMedecinsController] verzameld die, op één na, niet essentieel zijn. Ze kunnen in drie groepen worden ingedeeld:
- de hulpprogramma-methoden;
- methoden die de weergaven samenvoegen met hun modellen;
- de methode voor het initialiseren van een actie
| twee hulpprogramma's die een lijst met foutmeldingen weergeven. We zijn deze al tegengekomen en hebben ze al gebruikt; |
| geeft de weergave [accueil] weer zonder sjabloon |
| geeft de weergave [agenda] en het bijbehorende sjabloon weer |
| geeft de weergave [login] weer zonder sjabloon |
| geeft het antwoord aan de klant weer wanneer de gevraagde actie met een fout is beëindigd |
| de initialisatiemethode voor alle acties van de controller [RdvMedecinsController] |
Laten we twee van deze methoden eens bekijken.
De methode [getPartialViewAgenda] genereert de meest complexe weergave, namelijk die van de agenda. De code ervan is als volgt:
// stroom [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// contexten
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// we bouwen het paginamodel [agenda]
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// de agenda met zijn sjabloon
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- regels 9-10: de twee elementen van het agendamodel:
- regel 9: de weergegeven agenda.
- regel 10: de lijst met klanten die wordt weergegeven wanneer de gebruiker een afspraak maakt;
De methode [setModelforAgenda] op regel 7 is als volgt:
// sjabloon van de pagina [Agenda]
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// de titel van de pagina
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// de reserveringstijdvakken
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// tijdvak van de arts
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// ID
modelCréneau.setId(créneau.getId());
// tijdvak
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// klant en bestelling
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// volgend tijdvak
i++;
}
// het agendamodel wordt weergegeven
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- regel 6: de agenda heeft een titel:

of:

We zien dat de datumnotatie afhankelijk is van de taal. We halen deze notatie op uit de berichtbestanden (regel 4).
- regels 11-40: voor elk tijdslot moet de weergave worden getoond:
![]()
of de weergave:
![]()
- regels 19-20: geven het tijdvak weer;
- regels 25-28: het geval waarin het tijdslot vrij is. In dat geval moet de knop [Réserver] worden weergegeven;
- regels 31-36: het geval waarin het tijdslot bezet is. In dat geval moeten zowel de klant als de knop [Supprimer] worden weergegeven;
De andere methode waarover we meer uitleg geven, is de methode [getActionContext]. Deze wordt aan het begin van elke actie van [RdvMedecinsController] aangeroepen. De signatuur ervan is als volgt:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Deze methode retourneert het volgende type [ActionContext]:
public class ActionContext {
// gegevens
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- regel 4: de Thymeleaf-context van de actie;
- regel 5: de Spring-context van de actie;
- regel 6: de locale van de actie;
- regel 7: een eventuele lijst met foutmeldingen;
De parameters zijn als volgt:
- [lang]: de gevraagde taal voor de actie 'en' of 'fr';
- [origin]: de header HTTP [origin] in het geval van een domeinoverschrijdende aanroep;
- [request]: het verzoek HTTP dat momenteel wordt verwerkt, wat al enige tijd een actie wordt genoemd;
- [response]: het antwoord dat op dit verzoek zal worden gegeven;
- [result]: elke actie van [RdvMedecinsController] ontvangt een verzonden waarde waarvan de geldigheid wordt getest. [result] is het resultaat van deze test;
- [rdvMedecinsController]: de container-controller voor de acties;
De methode [getActionContext] is als volgt geïmplementeerd:
// context van een actie
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// taal?
if (lang == null) {
lang = "fr";
}
// lokale instellingen
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// Frans
locale = new Locale("fr", "FR");
} else {
// al het overige in het Engels
locale = new Locale("en", "US");
}
// kopteksten CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// initialisatiefouten
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// fouten in POST?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// geen fouten
return actionContext;
}
- regels 3-15: aan de hand van de parameter [lang] wordt de locale van de actie ingesteld;
- regel 17: de HTTP-headers die nodig zijn voor verzoeken tussen domeinen worden verzonden. We gaan hier niet in detail op in. De gebruikte techniek is die uit paragraaf 8.4.14;
- regel 19: foutloos aanmaken van een [ActionContext]-object;
- regel 21: in paragraaf 8.6.6.2 hebben we gezien dat de singleton [ApplicationModel] toegang had tot de database om zowel de cliënten als de artsen op te halen. Deze toegang kan mislukken. We slaan dan de uitzondering op die optreedt. Op regel 21 vangen we deze uitzondering op;
- regels 22-25: als er een uitzondering is opgetreden bij het opstarten van de applicatie, is geen enkele actie mogelijk. We retourneren dan voor elke actie een object [ActionContext] met de foutmeldingen van de uitzondering;
- regel 27-20: we analyseren de parameter [result] om te bepalen of de verzonden waarde geldig was of niet. Als deze ongeldig was, retourneren we een object [ActionContext] met de bijbehorende foutmeldingen;
- regel 32: geval zonder fouten;
We bekijken nu de acties van de controller [RdvMedecinsController]
8.6.6.4. De actie [/getNavBarStart]
De actie [/getNavBarStart] retourneert de weergave [navbar-start]. De handtekening ervan is als volgt:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
Deze retourneert het volgende type [Reponse]:
public class Reponse {
// ----------------- eigenschappen
// status van de bewerking
private int status;
// de navigatiebalk
private String navbar;
// het jumbotron
private String jumbotron;
// de hoofdtekst van de pagina
private String content;
// de agenda
private String agenda;
...
}
en heeft de volgende parameters:
- [PostLang postlang]: de volgende geposte waarde:
public class PostLang {
// gegevens
@NotNull
private String lang;
...
}
De klasse [PostLang] is de bovenliggende klasse van alle verzonden waarden. De klant moet namelijk altijd aangeven in welke taal de actie moet worden uitgevoerd.
De methode [getNavbarStart] is als volgt geïmplementeerd:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// actiecontext
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave wordt teruggestuurd [navbar-start]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- regel 7: initialisatie van de actie;
- regels 10-13: als de initialisatiemethode van de actie fouten heeft gemeld, worden deze in het antwoord naar de klant gestuurd (regel 12) met status 2:
- regels 15-18: de weergave [navbar-start] wordt verzonden met status 1:
Hieronder gaan we alleen in op de nieuwe functies.
8.6.6.5. De actie [/getNavbarRun]
De actie [/getNavBarRun] genereert de weergave [navbar-run]:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// contexten van de actie
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave wordt teruggestuurd [navbar-run]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
De actie kan twee soorten reacties opleveren:
- een foutmelding (regels 10-13):
- het antwoord met de weergave [navbar-run]:
8.6.6.6. De actie [/getJumbotron]
De actie [/getJumbotron] levert de weergave [jumbotron] op:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// contexten van de actie
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave [jumbotron] wordt teruggestuurd
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
De actie kan twee soorten reacties opleveren:
- een foutmelding (regels 10-13):
- het antwoord met de weergave [jumbotron]:
8.6.6.7. De actie [/getLogin]
De actie [/getLogin] levert de weergave [login] op:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// contexten van de actie
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave [login] wordt teruggestuurd
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
De actie kan twee soorten reacties opleveren:
- een foutmelding (regels 9-11):
- het antwoord met de weergave [login]:
8.6.6.8. De actie [/getAccueil]
De actie [/getAccueil] levert de weergave [accueil] op. De handtekening ervan is als volgt:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- regel 3, de geplaatste waarde is van het type [PostUser], als volgt:
public class PostUser extends PostLang {
// gegevens
@NotNull
private User user;
...
}
- regel 1: de klasse [PostUser] is een uitbreiding van de klasse [PostLang] en bevat dus een taal;
- regel 4: de gebruiker die de weergave wil opvragen;
De implementatiecode is als volgt:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// actiecontext
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave [accueil] is beveiligd
try{
// gebruiker
User user = postUser.getUser();
// de inloggegevens worden gecontroleerd [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// er wordt een foutmelding teruggestuurd
return getViewErreurs(thymeleafContext, e.getMessages());
}
// de weergave wordt teruggestuurd [accueil]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- regels 15-22: merk op dat de pagina [accueil] beveiligd is en dat de gebruiker daarom geauthenticeerd moet zijn;
De actie kan twee soorten reacties opleveren:
- het foutbericht (regels 11 en 21):
- het antwoord met de weergave [accueil] (regels 24-27):
8.6.6.9. De actie [/getNavbarRunJumbotronAccueil]
De actie [/getNavbarRunJumbotronAccueil] genereert de weergaven [navbar-run, jumbotron, accueil]. Deze heeft de volgende handtekening:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- regel 3: de verzonden waarde is van het type [PostUser];
De implementatie van de actie is als volgt:
// navigatiebalk + jumbotron + startpagina
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// context van de actie
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de weergave [accueil] is beveiligd
try {
// gebruiker
User user = postUser.getUser();
// de inloggegevens worden gecontroleerd [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// er wordt een foutmelding teruggestuurd
return getViewErreurs(thymeleafContext, e.getMessages());
}
// het antwoord wordt verzonden
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
De actie kan twee soorten antwoorden opleveren:
- het antwoord met fout (regels 13, 23):
- het antwoord met de weergaven [navbar-run, jumbotron, accueil] (regels 26-31):
8.6.6.10. De actie [/getAgenda]
De actie [/getAgenda] retourneert de weergave [agenda]. De handtekening ervan is als volgt:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- regel 3: de geposte waarde is van het type [PostGetAgenda], namelijk:
public class PostGetAgenda extends PostUser {
// gegevens
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- regel 1: de klasse [PostGetAgenda] is een uitbreiding van de klasse [PostUser] en bevat dus een taal en een gebruiker;
- regel 5: het nummer van de arts van wie de agenda gewenst is;
- regel 8: de gewenste dag van de agenda;
De implementatie is als volgt:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// context van de actie
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de geldigheid van de post wordt gecontroleerd
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// de weergave wordt teruggestuurd [erreurs]
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- tot en met regel 14 is de code inmiddels standaard;
- regels 16-21: er wordt een extra controle uitgevoerd op de ingevoerde waarde. De datum moet gelijk zijn aan of later zijn dan de huidige datum. Om dit te controleren, wordt een validator gebruikt:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// de gekozen dag voor de afspraak
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// de datums worden omgezet naar het formaat jjjj-MM-dd
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// de gekozen dag mag niet vóór de huidige datum liggen
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- regel 19: de validator werkt voor twee klassen: [PostGetAgenda] en [PostValiderRv];
Laten we teruggaan naar de code van de actie [/getAgenda]:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// actie
try {
// agenda van de arts
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// antwoord
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// de weergave [erreurs] wordt geretourneerd
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// de weergave wordt teruggegeven [erreurs]
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- regels 9-10: met de verzonden parameters wordt de agenda van de arts opgevraagd;
- regels 12-13: de agenda wordt teruggestuurd:
- regels 17, 21: er wordt een antwoord met fouten teruggestuurd:
8.6.6.11. De actie [/getNavbarRunJumbotronAccueilAgenda]
De actie [/getNavbarRunJumbotronAccueilAgenda] geeft de weergaven [navbar-run, jumbotron, accueil, agenda] weer. De implementatie ervan is als volgt:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// contexten van de actie
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// agenda
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
// het antwoord wordt verzonden
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- regels 15-18: er wordt gebruikgemaakt van de actie [/getAgenda] om deze aan te roepen. Vervolgens wordt gekeken naar de status in het antwoord (regel 16). Als er een fout wordt gedetecteerd, wordt het proces niet voortgezet en wordt het antwoord teruggestuurd;
- regel 20: we sturen de gevraagde weergaven:
8.6.6.12. De actie [/supprimerRv]
Met de actie [/supprimerRv] kan een afspraak worden verwijderd. De handtekening ervan is als volgt:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- regel 3: de verzonden waarde is van het type [PostSupprimerRv], namelijk:
public class PostSupprimerRv extends PostUser {
// gegevens
@NotNull
private Long idRv;
..
}
- regel 1: de klasse [PostSupprimerRv] is een uitbreiding van de klasse [PostUser] en bevat dus een taal en een gebruiker;
- regel 5: het nummer van de afspraak die moet worden verwijderd;
De implementatie van de actie is als volgt:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// context van de actie
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// geplaatste waarden
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// de afspraak wordt verwijderd
AgendaMedecinJour agenda = null;
try {
// de afspraak wordt teruggehaald
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// de bijbehorende afspraak verwijderen
application.supprimerRv(user, idRv);
// de agenda van de arts wordt opnieuw gegenereerd
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// de nieuwe agenda wordt weergegeven
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// de weergave wordt teruggestuurd [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// de weergave [erreurs] wordt teruggestuurd
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- regel 22: de afspraak die moet worden verwijderd, wordt opgehaald. Als deze niet bestaat, treedt er een uitzondering op;
- regels 23-25: aan de hand van deze afspraak worden de arts en de betreffende dag bepaald. Deze gegevens zijn nodig om de agenda van de arts opnieuw te genereren;
- regel 27: de afspraak wordt verwijderd;
- regel 29: de nieuwe agenda van de arts wordt opgevraagd. Dit is belangrijk. Naast het tijdvak dat zojuist is vrijgekomen, kunnen andere gebruikers van de applicatie wijzigingen in de agenda hebben aangebracht. Het is belangrijk om de gebruiker de meest recente versie ervan terug te sturen;
- regels 31-34: de agenda wordt teruggestuurd:
8.6.6.13. De actie [/validerRv]
De actie [/validerRv] voegt een afspraak toe aan de agenda van een arts. De handtekening ervan is als volgt:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- regel 3: de verzonden waarde is van het type [PostValiderRv], namelijk:
public class PostValiderRv extends PostUser {
// gegevens
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- regel 1: de klasse [PostValiderRv] is een uitbreiding van de klasse [PostUser] en bevat dus een taal en een gebruiker;
- regel 5: het nummer van het tijdvak;
- regel 7: het nummer van de klant voor wie de reservering is gemaakt;
- regel 10: de dag van de afspraak;
De implementatie van de actie is als volgt:
// een afspraak bevestigen
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// contexten van de actie
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// fouten?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// de geldigheid van de afspraakdag wordt gecontroleerd
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// de weergave [erreurs] wordt teruggestuurd
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// geplaatste waarden
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// actie
try {
// er wordt informatie over het tijdvak opgehaald
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// de afspraak wordt toegevoegd
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// de agenda wordt opnieuw gegenereerd
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// de nieuwe agenda wordt weergegeven
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// de weergave wordt teruggestuurd [erreurs]
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// de weergave [erreurs] wordt teruggestuurd
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
De code is vergelijkbaar met die van de actie [/supprimerRv].
8.6.7. Stap 4: testen van de Spring/Thymeleaf-server
We gaan nu de verschillende voorgaande acties testen met de Chrome-plugin [Advanced Rest Client] (zie paragraaf 9.6).
8.6.7.1. Configuratie van de tests
Alle acties verwachten een verzonden waarde. We zullen varianten van de volgende tekenreeks jSON verzenden:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Deze verzonden waarde bevat voor de meeste acties overbodige informatie. Deze informatie wordt echter genegeerd door de acties die deze ontvangen en leidt niet tot fouten. Deze verzonden waarde heeft het voordeel dat hij de verschillende te verzenden waarden omvat.
8.6.7.2. De actie [/getNavbarStart]
![]() |
- naar [1], de geteste actie;
- naar [2], de geposte waarde;
- in [3] is de geboekte waarde een tekenreeks jSON;
- in [4] wordt de weergave [navbar-start] in het Engels opgevraagd;
Het verkregen resultaat is als volgt:
![]() |
We hebben de weergave [navbar-start] in het Engels ontvangen (velden gemarkeerd).
Laten we nu eens een fout maken. We wijzigen de waarde van het attribuut [lang] van de verzonden waarde naar null. We ontvangen het volgende resultaat:
![]() |
We hebben een foutmelding (status 2) ontvangen waarin staat dat het veld [lang] verplicht is.
8.6.7.3. De actie [/getNavbarRun]
We vragen de actie [getNavbarRun] aan met de volgende verzonden waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
8.6.7.4. De actie [/getJumbotron]
We roepen de actie [getJumbotron] aan met de volgende ingevoerde waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
8.6.7.5. De actie [/getLogin]
We roepen de actie [getLogin] aan met de volgende ingevoerde waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
8.6.7.6. De actie [/getAccueil]
We roepen de actie [getAccueil] aan met de volgende geposte waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
We beginnen opnieuw met een onbekende gebruiker:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het resultaat is als volgt:
![]() |
We beginnen opnieuw met een bestaande gebruiker die echter geen toestemming heeft om de applicatie te gebruiken:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
8.6.7.7. De actie [/getAgenda]
We vragen de actie [getAgenda] aan met de volgende verzonden waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
We proberen het opnieuw met een datum die eerder is dan vandaag:
![]() |
We beginnen opnieuw met een niet-bestaande arts:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
8.6.7.8. De actie [/getNavbarRunJumbotronAccueil]
We vragen de actie [getNavbarRunJumbotronAccueil] aan met de volgende verzonden waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
Hetzelfde geldt voor een onbekende gebruiker:
![]() |
8.6.7.9. De actie [/getNavbarRunJumbotronAccueilAgenda]
We vragen de actie [getNavbarRunJumbotronAccueilAgenda] aan met de volgende verzonden waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
We voeren een arts in die niet bestaat:
![]() |
8.6.7.10. De actie [/supprimerRv]
We vragen de actie [supprimerRv] aan met de volgende geposte waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
De Rv van nr. 93 bestaat niet. Het verkregen resultaat is als volgt:
![]() |
Met een afspraak die wel bestaat:
![]() |
In de database kan worden gecontroleerd of de afspraak daadwerkelijk is verwijderd. De nieuwe agenda wordt teruggestuurd.
8.6.7.11. De actie [/validerRv]
We vragen om actie [validerRv] met de volgende geboekte waarde:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Het verkregen resultaat is als volgt:
![]() |
In de database kan worden gecontroleerd of de afspraak daadwerkelijk is aangemaakt. De nieuwe agenda is teruggestuurd.
We doen hetzelfde met een niet-bestaand tijdvaknummer:
![]() |
We doen hetzelfde met een niet-bestaand klantnummer:
![]() |
8.6.8. stap 5: Schrijven van de JavaScript-client
Laten we teruggaan naar de serverarchitectuur van [Web1]:
![]() |
De client [2] van de server [Web1] is een JavaScript-client van het type APU (Single Page Application):
- de client vraagt de opstartpagina op bij een webserver (niet noodzakelijkerwijs [Web1]);
- hij vraagt de volgende pagina’s op bij de server [Web1] via Ajax-verzoeken;
Om deze client te bouwen, gaan we de tool [Webstorm] gebruiken (zie paragraaf 9.8). Deze tool leek me handiger dan STS. Het belangrijkste voordeel is dat het automatische aanvulling biedt bij het invoeren van de code, evenals enkele opties van refactoring. Dit voorkomt veel fouten.
8.6.8.1. Het project JS
Het project JS heeft de volgende structuur:
![]() |
- in [1], de client JS in zijn geheel. [boot.html] is de startpagina. Dit is de enige pagina die door de browser wordt geladen;
- in [2], de stylesheets van de Bootstrap-componenten;
- in [3], de enkele afbeeldingen die door de applicatie worden gebruikt;
![]() |
- in [4], de scripts JS. Hier ligt ons werk;
- in [5], de gebruikte bibliotheken JS: voornamelijk jQuery, en die van de Bootstrap-componenten;
8.6.8.2. De architectuur van de code
De code is opgesplitst in drie lagen:
![]() |
- de laag [présentation] bevat de initialisatiefuncties van de pagina [boot.xml] en die van de diverse Bootstrap-componenten. Deze laag wordt geïmplementeerd door het bestand [ui.js];
- de laag [événements] bevat alle gebeurtenishandlers van de laag [présentation]. Deze wordt geïmplementeerd door het bestand [evts.js];
- de laag [DAO] verzendt de verzoeken van HTTP naar de server [Web1]. Deze wordt geïmplementeerd door het bestand [dao.js];
8.6.8.3. De laag [présentation]
![]() |
De laag [présentation] wordt geïmplementeerd door het volgende bestand [ui.js]:
//de laag [présentation]
var ui = {
// globale variabelen;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// componenten van de weergave
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// de gebeurtenissenlaag
var evts = {};
// de laag [dao]
var dao = {};
// ------------ document gereed
$(document).ready(function () {
// documentinitialisatie
console.log("document.ready");
// paginaonderdelen
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
// de inlogpagina wordt opgeslagen om deze later weer te kunnen weergeven
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL van de dienst
$("#urlService").val(ui.urlService);
});
// ------------------------ initialisatiefuncties voor Bootstrap-componenten
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// geeft de geselecteerde dag weer
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- Om de lagen van elkaar te scheiden, is besloten ze in drie objecten onder te brengen:
- [ui] voor de laag [présentation] (regels 2-27),
- [evts] voor de gebeurtenisbeheerlaag (regel 29),
- [dao] voor de laag [DAO] (regel 31);
Door deze opdeling van de lagen in drie objecten kunnen een aantal conflicten met variabelenamen en functienamen worden voorkomen. Elke laag gebruikt variabelen en functies met als voorvoegsel de naam van het object waarin de laag is ingekapseld.
- regels 38-44: de velden die altijd aanwezig zullen zijn, ongeacht de weergegeven weergaven, worden opgeslagen. Dit voorkomt herhaalde en onnodige zoekopdrachten naar jQuery;
- regels 46-49: de startpagina wordt lokaal opgeslagen, zodat deze kan worden weergegeven wanneer de gebruiker uitlogt en de taal niet heeft gewijzigd;
- regels 54-83: initialisatiefuncties voor de Bootstrap-componenten. Deze zijn allemaal besproken in de toelichting bij deze componenten in paragraaf 8.6.4;
8.6.8.4. De hulpprogramma's van de laag [événements]
![]() |
De gebeurtenishandlers zijn ondergebracht in het bestand [evts.js]. Verschillende functies worden regelmatig door de gebeurtenishandlers gebruikt. We zullen deze nu bespreken:
// begin van het wachten
evts.beginWaiting = function () {
// begin van de wachttijd
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// einde van de wachttijd
evts.stopWaiting = function () {
// einde wachttijd
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// weergave van het resultaat
evts.showResult = function (result) {
// de ontvangen gegevens worden weergegeven
var data = result.data;
// de status wordt geanalyseerd
switch (result.status) {
case 1:
// fout?
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// fout weergeven
evts.showException(data);
break;
}
};
// ------------ diverse functies
evts.showException = function (data) {
// foutmelding
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- regel 2: de functie [evts.beginwaiting] wordt aangeroepen vóór elke asynchrone actie [DAO];
- regels 4-5: de animatie voor het wachten wordt weergegeven;
- regels 6-7: het weergavegebied voor fouten en uitzonderingen (dit zijn niet dezelfde) wordt verborgen;
- regel 8: er wordt aangegeven dat er een asynchrone taak bezig is;
- regel 12: de functie [evts.stopwaiting] wordt aangeroepen nadat een asynchrone actie [DAO] haar resultaat heeft opgeleverd;
- regel 14: er wordt gemeld dat de asynchrone taak is voltooid;
- regel 15: de animatie die het wachten weergeeft, wordt verborgen;
- regel 20: de functie [evts.showResult] geeft het resultaat [result] weer van een asynchrone actie [DAO]. Het resultaat is een JS-object met de volgende vorm: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- regels 47-50: worden gebruikt als [result.status==2]. Dit gebeurt wanneer de server [Web1] een antwoord verstuurt met een foutheader HTTP (bijvoorbeeld 403 forbidden). In dit geval is [data] de tekenreeks jSON die door de server is verzonden om de fout te melden;
- regel 25: wanneer er een geldig antwoord is ontvangen van de server [Web1]. Het veld [data] bevat dan het antwoord van de server: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- regel 27: het geval waarin de server [Web1] een foutmelding heeft verzonden {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':fouten};
- regels 28-29: de weergave [erreurs] wordt weergegeven;
- regels 31-33: eventuele weergave van de navigatiebalk;
- regels 34-36: eventuele weergave van de jumbotron;
- regels 37-39: eventuele weergave van het veld [data.content]. Dit vertegenwoordigt, afhankelijk van het geval, een van de weergaven [accueil, agenda];
- regels 40-43: als de agenda opnieuw is gegenereerd, worden bepaalde referenties van de componenten opgeslagen, zodat deze niet telkens opnieuw hoeven te worden opgezocht wanneer ze nodig zijn;
- regel 54: de functie [evts.showException] heeft als taak de tekst van de uitzondering weer te geven die is opgenomen in de parameter [data];
- regels 57-58: de tekst van de uitzondering wordt weergegeven;
- regel 58: de titel van de uitzondering hangt af van de huidige taal;
Het bestand [evts.js] bevat meer dan 300 regels code, die ik niet allemaal zal toelichten. Ik zal slechts enkele voorbeelden nemen om de opzet van deze laag te illustreren.
8.6.8.5. Inloggen van een gebruiker

Het inloggen van een gebruiker wordt verzorgd door de volgende functie:
// ------------------------ verbinding
evts.connecter = function () {
// de te verzenden waarden worden opgehaald
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// de URL van de server wordt ingesteld
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// verzoekparameters
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// het verzoek wordt verzonden
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- regels 4-5: de gebruikersnaam en het wachtwoord van de gebruiker worden opgehaald;
- regels 7-8: de URL van de service [Web1] wordt opgehaald. Deze wordt zowel in de laag [ui] als in de laag [dao] opgeslagen;
- regels 10-16: de te verzenden waarde: de huidige taal en de gebruiker die probeert in te loggen;
- regels 17-23: het object [sendMeBack] is een object dat wordt doorgegeven aan de functie [DAO] die zal worden aangeroepen en dat deze functie moet teruggeven aan de functie in regel 22. Hier omvat het object [sendMeBack] de gebruiker die zich wil aanmelden;
- regels 25-29: de functie [evts.execute] kan een reeks asynchrone acties uitvoeren. Hier wordt een lijst doorgegeven die uit slechts één actie bestaat. De velden daarvan zijn als volgt:
- [name]: de naam van de uit te voeren asynchrone actie,
- [post]: de waarde die naar de server [Web1] moet worden verzonden,
- [sendMeBack]: de waarde die de asynchrone actie samen met het resultaat moet terugsturen;
Voordat we de functie [evts.execute] in detail bespreken, kijken we eerst naar de functie [evts.connecterDone] op regel 22. Dit is de functie waaraan de aangeroepen asynchrone functie [DAO] haar resultaat moet teruggeven:
evts.connecterDone = function (result) {
// weergave van het resultaat
evts.showResult(result);
// verbinding gelukt?
if (result.status == 1 && result.data.status == 1) {
// pagina
ui.page = "accueil-sans-agenda";
// de gebruiker wordt geregistreerd
ui.user = result.sendMeBack.user;
}
};
- regel 3: het door de server [Web1] teruggestuurde resultaat wordt weergegeven;
- regel 5: als dit resultaat geen fouten bevat, worden het type van de nieuwe pagina (regel 7) en de geauthenticeerde gebruiker (regel 9) opgeslagen;
De functie [evts.execute] voert een reeks asynchrone acties uit:
// een reeks acties uitvoeren
evts.execute = function (actions) {
// werk in uitvoering?
if (evts.travailEnCours) {
// er gebeurt niets
return;
}
// in afwachting
evts.beginWaiting();
// acties uitvoeren
dao.doActions(actions, evts.stopWaiting);
};
- regel 2: de parameter [actions] is een lijst met uit te voeren asynchrone acties;
- regels 4-7: de uitvoering wordt alleen geaccepteerd als er nog geen andere loopt;
- regel 9: het wachten wordt gestart;
- regel 11: de laag [DAO] wordt gevraagd de reeks acties uit te voeren. De tweede parameter is de naam van de functie die moet worden uitgevoerd zodra alle acties in de reeks hun resultaat hebben opgeleverd;
We gaan nu niet in detail in op de functie [dao.doActions]. We gaan een andere gebeurtenis bekijken.
8.6.8.6. Taalwijziging

De taalwijziging wordt verzorgd door de volgende functie:
// ------------------------ taalwijziging
evts.setLang = function (lang) {
// taalwijziging?
if (lang == ui.langue) {
// er gebeurt niets
return;
}
// nieuwe taal
ui.langue = lang;
// welke pagina moet worden vertaald?
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- regel 2: de parameter [lang] is de nieuwe taal: 'fr' of 'en';
- regels 4-7: als de nieuwe taal de huidige taal is, gebeurt er niets;
- regel 9: de nieuwe taal wordt opgeslagen;
- regels 12-20: bij een taalwijziging moet de pagina die momenteel door de browser wordt weergegeven, opnieuw worden gegenereerd. Er zijn drie mogelijke pagina's:
- de pagina met de naam [login], waarbij de weergegeven pagina de authenticatiepagina is,
- de pagina met de naam [accueil-sans-agenda], de pagina die direct na een succesvolle authenticatie wordt weergegeven,
- de pagina met de naam [accueil-avec-agenda], die wordt weergegeven zodra een eerste agenda is geopend. Deze blijft vervolgens permanent zichtbaar totdat de gebruiker zich afmeldt;
We gaan nu de pagina [accueil-avec-agenda] behandelen. Er zijn drie versies van deze functie:
![]() |
- de versie [ getAccueilAvecAgenda-one] voert één enkele asynchrone actie uit;
- de versie [ getAccueilAvecAgenda-parallel] voert vier asynchrone acties parallel uit;
- de versie [ getAccueilAvecAgenda-sequence] voert vier asynchrone acties na elkaar uit;
8.6.8.7. De functie [ getAccueilAvecAgenda-one]
Dit is de volgende functie:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// verzoekparameters
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// verzoek
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- regels 4-9: de te verzenden waarde bevat de aangemelde gebruiker, de gewenste taal, het nummer van de arts waarvan de agenda wordt opgevraagd en de dag van de gewenste agenda;
- regels 10-12: het object [sendMeBack] is het object dat wordt teruggestuurd naar de functie in regel 11. Hier bevat het geen informatie;
- regels 14-18: uitvoering van een reeks asynchrone acties, namelijk de actie met de naam [accueil-avec-agenda] (regel 15);
- regel 11: de functie die wordt uitgevoerd zodra de asynchrone actie [accueil-avec-agenda] haar resultaat heeft geleverd;
De functie [evts.getAccueilAvecAgendaDone] in regel 11 geeft het resultaat weer van de asynchrone functie met de naam [accueil-avec-agenda]:
evts.getAccueilAvecAgendaDone = function (result) {
// weergave van het resultaat
evts.showResult(result);
// nieuwe pagina?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- regel 1: [result] is het resultaat van de asynchrone functie met de naam [accueil-avec-agenda];
- regel 3: dit resultaat wordt weergegeven;
- regel 5: als het een foutloos resultaat is, wordt de nieuwe pagina genoteerd (regel 6);
8.6.8.8. De functie [ getAccueilAvecAgenda-parallel]
Dit is de volgende functie:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// acties [navbar-run, jumbotron, accueil, agenda] in //
// navbar-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// jumbotron
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// home
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// agenda
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'dag': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// acties uitvoeren in //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- regel 51: deze keer worden vier asynchrone acties uitgevoerd. Deze worden parallel uitgevoerd;
- regels 5-13: definitie van de actie [navbarRun] die de navigatiebalk [navbar-run] ophaalt;
- regel 12: de functie die moet worden uitgevoerd zodra de asynchrone actie [navbarRun] het resultaat heeft geretourneerd;
- regels 15-23: definitie van de actie [jumbotron] die de weergave [jumbotron] ophaalt;
- regel 22: de functie die moet worden uitgevoerd wanneer de asynchrone actie [jumbotron] haar resultaat heeft geleverd;
- regels 25-34: definitie van de actie [accueil] die de weergave [accueil] ophaalt;
- regel 33: de functie die moet worden uitgevoerd wanneer de asynchrone actie [accueil] haar resultaat heeft geleverd;
- regels 36-49: definitie van de actie [agenda] die de weergave [jumbotron] ophaalt;
- regel 48: de functie die moet worden uitgevoerd wanneer de asynchrone actie [agenda] het resultaat heeft opgeleverd;
8.6.8.9. De functie [ getAccueilAvecAgenda-sequence]
Dit is de volgende functie:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// acties [navbar-run, jumbotron, accueil, agenda] in volgorde
// agenda
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'dag' : ui.jourAgenda,
"caller" : evts.getAgendaDone
};
// home
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// jumbotron
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// navbar-run
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// acties in volgorde uitvoeren
evts.execute([ navbarRun ])
};
- regel 54: de actie [navbarRun] wordt uitgevoerd. Zodra deze is voltooid, gaat men verder met de volgende: [jumbotron], regel 51. Deze actie wordt vervolgens op haar beurt uitgevoerd. Zodra deze is voltooid, gaat men verder met de volgende: [accueil], regel 40. Deze wordt op haar beurt uitgevoerd. Zodra deze is voltooid, gaat men verder met de volgende: [agenda], regel 29. Deze wordt op zijn beurt uitgevoerd. Zodra deze is voltooid, stopt men, omdat de actie [agenda] geen volgende actie heeft.
8.6.8.10. De laag [DAO]
![]() |
Het bestand [dao.js] bevat alle functies van de laag [DAO]. We zullen deze stapsgewijs bespreken:
// URL door de server aangeboden
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server-URL
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- regels 16-18: de functie waarmee de URL van de dienst [Web1] kan worden ingesteld;
- regels 2-13: het woordenboek dat de naam van een asynchrone actie koppelt aan de URL van de te raadplegen server [Web1];
// ------------------ generiek beheer van acties
// uitvoering van een reeks asynchrone acties
dao.doActions = function (actions, done) {
// verwerking van acties
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// asynchrone DAO-aanvraag
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- regel 3: de functie [dao.doActions] voert een reeks asynchrone acties [actions] uit. De parameter [done] is de functie die moet worden uitgevoerd wanneer alle acties hun resultaat hebben opgeleverd;
- regels 7-12: de asynchrone acties worden parallel uitgevoerd. Als een van deze acties echter een vervolgactie heeft, wordt deze uitgevoerd na afloop van de voorafgaande actie;
- regel 9: er is een object [Deferred] in de toestand [pending];
- regel 10: wanneer dit object de status [resolved] bereikt, wordt de functie [dao.actionDone] uitgevoerd;
- regel 11: actie nr. i uit de lijst wordt asynchroon uitgevoerd. De parameter [done] uit regel 3 wordt als parameter doorgegeven;
De functie [dao.actionDone], die aan het einde van elke asynchrone actie wordt uitgevoerd, is als volgt:
// er is een resultaat ontvangen
dao.actionDone = function (result) {
// beller?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// volgende?
if (sendMeBack && sendMeBack.next) {
// asynchrone aanvraag DAO
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// klaar?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// klaar?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- regel 2: de functie [dao.actionDone] ontvangt het resultaat [result] van een van de asynchrone acties uit de lijst met uit te voeren acties;
- regels 4-7: als de voltooide asynchrone actie een functie had opgegeven waaraan het resultaat moest worden teruggestuurd, wordt deze functie aangeroepen;
- regels 9-14: als de voltooide asynchrone actie een volgende actie heeft, wordt deze actie op haar beurt uitgevoerd;
- regel 16: een actie is voltooid. De teller van voltooide acties wordt verhoogd. Een actie met een onbepaald aantal volgende acties telt als één actie;
- regels 19-21: als aanvankelijk een functie [done] was opgegeven om te worden uitgevoerd zodra alle acties in de reeks hun resultaat hadden geretourneerd, dan wordt deze functie nu uitgevoerd;
De methode [dao.doAction] voert een asynchrone actie uit:
// uitvoering van een actie
dao.doAction = function (deferred, action, done) {
// done-functie om in de actie op te nemen
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// actie uitvoeren
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- regels 4-10: zoals we zojuist hebben gezien, moet de functie die het resultaat van de uit te voeren asynchrone actie gaat verwerken, toegang hebben tot de functie [done]. Daarom plaatsen we deze laatste in het object [sendMeBack], een object dat deel zal uitmaken van het resultaat van de asynchrone bewerking;
- regel 12: we voeren de functie [dao.executePost] uit, die een aanroep HTTP doet naar de server [Web1]. De doel-URL is de URL die gekoppeld is aan de naam van de uit te voeren actie;
De functie [dao.executePost] voert een aanroep HTTP uit:
// verzoek HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// we geven het resultaat weer
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// de fout wordt weergegeven
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
We zijn deze functie al eerder tegengekomen en hebben er commentaar op gegeven. We merken alleen op dat in regel 9 de doel-URL het resultaat is van het samenvoegen van de URL van de server [Web1] met de URL die gekoppeld is aan de naam van de actie.
8.6.8.11. De opstartpagina
![]() |

De opstartpagina [boot.html] toont het bovenstaande beeld. Dit is de enige pagina die rechtstreeks door de browser wordt geladen. De overige pagina's worden via Ajax-verzoeken opgehaald. De code ervan is als volgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Bootstrap-kern CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- Aangepaste stijlen voor dit sjabloon -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!-- Bootstrap-kern JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- gebruikersscripts -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- aanmeldingsformulier -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- talen -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Bootstrap Jumbotron -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- foutmeldingen -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!-- inhoud -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- startpagina -->
<script>
// de pagina wordt geladen
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- we zijn dit type pagina al tegengekomen in het hoofdstuk over Bootstrap (paragraaf 8.6.4);
- regels 99-105: initialisatie van bepaalde elementen van de laag [présentation];
- regel 27: het script [getAccueilAvecAgenda-sequence.js] wordt gebruikt. Door het script in deze regel te wijzigen, ontstaan er drie verschillende manieren om de pagina [accueil-avec-agenda] op te halen:
- [getAccueilAvecAgenda-one.js] genereert de pagina met één enkele aanroep van HTTP,
- [getAccueilAvecAgenda-parallel.js] haalt de pagina op met vier gelijktijdige aanroepen van HTTP,
- [getAccueilAvecAgenda-sequence.js] haalt de pagina op met vier opeenvolgende aanroepen van HTTP;
8.6.8.12. Tests
Er zijn verschillende manieren om de tests uit te voeren. We gaan hier de tool [Webstorm] gebruiken:
![]() |
- in [1] openen we een project. We selecteren eenvoudigweg de map [2] die de statische boomstructuur (HTML, CSS, JS) van de te testen website bevat;
![]() |
- in [3], de statische website;
- in [4-5] wordt de pagina [boot.html] geladen;
![]() |
- in [5], zien we dat een server die is ingebed in [Webstorm] de pagina [boot.html] heeft geleverd vanaf poort [63342]. Dit is een belangrijk punt om te begrijpen, want het betekent dat de scripts van de pagina [boot.html] domeinoverschrijdende verzoeken zullen doen naar de server [Web1], die op zijn beurt weer op [localhost:8081] draait. De browser die [boot.html] heeft geladen, weet dat hij deze pagina heeft geladen vanaf [localhost:63342]. Hij zal daarom niet toestaan dat deze pagina verzoeken doet naar de site [localhost:8081], omdat dit niet dezelfde poort is. De browser zal daarom de in paragraaf 8.4.14 beschreven inter-domeinverzoeken uitvoeren. Om deze reden moet de applicatie [Web1] zo worden geconfigureerd dat deze inter-domeinverzoeken worden geaccepteerd. Dit wordt bepaald in het bestand [AppConfig] op de Spring/Thymeleaf-server:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// root webservice / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// time-out in milliseconden
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
We laten het aan de lezer over om de client JS te testen. Deze moet de in paragraaf 8.6.3 beschreven functionaliteiten kunnen reproduceren.
Zodra de client JS als correct is bevonden, kan deze worden geïmplementeerd in de map van de server [Web1] om te voorkomen dat verzoeken tussen domeinen moeten worden toegestaan:
![]() |
Hierboven hebben we de geteste site gekopieerd naar de map [src / main / resources / static]. Vervolgens kunnen we de URL en [http://localhost:8081/boot.html] opvragen:

Nu hebben we geen domeinoverschrijdende verzoeken meer nodig en kunnen we in het configuratiebestand [AppConfig] van de server [Web1] het volgende schrijven:
// CORS
private final boolean CORS_ALLOWED=false;
De bovenstaande applicatie blijft werken. Als we teruggaan naar de applicatie [Webstorm], werkt deze niet meer:


Als je naar de ontwikkelconsole gaat (Ctrl-Shift-I), zie je de oorzaak van de fout:

Het gaat om een fout vanwege een ongeautoriseerde cross-domain-verzoek.
8.6.8.13. Conclusion
We hebben de volgende architectuur JS gerealiseerd:
![]() |
- de lagen zijn vrij duidelijk gescheiden;
- we hebben een applicatie van het type APU (Single Page Application). Dankzij deze eigenschap kunnen we nu een native applicatie genereren voor verschillende mobiele platforms (Android, IoS, Windows Phone);
- we hebben een model ontwikkeld dat asynchrone acties parallel, sequentieel of een combinatie van beide kan uitvoeren;
8.6.9. stap 6: genereren van een native applicatie voor Android
Met de tool [Phonegap] [http://phonegap.com/] kan een uitvoerbaar bestand voor mobiele apparaten (Android, IoS, Windows 8, ...) op basis van een HTML / JS / CSS-app. Er zijn verschillende manieren om dit te bereiken. Wij gebruiken de eenvoudigste: een online tool op de website van Phonegap [http://build.phonegap.com/apps]. Deze tool zal het zip-bestand van de te converteren statische website ‘uploaden’. De startpagina moet [index.html] heten. We hernoemen de pagina [boot.html] dus naar [index.html]:
![]() |
vervolgens zetten we de map in een zip-bestand, in dit geval [rdvmedecins-client-js-03]. Daarna gaan we naar de Phonegap-website [http://build.phonegap.com/apps]:
![]() |
- voordat we naar [1] gaan, moet u mogelijk een account aanmaken;
- in [1] gaan we aan de slag;
- bij [2] kiezen we een gratis abonnement waarmee slechts één Phonegap-app is toegestaan;
![]() |
- in [3] download je de gezipte app [4];
![]() |
- in [5], geef je de app een naam;
- in [6] bouw je de app. Dit kan 1 minuut duren. Wacht tot de pictogrammen van de verschillende mobiele platforms aangeven dat het bouwen is voltooid;
![]() |
- alleen de Android-binaire bestanden [7] en de Windows-binaire bestanden [8] zijn gegenereerd;
- klik op [7] om het Android-binaire bestand te downloaden;
![]() |
- in [9] het gedownloade binaire bestand [apk];
Start een [GenyMotion]-emulator voor een Android-tablet (zie paragraaf 9.9):
![]() |
Hierboven starten we een tabletemulator met Android-versie API 19. Zodra de emulator is gestart,
- ontgrendel je deze door het grendelje (indien aanwezig) aan de zijkant naar de zijkant te schuiven en vervolgens los te laten;
- sleep met de muis het gedownloade bestand [PGBuildApp-debug.apk] naar de emulator. Het wordt dan geïnstalleerd en uitgevoerd;
![]() |
Je moet URL veranderen in [1]. Typ hiervoor in een opdrachtvenster de opdracht [ipconfig] (regel 1 hieronder) in, waardoor de verschillende IP-adressen van uw computer worden weergegeven:
C:\Users\Serge Tahé>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 15 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet Connexion au réseau local :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
...
Noteer ofwel het wifi-adres IP (regels 6-9), ofwel het adres IP op het lokale netwerk (regels 11-17). Gebruik vervolgens dit adres IP in de URL van de webserver:
![]() |
Zodra dit is gebeurd, maakt u verbinding met de webservice:
![]() |
Test de applicatie op de emulator. Deze zou nu moeten werken. Aan de serverzijde kan men al dan niet de headers CORS toestaan in de klasse [ApplicationModel]:
// CORS
private final boolean CORS_ALLOWED=false;
Dit maakt voor de Android-app niet uit. Deze draait immers niet in een browser. De vereiste voor de headers CORS komt namelijk van de browser en niet van de server.
8.6.10. Conclusie van de casestudy
We hebben de volgende architectuur ontwikkeld:
![]() |
Dit is een complexe 3-tier-architectuur. Het doel was om de laag [Web2] te hergebruiken, die de serverlaag was van de applicatie [AngularJS-Spring MVC] uit het document [Tutoriel AngularJS / Spring 4] naar deURL en [http://tahe.developpez.com/angularjs-spring4/]. Alleen om deze reden hebben we een drielagige architectuur. Terwijl in de applicatie [AngularJS-Spring MVC] de client van [Web2] een client van [AngularJS] was, is de client van [Web2] hier een 2-tier-architectuur [jQuery] / [Spring MVC / Thymeleaf]. We hebben het aantal lagen uitgebreid, waardoor we aan prestaties zullen inboeten.
De hier onderzochte applicatie is in de loop der tijd in drie verschillende documenten ontwikkeld:
- [Introduction aux frameworks JSF2, Primefaces et Primefaces mobile], URL en [http://tahe.developpez.com/java/primefaces/]. De casestudy was destijds ontwikkeld met de frameworks JSF2 / Primefaces. Primefaces is een bibliotheek met AJAX-componenten waarmee het schrijven van JavaScript wordt vermeden. De toen ontwikkelde applicatie was minder complex dan de applicatie die hier wordt onderzocht. Deze had een klassieke webversie voor de computer en een mobiele versie voor telefoons;
- [Tutoriel AngularJS / Spring 4] naar URL [http://tahe.developpez.com/angularjs-spring4/]. De toen ontwikkelde applicatie had dezelfde kenmerken als de applicatie die in dit document wordt besproken. De applicatie was ook naar Android geporteerd;
- dit document;
Uit dit werk komen voor mij de volgende punten naar voren:
- de applicatie [Primefaces] was veruit het eenvoudigst te schrijven en de mobiele webversie bleek goed te presteren. Er is geen kennis van JavaScript voor nodig. Het is niet mogelijk om de applicatie native te porten naar de OS-platforms van verschillende mobiele apparaten, maar is dat wel nodig? Het lijkt moeilijk om de stijl van de applicatie aan te passen. Er wordt namelijk gewerkt met de stylesheets van Primefaces. Dit kan een nadeel zijn;
- het schrijven van de [AngularJS-Spring MVC]-applicatie was complex. Het [AngularJS]-framework leek me vrij moeilijk te doorgronden als je het onder de knie wilt krijgen. De architectuur [client Angular] / [service web / jSON implémenté par Spring MVC] is bijzonder overzichtelijk en performant. Deze architectuur is reproduceerbaar voor elke webapplicatie. Dit is de architectuur die mij het meest veelbelovend lijkt, omdat ze aan de client- en serverzijde verschillende vaardigheden vereist (JS+HTML+CSS aan de clientzijde, Java of iets anders aan de serverzijde), waardoor de client en de server parallel kunnen worden ontwikkeld;
- voor de applicatie die in dit document wordt ontwikkeld met een 3-tier-architectuur [client jQuery] / [serveur Web1 / Spring MVC / Thymeleaf] / [serveur Web2 / Spring MVC], is het mogelijk dat sommigen de technologie van [jQuery+Spring MVC+Thymelaf] eenvoudiger te begrijpen vinden dan die van [AngularJS]. De [DAO]-laag van de JavaScript-client die we hebben geschreven, is herbruikbaar in andere toepassingen;

























































































































































































































































































