Java na webovém serveru: hlasování a grafy v SVG

Jak jsme si minule slíbili, dnes zase pokročíme trochu s funkcionalitou naší aplikace. Dnešním cílem bude umožnit uživatelům hlasovat, zda se v jejich oblíbeném podniku má kouřit nebo ne. Zavedeme jednoduchou ochranu proti podvodnému hlasování. Výsledky vykreslíme pomocí pěkného SVG grafu. Využijeme přitom to, co jsme se naučili v minulých dílech – zejména tvorbu REST API a vytváření vlastních JSP značek. V datové vrstvě si ukážeme, že i při používání ORM (JPA/Hibernate) máme stále k dispozici staré dobré SQL.

Seriál: Java na webovém serveru (16 dílů)

  1. Java na serveru: úvod 8.1.2010
  2. Java na webovém serveru: první web 15.1.2010
  3. Java na webovém serveru: práce s databází 29.1.2010
  4. Java na webovém serveru: práce s databází II 12.2.2010
  5. Java na webovém serveru: lokalizace a formátování 19.2.2010
  6. Java na webovém serveru: autorizace a autentizace 26.2.2010
  7. Java na webovém serveru: autorizace a autentizace II 5.3.2010
  8. Java na webovém serveru: porovnání Javy a PHP 10.3.2010
  9. Java na webovém serveru: Vlastní JSP značky a servlety 17.3.2010
  10. Java na webovém serveru: posílání e-mailů a CAPTCHA 24.3.2010
  11. Java na webovém serveru: píšeme REST API 7.4.2010
  12. Java na webovém serveru: SOAP webové služby 14.4.2010
  13. Java na webovém serveru: hlasování a grafy v SVG 28.4.2010
  14. Java na webovém serveru: Komentáře a integrace s Texy 9.6.2010
  15. Java na webovém serveru: AJAX formuláře 23.6.2010
  16. Java na webovém serveru: implementujeme Jabber 30.6.2010

Naše aplikace se sice jmenuje Nekuřák.net, ale je určena všem návštěvníkům hospod, barů a dalších podniků. Proto dáme uživatelům možnost, aby hlasovali o tom, zda se v daném podniku má nebo nemá kouřit. Výsledky hlasování – přání zákazníků – by v ideálním případě mohly ovlivnit rozhodování majitelů podniků (ale zatím je to spíše sci-fi).

Jako obvykle si nejprve si aktualizujeme zdrojové kódy aplikace pomocí Mercurialu:

$ hg pull
$ hg up "13. díl"

Případně je můžete stáhnout jako bzip2 archiv přes web.

Grafy v SVG

Začneme něčím zábavnějším a něčím, co je hned vidět – grafem, který prezentuje výsledky hlasování. A nebude to graf ledajaký – použijeme vektorovou grafiku, formát SVG, který je podporovaný moderními www prohlížeči.

Výhodou SVG je škálovatelnost (což má přímo v názvu), tzn. můžeme obrázky libovolně zvětšovat a zmenšovat a nebudou „zubaté“ jako když si zvětšíte bitmapu. Další výhodou je velikost – např. barevný přechod v SVG znamená dva řádky textu – místo abychom potřebovali popisovat barvu každého bodu v bitmapě. Do třetice, a to je pro nás podstatné, SVG nám umožňuje adresovat jednotlivé části obrázku, skriptovat je a vytvářet interaktivní aplikace (tak trochu jako Flash, ale bez nutnosti pluginů a zcela otevřeně).

Jelikož je formát SVG postavený na XML a naše JSP stránky jsou taky XML, nepotřebujeme dokonce ani žádnou knihovnu pro tvorbu grafů. Jednoduše budeme z JSP generovat SVG místo obvyklého XHTML.

V devátém díle jsme se naučili vytvářet vlastní JSP značky. Kód pro generování SVG grafu si zapouzdříme do JSP značky a pak ho můžeme kdekoli používat:

<nk:hlasovani podnik="${p.id}"/>

Definici značky naleznete v souboru hlasovani.tag a příklad jejího použití v uvod.jsp. Výsledek vypadá takto:

Jediným povinným parametrem je ID podniku, ke kterému se graf a hlasování vztahuje. Další parametry jsou:

  • svgUvnitrXhtml  – V XHTML máme dvě možnosti vkládání SVG obrázků – buď je můžeme vložit klasicky jako odkaz na externí soubor, nebo je vložíme přímo do textu stránky. Tento parametr říká, zda bude SVG vložené přímo do XHTML nebo jako <object/>. Můžeme tak poměrně zásadně změnit chování aplikace pouhým nastavením jednoho parametru.
  • hlasuAno, hlasuNe  – pokud bychom měli výsledky hlasování už načtené odjinud, můžeme je předat jako parametr a znovu už se zjišťovat nebudou.

V JSP generujeme takovéto SVG (zkráceno, plnou verzi najdete v souboru hlasovani.tag):

<svg width="200" height="200"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- pozadí a linka -->
    <rect x="0" y="0" width="200" height="200" class="pozadi"/>
    <line x1="10" y1="180" x2="190" y2="180" class="ramecek"/>
    <!-- nadpis grafu -->
    <text x="60" y="20">Mělo by se tu:</text>
    <!-- žádné hlasy -->
    <c:if test="${hlasuAno == 0 &amp;&amp; hlasuNe == 0}">
        <text x="30" y="100">(zatím nikdo nehlasoval)</text>
    </c:if>
    <!-- vypočteme si výšky sloupců grafu -->
    <c:set var="hlasuNeVyska" value="${150*hlasuNe/(hlasuAno+hlasuNe)}"/>
    <c:set var="hlasuAnoVyska" value="${150*hlasuAno/(hlasuAno+hlasuNe)}"/>
    <!-- nekuřáci -->
    <a xlink:href="javascript:hlasovani.hlasuj(${podnik}, false);" xlink:title="hlasů: ${hlasuNe}">
        <text x="30" y="195" class="ne">nekouřit</text>
        <rect x="30" y="${180 - hlasuNeVyska}" width="50" height="${hlasuNeVyska}" class="ne"/>
    </a>
    <!-- kuřáci -->
    <a xlink:href="javascript:hlasovani.hlasuj(${podnik}, true);" xlink:title="hlasů: ${hlasuAno}">
        <text x="130" y="195" class="ano">kouřit</text>
        <rect x="120" y="${180 - hlasuAnoVyska}" width="50" height="${hlasuAnoVyska}" class="ano"/>
    </a>
</svg>

Jak jste si jistě všimli, nejedná se o pouhý statický obrázek, ale máme tu i odkazy ( xlink:href), které vedou na Javascriptové funkce. Sloupce a popisky grafu jsou „klikací“ a slouží k hlasování.

Javascript

Pomocí Javascriptu odešleme hlas uživatele na server, kde se uloží do databáze. Již dříve jsme v naší aplikaci používali knihovnu jQuery a tentokrát využijeme její funkci pro práci s AJAXem. Kód naleznete v souboru hlasovani.js.

hlasovani.hlasuj = function (podnik, hlas) {
    var pozadavek = "<hlas><kourit>" + hlas + "</kourit><podnik>" + podnik + "</podnik></hlas>";
    $.ajax({
    type: "POST",
    url: "zdroje/hlas/",
    data: pozadavek,
    contentType: "text/xml",
    dataType: "text",
    success: function(odpoved) {
        …
    });
};

Po kliknutí na sloupec grafu se zavolá funkce hlasuj() a odešle XML data na REST API serveru.

REST API

V minulých dílech jsme se věnovali webovým službám a RESTu a říkali jsme si, že se jedná o do jisté míry konkurenční/al­ternativní přístupy. Proč tedy v tomto případě REST? Jednak se nám s ním bude jednoduššeji pracovat na straně klienta a jednak (a to je důležitější) operace, kterou provádíme je CRUD, přesněji řečeno jen Create – odeslání hlasu se promítne 1:1 jako INSERT do databáze.

REST API je v tomto případě velmi jednoduché a umožňuje pouze vkládání dat (POST), načítat data totiž není potřeba (zatím) – data se načítají jinde (při generování SVG grafu).

Teoretický úvod do RESTu pomocí JAX-RS jsme měli v jedenáctém díle, tak teď už jen prakticky. Kód přijímající hlasy od uživatelů naleznete v souboru HlasovaniREST.java v modulu nekurak.net-web.

@Path("hlas")
public class HlasovaniREST {
    @Context
    HttpServletRequest pozadavek;
    private static final String MIME_XML = "text/xml";
    private static final String MIME_TEXT = "text/plain";
    private HledacSluzby hledac = new HledacSluzby();
    @POST
    @Consumes(MIME_XML)
    @Produces(MIME_TEXT)
    public String hlasuj(HlasXML xml) {
    hledac.getPodnikEJB().hlasuj(xml.getPodnik(), xml.isKourit(), HttpPozadavek.getIPadresa(pozadavek));
    return "ok";
    }
}

Jednoduše předáme hlas od uživatele nižším vrstvám (EJB, DAO). Za zmínku stojí jen HttpPozadavek.getIPadresa(). Jelikož ve své konfiguraci používám reverzní proxy, přicházejí všechny HTTP požadavky jakoby od 127.0.0.1. Skutečná adresa je obsažena v HTTP hlavičce x-forwarded-for. Pomocí této funkce ji tedy vyextrahujeme. Přístup to není ideální, ale dočasně poslouží. V některém z dalších dílů si ukážeme elegantnější řešení pomocí „ventilů“ (Valve).

Bráníme se podvodníkům

Jak už to tak bývá, na Internetu se nepohybují jen samé poctivé a hodné bytosti, ale i podvodníci, kteří by mohli chtít nějakým nekalým způsobem ovlivnit výsledky hlasování.

K identifikaci osoby použijeme IP adresu. Není to ideální způsob (jednak může být více osob za NATem a jednak se jedné osobě mohou IP adresy měnit), ale lepší než nic (např. uživatelských účtů si podvodník může pořídit neomezené množství, takže to by nepomohlo).

Uživatelé mohou poslat libovolný počet hlasů, do výsledků se ale bude vždy počítat jen ten poslední. To má výhodu i v tom, že když někdo změní názor, může hlasovat znovu a tím „přebít“ svůj původní hlas. V budoucnu bychom mohli umožnit hlasovat jednou denně z jedné IP adresy (k tomu stačí úprava SQL dotazu).

Datová vrstva

Jednotlivé hlasy se ukládají do databáze do tabulky hlasovani. Její zjednodušená definice je následující (plnou verzi naleznete v souboru schéma.sql).

CREATE TABLE hlasovani
(
  id integer NOT NULL DEFAULT nextval('hlasovani_seq'::regclass),
  podnik integer NOT NULL,
  hlas boolean NOT NULL,
  datum date NOT NULL DEFAULT now(),
  ip_adresa character varying(255) NOT NULL,
  CONSTRAINT hlasovani_pk PRIMARY KEY (id),
  CONSTRAINT hlasovani_podnik_fk FOREIGN KEY (podnik)
      REFERENCES podnik (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE CASCADE
)

Hodnotu hlasu ukládáme jako boolean, jeho význam je v celé aplikaci konzistentní: true = hlas pro kouření, false = hlas proti kouření.

V naší aplikaci využíváme ORM (objektově-relační mapování) pomocí JPA (konkrétně Hibernate). ORM bývá někdy vnímáno jako nástroj, který sice šetří práci, ale na druhou stranu nás omezuje ve funkcionalitě nebo výkonu. Není tomu ale tak – přestože jsme se vydali cestou ORM v Javě, máme stále možnost používat klasické SQL a věci, které objektově-relačně mapovat nechceme nebo nemůžeme prostě mapovat nemusíme. Slouží k tomu tzv. NativeQuery.

Pro vkládání hlasů do databáze použijeme tento INSERT:

INSERT INTO hlasovani
(podnik, hlas, ip_adresa)
VALUES (:podnik, :hlas, :ip_adresa);

A pro načítání agregovaného výsledku tento SELECT:

SELECT  hlas,
    int4(count(*))
FROM (
    SELECT DISTINCT ON (ip_adresa)
    hlas
    FROM hlasovani
    WHERE podnik = :podnik
    ORDER BY ip_adresa, id DESC
) AS hlasy
GROUP BY hlas;

Výsledek bychom si mohli napočítat dopředu a uložit denormalizovaně zvlášť, ale prozatím nebudeme provádět předčasnou optimalizaci (při současném počtu hlasů a vytížení aplikace nás doba provádění SELECTu moc netrápí). Až budeme optimalizaci přidávat, upravíme pouze datovou vrstvu – vše nad ní zůstane nezměněné – rozhraní bude stejné.

Jak jste si ve výše uvedeném SQL asi všimli, používáme tu pojmenované parametry ( :podnik, :hlas, :ip_adresa). To je jedna z výhod, kterou nám dává JPA oproti obyčejnému JDBC. Nyní už k samotným metodám DAO třídy, které se starají o vkládání nebo načítání hlasů:

public void hlasuj(int podnik, boolean hlas, String ipAdresa) {
    Query insert = em.createNativeQuery(getSQL(SQL.HLASOVANI_INSERT));
    insert.setParameter("podnik", podnik);
    insert.setParameter("hlas", hlas);
    insert.setParameter("ip_adresa", ipAdresa);
    insert.executeUpdate();
}
public VysledekHlasovani getVysledekHlasovani(int podnik) {
    VysledekHlasovani vysledek = new VysledekHlasovani();
    Query select = em.createNativeQuery(getSQL(SQL.HLASOVANI_SELECT));
    select.setParameter("podnik", podnik);
    List<Object[]> vysledekDotazu = select.getResultList();
    for (Object[] radek : vysledekDotazu) {
    /** Transponujeme výsledek dotazu */
    if ((Boolean) radek[0]) {
        vysledek.setHlasuAno((Integer) radek[1]);
    } else {
        vysledek.setHlasuNe((Integer) radek[1]);
    }
    }
    return vysledek;
}

SQL dotazy načítáme z XML souboru PodnikDAO.sql.xml, jak jsme si ukázali v díle Práce s databází.

Díky NativeQuery si můžeme napsat SQL dotazy přesně podle svých představ a nemusíme vytvářet mapovací třídy/xml, když to není potřeba. ORM nás tedy v ničem neomezuje, pokud máte pocit, že pracuje s databází neefektivně, napište si vlastní-lepší SQL dotazy. S výsledkem nemusíte pracovat tak primitivním způsobem, jako je uvedeno výše (v tomto případě si vystačíme s polem objektů, protože máme jen jeden Boolean a jeden Integer a všeho všudy dva řádky ve výsledkové sadě), ale můžete si napsat vlastní SELECT a mapování (převedení výsledkové sady na objekty) nechat už zase na JPA (ORM).

Závěr

Dnes jsme se konečně zase trochu věnovali samotné aplikaci a ne jen technologiím. Ukázali jsme si generování vektorových grafů z JSP, odesílání hlasů z Javascriptu na REST API a práci s SQL v JPA. Do dnešního dílu už se nevešla ochrana proti XSRF  – o svůj návrh řešení se můžete podělit v komentářích. Případně poslat rovnou patch – vyvíjená aplikace je přeci svobodný/otevřený software!

Odkazy

Franta Kučera působí jako Java vývojář na volné noze. Programování je jeho koníčkem už od dětství. Kromě toho má rád Linux, relační SŘBD a XML.

Komentáře: 5

Přehled komentářů

František Kučera Hlasování
JK Re: Hlasování
František Kučera Re: Hlasování
xylon dalsia inspiracia
Tom Oli JPA native query
Zdroj: https://www.zdrojak.cz/?p=3224