Univerzita Karlova v Praze Matematicko-fyzikální fakulta
BAKALÁŘSKÁ PRÁCE
Tobiáš Potoček
Túúdl: webová aplikace postavená na platformě Google App Engine určená pro e-learning na středních školách
Kabinet software a výuky informatiky
Vedoucí bakalářské práce: Mgr. Daniel Lessner
Studijní program: Informatika Studijní obor: Obecná informatika Praha 2013
Poděkování: Rád bych zde poděkoval svému vedoucímu bakalářské práce Mgr. Danielu Lessnerovi za pomoc a příjemnou spolupráci při vznikání tohoto díla. Dále bych chtěl poděkovat Mgr. Davidu Tichému za to, že mi umožnil dílo otestovat a ověřit v praxi.
Prohlašuji, že jsem tuto bakalářskou práci vypracoval samostatně a výhradně s použitím citovaných pramenů, literatury a dalších odborných zdrojů. Beru na vědomí, že se na moji práci vztahují práva a povinnosti vyplývající ze zákona č. 121/2000 Sb., autorského zákona v platném znění, zejména skutečnost, že Univerzita Karlova v Praze má právo na uzavření licenční smlouvy o užití této práce jako školního díla podle § 60 odst. 1 autorského zákona. V Praze dne 16. května 2013
Tobiáš Potoček
Název práce: Túúdl: webová aplikace postavená na platformě Google App Engine určená pro e-learning na středních školách Autor: Tobiáš Potoček Katedra / Ústav: Kabinet software a výuky informatiky Vedoucí bakalářské práce: Mgr. Daniel Lessner, Kabinet software a výuky informatiky Abstrakt: Cílem práce bylo vytvořit nástroj umožňující jednoduše využívat výpočetní techniky a Internet při výuce na základních a středních školách. Konkrétně jsme se snažili pokrýt pohodlné zadávání a odevzdávání domácích úkolů. Na rozdíl od existujících řešení bylo naší snahou vytvořit aplikaci co nejjednodušší, aby ji byli schopní používat i méně zdatní uživatelé. Aplikace běží na platformě Google App Engine, což je cloudový hosting. Vývoj na této platformě se markantně liší od běžného vývoje webových aplikací, což znamená při vývoji řešit ne zcela triviální problémy. Sem patří kupříkladu práce s NoSQL databází, která se sice na jednu stranu bez problémů škáluje a přizpůsobuje aktuálním požadavkům, ale na straně druhé negarantuje okamžité aktuální a konzistentní výsledky na dotazy. V následujícím textu je podrobně popsáno, jakým způsobem je zevnitř aplikace vystavěna, aby se s těmito problémy vyrovnala. Díky reálnému nasazení aplikace se podařilo ověřit, že použitá řešení jsou skutečně v praxi funkční. Klíčová slova: e-learning, Google Apps Engine, střední školy
Title: Túúdl: web application for high school e-learning based on Google App Engine platform Author: Tobiáš Potoček Department: Department of Software and Computer Science Education Supervisor: Mgr. Daniel Lessner, Department of Software and Computer Science Education Abstract: The goal of this bachelor thesis was to create a tool which would make it easy for elementary schools and high schools to use information technologies and the Internet for educational purposes. Specifically, we tried to cover the area of assigning and evaluating homeworks. In contrast to existing solutions our effort was to create the application as simple as possible, so even users with basic computer skills could use this application. Our application is using Google App Engine, which is a cloud web
hosting.
Development
on
this
platform
is
remarkably
different
to the development of common web applications. That means that we had to face several quite complicated problems, such as working with NoSQL database. On one hand, this storage easily scales to the actual demands of the application, on the other hand, it does not guarantee up-to-date and consistent query results. The following text describes how the application is built from the inside in order to deal with these problems. We were able to test our application in a real high school environment which verified that our solution is really working. Keywords: e-learning, Google Apps Engine, high schools
Obsah Úvod.............................................................................................................................1 1. Analýza problematiky............................................................................................3 1.1. Webová aplikace jako řešení problému.............................................................3 1.1.1. Použité technologie...................................................................................3 1.2. Platforma Google App Engine..........................................................................4 1.2.1. Webová aplikace psaná v Javě...................................................................4 1.2.2. Obecný popis platformy............................................................................4 1.2.3. Ukládání dat do databáze...........................................................................6 1.2.4. Srovnání s běžnými webhostingy..............................................................7 1.3. Řešení problémů spojených s použitím cloudového webhostingu...................8 1.3.1. Konzistence dat v databázi........................................................................8 1.3.2. Šetrnost ke spotřebě prostředků...............................................................10 1.3.3. Využití cache při práci s databází............................................................10 1.3.4. Pomalý start instancí................................................................................12 1.4. Persistence objektů za využití vrstvy JDO......................................................12 1.5. Webový framework Spring MVC...................................................................14 1.6. Zpracování náročných operací........................................................................15 2. Implementace........................................................................................................16 2.1. Frontend vs. backend......................................................................................16 2.2. Aplikace na straně klienta...............................................................................17 2.2.1. Struktura JavaScriptové vrstvy................................................................17 2.2.2. Načítání dynamického obsahu a simulace historie..................................18 2.2.3. Další typy odkazů....................................................................................19 2.2.4. Vyhodnocování odpovědí serveru...........................................................20 2.2.5. Zpracovávání formulářů..........................................................................21 2.2.6. Technická implementace klientské vrstvy...............................................22 2.3. Databáze..........................................................................................................22 2.3.1. Vrstva JDO..............................................................................................22 2.3.2. Reprezentace entit pomocí objektů.........................................................23 2.3.3. Databázové schéma.................................................................................25 2.3.4. Implementace vztahů mezi entitami........................................................27 2.3.5. Dotazování a indexy................................................................................29
2.4. Architektura MVC..........................................................................................30 2.5. Fungování controllerů ve Spring MVC...........................................................31 2.5.1. Parametry v signatuře metody controlleru..............................................32 2.5.2. Předávání dat pohledům a práce s modely..............................................33 2.5.3. Pokročilé přetypování parametrů............................................................34 2.5.4. AJAXové odpovědi klientovi..................................................................35 2.6. Modelová vrstva..............................................................................................36 2.6.1. Odříznutí od specifikace databáze...........................................................38 2.6.2. Struktura modelů a jejich inicializace.....................................................39 2.6.3. Načítání entit a používání cache..............................................................40 2.6.4. Odstraňování z databáze..........................................................................44 2.6.5. Použití modelů v JSP šablonách..............................................................46 2.6.6. Statické třídy modelů...............................................................................48 2.7. Distribuce služeb napříč aplikací....................................................................48 2.8. Bezpečnostní architektura...............................................................................50 2.8.1. Princip ověřování uživatelů a přidělování práv.......................................50 2.8.2. Přihlašování pomocí Google účtů...........................................................51 2.8.3. Přihlašování pomocí lokálních účtů........................................................52 2.9. Import uživatelů..............................................................................................52 2.10. Práce se zdroji...............................................................................................52 2.10.1. Obecný popis pluginu pro nahrávání zdrojů do aplikace......................53 2.10.2. Nahrávání fyzických souborů do úložiště Blobstore.............................53 2.11. Widget pro výběr uživatelů...........................................................................54 2.12. Ošetřování chybových stavů a zachytávání výjimek....................................54 3. Zkušenosti s reálným provozem aplikace...........................................................56 Závěr..........................................................................................................................59 Seznam použitých zkratek.......................................................................................62 Přílohy........................................................................................................................64
Úvod Zatímco na vysokých školách je e-learning, tedy využívání Internetu a výpočetní techniky k výuce, poměrně zavedený, tak střední a základní školy jsou v tomto pozadu. Je otázka, čím je to způsobeno, a co by se s tím dalo dělat. My jsme se zaměřili na fakt, že menším školám chybí nějaký nástroj, který by přesně odpovídal jejich požadavkům. V oblasti e-learningu je etalonem systém Moodle. Tento software pokrývá oblast výuky skutečně dokonale a díky jeho modulárnímu charakteru se neustále rozšiřuje. Tato jeho komplexnost se ale změní v břímě v okamžiku, kdy potřebujeme jen zlomek jeho funkcí a naopak vyžadujeme snadnou správu a ovládání. Po konzultaci s potencionálními uživateli, tedy s lidmi skutečně se pohybujícími v oblasti středních škol, jsme si tedy za cíl dali vytvořit aplikaci, která bude implementovat jen zlomek funkcí v této oblasti. To konkrétně znamená zadávání a vyhodnocování domácích úkolů včetně pohodlného sdílení studijních materiálů. Tato značně omezená doména působnosti umožňuje aplikaci navrhnout takovým způsobem, aby nabídla uživateli pohodlné a přehledné ovládání a především ho nezahltila ohromným množstvím funkcí. To je nezbytné k tomu, aby kantoři, kteří často nemají k počítači nejvřelejší přístup, aplikaci okamžitě nezavrhli a naopak byli ochotní se s ní naučit pracovat a při výuce ji využívat. Naše aplikace běží na platformě Google App Engine. To je cloudový hosting, který se svými vlastnostmi značně liší od běžně používaných hostingů využívajících k běhu server Apache s podporou PHP. Základní odlišností je především to, že součástí není relační databáze, nýbrž NoSQL úložiště, a že cena za provoz aplikace není paušální, nýbrž se dynamicky odvíjí od spotřebovaných prostředků. Google zároveň nabízí do určité míry vytížení provoz aplikace zcela zdarma. Naším cílem tedy bylo navrhnout aplikaci takovým způsobem, aby byla překlenuta tato unikátní specifika, a aby byla aplikace schopná alespoň v případě menších škol běžet zcela bez nákladů. Těchto cílů je dosaženo komplexním návrhem vnitřní architektury aplikace a především pokročilými technikami cachování. V popisu této problematiky právě spočívá těžiště tohoto textu. Naší snahou rovněž bylo co možná nejlépe 1
připravit aplikaci na případný přesun na jinou platformu a zamezit tak problému vendor lock-in. Cílem při psaní aplikace bylo stvořit nástroj vhodný pro použití ve středním a základním školství. Naší snahou bylo stvořit ho takovým způsobem, aby byl schopný běžet na platformě Google App Engine s co nejnižšími náklady, aby uživatel byl co nejméně ovlivněn specifiky tohoto hostingu a v neposlední řadě aby přechod k alternativnímu poskytovali hostingu byl co možná nejsnazší. Cílem následujícího textu je pak seznámit čtenáře s tím, s jakými problémy jsme se při psaní potýkali, jakým způsobem jsme je byli schopni vyřešit a jak by se to případně dalo udělat lépe. Struktura práce bude následující. V první analytické části si především představíme hosting jako takový, vysvětlíme si, v čem se odlišuje od běžných hostingů, s jakými konkrétními problémy je nutné počítat a součástí kapitoly bude i obecný popis použitých řešení bez zabíhání do technických detailů. V této části budou rovněž zmíněny klíčové nástroje a knihovny použité v aplikaci, včetně zdůvodnění, proč byly vybrány zrovna tyto. Následovat bude implementační část, která bude svojí podrobností odpovídat programátorské dokumentaci. Bude zde detailně popsáno, jakým konkrétním způsobem byla implementována řešení popsaná v analytické části. Přirozeně při zabíhání do detailů se objeví ještě spousta dalších podružných problémů, které je nutné vyřešit a v analytické části se na ně nedostalo. Těsně před závěrem bude ještě krátká kapitolka shrnující reálné zkušenosti s přibližně 7měsíčním ostrým provozem na existující střední škole, což dá čtenáři jistou představu o tom, do jaké míry se nám povedlo předsevzané cíle splnit. To vše bude pak kompletně shrnuto v úplném závěru.
2
1. Analýza problematiky 1.1. Webová aplikace jako řešení problému Klíčovým cílem této práce bylo vytvořit nástroj, který měl za úkol usnadnit výuku na středních školách (a nejen tam) za využití Internetu. Tento nástroj jsme se rozhodli vytvořit jako webovou aplikaci, nicméně je třeba zmínit, že toto nebyla a není jediná možná cestou, kterou se lze vydat. Jednou z možností by bylo například vytvořit desktopovou nativní aplikaci, která by pouze pomocí jednoduchého protokolu komunikovala s centrálním serverem. Tento přístup je například hojně využíván dnes v oblasti chytrých telefonů. Většina známějších služeb má svého klienta pro mobilní telefon. Výhod je několik. Nativní aplikace znamená především vyšší výkon a pohodlnější práci. Aplikace také obvykle lépe zapadne do prostředí systému. My jsme se přesto rozhodli jít cestou webové aplikace místo nativní. V oblasti počítačů panují trochu jiné podmínky než v oblasti chytrých telefonů. Na jedné straně máme relativní dostatek výkonu, tedy webová aplikace může za využití moderních technologií běžet téměř stejně rychle jako nativní desktopová, a na druhé straně postrádáme nějaký centrální distribuční nástroj, kterými disponují všechny dnešní mobilní ekosystémy. Tedy bylo by nutné nějakým netriviálním způsobem řešit distribuci nástroje mezi uživatele a především distribuci pozdějších aktualizací a záplat. U webové aplikace tento problém zcela odpadá. Webová aplikace je rovněž automaticky multiplatformní. Lze ji stejně pohodlně ovládat na libovolném operačním systému. Jediné, co je potřeba ohlídat, je kompatibilita s různými prohlížeči. Nicméně rozdíly mezi jednotlivými vykreslovacími jádry, obzvláště u dnešních moderních prohlížečů, jsou zcela zanedbatelné oproti rozdílům mezi jednotlivými desktopovými platformami.
1.1.1. Použité technologie Každá webová aplikace se skládá z klientské a serverové části. Serverová část je obvykle ta důležitější a může být implementována řadou různých způsobů, na různých platformách, v různých jazycích. V našem případě jsme vybrali Javu běžící na platformě Google App Engine. Tomu bude věnována celá další podkapitola. Naopak klientská část je do určité míry totožná pro všechny webové aplikace. 3
Uživatelské rozhraní je vytvářeno ve značkovacím jazyce HTML za podpory kaskádových stylů (CSS). Naše aplikace bude v klientské části obsahovat navíc netriviální vrstvu v JavaScriptu. Tato vrstva bude zachytávat uživatelské vstupy, převádět je na asynchronní požadavky směrované serveru a zpátky přijímat odpovědi a překreslovat podle výsledků uživatelské rozhraní. Tato technologie asynchronních požadavků je obvykle označována jako AJAX. Za využití moderních vlastností všech tří zmiňovaných jazyků (HTML, CSS, JavaScript) lze skutečně vytvořit webovou aplikaci takovým způsobem, že její využívání je stejně pohodlné a rychlé, jako práce s klasickou desktopovou aplikací.
1.2. Platforma Google App Engine Pro běh aplikace jsme zvolili poměrně atypickou platformu, a to Google App Engine, což je cloudový webhosting. To přínáší spoustu úskalí a obecně se tvorba aplikace pro tuto platformu v mnoha věcech liší od běžné webové aplikace, typicky psané v PHP, využívající databáze MySQL a běžící na serveru Apache. Z tohoto důvodu použitá platforma značně ovlivnila celou tvorbu aplikace a je tedy nutné pro pochopení jednotlivých kroků a rozhodnutí alespoň částečně znát princip fungování App Engine.
1.2.1. Webová aplikace psaná v Javě Google App Engine nabízí pro tvorbu aplikací omezenou množinu jazyků. My jsme vybrali Javu jakožto standardní, spolehlivé a rozšířené řešení. V Javě se pro psaní webových aplikací využívá tzv. servletů. Servlet není nic jiného než obyčejná třída, která zachytává HTTP požadavky na dané adrese, zpracovává je a generuje odpovědi pro klienta. Konkrétní detaily v tuto chvíli nejsou podstatné a budou popsány v druhé, implementační části textu.
1.2.2. Obecný popis platformy Google App Engine je cloudový webhosting [1]. To znamená, že aplikace neběží na jednom fyzickém stroji, ba ani na jednom virtuálním, nýbrž počet strojů, na kterém aplikace běží, se dynamicky mění podle vytíženosti aplikace. V terminologii App Engine se už nemluví ani o přidělených strojích. Aplikace běží v tzv. instancích, přičemž každá instance má pevně přidělený procesorový výkon a operační paměť. 4
V případě, že je aplikace psaná v Javě, tak jedna instance odpovídá jednomu běžícímu servletu, který odpovídá a vyřizuje požadavky. V okamžiku, kdy přijde požadavků příliš a instance je přestane stíhat vyřizovat (což se začne projevovat mimo jiné zvyšující se odezvou na jednotlivé požadavky), nastartuje se automaticky další instance. Tímto způsobem je hosting schopný dynamicky reagovat na zátěž a aplikace se tak bez problémů přizpůsobuje situaci. Podobně dynamicky se vyvíjí i cena, kterou zákazník za provozování aplikace na tomto hostingu zaplatí. Zjednodušeně se dá říci, že zákazník zaplatí za spotřebovaný výkon, tedy čím více je jeho aplikace využívaná, tím více platí. Výkon v tomto případně není nějaká abstraktní veličina, nýbrž je dost konkrétně rozdělen do jednotlivých oblastí, které jsou obecně označovány jako resources neboli prostředky pro běh [2]. Mezi ty nejdůležitější mimo jiné patří: 1. Instance Hours: doba běhu instance. Zákazník platí za dobu, po kterou běžela daná instance. Tedy dvojnásobný počet běžících instancí znamená i dvojnásobnou cenu. 2. Datastore Stored Data: data uložená v databázi. Čím více dat je uložených, tím více se za úložiště platí. 3. Datastore Write/Read operations: čtení a zápis do databáze. Každý přístup do databáze je zpoplatněn. 4. Blobstore: spotřebované úložiště fyzických souborů je rovněž zpoplatněno. Důležité je, že Google App Engine nabízí v základu jisté množství prostředků zdarma a tedy aplikaci je do určité míry vytížení možné provozovat zcela bez nákladů. Jakmile jsou ale přidělené kvóty překročeny, např. příliš přístupů do databáze, lokální soubory zabírají moc místa apod., Google začne prostředky spotřebované nad limit účtovat podle určených sazeb. Toto silně ovlivňuje přístup ke psaní aplikace, protože každá optimalizace se pak může pozitivně projevit na konečném účtu za webhosting. Další aspekt je to, že sice na jedné straně cloudový charakter umožňuje pružně reagovat na zátěž a tedy mimo
5
jiné se vyrovnat i s DoS útoky, ale na straně druhé takový DoS útok také může přijít pěkně draho. App Engine přirozeně umožňuje nastavit nějaké bezpečností zarážky, nicméně s touto situací je potřeba počítat.
1.2.3. Ukládání dat do databáze Na rozdíl od běžných hostingů Google App Engine nenabízí klasickou relační databázi. Momentálně je jako primární úložiště aplikací pro App Engine používán High Replication Datastore. Toto úložiště v základu využívá i naše aplikace. V práci budou zaměňovány pojmy databáze a úložiště, vždy se tím bude mít na mysli App Engine High Replication Datastore. V High Replication Datastore se ukládají trvale přímo celé objekty, které se v terminologii App Engine nazývají entity [3]. Pokud bychom toto úložiště srovnali s běžnou relační databází, tak jedna entita odpovídá jednomu záznamu a „druh“ entity odpovídá jedné tabulce. Tedy množina entit může být stejného druhu, nicméně ani to neznamená, že musí mít stejné schéma. Toto úložiště je tzv. schema-less. Jediné pevné pravidlo je to, že každá entita musí mít svůj klíč. App Engine zavádí v úložišti koncept tzv. entity groups, skupiny entit. Entity group je množina entit uspořádaná do stromové struktury. Tedy jedna entita je kořenová a obsahuje „podentity“ a ty zase mohou obsahovat další entity atd. Budeme-li se na úložiště dívat z pohledu druhů entit, lze úložiště chápat jako relační databázi složenou s tabulek. Z pohledu entiy groups naopak úložiště odpovídá spíše lesu, použijeme-li grafové terminologie. Při práci s daty je nutné vnímat oba pohledy. Toto úložiště musí splňovat požadavky škálovatelnosti, aby zapadlo do prostředí cloudového hostingu. To přináší několik problematických vlastností. Mimo jiné i to, že úložiště nezaručuje 100% konzistenci a aktuálnost dat tak, jak ji zaručují běžné relační databáze. High Replication Datastore nabízí dvě úrovně konzistence výsledků na dotazy: 1. eventually consitent results: pokud provádíme dotaz napříč několika entity groups, nemáme záruku, že data jsou doopravdy aktuální. To se může projevit třeba tak, že pokud vložíme nový objekt do úložiště a vzápětí zavoláme dotaz
6
na množinu objektů, do které by ten náš objekt měl patřit, tak je reálná šance, že se tam onen objekt ještě neobjeví. Navíc toto úložiště negarantuje žádný konkrétní čas, kdy se výsledky stanou konzistentními. V praxi se nicméně jedná o jednotky sekund. 2. strong consistent results: naopak pokud provádíme dotaz v rámci jedné entity group (tzv. ancestor query), konzistenci výsledků zaručenou máme. Členství entity v nějaké entity group je napevno zakódováno do klíče této entity, klíč obsahuje kompletní cestu od kořene entity group až k dané entitě. Z toho vyplývá, že entitu nelze jednoduše přesunou z jedné skupiny do druhé, lze ji pouze na jednom místě smazat a na druhém vytvořit, což může být velice náročné, pokud sama entita je už rozsáhlým stromem. Toto je nutné vzít v potaz při návrhu schématu databáze. V rámci entity group je rovněž omezen počet zápisů za sekundu. Např. to, že jsou všechna data vztažená k jednomu uživateli aplikace v jedné entity group vadit nebude, protože s těmito daty bude v naprosté většině případů pracovat právě jen ten jeden uživatel a ten nevyvine takovou aktivitu, kterou by aplikaci přetížil. Na druhou stranu není možné dát do jedné entity group veškerá data aplikace (ač by to bylo výhodné, protože by tím byla víceméně zaručena úplná konzistence napříč celým systémem). V klasické relační databázi se pracuje se vztahy mezi tabulkami, ty jsou přirozeně i zde, jen se rozlišují, zda jsou v rámci entity group (owned relationship) či mezi různými entity groups (unowned relationship). V úložišti je také do značné míry omezeno dotazování. Například zcela chybí klasické JOINy a agregační funkce. Spoustu těchto vlastností lze dohnat „hrubou“ silou, ale vzhledem k tomu, že za hrubou sílu se zde tvrdě platí, je potřeba si předem rozmyslet, jakým způsobem budou dotazy prováděny a jestli za tu cenu vůbec stojí.
1.2.4. Srovnání s běžnými webhostingy Běžný webhosting určený pro běh webových aplikací obvykle znamená podporu nějakého skriptovacího jazyka (PHP) a relační databáze (MySQL). Takový hosting
7
nabízí za fixní měsíční cenu, pevný přidělený prostor a nějaký blíže nedefinovaný výkon. Při přímém srovnání s takovým hostingem App Engine rozhodně má navrch v tom, že člověk platí doopravdy pouze za to, co spotřebuje, a do určité míry může využívat hosting zcela zdarma. Přirozeně zde existují i běžné free hostingy, ty nicméně kvalitou silně pokulhávají, zatímco v případě App Enginu, ať už člověk platí či nikoliv, služby jsou pořád stejné. Na tu druhou stranu, je těžko měřitelné, jaký přístup v případě větších a náročnějších aplikací vyjde ekonomicky lépe, zda cloudové řešení postavené na App Engine a psané v Javě, či klasické řešení PHP + MySQL na běžném placeném hostingu. Samozřejmě v určitých případech může být naprosto klíčová škálovatelost cloudového hostingu, nicméně to v případě naší aplikace, která je mířena na školy s řádově stovkami potencionálních uživatelů, nehraje takovou roli. Naopak započítáme-li další aspekty, mimo jiné náročnější práci s databází z důvodů negarantované konzistence, problém vendor lock-in (aplikaci psanou pro App Engine lze plnohodnotně provozovat jen na serverech Google) a obecně jistou „exotičnost“ této platformy, stává se volba App Engine pro tento konkrétní projekt spíše takovým experimentem a výpravou do neznáma, tedy snahou vyzkoušet si, zda je možné napsat podobnou aplikace v takovém prostředí. Volba běžného ověřeného řešení by za normálních okolností byla racionálnější cestou.
1.3. Řešení problémů spojených s použitím cloudového webhostingu Z dosud psaného textu je zřejmé, že psaní aplikace pro cloudový hosting s sebou přináší celou řadu úskalí, přes které je nezbytné se nějak přenést a alespoň do určité míry je vyřešit.
1.3.1. Konzistence dat v databázi Jedním z nejpalčivějších problémů je fakt, že databáze nemusí podávat za každé situace zcela aktuální výsledky, což se projevuje primárně u přidávání nových objektů. V běžné webové aplikaci, když uživatel vytvoří nový objekt (např. vloží nový článek do databáze), tak se zašle požadavek serveru, ten jedním databázovým
8
dotazem objekt vytvoří a druhým dotazem získá aktualizovaný seznam objektů, který následně zašle uživateli, který tak má zpětnou vazbu, že se akce povedla. A toto právě v případě App Engine nemusí zcela fungovat, protože onen aktualizovaný seznam objektů vůbec nemusí ten nový objekt obsahovat. To je samozřejmě silně uživatelsky nepřívětivé, protože uživatel, když neuvidí nově přidaný objekt před sebou, bude vycházet z předpokladu, že jeho požadavek selhal a pokusí se objekt přidat znovu. Problém je možné řešit mimo jiné následujícími způsoby: 1. Vhodný návrh databázové struktury. Silná konzistence, tedy aktuální data za každé situace, je zaručená v rámci entity groups. Proto budou kritická data právě umístěna do stejných entity groups. Toto samozřejmě nelze aplikovat globálně, celá databáze nemůže být v jedné entity group, proto je nutné aplikovat další opatření. 2. Vytvoření iluze aktuálnosti. Jak bylo zmíněno na začátku, Túúdl bude obsahovat netriviální vrstvu běžící na straně klienta, která se bude starat o asynchronní komunikaci se serverem a aktualizování výsledků na straně klienta. Pokud tedy uživatel vytvoří objekt, bude serveru zaslán potřebný požadavek, nicméně server nevrátí žádná data, nýbrž jen kladnou (případně zápornou odpověď). Na základě odpovědi pak klientská část ručně upraví stránku, kterou uživatel vidí, a na správné místo vloží nový objekt. K odeslaným datům má klientská část přirozeně přístup, stačí si někde schovat data z odeslaného formuláře. Tímto způsobem se změny pro daného uživatele projeví okamžitě a to nezávisle na tom, jestli se už ony změny provedly v celém úložišti. Samozřejmě ostatní uživatelé aplikace mohou dostat potencionální zastaralé výsledky, ale vzhledem k tomu, že se skutečně jedná jen o několikasekundové „okno“, to není klíčové. 3. Upozornění uživatele na problém. U některých operací by výše postupy byly příliš komplikované ba co možná neproveditelné. V takovém případě nezbývá nic jiného, než u dané operace uživatele o problému informovat a doporučit mu, ať stránku zkusí obnovit, pokud se změny neprojeví okamžitě.
9
1.3.2. Šetrnost ke spotřebě prostředků Aby aplikace byla schopná fungovat za nějaké „rozumné“ peníze, či v optimálním případě zcela zdarma, je potřeba skutečně dbát maximální optimalizaci. To se samozřejmě týká především jednotlivých konkrétních případů a částí aplikace, nicméně je na toto potřeba pamatovat už při celkovém návrhu aplikace. Zde se velice pozitivně projeví AJAXový charakter aplikace. Klasická webová aplikace musí jako odpověď vygenerovat kompletní stránku, což obvykle znamená, že některé části stránky (hlavička, postranní navigační panel apod.) se generují pokaždé znovu a tedy by v našem případě spotřebovávaly nezanedbatelné množství prostředků zcela zbytečně. Toto lze samozřejmě řešit nějakou formou cachování, ale AJAX je v tomto případě mnohem elegantnějším řešením. Díky AJAXu totiž stačí, aby server posílal skutečně jen část, která se změnila a někdy ani toto není potřeba. V případě přidávání nových objektů (či jejich upravování), jak bylo popsáno o kapitolu výše, server doopravdy jen objekt uloží a klientovi pošle pouze krátkou odpověď informující o úspěchu či neúspěchu. O vše ostatní se postará klient a operace je tak velice šetrná ke spotřebovaným prostředkům.
1.3.3. Využití cache při práci s databází Další přirozenou cestou, jak šetřit zdroje, je cachování. App Engine nabízí API pro práci s Memcache [4] (což třeba u běžných webových hostingů není úplně obvyklé), která je pro toto přímo stvořená. Cachování se bude primárně týkat práce s databází, kde kupříkladu chybějící podpora JOINů vytváří ohromný tlak na spotřebu přístupů do databáze. Zcela triviální případ, kdy je potřeba k seznamu objektů načíst i informace o jejich vlastnících (vztah řešen cizím klíčem), se stává ohromných problémem. Abychom získali ony informace o vlastníkovi, je pro každý načtený objekt potřeba znovu zavolat databázi. Proto je využití cache naprosto nezbytně nutné. V části o Google App Engine Datastore bylo popsáno, jakým způsobem jsou ukládány objekty do databáze. Objekty se nazývají entity a každý z nich má svůj klíč (v terminologii relačních databází by se jednalo o primární klíč). Užitečná vlastnost tohoto klíče je to, že je unikátní napříč celou databází a to včetně typů entit. Tedy 10
nezávisle na typu, v databázi nemůžou býti dvě entity se stejným klíčem. Toho se dá využít při cachování, protože cache v App Engine není principiálně nic jiného než asociativní pole, tedy množina objektů z nichž každá je uložena pod unikátním klíčem. A protože klíč entity je unikátní, dá se automaticky použít i jako klíč pro ukládání do cache. Naše aplikace tedy zavádí univerzální transparentní rozhraní, přes které probíhá veškerá práce s entitami v datastore, tedy načítání, upravování i mazání. Za tímto rozhraním je schovaná mezivrstva cache. Pokud je tedy potřeba v nějakém dotazu substituovat funkci JOINu, přičemž jako cizí klíč je tu použit klíč nějaké další entity (naprostá většina případů), stačí využít toto transparentní rozhraní. Při prvním zavolání dotazu je samozřejmě nutné všechny napojené objekty načíst, což může být drahé, všechna pozdější opakovaná volání už jsou ale prakticky zadarmo, protože se platí pouze za ten hlavní dotaz a všechny připojené objekty se už načítají levně z cache. Díky tomu, že veškerá komunikace probíhá přes ono jedno univerzální rozhraní, nehrozí ani situace, že by se v cache nacházela zastaralá data. Pokud bude objekt smazán či upraven skrz toho rozhraní, bude i automaticky odstraněn z cache. Při provádění výše uvedeného dotazu se pak akorát tento upravený objekt znovu načte čerstvý z databáze (a opět se uloží do cache), zatímco ostatní nedotčené objekty poputují stejně jako před tím z cache. Problém by nastal, kdyby začalo být nutné cachovat i dotazy vracející množiny objektů. Potom určit, co všechno za informace v cache je nutné invalidovat při úpravě jednoho jediného objektu, by nebylo zrovna triviální. Reálné nasazení aplikace nicméně ukázalo, že i bez tohoto běží aplikace svižně a poměrně šetrně. Dotazy jsou cachovány jen v jednom konkrétním případě, který bude popsán samostatně v implementační části (2.10 Práce se zdroji, str. 52). Současný návrh využití cache v podstatě směřuje k tomu, že se dříve či později celá databáze zkopíruje do cache. To je svým způsobem ideální stav, protože v takovém případě budou prostředky šetřeny zcela maximální. Ale tady už samozřejmě nutné brát v potaz omezenou velikost cache. Pokud aplikace nabobtná co do množství dat v databází, tak během špičky ve vytížení může přirozeně dojít k tomu, že cache přestane stačit a nové objekty budou ty staré vytlačovat z databáze takovou rychlostí, že cache přestanou plnit svou roli. Dle Google velikost cache v App Enginu není 11
pevně stanovená a automaticky se odvíjí od množství požadavků mířících do aplikace. Dá se tedy říci, že existuje jakési optimální využití cache vzhledem k momentálnímu vytížení aplikace, při kterém cache nikdy nedojde, tedy že nárok na množství cache roste vzhledem k aktuálnímu vytížení maximálně tak rychle, jako samotné množství dostupné cache. Bohužel toto optimální využití není nikde popsáno a nelze než se opět odkázat na zkušenosti s reálným provozem aplikace, podle kterého se zdá, že to funguje dostatečně dobře. Na tu druhou stranu, je těžké odhadovat (či simulovat takový provoz), jak se bude aplikace chovat, pokud v ní budou pracovat místo stovek řádově tisíce uživatelů.
1.3.4. Pomalý start instancí V úvodu této kapitoly byl popsán princip fungování App Enginu a především role instancí. Instance běžící aplikace vznikají a naopak zanikají dynamicky podle aktuálního využití. To hypoteticky může vést k tomu, že v případě, že nikdo delší dobu aplikaci nevyužije, zanikne i ta poslední instance a aplikace se doslova vypne. Zapne se až s prvním příchozím návštěvníkem, a protože inicializace instance trvá nezanedbatelně dlouhý čas (až desítky sekund), je návštěvník vystaven čekání. Pokud toto vadí, a jakože u většiny webových stránek by toto bylo silně nežádoucí, nabízí App Engine jako placenou službu trvale zapnout určitý počet instancí. Tedy aplikace je schopná reagovat kdykoliv a okamžitě. To je pro naši aplikaci relativně zbytečné, resp. zbytečně luxusní. Naše aplikace bude využívána spíše nárazově, během školního vyučování či naopak večer před termínem odevzdání, a je tedy zbytečné, aby spotřebovávala prostředky (Instance Hours) během nečinnosti. Nevýhodou je, že uživatel může být vystaven čekání. Aby se mu během této doby nezobrazovalo jen nic neříkající bíle okno, je jako vstupní brána do aplikace použita statická informační stránka, která je zcela nezávislá na instancích a zobrazí se okamžitě. Tato stránka pomocí asynchronního požadavku ověří, zda už je aplikace připravená, případně počká, až bude, a teprve pak na ní uživatele přesměruje.
1.4. Persistence objektů za využití vrstvy JDO V základu nabízí Google App Engine pro přístup do databáze nízkoúrovňové API. Toto API umožňuje naplno využít každou ze specifických vlastností tohoto úložiště, 12
pro běžnou práci ale není příliš pohodlné. Proto bylo vhodné zvolit nějakou nadstavbu, která by práci s úložištěm usnadnila. Na základě dostupných informací jsme se na úplném začátku práce rozhodli pro knihovnu Java Data Objects [5]. Při výběru hrály roli mimo jiné tyto argumenty: 1. JDO je standardní součástí Javy a jako nástroj pro prezistenci objektů je všeobecně hodně využívaný. Na základě toho se dala od JDO celkem odůvodnitelně očekávat spolehlivost, stabilita, podrobná dokumentace a podpora komunity. 2. Dokumentace Google App Engine pro Javu obsahuje přímo rozsáhlou část věnující se použití JDO, podrobně popisuje jednotlivé příklady použití a zároveň upozorňuje na vlastnosti JDO, které nelze použít kvůli omezením App Engine Datastore. 3. JDO je univerzální vrstva, která je schopna transparentně fungovat nad řadou různých databází. To jinými slovy znamená, že pokud by bylo potřeba aplikaci portovat na jinou platformu, použití JDO by mělo tento přechod značně usnadnit. Čistě teoreticky by mělo stačit vyměnit databázový driver a aplikace by měla běžet bez změny kódu na úplně jiné databázi. Bohužel se ukázaly téměř všechny předpoklady jako mylné a volba JDO jako nepříliš vhodná. JDO není omezeno jen na relační databáze a dokáže stejně pohodlně spolupracovat i s objektovými databázemi, nicméně Google App Engine Datastore je natolik unikátní a specifický, že JDO není schopno běžet nad tímto úložištěm efektivně. Síla JDO spočívá v tom, že umí naplno využívat vlastností typické pro relační databáze, jako jsou například právě JOINy. To samozřejmě v App Engine nefunguje a z JDO se tak stává poměrně primitivní nástroj. Naopak JDO není schopné reflektovat některé exkluzivní vlastnosti App Enginu. Například není možné nijak elegantně rozlišit mezi owned a unowned vztahy. Koncept entity groups také v JDO zcela chybí a využít toho, že klíč potomka v sobě obsahuje klíč rodiče, JDO také nedokáže. V konečném důsledku je tak nutné při práci s databází využívat všemožných triků specifických čistě pro App Engine, jen aby výsledná aplikace byla schopná rozumně rychle fungovat. Z toho jasně vyplývá, že nějaká pohodlná 13
přenositelnost mezi platformami není příliš reálná a aplikace psaná pro App Engine bude buď dobře fungovat pouze pro App Engine, a nebo prakticky vůbec. JDO je pro App Engine dostupné ve dvou verzích, 2.0 a 3.0, přičemž ta druhá sice nabízí některé užitečné funkce navíc (například je schopná pracovat transparentně s owned i unowned vztahy), ale zároveň je označena za experimentální, přičemž toto označení není bezdůvodné. Po několika neúspěšných pokusech implementovat vztahy v JDO 3.0 jsme se vrátili k verzi 2.0, která byla o něco spolehlivější. Nicméně i u ní jsme narazili na zásadní nedostatky v dokumentaci, která byla neúplná a často popisovala postupy, které v praxi nebyly příliš použitelné. Většinu postupů, především při modelování vztahů, bylo nezbytné vyřešit ručně a vyzkoušet si, co bude v praxi fungovat nejlépe. Výsledek je použitelný a JDO tedy lze principiálně použít pro ukládání objektů v Google App Engine, nicméně zcela tu chyběl ten aspekt „spolehlivého a ověřeného“ řešení. Je zřejmé, že JDO není vhodné pro použití na této platformě, a z nekvalitní dokumentace lze usuzovat, že ani Google ji za vhodnou nepovažuje. Pro příští projekt bychom určitě zvolili nějakou jinou knihovnu, jakou je například Twig [6] či Objectify [7]. Tyto knihovny jsou určené pouze pro Google App Engine a respektují tak všechny specifické vlastnosti tohoto úložiště. Zároveň ale poskytují podstatně větší pohodlí než nízkoúrovňové API.
1.5. Webový framework Spring MVC Stejně jako práce s nízkoúrovňovým API pro komunikaci s úložištěm není zrovna pohodlná, ani výchozí servletové rozhraní Javy není přespříliš přátelské. Z tohoto důvodu jsme se i zde rozhodli využít nějakou nadstavbu, která by vývoj usnadnila. Volba padla nakonec na webový framework Spring MVC [8]. Už podle názvu tento framework
následuje
architekturu
MVC,
která
je
podrobněji
popsána
v implementační části této práce. Spring MVC je poměrně komplexní framework, který nabízí nějaké vlastní řešení a postup téměř pro každý problém, na který člověk při vytváření webové aplikace může narazit. Nicméně naší snahou nebylo strávit týdny čtením dokumentace a zkoumáním XML konfiguračních souborů, a proto jsme si z tohoto frameworku 14
vypůjčili jen to nejdůležitější. Klíčové bylo především velice pohodlné routování a práce s URL adresami skrz controllery. Naopak např. Spring Security, komplexní systém pro autentizaci a autorizaci uživatelů, byl zcela ignorován jakožto příliš složitý a byl nahrazen několika krátkými ručně psanými třídami. Na rozdíl od JDO byla volba Spring MVC výhodná. Usnadnila dost práce a především významně zpřehlednila kód a strukturu aplikace. Jedinou nevýhodou je náročnost frameworku. Kvůli Springu (a částečně i JDO) trvá start instance řádově desítky sekund.
1.6. Zpracování náročných operací V App Engine je pro každý požadavek nastavený pevný limit 60 sekund, během kterých musí být požadavek vyřízen. Při překročení dojde k násilnému přerušení výpočtu a pokud kupříkladu operace prováděné nad úložištěm neběží v transakci, může úložiště skončit v nekonzistentním stavu. V praxi je samozřejmě nemyslitelné, aby se vyřizování jednoho požadavku byť jen přiblížilo 60 sekundám, a to ani ne tak kvůli hrozícímu nebezpečí, jako spíše kvůli uživateli, který očekává od aplikace rychlou odezvu. Nicméně existují případy, kdy toto neplatí. Aplikace konkrétně nabízí funkce importu uživatelů (ať už ze souboru CSV či z Google Apps domény) a zde už reálně hrozí, že 60 sekund nebude stačit. Proto App Engine přichází s poměrně unikátním nástrojem, který běžné hostingy nenabízejí, a to je možnost provádět určité operace na pozadí, mimo základní požadavek. Při běžném požadavku se vytvoří tzv. Task [9], tedy úkol, a ten je vložen do Task Queue, tedy do fronty. Původní požadavek vrátí odpověď klientovi a Task nyní žije ve frontě už vlastním životem. To znamená, že v okamžiku, kdy na něj přijde řada, tak je proveden. O provedení se stará tzv. Worker, což není nic jiného, než nějaká funkce či metoda, která naslouchá na pevně dané URL. Provedení úkolu tedy není nic jiného, než zavolání této speciální adresy, přičemž vlastnosti úkolu jsou předávány formou běžných HTTP parametrů. Takto zavolaný požadavek má k dispozici podstatně více času, a to 10 minut.
15
2. Implementace 2.1. Frontend vs. backend Túúdl jakožto webová aplikace bude vycházet z klasického paradigmatu požadavekodpověď. Bude tedy obsahovat dvě základní části, backend, nebo-li program na straně serveru, který generuje obsah, a potom frontend, tedy to, co uživatel vidí a s čím pracuje. Pro ujasnění principů před samotným ponořením do detailů vnitřní implementace nám přišlo vhodné to znovu zdůraznit. U běžných webových stránek je frontend v zásadě jen seznam statických odkazů a formulářů, které vygeneroval server, nicméně Túúdl půjde cestou moderních AJAXových aplikací, kde frontend v podstatě tvoří samostatně fungující jednotku, která komunikuje se serverovým backendem pomocí asynchronních požadavků. Asynchronní v tomto případě znamená, že uživatel nemusí nezbytně čekat, až se požadavek provede, ale může webovou stránku využívat dále i během čekání. Tato vlastnost nebude v případě naší aplikace příliš využívána, z čeho ale budeme těžit hodně je to, že tento přístup umožňuje serveru zasílat klientovi jen minimální množství dat, přičemž aplikace na straně klienta se sama postará o překreslení toho, co uživatel vidí. V předchozí kapitole práce bylo celkem podrobně popsáno, jak tento přístup šetří výkon a spotřebu prostředků na straně serveru. To ale není vše, tento přístup je schopen do značené míry šetřit i výkon na straně klienta, protože pro klientský počítač je vždy náročnější vykreslit kompletní celou stránku znova než jen upravit nějakou její část, která to zrovna vyžaduje. V tuto chvíli by ještě bylo na místě přesné vymezení pojmů v kontextu této práce. Backendem se myslí aplikace běžíci v Javě v rámci Google App Engine. Tato aplikace přijímá a odesílá požadavky přicházející od klienta. Klientem je v tuto chvíli počítač, respektive přímo webový prohlížeč, ve kterém běží a je zobrazován frontend aplikace. Frontend je vše, co uživatel vidí a co běží na jeho straně, což zahrnuje uživatelské rozhraní (tj. aktuální podobu stránky definovanou HTML kódem a CSS styly) a programovou vrstvu psanou v JavaScriptu, která umožňuje asynchronní komunikaci s backendem. Pokud budeme mluvit o aplikaci na straně klienta, budeme mít na mysli právě onu JavaScriptovou vrstvu. 16
2.2. Aplikace na straně klienta Celé uživatelské rozhraní je poháněno technologií AJAX, a tedy JavaScriptová vrstva, která toto zajišťuje, není zcela triviální. Na tu druhou stranu, je ponechána tak triviální, jak jen to bylo možné, a v konečném důsledku je relativně „hloupá“. Sama o sobě totiž funguje pouze jako jakýsi prostředník mezi uživatelským rozhraním a backendem. V praxi to znamená, že vrstva například zachytí uživatelovo kliknutí na odkaz, vyhodnotí, o jaký odkaz se jedná, a na základě toho vygeneruje dotaz na server a odešle ho. Příchozí odpověď analyzuje a provede pokyny zaslané serverem. Vrstva sama o sobě o ničem nerozhoduje. Jakým způsobem bude s požadavkem zacházeno je definováno už v samotném HTML kódu. Tedy při vytváření odkazu lze pomocí různých atributů ovlivnit, co se stane, až uživatel na odkaz klikne (tedy jestli má být nový obsah načten do hlavního panelu či do dialogového okna, zda má být uživatel požádán o potvrzení před provedením akce apod.). Na opačné straně pak může backend při formování odpovědi nadefinovat akci, která se má na straně klienta provést (obnovit obsah, zavřít dialogové okno apod.). Tedy ze strany serveru je možné přímo ovládat uživatelské rozhraní. JavaScriptová vrstva je tak v podstatě složena pouze akcí, které mohou být serverem aktivovány, a z definic, jakým způsobem je zacházeno s jednotlivými akcemi v uživatelském rozhraní. Navíc jsou zde pouze funkce pro manipulaci s uživatelským rozhraním, mimo jiné vytváření dialogových oken, různé aktivní formulářové prvky, řazení tabulek a jiné.
2.2.1. Struktura JavaScriptové vrstvy Aplikace na straně klienta je rozdělena do třech základních modulů, které jsou rozděleny do třech odpovídajících souborů. 1. tuudl-common.js. Tento soubor je ústřední klíčový modul této vrstvy a obsahuje nezbytné funkce pro „rozhýbání“ celého uživatelského rozhraní. V tomto souboru se nachází funkce, které jsou volány při kliknutí na odkaz či odeslání formuláře a vytváří asynchronní požadavky pro server. Dále jsou zde definovány funkce, které následně zachytávají odpovědi a analyzují je. Lze tu nalézt i základní akce, které lze volat ze strany serveru, mezi které patří funkce pro aktualizaci stránky, zavření dialogového okna, vypsání chybové hlášky, zamítnutí formuláře, přesměrování a mnoho dalších. 17
2. tuudl-init.js. Tento soubor obsahuje kromě inicializačních záležitostí provedených při startu aplikace i seznam definic, které se aplikují na jednotlivé objekty v uživatelském rozhraní. Zde se definuje, jaká funkce z tuudle-common.js se má spustit při kliknutí na odkaz či při odeslání formuláře. Aktivují se zde různé dynamické pomocné prvky, které zpříjemňují práci s uživatelským rozhraním, mimo jiné rozbalovací nabídky, kalendáře k formulářům či řazení tabulek. 3. tuudl-actions.js. Tento soubor obsahuje seznam specifických funkcí, které jsou volatelné ze strany serveru. Jedná se o konkrétní úkony svázané s přesně danými činnostmi. Kupříkladu pokud na straně serveru proběhne přidání objektu do databáze, tak server pak vydá pokyn klientovi, ať onen objekt zobrazí i v uživatelském rozhraní.
2.2.2. Načítání dynamického obsahu a simulace historie Běžné odkazy na webové stránce mají za úkol načíst nějaký nový obsah. V tom se ve většině případů neliší ani naše aplikace. Odkaz obvykle posune uživatele někam dále v uživatelském rozhraní, zobrazí mu další obsah. Rozdíl je ten, že při kliknutí na odkaz se neprovede synchronní požadavek a nenačte se celá stránka, nýbrž se onen požadavek provede asynchronně, a vrácená odpověď, obvykle útržek HTML kódu, který vygeneruje server na základě svých šablon (viz další kapitoly), se „vlepí“ na požadované místo. Tento přístup v podstatě odpovídá použití HTML rámů, kdy se také aktivně měnila pouze část stránky a zbytek zůstal nedotčen. Uživatelské rozhraní je přirozeně rozděleno do několika oblastí, máme tu hlavičku s názvem aplikace a základním menu pro přepínání mezi rolemi (to jediné menu není AJAXové a způsobuje kompletní načtení celé stránky), pak levý pruh, které obsahuje aktuální nabídku, a především vpravo hlavní obsah, kde se odehrává to důležité. Kdykoliv uživatel klikne na obyčejný odkaz, označený pouze CSS třídou ajax, která značí, že má být odkaz zpracován asynchronně, načte se jeho URL právě do oblasti tohoto hlavního obsahu. Spolu s ním se změní i titulek okna na základě nadpisu obsaženého v načteném obsahu. Problém je, že při běžném využití asynchronních požadavků zaniká možnost 18
využívat historii prohlížeče. Nemění se základní URL stránky a tedy není ani možno používat tlačítko Zpět, což není příliš uživatelsky přívětivé. Proto se zde používá drobný trik, který spočívá v tom, že se při změně hlavního obsahu připojí za aktuální neměnnou URL tzv. hash, což je část adresy uvedená za křížkem. V praxi to vypadá tak, že pokud uživatel asynchronně načte adresu /baseurl/listofobjects, tak v adresním řádku se objeví /baseurl/#listofobjects. Důležité je, že prohlížeč si tyto změny zapamatuje a stanou se součástí jeho historie. Pokud tedy pak uživatel stiskne tlačítko Zpět, prohlížeč do adresy dosadí zpátky předchozí hodnotu hashe, klientská aplikace to zachytí a do hlavního obsahu načte předchozí obsah. V nejnovějších prohlížečích je už podporována vlastnost, která umožňuje se bez tohoto triku obejít. V těchto prohlížečích je možné už dynamicky měnit přímo aktuální URL adresu prohlížeče a uživatel tak ani nepozná, zda komunikace probíhá synchronně či asynchronně. Nicméně pro zachování kompatibility byl použit ještě tento starší způsob. Při načítání nového obsahu do stránky je také potřeba myslet na to, že se součástí uživatelského rozhraní stane velké množství dalších prvků. Tyto prvky je opět nezbytné ručně aktivovat a napojit na ně jednotlivé funkce, aby byly schopny reagovat na uživatelské akce. K tomu je obecně určena funkce addHandlers() z tuudle-init.js, která právě obsahuje výše zmiňované definice. Při inicializaci rozhraní je tato funkce zavolaná na celou stránku (tím se uživatelskému rozhraní „vdechne život“) a pak už je volána pouze na nově načtené části stránky.
2.2.3. Další typy odkazů Odkaz nemusí pouze načíst další obsah do stránky, ale jeho kliknutí může být doprovozeno nějakou pokročilejší akcí. Jednotlivé definice akcí jsou uvedeny ve funkci linkDispatcher(), která je zavolána, kdykoliv uživatel klikne na AJAXový odkaz. Ve výchozím stavu se postará pouze o načtení nového obsahu jak bylo popsáno v předchozí podkapitole, nicméně odkaz může mít ve svém atributu rel zadefinovanou nějakou speciální akci. Definovány jsou mimo jiné následující možnosti: 1. Cíl odkazu může být otevřen v dialogovém okně (tj. odpověď na dotaz je 19
zobrazena v dialogovém okně). Toto je využíváno především u formulářů. Tato funkce nemá nějaký konkrétní praktický význam, nicméně dává uživateli pocit, že pracuje se skutečnou aplikací a ne pouze s webovou stránkou. Prostředí se stává bohatší a přehlednější. 2. Odkaz může sloužit k odstranění nějakého objektu. V takovém případě se nikam nenačítá žádný obsah, pouze se vyšle požadavek serveru a pokud se vrátí kladná odpověď, tak je objekt, jehož je odkaz součástí (například řádek tabulky či položka seznamu), skryt a odebrán z uživatelského rozhraní. 3. Odkaz může vyžadovat potvrzení před samotným zasláním požadavku serveru. V takovém případě se uživateli zobrazí elegantní dialogové okno se zprávou definovanou v rámci odkazu. 4. Odkaz může sloužit pouze k obnovení hlavního obsahu stránky, v takovém případě vůbec nezáleží na konkrétní adrese daného odkazu.
2.2.4. Vyhodnocování odpovědí serveru Kdykoliv je od serveru očekávaná jiná odpověď, než jen útržek HTML kódu, který má být vložen do stránky, případně pokud odpověď selže (což se bezpečně pozná podle HTTP kódu odpovědi), využije se funkce ajaxAction(), která odpověď analyzuje. Odpověď je obvykle zaslána v běžném formátu pro přenos dat json, který dokáže JavaScript převést na standardní objekt, který pak značně usnadňuje přístup k přijatým datům. Zaslaná zpráva obvykle obsahuje atribut action, který definuje akci k provedení, a dále atribut msg, což je nějaká doprovodná zpráva. Typicky může přijít od serveru odpověď s akcí failure, což značí, že uživatel má být informován o selhání. Pokud je zrovna otevřené okno s formulářem, u formuláře se objeví informace o selhání a ona zaslaná zpráva. V obecném případě na uživatele vyskočí malé dialogové okno s onou zprávou. To jsou ale ty nejjednodušší obecné případy. Specifické akce, jak bylo již zmíněno, jsou definovány odděleně v tuudl-actions.js a pokud při vyhodnocování odpovědi není nalezena požadovaná akce mezi těmi výchozími, obrátí se skript právě sem. Odpověď může mít kromě informaci o požadované akci a textové zprávě k sobě
20
přibalená v podstatě libovolná strukturovaná data, což se právě u těchto specifických akcí velmi hodí. Tady je na místě zmínit asi největší nedostatek tohoto návrhu. Problém je právě s těmito specifickými akcemi. Pokud například máme definovanou funkci, která na pokyn serveru přidá do tabulky v uživatelském rozhraní nově vložený objekt, musí tato funkce již respektovat formát dané tabulky. Nicméně ten je dán šablonou uloženou kdesi na serveru, která byla použita při generování oné tabulky. To znamená, že onu šablonu je nutné mít uloženou dvojmo, jednou u klienta a jednou na serveru, a tedy pokud dojde k úpravě šablony, je nutné ji upravit na obou místech. Tento problém lze řešit více způsoby. Bylo by teoreticky možné na straně serveru na základě tamější šablony „vygenerovat“ nový řádek a poslat ho zpět, přičemž klientská aplikace by ho pak už pouze vlepila na správné místo. Toto by ale vyžadovalo podstatné změny ve způsobu používání šablon na straně serveru. Či by bylo možné nechat si serverem zaslat kompletní novou vygenerovanou tabulku. Tento přístup je ve skutečnosti i na některých místech používán, nicméně v tu chvíli člověk zcela přijde o několikrát opakované výhody AJAXového řešení a musí mimo jiné počítat s problémem konzistence dat v databázi. Jinými slovy, těsně po vložení nového objektu se změna nemusí okamžitě projevit a nově vygenerovaná tabulka nový objekt nemusí vůbec obsahovat. Obecně asi nejčistší řešení (ale také nejnáročnější) by bylo napsat klientskou aplikaci podstatně složitější a v podstatě veškeré generování HTML kódu přesunout z backendu do klientské části. Server by tak zasílal jen seznamy objektů v nějakém strukturovaném formátu (například zmiňovaný json) a o konečný formát a způsob zobrazení v uživatelském rozhraní by se postarala čistě klientská aplikace. Tento způsob se v současné době v běžných webových aplikacích teprve prosazuje a pro naše využití se jedná o příliš komplexní řešení. I z tohoto důvodu jsme se rozhodli jít tou méně elegantní cestou.
2.2.5. Zpracovávání formulářů I při zpracování formulářů si vrstva udržuje maximální jednoduchost. JavaScript se běžně používá při validaci formulářů, nicméně to vždy znamená duplicitu kódu, 21
protože JavaScript se dá jednoduše vypnout a kontrola na straně serveru je tak jako tak nezbytná. Z tohoto důvodu je v naší aplikaci veškerá validace ponechána čistě na backendu, JavaScript se pouze postará o to, aby byl formulář v pořádku odeslán. Pokud formulář není validní a server s ním není spokojen, tak je pouze zobrazena u formuláře zaslaná chybová hláška. Tento přístup možná trochu kontrastuje s obecnou snahou maximálně šetřit prostředky serveru. Nicméně si je třeba uvědomit, že zrovna validace formulářů není nijak náročný proces, protože se v podstatě vůbec netýká úložiště (a kdyby ano, tak pomocí JavaScriptu by taková věc stejně nešla ošetřit). Veškeré náročné operace (tj. například vytvoření nového objektu na základu odeslaného formuláře) se provádějí až v okamžiku, kdy je formulář označen za korektní.
2.2.6. Technická implementace klientské vrstvy Ani v tomto případě jsme si nevystačili s poskytovaným jazykem (JavaScript v tomto případě) v čisté podobě. Pro usnadnění práce jsme využili svobodnou knihovnu jQuery. Tato knihovna sice nepatří k těm, které by vedly programátora k přehlednému strukturovanému kódu, to ale ani nebylo v našem případě potřeba, protože klientská vrstva je skutečně jednoduchá. I tak nám jQuery usnadnilo prakticky každý aspekt klientské aplikace, ať už šlo o asynchronní komunikaci se serverem, práci s formuláři či aktualizování uživatelského rozhraní. Pro jQuery navíc existuje velké množství hotových pluginů, které jsme pro naši aplikaci mohli okamžitě využít. Nicméně to už jsou technické detaily nepodstatné pro celkovou funkčnost aplikace.
2.3. Databáze Nyní se už přesuneme backendu, tedy části aplikace běžící na straně serveru, a začneme rovnou na nejnižší úrovni, u ukládání dat.
2.3.1. Vrstva JDO Jak bylo popsáno v analytické části práce (1.2.3 Ukládání dat do databáze, str. 6), k přístupu do úložiště nebylo využito přímo poskytovaného nízkoúrovňového API, nýbrž nadstavby JDO. 22
Při práci s JDO se pro manipulaci s objekty používá Persistence Manager. Tento nástroj umožňuje vytvářet, získávat, aktualizovat a mazat objekty z databáze. Při zpracování každého požadavku je na začátku vytvořena nová instance Peristence Manageru a na konci je korektně ukončena. O toto se stará statická třída tuudl.misc.Datastore. HTTP požadavky mohou být zpracovávány paralelně ve více vláknech, což tato třída zohledňuje při práci s jednotlivými instancemi Persistence Manageru. Samotné vytváření instancí má na starost statická třída tuudl.misc.PMF, což je zkratka pro Persistence Manager Factory.
2.3.2. Reprezentace entit pomocí objektů Jednotlivé objekty, dle terminologie App Engine entity, ukládané do databáze, jsou definovány pomocí odpovídajících Java tříd, které se nacházejí všechny v balíku tuudl.entities. Tyto třídy bývají často označovány jako „POJO“, tedy plain old java object. To znamená, že se jedná o naprosto primitivní objekty, které na sebe nenabalují žádnou další funkčnost ani nevyužívají nějakých pokročilých technik. Klíčové jsou především atributy těchto objektů. Kdybychom byli v oblasti relačních databází, tak by tyto atributy odpovídaly jednotlivým sloupcům tabulky. V případě Google App Engine Datastore odpovídají atributy jednotlivým vlastnostem entit. Tyto vlastnosti jsou označovány oficiálně jako properties. Klíčovou podmínkou těchto tříd je, že musí být serializovatelné, aby mohly být ukládány. Na místě je drobné sjednocení pojmů: 1. Entita je datový objekt uložený v Google App Engine Datastore. Tento objekt má svůj primární klíč a sadu vlastností. 2. Třídou se zde bude myslet Java třída, která definuje entitu (přesněji druh entity). Tedy každý atribut třídy odpovídá nějaké vlastnosti entity. Pokud by JDO běželo nad relační databází, jedna třída by definovala schéma jedné tabulky. 3. Objekt bude instancí třídy a tedy bude reprezentovat nějakou konkrétní entitu z úložiště. Tento objekt se bude ukládat pomocí Persistence Manageru.
23
V závislosti na kontextu také může objekt označovat obecnou součást aplikace, např. uživatele. Aby vrstva JDO poznala, že pracuje s objekty, které lze ukládat, a především aby věděla, jak je ukládat, je nutné odpovídající třídy takzvaně „povyýšit“. Toho se dosáhne pomocí javovských anotací. Je nutné označit celou třídu jako „persistence capable“ a dále pak podobným způsobem označit všechny atributy, které mají být ukládány. Ten druhý krok už není zcela nezbytný, většina atributů (konkrétně primitivní typy) jsou obvykle ukládány s třídou automaticky, aniž by musely být explicitně označovány. Nicméně je to dobrým zvykem pro zlepšení přehlednosti. Aby entita mohla být ukládána do databáze, je naprosto nezbytné, aby jeden z atributů třídy byl označen jako primární klíč (toho je rovněž dosaženo vhodnou anotací). Klíče mohou být vícero druhů. V případě, že se jedná o kořenovou entitu, tzn. že entita je kořenem nějaké entity group (to je i v případě, že je tato entita ve skupině zcela sama), lze jako klíč využít obyčejné číslo či řetězec. V terminologii App Engine jsou tyto klíče označovány za id resp. name. Pro třídy potomků už samozřejmě toto nestačí, protože jak jsme si vysvětlili v předchozí části, tyto entity jsou identifikovány klíčem, který obsahuje přímo klíč předka (a tedy všech předků až ke kořenové entitě). Toto musí třída reprezentující danou entitu respektovat a nabídnout jí odpovídající datový typ pro klíč. Zde se už obvykle využívá přímo typ Key (com.google.appengine.api.datastore.Key), případně jeho serializovaná podoba v řetězci. Tento datový typ není součástí JDO nýbrž přímo API poskytovaného App Engine. Klíč v tomto datovém typu je schopen naprosto jednoznačně identifikovat libovolnou entitu napříč úložištěm a přesně odpovídá tomu „reálnému“ klíči, se kterým jsou entity ukládány. To znamená, že tento klíč obsahuje sám o sobě všechny potřebné údaje k identifikaci. V případě kořenových entit to jsou buď jméno či id entity a její typ, v případě entit potomků obsahuje klíč dané entity (opět id či jméno, ale ty jsou generovány automaticky a jsou tak méně podstatné), klíč předka (a tedy klíč všech předků) a typ entity. Typ entity, jak bylo zmíněno, logicky odpovídá tabulce, do které je entita ukládána. V případě JDO, kdy se entity definují pomocí tříd, odpovídá typ názvu dané třídy. 24
API Google App Engine umožňuje kompletní klíč vypočítat, pokud máme k dispozici potřebné údaje. To se velice hodí, protože při komunikaci mezi frontendem a backendem není potřeba často předávat kompletní klíč, nýbrž u kořenových entit úplně stačí id či jméno. Typ entity obvykle automaticky vyplyne z aktuálního kontextu a jen na základě těchto dvou údajů lze vypočítat kompletní klíč a potřebnou entitu načíst. V případě nekořenových entit to už bohužel takto nefunguje a je skutečně nutné (resp. je to ta nejsnazší cesta) předávat kompletní klíč. V případě URL se využívá pro předání serializovaná textová podobna klíče. Důsledkem je, že uživatel při práci s aplikací vidí v adresním řádku velmi dlouhé adresy, protože klíče některých hluboko zanořených entit můžou narůst skutečně do značných rozměrů. Třída reprezentující nějakou entitu může přirozeně obsahovat i nějakou pokročilou logiku (metody) pro manipulaci s danou entitou. Je to sice možné, ale není to doporučované. Při ukládání entity se spolu s ní ukládá i samotná třída v serializované podobě. Pokud by taková třída byla komplexní a rozsáhlá, projevilo by se to na výkonu při ukládání takové třídy. Proto je role těchto tříd omezena na čistě reprezentativní funkci (tedy kromě samotných definic atributů obsahují obvykle jen jednoduchý konstruktor a gettery a settery) a veškerá pokročilá logika je přesunuta do modelů, o kterých bude řečeno více později. Pro teď pouze uvedeme, že každá entita je obalena odpovídajícím modelem, který obsahuje veškerou funkčnost svázanou s danou entitou a poskytuje univerzální jednotné API pro zbytek aplikace.
2.3.3. Databázové schéma Následující UML-like diagram (Ilustrace 1) popisuje strukturu modelů v aplikaci. V předchozím odstavci jsme uvedli, že model pouze obaluje entitu, nicméně to není úplně pravda ve všech případech a tedy reálná struktura entit v databázi se mírně liší, především v závislosti na použitém vztahu.
25
Ilustrace 1 – schéma modelů v aplikaci Z diagramu (Ilustrace 1) je jasně vidět, jaké entity se budou v úložišti nacházet (až na drobné výjimky, viz dále), jaké budou obsahovat vlastnosti a především jaké vztahy budou mezi entitami panovat. Ústředním prvkem bude uživatel. Ten bude řazen do organizačních skupin. Na uživatele budou navázány členství ve studijních skupinách (v podstatě reprezentace vztahu n:n mezi uživatelem a studijní skupinou) a na každé členství budou navázaný jeho řešení domácích úkolů. Dále se v úložišti budou nacházet entity studijních skupin, na které budou navázány jednotlivé domácí úkoly. Kousek strano budou stát „zdroje“, které budou moci býti napojeny na řadu dalších entit. V schématu (Ilustrace 1) je důležité barevné označení jednotlivých entit. Entity stejného odstínu šedi se budou nacházet ve stejné entity group. Vztahy mezi těmito entitami budou owned, zatímco vztahy mezi entitami označenými různými odstíny šedi budou unowned. Ze schématu je tak krásně vidět, které dotazy budou náchylné 26
na problém s konzistencí a které naopak budou spolehlivé. Pokud si student zobrazí seznam skupin, jejichž je členem, případně seznam svých řešení domácích úkolů, dostane vždy spolehlivé výsledky. Stejně tak, pokud učitel zobrazí seznam úkolů v rámci studijní skupiny, tak i tento seznam bude spolehlivě aktuální. Ale naopak, pokud si učitel zobrazí seznam svých studijních skupin, z nichž každá tvoří svou vlastní samostatnou study group, tak tento seznam už konzistentní být nemusí. Smysl by tedy dávalo implementovat i tento vztah (studijní skupiny a jejího kantora) jako owned. Problém nekořenových entit je ten, že jejich klíče automaticky obsahují klíč kořenové entity. Tedy pokud bychom chtěli vzít studijní skupinu a přesunout ji jinému učiteli (což není vůbec nereálný požadavek), museli bychom změnit klíč této studijní skupiny a tedy změnit klíč veškerých dalších navázaných entit. V podstatě by bylo jednodušší vytvořit kopii celého podstromu a tu původní smazat. To ale už zcela postrádá smysl. Obecně je aplikace počínaje schématem databáze navržena tak, aby nejširší základna uživatelů, tedy studenti, s problémem konzistence vůbec nepřišli do styku. Kantorů a správců se už bude dotýkat více, nicméně tam je zase kryta celkem důsledně klientskou vrstvou a pokud už se tento problém projeví, tak pouze u těch méně častých operací a uživatel je na toto riziko upozorněn.
2.3.4. Implementace vztahů mezi entitami V případě relačních databází je vztah mezi dvěma tabulkami vyřešen jednoduše cizím klíčem. Nic více se neřeší. V případě naší aplikace je to podstatně komplikovanější a přístupů k implementaci vztahu je využita celá řada. 1. „Neexistující“ vztah mezi uživatelem a studentem (resp. kantorem). Ve výše uvedeném schématu je naznačený 1:1 vztah mezi uživatelem a studentem (resp. kantorem). Tento vztah by eventuálně přicházel skutečně k úvahu, pokud by se na studentskou (či kantorskou) roli uživatele navazovalo nějaké velké množství dalších informací. Nicméně to se neděje a student (resp. kantor) by tak byla v podstatě jen prázdná entita navíc. Fyzicky tak existuje pouze entita uživatele, která je rovnou napojena na další entity. Nicméně existence těchto dvou pseudo objektů není bezdůvodná. Sice
27
neobsahují žádné extra informace, ale nabalují na sebe funkčnost specifickou pro konkrétní roli. A tu bylo vhodné oddělit od funkčnosti čistého uživatele. 2. „Neexistující“ vztah mezi zdrojem a konkrétním typem zdroje. V aplikaci se nachází dva typy zdrojů. Fyzické soubory a externí webové odkazy. Každý z nich je reprezentován samostatnou entitou, protože každý z nich potřebuje ukládat trochu jiný typ dat. Nicméně zbytek aplikace potřebuje s těmito zdroji pracovat univerzálně, naprosto nezávisle na typu, a proto jsou tyto zdroje poděděné od jedné mateřské třídy, která obsahuje pouze vlastnosti společné pro oba typy zdrojů. Byť by to tak ze schématu mohlo vypadat, univerzální předek zdroje žádnou entitu nemá a na další objekty jsou napojeny přímo jednotlivé entity zdrojů, ať už jednoho či druhého typu. 3. Owned vztah v rámci entity group. Ve všech ostatních případech je už vztah v rámci entity group klasický reálný, mezi dvěma existujícími entitami. Owned vztah je reprezentován javovskou kolekcí objektů. Tedy rodičovská entita, resp. třída, která ji definuje, má v sobě jako atribut přímo kolekci potomků. Použitý je obvykle List. Samotný potomek potom neobsahuje explicitně uvedený cizí klíč na rodiče. Nicméně pokud potřebujeme z potomka přistoupit na rodiče, můžeme k tomu využít onu vlastnost klíče a to, že klíč potomka v sobě implicitně obsahuje klíč rodiče. Tedy stačí vzít klíč potomka, pomocí Google App Engine API z něj rodičovský klíč získat a ten použít pro načtení rodiče. Toto je jeden z případů, kdy se využívá unikátních vlastností Google App Engine a znemožňuje se tak pohodlná změna použité databáze. JDO nabízí oficiální cestu, jak tento problém řešit, nicméně po počátečních pokusech se ukázalo, že popsaný způsob je nejefektivnější. 4. Unowned n:n vztah mezi objektem a zdrojem. Zdroje jsou speciální v tom, že jsou navrženy tak, aby šly univerzálně napojovat na libovolný jiný objekt v aplikaci. Vzniká tu tak klasický n:n vztah, objekt může mít k sobě připojenou celou řadu zdrojů, přičemž zdroj samotný může být uveden u několika různých objektů. Této později jmenované vlastnosti se zatím v aplikaci nikde nevyužívá, nicméně je pro ni připravena podpora, pokud by 28
se aplikace později rozšiřovala. Samotný vztah je opět reprezentován pomocí kolekcí, ale ne kolekcí objektů, nýbrž kolekcí klíčů. Třída entity, která na sebe navazuje zdroje, tak v definici obsahuje kolekci klíčů. Výhodou je, že klíč (Key), je jednotný datový typ pro všechny typy entit. Tedy vůbec nevadí, že v jedné kolekci ukládáme různé typy zdrojů, nemusí se zde pracovat ani s onou dědičností. Pokud potřebujeme získat z databáze všechny napojené zdroje, stačí vzít onu kolekci a vytvořit z ní tzv. batch query. To je speciálně nástroj úložiště (opět běžná databáze toto nepodporuje), který umožňuje relativně levně vrátit množinu objektů na základě jejich klíčů. Samotná entita zdroje v sobě neobsahuje informaci o tom, k jakým objektům je připojená. Pouze si udržuje číslo, které udává aktuální počet objektů, které jí využívají, tj. počet referencí. V okamžiku, kdy toto číslo klesne na nulu, je tento zdroj odstraněn. 5. Všechny ostatní unowned vztahy. Zbylé unowned vztahy už jsou ve své podstatě jednoduché a využívají standardní techniky cizích klíčů. Jako datový typ klíče je opět použit nativní Key. Tedy například studijní skupina má v sobě uložený klíč kantora, který ji má na starosti. Na základě tohoto klíče je možné kdykoliv kantora z databáze získat a naopak, pokud kantor chce přistoupit ke všem svým studijním skupinám, stačí podle tohoto atributu filtrovat. Přirozenou výhodou tohoto vztahu je, že klíč lze jednoduše změnit, tedy není problém předat studijní skupinu jinému uživateli.
2.3.5. Dotazování a indexy Dotazy do databáze se provádí pomocí Persistence Manageru, který vrací kolekce objektů. K dotazování lze použít JDO API či přímo klasickou SQL syntaxi, která je nicméně silně omezená. Vybírat entity z databáze lze samozřejmě podle jejich typu, lze je filtrovat podle valné většiny atributů a stejně tak řadit. Podmínky filtrování lze řetězit, nicméně při použití běžného logického operátoru OR je potřeba myslet na to, že se ve skutečnosti provádí za každé použití tohoto operátoru jeden dotaz navíc. Tedy dotaz na uživatele, kteří se jmenují Jan či Pavel, jsou ve skutečnosti dva dotazy, jeden na všechny Jany a jeden na všechny Pavly. Dotazy přirozeně spotřebovávají prostředky, proto je toto potřeba brát na zřetel. 29
Pro každý atribut, podle kterého se v dotazech řadí či filtruje, je nezbytné vytvořit index. Tyto indexy jsou uváděny v souboru datastore-indexes.xml. Pokud je pro vývoj použito vývojové prostředí poskytované Googlem, jsou veškeré potřebné indexy vytvořeny již při vývoji během testování. Nahraná aplikace je pak schopná okamžitě sloužit. Problém s indexy je ten, že podstatně zvyšují ceny zápisu a aktualizací entit.
2.4. Architektura MVC Architektura MVC [10] je často používaný koncept při výstavbě webových (a nejen webových) aplikací. Architektura definuje tři základní komponenty, model, view, controller, a určuje jednak povinnosti jednotlivých komponent a jednak jakým způsobem mezi sebou komponenty komunikují. Koncept MVC není nicméně zcela jednoznačně určen. Jsou zde určité základní myšlenky, nicméně konkrétní realizace a rozdělení úloh se už může aplikace od aplikace lišit. Ani Túúdl v tomto nebude jiný. Pevně daná je pouze základní struktura, která vychází z frameworku Spring MVC. Jednotlivé komponenty budou mít v Túúdlu následující úlohy: 1. Model bude reprezentovat vnitřní logiku aplikace. V zásadě se bude jednat o soubor tříd, které se budou starat o zpracování dat a ukládání do databáze. Kupříkladu model pro studijní skupiny bude zajišťovat veškeré možné operace se studijními skupinami, tedy úpravy, zobrazování, vytváření nových apod. Tyto akce bude umožňovat přes veřejné rozhraní. 2. View (či pohled) bude reprezentovat konkrétní uživatelský výstup, tedy co uživateli bude odesláno a co uvidí. V zásadě půjde o HTML šablonu, do které budou vyplněny informace poskytnuté modelem. Technicky se bude jednat o JSP soubory, které budou využívat JSLT tagy.. 3. Controller pak bude zachytávat jednotlivé požadavky ze strany klienta a na jejich základě bude aktualizovat modely a aktivovat pohledy. Každý controller bude reprezentován speciální třídou, která bude pomocí Spring frameworku povýšena na controller a tedy nabude schopnosti zachytávat 30
požadavky. V praxi to bude vypadat tak, že na každou metodu controlleru bude namapována konkrétní URL adresa (či nějaká její část). Efektivní namapování opět umožní Spring framework. Aplikace bude obsahovat z důvodů přehlednosti controllerů více, konkrétně 4 základních a několik doplňkových. Bude jeden základní, který bude obstarávat důležité systémové požadavky (autentizace uživatelů, změna hesla apod.) a poté každý modul aplikace (tak, jak byly popsány v uživatelské dokumentaci, viz přílohy, str. 64) bude mít svůj vlastní controller. Tedy modul pro správce Túúdlu bude mít svůj controller, modul pro kantora bude mít svůj controller a to samé i modul pro studenta. Naopak modely budou moci být využívány napříč celou aplikací. Kupříkladu upravovat studijní skupiny bude moci jak kantor, tak správce, nicméně každý trochu jiným způsobem. Celou funkčnost bude zastřešovat model pro práci se studijními skupinami a jednotlivé controllery pak rozhodnou, jakým způsobem bude s modelem pracováno a následně pomocí pohledů jakým způsobem budou data zobrazena. Typické vyhodnocení požadavku tedy bude probíhat tak, že Spring framework požadavek zachytí a na základě adresy najde odpovídající controller. Controller provede požadovanou akci, např. vyžádá od modelu seznam nějakých informací, předá je správnému pohledu (view), který se pak postará o konkrétní výstup, který následně bude odeslán uživateli.
2.5. Fungování controllerů ve Spring MVC Controllery ve Spring MVC jsou poměrně zajímavé a případnému čtenáři kódu neznalému Spring MVC frameworku nemusí být z kódu některé věci na první pohled jasné. Tato podkapitola bude proto spíše technická a pokusí se stručně osvětlit některé techniky v controllerech, jak fungují a jakým způsobem jich naše aplikace využívá. Samotný controller je jednoduchá třída, která je namapována na nějakou adresu, na které naslouchá. O vyřizování požadavků se starají metody controlleru, přičemž ty jsou rovněž namapovány na nějakou adresu, resp. podadresu. Toto mapování může být nakonfigurováno externě v XML konfiguraci frameworku, nicméně mnohem
31
intuitivnější je konfigurace pomocí anotace @RequestMapping. Pokud je controller nastaven, aby naslouchal na adrese /admin, a nějaké metoda je nastavena, aby naslouchala na adrese home, potom při přístupu na adresu /admin/home bude tento požadavek vyřizovat právě tato metoda. Důležitá je návratová hodnota metody, ta totiž určuje, co se bude dít dále. Pokud je návratová hodnota řetězec, tak framework očekává, že mu bude vrácen název pohledu, který se má zobrazit. To je nejčastější případ. Ve vráceném řetězci může být nicméně obsažen i nějaký příkaz, třeba na přesměrování někam jinam. Vracet se také nemusí nic (návratový typ void), v takovém případě nebude aktivován žádný pohled a o vygenerování odpovědi se musí postarat samotná metoda.
2.5.1. Parametry v signatuře metody controlleru To opravdu zajímavé nastává až u parametrů controllerové metody. Spring MVC nám totiž nabízí ohromnou škálu parametrů, které si můžeme do signatury metody jednoduše dopsat, a Spring MVC nám při volání této metody potřebné hodnoty prostě dosadí. Možnosti dosazovaných parametrů jsou mimo jiné tyto: 1. HttpServletRequest a HttpServletResponse. Pokud si nějaký parametr v signatuře označíme jedním z těchto datových typů, bude do této proměnné dosazen objekt aktuálního HTTP požadavku, resp. objekt odpovědi, která bude vrácena klientovi. To se hodí v případě, kdy potřebujeme jemněji pracovat s požadavkem/odpovědí, například nastavit HTTP kód odpovědi. 2. Parametry označené anotací @PathVariable. Spring MVC umožňuje při mapování metody na danou adresu označit část (či více části) adresy jako proměnnou s nějakou variabilní hodnotou. Tato hodnota je pak automaticky dosazena do takto označeného parametru. Co je důležité, funguje zde automatické přetypování. Takže pokud takové proměnné dáme datový typ Boolean, bude tato metoda akceptovat pouze adresy obsahující řetězce true či false, které budou automaticky přetypovány na odpovídající Boolean hodnotu. V opačném případě požadavek skončí výjimkou. 3. Podobně lze přistupovat přímo k HTTP parametrům požadavků, ať už se
32
jedná o POST či GET data. Stačí označit nějaký parametr metody anotací @RequestParam spolu s názvem parametru a jeho hodnota je automaticky dosazena. I zde funguje přetypování. 4. Speciální parametry jsou pak s anotací @ModelAttribute nebo s typem ModelMap. Ty slouží pro manipulaci s modely a budou popsány podrobněji v následující podkapitole.
2.5.2. Předávání dat pohledům a práce s modely Role controlleru, jak je psáno v architektuře MVC, spočívá v tom, že zachytává požadavky od uživatele a podle nich aktualizuje modely. K těmto modelům pak má přístup pohled a na jejich základě vykreslí stránku. Někdy (či spíše většinou v případě naší aplikace) je nutné explicitně model pohledu předat. Tady je na místě specifikovat, co se konkrétně modelem myslí. V případě Túúdlu je model v tom nejužším smyslu objekt, který obaluje nějakou entitu a poskytuje API pro práci s touto entitou. V kontextu Spring MVC je model obecně objekt, který se „vznáší“ v prostoru a má k němu přístup jak controller, tak pohled (obecně vzato bývá ve Spring MVC jako model označována celá množina těchto objektů, přičemž jednotlivé objekty jsou označovány jako atributy modelu). Controller si k tomuto prostoru může vynutit explicitní přístup právě skrz parametr označený datovým typem ModelMap. Jak název napovídá, jedná se o běžnou javovsku mapu, která v sobě obsahuje jednotlivé modely. V controlleru pak stačí do této mapy přidat vše, co potřebujeme (kupříkladu nějaká načtená data z databáze), a v pohledu k nim pak máme pohodlný přístup. V Javě jsou z principu všechny objekty předávány referencí, a proto i v tomto případě dostaneme do metody pouze „odkaz“ na prostor s modely. Není ho proto třeba nijak vracet, pouze ho upravit, a o zbytek se SpringMVC postará sám. Model není nicméně potřeba do prostoru přidávat takto explicitně. V rámci controllerů (a teoreticky i mimo ně) lze definovat speciální metody označené anotací @ModelAttribute včetně určujícího názvu. Pokud se pak kupříkladu v pohledu pokusím přistoupit k modelu s tímto názvem, a model dosud v prostoru neexistuje, je
33
Springem automaticky zavolána takto označená metoda, která model vrátí. Tento model je pak přidán do prostoru modelů a od toho okamžiku je pak obecně přístupný, tedy ona metoda je v rámci požadavku volána vždy jen jednou. Tohoto lze využívat nejen v pohledech, ale přímo v controllerech. Přirozeně controller má k prostoru modelů přístup skrz ModelMap objekt, nicméně existuje ještě jednodušší cesta, a to pomocí zmíněných parametrů metody označných anotací @ModelAttribute (tedy stejnou anotací, jako v případě metod generujících samotný model). V tomto případě ale tato anotace zaručí, že do takto označeného parametru je automaticky dosazen objekt z modelového prostoru. A pokud model daného názvu ještě neexistuje, využije Spring k vytvoření právě ony generující metody. Tyto atributy v praxi zpřehledňují kód a umožňují vyhnout se opakovanému psaní toho samého. K některým informacím je nutné mít přístup prakticky pořád. Jedná se například o aktuálně přihlášeného uživatele či globální nastavení aplikace. Proto pro tyto dva případy máme připravené generující metody, které potřebné údaje zajistí, ať už přihlášeného uživatele či systémové nastavení. V případě controllerů pro studentské resp. kantorské rozhraní je to dotažené do takové míry, že není vracen objekt uživatele, ale přímo instance studenta resp. kantora. Do signatury metody zpracující požadavek pak stačí dopsat parametr s potřebnou anotací a datovým typem a získáme tak okamžitě přístup k potřebným informacím, aniž bychom je museli někde pracně dohledávat.
2.5.3. Pokročilé přetypování parametrů Výše bylo zmíněno, že Spring MVC umožňuje veškeré vstupní parametry požadavku automaticky přetypovat z řetězce na konkrétní typ. Nicméně v základu se to týká pouze obecných datových typů (čísla, Boolean). Pokud převod selže, je vyhozena výjimka. Spring MVC nicméně umožňuje definovat vlastní typy pro převod. To je extrémně užitečné v případech, kdy parametr obsahuje id či klíč nějaké entity v úložišti. Běžný přístup by byl ten, že by metoda controlleru dostala klíč v surovém stavu, tedy jako nějaký řetězec, a ten by pak musela použít k získání objektu z databáze. To je
34
nicméně neustále se opakující operace, které je velice příhodné se zbavit. Slouží k tomu tzv. PropertyEditory. PropertyEditor je jednoduchá třída, která rozšiřuje obecný PropertyEditor Springu, a obsahuje mimo konstruktoru jedinou metodu setAsText(). Tato metoda dostane jako parametr řetězec obsahující daný HTTP parametr a má za úkol tento parametr převést na požadovaný datový typ. V případě, že vstupním parametrem je nějaký klíč, tak toto „převedení“ znamená načtení objektu z databáze. Každý potřebný datový typ má svůj PropertyEditor, přičemž propojení datových typů a jednotlivých PropertyEditorů probíhá ve třídě GlobalBindingInitializer, který se spolu se všemi ručně definovanými Property Editory nachází v balíku tuudl.misc.propertyeditors. V praxi to tedy funguje tak, že u potřebné metody controlleru si místo String studyGroupKey vložíme do signatury StudyGroup studyGroup. Spring MVC detekuje datový typ StudyGroup a najde k němu odpovídající Property Editor. Editor dostane k dispozici zaslaný klíč, načte z databáze odpovídající entitu a metodě controlleru předá připravený model s touto entitou. Elegantní na tomto řešení je i to, že už v této mezivrstvě je ošetřeno, že skutečně požadovaný objekt existuje, a není to tedy potřeba řešit pro každý požadavek zvlášť.
2.5.4. AJAXové odpovědi klientovi Při komunikaci mezi klientem a serverem probíhá naprostá většina požadavků asynchronně, přičemž je tlak na to, aby bylo přenášeno minimum dat. To znamená, že jen některé odpovědi obsahují přímo nějaký rozsáhlejší kus stránky (tabulka s výpisem objektů, formulář), zatímco ty ostatní, většinou odpovědi po nějaké provedené změně, obsahují v podstatě jen krátkou zprávu o úspěchu či neúspěchu. Pro prve jmenovaný případ se využívají pohledy, které jsou na to přímo zamýšlené. Pokud uživatel vyžaduje například tabulku svých domácích úkolů, použije se odpovídající pohled. Nicméně pro odesílání krátkých odpovědí je tento přístup zbytečný a místo nich odesíláme odpověď ve strukturovaném formátu json. Aby bylo takovéto odpovídání pohodlné, je v aplikaci k dispozici statická třída tuudl.misc.AJAX. Tato třída obsahuje několik předpřipravených metod, které umožňují z controllerů nejen pohodlně odesílat zprávu o výsledku, ale i přímo
35
transparentně volat jednotlivé akce na straně klienta. Kupříkladu zavoláním metody AJAX.hide() se na straně klienta skryje právě otevřené dialogové okno. Tato metoda jednoduše vytvoří zprávu ve formátu json v potřebném formátu a pošle ji klientovi, který si ji už přebere. Samozřejmě z principu HTTP protokolu, tedy princip požadavek/odpověď, není možné se kdykoliv rozhodnout (třeba v okamžiku, kdy uživatel s aplikací vůbec nepracuje a má ji pouze otevřenou) a začít manipulovat s klientem na dálku. Vždy je nutné, aby napřed uživatel vyvolal nějaký požadavek a teprve odpověď na tento požadavek může obsahovat nějakou akci, která má být na straně klienta provedena. Samozřejmě bylo by teoreticky možné otevřít mezi klientem a serverem trvalé spojení (resp. to v HTTP možné není, ale lze to docela efektivně simulovat), nicméně bylo by to zbytečné, v Túúdlu pro to není reálné využití. Tento postup sice lehce nabourává koncept MVC a především doporučovanou práci se Spring MVC (to je ten případ, kdy metoda obsluhující požadavek nevrací nic, protože o odpověď se zcela postará třída AJAX), nicméně se velice pohodlně používá.
2.6. Modelová vrstva Modelová vrstva reprezentuje vnitřní logiku aplikace. V našem případě je tu od toho, aby odstínila zbytek aplikace od přímého přístupu k databázi a místo toho nabídla univerzální pohodlné API pro získávání a manipulaci s daty. Tohoto API využívají controllery při řešení požadavků od klienta a stejně tak ho využívají pohledy při generování hotových stránek pro klienta. Na nejnižší úrovni (resp. na nejnižší úrovni v rámci naší aplikace) jsou entity z databáze reprezentovány pomocí speciálních jednoduchých objektů, které lze načítat a zase zpět ukládat do databáze (viz kapitola o JDO). Modelová vrstva pracuje právě s těmito objekty. Pro zjednodušení, kdykoliv bude dále v textu o modelech zmíněna entita, budeme tím mít na mysli jak fyzickou entitu uloženou v databázi, tak i přímo objekt, který v naší aplikaci tuto entitu reprezentuje. Způsobů, jak implementovat modelovou vrstvu je více. Jednou z možností je napsat si pro jednotlivé oblasti služby. Službou se zde myslí nějaká třída, která poskytuje 36
API pro snadnou manipulaci s daným typem objektu. Tedy například bychom mohli mít službu pro práci se studijními skupinami, která by umožňovala jednoduše vytvářet, mazat, upravovat a vyhledávat studijní skupiny. Tato služba bývá často označována jako Data Access Object. Túúdl jde nicméně trochu jinou cestou. Zatímco služba by existovala v jedné instanci a pracovala by s libovolným množstvím entit, tak v Túúdlu se pracuje s objekty, které mají sice stejný úkol, jako služby, ale na rozdíl od nich instance jednoho takového objektu je pevně svázána s nějakou konkrétní entitou. A tomuto objektu se právě v naší aplikaci říká model. Model je tedy jakási schránka, která obaluje entitu, a poskytuje vnější rozhraní pro práci s touto entitou. V praxi tedy máme pro každý objekt v Túúdlu model, který poskytuje veškerou související funkčnost. Uživatel má svůj model, kantor má svůj model, úkol má svůj model, studijní skupina má svůj model atd. Všechny modely jsou umístěny do balíku tuudl.models. Záměrně tady říkáme, že každý „objekt“ má svůj model, nikoliv entita, protože to by nebyla úplně pravda. V aplikaci se sice operuje s učitelem či studenty, nicméně tyto dva „objekty“ nemají v databázi odpovídající entitu. Ještě je na místě si ujasnit, co naopak modely nedělají. Modely sice umožňuji libovolným způsobem s objektem manipulovat, nicméně jsou v tomto ohledu naprosto důvěřivé. Tedy neřeší, zda aktuální uživatel má oprávnění k prováděné akci. Neřeší ani, zda vkládaná data jsou validní či zda daná operace nemůže narušit konzistenci databáze. Tato odpovědnost je zcela na controllerech. Z tohoto pohledu by mělo smysl zavést ještě nějaký koncept služeb, které by byly umístěny mezi controller a konkrétní model. Tyto služby by měly přístup k aktuálně přihlášenému uživateli a tedy by byly schopny samostatně vyhodnocovat, zda je daná operace povolená. Odpovídalo by to i lépe MVC architektuře, protože tato funkce skutečně spíš přísluší do modelu a ne do controlleru. Nicméně v Túúdlu jsou veškeré tyto validce a ověřovány zajištěny v controllerech a to hlavně proto, že každý controller má tyto požadavky trochu jiné a ověřování probíhá trochu jiným způsobem. Vznikla by tak poměrně rozsáhlá a zbytečně „tlustá“ vrstva služeb poskytujících funkcionalitu, která se poměrně pohodlně vyřeší přímo v controllerech.
37
2.6.1. Odříznutí od specifikace databáze Jednou z nejzákladnějších vlastností modelové vrstvy je vytváření abstrakce nad specifickými vlastnostmi úložiště. Tedy přímo k databázi lze přistupovat výhradně skrz modely a zbytek aplikace se tedy nestará, jakým způsobem je uvnitř databáze řešena. Toto bylo už několikrát zmíněno, nicméně jedná se o tak klíčovou záležitost, že jí věnujeme samostatnou podkapitolu. Už samotná vrstva JDO vytváří dost silnou vrstvu abstrakce. Nicméně jak bylo popsáno v příslušné kapitole (1.2.3 Ukládání dat do databáze, str. 6), Google App Engine Datastore je tak osobité úložiště, že některá jeho specifika stejně proniknou i skrz tuto vrstvu. Tedy zamýšlený záměr, aby šlo změnit typ databáze prostou výměnou ovladače ve vrstvě JDO, se úplně splnit nepodařilo, nicméně posuneme-li se o úroveň výše, tedy do vrstvy modelů, tady už to splnitelné je. Pokud by bylo kdy nutné změnit platformu, stačí upravit pouze vrstvu modelů, aby odpovídala novému úložišti. Ve zbytku aplikace by se to absolutně nijak neprojevilo. Jediný bod, který v návrhu nebyl dotažený úplně do důsledků, se týká identifikace entit. Některé entity využívají číselné id, některé jednoduché řetězcové klíče a některé kompletní dlouhé App Engine Datastore klíče. V případě přechodu na jiné úložiště by bylo toto jediné nezbytné nějakým způsobem sjednotit. Samotná abstrakce funguje následujícím způsobem. Typicky máme v controlleru k dispozici model nějakého objektu, třeba studijní skupiny. Pokud chci upravit název této
skupiny,
stačí
mi
zavolat
studyGroup.setName()
a
následně
studyGroup.save(). Model zachytí jméno a předá ho entitě. Zde bohužel dochází k jisté duplikaci rozhraní, protože pro každou vlastnost entity, což znamená jeden getter a jeden setter, je nutné dodefinovat odpovídající gettery a settery ještě do modelu. Nicméně to je jen daň za to, že controller, který úpravu provádí, nemá vůbec ponětí o nějaké entitě. Abstrakce je také extrémně užitečná při práci se vztahy mezi entitami. Jak bylo popsáno, existuje několik způsobů, jakým může být vztah mezi entitami implementován, nicméně modely toto efektně schovávají pod univerzální API. Pokud například na modelu studijní skupiny zavolám getTeacher(), tak se jedná 38
o unowned vztah speciální navíc tím, že entita učitele jako takového neexistuje a pracuje se přímo s entitou uživatele. V takovém případě si model studijní skupiny sáhne do odpovídající entity, vytáhne klíč uživatele, načte entitu uživatele, zabalí ho do modelu a přetypuje na na model učitele. Jiný případ může nastat, pokud zavoláme na modelu domácího úkolu metodu getStudyGroup(). Tento vztah už je owned. Model domácího úkolu tedy vezme klíč svojí entity, pomocí Google App Engine API z něj vytáhne klíč mateřské studijní skupiny, načte z databáze její entitu, obalí ji modelem a vrátí ji. V obou případech se jedná o implementační detaily, o kterých controller či pohled nemají vůbec ponětí a nemusí je vůbec zajímat. Z tohoto přístupu bohužel vyplývá jedna nehezká povinnost a tou je neustále obalování modelů. Toto je nepříjemné především u dotazů. V případě JDO dotaz vrátí kolekci entit. Aby byla nicméně zachována důsledná abstrakce, je vždy nutné před vrácením tuto kolekci ručně projít a každou z entit obalit odpovídajícím modelem. Výkonově to problematické není, protože se obvykle pracuje s malými počty entit, nicméně jedná se opět o opakující se činnost, které je vhodné se za běžných okolností vyhnout.
2.6.2. Struktura modelů a jejich inicializace Každý model v aplikaci je potomkem společného předka, kterým je třída tuudl.models.Model. Tato třída je abstraktní a generická, což znamená, že se jedná o obecnou obálku nějaké entity. Odvozené třídy pak přesně specifikují, který konkrétní typ entity bude model obalovat. Tento společný předek zajišťuje veškeré operace společné pro všechny modely. Týká se to především záležitostí na té nejnižší úrovni, což zahrnuje načítání, ukládání a mazání entit. V podstatě se tak dá říci, že tyto nízkoúrovňové operace jsou odděleny do samostatné vrstvy a konkrétní implementace modelů se už o tyto záležitosti nestarají. Pokud chce aplikace pracovat s modelem, je napřed nezbytné ho inicializovat. K inicializaci modelů slouží továrnička ModelFactory. K této továrně mají přístup controllery (a obecně téměř všechny objekty v aplikac) a je díky ní možné získat novou instanci libovolného modelu. Továrna nové instanci automaticky dodá potřebné služby pro přístup do databáze (Persistence Manager) a do cache. Předá jim
39
i referenci na sebe samu, protože i samotné modely musí být schopny inicializovat další modely. Takto inicializovaná instance modelu samozřejmě není ničím jiným než prázdnou schránkou, do které je potřeba vložit nějakou tu entitu. Ta se tam může dostat třemi různými způsoby. 1. V případě, že chceme přidat do databáze nějaký nový objekt, třeba vytvořit nového uživatele, necháme model, ať vytvoří sám novou odpovídající prázdnou entitu. K tomu slouží metoda modelu create(). 2. V případě, že požadujeme načtení nějakého objektu, máme k dispozici metodu fetch(). Ta existuje v několika různých mutacích, které se liší vstupním parametrem. Může se jednat o číselné id, kompletní klíč v textové formě (označován jako encodedKey) či přímo o klíč v nativním datovém typu Key. Ve všech případech se jedná o identifikaci, která musí přesně určit nějakou entitu. Model se tuto entitu pokusí načíst a pokud se mu to podaří, tak si ji zapamatuje. V opačném případě vyhodí ObjectNotFound výjimku. 3. Třetí možností je modelu přímo předat existující entitu. To se hodí v případě, že obalujeme modely výsledky nějakého dotazu. Dotaz z databáze vrátí kolekci entit, mi ji projedeme a pro každou z nich vytvoříme model, přičemž tomuto modelu pak danou entitu pouze předáme. K tomu se používá metoda setEntity().
2.6.3. Načítání entit a používání cache Samotné načtení entity z databáze je poměrně přímočará záležitost. Model dostane skrz metodu fetch() nějakou variantu klíče, tu převede na nativní datový typ klíče Key a pomocí Persistence Manageru načte entitu z databáze. Načítání entit z databáze je neustále se opakující se operace, která je relativně drahá. Navíc jak bylo zmíněno, není možné využívat při volaní dotazů JOINů, takže pokud potřebujeme nějaký takový JOIN nasimulovat, v podstatě to znamená, že musíme pro každý záznam z původního dotazu sáhnout do databáze a vytáhnout požadovanou odpovídající entitu. Tyto dotazy jsou přirozeně relativně časté a tak v konečném 40
důsledku dochází k tomu, že některé entity (především ty nejvíce vytížené jako třeba uživatelé) jsou načítány znovu a znovu. A proto zde přichází na řadu cachování. Aby bylo ukládání objektů do cache co nejtransparentnější a nejpohodlnější, je implementováno přímo na té nejnižší vrstvě při načítání entit z databáze. Každý model má k dispozici instanci obecné cache TCache. Model v tuto chvíli nezajímá, jak je cache implementována zevnitř, důležité je pouze to, že poskytnutaá cache nabízí metody put() a get(). Prve jmenovanou se objekty do cache vkládají, tou druhou lze objekty získat. Pokud tedy přijde požadavek na načtení entity, model se napřed podívá do cache. Pokud tam už entita je, vrátí cachovanou verzi, pokud není, načte entitu z databáze a přidá ji do cache, aby byla příště dostupná. Entity se do cache ukládají přímo pomocí jejich klíče (v textové podobě), protože klíč je unikátní napříč celým úložištěm. Ukládání je tak velice pohodlné. Zevnitř je TCache je složena ze dvou vrstev, Memcache a RequestCache. Memcache je nadstavba stejnojmenné služby poskytované App Engine. Objekty jsou ukládány do paměti, odkud jsou dostupné napříč celou aplikací a především jsou dostupné i při vyřizování dalších požadavků. Pokud tedy v jednom požadavku provedu náročný dotaz simulující JOIN, v každém následujícím požadavku už jsou napojené entity získávány z této paměti a tedy celkový dotaz je podstatně levnější a rychlejší. I přesto, že tento způsob cachování by už sám o sobě ušetřil velké množství prostředků, Túúdl jde ještě dále. Při generování některých náročných stránek (např. generování výsledkové tabulky studijní skupiny) dochází k tomu, že některé entity jsou vyžadovány několikrát za sebou. A i přestože je využívání Memcache levné a rychlé, úplně zadarmo není. Proto se zde využívá ještě jedna vrstva, kterou je RequestCache. Tato cache je jednoduchá mapa objektů (tedy také poskytuje metody put() a get()), která vzniká a zaniká spolu s každým novým požadavkem. Když tedy načtu nějakou novou entitu, je umístěna jak do Memcache, tak do RequestCache. Při opětovném pokusu o načtení této entity v rámci jednoho požadavku je pak vrácena entita z RequestCache, což je samozřejmě ještě daleko rychlejší než použití Memcache. Vzhledem k tomu, že ona mapa je přímo součástí aplikačního kontextu, je přístup do této cache zadarmo a okamžitý. Přirozeně s ukončením požadavku tato paměť zanikne a je jí nutno vybudovat znovu. Díky této 41
implementaci můžu tedy kdekoliv v aplikaci vesele načítat entity, jak potřebuji, a kdy potřebuji, aniž bych se nějak výrazně musel obávat o výkon. Přirozeným problémem při cachování je invalidace zastaralých objektů v cache. To je ale v našem případě vyřešeno velice elegantně. Veškeré vytváření, načítání, ukládání i mazání entit probíhá na jednom místě, a tedy i práce s cache probíhá na jednom místě. Tedy není nic snazšího, než si pohlídat, aby při ukládání nějaké entity či jejím smazání byl odstraněn odpovídající záznam z cache. Při příštím přístupu bude načtena čerstvá aktuální verze. Důsledkem je, že cache je v Túúdlu využívána takovým způsobem, že nemá vliv na aktuálnost dat, které uživatel vidí, ale zároveň má ohromný vliv na výkon a spotřebu prostředků. Cachování se nicméně týká jiný problém. Načtená entita z databáze je vnitřně neustále napojená na JDO vrstvu. To je v praxi užitečné, protože to umožňuje donačítat další data podle potřeby. Pokud mám k dispozici entitu studijní skupiny a chci získat všechny její domácí úkoly, stačí proiterovat kolekci dané entity obsahující domácí úkoly a JDO se transparentně postará o načtení těchto objektů. Funguje zde tzv. lazy loading, což znamená, že ony domácí úkoly se načtou skutečně až v okamžiku, kdy si k nim vyžádáme přístup, tj. kdy se „dotkneme“ oné kolekce. To je ale problém při cachování takové entity. Při procesu ukládání je objekt serializován a teprve pak uložen do cache. Což znamená, že se cache „dotkne“ úplně všech dat obsažených v entitě a přirozeně tak spustí donačítání potomků z databáze. V konečném důsledku je tak do cache uložena ne samotná entita, ale rovnou celá entity group. A to je samozřejmě nežádoucí, protože v některých případech může entity group dosáhnout tak obludných rozměrů, že jen její kompletní načtení při ukládání do cache bude natolik drahé, že cachování zcela ztratí smysl. Z tohoto důvodu je nutné před uložením do cache objekt od JDO odpojit, tedy vytvořit jeho tzv. detached kopii, a teprve tu uložit do cache. Přirozený důsledek je ten, že když potom načteme entitu z cache, je pořád v tomto detached stavu, a pokud bychom zkusili u takové entity iterovat přes pole potomků, skončilo by to výjimkou. Proto je nutné entitu zpět k JDO připojit, což se udělá příkazem pro uložení entity. To má ale přirozeně dopady na spotřebu prostředků a mohlo by se zdát, že tedy v konečném důsledku nic neušetříme. Tady je důležité si uvědomit, že ne vždy je 42
nutné entitu zpět k JDO připojovat. Cachování entit obecně nejvíce prostředků ušetří v případech, kdy simulujeme nějaký JOIN. V takovém případě ale z napojované entity typicky potřebujeme jen nějaké statické údaje (jméno a příjemní, název apod.), kvůli kterým není třeba entity připojovat. V praxi to funguje tak, že model si udržuje přehled o tom, jestli je entita v attached či detached stavu. Pokud entitu čerstvě načte z databáze, označí ji za attached, pokud přijde z cache, označí ji za detached (i když nezbytně nutně nemusí být). Model lze pak normálně používat a pouze v okamžiku, kdy je z vnějšku požadována nějaká operace, která připojení k JDO vyžaduje, tak se před samotnou operací entita zpět k JDO připojí metodou attach(). Díky tomu, že případy, kdy cachování hraje největší roli, obvykle nevyžadují, aby použité entity byly připojené k JDO, jsme pořád schopni ušetřit nezanedbatelné množství prostředků. To se týká právě situací, kde by se normálně využil JOIN (např. vypisujeme seznam řešení domácích úkolů a potřebujeme k nim načíst jméno a příjmení studenta). Model má v sobě zabudovanou i podporu cachování dotazů do databáze. Pomocí metod getCachedQuery() a cacheQuery() lze jednoduše uložit do cache výsledek libovolného dotazu, tedy typicky nějakou kolekci. Dotaz, aby mohl být uložen, musí být pojmenován nějakým unikátním jménem. Model automaticky ke jménu doplní klíč aktuální entity, ke které se dotaz váže. To je důležité, protože pro každou entitu může dotaz vracet jiný výsledek (např. seznam domácích úkolů se liší skupina od skupiny). Tato funkce se nicméně v naší aplikaci téměř nevyužívá. Problém je v tom, že v případě dotazů je podstatně náročnější určit, zda je dotaz pořád aktuální. Jinými slovy, dotaz by bylo nutné odstranit z cache kdykoliv, kdy by se změnila nějaká entita, která byla součástí výsledků. Je jasné, že tato úloha je silně netriviální. Na tu druhou stranu, dotazy jako takové nejsou nijak extrémně drahé, a to především proto, že si vystačíme s malým počtem dotazů na požadavek. Jedinou výjimkou je práce se zdroji. Ta bude popsána dále v samostatné podkapitole. Zde si jen uvedeme to, že důvod, proč je možné v tomto případě cachování využít, je ten, že se zdroji se opět pracuje v jednom jediném centrálním místě, což umožňuje pohodlně udržovat cachovanou verzi aktuální. Navíc zdroje jako takové se mění minimálně (zdroje je 43
možné pouze přidat či odebrat, nikoliv upravit), což situaci samozřejmě značně usnadňuje.
2.6.4. Odstraňování z databáze Pokud chceme odebrat entitu z databáze, přirozeně požadujeme, aby spolu s ní zmizely i všechny relevantní objekty. Tedy když smažeme uživatele, aby byly odstraněny i všechny jeho domácí úkoly a dokumenty, které on nahrál. Tady opět narážíme na různé způsoby implementace vztahů mezi entitami. V případě owned vztahů, je mazání jednoduché. Když smažu kořenovou entitu, spolu s ní zmizí automaticky i všechny podřízené entity. V případě unowned vztahů je to horší, protože tam je potřeba ručně najít všechny napojené entity podle cizích klíčů a odebrat je ručně. Aby toto přehledně a spolehlivě fungovalo, implementuje každý model dvě metody. Jednou je delete() a tou druhou je prepareToDelete(). Prve jmenovaná metoda fyzicky odebere entitu z databáze. Tato metoda je implementovaná přímo ve společném předku modelů tuudl.models.Model a poděděné metody nemají obvykle zapotřebí ji měnit. To už ale neplatí o metodě prepareToDelete(). Tato metoda je automaticky zavolána metodou delete() před samotným odstraněním a má za úkol „připravit“ entitu na to, aby mohla být odebrána. To především znamená, že se postará o to, aby byly napřed odstraněny všechny navazující entity, které nebudou smazány automaticky. Nicméně i u entit, jejichž všichni potomci jsou vázání owned vztahy, je potřeba při mazání dávat pozor. Tito potomci sice budou odstraněni automaticky spolu s mateřskou entitou, nicméně to už nemusí platit o potomcích potomků, kteří naopak můžou být navázány už unowned vztahy. Proto když mažeme například studijní skupinu, tak tato skupina projde všechny své navázané úkoly a na každém z nich zavolá prepareToDelete(). Díky tomu si úkoly po sobě uklidí (smažou navazující zdroje a řešení studentů), a když jsou potom fyzicky odstraněny spolu se studijní skupinou, nic po nich nezůstane. V aplikaci tak v podstatě funguje princip „rozděl a panuj“. Pokud model dostane příkaz k odstranění, tak tento příkaz deleguje dál na všechny své potomky, kteří se o sebe postarají, a když je vše hotovo, tak odstraní sám sebe. Tedy lze jednoduše
44
smazat objekt někde vysoko v hierarchii a máme jistotu, že spolu s ním zmizí vše podřízené. Toto řešení nicméně na svou eleganci doplácí výkonem. V podstatě všechny objekty jsou mazány po jednom, což je přirozeně z hlediska času i spotřebovaných prostředků velice náročné. Výjimkou jsou entity groups, které jsou skutečně mazány najednou (z tohoto důvodů se liší obsah metody prepareToDelete() pro owned a unowned vztahy, což narušuje „jednotnost“ řešení, nicméně to je už malá daň). To v praxi tolik nevadí, protože ze všech možných operací bude zrovna mazání v rámci Túúdlu prováděno minimálně a tedy nejedná se o něco, co by mělo nějak zásadní vliv na dlouhodobý výkon a cenu provozování. Nicméně může dojít k tomu, že pokud se rozhodneme smazat velké množství dat najednou, například skupinu uživatelů, na které je navázána spousta dalších informací, tak se operace nestihne provést v časovém limitu. I z tohoto důvodu není mazání prováděno v transakcích. Tedy pokud z časových důvodů mazání selže, stačí znovu vyslat požadavek na smazání a aplikace plynule naváže tam, kde před tím skončila. Toto funguje díky tomu, že celé datové schéma má v podstatě strukturu stromu, přičemž když už strom mažeme, tak ho mažeme po větvích a po jednotlivých patrech. Tím, že před samotným
smazáním
entity
musí
úspěšně
proběhnout
čistící
metoda
prepareToDelete(), máme jistotu, že entita bude skutečně smazána až v okamžiku, kdy po sobě nic nezanechá. Tedy není třeba, aby mazání proběhlo atomicky, a může tak v případě nutnosti proběhnout „navícekrát“. Je na místě přiznat, že v určitých naprosto extrémních situacích skutečně může k jisté nekonzistenci dojít. Pokud kupříkladu někdo vytvoří kantorovi novou studijní skupinu v okamžiku, kdy je tento kantor mazán, a k tomu vytvoření dojde přesně v momentě mezi ukončením funkce prepareToDelete() a voláním delete(), tak tato skupina může skončit s nastaveným neplatným kantorem. Nicméně vzhledem k tomu, kolik uživatelů bude běžně aplikaci využívat (spravovat ji bude obvykle jenom jeden), je tato situace skutečně velice málo pravděpodobná. Navíc v takovém případě není nic snazšího, než nově vytvořenou skupinu smazat, čímž se databáze vrátí zpět do konzistentního stavu. Pokud bychom se tomuto riziku chtěli skutečně vyhnout, museli bychom zavést 45
transakce. Podpora transakcí je nicméně v případě App Engine Datastore omezená, v jedné transakci je možné maximálně pracovat s pěti různými entity groups. Tedy stačilo by, aby jeden učitel spravoval více než pět různých studijních skupin, a už by nebylo možné ho jen tak jednoduše smazat v jediné transakci. Bylo by nutné napřed odstranit všechny jeho studijní skupiny, následně vstoupit do transakce, ověřit, že mu žádná nezmizela, a teprve potom ho odstranit. Tímto by se mazání samozřejmě ještě zpomalilo a bylo by pravděpodobně nutné nechat mazání velkých bloků provádět mazání asynchronně na pozadí pomocí Tasků (viz dále). Z těchto důvodů aplikace v současné verzi mazání v transakcích nepodporuje.
2.6.5. Použití modelů v JSP šablonách V čem naopak popsaná implementace modelů exceluje, je použití v JSP šablonách. JSP šablony zastávají roli pohledů a jedná se o jednoduché HTML soubory, ve kterých lze využívat různé rozšiřující značky. Je možné v JSP souboru psát i přímo Java kód, nicméně to v naprosté většině případů není potřeba. JSP přístupují k datům skrz modely, přičemž v tuto chvíli je už jasné, že když mluvíme o modelu, tak se jedná o obálkou s nějakou konkrétní entitou z databáze. Velice užitečné je, že v JSP souboru lze přistupovat přímo k rozhraní modelu, konkrétně ke všem metodám začínajícím na get, tedy getterům. Je úplně jedno, co taková metoda vrací. Typicky se bude jednat o nějakou vlastnost entity, tedy pokud mám model uživatele, můžu snadno přistupovat k jeho jménu a příjmení. Nicméně JSP pohledu vůbec nevadí, pokud dostane k dispozici další model či dokonce kolekci modelů. Tou pak dokáže pohodlně proiterovat. Například pokud mám v pohledu přístup k nějakému modelu uživatele, můžu napsat ${user.teacher.studyGroups}, což se přeloží na user.getTeacher().getStudyGroups(). Získaný výsledek je pak kolekce modelů studijních skupin, se kterými pak můžu dále úplně stejným způsobem pracovat. Jak bylo zmíněno výše, modely jsou navzájem propojené a pomocí univerzálního API zcela pokrývají vztahy mezi jednotlivými objekty. Toho se pak v pohledech maximálně využívá a je zde opravdu jedno, jak je zevnitř vztah implementovaný. Zmíněný seznam studijních skupin můžeme proběhnout cyklem a u každé skupiny si načíst seznam jejich členů a u každého člena jméno a příjmení odpovídajícího uživatele. Zde by v případě relačních databází už bylo maximálně 46
vhodné použít na pozadí nějaký JOIN, to ale zde není možné, a proto zde nastupuje výše popsaný proces cachování. To se děje zcela automaticky na pozadí a při psaní pohledu, kdy se soustředíme primárně na to, jak bude náš výsledek vypadat, nás toto nezajímá. Toto opět neplatí zcela dokonale. JSP pohled přistupuje k modelům skrz gettery a získané hodnoty si nijak necachuje. To nevadí, protože v případě jednotlivých entit je cachování prováděno za nás přímo ve vrstvě modelu. Horší je to u getterů, za kterými stojí nějaký dotaz do databáze. Pokud bych ${user.teacher.studyGroups} použil třikrát za sebou v HTML kódu, tak by se dotaz na seznam studijních skupin také provedl třikrát za sebou. A to je občas potřeba, kupříkladu pokud chci ošetřit prázdný seznam. Pokud je seznam prázdný, vypíšu hlášku, pokud není, teprve iteruji. Z tohoto důvodu je potřeba si napřed získaný výsledek uložit do nějaké speciální JSP proměnné a teprve pak s ním pracovat. Toto bohužel dost silně nabourává celý princip, protože v této nejvyšší vrstvě musíme brát v potaz, jakým způsobem je udělaná vnitřní vrstva. Řešením by bylo nějakým způsobem alespoň v rámci jednoho požadavku cachovat i návratového hodnoty jednotlivých getterů modelu. Nicméně řešit toto v každé metodě modelu zvlášť by dost znepřehlednilo kód. Ideální způsob by byl vytvořit nějakou proxy vrstvu a postavit ji mezi modely a JSP pohled. Tato vrstva by automaticky zachytávala volání z pohledu a postarala by se o jejich cachování. Vzhledem k tomu, že pohled má pouze zobrazovat aktuální stav modelu/aplikace, cachování by nijak nevadilo a tento princip by naopak ještě podpořilo. Bohužel se nám nepodařilo přijít na způsob, kterým by se tato vrstva zakomponovala do Spring frameworku. Ještě jednu věc je potřeba u JSP pohledů uvést. Z pohledu je skutečně možné přistupovat přímo pouze k bezparametrickým getterům daného modelu. Tedy pro každý dotaz, který se třeba liší jen drobnou podmínkou, je třeba nadefinovat speciální getter s unikátním názvem. Nicméně u těch nejkomplikovanějších pohledů (např. tabulka s výsledky) je bohužel nutné zavolat nějakou metodu modelu s parametrem a v takovém případě je nutné vyskočit z HTML a použít na malém prostoru přímo Java kód. Toto by bylo asi opět lépe řešeno nějakým speciálním modelem. Tedy kupříkladu pro tabulku výsledků by se vytvořil speciální model, 47
který by nebyl navázán na žádnou entitu. Pouze by byl vytvořen a ručně naplněn daty v controlleru a následně předán pohledu, který by pouze data vypsal. V aktuální implementaci je pohled výsledkové tabulky dost komplikovaný a nepřehledný, protože se v něm mimo jiné i sčítají počty bodů za úkol a další záležitosti, které by měly být řešeny mimo. Na tu druhou stranu, i tento pohled pořád jen zobrazuje vnitřní stav aplikace, a tedy neporušuje tu základní definici pohledu.
2.6.6. Statické třídy modelů Pokud potřebujeme v aplikaci získat nějaký seznam objektů, většinou tak činíme přes nějakého společného předka. Tento přístup nám umožňuje stromovitá struktura entit v databázi. Naprostá většina dotazů jsou stylu „všechny studijní skupiny daného učitele, všechny studijní skupiny daného studenta, všechny úkoly ve skupině, všechna řešení daného studenta“ a podobně. Tedy máme k dispozici nějakého přirozeného předka a k objektům se tak dostaneme skrz něj. Existuje ale pár výjimek, kdy toto neplatí. Například seznam všech organizačních skupin. Organizační skupiny totiž nemají žádného společného předka, kterého bychom mohli využít. Proto tyto specifické případy jsou do modelů doplněny statické metody, které tyto požadavky pokrývají. Například tedy model organizační skupiny má statickou metodu getAll(). Nicméně je důležité si uvědomit, že tyto statické metody nemají automaticky přístup k ModelFactory. Ta je součástí pouze konkrétních instancí modelů. Z tohoto důvodů je nutné každé takové statické metodě ručně ModelFactory předat parametrem. Existence těchto statických metod je další důvod, proč by bylo možná vhodné zavést do aplikace vrstvu služeb. Tyto služby by získaly přístup k ModelFactory už při inicializaci a nebylo by tedy nutné ji předávat ručně. Navíc jak bylo zmíněno výše, do služeb by se mohlo přesunout z controllerů bezpečnostní ověřování při práci s modely a také obecná validace dat vstupujících do modelů.
2.7. Distribuce služeb napříč aplikací Pro práci s modely je klíčová továrnička ModelFactory. Tato továrnička v sobě zastřešuje služby pro přístup do databáze (Persistence Manager) i pro přístup do
48
cache, přičemž tyto služby nejenže automaticky poskytuje všem novým modelům, ale zároveň je i ochotná je poskytnout navenek. Což je velice užitečné, protože Persistence Manager je potřeba všude, kde je vyžadován přístup do databáze. Obecně sice platí, že přístup do databáze je prováděn pouze a jenom skrze modely, které automaticky dostávají instanci ModelFactory a tedy i Persistence Manager při vzniku, nicméně v případě statických metod modelů je nutné těmto metodám ručně připojení k databázi dodat. Obecně je tedy klíčové, aby kterákoliv část aplikace měla jednoduchý přístup k instanci ModelFactory. Tím automaticky získá nejen možnost pracovat s modely, ale i přímé spojení s databází. Aby se tento předpoklad zajistil, využívá se další vlastnosti Spring MVC, a tou je autowiring. Továrnička ModelFactory má bezparametrický konstruktor, což znamená, že může být inicializována bez jakýkoliv dalších informací (přístup do cache a do databáze si třída zajistí sama pomocí k tomu určených statických tříd). Díky tomu je možné nechat inicializaci na Spring MVC, čímž se stane ModelFactory součástí kontextu frameworku a je jí možné automaticky napojovat na libovolné objekty, které jsou rovněž inicializovány přímo frameworkem. A to se týká všech controllerů, interceptorů (viz dále) ale i Property Editorů. Stačí jako vlastnost třídy nastavit nějakou proměnnou s datovým typem ModelFactory a označit ji anotací @Autowired. Při inicializaci těchto objektů si Spring této anotace všimne a automaticky do této proměnné dosadí instanci ModelFactory. Pokud ještě žádná instance v kontextu neexistuje, tak ji vytvoří, což mu nečiní problém z důvodů popsaných výše. Vzhledem k tomu, že s tímto datovým typem se nachází v kontextu pouze jeden takový objekt, nemá Spring problém poznat, co má do proměnné dosadit (v opačném případě by šlo jednotlivé objekty v kontextu pojmenovat, podobně jako v případě @ModelAttribute). Tento přístup následuje princip Dependeny Injection. Ten ve zkratce říká, že každé třídě je vše potřebné poskytnuto z vnějšku a nikde si nic „nekouzlí“ z prostoru (nepoužívají se globální proměnné ani statické třídy). Výjimku tvoří přirozeně samotná ModelFactory, která stojí na vrcholu této pyramidy. Ta musí instanci cache či Persistence Manageru odněkud získat a využívá k tomu přímo (jen lehce obalené) API poskytnuté App Enginem. Tedy to ani jinak udělat nelze. Nicméně všechny 49
ostatní objekty v aplikaci už využívají služeb poskytnutých naší továrničkou a je tedy zřejmé, odkud která služba pochází a také jaké má který objekt pravomoci. Tedy se nestane, že by nějaký objekt jen tak mimochodem zapisoval do databáze, aniž by to bylo z vnějšku vidět. Na místě je ještě pochybnost, zda by Persistence Manager a přístup do cache měly skutečně být součástí ModelFactory (což je už dle názvu především továrna na modely), zda by nebylo vhodnější je mít jako samostatné služby, které by opět byly automaticky napojeny do továrničky (a kamkoliv jinam, kde by byly potřeba). Tady je potřeba si znovu připomenout, že modelová vrstva zcela odstiňuje veškerý zbytek aplikace od přímého přístupu do databáze či od způsobu cachování. I statickým třídám modelů je předávána instance ModelFactory a nikoliv Persistence Manageru. Tedy pokud bychom definovali přístup do databáze a cache jako samostatné služby, stejně by byly využívány pouze a jenom v ModelFactory. Proto byly především kvůli zjednodušení a přehlednosti zahrnuty přímo do samotné továrničky. Ještě poznámka k Persistence Manageru. ModelFactory získala tímto způsobem implementace status služby, která běží jenom jednou v rámci celé aplikace (jedna instance pro všechny i paralelně vyřizované požadavky). Nicméně instance Persistence Manageru se vytváří nová pro každý nový požadavek (zohledňováno je i zpracováváno více požadavků najednou ve více vláknech). Tuto funkcionalitu kompletně zaštiťuje statická třída Datastore, přičemž naše továrnička pouze využívá funkce této třídy a získaný Persistence Manager distribuuje dál.
2.8. Bezpečnostní architektura 2.8.1. Princip ověřování uživatelů a přidělování práv Byť Spring poskytuje komplexní řešení pro autentizaci uživatelů a přidělování práv, je bezpečnostní architektura implementována ručně, a to především proto, že je velice jednoduchá. V systému jsou tři role, správce (admin), kantor (teacher) a student, přičemž uživateli může být přidělená libovolná podmnožina z nich. Každé roli odpovídá jedna část uživatelského rozhraní, která je každá zvlášť pokryta svým controllerem. Controller naslouchá na základní URL adrese, která je shodná s názvem role. Pokud se tedy pokusí uživatel pokusí přistoupit na nějakou adresu, tak 50
se napřed ověří, zda pro onu adresu existuje role. Např. pokud přistupuje na adresu začínající /admin/, tak se ověří, zda existuje odpovídající role s tímto názvem. Tato role skutečně existuje, a tedy je nezbytné ověřit, že přihlášený uživatel onu roli admin má. Pokud by ale uživatel vstoupil na adresu /entrance/ (což je úvodní strana), tak žádná role s tímto názvem neexistuje. Tato adresa je tedy nechráněná a může na ni vstoupit kdokoliv. Autentizace uživatelů a autorizace přístupu je ověřena pomocí tzv. interceptorů. Interceptor je speciální třída s pevně daným rozhraním, která umožňuje spouštět akce na začátku či na konci zpracování každého požadavku. Interceptorů může být více a je možné přesně určit jejich pořadí v XML konfiguračním souboru. Napřed proběhnou interceptory, které zjistí, zda je přihlášený nějaký uživatel. Uživatel může být přihlášen více způsoby (buď lokálně či pomocí Google účtu, viz dále) a každému způsobu odpovídá jeden interceptor. Pokud se povede uživatele ověřit a načíst z databáze, je model uživatele napojen do objektu požadavku a je tak přístupný i dále v aplikaci. Další autentizační interceptory již automaticky rozpoznají, že byl uživatel už přihlášen, a nepokouší se ho přihlásit jiným způsobem. Doplněním dalších interceptorů je možné snadno rozšířit možnosti ověřování. Po této fázi se dostane na řadu AccessControlInterceptor, který vezme přihlášeného uživatele (jestli nějaký je) a výše popsaným postupem ověří, zda má uživatel přístup k dané adrese.
2.8.2. Přihlašování pomocí Google účtů Přihlašování pomocí Google účtů je kompletně řešeno pomocí poskytovaného App Engine API. Pomocí tohoto API lze vygenerovat přihlašovací URL adresu, na kterou když uživatel vstoupí, tak je vyzván, aby se přihlásil ke svému Google účtu a umožnil naší aplikaci pracovat se základními údaji svého účtu (jméno, příjmení, email). Po tomto ověření je uživatel automaticky přesměrován do naší aplikace, kde už připravený interceptor opět pomocí poskytnutého API přihlášeného uživatele zachytí.
51
2.8.3. Přihlašování pomocí lokálních účtů Lokální přihlašování probíhá klasicky pomocí uživatelského jména (e-mail) a hesla. Heslo je nutné napřed uživateli vytvořit, přičemž vytvořené heslo se uloží v zahashované osolené podobě do databáze. K přihlášení takového uživatele je nezbytná existence formuláře, kde je možné přihlašovací heslo zadat. Po odeslání formuláře je jednoduše porovnáno zadané heslo s údajem v databázi, pokud údaje souhlasí, je uložena do SESSION informace o uživateli. SESSION je standardní metoda, jak v případě HTTP serverů udržovat nějaké stavové informace napříč požadavky jednoho klienta. Jinými slovy, skrze SESSION bude mít k identitě přihlášeného uživatele přístup každý další požadavek. Toho se využívá v interceptoru, který přihlášeného uživatele zachytí a dá k dispozici zbytku aplikace. Tento způsob přihlášení vyžaduje, aby aplikace uživateli poskytla nejen přihlašovací formulář, ale i speciální URL na odhlášení a pak také možnost změnit si své heslo. Toto všechno je implementováno v MainControlleru. Při přihlašování tímto způsobem dochází k přenosu hesla v plaintextu. Naštěstí App Engine automaticky běží v šifrovaném režimu (HTTPS), a tedy heslo není možné jen tak zachytit.
2.9. Import uživatelů Túúdl nabízí hromadné importování uživatelů do systému, a to buď ze souboru CSV či pomocí GData API z nějaké Google Apps domény. Oba způsoby můžou být poměrně náročné na prostředky i čas, a proto jsou řešeny na pozadí pomocí Tasků, jak bylo popsáno v úvodní části práce. V obou případech je import zahájen vyplněním jednoduchého formuláře, který obsahuje všechny nezbytné údaje k aktivaci importu. Odeslaný formulář je zachycen, a pokud jsou všechny poskytnuté informace v pořádku, je import zařazen do fronty. Konkrétní detaily implementace jednotlivých importů jsou podrobně zdokumentovány přímo ve zdrojovém kódu.
2.10. Práce se zdroji Zdroje (fyzické soubory a odkazy) mohou být napojeny na několik různých objektů v aplikaci (úkol, řešení domácího úkolu a studijní skupina). Napojení funguje pro 52
všechny případy naprosto totožně. Z tohoto důvodu existuje kromě základního předka
pro
modely
tuudl.models.Model
ještě
upravený
předek
tuudl.models.ModelWithResources. Od tohoto modelu jsou pak odvozeny všechny další modely, na které se nějaké zdroje napojují. Tento společný předek poskytuje jednotné API pro přidávání, odebírání a získávání zdrojů. Díky tomu, že je se zdroji manipulováno z tohoto jediného místa a navíc je s nimi manipulováno minimálně (zdroj je jednou nahrán a může být tak maximálně smazán), je zde možné pohodlně využívat cachování jednotlivých dotazů na napojené zdroje. Pokud dojde k přidání či odstranění zdroje, cache se jednoduše vymaže.
2.10.1.
Obecný popis pluginu pro nahrávání zdrojů do aplikace
V aplikaci je možno využívat pro přidávání zdrojů univerzální JavaScriptový widget. Pokud chceme někde na stránce umožnit uživateli nahrát nějaký zdroj, widget zde aktivuji a dám mu k dispozici callback URL, které je zodpovědné za přidání zdroje. Widget se pak sám postará o uživatele a dá mu možnost nahrát soubor či vytvořit nový odkaz. V okamžiku, kdy je nový zdroj vytvořen, tak vezme klíč zdroje a zašle ho na poskytnuté URL, čímž se zdroj propojí s daným objektem. V tomto okamžiku se již neřeší, o jaký typ zdroje se jedná, je to zcela transparentní.
2.10.2.
Nahrávání fyzických souborů do úložiště Blobstore
Ještě stojí za zmínku, jakým způsobem funguje v App Engine práce s fyzickými soubory. Aplikace zde totiž nemá přímý přístup k žádnému klasickému souborovému systému. Místo toho je zde poskytnuta služba zvaná Blobstore, která sice umožňuje ukládat fyzické soubory, nicméně nenabízí žádnou strukturu adresářů. Soubory, zvané bloby, jsou zde podobně jako entity identifikovány klíčem. Entita zdroje nějakého souboru tedy v sobě musí držet klíč na odpovídající blob. I nahrávání souborů je poměrně komplikované. App Engine vyžaduje, aby formulář, skrz který se má soubor nahrát, byl odeslán na speciální adresu (tato adresa je generována jednorázově a má platnost na jedno jediné nahrání). Na této adrese je odesílaný soubor automaticky zachycen, uložen do Blobstore a teprve pak se dostane ke slovu naše aplikace, které je předán klíč nahraného blobu. Tento proces je
53
přirozeně nutné absolvovat i při importu uživatelů z CSV souboru. CSV soubor je nutné uložit jako blob a teprve pak s ním je možné pracovat.
2.11. Widget pro výběr uživatelů Podobně, jako widget na přidávání zdrojů, je řešen i widget na přidávání uživatelů. Tedy v místě, kde potřebujeme vybrat množinu uživatelů (třeba kvůli vložení do studijní skupiny), je zavolán externí JavaScriptový widget, kterému je pouze předána callback URL, na kterou má být seznam uživatelů zaslán. Tento widget umožňuje vyhledávat uživatele pomocí tzv. autocomplete tedy automatického doplňování. Uživatel začne psát název toho, co chce najít (např. uživatelské jméno či název studijní skupiny), a aplikace za něj zbytek asynchronním požadavkem doplní. Problém je s tím, že App Engine Datastore nepodporuje žádný LIKE operátor, běžně dostupný v relačních databázích, který se k tomu obvykle využívá. Zde se proto LIKE nahrazuje trikem, kdy se využívá porovnávání řetězců. Prohledávané objekty se jednoduše slovníkově seřadí a vyberou se ty řádky, které mají název větší než vyhledávaný řetězec a menší než vyhledávaný řetězec doplněný o poslední znak v abecedě (respektive skutečně poslední znak v řadě všech možných znaků, v Javě dostupný přes Character.MAX_VALUE). Nevýhody tohoto řešení jsou zřejmé. Jednak lze vyhledávat pouze podle začátku řetězce (tedy najdeme záznamy začínající daným řetězcem, nikoliv obsahující daný řetězec, jak to umí klasické LIKE), a navíc je toto vyhledávání citlivé na velikost písmen. Lze využívat jen určitých heuristik, typicky že e-mail uživatele je vždy malými písmeny, zatímco jméno/název začíná velkým písmenem a další jsou malá.
2.12. Ošetřování chybových stavů a zachytávání výjimek Poslední část bude věnována tomu, jakým způsobem se aplikace chová v případě nějakého selhání či neplatného požadavku. Značná část chyb je vyřešena přímo v controllerech. Typicky se jedná o chyby, kdy uživatel odešle nevalidní formulář. Taková výjimka je zachycena přímo na místě a uživateli je odeslána zpráva v json o neúspěchu, přičemž se případně provedou nějaké uklízecí akce. 54
Ostatní chyby jsou už řešeny jinak. Jedná se většinou o závažnější problémy, typicky jde o situace, kdy se aplikace pokusí přistoupit k neexistující entitě v databázi, metoda controlleru je zadána s neplatnými parametry (očekáváno je číslo, přijde řetězec), uživatel nemá oprávnění k provedení dané akce, či úplně extrémní případ, kdy je vyčerpán nějaký limit prostředků App Engine (např. již nelze dále přistupovat do databáze). Každá z těchto výjimek má k sobě nadefinovaný JSP pohled, který zobrazí potřebnou zprávu uživateli. Toto definování se nastavuje v XML konfiguraci Spring frameworku, tedy za zachytávání výjimek a směrování na pohledy je zodpovědný Spring. Jednotlivé pohledy jsou vyřešeny tak, že automaticky rozpoznají, jaký formát chybové zprávy klient vyžaduje. Pokud je akce volána skrz AJAX, je obecně vhodné zpátky poslat zprávu zabalenou do json, pokud se jedná o klasický požadavek, zobrazí se kompletní naformátovaná webová stránka. Formulářové výjimky jsou zachytávány přímo lokálně, což mimo jiné vylepšuje přehlednost. Je na první pohled jasné, co se děje, když formulář selže. Nicméně vzhledem k tomu, že ve valné většině případů znamená zachycení výjimky stejně jen zformátování zprávy do json a odeslání klientovi, mohly by i tyto výjimky být řešeny takto obecně. Pouze ve specifických případech, kdy je nezbytné po sobě i něco uklidit, by se výjimky řešily lokálně. Je možné, že aplikace selže ještě mnohem „tvrdším“ způsobem. Tedy že výjimku nezachytí ani Spring. V takovém případě se zobrazí statická pevně předdefinovaná HTML stránka.
55
3. Zkušenosti s reálným provozem aplikace První životaschopná verze aplikace vznikla poměrně brzy, a proto bylo možné aplikaci přibližně 8 měsíců testovat v prostředí skutečné střední školy. Během této doby aktivně využívalo aplikace téměř 400 studentů, kteří pracovali ve 43 studijních skupinách, bylo jim zadáno 144 domácích úkolů, vypracováno bylo 1774 řešení a nahráno bylo 1534 souborů. Z čísel je vidět, že se jedná o nepříliš velkou střední školu. Aplikace byla primárně využívána při výuce informatiky a výpočetní techniky. Otázka tedy zní, zda takovýto typ školy je schopen aplikaci provozovat bez nákladů, tedy zda se vejde do neplacených limitů pro využívání prostředků App Engine. Napovědět by mohla následující tabulka, která obsahuje základní údaje o využití aplikace z posledních 3 měsíců před odevzdáním práce (leden až březen): Tabulka 1 – průměrné využití prostředků z ledna až března 2013, udávané v procentech Prostředek
Frontend
průměr pondělí úterý
středa
čtvrtek pátek
sobota
neděle
max
34,6
38,6
37,6
39,3
27,5
29,0
29,5
41,0
72,2
19,6
18,5
20,0
20,0
20,0
18,5
20,0
20,0
20,0
34,9
40,0
46,2
53,3
26,2
23,1
21,5
35,4
120,0
Instance Hours
Datastore Writes
Datastore Reads
V tabulce 1 jsou uvedeny tři nejdůležitější prostředky a jejich průměrná denní procentuální spotřeba z dostupné volné kapacity. Kupříkladu Túúdl průměrně denně spotřebuje 34,6% dostupných Instance Hours, což znamená, že spotřebuje přibližně 9 a půl hodiny z 28 možných. Průměrná hodnota jako taková nemá přílišný význam. Volné prostředky jsou přidělovány po dnech, tedy klidně by se mohlo stát, že zatímco ve všední dny by spotřeba stěží překročila 10%, o víkendech by se dostala daleko do
56
placené zóny. Proto jsou zde uvedeny i průměrné hodnoty pro jednotlivé dny v týdnu. Z těchto hodnot je hezky vidět cyklus školního týdne. Zatímco o víkendu je aktivita utlumena, tak již v neděli pozvolna stoupá (studenti začínají řešit úkoly na pondělí) a uprostřed týdne dosahuje svého vrcholu. Nicméně ani ten vrchol není nijak extrémní, protože sahá jen mírně přes 50% u zápisů do databáze. Tady je na místě zmínit, že hodnoty o přístupech do databáze jsou velice hrubé. Zápisy a čtení se počítají po 10 000 přístupech, přičemž limit je 50 000. Jinými slovy, při takto nízké zátěži jsme schopni z poskytnutých údajů určit spotřebu jen s přesností na „dvacítky“ procent. Na otázku, zda je možné aplikaci provozovat zadarmo na podobné škole, lze tedy odpovědět celkem s klidným svědomím, že ano, což jasně ukazuje dlouhodobá průměrná spotřeba. Samozřejmě, v určitých momentech může dojít ke krátkodobým překročením limitů (což se stalo i nám, jak je vidět z tabulky, kde v případě Datastore Reads překročilo v jednom dni využití 100%) a aplikace se vypne. Těmto výpadkům lze přirozeně předejít aktivováním placené verze hostingu. V porovnání s běžným hostingem je tento přístup pořád (alespoň principiálně) výhodný, protože díky způsobu účtování zaplatí zákazník skutečně jen prostředky spotřebované nad limit. Pokud těmto překročením bude docházet výjimečně, budou náklady na provoz pořád minimální. Ze statistik by se mohlo zdát, že aplikace má v současné době nejužší hrdlo ve spotřebě Instance Hours. Nicméně to není zcela pravda. Údaj nám říká, jak dlouho instance běžela, nikoliv, jak moc byla vytížena. Tedy teoreticky, i kdyby aplikaci využíval dvojnásobek uživatelů, kteří by nicméně chodili pořád ve stejné časy, tak by se spotřeba Instance Hours ani nehnula. Problém by nastal v okamžiku, kdy by jedna instance přestala stíhat vyřizovat požadavky a bylo by nutné nastartovat druhou. To by přirozeně způsobilo dvojnásobnou spotřebu tohoto prostředku. Větším problémem jsou přístupy do databáze. I u nich je ale otázka, jak se bude jejich spotřeba vyvíjet spolu s rostoucími nároky. Kupříkladu pokud by se zachoval počet studentů, pouze vzrostla míra využívání aplikace z jejich strany (například výuka ve více předmětech), mohlo by se zde pozitivně projevit cachování, protože jestli něco bude drženo v cache především, tak to budou uživatelé. 57
Na místě je zmínit ještě jedno riziko, které se dosud v praxi neprojevilo, ale skýtá potencionální nebezpečí. Pokud by nějaký student odhalil vnitřní charakter aplikace, mohl by sám vygenerovat dostatečný provoz (pouhým obnovováním některé ze složitějších stránek), že by došlo k vyčerpání prostředků během několika hodin. Nicméně tato aktivita by pak byla později dohledána v logu administrátorské konzole App Engine včetně uživatelského jména (bohužel pouze v případě Google účtů), a případný student by mohl být za své chování exemplárně potrestán. Na druhou stranu, tímto způsobem lze těžko bojovat, pokud se vzbouří proti Túúdlu celá škola. Tedy aplikaci lze skutečně provozovat levně či úplně zadarmo, ale jen v případě, že budou spolupracovat i uživatelé aplikace, což je předpoklad u studentů střední školy poměrně těžko splnitelný. I proto lze celkem oprávněně pochybovat o tom, zda zvolená platforma byla skutečně tou vhodnou pro daný úkol. Kdyby aplikaci využívaly miliony uživatelů (k čemuž jsou cloudové hostingy zamýšleny především), tak by podobné riziko bylo přirozeně zanedbatelné. K nějakému významnějšímu navýšení účtu za provoz u takové aplikace by byl potřeba již regulérní DDoS útok.
58
Závěr Hlavním cílem práce bylo vytvořit skutečně reálně použitelnou aplikaci, která by usnadnila práci kantorům na středních školách a nejen tam. Vzhledem k tomu, že aplikace byla skutečně nasazena v reálném prostředí, lze tento cíl prohlásit za splněný. Odezva od uživatelů je veskrze pozitivní. Snaha byla nabídnout spíše méně funkcí než více, ale podat je takovým způsobem, aby je byli schopní využívat i počítačově méně zdatní uživatelé. Výsledná aplikace nabízí jednoduché uživatelské rozhraní s příjemným moderním vzhledem. Uživatel se tak necítí přehlcen funkcemi a prostředí se pohodlně používá. Navíc díky použití moderních technologií a především asynchronní komunikaci se serverem je aplikace velice svižná a chvílemi připomíná spíše plnohodnotnou desktopovou aplikaci. Nicméně to nejzajímavější se děje pod kapotou, skryto před zraky uživatele. Aplikace běží na cloudovém hostingu Google App Engine, což přináší s sebou specifika neobvyklá pro běžné hostingy. S těmito překážkami bylo nutné se vyrovnat. Jednalo se především o práci s poskytovaným schema-less úložištěm, které postrádalo spoustu vlastností, se kterými spolehlivě člověk počítá u běžných relačních databází. Zde nebylo možné se spolehnout ani na to, že databázový dotaz vrátí vždy aktuální výsledky. Navíc bylo nutné přistoupit na zcela odlišný způsob účtování za služby, kdy skutečně každý přístup do databáze a každá ne zcela optimalizovaná funkce se projeví ve výsledné ceně za provoz aplikace. Z počátku tedy nebylo jasné, zda se vůbec podaří podobnou aplikaci napsat takovým způsobem, aby byla schopná fungovat za „rozumné“ peníze, ideálně zcela zdarma v rámci základních volně poskytnutých prostředků. Jak ale ukázal téměř roční ostrý provoz, výsledná aplikace tohoto schopná je, byť je nutné počítat s určitými riziky. Cesta k tomuto výsledku byla nicméně hodně trnitá a vyžadovala spoustu pokusů, než se podařilo aplikaci navrhnout zevnitř takovým způsobem, aby toto dokázala. A jak je v práci podrobně popsáno, ne všechna rozhodnutí byla zcela šťastná. Konkrétně máme na mysli třeba volbu databázové vrstvy JDO, která nám měla usnadnit práci s databází a zároveň učinit aplikaci přenositelnou, přičemž v praxi se ukázaly naše předpoklady jako mylné.
59
Snahou bylo navrhnout aplikaci takovým způsobem, aby nebyla zcela odkázaná na služby poskytované Googlem. To se bohužel úplně nepodařilo a aplikaci skutečně nelze vzít a jen tak spustit na běžném Java webhostingu. App Engine je příliš specifický a odlišný, aby toto bylo možné. V aplikaci se nicméně podařilo mezi jednotlivými komponentami vytvořit dostatečnou vrstvu abstrakce, že by většina kódu byla beze změny použitelná i u alternativního poskytovatele. Bohužel ta nejdůležitější modelová vrstva, která tvoří nezanedbatelnou část aplikace, ta by musela být přepsána od základu. Navíc některé funkce, jako třeba přihlašování implementované pomocí Google účtů za využití poskytovaného API, jsou z App Engine zcela nepřenositelné. Práce na několika místech otevřeně přiznává, že některé věci by šlo v aplikaci řešit lépe. Něco z toho jsou nepříliš šťastná rozhodnutí, která už později nešla vrátit, nicméně často se jedná o věci, které by ještě bylo možné upravit. Jak to tak ale bývá, vždy existuje něco, co lze udělat lépe. V našem případě bylo vždy hlavní snahou dotáhnout vše do konce. Už v první fázi bylo nezbytné dát co nejdříve dohromady funkční prototyp, který by mohl být nasazen a testován v reálném provozu. To poskytlo nezbytnou odezvu, na základě které byla aplikace upravována. V tomto procesu byla aplikace několikrát zevnitř přepsána a vylepšena, výsledek se tedy značně liší od první nasazené verze. Nicméně bylo nutné tento proces v určitou chvíli ukončit a aplikaci dotáhnout do skutečně použitelného a funkčního stavu i přes vědomí, že ne vše je dokonalé a perfektní. Práce na této aplikaci byla zajímavá a skutečně obohacující, protože umožnila zcela jiný pohled na vývoj webových aplikací. Navíc se podařilo vytvořit něco, co je skutečně užitečné a prakticky použitelné, což bylo přirozeně při práci na tomto projektu motivující.
60
Seznam použité literatury 1. What Is Google App Engine? - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2013 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/whatisgoogleappengine 2. Quotas - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2013 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/quotas 3. Using the Datastore - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2012 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/java/gettingstarted/usingdatastore 4. Memcache Java API Overview - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2013 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/java/memcache/overview?hl=en 5. Using JDO 2.3 with App Engine - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2012 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/java/datastore/jdo/overview?hl=en 6. twig-persist - Object Datastore for Google App Engine - Google Project Hosting. TWIG-PERSIST. Google Project Hosting [online]. 2010 [cit. 2013-05-11]. Dostupné z: http://code.google.com/p/twig-persist/ 7. objectify-appengine - The simplest convenient interface to the Google App Engine datastore - Google Project Hosting. OBJECTIFY-APPENGINE. Google Project Hosting [online]. 2013 [cit. 2013-05-11]. Dostupné z: http://code.google.com/p/objectify-appengine/ 8. Web MVC framework. SPRINGSOURCE. SpringSource.org [online]. 2012 [cit. 2013-05-11]. Dostupné z: http://static.springsource.org/spring/docs/3.1.x/springframework-reference/html/mvc.html 9. The Task Queue Java API - Google App Engine - Google Developers. GOOGLE. Google Developers [online]. 2012 [cit. 2013-05-11]. Dostupné z: https://developers.google.com/appengine/docs/java/taskqueue/ 10. BERNARD, Borek. Úvod do architektury MVC | Zdroják. DEVEL.CZ. Zdroják [online]. 2009 [cit. 2013-05-14]. Dostupné z: http://www.zdrojak.cz/clanky/uvod-doarchitektury-mvc/
61
Seznam použitých zkratek •
HTML – HyperText Markup Language – značkovací jazyk pro tvorbu webových dokumentů.
•
CSS – Cascading Style Sheet – kaskádové styly; jazyk pro popis způsobu zobrazování HTML stránek.
•
AJAX – Asynchronous JavaScript and XML – obecné označení pro technologie a postupy při vývoji interaktivních asynchronních aplikací.
•
PHP – PHP: Hypertext Preprocessor – skriptovací programovací jazyk určený primárně pro vývoj webových stránek.
•
SQL – Structured Query Language – standardizovaný dotazovací jazyk, používaný pro práci v relačních databázích.
•
NoSQL – označení pro databáze, které nesledují tradiční principy organizace dat v relačních databázích, což má za následek chybějící podporu pro dotazování pomocí SQL. Někdy bývá toto označení vykládáno jako „Not only SQL“, protože některé NoSQL databáze (včetně Google App Engine Datastore) do jisté míry SQL podporují.
•
DoS – Denial of Service – technika útoku na internetové služby, kdy dochází k přehlcení dané služby požadavky z vnějšku a jejímu následnému pádu.
•
JDO – Java Data Objects – nástroj pro perzistenci objektů v Javě.
•
JSP – JavaServer Pages – Java technologie podobná PHP, která umožňuje dynamicky používat jazyk Java přímo v HTML a XML textových souborech.
•
POJO – Plain Old Java Object – primitivní Java objekt, který na sebe nenavazuje žádnou další funkčnost ani nevyužívá žádných pokročilých technik.
•
UML – Unified Modeling Language – grafický jazyk pro vizualizaci, 62
specifikaci, navrhování a dokumentaci aplikací. Zde se používá pro znázornění vztahů mezi objekty v aplikaci. •
MVC – Model-View-Controller – architektura definující vnitřní strukturu aplikace.
•
CSV – Comma-separated values – obvyklé označení pro jednoduchý textový soubor, který obsahuje tabulková data, ve kterém jsou hodnoty odděleny čárkami (či jiným textovým oddělovačem) a konci řádků.
•
JSON – JavaScript Object Notation – způsob zápisu strukturovaných dat, nezávislý na platformě, používá se typicky pro přenos dat v oblasti Internetu.
63
Přílohy 1. CD obsahující elektronickou verzi této práce, zdrojové kódy, instalační balíček, Google App Engine SDK 1.8.0 pro jazyk Java, vygenerovanou programátorskou dokumentaci Javadoc, uživatelskou dokumentaci včetně pokynů k instalaci a specifikaci softwarového díla.
64