Hlavní navigace

Cocoon v příkladech: Flowscript, Continuations a MVC+

21. 1. 2004
Doba čtení: 10 minut

Sdílet

V tomto dílu se podíváme, jak Cocoon řeší (nejen) tok stránek složitějších webových aplikací pomocí flowscriptu a co jsou to objekty typu Continuation. Také se dozvíme, jak je řešen návrhový vzor MVC+ v Cocoonu a proč je tam to plus na konci. To vše na příkladu jednoduchého webového obchodu.

V předchozích dílech jsme se hodně zabývali základy Cocoonu, zvládli jsme mapu i většinu základních typů komponent. Prozatím se Cocoon zdál výborný zvláště pro webové publikace dat, které ve formátu XML buď již máme, nebo je výhodné je v XML udržovat. V tomto dílu se podíváme, jak Cocoon řeší (nejen) tok stránek složitějších webových aplikací pomocí flowscriptu a co jsou to objekty typu Continuation. Také se dozvíme, jak je řešen návrhový vzor MVC+ v Cocoonu a proč je tam to plus na konci. To vše na příkladu jednoduchého webového obchodu.

MVC = Model-View-Controller

Při návrhu webových aplikací se obvykle používá návrhový vzor MVC (Model-View-Controller). Model představuje entity reálného života a jejich vztahy. V případě webového obchodu budeme muset modelovat zboží, nákupní košík, sklad, objednávku, fakturu atd. včetně jejich vztahů a operací mezi nimi. Pokud si například zákazník něco koupí, musíme o to snížit skladové zásoby. Tyto modely nejsou komponentami Cocoonu, jde o běžné třídy v Javě.

Pohled (view) definuje, jak model prezentujeme. V našem případě to jsou HTML stránky, které aplikační server zasílá klientovi – webovému prohlížeči zákazníka e-obchodu. Kontrolér volá model a vybírá pohled.

V tradičních interaktivních webových aplikacích je tok stránek obvykle modelován pomocí stavového automatu. Logika toku stránek je skryta v tom, že aplikace přijme požadavek klienta a po jeho vyhodnocení určí následující stav automatu. Cocoon však přišel s převratnou myšlenkou: Proč bychom tok stránek nemohli prostě napsat stejně jako kterýkoliv jiný program?

Flowscript

Kontrolér lze v Cocoonu napsat pomocí tzv. flowscriptu. Je to program v JavaScriptu s poměrně jednoduchým API (resp. objektovým modelem) pro přístup k parametrům HTTP požadavku, kontextu apod. Kromě toho lze do skriptu importovat (a v něm použít) javovskou třídu nebo objekt Java Beans, DOM, případně JDOM. Tyto objekty budou nejčastěji reprezentovat model. Pro zaslání stránky, která očekává vstup uživatele (obvykle webový formulář), slouží funkce sendPageAndWait. Program se zde zastaví a čeká na odpověď klienta. Po jejím obdržení skript pokračuje dále. Poslední stránka (obvykle výsledek, potvrzení apod.) nebo stránka, kde není potřeba zastavit flowscript, se obvykle zasílá funkcí sendPage.

Continuations

Tento přístup skrývá v sobě jedno úskalí, vyjádřené také ve jménu výše uvedené funkce sendPageAndWait. Serverová aplikace musí na odpověď klienta čekat. Aby nebylo nutné blokovat vlákno zpracovávající požadavek, Cocoon ukládá stav (lokální proměnné a zásobník volání funkcí) do objektů typu Continuation. Kromě výše uvedené výhody nezablokovaného vlákna tento přístup umožňuje i další kouzla:

  • k vytvoření webových aplikací typu wizard, přihlášení s možností registrace nového uživatele, e-obchod apod. nebudeme potřebovat cookies ani žádný identifikátor uživatelské relace (session-id),
  • nejsou problémy s použitím tlačítka Zpět/Back v prohlížeči ani s otevřením nového okna (či přesněji řečeno s jeho „klonováním“ např. v Internet Exploreru pomocí Ctrl+N) a
  • Continuations jsou prostě objekty jako kterékoliv jiné, a tak je lze také serializovat a například uložit do databáze. Tím můžeme poměrně jednoduše získat perzistentní uživatelské relace. Není to pěkné mít možnost webovou aplikaci kdykoliv přerušit a pokračovat třeba další den?

Objekty Continuations jsou tak významnou inovací tradičního přístupu k vytváření webových aplikací, že pokud se v souvislosti s Cocoonem setkáte s označením návrhového vzoru MVC+, tak to plus reprezentuje právě existenci Continuations.

Pohled

Pro vytvoření pohledu musíme mít nějakou možnost získávat data z modelu přes flowscript. Snad neexistuje ani jiná možnost než použít nějaký systém šablon. Jak je v Cocoonu zvykem, má na to prostředků několik:

  • XSP (XML Server Pages) s logicsheetem JPath,
  • systém šablon JXTemplate, který lze použít jako generátor nebo transformátor a ve kterém lze použít dva různé makrojazyky (JXPath a Jexl) včetně přístupu například k objektům Java Beans, XML/DOM a JDOM a
  • Velocity, což je jednoduchý, ale poměrně mocný systém maker a šablon rodiny projektů Jakarta Apache, který je v Cocoonu k dispozici jako generátor.

Pokud má pohled zobrazovat formulář, je k dispozici ještě:

  • starší (ale stabilní :-) generátor JXForms pro práci s podmnožinou značek definovaných W3C XForms a validaci formulářů a
  • novější (ale zatím v beta verzi) přístup nazvaný Cocoon Forms(a.k.a. Woody), který řeší celou problematiku formulářů obecněji a důkladněji. Budeme se jím zabývat v některém z příštích dílů.

Mapa

Při použití flowscriptu slouží mapa ke slepení všech výše uvedených částí dohromady a hlavně k definicím, jak se mají vytvářet jednotlivé pohledy. Kromě faktu, že budete muset použít jeden z generátorů (resp. transformátorů) uvedených výše, můžete v rouře použít pro získání pohledu všechny prostředky a přístupy, se kterými jsme se setkali v minulých dílech. To dává návrhu i realizaci pohledů značnou volnost a nabízí i bohaté možnosti užití hotových komponent (např. pro internacionalizaci aplikace).

Příklad: e-obchod

Pro lepší pochopení bude nejlépe ukázat příklad – fragment (velmi) zjednodušeného webového obchodu, který bude prodávat (spíše „vydávat“ – cena tam není :-) dva druhy zboží: kokosy a lososy. Pro jednoduchost budeme mít jen dva pohledy. První z nich bude pro výběr zboží a vložení vybraného zboží do nákupního košíku podle následujícího obrázku:

Pohled - výběr zboží

Na stránce je informace, kolik položek (tj. druhů zboží) je již v košíku, formulář se vstupními poli pro výběr určitého počtu kusů každé položky, s informací o počtů kusů položek, které jsou ještě skladem (nemůžeme vybrat víc), a s tlačítkem na potvrzení výběru, a také odkaz na stránku nákupního košíku. Pohled budeme vytvářet přímo HTML formulářem (takže žádná transformace XSLT nebude v rouře potřeba) pomocí šablon Velocity. Šablona je uložena jako soubor vyber.vm a vypadá takto:

#parse ("nahore.vm")

<h1>Vyber zbozi</h1>
<h2></h2>
<h3>V kosiku je uz polozek:  $polozek</h3>

<h4>Chci objednat:</h4>

<form method="post" action="${continuation.id}.kont">
  <input type="text" name="kokos"/>
    kokosu (skladem $kokosy)<br/>
  <input type="text" name="losos"/>
    lososu (skladem $lososi)<br/>
  <input type="submit" value="Do kosiku"/>

</form>

<a href="kosik?kont=${continuation.id}">Kosik</a>

#parse ("dole.vm")

VTL (Velocity Template Language) obsahuje direktivy začínající znakem #. Direktivy #parse vkládají v našem případě soubory s korektním zahájením a ukončením (X)HTML souboru. Kromě této direktivy patrně využijete už jen #if, #foreach a #end. Odkazy na objekty jsou uvozeny znakem $, notace přístupu k jejich atributům a metodám je stejná jako v Javě – oddělují se tečkou. Pokud by nebylo jasné, kde specifikace odkazu končí, je ohraničena složenými závorkami. Všimněte si použití odkazu ${continuation.id}, který znamená identifikátor objektu Continuation. Je použit jednak jako cíl, který zpracuje HTTP požadavek při odeslání formuláře, jednak jako parametr pro URL odkazu na košík (košík toto ID musí znát, abychom se sem dokázali vrátit). Další použité odkazy jsou $kokosy a $lososi, což jsou dvě čísla, která určují, kolik kokosů a lososů je ještě k dispozici ve skladu.

E-obchod: nákupní košík

Řešení nákupního košíku je na následujícím obrázku:

Pohled - nákupní košík

Jeho funkce je velmi jednoduchá. Vypíše vybrané položky spolu s počtem kusů. Na konci je odkaz, pomocí kterého se dostaneme zpět na stránku výběru zboží. Šablona (soubor kosik.vm) vypadá takto:

#parse ("nahore.vm")


<h1>Nakupni kosik</h1>
<h3>V kosiku je uz polozek: ${polozky.size()}</h3>

<p>
#foreach ($polozka in ${polozky.keySet()})
 $polozka: ${polozky.get($polozka)} ks<br/>
#end
</p>

<a href="${kont}.kont">Zpet na vyber</a>

#parse ("dole.vm")

Všimněte si volání metod size(), keySet() a get(...) objektu s názvem polozky. Jsou to volání metod objektu implementujícího javovský interface Map (objekt je typu HashMap, jak uvidíme dále). Uprostřed kódu šablony můžete vidět iteraci po položkách kontejneru. Odkaz na konci ukazuje na URL typu 7c3521...713772.kont. Jak později uvidíme, mapa definuje takovéto URL jako obnovení stránky, jejíž data jsou v objektu Continuation s daným identifikátorem. Ukazatel instrukcí flowscriptu se zároveň nastaví na místo, kde se daná stránka zobrazuje.

E-obchod: flowscript

Na dalším výpisu je ilustrován flowscript naší aplikace. Je to soubor eshop.js. Všimněte si na začátku importu javovského balíčku java.lang, hned pod ním je vytváření instancí třídy HashMap (standardní javovská třída) pro nákupní košík a sklad. V tomto kontejneru je vždy uložen jako klíč řetězec s názvem zboží a jako hodnota počet kusů. Funkce main nejprve naplní sklad. Tato činnost vlastně do flowscriptu nepatří, měla by být součástí modelu. V reálné aplikaci by zde byly nejspíše objekty přistupující k datům v databázi. Pak se volá funkce vyberZbozi.

importPackage(java.lang);

var kosik = java.util.HashMap();
var sklad = java.util.HashMap();

function main()
{
  sklad.put("kokos", 15);
  sklad.put("losos", 9);
  kosik.clear();

  vyberZbozi();
}

function vyberZbozi()
{
  while (true) {

    cocoon.sendPageAndWait("vyber.vm", {
     "polozek" : kosik.size(),
     "kokosy": sklad.get("kokos"),
     "lososi" : sklad.get("losos")
    } );

    pridejDoKosiku();
  }
}

function zobrazKosik()
{
  kont = cocoon.request.get("kont");
  cocoon.sendPage("kosik.vm", {
   "kont" : kont,
   "polozky" : kosik,
  } );
}

...

Funkce vyberZbozi v cyklu zobrazuje stránku definovanou šablonou vyber.vm. Do šablony jsou předávány parametry: počet položek v košíku a počty kokosů a lososů, které jsou na skladě. Pokud uživatel zadává do formuláře počty kusů, při každém kliknutí na tlačítko Do kosiku (Submit) se zavolá funkce pridejDoKosiku, která odečte kusy ze skladu a přičte je k obsahu košíku. Tato funkce je v tomto jednoduchém příkladu (poněkud těžkopádně) realizována také jako součást flowscriptu, ale v reálné aplikaci by patřila do logiky modelu a byla by nejspíše napsaná v Javě.

Pokud uživatel na stránce s výběrem zboží klikne na odkaz „Košík“, mapa zajistí, že se zavolá funkce zobrazKosik z flowscriptu, která zobrazí podrobnosti o nákupním košíku. Ještě předtím ale byl uložen stav aplikace ze stránky s výběrem zboží do objektu Continuation. Identifikace tohoto objektu je předána (pomocí parametru kont) do šablony košíku, aby bylo možné zkonstruovat odkaz pro návrat na stránku s výběrem zboží v tom stavu, v jakém jsme ji opustili ( <a href="${kont}.kont">...). Všimněte si, že flowscript po výpisu stránky s obsahem košíku nemusíme zastavovat, takže nám stačí pro zaslání stránky funkce sendPage.

E-obchod: mapa

Na mapě aplikace toho příliš mnoho zajímavého nenajdeme. Na začátku je deklarace generátoru Velocity, po něm následuje určení, který soubor s flowscriptem aplikaci patří. Za pozornost stojí snad jen prvky <map:call function=... a <map:call continuation=... v rouře. První z nich volá zadanou funkci z flowscriptu, druhý obnovuje aplikaci do stavu podle zadaného objektu Continuation. Zpracování vlastních šablon Velocity je standardní.

CS24_early

<?xml version="1.0"?>

<map:sitemap
xmlns:map="http://apache.org/cocoon/sitemap/1.0">

 <map:components>
  <map:generators default="file">
   <map:generator name="velocity" src=
"org.apache.cocoon.generation.VelocityGenerator"/>
  </map:generators>
 </map:components>

 <map:flow language="javascript">

  <map:script src="eshop.js"/>
 </map:flow>

 <map:pipelines>
  <map:pipeline>

   <map:match pattern="*.vm">

    <map:generate type="velocity" src="{1}.vm"/>
    <map:serialize/>
   </map:match>

   <map:match pattern="*.kont">
    <map:call continuation="{1}"/>
   </map:match>

   <map:match pattern="">
    <map:call function="main"/>
   </map:match>

   <map:match pattern="kosik">
    <map:call function="zobrazKosik"/>

   </map:match>

  </map:pipeline>
 </map:pipelines>
</map:sitemap>

Všechny soubory můžete najít v archivu eshop.zip. Pokud archiv rozbalíte například do adresáře cocoon-2.1.3/build/webapp/eshop, můžete aplikaci spustit pomocí URL: http://localhost:8888/eshop/(nezapomeňe na ukončovací lomítko).

Závěr

Flowscript a objekty Continuations jsou jedním z nejzajímavějších rysů frameworku Cocoon 2.1 (ne-li nejzajímavějším vůbec). Nesmírně usnadní návrh a vývoj aplikací se složitějším tokem stránek a svým těsným propojením s Javou posunují Cocoon z pozice frameworku pro publikování na webu do pozice frameworku pro efektivní vývoj webových aplikací. Flowscript a Continuations ve spojení s Cocoon Forms (na které se podíváme v některém z příštích dílů), principem roury, oddělením obsahu a prezentace, možností použití již hotových komponent a celkem snadného vývoje komponent vlastních vytváří z Cocoonu nástroj síly opravdu nevídané. V tomto dílu nebylo možné probrat celou problematiku řízení toku stránek. Zájemce o podrobnosti odkazuji na velmi pěknou prezentaci z listopadové konference ApacheCon 2003, ale i na vlastní dokumentaci řízení toku Cocoonu, která patří k těm lépe zpracovaným.

Byl pro vás článek přínosný?