3. Artikel 2 - Voorbeelden van drielaagse webarchitecturen
Doelstellingen van het artikel:
- drielaagse architecturen
- eenvoudige webarchitectuur MVC
- Struts-architectuur MVC
- Spring-architectuur MVC
Gebruikte tools:
- Spring: http://www.springframework.org/
- Ibatis SqlMap: http://www.ibatis.com/
- JUnit: http://www.junit.org/index.htm
- Eclipse: http://www.eclipse.org/
- Struts: http://struts.apache.org/
- Firebird: http://firebird.sourceforge.net/: SGBD, stuurprogramma JDBC. Eigenlijk volstaat elke bron met JDBC.
- IBExpert, Personal Edition: http://www.hksoftware.net/download/ibep_2005.2.14.1_full.exe (maart 2005). Met IBExpert kun je de SGBD Firebird grafisch beheren.
- Tomcat: http://jakarta.apache.org/tomcat/
- Tomcat-plug-in voor Eclipse: http://www.sysdeo.com/eclipse/tomcatPlugin.html. Zie ook het document https://tahe.developpez.com/java/eclipse/
Om dit document te kunnen begrijpen, zijn diverse voorkennisvereisten nodig. Sommige daarvan zijn te vinden in documenten die ik heb geschreven. In dat geval vermeld ik ze. Het spreekt voor zich dat dit slechts een suggestie is en dat de lezer zijn eigen favoriete documenten kan gebruiken.
- Java-taal: [https://tahe.developpez.com/java/cours]
- webprogrammeren in Java: [https://tahe.developpez.com/java/web/]
- Webprogrammering met Java, Eclipse, Tomcat: [https://tahe.developpez.com/java/eclipse/]
- webprogrammering met Struts: [https://tahe.developpez.com/java/struts/]
- gebruik van het aspect IoC van Spring: [https://tahe.developpez.com/java/springioc]
- tagbibliotheek: JSTL (gedeeltelijk)
- Ibatis-documentatie SqlMap: [https://prdownloads.sourceforge.net/ibatisnet/DevGuide.pdf?download]
- Firebird: [http://firebird.sourceforge.net/pdfmanual/Firebird-1.5-QuickStart.pdf] (maart 2005).
De ideeën in dit document zijn afkomstig uit een boek dat ik in de zomer van 2004 heb gelezen, een prachtig werk van Rod Johnson: J2EE Development without EJB, uitgegeven door Wrox.
3.1. De webapplicatie „articles“
We willen hier enkele elementen van een webapplicatie voor e-commerce presenteren. Hiermee kunnen webklanten
- een lijst met artikelen uit een database te bekijken
- sommige daarvan in een elektronisch winkelmandje te plaatsen
- en dit winkelmandje te bevestigen. Deze bevestiging heeft als enig gevolg dat de voorraad van de gekochte artikelen in de database wordt bijgewerkt.
De gebruiker krijgt de volgende schermen te zien:
- het scherm [LISTE], dat een lijst met te koop aangeboden artikelen toont

- de weergave [INFOS], die aanvullende informatie over een product geeft:

- de weergave [PANIER], die de inhoud van het winkelmandje van de klant toont

- de weergave [PANIERVIDE] voor het geval het winkelmandje van de klant leeg is

- de weergave [ERREURS] die eventuele fouten in de applicatie meldt

3.2. Algemene architectuur van de applicatie
We willen een applicatie bouwen met de volgende drielaagse structuur:
- de drie lagen zijn onafhankelijk van elkaar gemaakt door het gebruik van Java-interfaces
- De integratie van de verschillende lagen wordt gerealiseerd door Spring
- Elke laag is ondergebracht in afzonderlijke pakketten: web (gebruikersinterface), domain (bedrijfslogica) en DAO (gegevenslaag).
We gaan er hier vanuit dat de lagen [domain] en [dao] al aanwezig zijn. We richten ons uitsluitend op de laag [web], die we op verschillende manieren willen bouwen:
- met behulp van klassieke servlet-controller-technologie – pagina’s JSP
- met behulp van Struts-technologie MVC
- met behulp van Spring-technologie MVC
In alle gevallen zal de applicatie een MVC-architectuur (Model - View - Controller) volgen. Als we het bovenstaande lagenmodel weergeven, past de MVC-architectuur daar als volgt in:
De verwerking van een verzoek van een klant verloopt volgens de volgende stappen:
- de klant doet een verzoek aan de controller. Deze controller is een servlet die alle verzoeken van klanten verwerkt. Dit is de toegangspoort tot de applicatie. Dit is de C van MVC.
- de controller verwerkt dit verzoek. Hiervoor kan hij de hulp nodig hebben van de businesslaag, het zogenaamde M-model in de structuur MVC.
- De controller ontvangt een antwoord van de businesslaag. Het verzoek van de klant is verwerkt. Dit kan verschillende mogelijke antwoorden opleveren. Een klassiek voorbeeld is
- een foutpagina als het verzoek niet correct kon worden verwerkt
- een bevestigingspagina in het andere geval
- de controller kiest welk antwoord (= weergave) naar de klant moet worden verzonden. Dit is meestal een pagina met dynamische elementen. De controller levert deze aan de weergave.
- De weergave wordt naar de klant verzonden. Dit is de V van MVC.
3.3. Het model
Hier bekijken we de M van MVC. Het model bestaat hier uit de volgende elementen:
- de businessklassen
- de klassen voor gegevenstoegang
- de database
3.3.1. De database
De database bevat slechts één tabel met de naam ARTICLES. Deze is gegenereerd met de volgende SQL-opdrachten:
| CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(30) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* beperkingen */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
/* primaire sleutel */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
|
| primaire sleutel die een artikel op unieke wijze identificeert |
| naam van het artikel |
| de prijs ervan |
| huidige voorraad |
| de voorraad onder welke een herbevoorradingsorder moet worden geplaatst |
In de volgende tests is gebruikgemaakt van een database met de naam [Firebird]. [Firebird] is een „open source“-versie van SGBD. Het stuurprogramma JDBC [firebirdsql-full.jar] is geplaatst in de map [WEB-INF/lib] van de webapplicatie.
3.3.2. De pakketten van het sjabloon
Model M wordt hier geleverd in de vorm van drie archieven:
- istia.st.articles.dao: bevat de klassen voor toegang tot de gegevens van de laag [dao]
- istia.st.articles.exception: bevat een uitzonderingsklasse voor dit artikelbeheer
- istia.st.articles.domain: bevat de bedrijfsspecifieke klassen van de laag [domain]
archive | contenu | rôle |
istia.st.articles.dao | - bevat het pakket [istia.st.articles.dao], dat op zijn beurt de volgende elementen bevat: - [IArticlesDao]: de interface voor toegang tot de Dao-laag. Dit is de enige interface die de laag [domain] ziet. Deze ziet geen andere. - [Article]: klasse die een artikel definieert - [ArticlesDaoSqlMap]: implementatieklasse van de interface [IArticlesDao] met de tool SqlMap | gegevenslaag – bevindt zich volledig in de laag [dao] van de 3-tier-architectuur van de webapplicatie |
istia.st.articles.domain | - bevat het pakket [istia.st.articles.domain], dat op zijn beurt de volgende elementen bevat: - [IArticlesDomain]: de interface voor toegang tot de laag [domain]. Dit is de enige interface die de weblaag ziet. De weblaag ziet geen andere interfaces. - [AchatsArticles]: een klasse die [IArticlesDomain] implementeert - [Achat]: klasse die de aankoop van een klant vertegenwoordigt - [Panier]: klasse die alle aankopen van een klant vertegenwoordigt | vertegenwoordigt het model voor online aankopen – bevindt zich volledig in de laag [domain] van de 3-tier-architectuur van de webapplicatie |
istia.st.articles.exception | - bevat het pakket [istia.st.articles.exception], dat op zijn beurt de volgende elementen bevat: - [UncheckedAccessArticlesException]: klasse die een uitzondering van het type [RuntimeException] definieert. Dit type uitzondering wordt door de laag [dao] gegenereerd zodra er een probleem met de toegang tot de gegevens optreedt. | |
3.3.3. Het pakket [istia.st.articles.dao]
De klasse die een artikel definieert, is de volgende:
| package istia.st.articles.dao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
/**
* @author ST - ISTIA
*
*/
public class Article {
private int id;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
/**
* constructeur par défaut
*/
public Article() {
}
public Article(int id, String nom, double prix, int stockActuel,
int stockMinimum) {
// instantieattributen initialiseren
setId(id);
setNom(nom);
setPrix(prix);
setStockActuel(stockActuel);
setStockMinimum(stockMinimum);
}
// getters - setters
public int getId() {
return id;
}
public void setId(int id) {
// geldige id?
if (id < 0)
throw new UncheckedAccessArticlesException("id[" + id + "] invalide");
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
// geldige naam?
if(nom==null || nom.trim().equals("")){
throw new UncheckedAccessArticlesException("Le nom est [null] ou vide");
}
this.nom = nom;
}
public double getPrix() {
return prix;
}
public void setPrix(double prix) {
// geldige prijs?
if(prix<0) throw new UncheckedAccessArticlesException("Prix["+prix+"]invalide");
this.prix = prix;
}
public int getStockActuel() {
return stockActuel;
}
public void setStockActuel(int stockActuel) {
// geldige voorraad?
if (stockActuel < 0)
throw new UncheckedAccessArticlesException("stockActuel[" + stockActuel + "] invalide");
this.stockActuel = stockActuel;
}
public int getStockMinimum() {
return stockMinimum;
}
public void setStockMinimum(int stockMinimum) {
// geldige voorraad?
if (stockMinimum < 0)
throw new UncheckedAccessArticlesException("stockMinimum[" + stockMinimum + "] invalide");
this.stockMinimum = stockMinimum;
}
public String toString() {
return "[" + id + "," + nom + "," + prix + "," + stockActuel + ","
+ stockMinimum + "]";
}
}
|
Deze klasse biedt:
- een constructor waarmee de 5 gegevens van een artikel kunnen worden vastgelegd
- accessors, vaak getters/setters genoemd, waarmee de 5 gegevens kunnen worden gelezen en geschreven. De namen van deze methoden volgen de norm JavaBean. Het is gebruikelijk om JavaBean-objecten in de DAO-laag te gebruiken om te communiceren met de gegevens van de SGBD.
- een controle van de gegevens die in het artikel zijn ingevoerd. Bij foutieve gegevens wordt een uitzondering gegenereerd.
- een methode toString waarmee de waarde van een artikel in de vorm van een tekenreeks kan worden opgehaald. Dit is vaak handig bij het debuggen van een applicatie.
De interface [IArticlesDao] is als volgt gedefinieerd:
| package istia.st.articles.dao;
import istia.st.articles.domain.Article;
import java.util.List;
/**
* @author ST-ISTIA
*
*/
public interface IArticlesDao {
/**
* @return : liste de tous les articles
*/
public List getAllArticles();
/**
* @param unArticle :
* l'article à ajouter
*/
public int ajouteArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article à supprimer
*/
public int supprimeArticle(int idArticle);
/**
* @param unArticle :
* l'article à modifier
*/
public int modifieArticle(Article unArticle);
/**
* @param idArticle :
* id de l'article cherché
* @return : l'article trouvé ou null
*/
public Article getArticleById(int idArticle);
/**
* vide la table des articles
*/
public void clearAllArticles();
/**
*
* @param idArticle id de l'article dont on change le stock
* @param mouvement valeur à ajouter au stock (valeur signée)
*/
public int changerStockArticle(int idArticle, int mouvement);
}
|
De verschillende methoden van de interface hebben de volgende functies:
| geeft alle artikelen uit de tabel ARTICLES weer in een lijst met objecten [Article] |
| maakt de tabel ARTICLES leeg |
| haalt het object [Article] op, geïdentificeerd aan de hand van de primaire sleutel |
| maakt het mogelijk een artikel toe te voegen aan de tabel ARTICLES |
| maakt het mogelijk een artikel in de tabel [ARTICLES] te wijzigen |
| hiermee kunt u een artikel uit de tabel [ARTICLES] verwijderen |
| maakt het mogelijk de voorraad van een artikel in de tabel [ARTICLES] te wijzigen |
De interface stelt klantprogramma’s een aantal methoden ter beschikking die uitsluitend door hun handtekeningen worden gedefinieerd. De interface houdt zich niet bezig met de manier waarop deze methoden daadwerkelijk worden geïmplementeerd. Dit zorgt voor flexibiliteit in een toepassing. Het klantprogramma doet zijn aanroepen op een interface en niet op een specifieke implementatie daarvan.
De keuze voor een specifieke implementatie gebeurt via een Spring-configuratiebestand. We stellen voor om hier de interface IArticlesDao te implementeren met behulp van een open-sourceproduct genaamd SqlMap. Hiermee kunnen we alle SQL-instructies uit de Java-code verwijderen.
De implementatieklasse [ArticlesDaoSqlMap] is als volgt gedefinieerd:
| package istia.st.articles.dao;
// Imports
import com.ibatis.sqlmap.client.SqlMapClient;
import istia.st.articles.domain.Article;
import java.util.List;
public class ArticlesDaoSqlMap implements IArticlesDao {
// Velden
private SqlMapClient sqlMap;
// Constructors
public ArticlesDaoSqlMap(String sqlMapConfigFileName) { }
// Methoden
public SqlMapClient getSqlMap() {}
public void setSqlMap(SqlMapClient sqlMap) { }
public synchronized List getAllArticles() {}
public synchronized int ajouteArticle(Article unArticle) {}
public synchronized int supprimeArticle(int idArticle) {}
public synchronized int modifieArticle(Article unArticle) {}
public synchronized Article getArticleById(int idArticle) {}
public synchronized void clearAllArticles() { }
public synchronized int changerStockArticle(int idArticle, int mouvement) {}
}
|
Alle methoden voor gegevenstoegang zijn gesynchroniseerd om problemen met gelijktijdige toegang tot de gegevensbron te voorkomen. Op elk moment heeft slechts één thread toegang tot een bepaalde methode.
De klasse [ArticlesDaoSqlMap] maakt gebruik van de tool [Ibatis SqlMap]. Het voordeel van deze tool is dat hiermee de SQL-code voor gegevenstoegang uit de Java-code kan worden gehaald. Deze wordt vervolgens in een configuratiebestand geplaatst. We zullen hier later op terugkomen. Om te worden aangemaakt, heeft de klasse [ArticlesDaoSqlMap] een configuratiebestand nodig waarvan de naam als parameter wordt doorgegeven aan de constructor van de klasse. Dit configuratiebestand definieert de informatie die nodig is om:
- toegang te krijgen tot de SGBD waarin de artikelen zich bevinden
- een verbindingspool te beheren
- transacties te beheren
In ons voorbeeld heet dit bestand [sqlmap-config-firebird.xml] en definieert het de toegang tot een Firebird-database:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:D:/data/Databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Met het hierboven genoemde configuratiebestand [articles.xml] kan worden gedefinieerd hoe een instantie van de klasse [istia.st.articles.dao.Article] moet worden opgebouwd op basis van een rij uit de tabel [ARTICLES] van SGBD. Het definieert tevens de query’s SQL waarmee de laag [dao] de gegevens uit de Firebird-gegevensbron kan ophalen.
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- een alias voor de klasse istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- de mapping ORM : tabelrij ARTICLES - instantie van de klasse Artikel -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- de query SQL om alle artikelen op te halen -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- de query SQL om alle artikelen te verwijderen -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- de query SQL om een artikel in te voegen -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#naam#,#prijs#,#huidige voorraad#,#minimale voorraad#)
</statement>
<!-- de query SQL om een bepaald artikel te verwijderen -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- de aanvraag SQL om een bepaald artikel te wijzigen -->
<statement id="modifyArticle">
update ARTICLES set nom=#naam#,
prix=#prijs#,huidigeVoorraad=#huidigeVoorraad#,minimaleVoorraad=#minimaleVoorraad# where
id=#id#
</statement>
<!-- de query SQL om een bepaald artikel op te halen -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- de query SQL om de voorraad van een bepaald artikel te wijzigen -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#voorraadmutatie#
where id=#id# en stockActuel+#beweging#>=0
</statement>
</sqlMap>
|
De code van het pakket [dao] is te vinden in de bijlage.
3.3.4. Het pakket [istia.st.articles.domain]
De interface [IArticlesDomain] ontkoppelt de laag [métier] van de laag [web]. Deze laatste krijgt via deze interface toegang tot de laag [métier/domain], zonder zich zorgen te maken over de klasse die deze daadwerkelijk implementeert. De interface definieert de volgende acties voor toegang tot de bedrijfslaag:
| package istia.st.articles.domain;
// Importen
import java.util.ArrayList;
import java.util.List;
public abstract interface IArticlesDomain {
// Methoden
void acheter(Panier panier);
List getAllArticles();
Article getArticleById(int idArticle);
ArrayList getErreurs();
}
|
| levert de lijst met objecten [Article] op die aan de klant moet worden gepresenteerd |
Article getArticleById(int idArticle)
| levert het object [Article] op, geïdentificeerd door [idArticle] |
void acheter(Panier panier)
| valideert het winkelmandje van de klant door de voorraad van de gekochte artikelen met de gekochte hoeveelheid te verminderen – kan mislukken als de voorraad onvoldoende is |
| geeft de lijst met opgetreden fouten weer – leeg als er geen fouten zijn |
Hier wordt de interface [IArticlesDomain] geïmplementeerd door de volgende klasse [AchatsArticles]:
| package istia.st.articles.domain;
// Imports
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.List;
public class AchatsArticles implements IArticlesDomain {
// Velden
private IArticlesDao articlesDao;
private ArrayList erreurs;
// Constructors
public AchatsArticles(IArticlesDao articlesDao) { }
// Methoden
public ArrayList getErreurs() {}
public List getAllArticles() {}
public Article getArticleById(int id) {}
public void acheter(Panier panier) { }
}
|
Deze klasse implementeert de vier methoden van de interface [IArticlesDomain]. Ze heeft twee privévelden:
| het object voor gegevenstoegang dat door de gegevenslaag wordt geleverd |
| de lijst met eventuele fouten |
Om een instantie van de klasse aan te maken, moet het object worden opgegeven dat toegang geeft tot de gegevens van SGBD:
public AchatsArticles(IArticlesDao articlesDao)
| constructor |
De klasse [Achat] vertegenwoordigt een aankoop van de klant:
| package istia.st.articles.domain;
public class Achat {
// Velden
private Article article;
private int qte;
// Constructors
public Achat(Article article, int qte) { }
// Methoden
public double getTotal() {}
public Article getArticle() {}
public void setArticle(Article article) { }
public int getQte() {}
public void setQte() { }
public String toString() {}
}
|
De klasse [Achat] is een JavaBean met de volgende velden en methoden:
| het gekochte artikel |
| de gekochte hoeveelheid |
| geeft het aankoopbedrag weer |
| identiteitsstring van het object |
De klasse [Panier] vertegenwoordigt alle aankopen van de klant:
| package istia.st.articles.domain;
// Imports
import java.util.ArrayList;
public class Panier {
// Velden
private ArrayList achats;
// Constructors
public Panier() { }
// Methoden
public ArrayList getAchats() {}
public void ajouter(Achat unAchat) { }
public void enlever(int idAchat) { }
public double getTotal() {}
public String toString() { }
}
|
De klasse [Panier] is een JavaBean met de volgende velden en methoden:
| de lijst met aankopen van de klant – lijst met objecten van het type [Achat] |
void ajouter(Achat unAchat)
| voegt een aankoop toe aan de aankooplijst |
void enlever(int idArticle)
| verwijdert de aankoop van het artikel idArticle |
| berekent het totaalbedrag van de aankopen |
| geeft de identiteitsstring van het winkelmandje weer |
| geeft de lijst met aankopen weer |
De code van het pakket [domain] is te vinden in de bijlage.
3.3.5. Het pakket [istia.st.articles.exception]
Dit pakket bevat de klasse die de uitzondering definieert die door de laag [dao] wordt gegenereerd wanneer er een probleem optreedt bij de toegang tot de gegevensbron:
| package istia.st.articles.exception;
public class UncheckedAccessArticlesException
extends RuntimeException {
public UncheckedAccessArticlesException() {
super();
}
public UncheckedAccessArticlesException(String mesg) {
super(mesg);
}
public UncheckedAccessArticlesException(String mesg, Throwable th) {
super(mesg, th);
}
}
|
3.3.6. Tests van het model
Het model M is getest in Eclipse met de volgende configuratie:

Opmerkingen:
- in [WEB-INF/lib] bevinden zich:
- de bestanden die nodig zijn voor de tool [ibatis SqlMap], die toegang verschaft tot de Firebird-database SGBD: ibatis-*.jar
- het bestand dat nodig is voor de tool [spring]: spring.jar
- de driver JDBC van de SGBD [Firebird]: firebirdsql-full.jar
- de benodigde bestanden voor de logbestanden: log4-*.jar, commons-logging.jar
- de drie archiefbestanden van het geteste model: istia.st.articles.*.jar
- het archief dat nodig is voor de testtool [junit]
- in [WEB-INF/src] bevinden zich de configuratiebestanden die door Eclipse automatisch naar [WEB-INF/classes] worden gekopieerd:
- de configuratiebestanden van de tool [sqlmap]: sqlmap-config-firebird.xml, articles.xml
- die van de tool [spring]: spring-config-test-dao.xml, spring-config-test-domain.xml
- het configuratiebestand van de tool [log4j]: log4j.properties
- in het pakket [istia.st.articles.tests] bevinden zich de testklassen van het model
3.3.6.1. Tests van de laag [dao]
De testklasse JUnit van de laag [dao] ziet er als volgt uit. Door deze te lezen, wordt duidelijk hoe de methoden van de interface [IArticlesDao] worden gebruikt:
| package istia.st.articles.tests.dao;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.dao.Article;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test van de klasse ArticlesDaoSqlMap
public class JunitModeleDaoArticles extends TestCase {
// een instantie van de geteste klasse
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// haalt een instantie op voor gegevenstoegang
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
public void testGetAllArticles() {
// geeft de artikelen weer
listArticles();
}
public void testClearAllArticles() {
// maakt de artikeltabel leeg
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
}
public void testAjouteArticle() {
// verwijdert de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// leest de tabel ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
//geeft deze weer
listArticles();
}
public void testSupprimeArticle() {
// verwijdert de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// leest de tabel ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// verwijdering
articlesDao.supprimeArticle(4);
// leest de tabel ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(1, articles.size());
// toont de tabel
listArticles();
}
public void testModifieArticle() {
// verwijdering van de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// leest de tabel ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
// wijziging
articlesDao.modifieArticle(new Article(4, "article4", 44, 44, 44));
// getById
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getPrix(), 44, 1e-6);
// toont de tabel
listArticles();
}
public void testGetArticleById() {
// verwijdering van de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// leest de tabel ARTICLES
articles = articlesDao.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
private void listArticles() {
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
// de gelezen artikelen worden weergegeven
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
public void testChangerStockArticle() throws InterruptedException {
// verwijdering van de inhoud van ARTICLES
articlesDao.clearAllArticles();
// invoegen
int nbArticles = articlesDao.ajouteArticle(new Article(3, "article3",
30, 101, 3));
assertEquals(nbArticles, 1);
nbArticles = articlesDao.ajouteArticle(new Article(4, "article4", 40,
40, 4));
assertEquals(nbArticles, 1);
// aanmaken van 100 threads voor het bijwerken van de voorraad van artikel 3
Thread[] taches = new Thread[100];
for (int i = 0; i < taches.length; i++) {
taches[i] = new ThreadMajStock("thread-" + i, articlesDao);
taches[i].start();
}
// wachten tot de threads zijn voltooid
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// artikel 3 ophalen en de voorraad ervan controleren
Article unArticle = articlesDao.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
assertEquals(1, unArticle.getStockActuel());
// voorraadartikel 4 aanpassen
boolean erreur = false;
int nbLignes = articlesDao.changerStockArticle(4, -100);
assertEquals(0, nbLignes);
// de tabel weergeven
listArticles();
}
}
|
Opmerkingen:
- de testklasse slaat, via de methode setUp, een instantie van de te testen klasse op:
| // een instantie van de geteste klasse
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// haalt een instantie voor gegevenstoegang op
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-dao.xml"))).getBean("articlesDao");
}
|
- Het te testen object wordt geleverd door [Spring]. Hierboven wordt de Spring-bean met de naam [articlesDao] opgevraagd. Deze bean is gedefinieerd in het Spring-configuratiebestand [spring-config-test-dao.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
</beans>
|
Hierboven zien we dat de bean [articlesDao] een instantie is van de klasse [istia.st.articles.dao.ArticlesDaoSqlMap]. Deze klasse heeft een constructor die als parameter de naam van het configuratiebestand van de tool [SqlMap] verwacht. Deze naam wordt hier opgegeven. Het is [sqlmap-config-firebird.xml]. Deze is al eerder beschreven. Hij bevat alle benodigde informatie om toegang te krijgen tot de gegevens van SGBD.
De methode [testChangerStockArticle] maakt 100 threads aan die de voorraad van een bepaald artikel moeten verlagen. Het doel is hier om gelijktijdige toegang tot SGBD te testen. Omdat de methode [changerStockArticle] van de klasse [istia.st.articles.dao.ArticlesDaoSqlMap] is gesynchroniseerd, slaagt deze test. Als de synchronisatie wordt verwijderd, slaagt de test niet meer. De klasse die verantwoordelijk is voor het bijwerken van de voorraad is de volgende:
| package istia.st.articles.tests;
import istia.st.articles.dao.IArticlesDao;
public class ThreadMajStock extends Thread {
/**
* nom du thread
*/
private String name;
/**
* objet d'accès aux données
*/
private IArticlesDao articlesDao;
/**
*
* @param name
* le nom du thread afin de l'identifier
* @param articlesDao
* l'objet d'accès aux données du sgbd
*/
public ThreadMajStock(String name, IArticlesDao articlesDao) {
this.name = name;
this.articlesDao = articlesDao;
}
/**
* décrémente le stock de l'article 3 d'une unité fait un suivi écran des
* opérations
*/
public void run() {
// opvolging
System.out.println(name + " lancé");
// wijziging voorraad artikel 3
articlesDao.changerStockArticle(3, -1);
// opvolging
System.out.println(name + " terminé");
}
}
|
- de bovenstaande klasse vermindert de voorraad van artikel nr. 3 met 1
3.3.6.2. Tests van de laag [domain]
De testklasse JUnit van de laag [domain] is als volgt:
| package istia.st.articles.tests.domain;
import java.util.List;
import junit.framework.TestCase;
import istia.st.articles.dao.Article;
import istia.st.articles.dao.IArticlesDao;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
// test van de klasse ArticlesDaoSqlMap
public class JunitModeleDomainArticles extends TestCase {
// een instantie van de domeintoegangsklasse
private IArticlesDomain articlesDomain;
// een instantie van de klasse voor gegevenstoegang
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// haalt een instantie van de domeintoegang op
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// haalt een instantie voor de toegang tot de gegevens op
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
// ophalen van een specifiek artikel
public void testGetArticleById() {
// verwijderen van de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
articlesDao.ajouteArticle(new Article(3, "article3", 30, 30, 3));
articlesDao.ajouteArticle(new Article(4, "article4", 40, 40, 4));
// leest de tabel ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// getById
Article unArticle = articlesDomain.getArticleById(3);
assertEquals(unArticle.getNom(), "article3");
unArticle = articlesDao.getArticleById(4);
assertEquals(unArticle.getNom(), "article4");
}
// schermweergave
private void listArticles() {
// leest de tabel ARTICLES
List articles = articlesDomain.getAllArticles();
// de gelezen artikelen worden weergegeven
for (int i = 0; i < articles.size(); i++) {
System.out.println(((Article) articles.get(i)).toString());
}
}
// artikelen kopen
public void testAchatPanier(){
// verwijderen van de inhoud van ARTICLES
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// leest de tabel ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// aanmaken van een winkelmandje met twee aankopen
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// controles
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// winkelwagen bevestigen
articlesDomain.acheter(panier);
// controles
assertEquals(0,articlesDomain.getErreurs().size());
assertEquals(0,panier.getAchats().size());
// artikel nr. 3 zoeken
article3=articlesDomain.getArticleById(3);
assertEquals(20,article3.getStockActuel());
// artikel nr. 4 zoeken
article4=articlesDomain.getArticleById(4);
assertEquals(30,article4.getStockActuel());
// nieuw winkelmandje
panier.ajouter(new Achat(article3,100));
// winkelwagen bevestigen
articlesDomain.acheter(panier);
// controle - er is te veel gekocht
// er moet een fout zijn opgetreden
assertEquals(1,articlesDomain.getErreurs().size());
// artikel nr. 3 zoeken
article3=articlesDomain.getArticleById(3);
// de voorraad is waarschijnlijk niet veranderd
assertEquals(20,article3.getStockActuel());
}
// aankopen verwijderen
public void testRetirerAchats(){
// de inhoud van ARTICLES verwijderen
articlesDao.clearAllArticles();
// leest de tabel ARTICLES
List articles = articlesDao.getAllArticles();
assertEquals(0, articles.size());
// invoegen
Article article3=new Article(3, "article3", 30, 30, 3);
articlesDao.ajouteArticle(article3);
Article article4=new Article(4, "article4", 40, 40, 4);
articlesDao.ajouteArticle(article4);
// leest de tabel ARTICLES
articles = articlesDomain.getAllArticles();
assertEquals(2, articles.size());
// aanmaken van een winkelmandje met twee aankopen
Panier panier=new Panier();
panier.ajouter(new Achat(article3,10));
panier.ajouter(new Achat(article4,10));
// controles
assertEquals(700.0,panier.getTotal(),1e-6);
assertEquals(2,panier.getAchats().size());
// een reeds gekocht artikel toevoegen
panier.ajouter(new Achat(article3,10));
// controles
// het totaal moet worden aangepast naar 1000
assertEquals(1000.0,panier.getTotal(),1e-6);
// nog steeds 2 artikelen in het winkelmandje
assertEquals(2,panier.getAchats().size());
// het aantal van artikel 3 moet worden gewijzigd naar 20
Achat achat=(Achat)panier.getAchats().get(0);
assertEquals(20,achat.getQte());
// artikel 3 wordt uit het winkelmandje verwijderd
panier.enlever(3);
// controles
// het totaal moet zijn aangepast naar 400
assertEquals(400.0,panier.getTotal(),1e-6);
// slechts 1 artikel in het winkelmandje
assertEquals(1,panier.getAchats().size());
// dit moet artikel nr. 4 zijn
assertEquals(4,((Achat)panier.getAchats().get(0)).getArticle().getId());
}
}
|
Opmerkingen:
- de testklasse slaat, via haar methode setUp, een instantie van de te testen klasse op, evenals een instantie van de klasse voor gegevenstoegang. Dit laatste punt is omstreden. De testklasse zou in theorie geen toegang hoeven te hebben tot de laag [dao], die ze niet eens zou mogen kennen. Hier hebben we deze „ethiek“ genegeerd, die ons, als we ons eraan hadden gehouden, zou hebben gedwongen om nieuwe methoden te creëren in onze interface [IArticlesDomain].
| // een instantie van de domeintoegangsklasse
private IArticlesDomain articlesDomain;
// een instantie van de klasse voor gegevenstoegang
private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// haalt een instantie voor domeintoegang op
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
// haalt een instantie van de klasse voor gegevenstoegang op
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
}
|
- Het te testen object wordt geleverd door [Spring]. Hierboven wordt de Spring-bean met de naam [articlesDomain] opgevraagd. Deze bean is gedefinieerd in het Spring-configuratiebestand [spring-config-test-domain.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- de bedrijfsklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Hierboven zien we dat de bean [articlesDomain] een instantie is van de klasse [istia.st.articles.domain.AchatsArticles]. Deze klasse heeft een constructor die als parameter een object verwacht dat toegang geeft tot de laag [dao] van het type [IArticlesDao]. Hier geeft het configuratiebestand aan dat dit object de bean met de naam [articlesDao] is. Dit dwingt Spring om deze bean te instantiëren. Het instantiëren van de bean [articlesDao] is eerder uitgelegd. Uiteindelijk zijn er dus twee beans geïnstantieerd:
- [articlesDao] van het type [istia.st.articles.dao.ArticlesDaoSqlMap]
- [articlesDomain] van het type [ istia.st.articles.domain.AchatsArticles]
Deze twee instantiaties worden veroorzaakt door de eerste aanroep van Spring:
| // haalt een domeintoegangsinstantie op
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource("spring-config-test-domain.xml")))
.getBean("articlesDomain");
|
Vervolgens wordt de bean [articlesDomain] opgehaald. Bij de tweede aanroep van Spring:
| // haalt een instantie voor gegevenstoegang op
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"spring-config-test-domain.xml"))).getBean("articlesDao");
|
[Spring] geeft alleen een verwijzing naar de bean [articlesDao], die al bij de vorige aanroep is aangemaakt. Dit is het principe van de singleton. Als we een bean aanvragen bij Spring, maakt Spring er een instantie van als deze nog niet bestaat; anders geeft Spring een verwijzing naar de bestaande bean.
3.4. Drielaagse webapplicatie MVC
Vervolgens willen we de volgende webapplicatie met drie lagen bouwen:
De applicatie krijgt een architectuur MVC. Het model M is geschreven en getest. Dit is het model dat eerder is beschreven. Het wordt ons geleverd in drie archieven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception]. We moeten de controller C en de views V schrijven.
We bekijken eerst een klassieke methode, waarbij:
- de controller C wordt verzorgd door één enkele servlet
- de weergaven V worden verzorgd door JSP-pagina’s
3.5. MVC-architectuur op basis van een controller-servlet en JSP-pagina’s
De architectuur MVC van de applicatie ziet er als volgt uit:
| de businessklassen, de klassen voor gegevenstoegang en de database |
| de pagina's JSP |
| de servlet voor de verwerking van verzoeken van klanten |
3.5.1. Het model
Dit is eerder al voorgesteld. Het bestaat uit de Java-archieven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.5.2. De weergaven
De weergaven komen overeen met die welke aan het begin van dit document zijn gepresenteerd:
| liste.jsp | De weergaven zijn verzameld in de map [vues] van de applicatie  |
| infos.jsp |
| panier.jsp |
| paniervide.jsp |
| erreurs.jsp |
3.5.3. De controller
De controller bestaat uit één enkele servlet met de naam [WebArticles]. Deze verwerkt de verschillende verzoeken van de clients. Deze verzoeken worden aangeduid door de aanwezigheid van de parameter [action] in het verzoek HTTP van de client:
| betekenis | actie van de controller | mogelijke antwoorden |
| de klant wil de lijst met artikelen | - vraagt de lijst met artikelen aan de bedrijfslaag | - [LISTE] - [ERREURS] |
| de klant vraagt om informatie over een van de artikelen die in de weergave worden getoond [LISTE] | - vraagt het artikel op bij de bedrijfslaag | - [INFOS] - [ERREURS] |
| de klant koopt een artikel | - vraagt het artikel op bij de bedrijfslaag en voegt het toe aan het winkelmandje van de klant | - [INFOS] bij een fout in de hoeveelheid - [LISTE] indien geen fout |
| de klant wil een aankoop uit zijn winkelmandje verwijderen | - haalt het winkelmandje op uit de sessie en wijzigt het | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| de klant wil zijn winkelwagen | - haalt het winkelmandje op uit de sessie | - [PANIER] - [PANIERVIDE] - [ERREURS] |
| de klant heeft zijn aankopen afgerond en gaat over naar de betaalfase | - de voorraad van de gekochte artikelen - verwijder uit het winkelmandje van de klant de artikelen waarvan de aankoop is bevestigd | - [LISTE] - [ERREURS] |
3.5.4. Configuratie van de applicatie
We zullen de applicatie zo configureren dat deze zo flexibel mogelijk is ten aanzien van wijzigingen zoals:
- het wijzigen van de URL's van de verschillende weergaven
- het wijzigen van de klassen die de interfaces [IArticlesDao] en [IArticlesDomain] implementeren
- wijzigingen in SGBD, de database of de artikeltabel
3.5.5. Wijzigingen in de URL's
De namen van de URL's van de weergaven worden samen met enkele andere parameters in het configuratiebestand [web.xml] van de applicatie geplaatst:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
<init-param>
<param-name>urlMain</param-name>
<param-value>/main</param-value>
</init-param>
<init-param>
<param-name>urlErreurs</param-name>
<param-value>/vues/erreurs.jsp</param-value>
</init-param>
<init-param>
<param-name>urlListe</param-name>
<param-value>/vues/liste.jsp</param-value>
</init-param>
<init-param>
<param-name>urlInfos</param-name>
<param-value>/vues/infos.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanier</param-name>
<param-value>/vues/panier.jsp</param-value>
</init-param>
<init-param>
<param-name>urlPanierVide</param-name>
<param-value>/vues/paniervide.jsp</param-value>
</init-param>
<init-param>
<param-name>urlDebug</param-name>
<param-value>/vues/debug.jsp</param-value>
</init-param>
</servlet>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
</web-app>
|
In [web.xml] staan
- de URL's van de verschillende weergaven van de applicatie
- de naam [springConfigFileName] van het Spring-configuratiebestand waarmee de singleton-objecten voor toegang tot de business- en DAO-lagen kunnen worden aangemaakt
- de weergave [/vues/index.jsp] die wordt weergegeven wanneer de door de client opgevraagde URL /<context> is, waarbij <context> de context van de applicatie is
3.5.6. De wijziging van de implementatieklassen van de interfaces
In de geest van drielaagse architecturen moeten de lagen onderling gescheiden zijn. Deze scheiding wordt als volgt bereikt:
- de lagen communiceren onderling via interfaces en niet via concrete klassen
- de code van een laag maakt nooit zelf een instantie van de klasse van een andere laag om deze te gebruiken. Ze vraagt simpelweg aan een extern hulpmiddel, in dit geval [Spring], om een implementatie-instantie van de interface van de laag die ze wil gebruiken. Hiervoor weten we dat de laag de naam van de implementatieklasse niet hoeft te kennen, maar alleen de naam van de Spring-bean waarnaar zij een verwijzing wil.
In onze applicatie zou het Spring-configuratiebestand er als volgt uit kunnen zien:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- de businessklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
</beans>
|
Om toegang te krijgen tot de laag [métier], kan een klasse uit de laag [Interface utilisateur, UI] de bean [articlesDomain] opvragen. Spring zal dan een object van het type [istia.st.articles.domain.AchatsArticles] instantiëren. Voor deze instantiatie heeft het een bean van het type [articlesDao] nodig, dat wil zeggen een object van het type [istia.st.articles.dao.ArticlesDaoSqlMap]. Spring zal vervolgens een dergelijk object instantiëren. Deze instantiatie vindt plaats op basis van de informatie in het bestand [sqlmap-config-firebird.xml], het configuratiebestand voor de gegevenstoegang via SqlMap. Aan het einde van de bewerking beschikt de klasse [UI], die de bean [articlesDomain] heeft aangevraagd, over de volledige keten die haar verbindt met de gegevens van SGBD:
3.5.7. Wijzigingen met betrekking tot SGBD of de database
De onafhankelijkheid van de webapplicatie ten opzichte van wijzigingen met betrekking tot SGBD of de database wordt hier gewaarborgd door de configuratiebestanden van SqlMap. Er zijn er twee:
- het bestand [sql-map-config-firebird.xml]
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<transactionManager type="JDBC">
<dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.firebirdsql.jdbc.FBDriver"/>
<property name="JDBC.ConnectionURL"
value="jdbc:firebirdsql:localhost/3050:d:/data/databases/firebird/dbarticles.gdb"/>
<property name="JDBC.Username" value="sysdba"/>
<property name="JDBC.Password" value="masterkey"/>
<property name="JDBC.DefaultAutoCommit" value="true"/>
</dataSource>
</transactionManager>
<sqlMap resource="articles.xml"/>
</sqlMapConfig>
|
Dit bestand verwijst hier naar een Firebird-database. U hoeft alleen de naam van de driver JDBC te wijzigen om met een andere driver, bijvoorbeeld SGBD, te werken.
- het bestand [articles.xml], dat de verschillende instructies SQL bevat die nodig zijn voor de toepassing:
| <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
"http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Articles">
<!-- een alias voor de klasse istia.st.articles.dao.Article -->
<typeAlias alias="article" type="istia.st.articles.dao.Article"/>
<!-- de mapping ORM : tabelrij ARTICLES - instantie van de klasse Artikel -->
<resultMap id="article" class="article">
<result property="id" column="ID"/>
<result property="nom" column="NOM"/>
<result property="prix" column="PRIX"/>
<result property="stockActuel" column="STOCKACTUEL"/>
<result property="stockMinimum" column="STOCKMINIMUM"/>
</resultMap>
<!-- de query SQL om alle artikelen op te halen -->
<statement id="getAllArticles" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum from ARTICLES
</statement>
<!-- de query SQL om alle artikelen te verwijderen -->
<statement id="clearAllArticles">delete from ARTICLES</statement>
<!-- de query SQL om een artikel in te voegen -->
<statement id="insertArticle">
insert into ARTICLES (id, nom, prix,
stockactuel, stockminimum) values
(#id#,#naam#,#prijs#,#huidige voorraad#,#minimale voorraad#)
</statement>
<!-- de query SQL om een bepaald artikel te verwijderen -->
<statement id="deleteArticle">delete FROM ARTICLES where id=#id#</statement>
<!-- de aanvraag SQL om een bepaald artikel te wijzigen -->
<statement id="modifyArticle">
update ARTICLES set nom=#naam#,
prix=#prijs#,huidigeVoorraad=#huidigeVoorraad#,minimaleVoorraad=#minimaleVoorraad# where
id=#id#
</statement>
<!-- de query SQL om een bepaald artikel op te halen -->
<statement id="getArticleById" resultMap="article">
select id, nom, prix,
stockactuel, stockminimum FROM ARTICLES where id=#id#
</statement>
<!-- de query SQL om de voorraad van een bepaald artikel te wijzigen -->
<statement id="changerStockArticle">
update ARTICLES set
stockActuel=stockActuel+#voorraadmutatie#
where id=#id# en stockActuel+#beweging#>=0
</statement>
</sqlMap>
|
Als de namen van de artikeltabel of de kolommen zouden veranderen, zouden we de query's in dit configuratiebestand moeten herschrijven zonder de Java-code te hoeven wijzigen. Dit zou ook het geval zijn als een query om prestatieredenen zou worden vervangen door een opgeslagen procedure.
3.5.8. De algemene architectuur van de applicatie [webarticles]
Een Java-webapplicatie is een puzzel met talrijke onderdelen. Door er een architectuur aan toe te voegen, neemt het aantal onderdelen doorgaans toe. De structuur van de applicatie [webarticles] onder [eclipse] is als volgt:
structure générale - on voit ci-dessous les
archives Java utilisées par le projet
Eclipse.
spring : pour Spring
ibatis : pour SqlMap
log4j, commons-logging : pour les logs
de Spring et Sqlmap
firebird : pour le SGBD firebird
mysql : pour le SGBD MySQL
jstl, standard : pour la bibliothèque
de balises JSTL
|
le dossier des sources java : contient le code Java
ainsi que les fichiers de configuration
spring et sqlmap. Eclipse recopie
automatiquement ces fichiers
dans [WEB-INF/classes].
C'est là que l'application les trouvera.
|
 |  |
le dossier [WEB-INF] de l'application : contient le
descripteur [web.xml] de l'application ainsi que le
fichiers de définition de la bibliothèques JSTL
| |
 |  |
3.5.9. De weergaven JSP
De weergaven JSP maken gebruik van de tagbibliotheek JSTL.
3.5.9.1. entete.jsp
Om de verschillende weergaven een zekere uniformiteit te geven, zullen deze dezelfde koptekst delen, namelijk die waarin de naam van de applicatie en het menu worden weergegeven:
Het menu is dynamisch en wordt bepaald door de controller. Deze voegt in de verzoek die naar de pagina JSP wordt verzonden, een sleutelattribuut "actions" toe met als bijbehorende waarde een array van het type Hastable[]. Elk element van deze array is een woordenboek dat bedoeld is om een optie in het koptekstmenu te genereren. Elk woordenboek heeft twee sleutels:
- href: de URL die bij de menuoptie hoort
- lien: de tekst van het menu
De andere weergaven van de applicatie zullen de door [entete.jsp] gedefinieerde koptekst gebruiken met behulp van de volgende tag JSP:
<jsp:include page="entete.jsp"/>
Bij uitvoering zorgt deze tag ervoor dat de code van de pagina JSP, waarin deze tag is opgenomen, de code van de pagina [entete.jsp] bevat. Aangezien de URL van de pagina een relatieve URL is (zonder /), wordt de pagina [entete.jsp] gezocht in dezelfde map als de pagina met de tag <jsp:include>.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
3.5.9.2. liste.jsp
Deze weergave toont de lijst met artikelen die te koop zijn:
Deze wordt weergegeven na een verzoek /main?action=lijst of /main?action=winkelwagenbevestiging. De elementen van het verzoek aan de controller zijn als volgt:
| object Hashtable[] - de array met menuopties |
| ArrayList objecten van het type [Article] |
| String-object - bericht dat onderaan de pagina moet worden weergegeven |
Elke link [Infos] in de array HTML met artikelen heeft een URL in de vorm [?action=infos&id=ID], waarbij ID het id-veld is van het weergegeven artikel.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="?action=infos&id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
3.5.9.3. infos.jsp
Deze weergave toont informatie over een artikel en maakt het ook mogelijk om het te kopen:

Deze wordt weergegeven na een verzoek /main?action=infos&id=ID of een verzoek /main?action=achat&id=ID wanneer de gekochte hoeveelheid onjuist is. De elementen van het verzoek van de controller zijn als volgt:
| object Hashtable[] - de lijst met menuopties |
| object van het type [Article] – weer te geven artikel |
| String-object - bericht dat moet worden weergegeven bij een fout in de hoeveelheid |
| String-object - waarde die in het invoerveld moet worden weergegeven [Qte] |
De velden [msg] en [qte] worden gebruikt bij een invoerfout in de hoeveelheid:

Deze pagina bevat een formulier dat wordt verzonden via de knop [Acheter]. De doel-URL van POST is [?action=achat&id=ID], waarbij ID de ID is van het gekochte artikel.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="?action=achat&id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
3.5.9.4. panier.jsp
Dit scherm toont de inhoud van het winkelmandje:

Deze wordt weergegeven na een verzoek /main?action=winkelwagen of /main?action=aankoopverwijderen&id=ID. De elementen van het verzoek aan de controller zijn als volgt:
| object Hashtable[] - de array met menuopties |
| object van het type [Panier] – het weer te geven winkelmandje |
Elke link [Retirer] in de tabel HTML met de aankopen in het winkelmandje heeft een URL in de vorm [?action=retirerachat&id=ID], waarbij ID het veld [id] is van hetartikel dat uit het winkelmandje moet worden verwijderd.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="?action=retirerachat&id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
3.5.9.5. paniervide.jsp
Dit scherm geeft aan dat het winkelmandje leeg is:

Deze wordt weergegeven na een verzoek /main?action=winkelwagen of /main?action=aankoopverwijderen&id=ID. De elementen van het verzoek aan de controller zijn als volgt:
| object Hashtable[] - de array met menuopties |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
3.5.9.6. erreurs.jsp
Dit scherm wordt weergegeven bij fouten:

Deze wordt weergegeven na elke aanvraag die tot een fout leidt, behalve bij een aankoopactie met een onjuiste hoeveelheid, die wordt afgehandeld door de weergave [INFOS]. De elementen van de aanvraag van de controller zijn als volgt:
| object Hashtable[] – de lijst met menuopties |
| ArrayList met String-objecten die de weer te geven foutmeldingen vertegenwoordigen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
3.5.9.7. index.jsp
Deze pagina is gedefinieerd als startpagina van de applicatie in het bestand [web.xml] van de applicatie:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
De weergave [index.jsp] leidt de client gewoon door naar het startpunt van de applicatie:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
|
3.5.10. De controller
Nu moeten we nog de kern van onze webapplicatie schrijven: de controller. Deze heeft als taak:
- het verzoek van de klant ophalen,
- de door de klant gevraagde actie te verwerken met behulp van de businessklassen,
- de juiste weergave als antwoord terugsturen.
3.5.10.1. Initialisatie van de controller
Wanneer de controllerklasse door de servletserver wordt geladen, wordt de methode [init] uitgevoerd. Dit gebeurt slechts één keer. Zodra de controller in het geheugen is geladen, blijft deze daar aanwezig en verwerkt hij de verzoeken van de verschillende clients. Voor elke client wordt een afzonderlijke thread aangemaakt, waardoor de methoden van de controller gelijktijdig door verschillende threads worden uitgevoerd. We herinneren eraan dat de controller om deze reden geen velden mag hebben die door zijn methoden kunnen worden gewijzigd. Zijn velden moeten alleen-lezen zijn. Ze worden geïnitialiseerd door de methode [init], wat de belangrijkste taak van deze methode is. Deze methode heeft namelijk de bijzonderheid dat ze slechts één keer door één enkele thread wordt uitgevoerd. Er zijn dus geen problemen met gelijktijdige toegang tot de velden van de controller in deze methode. De methode [init] heeft als doel de objecten te initialiseren die nodig zijn voor de webapplicatie en die door alle client-threads op alleen-lezenbasis worden gedeeld. Deze gedeelde objecten kunnen op twee plaatsen worden geplaatst:
- de privévelden van de controller
- de uitvoeringscontext van de applicatie (ServletContext)
De methode [init] van de applicatie [webarticles] voert de volgende acties uit:
- controleren of het bestand [web.xml] de parameters bevat die nodig zijn voor de correcte werking van de applicatie. Deze zijn beschreven in paragraaf 3.5.5.
- vult een privéveld [ArrayList erreurs] met de lijst van eventuele fouten. Deze lijst is leeg als er geen fouten zijn, maar bestaat niettemin.
- Als er fouten zijn opgetreden, stopt de methode [init] hier. Anders maakt deze een object van het type [IArticlesDomain] aan, dat het bedrijfsobject zal zijn dat de controller voor zijn doeleinden zal gebruiken. Zoals uitgelegd in 3.5.6, vraagt de controller het Spring-framework om de bean die hij nodig heeft. Deze instantiëring kan verschillende fouten opleveren. Als dat het geval is, worden deze fouten opnieuw opgeslagen in het veld [erreurs] van de controller.
3.5.10.2. Methoden doGet, doPost
Deze twee methoden verwerken de verzoeken HTTP, GET en POST van de clients. Deze worden zonder onderscheid verwerkt. De methode [doPost] kan dus doorverwijzen naar de methode [doGet] of omgekeerd. Het verzoek van de klant wordt als volgt verwerkt:
- het veld [erreurs] wordt gecontroleerd. Als dit veld niet leeg is, betekent dit dat er fouten zijn opgetreden tijdens het opstarten van de applicatie en dat deze niet kan functioneren. In dat geval wordt als antwoord de weergave [ERREURS] teruggestuurd.
- de parameter [action] van het verzoek wordt opgehaald en gecontroleerd. Als deze niet overeenkomt met een bekende actie, wordt de weergave [ERREURS] verzonden met een passend foutbericht.
- Als de parameter [action] geldig is, wordt het verzoek van de klant doorgegeven aan een actiespecifieke procedure voor verwerking. De procedure die de actie [uneAction] verwerkt, heeft de volgende handtekening:
| /**
* @param request la requête du client
* @param response la réponse au client
* @throws IOException
* @throws ServletException
*/
private void doUneAction(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
|
3.5.10.3. Verwerking van de verschillende acties
De methoden die de verschillende mogelijke acties van de applicatie verwerken, zijn de volgende:
methode | verzoek | verwerking | mogelijke reacties |
| GET /main?action=lijst | - de lijst met artikelen opvragen bij de businessklasse - deze weergeven | [LISTE] of [ERREURS] |
| GET /main?action=infos&id=ID | - het artikel met id=ID opvragen bij de businessklasse - het weergeven | [INFOS] of [ERREURS] |
| POST /main?action=achat&id=ID - de gekochte hoeveelheid maakt deel uit van de verzonden parameters | - vraag het artikel met id=ID op bij de businessklasse - voeg het toe aan het winkelmandje in de klantsessie | [LISTE] of [INFOS] of [ERREURS] |
| GET /main?action=retirerachat&id=ID | - het artikel met id=ID verwijderen uit de aankooplijst van het winkelmandje van de klantsessie | [PANIER] |
| GET /main?action=winkelwagen | - het winkelmandje van de klantsessie | [PANIER] of [PANIERVIDE] |
| GET /main?action=winkelwagenbevestiging | - de voorraad van alle artikelen die zich in het winkelmandje van sessie van de klant | [LISTE] of [ERREURS] |
3.5.10.4. De code
| package istia.st.articles.web;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import istia.st.articles.domain.Achat;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST
*
*/
public class WebArticles extends HttpServlet {
// privévelden
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String URL_MAIN = "urlMain";
private final String URL_ERREURS = "urlErreurs";
private final String URL_LISTE = "urlListe";
private final String URL_INFOS = "urlInfos";
private final String URL_PANIER = "urlPanier";
private final String URL_PANIER_VIDE = "urlPanierVide";
private final String URL_DEBUG = "urlDebug";
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters =
{
URL_MAIN,
URL_ERREURS,
URL_LISTE,
URL_INFOS,
URL_PANIER,
URL_PANIER_VIDE,
URL_DEBUG,
SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste";
private final String ACTION_PANIER = "panier";
private final String ACTION_ACHAT = "achat";
private final String ACTION_INFOS = "infos";
private final String ACTION_RETIRER_ACHAT = "retirerachat";
private final String ACTION_VALIDATION_PANIER = "validationpanier";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
public void init() {
// de initialisatieparameters van de servlet worden opgehaald
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// de fout wordt opgeslagen
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// fouten?
if (erreurs.size() != 0) {
return;
}
// er wordt een object IArticlesDomain aangemaakt voor toegang tot de bedrijfslaag
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// de fout wordt opgeslagen
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// er worden bepaalde URL's van de applicatie opgeslagen
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// het is klaar
return;
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// we controleren hoe de initialisatie van de servlet is verlopen
if (erreurs.size() != 0) {
// hebben we de URL van de foutpagina?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
// de actie wordt verwerkt
String action = request.getParameter("action");
if (action == null) {
// lijst met artikelen
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// lijst met artikelen
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// informatie over een artikel
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// een artikel kopen
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// winkelwagen weergeven
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// een artikel uit het winkelmandje verwijderen
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// Winkelwagen bevestigen
doValidationPanier(request, response);
return;
}
// onbekende actie
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// de foutpagina wordt weergegeven
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// einde
return;
}
private void doValidationPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// de koper heeft zijn winkelmandje bevestigd
Panier panier = (Panier) request.getSession().getAttribute("panier");
// dit winkelmandje wordt bevestigd
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// de fouten worden opgehaald
ArrayList erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionPanier });
afficheErreurs(request, response, erreurs);
return;
}
// de lijst met artikelen wordt weergegeven
request.setAttribute("message", "Votre panier a été validé");
doListe(request, response);
// einde
return;
}
private void doRetirerAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// een artikel uit het winkelmandje verwijderen
try {
Panier panier =
(Panier) request.getSession().getAttribute("panier");
String strIdAchat = request.getParameter("id");
panier.enlever(Integer.parseInt(strIdAchat));
} catch (NumberFormatException ignored) {
} catch (NullPointerException ignored) {
}
// het winkelmandje weergeven
doPanier(request, response);
}
private void doPanier(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// het winkelmandje weergeven
Panier panier = (Panier) request.getSession().getAttribute("panier");
// is het winkelmandje leeg?
if (panier == null || panier.getAchats().size() == 0) {
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER_VIDE))
.forward(request, response);
// einde
return;
}
// er zit iets in het winkelmandje
request.setAttribute("panier", panier);
request.setAttribute(
"actions",
new Hashtable[] { hActionListe, hActionValidationPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_PANIER))
.forward(request, response);
// einde
return;
}
private void doAchat(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// een artikel kopen
// hoeveelheid ophalen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// verkeerde hoeveelheid
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
String url =
config.getInitParameter(URL_MAIN)
+ "?action=infos&id="
+ request.getParameter("id");
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// einde
return;
}
// de sessie van de klant wordt opgehaald
HttpSession session = request.getSession();
// de aankoop wordt aangemaakt
Article article = (Article) session.getAttribute("article");
Achat achat = new Achat(article, qté);
// de aankoop wordt aan het winkelmandje van de klant toegevoegd
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// terug naar de artikellijst
String url = config.getInitParameter(URL_MAIN) + "?action=liste";
getServletContext().getRequestDispatcher(url).forward(
request,
response);
// einde
return;
}
private void afficheDebugInfos(
HttpServletRequest request,
HttpServletResponse response,
ArrayList infos)
throws ServletException, IOException {
// de artikellijst wordt weergegeven
request.setAttribute("infos", infos);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_DEBUG))
.forward(request, response);
// einde
return;
}
public void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// idem get
doGet(request, response);
}
private void doInfos(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de gevraagde id wordt opgehaald
String strId = request.getParameter("id");
// iets?
if (strId == null) {
// niet normaal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// strId wordt omgezet in een geheel getal
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// niet normaal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// het artikel met de sleutel-ID wordt opgevraagd
Article article = null;
try {
article=articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
if (article == null) {
// niet normaal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
return;
}
// het artikel wordt in de sessie geplaatst
request.getSession().setAttribute("article", article);
// de informatiepagina wordt weergegeven
request.setAttribute("actions", new Hashtable[] { hActionListe });
/ / request.setAttribute("urlMain",config.getInitParameter(URL_MAIN));
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_INFOS))
.forward(request, response);
// einde
return;
}
private void afficheErreurs(
HttpServletRequest request,
HttpServletResponse response,
ArrayList erreurs)
throws ServletException, IOException {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de artikellijst wordt opgevraagd
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// fouten?
if (erreurs.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
// de lijst met artikelen weergeven
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// einde
return;
}
/**
* suivi console pour débogage
* @param message : le message à afficher
*/
private void affiche(String message) {
System.out.println(message);
}
}
|
We laten de lezer de tijd nemen om deze code te lezen en te begrijpen. We hopen dat de opmerkingen daarbij zullen helpen.
3.5.10.5. Testen van de applicatie
Laten we enkele schermafbeeldingen van tests bekijken. Allereerst de startpagina van de applicatie:

De opgevraagde URL was in werkelijkheid [http://localhost:8080/webarticles]. De lezer zal zien dat we in het bestand [web.xml] een startpagina voor de applicatie definiëren:
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
De weergave [index.jsp] is als volgt gedefinieerd:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main?action=liste"/>
Er heeft dus een omleiding plaatsgevonden naar de URL [http://localhost:8080/webarticles/main?action=liste], zoals te zien is aan de URL in de browser op de schermafbeelding. De URL [/main?action=liste] is dus opgevraagd. Nog steeds in [web.xml] is de URL /main gekoppeld aan de servlet [webarticles]:
<servlet-mapping>
<servlet-name>webarticles</servlet-name>
<url-pattern>/main</url-pattern>
</servlet-mapping>
Ook in [web.xml] is de servlet [webarticles] gekoppeld aan de servlet [ istia.st.articles.web.WebArticles]:
<servlet-name>webarticles</servlet-name>
<servlet-class>istia.st.articles.web.WebArticles</servlet-class>
De servlet [ istia.st.articles.web.WebArticles] wordt dus door de Tomcat-servletcontainer geladen als dat nog niet was gebeurd, en de methode [init] wordt uitgevoerd:
| public void init() {
// de initialisatieparameters van de servlet worden opgehaald
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// de fout wordt opgeslagen
erreurs.add(
"Paramètre ["
+ parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// fouten?
if (erreurs.size() != 0) {
return;
}
// er wordt een object IArticlesDomain aangemaakt voor toegang tot de bedrijfslaag
try {
articlesDomain =
(IArticlesDomain)
(
new XmlBeanFactory(
new ClassPathResource(
(String) config.getInitParameter(
SPRING_CONFIG_FILENAME)))).getBean(
"articlesDomain");
} catch (Exception ex) {
// de fout wordt opgeslagen
erreurs.add(
"Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// bepaalde URL's van de applicatie worden opgeslagen
hActionListe.put("href", "?action=" + ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", "?action=" + ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put(
"href",
"?action=" + ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// het is klaar
return;
}
|
Opmerkingen: de methode [init]
- controleert of bepaalde configuratieparameters aanwezig zijn
- instantiëert een service voor toegang tot het applicatiedomein via Spring
- stelt een foutenlijst in om eventuele initialisatiefouten aan te geven
- een aantal privévelden:
- fouten: de lijst met fouten die door [init] zijn gedetecteerd
- [hActionListe, hActionPanier, hActionValidationPanier]: woordenboeken. Elk daarvan bevat de informatie die nodig is om een optie weer te geven in het hoofdmenu dat wordt weergegeven door de weergave [entete.jsp]
- acticlesDomain: de service voor toegang tot het applicatiemodel
De methode [init] wordt slechts één keer uitgevoerd, bij het eerste laden van de servlet. Vervolgens wordt een van de methoden [doGet, doPost] uitgevoerd, afhankelijk van het type [GET, POST] van het verzoek van de client. In dit geval doen beide methoden hetzelfde en is de code in [doGet] geplaatst:
| public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// er wordt gecontroleerd hoe de initialisatie van de servlet is verlopen
if (erreurs.size() != 0) {
// hebben we de URL van de foutpagina?
if (config.getInitParameter(URL_ERREURS) == null) {
throw new ServletException(erreurs.toString());
}
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] {
});
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
// de actie wordt verwerkt
String action = request.getParameter("action");
if (action == null) {
// lijst met artikelen
doListe(request, response);
return;
}
if (action.equals(ACTION_LISTE)) {
// lijst met artikelen
doListe(request, response);
return;
}
if (action.equals(ACTION_INFOS)) {
// informatie over een artikel
doInfos(request, response);
return;
}
if (action.equals(ACTION_ACHAT)) {
// een artikel kopen
doAchat(request, response);
return;
}
if (action.equals(ACTION_PANIER)) {
// winkelwagen weergeven
doPanier(request, response);
return;
}
if (action.equals(ACTION_RETIRER_ACHAT)) {
// een artikel uit het winkelmandje verwijderen
doRetirerAchat(request, response);
return;
}
if (action.equals(ACTION_VALIDATION_PANIER)) {
// Winkelmandje bevestigen
doValidationPanier(request, response);
return;
}
// onbekende actie
ArrayList erreurs = new ArrayList();
erreurs.add("action [" + action + "] inconnue");
// de foutpagina wordt weergegeven
request.setAttribute("actions", new Hashtable[] { hActionListe });
afficheErreurs(request, response, erreurs);
// einde
return;
}
|
- De methode [doGet] controleert eerst of er initialatiefouten zijn opgetreden na afloop van de methode [init]. Zo ja, dan wordt de weergave [ERREURS] weergegeven en is de methode voltooid.
- Zo niet, dan haalt de methode de parameter [action] op uit het verzoek van de klant. Ter herinnering: de applicatie is zo ontworpen dat deze alleen verzoeken verwerkt waarin de parameter [action] verplicht aanwezig is.
- De applicatie laat de methode uitvoeren die aan de actie is gekoppeld. In dit geval is dat de methode [doListe].
De methode [doListe] is als volgt:
| private void doListe(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de artikellijst wordt opgevraagd
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// fouten?
if (erreurs.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
// de lijst met artikelen wordt weergegeven
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
// einde
return;
}
|
- Laten we niet vergeten dat de methode [init] de toegangsservice tot het applicatiemodel (domeinlaag) heeft opgeslagen in een privéveld van de servlet:
// privévelden
private IArticlesDomain articlesDomain = null;
- Vanuit deze toegangsdienst kan de lijst met artikelen worden opgevraagd:
| // de lijst met fouten
ArrayList erreurs = new ArrayList();
// de lijst met artikelen wordt opgevraagd
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
erreurs.add(
"Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Als er fouten optreden, wordt de weergave [ERREURS] verzonden:
| // fouten?
if (erreurs.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { hActionListe });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_ERREURS))
.forward(request, response);
// einde
return;
}
|
- anders wordt de weergave [LISTE] verzonden:
| // de lijst met artikelen weergeven
request.setAttribute("listarticles", articles);
request.setAttribute("message","");
request.setAttribute("actions", new Hashtable[] { hActionPanier });
getServletContext()
.getRequestDispatcher(config.getInitParameter(URL_LISTE))
.forward(request, response);
|
In ons voorbeeld is alles goed verlopen en hebben we inderdaad de weergave [LISTE] ontvangen. De lezer wordt verzocht de code van de weergave [LISTE] nog eens door te nemen om te zien dat de dynamische parameters die deze weergave verwacht, hierboven inderdaad door de controller worden aangeleverd. Voor elke weergave moet dezelfde controle worden uitgevoerd:
- de dynamische parameters van de weergave terugvinden
- controleren of de controller deze inderdaad in de attributen van de aan de weergave doorgegeven aanvraag opneemt
We geven nu eenvoudigweg het schermtraject weer dat een gebruiker van de applicatie doorloopt. De lezer wordt verzocht telkens opnieuw een redenering te volgen die vergelijkbaar is met de vorige:
Vanuit de lijst met artikelen kan de gebruiker een artikel kiezen:
De koper kan hier artikel nr. 3 kopen. Laten we een typefout maken bij het invoeren van de hoeveelheid:
De fout is gemeld. Laten we nu een paar artikelen kopen:
De aankoop is geregistreerd en de lijst met artikelen is opnieuw weergegeven. Laten we het winkelmandje bekijken:
Het artikel staat inderdaad in het winkelmandje. Laten we het verwijderen:
Het artikel is uit het winkelmandje verwijderd en het winkelmandje is opnieuw weergegeven. Het is nu leeg.
Laten we 100 artikelen nr. 3 en 2 artikelen nr. 4 kopen:
Het artikel nr. 3 kon niet worden gekocht omdat we er 100 wilden kopen en er slechts 30 op voorraad waren. Dit artikel is in het winkelmandje gebleven:
Artikel nr. 4 is wel gekocht, zoals blijkt uit de nieuwe voorraadstand van 39 (40-1):
3.6. Architectuur MVC met Struts
3.6.1. Algemene architectuur van de applicatie
Laten we nog eens terugkomen op de architectuur MVC van de applicatie:
In de vorige versie:
- werd de besturing verzorgd door een servlet
- de weergaven werden verzorgd door JSP-pagina's
- het model bestond uit een set van drie .jar-bestanden
In de Struts-versie:
- wordt de controller verzorgd door een servlet die is afgeleid van de generieke Struts-servlet [ActionServlet]
- de weergaven worden verzorgd door dezelfde JSP-pagina's als voorheen, op enkele details na
- het model wordt verzorgd door dezelfde drie archieven
We zullen zien dat de overstap naar Struts vanuit de vorige applicatie uit de volgende taken bestaat:
- de acties die voorheen in specifieke methoden van de servlet/controller werden verwerkt, worden nu verwerkt door instanties van klassen die zijn afgeleid van de Struts-klasse [Action]
- het schrijven van de configuratiebestanden [web.xml] en [struts-config.xml]
- enkele wijzigingen aanbrengen in de pagina's JSP
Laten we nog eens kijken naar de generieke architectuur MVC die door STRUTS wordt gebruikt:
| de businessklassen, de klassen voor gegevenstoegang en de database |
| de pagina's JSP |
| de servlet voor de verwerking van verzoeken van klanten, de objecten [Action] en de beans [ActionForm] die aan de formulieren zijn gekoppeld. |
- De controller vormt het hart van de applicatie. Alle verzoeken van de client lopen hierdoorheen. Het is een generieke servlet die wordt geleverd door STRUTS. In bepaalde gevallen kan het nodig zijn om hier een afgeleide van te maken. Voor eenvoudige gevallen is dit niet nodig. Deze generieke servlet haalt de benodigde informatie uit een bestand dat meestal struts-config.xml wordt genoemd.
- Als het verzoek van de klant formulierparameters bevat, worden deze door de controller in een Bean-object geplaatst. De Bean-objecten die op deze manier in de loop van de tijd worden aangemaakt, worden opgeslagen in de sessie of het verzoek van de klant. Dit is configureerbaar. Ze hoeven niet opnieuw te worden aangemaakt als ze al bestaan.
- In het configuratiebestand struts-config.xml wordt aan elk URL dat programmatisch moet worden verwerkt (en dus niet overeenkomt met een weergave JSP die rechtstreeks kan worden opgevraagd) bepaalde informatie gekoppeld:
- de naam van de Action-klasse die verantwoordelijk is voor de verwerking van het verzoek. Ook hier kan het geïnstantieerde Action-object worden bewaard in de sessie of het verzoek.
- als de aangevraagde URL is geconfigureerd (bijvoorbeeld bij het verzenden van een formulier naar de controller), wordt de naam van de bean vermeld die verantwoordelijk is voor het opslaan van de formuliergegevens.
- Op basis van deze informatie uit het configuratiebestand kan de controller bij ontvangst van een URL-verzoek van een client bepalen of er een bean moet worden aangemaakt en welke. Zodra de bean is geïnstantieerd, kan deze controleren of de gegevens die hij heeft opgeslagen en die afkomstig zijn uit het formulier, geldig zijn of niet. Een methode van de bean met de naam validate wordt automatisch aangeroepen door de controller. De bean wordt door de ontwikkelaar geïmplementeerd. Deze plaatst dus in de methode validate de code die de geldigheid van de formuliergegevens controleert. Als de gegevens ongeldig blijken te zijn, gaat de controller niet verder. Hij draagt het stokje over aan een weergave waarvan hij de naam in zijn configuratiebestand vindt. De uitwisseling is dan voltooid. Opgemerkt moet worden dat de ontwikkelaar kan aangeven dat de geldigheid van het formulier niet moet worden gecontroleerd. Dit doet hij eveneens in het bestand struts-config.xml. In dat geval roept de controller de methode `validate` van de bean niet aan.
- Als de gegevens van de bean correct zijn, of als er geen controle plaatsvindt, of als er geen bean is, draagt de controller het over aan het object van het type Action dat gekoppeld is aan URL. Dit doet hij door de methode `execute` van dit object aan te roepen, waarbij hij de referentie van de bean doorgeeft die hij eventueel heeft aangemaakt. Hier voert de ontwikkelaar de benodigde bewerkingen uit: hij zal eventueel gebruik moeten maken van businessklassen of klassen voor gegevenstoegang. Aan het einde van de verwerking geeft het Action-object de naam van de weergave terug aan de controller, die deze als antwoord naar de client moet sturen.
- In zijn configuratiebestand vindt de controller de URL die gekoppeld is aan de naam van de weergave die hij moet weergeven. Vervolgens verstuurt hij deze weergave. De communicatie met de klant is voltooid.
In onze applicatie zullen we geen [Bean]-objecten gebruiken als bufferobjecten tussen de client en de [Action]-klassen. Het object [Action] haalt de parameters van het verzoek van de client rechtstreeks op uit het object [HttpServletRequest] dat het ontvangt. Dit vergemakkelijkt de porting van onze oorspronkelijke applicatie. De uiteindelijke architectuur van onze applicatie zal dus als volgt zijn:
| de businessklassen, de klassen voor gegevenstoegang en de database |
| de pagina’s JSP |
| de servlet voor de verwerking van verzoeken van klanten, de objecten [Action] |
3.6.2. Het model
Dit is eerder al besproken. Het bestaat uit de Java-archieven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.6.3. Configuratie van de applicatie
3.6.3.1. Algemene architectuur
De algemene architectuur van het Eclipse-project is als volgt:

3.6.3.2. Configuratie van de gegevenstoegang
Aangezien de interface voor de toegang tot de gegevens niet verandert, zijn de bijbehorende configuratiebestanden dezelfde als in de vorige versie. Ze zijn gedefinieerd in [WEB-INF/src]:

Op de bovenstaande schermafbeelding zijn de bestanden [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] afkomstig uit de vorige versie.
3.6.3.3. De map met de archieven
In [WEB-INF/lib] bevinden zich dezelfde archieven als in de vorige versie, plus het archief dat nodig is voor Struts:

3.6.3.4. Configuratie van de applicatie
De applicatie wordt geconfigureerd met behulp van twee bestanden: [web.xml, struts-config.xml] in de map [WEB-INF]:

Het bestand [web.xml] ziet er als volgt uit:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Wat staat er in dit bestand?
- de startpagina van de applicatie is [vues/index.jsp] (welcome-file)
- URL-verzoeken in de vorm *.do worden doorgestuurd naar de servlet [strutswebarticles] (servlet-mapping)
- de servlet [strutswebarticles] is een instantie van de klasse [ istia.st.articles.web.struts.MainServlet] (servlet-name, servlet-class)
- deze servlet accepteert twee initialisatieparameters
- de naam van het Struts-configuratiebestand (config)
- de naam van het Spring-configuratiebestand (springConfigFileName)
Het bestand [struts-config.xml] ziet er als volgt uit:
| <?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<action-mappings>
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
</action-mappings>
<message-resources parameter="ApplicationResources" null="false" />
</struts-config>
|
Wat staat er in dit configuratiebestand?
- dat onze controller de volgende URL's zal verwerken:
| om de lijst met artikelen weer te geven |
| om de lijst met artikelen weer te geven |
| om informatie over een bepaald artikel weer te geven |
| om een bepaald artikel te kopen |
| om het winkelmandje te bekijken |
| om een aankoop uit het winkelmandje te verwijderen |
| om een winkelmandje te bevestigen |
- de bovenstaande acties komen één op één overeen met de acties die door de servlet in de vorige versie werden verwerkt. Voor elk van deze acties wordt de volgende informatie vermeld:
- de naam van de klasse die verantwoordelijk is voor de verwerking van deze actie
- de mogelijke reacties (= weergaven) na de verwerking van de actie. De controller kiest er slechts één uit.
- de naam van een berichtenbestand voor de applicatie (message-resources). Hier bestaat het bestand wel, maar is het leeg. Het wordt niet gebruikt. Het moet in de map [ClassPath] van de applicatie worden geplaatst. Hier wordt het in [WEB-INF/classes] geplaatst. In Eclipse bereikt men dit resultaat door het in [WEB-INF/src] te plaatsen:

3.6.4. De weergaven JSP
De gebruikte weergaven JSP zijn ook hier die van de vorige versie. Er is maar heel weinig gewijzigd: het gaat om de URL’s in de vorm [?action=XX?id=YY& ...] die [/XX.do?id=YY&....] worden. We herhalen hier de reeds gegeven uitleg om te voorkomen dat de gebruiker terug moet bladeren. Het is belangrijk om te begrijpen dat de informatie die door de controller naar de weergave wordt doorgegeven in beide versies precies hetzelfde is. Op dit punt is er niets veranderd.
3.6.4.1. entete.jsp
Om de verschillende weergaven een zekere uniformiteit te geven, zullen deze dezelfde koptekst delen, namelijk die waarin de naam van de applicatie en het menu worden weergegeven:
Het menu is dynamisch en wordt bepaald door de controller. Deze voegt in de verzoek die naar de pagina JSP wordt verzonden, een sleutelattribuut "actions" toe met als bijbehorende waarde een array van het type Hastable[]. Elk element van deze array is een woordenboek dat bedoeld is om een optie in het koptekstmenu te genereren. Elk woordenboek heeft twee sleutels:
- href: de URL die bij de menuoptie hoort
- lien: de tekst van het menu
De andere weergaven van de applicatie zullen de koptekst gebruiken die is gedefinieerd door [entete.jsp] met behulp van de volgende tag JSP:
<jsp:include page="entete.jsp"/>
Bij uitvoering zorgt deze tag ervoor dat de code van de pagina JSP, waarin deze tag is opgenomen, de code van de pagina [entete.jsp] opneemt. Aangezien de URL van de pagina een relatieve URL is (zonder /), wordt de pagina [entete.jsp] gezocht in dezelfde map als de pagina met de tag <jsp:include>.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<html>
<head>
<title>webarticles</title>
</head>
<body>
<table>
<tr>
<td><h2>Magasin virtuel</h2></td>
<c:forEach items="${actions}" var="action">
<td>|</td>
<td><a href="<c:out value="${action.href}"/>"><c:out value="${action.lien}"/></a></td>
</c:forEach>
</tr>
</table>
<hr>
|
Opmerkingen: geen wijzigingen ten opzichte van de vorige versie
3.6.4.2. liste.jsp
Deze weergave toont de lijst met artikelen die te koop zijn:
Deze wordt weergegeven na een verzoek /main.do of /validerpanier.do. De elementen van het verzoek van de controller zijn als volgt:
| object Hashtable[] - de lijst met menuopties |
| ArrayList objecten van het type [Article] |
| String-object - bericht dat onderaan de pagina moet worden weergegeven |
Elke link [Infos] in de array HTML met artikelen heeft een URL in de vorm [/infos.do?id=ID], waarbij ID het id-veld is van het weergegeven artikel.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Liste des articles</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th>
</tr>
<c:forEach var="article" items="${listarticles}">
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><a href="<c:out value="infos.do?id=${article.id}"/>">Infos</a></td>
</tr>
</c:forEach>
</table>
<p>
<c:out value="${message}"/>
</body>
</html>
|
Opmerkingen: een wijziging (hierboven gemarkeerd)
3.6.4.3. infos.jsp
Deze weergave toont informatie over een artikel en maakt het ook mogelijk om het te kopen:

Deze wordt weergegeven na een verzoek /infos.do?id=ID of een verzoek /achat.do?id=ID wanneer de gekochte hoeveelheid onjuist is. De onderdelen van de verzoek-URL van de controller zijn als volgt:
| object Hashtable[] - de lijst met menuopties |
| object van het type [Article] – weer te geven artikel |
| String-object - bericht dat moet worden weergegeven bij een fout in de hoeveelheid |
| object String - waarde die moet worden weergegeven in het invoerveld [Qte] |
De velden [msg] en [qte] worden gebruikt in geval van een invoerfout bij de hoeveelheid:

Deze pagina bevat een formulier dat wordt verzonden via de knop [Acheter]. De doel-URL van POST is [/achat.do?id=ID], waarbij ID de ID is van het gekochte artikel.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Article d'id [<c:out value="${article.id}"/>]</h2>
<table border="1">
<tr>
<th>NOM</th><th>PRIX</th><th>STOCK ACTUEL</th><th>STOCK MINIMUM</th>
</tr>
<tr>
<td><c:out value="${article.nom}"/></td>
<td><c:out value="${article.prix}"/></td>
<td><c:out value="${article.stockActuel}"/></td>
<td><c:out value="${article.stockMinimum}"/></td>
</tr>
</table>
<p>
<form method="post" action="achat.do?id=<c:out value="${article.id}"/>"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value="<c:out value="${qte}"/>"></td>
<td><c:out value="${msg}"/></td>
</tr>
</table>
</form>
</body>
</html>
|
Opmerkingen: één wijziging (hierboven gemarkeerd)
3.6.4.4. panier.jsp
Dit scherm toont de inhoud van het winkelmandje:

Deze wordt weergegeven na een verzoek /panier.do of /retirerachat.do?id=ID. De elementen van het verzoek van de controller zijn als volgt:
| object Hashtable[] – de lijst met menuopties |
| object van het type [Panier] – het weer te geven winkelmandje |
Elke link [Retirer] in de tabel HTML met de aankopen in het winkelmandje heeft een URL in de vorm [retirerachat.do?id=ID], waarbij ID het veld [id] is van hetartikel dat uit het winkelmandje moet worden verwijderd.
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<table border="1">
<tr>
<td>Article</td><td>Qte</td><td>Pu</td><td>Total</td>
</tr>
<c:forEach var="achat" items="${panier.achats}">
<tr>
<td><c:out value="${achat.article.nom}"/></td>
<td><c:out value="${achat.qte}"/></td>
<td><c:out value="${achat.article.prix}"/></td>
<td><c:out value="${achat.total}"/></td>
<td><a href="<c:out value="retirerachat.do?id=${achat.article.id}"/>">Retirer</a></td>
</tr>
</c:forEach>
</table>
<p>
Total de la commande : <c:out value="${panier.total}"/> euros
</body>
</html>
|
Opmerkingen: een wijziging (hierboven gemarkeerd)
3.6.4.5. paniervide.jsp
Dit scherm geeft aan dat het winkelmandje leeg is:

Deze wordt weergegeven na een verzoek /panier.do of /retirerachat.do?id=ID. De elementen van het verzoek van de controller zijn als volgt:
| object Hashtable[] - de lijst met menuopties |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Contenu de votre panier</h2>
<p>
Votre panier est vide.
</body>
</html>
|
Opmerkingen: geen wijzigingen.
3.6.4.6. erreurs.jsp
Dit scherm wordt weergegeven bij fouten:

Deze wordt weergegeven na elke aanvraag die tot een fout leidt, behalve bij de aankoopactie met een onjuiste hoeveelheid, die wordt afgehandeld door de weergave [INFOS]. De elementen van de aanvraag van de controller zijn als volgt:
| object Hashtable[] – de lijst met menuopties |
| ArrayList met String-objecten die de weer te geven foutmeldingen vertegenwoordigen |
Code:
| <%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<jsp:include page="entete.jsp"/>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
<c:forEach var="erreur" items="${erreurs}">
<li><c:out value="${erreur}"/></li>
</c:forEach>
</ul>
</body>
</html>
|
Opmerkingen: geen wijzigingen.
3.6.4.7. index.jsp
Deze pagina is gedefinieerd als startpagina van de applicatie in het bestand [web.xml] van de applicatie:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
....
</servlet>
<servlet-mapping>
....
</servlet-mapping>
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
De weergave [index.jsp] leidt de klant gewoon door naar het startpunt van de applicatie:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
Opmerkingen: één wijziging (hierboven gemarkeerd)
3.6.5. De Struts-controller
Struts beschikt over een generieke controller met de naam [ActionServlet]. We weten dat een servlet een methode heeft, [init], waarmee de applicatie bij het opstarten kan worden geïnitialiseerd. Als we de generieke Struts-controller [ActionServlet] gebruiken, hebben we geen toegang tot de bijbehorende methode [init]. Hier moeten we bij het opstarten van de applicatie een aantal zaken uitvoeren, voornamelijk het instantiëren van een object voor toegang tot het model. Daarom hebben we een methode [init] nodig. We leiden de klasse [Actionservlet] daarom af in de volgende klasse [MainServlet]:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.apache.struts.action.ActionServlet;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* @author ST - ISTIA
*
*/
public class MainServlet extends ActionServlet {
// privévelden
private ArrayList erreurs = new ArrayList();
private IArticlesDomain articlesDomain = null;
private final String SPRING_CONFIG_FILENAME = "springConfigFileName";
private final String[] parameters = { SPRING_CONFIG_FILENAME };
private ServletConfig config;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_ACHAT = "achat.do";
private final String ACTION_INFOS = "infos.do";
private final String ACTION_RETIRER_ACHAT = "retirerachat.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private String urlActionListe;
private final String lienActionListe = "Liste des articles";
private String urlActionPanier;
private final String lienActionPanier = "Voir le panier";
private String urlActionValidationPanier;
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters - setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public ArrayList getErreurs() {
return erreurs;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public void setHActionListe(Hashtable actionListe) {
hActionListe = actionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public void setHActionPanier(Hashtable actionPanier) {
hActionPanier = actionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
public void setHActionValidationPanier(Hashtable actionValidationPanier) {
hActionValidationPanier = actionValidationPanier;
}
public void init() throws ServletException{
// de bovenliggende klasse initialiseren
super.init();
// de initialisatieparameters van de servlet worden opgehaald
config = getServletConfig();
String param = null;
for (int i = 0; i < parameters.length; i++) {
param = config.getInitParameter(parameters[i]);
if (param == null) {
// de fout wordt opgeslagen
erreurs.add("Paramètre [" + parameters[i]
+ "] absent dans le fichier [web.xml]");
}
}
// fouten?
if (erreurs.size() != 0) {
return;
}
// er wordt een object IArticlesDomain aangemaakt voor toegang tot de bedrijfslaag
try {
articlesDomain = (IArticlesDomain) (new XmlBeanFactory(
new ClassPathResource((String) config
.getInitParameter(SPRING_CONFIG_FILENAME))))
.getBean("articlesDomain");
} catch (Exception ex) {
// de fout wordt opgeslagen
erreurs.add("Erreur de configuration de l'accès aux données : "
+ ex.toString());
return;
}
// bepaalde URL's van de applicatie worden opgeslagen
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// het is klaar
return;
}
}
|
Opmerkingen:
- het belang van de klasse ligt in de methode [init] en de privévelden
- de methode [init] doet hetzelfde als de methode [init] van de controller uit de vorige versie:
- ze controleert of bepaalde configuratieparameters aanwezig zijn
- ze instantiëert via Spring een service voor toegang tot het applicatiedomein
- zij stelt een foutenlijst in om eventuele initialisatiefouten aan te geven
- voordat de methode [init] aan de slag gaat, roept deze de methode [init] aan van de bovenliggende klasse [ActionServlet]. Deze methode verwerkt het Struts-configuratiebestand [struts-config.xml].
- Er zijn een aantal privévelden met bijbehorende accessors gedefinieerd:
- fouten: de lijst met fouten die door [init] zijn gedetecteerd
- [hActionListe, hActionPanier, hActionValidationPanier]: woordenboeken. Elk daarvan bevat de informatie die nodig is om een optie weer te geven in het hoofdmenu dat wordt weergegeven door de weergave [entete.jsp]
- acticlesDomain: de service voor toegang tot het applicatiemodel
- De controller van een Struts-applicatie is toegankelijk voor de klassen [Action] die verantwoordelijk zijn voor de verwerking van de verschillende mogelijke acties. Deze klassen hebben toegang tot de eerder genoemde privévelden, omdat deze zijn voorzien van openbare accessors.
Deze controller wordt geïnstantieerd via het bestand [web.xml]:
| <web-app>
<servlet>
<servlet-name>strutswebarticles</servlet-name>
<servlet-class>istia.st.articles.web.struts.MainServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>springConfigFileName</param-name>
<param-value>spring-config-sqlmap-firebird.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>strutswebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
|
Elk URL-bestand dat eindigt op .do, wordt verwerkt door een instantie van de klasse [istia.st.articles.web.struts.MainServlet]
3.6.6. De acties van de Struts-applicatie
3.6.6.1. Inleiding
Elke Struts-actie wordt het onderwerp van een klasse. In de vorige versie was elke actie het onderwerp van een methode in de controller van de applicatie. Het schrijven van de klasse [Action] bestaat meestal uit:
- het kopiëren en plakken van de methode die in de vorige versie werd gebruikt
- de code aanpassen aan de Struts-conventies
3.6.6.2. main.do, liste.do
Deze twee acties zijn identiek en worden in [struts-config.xml] gedefinieerd door:
| <action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
<action path="/liste" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
|
Wanneer een van deze acties, [main.do, liste.do], in een browser wordt uitgevoerd, krijgt men het volgende resultaat:

De code van de klasse [ListeArticlesAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST - ISTIA
*
*/
public class ListeArticlesAction extends Action {
/**
* affichage de la liste des articles - s'appuie sur la couche [domain]
*
* @param mapping :
* configuration de l'action dans struts-config.xml
* @param form :
* le formulaire passé à l'action - ici aucun
* @param request :
* la requête HTTP du client
* @param response :
* la réponse HTTP au client
*/
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// het object voor toegang tot het domein
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de artikellijst wordt opgevraagd
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
// fouten?
if (erreurs.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// de lijst met artikelen wordt weergegeven
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
}
}
|
Opmerkingen:
- het schrijven van de code voor een klasse [Action] bestaat in wezen uit het schrijven van de code voor de bijbehorende methode [execute]
- Er is een aantal gegevens opgeslagen in de instantie van de controller. We halen een verwijzing hiernaar op via:
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
- We halen de lijst met initialisatiefouten op die door de controller zijn opgeslagen. Als deze lijst niet leeg is, wordt de weergave [ERREURS] naar de client verzonden:
| // initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
|
De weergave die daadwerkelijk naar de klant wordt verzonden, wordt ons geleverd door [struts-config.xml]:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Dit is de weergave [/vues/erreurs.jsp]. De lezer wordt verzocht te controleren wat er van deze weergave wordt verwacht. Deze informatie wordt hier door de actie in het object [request] als attributen verstrekt.
- Nog steeds via de controller kan de actie het object voor toegang tot het applicatiemodel (domeinlaag) ophalen:
// het domeintoegangsobject
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
- Zodra dit is gebeurd, kan de lijst met artikelen worden opgevraagd:
| // de lijst met fouten
ArrayList erreurs = new ArrayList();
// de artikellijst wordt opgevraagd
List articles = null;
try {
articles = articlesDomain.getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
}
|
- Als er fouten optreden, wordt de weergave [ERREURS] verzonden:
| // fouten?
if (erreurs.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- anders wordt de weergave [LISTE] verzonden:
| // de lijst met artikelen wordt weergegeven
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionPanier() });
return mapping.findForward("afficherListeArticles");
|
De weergave die daadwerkelijk naar de klant wordt verzonden, wordt ons geleverd door [struts-config.xml]:
<action path="/main" type="istia.st.articles.web.struts.ListeArticlesAction">
<forward name="afficherListeArticles" path="/vues/liste.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Dit is de weergave [/vues/liste.jsp]. De lezer wordt verzocht na te gaan wat er van deze weergave wordt verwacht. Deze informatie wordt hier als attributen verstrekt door de actie in het object [request].
3.6.6.3. infos.do
Deze actie dient om informatie te verstrekken over een van de artikelen die worden weergegeven in de weergave [LISTE]:
Deze actie is als volgt geconfigureerd in [struts-config.xml]:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
De klassecode van [InfosArticleAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class InfosArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// het domeintoegangsobject
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de gevraagde id wordt opgehaald
String strId = request.getParameter("id");
// iets?
if (strId == null) {
// niet normaal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// strId wordt omgezet in een geheel getal
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// niet normaal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// het artikel met de sleutel-ID wordt opgevraagd
Article article = null;
try {
article = articlesDomain.getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
if (article == null) {
// niet normaal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// het artikel wordt in de sessie geplaatst
request.getSession().setAttribute("article", article);
// de informatiepagina wordt weergegeven
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
}
|
Opmerkingen:
- het begin van de methode [execute] is identiek aan de eerder besproken methode. Dit geldt ook voor de overige acties.
- De methode haalt de parameter [id] op, die normaal gesproken in de URL moet staan. Deze moet namelijk de vorm [/infos.do?id=X] hebben. Er worden verschillende controles uitgevoerd om de aanwezigheid en geldigheid van de parameter [id] te verifiëren. Bij een probleem wordt de weergave [ERREURS] verzonden.
- Als [id] geldig is, wordt het bijbehorende artikel opgevraagd bij de laag [domain]. Als deze een uitzondering genereert of als het artikel niet wordt gevonden, wordt ook hier de weergave [ERREURS] verzonden.
- Als alles goed gaat, wordt het verkregen artikel in de sessie opgeslagen. Dit is een discutabel punt. Hier gaan we ervan uit dat de klant dit artikel mogelijk zal kopen. Als hij dat doet, halen we het uit de sessie, in plaats van het opnieuw op te vragen bij de laag [domain].
- Ten slotte wordt de weergave [INFOS] weergegeven. De weergave die daadwerkelijk naar de klant wordt verzonden, wordt ons geleverd door [struts-config.xml]:
<action path="/infos" type="istia.st.articles.web.struts.InfosArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
Dit betreft de weergave [/vues/infos.jsp]. De lezer wordt verzocht na te gaan wat er van deze weergave wordt verwacht. Deze informatie wordt hier door de actie in het object [request] als attributen verstrekt.
3.6.6.4. achat.do
Deze actie dient om het artikel te kopen dat wordt weergegeven door de vorige weergave [INFOS]:
Zodra het artikel is gekocht, wordt de weergave [LISTE] opnieuw weergegeven (weergave rechts). Als we de code HTML van de weergave links hierboven bekijken, zien we dat de tag <form> als volgt is gedefinieerd:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
We zien dus dat het formulier naar de controller wordt verzonden met de actie [achat.do].
Deze actie is als volgt geconfigureerd in [struts-config.xml]:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
De code van de klasse [AchatArticleAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST
*
*/
public class AchatArticleAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de gekochte hoeveelheid ophalen
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// verkeerde hoeveelheid
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
// de sessie van de klant wordt opgehaald
HttpSession session = request.getSession();
// het artikel in de sessie ophalen
Article article = (Article) session.getAttribute("article");
// is de sessie verlopen?
if(article==null){
// de foutpagina wordt weergegeven
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// de nieuwe aankoop wordt aangemaakt
Achat achat = new Achat(article, qté);
// de aankoop wordt aan het winkelmandje van de klant toegevoegd
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// terug naar de artikellijst
return mapping.findForward("afficherListeArticles");
}
}
|
Opmerkingen:
- het begin van de methode [execute] is identiek aan de eerder besproken methoden.
- Laten we nog eens de vorm van het formulier dat naar de controller wordt verzonden, in herinnering brengen:
| <form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
|
- Er zijn twee parameters in het verzoek: [id]: nummer van het gekochte artikel, [qte]: gekochte hoeveelheid.
- Er wordt gecontroleerd of de parameter [qte] aanwezig en geldig is. Als deze parameter als onjuist wordt herkend, wordt de weergave [INFOS] teruggestuurd naar de gebruiker, vergezeld van een foutmelding:
| // de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de gekochte hoeveelheid wordt opgehaald
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// verkeerde hoeveelheid
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherInfosArticle");
}
|
- het gekochte artikel wordt opgehaald uit de sessie. Deze kan zijn verlopen. In dat geval wordt de weergave [ERREURS] verzonden:
| // de sessie van de klant ophalen
HttpSession session = request.getSession();
// het artikel in de sessie ophalen
Article article = (Article) session.getAttribute("article");
// is de sessie verlopen?
if(article==null){
// de foutpagina wordt weergegeven
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- Als de sessie niet is verlopen, wordt het artikel in het winkelmandje geplaatst, dat eveneens uit de sessie wordt opgehaald:
| // de sessie van de klant wordt opgehaald
HttpSession session = request.getSession();
// het artikel uit de sessie ophalen
Article article = (Article) session.getAttribute("article");
// sessie verlopen?
if(article==null){
// de foutpagina wordt weergegeven
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// we maken de nieuwe aankoop aan
Achat achat = new Achat(article, qté);
// de aankoop wordt aan het winkelmandje van de klant toegevoegd
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- ten slotte wordt de weergave [LISTE] verzonden:
// terug naar de artikellijst
return mapping.findForward("afficherListeArticles");
- De weergave die daadwerkelijk naar de klant wordt verzonden, wordt ons geleverd door [struts-config.xml]:
<action
path="/achat" type="istia.st.articles.web.struts.AchatArticleAction">
<forward name="afficherInfosArticle" path="/vues/infos.jsp"/>
<forward name="afficherListeArticles" path="/main.do"/>
</action>
Dit is de weergave [/main.do]. Deze weergave is geen weergave, maar een actie. De eerder beschreven actie [/main.do] wordt dus uitgevoerd en geeft de lijst met artikelen weer.
3.6.6.5. panier.do
Deze actie dient om alle aankopen van de klant weer te geven. Ze is beschikbaar via de menuoptie [Voir le panier]:
De code HTML die bij de link [Voir le panier] hoort, is als volgt:
<a href="panier.do">Voir le panier</a>
De actie [panier.do] is als volgt geconfigureerd in [struts-config.xml]:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
De code van de klasse [VoirPanierAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class VoirPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// het winkelmandje wordt weergegeven
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// winkelwagen leeg
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
} else {
// er zit iets in het winkelmandje
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(), mainServlet.getHActionValidationPanier() });
return mapping.findForward("afficherPanier");
}
}
}
|
Opmerkingen:
- het begin van de methode [execute] is identiek aan de eerder besproken methoden.
- Het winkelmandje wordt opgehaald uit de sessie waarin het zich normaal gesproken bevindt. Het kan zijn dat de sessie is verlopen, waardoor er geen winkelmandje is. Dit wordt niet als een fout behandeld, maar we gaan er gewoon vanuit dat het winkelmandje leeg is.
- Als het winkelmandje leeg is, wordt de weergave [PANIERVIDE] getoond
- anders is het de weergave [PANIER]
De weergaven die daadwerkelijk naar de klant worden verzonden, worden bepaald door de actie:
<action
path="/panier" type="istia.st.articles.web.struts.VoirPanierAction">
<forward name="afficherPanier" path="/vues/panier.jsp"/>
<forward name="afficherPanierVide" path="/vues/paniervide.jsp"/>
</action>
3.6.6.6. retirerachat.do
Deze actie dient om een aankoop uit het winkelmandje te verwijderen:
Na de actie [retirerachat.do] wordt het winkelmandje opnieuw weergegeven (zie afbeelding rechts hierboven). Als we kijken naar de code HTML van de link [Valider le panier] in de afbeelding links hierboven, zien we het volgende:
<a href="retirerachat.do?id=3">Retirer</a>
De actie [retirerachat.do] ontvangt dus als parameter de id van het artikel dat uit het winkelmandje moet worden verwijderd. Deze actie is als volgt geconfigureerd in [struts-config.xml]:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
De code van de klasse [RetirerAchatAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.Panier;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class RetirerAchatAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// het winkelmandje wordt opgehaald
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherPanierVide");
}
// de id van het te verwijderen artikel wordt opgehaald
String strId = request.getParameter("id");
// iets?
if (strId == null) {
// niet normaal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// we zetten strId om in een geheel getal
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// niet normaal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// de aankoop wordt verwijderd
panier.enlever(id);
// het winkelmandje wordt opnieuw weergegeven
return mapping.findForward("afficherPanier");
}
}
|
Opmerkingen:
- het begin van de methode [execute] is identiek aan de eerder besproken methoden.
- De daaropvolgende code is bedoeld om de aanwezigheid en geldigheid van de parameter [id] te controleren. Als deze onjuist blijkt te zijn, wordt de weergave [ERREURS] verzonden.
- Anders wordt het artikel uit het winkelmandje verwijderd:
// de aankoop wordt verwijderd
panier.enlever(id);
- waarna het winkelmandje opnieuw wordt weergegeven:
// het winkelmandje wordt opnieuw weergegeven
return mapping.findForward("afficherPanier");
De weergave die daadwerkelijk naar de klant wordt verzonden, wordt bepaald door de actie:
<action
path="/retirerachat" type="istia.st.articles.web.struts.RetirerAchatAction">
<forward name="afficherPanier" path="/panier.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
We zien dat wat de weergave betreft, de actie [/panier.do] wordt geactiveerd. Deze is al beschreven. Deze actie geeft de weergave [PANIER] of [PANIERVIDE] weer, afhankelijk van de status van het winkelmandje.
3.6.6.7. validerpanier.do
Deze actie dient om de aankopen van de klant te bevestigen. Concreet komt dit neer op één enkele handeling: de voorraad van de gekochte artikelen wordt in de database met de gekochte hoeveelheden verminderd. Deze actie is afkomstig uit het volgende menu:
De code HTML van de link [Valider le panier] is als volgt:
<a href="validerpanier.do">Valider le panier</a>
Wanneer deze link wordt geactiveerd, worden de voorraden verlaagd en wordt de artikellijst opnieuw weergegeven.
Deze actie is als volgt geconfigureerd in [struts-config.xml]:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
De code van de klasse [ValiderPanierAction] is als volgt:
| package istia.st.articles.web.struts;
import istia.st.articles.domain.IArticlesDomain;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
/**
* @author ST-ISTIA
*
*/
public class ValiderPanierAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// de controleservlet
MainServlet mainServlet = (MainServlet) this.getServlet();
// initialisatiefouten?
ArrayList erreursInit = mainServlet.getErreurs();
if (erreursInit.size() != 0) {
// de foutpagina weergeven
request.setAttribute("erreurs", erreursInit);
request.setAttribute("actions", new Hashtable[] {});
return mapping.findForward("afficherErreurs");
}
// het domeintoegangsobject
IArticlesDomain articlesDomain = mainServlet.getArticlesDomain();
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de koper heeft zijn winkelmandje bevestigd
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// het winkelmandje wordt gevalideerd
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// eventuele fouten worden opgehaald
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
// alles lijkt in orde OK - de lijst met artikelen wordt weergegeven
return mapping.findForward("afficherListeArticles");
}
}
|
Opmerkingen:
- het begin van de methode [execute] is identiek aan de eerder besproken methoden.
- Het winkelmandje wordt uit de sessie opgehaald. Als de sessie is verlopen, wordt de weergave [ERREURS] verzonden:
| // de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de koper heeft zijn winkelmandje bevestigd
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
|
- De aankopen in het winkelmandje worden bevestigd. Er kunnen fouten optreden als de voorraad onvoldoende is om aan de aankopen te voldoen. In dat geval wordt de weergave [ERREURS] verzonden:
| // het winkelmandje wordt bevestigd
try {
articlesDomain.acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe() });
return mapping.findForward("afficherErreurs");
}
// eventuele fouten worden opgehaald
erreurs = articlesDomain.getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { mainServlet
.getHActionListe(),mainServlet.getHActionPanier() });
return mapping.findForward("afficherErreurs");
}
|
- Als alles goed is verlopen, wordt de lijst met artikelen opnieuw weergegeven:
// alles lijkt in orde OK - de lijst met artikelen wordt weergegeven
return mapping.findForward("afficherListeArticles");
De weergave die daadwerkelijk naar de klant wordt verzonden, wordt bepaald door de actie:
<action
path="/validerpanier" type="istia.st.articles.web.struts.ValiderPanierAction">
<forward name="afficherListeArticles" path="/main.do"/>
<forward name="afficherErreurs" path="/vues/erreurs.jsp"/>
</action>
We zien dat wat de weergave betreft, de actie [/main.do] wordt geactiveerd. Deze is al beschreven. Hiermee wordt de weergave [LISTE] weergegeven.
3.7. Architectuur van MVC met Spring
3.7.1. Algemene architectuur van de applicatie
Laten we nog eens terugkomen op de architectuur MVC van de applicatie:
In de eerste versie:
- werd de besturing verzorgd door een servlet
- de weergaven werden verzorgd door JSP-pagina's
- het model bestond uit een set van drie .jar-bestanden
In de Struts-versie:
- werd de controller verzorgd door een servlet die was afgeleid van de generieke Struts-servlet [ActionServlet]
- de weergaven werden verzorgd door dezelfde JSP-pagina's als in de [Struts]-versie
- het model werd verzorgd door dezelfde drie archieven
In de Spring-versie:
- wordt de controller verzorgd door een door Spring geleverde servlet [DispatcherServlet]
- de weergaven worden verzorgd door dezelfde JSP-pagina's als voorheen, op enkele details na
- wordt het model verzorgd door dezelfde drie archieven
We zullen zien dat de overstap van Struts naar Spring voor onze applicatie eenvoudig is, mits we bereid zijn niet alle elementen te gebruiken die worden aanbevolen voor een orthodoxe Spring-architectuur MVC. De belangrijkste aanpassingen zijn de volgende:
- de acties die in Struts werden verwerkt in specifieke methoden van de servlet/controller, of door instanties van klassen afgeleid van de klasse [Action], worden nu verwerkt door instanties van klassen die de Spring-interface [Controller] implementeren
- de benodigde configuratiebestanden zijn nu:
- [web.xml], omdat het hier om een webapplicatie gaat. Dit bestand bevat een Listener die bij het initialiseren van de applicatie gebruikmaakt van het bestand [applicationContext.xml]
- [applicationContext.xml], waarmee de beans kunnen worden aangemaakt die de applicatie nodig heeft, met name die van de service voor toegang tot het model
- De weergaven JSP zullen identiek zijn aan die van Struts. We zullen er een nieuwe moeten aanmaken.
Laten we nog even de architectuur MVC STRUTS uit de vorige versie in herinnering brengen:
| de businessklassen, de klassen voor gegevenstoegang en de database |
| de pagina's JSP |
| de servlet voor de verwerking van clientverzoeken, de objecten [Action] |
Met Spring hanteren we een identieke architectuur:
| de businessklassen, de klassen voor gegevenstoegang en de database |
| de pagina's JSP |
| de servlet voor de verwerking van verzoeken van klanten, de objecten die de interface implementeren [Controller] |
- de controller vormt het hart van de applicatie. Alle verzoeken van de client lopen via deze controller. Het is een generieke servlet die wordt geleverd door SPRING. Deze is van het type [DispatcherServlet]. We zullen deze controller voortaan de [Spring]-controller noemen.
- De controller [Spring] stuurt het verzoek van de client door naar een van de instanties van het type [Controller]. Er is één dergelijke instantie per te verwerken actie. Deze wordt in de opgevraagde URL gedefinieerd, net als bij Struts. Zo weten we dat de opgevraagde actie de actie [liste] is, omdat de opgevraagde URL [liste.do] is
- Als C de context van de applicatie is, maakt de controller [Spring] gebruik van een bestand [C-servlet.xml] dat de rol vervult van het configuratiebestand struts-config.xml in de Struts-versie. Aan elke actie die door de applicatie moet worden verwerkt, wordt de naam van de Controller-klasse gekoppeld die verantwoordelijk is voor de verwerking van het verzoek.
- De controller draagt het stokje over aan het Controller-object dat aan de actie is gekoppeld. Dit doet hij door de methode handleRequest van dit object aan te roepen, waarbij hij het verzoek van de client doorgeeft. Hier voert de ontwikkelaar de benodigde bewerkingen uit: hij zal eventueel gebruik moeten maken van businessklassen of klassen voor gegevenstoegang. Aan het einde van de verwerking geeft het Controller-object de naam van de weergave terug aan de controller, die deze als antwoord naar de klant moet sturen.
- In zijn configuratiebestand vindt de controller de URL die gekoppeld is aan de naam van de weergave die hij moet weergeven. Vervolgens verstuurt hij deze weergave. De communicatie met de klant is voltooid.
3.7.2. Het model
Dit is hetzelfde als in de twee voorgaande versies. Het bestaat uit de Java-archieven [istia.st.articles.dao, istia.st.articles.domain, istia.st.articles.exception].
3.7.3. Configuratie van de applicatie
3.7.3.1. Algemene architectuur
De algemene architectuur van het Eclipse-project is als volgt:

3.7.3.2. Configuratie van de gegevenstoegang
Aangezien de interface voor de toegang tot de gegevens niet verandert, zijn de bijbehorende configuratiebestanden dezelfde als in de vorige versie. Ze zijn gedefinieerd in [WEB-INF/src]:

Op de bovenstaande schermafbeelding zijn de bestanden [articles.xml, spring-config-sqlmap-firebird.xml, sqlmap-config-firebird.xml, log4j.properties] afkomstig uit eerdere versies.
3.7.3.3. De map met de archieven
In [WEB-INF/lib] bevinden zich dezelfde archieven als in de vorige versie, met uitzondering van die van Struts, dat niet langer nodig is:

3.7.3.4. Configuratie van de applicatie
De applicatie wordt geconfigureerd met behulp van drie bestanden: [web.xml, applicationContext.xml, springwebarticles-servlet.xml] in de map [WEB-INF]:

Het bestand [web.xml] ziet er als volgt uit:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- de Spring-contextloader van de applicatie -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- de servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- de URL-toewijzing -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- het invoerdocument -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
Wat staat er in dit bestand?
- de startpagina van de applicatie is [/vues/index.jsp] (welcome-file)
- URL-verzoeken in de vorm *.do worden doorgestuurd naar de servlet [springwebarticles] (servlet-mapping)
- de servlet [springwebarticles] is een instantie van de klasse [org.springframework.web.servlet.DispatcherServlet] (servlet-name, servlet-class) die door Spring wordt geleverd.
- De listener [org.springframework.web.context.ContextLoaderListener] wordt gestart bij het opstarten van de applicatie. Zijn belangrijkste taak is het instantiëren van de Spring-beans die zijn gedefinieerd in het bestand [applicationContext.xml]
Het bestand [applicationContext.xml] ziet er als volgt uit:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- de businessklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- de configuratie van de webapplicatie-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
We zien hier zowel bekende als minder bekende elementen. Bij het opstarten van de applicatie worden drie beans geïnstantieerd:
- articlesDao: service voor toegang tot de laag [dao]
- articlesDomain: service voor toegang tot het model
- config: een bean waarin we de informatie gaan verzamelen die alle clients moeten delen. Deze bean zal de rol vervullen die traditioneel door de applicatiecontext wordt vervuld, maar dan met getypeerde in plaats van ongetypeerde informatie.
Het laatste bestand [springwebarticles.xml] definieert de acties die door de applicatie worden geaccepteerd op een manier die sterk lijkt op die van het Struts-bestand [struts-config.xml]. De inhoud ervan is als volgt:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de actiebeheerders = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- weergavehandler -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- het berichtenbestand -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
Wat staat er in dit configuratiebestand?
- dat onze controller de volgende URL’s zal verwerken:
| om de lijst met artikelen weer te geven |
| om de lijst met artikelen weer te geven |
| om informatie over een bepaald artikel weer te geven |
| om een bepaald artikel te kopen |
| om het winkelmandje te bekijken |
| om een aankoop uit het winkelmandje te verwijderen |
| om een winkelmandje te bevestigen |
- de bovenstaande acties komen één op één overeen met de acties die in eerdere versies door de controller werden verwerkt. Voor elk van deze acties wordt de naam vermeld van de klasse die verantwoordelijk is voor de verwerking ervan. Laten we het voorbeeld nemen van de actie [/panier.do]:
- deze moet worden verwerkt door de bean [VoirPanierController]. Deze naam is willekeurig. Het is slechts een sleutel.
<prop key="/panier.do">VoirPanierController</prop>
- de sleutel [VoirPanierController] is de naam van een bean die in hetzelfde configuratiebestand is gedefinieerd:
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
- de bean [VoirPanierController] definieert:
- de te instantiëren klasse [ istia.st.articles.web.spring.VoirPanierController] om de actie te verwerken
- hoe deze moet worden geïnstantieerd. Hier wordt de bean [config], gedefinieerd door [applicationContext.xml] en geïnstantieerd bij het opstarten van de applicatie, als parameter doorgegeven. Dit gebeurt voor alle acties [Controller]. Zo beschikt elk van deze acties in een privéveld over het object [config], waarin alle informatie staat die door alle clients wordt gedeeld.
- De manier waarop de namen van de weergaven moeten worden omgezet:
<!-- weergavebeheerder -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
Net als bij Struts zal de instantie [Controller] die een actie verwerkt, na verwerking een sleutel teruggeven aan de Spring-controller om aan te geven welke weergave deze moet weergeven. Op basis van deze sleutel kunnen er verschillende strategieën bestaan om de bij de sleutel behorende weergave te genereren. De gekozen strategie is die welke wordt gedefinieerd door de bean [viewResolver]. Hier is deze bean gekoppeld aan de klasse [org.springframework.web.servlet.view.InternalResourceViewResolver] met verschillende initialisatieparameters. Zonder in detail te treden, geeft de bean [viewResolver] hier aan dat als de sleutel van de weergave "XX" is, de gegenereerde weergave [/vues/XX.jsp] zal zijn. Het type weergaven dat naar de client wordt verzonden, kan op verschillende manieren worden gewijzigd:
- door de implementatieklasse voor de bean [viewResolver] te wijzigen
- door de initialisatieparameters van de implementatieklasse te wijzigen
Zo kan men van een weergave HTML naar een weergave XML overschakelen door simpelweg de waarde van de bean [viewResolver] te wijzigen
- de naam van een berichtenbestand voor de applicatie (messageSource). In dit geval bestaat het bestand wel, maar is het leeg. Het wordt niet gebruikt. Het moet in de map [ClassPath] van de applicatie worden geplaatst. Hier wordt het in [WEB-INF/classes] geplaatst. In Eclipse bereikt men dit resultaat door het in [WEB-INF/src] te plaatsen:

3.7.4. De weergaven JSP
De gebruikte weergaven JSP zijn dezelfde als die door Struts worden gebruikt. Er is niets gewijzigd:

Er wordt één nieuwe weergave aangemaakt: redirpanier.jsp. Deze dient uitsluitend om de client door te sturen naar de actie [/panier.do]. De code ervan is als volgt:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
De lezer wordt verzocht de definitie van de verschillende weergaven in de Struts-versie nogmaals door te nemen.
3.7.5. De verwerking van acties
De klassen die nodig zijn voor de verwerking van de verschillende acties zijn gebundeld in het pakket [istia.st.articles.web.spring]:

Laten we de werking van de Spring-applicatie aan de hand van een voorbeeld nog eens op een rijtje zetten:
- de gebruiker vraagt de URL [http://localhost:8080/springwebarticles/main.do] op

Wat is er gebeurd?
- het bestand [web.xml] van de applicatie [springwebarticles] is opgevraagd:
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<!-- de Spring-contextloader van de applicatie -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- de servlet -->
<servlet>
<servlet-name>springwebarticles</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<!-- de URL-toewijzing -->
<servlet-mapping>
<servlet-name>springwebarticles</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- het invoerdocument -->
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
</web-app>
|
- Als dit het eerste verzoek aan de applicatie was, zijn er een aantal dingen in gang gezet:
- de listener [ org.springframework.web.context.ContextLoaderListener] werd geladen
- deze heeft het configuratiebestand [applicationContext.xml] verwerkt:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- de businessklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- de configuratie van de webapplicatie-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
- de bovenstaande beans werden aangemaakt in de context van de applicatie
- vervolgens werd het bestand [springwebarticles-servlet.xml] verwerkt:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de actiebeheerders = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
<!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
<!-- weergavehandler -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass">
<value>org.springframework.web.servlet.view.JstlView</value>
</property>
<property name="prefix">
<value>/vues/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
<!-- het berichtenbestand -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename">
<value>messages</value>
</property>
</bean>
</beans>
|
- de beans [Controller] die door dit bestand worden gedefinieerd, zijn eveneens aangemaakt
- Alles is nu klaar om het verzoek van de klant te verwerken. Dit was: [http://localhost:8080/springwebarticles]. Hier wordt niet gevraagd om een URL uit de context, maar om de context zelf. Er wordt dan gebruikgemaakt van de sectie [welcome-file] uit het bestand [web.xml].
<welcome-file-list>
<welcome-file>/vues/index.jsp</welcome-file>
</welcome-file-list>
- De weergave [index.jsp] ziet er als volgt uit:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/main.do"/>
- de klant wordt dus gevraagd om door te gaan naar de URL [http://localhost:8080/springwebarticles/main.do]. Dat doet hij.
- De Spring-controller ontvangt dus een nieuw verzoek. Hij gebruikt zijn bestand [springwebarticles-servlet.xml]:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de actiebeheerders = controllers -->
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
....
</bean>
<!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
<prop key="/infos.do">InfosController</prop>
<prop key="/achat.do">AchatController</prop>
<prop key="/panier.do">VoirPanierController</prop>
<prop key="/retirerachat.do">RetirerAchatController</prop>
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
</beans>
|
- Dit bestand geeft aan dat de actie [/main.do] moet worden verwerkt door de bean [ListController].
- Het verzoek van de client wordt doorgestuurd naar de methode [handleRequest] van de bean [ListController]. Deze methode voert zijn taak uit en geeft de controller de sleutel van de weer te geven weergave terug. Als alles goed gaat, is deze sleutel hier [liste].
- De Spring-controller gebruikt de bean [viewResolver] uit het configuratiebestand [springwebarticles-servlet.xml] om de weergave te bepalen die bij deze sleutel hoort. In dit geval is dat de weergave [/vues/liste.jsp]
- de weergave [/vues/liste.jsp] wordt naar de client verzonden
3.7.6. Initialisatie van de Spring-applicatie
We hebben gezegd dat bij het opstarten van de applicatie de beans uit het bestand [applicationContext.xml] worden geïnstantieerd:
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- de klasse voor gegevenstoegang -->
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoSqlMap">
<constructor-arg index="0">
<value>sqlmap-config-firebird.xml</value>
</constructor-arg>
</bean>
<!-- de businessklasse -->
<bean id="articlesDomain" class="istia.st.articles.domain.AchatsArticles">
<constructor-arg index="0">
<ref bean="articlesDao"/>
</constructor-arg>
</bean>
<!-- de configuratie van de webapplicatie-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
</beans>
|
We kennen de beans [articlesDao, articlesDomain], maar niet de bean [config]. Deze wordt gedefinieerd door de volgende Java-klasse:
| package istia.st.articles.web.spring;
import java.util.Hashtable;
import istia.st.articles.domain.IArticlesDomain;
/**
* @author ST - ISTIA
*/
public class Config {
// privévelden
private IArticlesDomain articlesDomain = null;
private final String ACTION_LISTE = "liste.do";
private final String ACTION_PANIER = "panier.do";
private final String ACTION_VALIDATION_PANIER = "validerpanier.do";
private final String lienActionListe = "Liste des articles";
private final String lienActionPanier = "Voir le panier";
private final String lienActionValidationPanier = "Valider le panier";
private Hashtable hActionListe = new Hashtable(2);
private Hashtable hActionPanier = new Hashtable(2);
private Hashtable hActionValidationPanier = new Hashtable(2);
// getters en setters
public IArticlesDomain getArticlesDomain() {
return articlesDomain;
}
public void setArticlesDomain(IArticlesDomain articlesDomain) {
this.articlesDomain = articlesDomain;
}
public Hashtable getHActionListe() {
return hActionListe;
}
public Hashtable getHActionPanier() {
return hActionPanier;
}
public Hashtable getHActionValidationPanier() {
return hActionValidationPanier;
}
// initialisatie van de webapplicatie
public void init() {
// we slaan bepaalde URL's van de applicatie op
hActionListe.put("href", ACTION_LISTE);
hActionListe.put("lien", lienActionListe);
hActionPanier.put("href", ACTION_PANIER);
hActionPanier.put("lien", lienActionPanier);
hActionValidationPanier.put("href", ACTION_VALIDATION_PANIER);
hActionValidationPanier.put("lien", lienActionValidationPanier);
// klaar
return;
}
}
|
Deze klasse doet wat de methode [init] van een webservlet doet. Ze initialiseert de applicatie. Hier verloopt dit als volgt:
- omdat de bean [config] als volgt is gedefinieerd in [applicationContext.xml]:
<!-- de configuratie van de webapplicatie-->
<bean id="config" class="istia.st.articles.web.spring.Config" init-method="init">
<property name="articlesDomain">
<ref bean="articlesDomain"/>
</property>
</bean>
bij het aanmaken ervan is het privéveld [articlesDomain] geïnitialiseerd
- Vervolgens wordt, vanwege het attribuut [init-method="init"] van de bovenstaande bean, de methode [init] van de aan de bean gekoppelde klasse uitgevoerd. Hier initialiseert deze methode de drie woordenboeken [hActionListe, hActionPanier, hActionValidationPanier] die worden gebruikt om de drie mogelijke links te genereren in het menu dat aan de gebruiker wordt aangeboden.
- Er worden openbare accessors aangemaakt om deze privévelden toegankelijk te maken voor instanties van het type [Controller] die de acties zullen verwerken.
3.7.7. De acties [Controller] van de Spring-applicatie
3.7.7.1. Inleiding
Elke Spring-actie wordt ondergebracht in een klasse van het type [Controller]. In de Struts-versie werd elke actie ondergebracht in een klasse van het type [Action]. Het schrijven van de klasse [Controller] bestaat meestal uit:
- het kopiëren en plakken van de klasse [Action] die in de Struts-versie werd gebruikt
- de code aan te passen aan de Spring-conventies
3.7.7.2. main.do, liste.do
Deze twee acties zijn identiek en worden in [springwebarticles-servlet.xml] gedefinieerd door:
| <!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/liste.do">ListController</prop>
<prop key="/main.do">ListController</prop>
...
</props>
</property>
</bean>
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
Ze zijn gekoppeld aan de klasse [ istia.st.articles.web.spring.ListController], die we straks nader zullen toelichten. Wanneer een van deze acties in een browser wordt uitgevoerd, krijgt men het volgende resultaat:

De code van de klasse [istia.st.articles.web.spring.ListController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ListController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// de lijst met artikelen wordt opgevraagd
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// de foutweergave verzenden
return new ModelAndView("erreurs");
}
// de artikellijst wordt weergegeven
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// de weergave verzenden
return new ModelAndView("liste");
}
}
|
Opmerkingen:
- de klasse heeft een privéveld [config]. Dit veld is door Spring geïnitialiseerd toen de bean [ListController] werd geïnstantieerd:
<bean id="ListController" class="istia.st.articles.web.spring.ListController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
Hierboven is te zien dat het veld [config] van [ListController] wordt geïnitialiseerd met de bean [config]. Wat houdt dit in? Het gaat om de bean [config] die is gedefinieerd in [applicationContext.xml] en c.a.d. Een instantie van [istia.st.articles.web.spring.Config], zoals hierboven beschreven.
- Het schrijven van de code voor een klasse [Controller] bestaat in wezen uit het schrijven van de code voor de methode [handleRequest]
- We vragen de lijst met artikelen op bij het model. Dit is toegankelijk via [config.getArticlesDomain()]. Als er een uitzondering optreedt, wordt de weergave [ERREURS] verzonden. Het door [handleRequest] geretourneerde resultaat moet van het type [ModelAndView] zijn. Deze klasse kan op verschillende manieren worden geïnstantieerd. Hier, en dat zal altijd het geval zijn, wordt een instantie van [ModelAndView] aangemaakt door de sleutel van de weer te geven weergave door te geven. Ter herinnering: door de configuratie van de bean [viewResolver] zal het opvragen van de weergave met sleutel XX ertoe leiden dat de weergave [/vues/XX.jsp] wordt verzonden.
| // de artikellijst wordt opgevraagd
List articles = null;
try {
articles = config.getArticlesDomain().getAllArticles();
} catch (UncheckedAccessArticlesException ex) {
// de fout wordt opgeslagen
ArrayList erreurs = new ArrayList();
erreurs.add("Erreur lors de l'obtention de tous les articles : "
+ ex.toString());
// de foutpagina wordt weergegeven
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
// de foutpagina verzenden
return new ModelAndView("erreurs");
}
|
- als er geen fouten zijn, wordt de weergave [LISTE] verzonden:
| // de lijst met artikelen weergeven
request.setAttribute("listarticles", articles);
request.setAttribute("message", "");
request.setAttribute("actions", new Hashtable[] { config
.getHActionPanier() });
// de weergave verzenden
return new ModelAndView("liste");
|
3.7.7.3. infos.do
Deze actie dient om informatie te verstrekken over een van de artikelen die in de weergave [LISTE] worden getoond:
Deze actie is gedefinieerd in [springwebarticles-servlet.xml] door:
| <!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/infos.do">InfosController</prop>
...
</props>
</property>
</bean>
...
<bean id="InfosController"
class="istia.st.articles.web.spring.InfosController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
De code van de klasse [InfosController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class InfosController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// de lijst met fouten
ArrayList erreurs = new ArrayList();
// de gevraagde id wordt opgehaald
String strId = request.getParameter("id");
// iets?
if (strId == null) {
// niet normaal
erreurs.add("action incorrecte([infos,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// strId wordt omgezet in een geheel getal
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// niet normaal
erreurs.add("action incorrecte([infos,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// het artikel met de sleutel-ID wordt opgevraagd
Article article = null;
try {
article = config.getArticlesDomain().getArticleById(id);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
if (article == null) {
// niet normaal
erreurs.add("Article de clé [" + id + "] inexistant");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("erreurs");
}
// het artikel wordt in de sessie geplaatst
request.getSession().setAttribute("article", article);
// de informatiepagina wordt weergegeven
request.setAttribute("actions", new Hashtable[] { config.getHActionListe() });
return new ModelAndView("infos");
}
}
|
Opmerkingen:
- De methode [handleRequest] haalt de parameter [id] op, die normaal gesproken in de URL moet staan. Deze moet namelijk de vorm [/infos.do?id=X] hebben. Er worden verschillende tests uitgevoerd om de aanwezigheid en geldigheid van de parameter [id] te controleren. Bij een probleem wordt de weergave [ERREURS] verzonden.
- Als [id] geldig is, wordt het bijbehorende artikel opgevraagd bij de laag [domain]. Als deze een uitzondering genereert of als het artikel niet wordt gevonden, wordt ook hier de weergave [ERREURS] verzonden.
- Als alles goed gaat, wordt het verkregen artikel in de sessie opgeslagen. Dit is een discutabel punt. Hier gaan we ervan uit dat de klant dit artikel mogelijk zal kopen. Als hij dat doet, halen we het uit de sessie, in plaats van het opnieuw op te vragen bij de laag [domain].
- Ten slotte wordt de weergave [INFOS] weergegeven.
3.7.7.4. achat.do
Deze actie dient om het artikel te kopen dat in de vorige weergave [INFOS] werd getoond:

Als we de code HTML van deze weergave bekijken, zien we dat de tag <form> als volgt is gedefinieerd:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
We zien dus dat het formulier naar de controller wordt verzonden met de actie [achat.do].
Deze actie is als volgt geconfigureerd in [springwebarticles-servlet.xml]:
<!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/achat.do">AchatController</prop>
...
</props>
</property>
</bean>
<bean id="AchatController"
class="istia.st.articles.web.spring.AchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
De code van de klasse [AchatController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.dao.Article;
import istia.st.articles.domain.Achat;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class AchatController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de gekochte hoeveelheid wordt opgehaald
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// verkeerde hoeveelheid
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
// de sessie van de klant wordt opgehaald
HttpSession session = request.getSession();
// het artikel in de sessie ophalen
Article article = (Article) session.getAttribute("article");
// is de sessie verlopen?
if (article == null) {
// de foutpagina wordt weergegeven
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// we maken de nieuwe aankoop aan
Achat achat = new Achat(article, qté);
// de aankoop wordt aan het winkelmandje van de klant toegevoegd
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
// terug naar de artikellijst
return new ModelAndView("index");
}
}
|
Opmerkingen:
- Laten we nog eens de vorm van het formulier dat naar de controller wordt verzonden in herinnering brengen:
<form method="post" action="achat.do?id=3"/>
<table>
<tr>
<td><input type="submit" value="Acheter"></td>
<td>Qte <input type="text" name="qte" size="3" value=""></td>
<td></td>
</tr>
</table>
</form>
- Er zijn twee parameters in de aanvraag: [id]: nummer van het gekochte artikel, [qte]: gekochte hoeveelheid.
- Er wordt gecontroleerd of de parameter [qte] aanwezig en geldig is. Als deze parameter als onjuist wordt herkend, wordt de weergave [INFOS] teruggestuurd naar de gebruiker, vergezeld van een foutmelding:
| // de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de gekochte hoeveelheid wordt opgehaald
int qté = 0;
try {
qté = Integer.parseInt(request.getParameter("qte"));
if (qté <= 0)
throw new NumberFormatException();
} catch (NumberFormatException ex) {
// verkeerde hoeveelheid
request.setAttribute("msg", "Quantité incorrecte");
request.setAttribute("qte", request.getParameter("qte"));
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("infos");
}
|
- het gekochte artikel wordt opgehaald uit de sessie. Deze kan zijn verlopen. In dat geval wordt de weergave [ERREURS] verzonden:
| // de sessie van de klant ophalen
HttpSession session = request.getSession();
// het artikel in de sessie ophalen
Article article = (Article) session.getAttribute("article");
// is de sessie verlopen?
if (article == null) {
// de foutpagina wordt weergegeven
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- Als de sessie niet is verlopen, wordt het artikel in het winkelmandje geplaatst, dat eveneens uit de sessie wordt opgehaald:
| // de nieuwe aankoop wordt aangemaakt
Achat achat = new Achat(article, qté);
// het aankoopartikel wordt aan het winkelmandje van de klant toegevoegd
Panier panier = (Panier) session.getAttribute("panier");
if (panier == null) {
panier = new Panier();
session.setAttribute("panier", panier);
}
panier.ajouter(achat);
|
- ten slotte wordt de weergave [LISTE] verzonden:
// terug naar de artikellijst
return new ModelAndView("index");
- Hierboven wordt de weergave [/vues/index.jsp] verzonden. We weten dat deze de browser van de klant vraagt om door te verwijzen naar de URL [/main.do]. Door deze omleiding wordt de lijst met artikelen weergegeven.
3.7.7.5. panier.do
Deze actie dient om alle aankopen van de klant weer te geven. Ze is beschikbaar via het menu:

De code HTML die bij de bovenstaande link hoort, is als volgt:
<a href="panier.do">Voir le panier</a>
De pagina die via deze link wordt weergegeven, is de volgende:

Deze actie is als volgt geconfigureerd in [springwebarticles-servlet.xml]:
| <!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/panier.do">VoirPanierController</prop>
...
</props>
</property>
</bean>
...
<bean id="VoirPanierController"
class="istia.st.articles.web.spring.VoirPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
De code van de klasse [VoirPanierController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class VoirPanierController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// het winkelmandje wordt weergegeven
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null || panier.getAchats().size() == 0) {
// winkelwagen leeg
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("paniervide");
} else {
// er zit iets in het winkelmandje
request.setAttribute("actions", new Hashtable[] {
config.getHActionListe(), config.getHActionValidationPanier() });
return new ModelAndView("panier");
}
}
}
|
Opmerkingen:
- het winkelmandje wordt opgehaald uit de sessie waarin het zich normaal gesproken bevindt. De sessie kan zijn verlopen, waardoor er geen winkelmandje is. Dit wordt niet als een fout behandeld, maar er wordt simpelweg aangenomen dat het winkelmandje leeg is.
- Als het winkelmandje leeg is, wordt de weergave [PANIERVIDE] weergegeven
- anders is het de weergave [PANIER]
3.7.7.6. retirerachat.do
Deze actie dient om een aankoop uit het winkelmandje te verwijderen:

Als we de code HTML van de bovenstaande link bekijken, zien we het volgende:
<a href="retirerachat.do?id=3">Retirer</a>
De actie [retirerachat.do] ontvangt dus als parameter de id van het artikel dat uit het winkelmandje moet worden verwijderd. Deze actie is als volgt geconfigureerd in [springwebarticles-servlet.xml]:
| <!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/retirerachat.do">RetirerAchatController</prop>
</props>
</property>
</bean>
...
<bean id="RetirerAchatController"
class="istia.st.articles.web.spring.RetirerAchatController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
De code van de klasse [RetirerAchatController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class RetirerAchatController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// het winkelmandje wordt opgehaald
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// het ID van het te verwijderen artikel ophalen
String strId = request.getParameter("id");
// iets?
if (strId == null) {
// niet normaal
erreurs.add("action incorrecte([retirerachat,id=null]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// we zetten strId om in een geheel getal
int id = 0;
try {
id = Integer.parseInt(strId);
} catch (Exception ex) {
// niet normaal
erreurs.add("action incorrecte([retirerachat,id=" + strId + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// de aankoop wordt verwijderd
panier.enlever(id);
// het winkelmandje wordt opnieuw weergegeven
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
}
}
|
Opmerkingen:
- de code controleert of de parameter [id] aanwezig en geldig is. Als deze onjuist blijkt te zijn, wordt de weergave [ERREURS] verzonden.
- Anders wordt het artikel uit het winkelmandje verwijderd:
// de aankoop wordt verwijderd
panier.enlever(id);
- waarna het winkelmandje opnieuw wordt weergegeven:
// het winkelmandje wordt opnieuw weergegeven
request.setAttribute("actions",
new Hashtable[] { config.getHActionListe() });
return new ModelAndView("redirpanier");
Laten we de code van de weergave [/vues/redirpanier.jsp] nog eens bekijken:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<c:redirect url="/panier.do"/>
We zien dat de klant wordt doorgestuurd naar de actie [/panier.do]. Deze is al beschreven. Afhankelijk van de status van het winkelmandje wordt de weergave [PANIER] of [PANIERVIDE] weergegeven.
3.7.7.7. validerpanier.do
Deze actie dient om de door de klant gedane aankopen te bevestigen. Concreet komt dit neer op één enkele handeling: de voorraad van de gekochte artikelen wordt in de database met de gekochte hoeveelheden verminderd. Deze actie is afkomstig uit het volgende menu:

De code HTML van de link [Valider le panier] is als volgt:
<a href="validerpanier.do">Valider le panier</a>
Wanneer deze koppeling wordt geactiveerd, worden de voorraden bijgewerkt en wordt de artikellijst opnieuw weergegeven.
Deze actie is als volgt geconfigureerd in [springwebarticles-servlet.xml]:
| <!-- de mapping van de applicatie-->
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
...
<prop key="/validerpanier.do">ValiderPanierController</prop>
</props>
</property>
</bean>
...
<bean id="ValiderPanierController"
class="istia.st.articles.web.spring.ValiderPanierController">
<property name="config">
<ref bean="config"/>
</property>
</bean>
|
De code van de klasse [ValiderPanierController] is als volgt:
| package istia.st.articles.web.spring;
import istia.st.articles.domain.Panier;
import istia.st.articles.exception.UncheckedAccessArticlesException;
import java.util.ArrayList;
import java.util.Hashtable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class ValiderPanierController implements Controller {
// de configuratie van de webapp
Config config;
public void setConfig(Config config) {
this.config = config;
}
// verwerking van het verzoek
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de koper heeft zijn winkelmandje bevestigd
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// het winkelmandje wordt gevalideerd
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// eventuele fouten worden opgehaald
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
// alles lijkt in orde OK - de lijst met artikelen wordt weergegeven
return new ModelAndView("index");
}
}
|
Opmerkingen:
- het winkelmandje wordt uit de sessie opgehaald. Als de sessie is verlopen, wordt de weergave [ERREURS] verzonden:
| // de lijst met fouten bij deze actie
ArrayList erreurs = new ArrayList();
// de koper heeft zijn winkelmandje bevestigd
Panier panier = (Panier) request.getSession().getAttribute("panier");
if (panier == null) {
// sessie verlopen
erreurs.add("Votre session a expiré");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
|
- De aankopen in het winkelmandje worden bevestigd. Er kunnen fouten optreden als de voorraad onvoldoende is om aan de bestelling te voldoen. In dat geval wordt de weergave [ERREURS] verzonden:
| // het winkelmandje wordt bevestigd
try {
config.getArticlesDomain().acheter(panier);
} catch (UncheckedAccessArticlesException ex) {
// niet normaal
erreurs.add("Erreur d'accès aux données [" + ex.toString() + "]");
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe() });
return new ModelAndView("erreurs");
}
// eventuele fouten worden opgehaald
erreurs = config.getArticlesDomain().getErreurs();
if (erreurs.size() != 0) {
request.setAttribute("erreurs", erreurs);
request.setAttribute("actions", new Hashtable[] { config
.getHActionListe(), config.getHActionPanier() });
return new ModelAndView("erreurs");
}
|
- Als alles goed is verlopen, wordt de lijst met artikelen opnieuw weergegeven:
// alles lijkt in orde OK - de lijst met artikelen wordt weergegeven
return new ModelAndView("index");
We weten dat de weergave [/vues/index.jsp] de klant doorverwijst naar de actie [/main.do]. Deze actie geeft de weergave [LISTE] weer.