Přejít k navigační liště

Zdroják » Různé » Java na webovém serveru: hlasování a grafy v SVG

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

Články Různé

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.

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

Komentáře

Subscribe
Upozornit na
guest
5 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
JK

Pouziti IP mi neprijde vhodne, hlavne kvuli natu. Osvedcila se mi captcha v kombinaci z cookie – kdyz se uzivatel snazi, tak zahlasuje nekolikrat, ale vyhnu se tisucum hlasu pripadneho bota co by zkousel free proxy…

xylon

chcel by som poprosit autora ci by vedel spravit clanok na pracu s aplikacnou logikou. Bud ejb alebo spring. Velmi by sa nieco take hodilo :).

Tom Oli

Dobrý den, v poslední části článku se používá native query např. takto:
Query q = em.createNati­veQuery(NATIVE_SQL);
Trochu jsem si stím hrál a narazil jsem na problém, který se mi nepodařilo vyřešit (resp. nikde jsem nenalezl odpověď).
Představte si, že chci z tabulky hlasovani vypsat všechny ip_adresa.
Vytvořím select : SELECT ip_adresa as ip from hlasovani
potom si nechám vrátit list
List<Object[]> result = q.getResultList();
problém nastáva, když chci vypysovat jednotlivé adresy
for (Object[] row : result) {
if(row[0] instanceof Character){
Sysout…(„to snad neni mozne“);bohuzel realita
}
}
– objekty, které jsou uloženy v řádcích (row) jsou typu Character, nikoli jak jsem předpokládal String. Zjednodušeně řečeno se do nich uloží jen první znak ip_adresy. Přetypování samozřejmě nefunguje.
V Hibernatu se tento problém řeší poměrně jednoduše:
SQLQuery q = hibernateSessi­on.createSQLQu­ery(NATIVE_SQL);
q.addScalar( „ip“, Hibernate.STRIN­G);
nastaví se typ výsledku
a v listu je pak doopravdy objekt typu String. V JPA jsem tuto možnost nenašel. Pokud se scalar v Hibernatu nenastaví, opět defaultně vrací Character.
Možná se to může zdát jako blbost a někdo bude namítat, proč si rovnou nenechám vrátit entitu hlasovani, nebo proč to nevyřeším v JPQL, ale pokud dělám složitější selecty, kde chci využít databázových funkcí a vybírám data např. z 5 tabulek a potřebuju je pohromadě vypsat, tak je JPQL dost neefektivní oproti native (query). (dále to nebudu rozpitvávat)
Konkrétní problém mám v Hiberntate 3.4 – 3.5 JPA impl a DB Oracle 10g s tabulkovými atributy typu CHAR a VARCHAR.
Pokud znáte nějaké jednoduché řešení dejte prosím vědět, jinak velice pěkný článek.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.