Java na web IX. - Databáze

V dnešním díle se budeme věnovat databázím. Popíšeme si fungování JDBC, JNDI, driverů, konfiguraci aplikace i aplikačního serveru, instalaci databázového serveru a nakonec si pomocí nabytých znalostí vylepšíme naši aplikaci.

9.7.2013 00:00 | Petr Horáček | přečteno 10828×

Databáze jsou téměř neoddělitelnou součástí webových aplikací, naše aplikace z minulého dílu využívala jako databázi jednoduchý Java seznam, to ale není úplně ono. V dnešním díle si představíme práci s SQL databázemi prostřednictvím Tomcatu. Začneme teoretickým úvodem, pokračovat budeme možnostmi konfigurace spojení s databází a nakonec si zakomponujeme databázi i do naší aplikace.

Databáze a Tomcat

Pro komunikaci s databázemi používá Tomcat technologii JDBC (Java Database Connectivity), ta slouží jako prostřední vrstva mezi aplikací a databázovým serverem. Pro práci s konkrétním databázovým systémem využívá JDBC tzv. drivery. Díky tomu se nemusíme starat o nízkoúrovňovou komunikaci s databázovým serverem a pouze využíváme API JDBC.

Aby pod množstvím spojení nedocházelo k přetížení, využívá JDBC takzvaného poolování. Při každém volání databáze není vytvářeno nové spojení, ale využije se spojení vytvořené předchozím dotazem, jde tedy o jakousi recyklaci. Nastavení poolování i připojení k databázi lze snadno provést pomocí JNDI (Java Naming and Directory Interface) Resource.

Instalace databázového serveru

Nyní trochu odbočíme od Javy a popíšeme si instalaci databázového serveru. V naší aplikaci budeme používat databázový systém MySQL (více se o něm můžete dozvědět zde: http://www.linuxsoft.cz/article_list.php?id_kategory=232), pusťme se tedy do jeho instalace:

sudo apt-get install mysql-server

Během instalace budete požádání o vyplnění hesla administrátora databázového serveru. Pokud nechcete aby se server spouštěl při každém spuštění počítače a nezatěžovat zbytečně systém, proveďte následující kroky: Otevřete soubor mysql.conf ve složce /etc/init:

sudo nano /etc/init/mysql.conf

Pomocí dvojité mřížky (#) zakomentujte řádky začínající „start on“ a soubor uložte (Ctrl + X, yes). Nyní je vždy pro spuštění databázového serveru nutné zadat příkaz:

sudo service mysql start

Konfigurace

A nyní se již dejme do konfigurace Tomcatu a aplikace samotné.

JDBC Driver

Nejprve je nutné přidat knihovnu driveru používané databáze, v našem případě tedy MySQL. Pokud se rozhodnete umístit knihovnu přímo do aplikace, stačí v projektu NetBeans kliknout pravým tlačítkem na záložku Libraries, Add Library a vyhledat knihovnu MySQL JDBC Driver.

Pokud chcete knihovnu zpřístupnit hromadně všem aplikacím na aplikačním serveru, stáhněte ji z adresy http://dev.mysql.com/downloads/connector/j/ a její JAR archiv vložte do složky lib v místě instalace Tomcatu.

JNDI Resource, možnost A

Konfigurace přístupu k databázi probíhá pomocí tzv. JNDI Resource, ten lze použít v různém rozsahu a na různém umístění.

První možností je jeho umístění přímo v souboru context.xml aplikace a mít tak všechno nastavení na jednom místě. V tomto případě bude konfigurace načtena při startu aplikace a přístup k databázi bude umožněn pouze jí. Soubor context.xml je umístěn ve složce META-INF aplikace a v základu může vypadat nějak takto:

<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/JNW">
  
</Context>

Všimněte si, že pod atributem path se nachází kořenová adresa aplikace. Pro přidání resource je nutné do těla tagu Context přidat následující:

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver"
    maxActive="15" maxIdle="3"
    name="jdbc/nazevDatabaze" type="javax.sql.DataSource"
    url="jdbc:mysql://server:3306/nazevDatabaze?useEncoding=true&amp;characterEncoding=UTF-8"
    username="jmeno" password="heslo" validationQuery="/* ping */ SELECT 1"/>

DriverClassName zadává cestu ke třídě driveru, maxActive nastavuje maximální počet aktivních spojení, maxIdle pak nastavuje maximální počet připravených a neaktivních spojení čekajících v poolu na dotazy, name udává název databáze, přes který budeme ke zdroji přistupovat, pomocí url zadáváme adresu k databází (navíc přidáme parametry pro správné kódování), username a password udává přístupové údaje, validationQuery je dotaz prováděný vždy před připojením k databázi (pomocí SELECT 1 vždy „pingneme“ na spojení a zamezíme tak problémům s již ukončeným spojením).

JNDI Resource, možnost B

Druhou možností konfigurace je definování Resource v souboru Tomcatu server.xml, v tomto případě budou moci komunikovat s databázi všechny aplikace. Pokud chcete využívat tento způsob, vložte do souboru server.xml tento kód (pozn.: tag GlobalNamingResources už nejspíš někde v souboru bude):

<GlobalNamingResources>
    <Resource auth="Container" driverClassName="com.mysql.jdbc.Driver"
        maxActive="15" maxIdle="3"
        description="Global Naming Resource"
        name="jdbc/nazevGlobalDatabaze" type="javax.sql.DataSource"
        url="jdbc:mysql://server:3306/nazevDatabaze?useEncoding=true&amp;characterEncoding=UTF-8"
        username="jmeno" password="heslo" validationQuery="/* ping */ SELECT 1"/>
</GlobalNamingResources>

Jak vidíte, jediný rozdíl oproti předchozímu způsobu je umístění a také obalení Resource tagem GlobalNamingResources.

Tip: Úpravu konfiguračního souboru Tomcatu server.xml lze provést přímo z NetBeans. Přesuňte se do karty Services, rozklikněte záložku Servers, klikněte pravým tlačítkem na server Apache Tomcat a zvolte Edit server.xml.

Pokud chcete takto definovaný zdroj využívat v aplikaci musíte vytvořit tzv. ResourceLink, ten je třeba vložit do souboru context.xml aplikace a může vypadat například takto:

<ResourceLink name="jdbc/nazevDatabaze"
global="jdbc/nazevGlobalDatabaze"
type="javax.sql.DataSource" />

Pomocí parametru name budeme přistupovat ke spojení z aplikace, parametr global udává název zdroje zadaný v GlobalNamingResources.

Získání spojení

Posledním krokem před samotným zasíláním dotazů je získání spojení s databází:

Context initCtx = new InitialContext();
Context ctx = (Context) initCtx.lookup("java:comp/env");
DataSource ds = (DataSource) ctx.lookup("jdbc/nazevDatabaze");
Connection conn = ds.getConnection();

Nejprve získáme kontext aplikace, poté se v něm pokusíme pomocí nadefinovaného názvu najít zdroj dat a z něj nakonec získáme samotné spojení.

Rozšíření aplikace

Vraťme se nyní k naší aplikaci na níž si použití databází předvedeme.

Vytvoření databáze a tabulky

Nejprve musíme pro naši aplikaci vytvořit databázi, využijeme pro to prostředí NetBeans. Přesuňte se do karty Services, klikněte pravým tlačítkem na záložku Databases a zvolte New connection. Objeví se před vámi průvodce, v prvním okně vyberete Driver, v dalším vyplníte přihlašovací údaje k databázovému serveru, položku Database vymažte. Další okno nechejte být a v posledním okně zadejte libovolný název spojení. Vytvořené spojení se objeví pod záložkou Databases, klikněte na něj pravým tlačítkem a zvolte Execute Command. Do nového editačního okna vložte tento příkaz a spusťe jej ikonkou „Run SQL“:

CREATE DATABASE JNW;

To vytvoří novou databázi JNW. Klikněte pravým tlačítkem na před chvílí vytvořené spojení v levém panelu a zvolte Referesh, teď byste měli v seznamu databází vidět i tu vaši. Klikněte na ni pravým tlačítkem a zvolte Set as default catalog, poté ji rozklikněte, na položku Tables klikněte pravým tlačítkem, stiskněte Execute Command a vložte příkaz:

CREATE TABLE zapisky ( 
  id int AUTO_INCREMENT,
  nadpis varchar(30),
  obsah varchar(300),
  PRIMARY KEY (id)
);

Nyní by měla být vytvořena nová tabulka zapisky s indexem id a dvěma textovými poli nadpis a obsah. Když kliknete pravým tlačítkem na záložku Tables a stisknete Refresh, měla by pod záložku přibýt nová tabulka, klikněte na ni pravým tlačítkem, View Data. Objeví se data uložená v tabulce (zatím žádná nejsou), je možné je editovat, mazat či přidávat nové, to vše v grafickém rozhraní.

Grafické prostředí NetBeans

Konfigurace Resource

Konfiguraci připojení k databázi umístíme do souboru context.xml aplikace, ještě před tím ale klikněte na záložku Libraries a importujte knihovnu MySQL JDBC Driver.

Nyní můžete v kartě Configuration files otevřít soubor context.xml, pokud jste jej ještě neupravovali, bude se zde nejspíš nacházet jediný uzavřený tag Context. Udělejte z něj párový tag a dovnitř vložte konfiguraci přístupu do databáze. Výsledný kód může vypadat nějak takto:

<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="[původníUrlAplikace]">
    <Resource auth="Container" driverClassName="com.mysql.jdbc.Driver"
        maxActive="15" maxIdle="3"
        name="jdbc/mysql" type="javax.sql.DataSource"
        url="jdbc:mysql://localhost:3306/JNW?useEncoding=true&amp;characterEncoding=UTF-8"
        username="[jmeno]" password="[heslo]" validationQuery="/* ping */ SELECT 1"/>
</Context>

Úprava modelu

Musíme také trochu upravit model Zapisek. S přechodem na databázi nám totiž přibyla nová proměnná id, kterou jsme dosud získávali ze seznamu. Upravená třída bude vypadat takto:

package modely;

public class Zapisek {
    
    private int id;
    private String nadpis;
    private String obsah; 
    
    public Zapisek(String nadpis, String obsah){
        this.nadpis = nadpis;
        this.obsah = obsah;
    }
    
    public Zapisek(int id, String nadpis, String obsah){
        this.id = id;
        this.nadpis = nadpis;
        this.obsah = obsah;
    }
    
    public void setId(int id){
        this.id = id;
    }
    
    public void setNadpis(String nadpis){
        this.nadpis = nadpis;
    }
    
    public void setObsah(String obsah){
        this.obsah = obsah;
    }
    
    public int getId(){
        return id;
    }
    
    public String getNadpis(){
        return nadpis;
    }
    
    public String getObsah(){
        return obsah;
    }  
}

Práce s databází

Pro práci s databází si vytvoříme v záložce Source Packages nový balíček databaze, do něj vložte novou Java Class s názvem UpravaZapisku. V této třídě se budou nacházet všechny operace probíhající mezi zápisky a databází, tedy: získání všech zápisků, získání konkrétního zápisku, uložení zápisku, přidání zápisku a smazání zápisku.

Abychom se nemuseli opakovat ve vypisování kódu pro získání spojení, vytvoříme si jednoduchou metodu:

private Connection getConnection() throws NamingException, SQLException {
    Context initCtx = new InitialContext();
    Context ctx = (Context) initCtx.lookup("java:comp/env");
    DataSource ds = (DataSource) ctx.lookup("jdbc/mysql");
    return(ds.getConnection());
}

getZapisky()

Další metoda bude sloužit k získání všech uložených zápisků, zde je její kód:

public List<Zapisek> getZapisky() throws SQLException {
    Connection connection = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;       
    List<Zapisek> zapisky = new ArrayList();

    try {
        String query = "SELECT * FROM zapisky";            
        connection = getConnection();            
        stmt = connection.prepareStatement(query);           
        rs = stmt.executeQuery();
            
        while (rs.next()) {
            zapisky.add(new Zapisek(rs.getInt("id"), rs.getString("nadpis"), rs.getString("obsah")));
        }
    } catch (NamingException ex) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException sQLException) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, sQLException);
    } finally {
        if(rs != null) {rs.close();}
        if(stmt != null) {stmt.close();}
        if(connection != null) {connection.close();}
    }
    return zapisky;
}

Jak vidíte, metoda getZapisky() vrací seznam modelů Zapisek. Nejdříve si přichystáme potřebné třídy Connection, PreparedStatement a ResultSet z balíčku java.sql.* a prázdný seznam zapisky.

Další část kódu je uzavřená ve vyjímce try, catch, finally, ve které zachycujeme chyby NamingException a SQLException. Do proměnné query vložíme samotný SQL dotaz, do proměnné connection přiřadíme spojení získané vytvořenou metodou. Do PreparedStatementu stmt přichystáme dotaz a nakonec dotaz odešleme a odpověď databáze zapíšeme do ResultSetu rs.

Pomocí cyklu while s podmínkou rs.next() projdeme všechny vrácené řádky. Ke sloupcům ve vrácených řádcích můžeme přistupovat pomocí jejich názvu (např.: rs.getInt('id'); rs.getString('nadpis'); a podobně) nebo pořadí (např.: rs.getInt(1);). V našem případě získaná data zapíšeme do seznamu.

Zvlášť důležité je nevynechat závěrečný kód finally, v něm ukončujeme spojení a vracíme jej do poolu. Pokud bychom tak neučinili, žádná spojení by se nevracela a zanedlouho by došlo k jejich vyčerpání (dosažení hodnoty maxActive).

getZapisek()

Velmi podobná bude i metoda pro získání zápisku definovaného pomocí jeho ID:

public Zapisek getZapisek(int id) throws SQLException {
    Connection connection = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;        
    Zapisek zapisek = null;

    try {
        String query = "SELECT * FROM zapisky WHERE id = ?";            
        connection = getConnection();            
        stmt = connection.prepareStatement(query); 
        stmt.setInt(1, id);
        rs = stmt.executeQuery();
            
        while (rs.next()) {
            zapisek = new Zapisek(rs.getInt("id"), rs.getString("nadpis"), rs.getString("obsah"));
        }
    } catch (NamingException ex) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException sQLException) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, sQLException);
    } finally {
        if(rs != null) {rs.close();}
        if(stmt != null){stmt.close();}
        if(connection != null){connection.close();}
    }
    return zapisek;
}

Kód je velice podobný, všimněte si rozdílu zde:

String query = "SELECT * FROM zapisky WHERE id = ?";            
connection = getConnection();            
stmt = connection.prepareStatement(query); 
stmt.setInt(1, id);
rs = stmt.executeQuery();

V dotazu query se objevil otazník, ten udává místo pro vložení proměnné, ta je definována na čtvrtém řádku, kde za první otazník dosazujeme proměnnou id. Pomocí třídy PreparedStatement provedeme korektní dosazení hodnot do dotazu a zároveň ochráníme databázi před SQL injection útoky.

setZapisek()

Další velice podobný kód, tentokrát používáme příkaz UPDATE, a proto místo metody stmt.executeQuery() využijeme stmt.executeUpdate(). Jako výsledek této operace můžeme přijmout například index uloženého zápisku, ten ale nyní nepotřebujeme, a proto necháme odpověď netknutou.

public void setZapisek(int id, String nadpis, String obsah) throws SQLException {
    Connection connection = null;
    PreparedStatement stmt = null;
        
    try {
        String query = "UPDATE zapisky SET nadpis = ?, obsah = ? WHERE id = ?";            
        connection = getConnection();            
        stmt = connection.prepareStatement(query); 
        stmt.setString(1, nadpis);
        stmt.setString(2, obsah);
        stmt.setInt(3, id);
        stmt.executeUpdate();            
    } catch (NamingException ex) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException sQLException) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, sQLException);
    } finally {
        if(stmt != null){stmt.close();}
        if(connection != null){connection.close();}
    }
}

addZapisek()

U příkazu INSERT opět využíváme stmt.executeUpdate().

public void addZapisek(String nadpis, String obsah) throws SQLException {
    Connection connection = null;
    PreparedStatement stmt = null;
        
    try {
        String query = "INSERT INTO zapisky (nadpis, obsah) VALUES (?, ?)";            
        connection = getConnection();            
        stmt = connection.prepareStatement(query); 
        stmt.setString(1, nadpis);
        stmt.setString(2, obsah);
        stmt.executeUpdate();          
    } catch (NamingException ex) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException sQLException) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, sQLException);
    } finally {
        if(stmt != null){stmt.close();}
        if(connection != null){connection.close();}
    }
}

removeZapisek()

A poslední metoda využívající příkaz DELETE.

public void removeZapisek(int id) throws SQLException {
    Connection connection = null;
    PreparedStatement stmt = null;
        
    try {
        String query = "DELETE FROM zapisky WHERE id = ?";            
        connection = getConnection();            
        stmt = connection.prepareStatement(query); 
        stmt.setInt(1, id);
        stmt.executeUpdate();            
    } catch (NamingException ex) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, ex);
    } catch (SQLException sQLException) {
        Logger.getLogger(UpravaZapisku.class.getName()).log(Level.SEVERE, null, sQLException);
    } finally {
        if(stmt != null){stmt.close();}
        if(connection != null){connection.close();}
    }
}

Controller

Nyní provedeme úpravu controlleru, ten bude nyní využívat metody a funkce třídy UpravaZapisku.

public class Controller extends HttpServlet {   
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        String adresa = request.getServletPath();
        UpravaZapisku upravaZapisku = new UpravaZapisku();
        
        if(adresa.equals("/zapisky")) {
            try {
                List<Zapisek> zapisky = upravaZapisku.getZapisky();
                request.setAttribute("zapisky", zapisky);
                request.getRequestDispatcher("/WEB-INF/view/zapisky.jsp").forward(request, response);    
            } catch (SQLException ex) {
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
            }            
        }
        else if(adresa.equals("/upravit")){
            try {
                int id = Integer.parseInt(request.getParameter("id"));
                Zapisek zapisek = upravaZapisku.getZapisek(id);
                request.setAttribute("zapisek", zapisek);
                request.getRequestDispatcher("/WEB-INF/view/upravit.jsp").forward(request, response);
            } catch (SQLException ex) {
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }        
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        String adresa = request.getServletPath();
        request.setCharacterEncoding("UTF-8");
        UpravaZapisku upravaZapisku = new UpravaZapisku();
        
        if(adresa.equals("/pridat")) {
            String nadpis = request.getParameter("nadpis");
            String obsah = request.getParameter("obsah");            
            if(!nadpis.isEmpty() && !obsah.isEmpty()){
                try {
                    upravaZapisku.addZapisek(nadpis, obsah);
                } catch (SQLException ex) {
                    Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
                }
                presmeruj(request, response, "");
            }
            else {
                presmeruj(request, response, "?upozorneni=True");
            }           
        }
        else if(adresa.equals("/ulozitupravy")){
            int id = Integer.parseInt(request.getParameter("id"));
            String nadpis = request.getParameter("nadpis");
            String obsah = request.getParameter("obsah");            
            if(!nadpis.isEmpty() && !obsah.isEmpty()){
                try {
                    upravaZapisku.setZapisek(id, nadpis, obsah);
                } catch (SQLException ex) {
                    Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
                }
                presmeruj(request, response, "");
            }
            else {
                presmeruj(request, response, "upravit?id=" + id + "&upozorneni=True");
            }
        }
        else if(adresa.equals("/smazat")){
            int id = Integer.parseInt(request.getParameter("id"));
            try {
                upravaZapisku.removeZapisek(id);
            } catch (SQLException ex) {
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
            }
            presmeruj(request, response, "");
        }
        else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }       
    }

Viewy

Na závěr si upravíme viewy aplikace, zde budou změny nejmenší.

V souboru zapisky.jsp, změníme všechny „indexy“ na „id“. ID zápisků už nebudeme získávat z varStatus-u ale ze samotného zápisku:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib tagdir="/WEB-INF/tags" prefix="m" %>

<m:Base titulek="Zápisky">
    <h1>Zápisník</h1>
    
    <form action="<c:url value='/pridat' />" method="POST">
        <label for="nadpis">Nadpis</label>
        <input type="text" name="nadpis" />
        <br>
        <textarea name="obsah" cols="40" rows="5"></textarea>
        <br>
        <input value="Přidat" type="submit" />
    </form>

    <c:if test="${param.upozorneni}">
        <span>Musíte vyplnit obě pole.</span>
    </c:if>

    <c:choose>
        <c:when test="${not empty zapisky}">
            <c:forEach var="zapisek" items="${zapisky}">
                <div class="zapisek">

                    <div class="nadpis"><c:out value="${zapisek.nadpis}"/></div>

                    <div class="tlacitka">
                        <form method="GET" action="<c:url value='/upravit'/>">
                            <input type="hidden" value="${zapisek.id}" name="id" />
                            <input type="submit" value="Upravit"/>
                        </form>                               
                        <form method="POST" action="<c:url value='/smazat'/>">
                            <input type="hidden" value="${zapisek.id}" name="id" />
                            <input type="submit" value="Smazat"/>
                        </form>                               
                    </div>

                    <div class="obsah"><c:out value="${zapisek.obsah}"/></div>

                </div>
            </c:forEach>
        </c:when>
        <c:otherwise>
            <span>Dosud nebyl přidán žádný zápisek.</span>
        </c:otherwise>
    </c:choose>
           
</m:Base>

V souboru upravit.jsp už nebudeme získávat id zápisku z URL, ale také ze samotného zápisku:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib tagdir="/WEB-INF/tags" prefix="m" %>

<m:Base titulek="Zápisky">
    <h1>Zápisník - úprava</h1>
    
    <form action="<c:url value='/ulozitupravy' />" method="POST">
        <input type="hidden" name="id" value="${zapisek.id}" />
        <label for="nadpis">Nadpis</label>
        <input type="text" name="nadpis" value="<c:out value='${zapisek.nadpis}' />" />
        <br>
        <textarea name="obsah" cols="40" rows="5"><c:out value='${zapisek.obsah}' /></textarea>
        <br>
        <input value="Upravit" type="submit" />
    </form>

    <c:if test="${param.upozorneni}">
        <span>Musíte vyplnit obě pole.</span>
    </c:if>
</m:Base>

Závěr

To je k dnešnímu dílu vše, nyní můžete spustit aplikaci a vyzkoušet její staronové funkce. V příštím díle se budeme věnovat autentizaci a autorizaci.

Zdrojové kódy aplikace naleznete na GitHubu: https://github.com/PetrHoracek/JavaNaWeb

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1985