JavaServer Pages programozóknak Írta: Mika Péter 2000. szeptember. Minden jog fenntartva.
Tartalomjegyzék 1. BEVEZETÉS.............................................................................................................................. 1 2. A JSP ÉS ELŐNYEI ................................................................................................................. 2 3. A JSP ELEMEI.......................................................................................................................... 5 3.1 DIREKTÍVÁK ........................................................................................................................... 5 3.1.1 A page direktíva............................................................................................................... 5 3.1.2 Az include direktíva ......................................................................................................... 6 3.1.3. A taglib direktíva ............................................................................................................ 7 3.2 SCRIPT-ELEMEK ...................................................................................................................... 7 3.2.1 Deklarációk ..................................................................................................................... 7 3.2.2 Script-részletek ................................................................................................................ 8 3.2.3 Kifejezések ....................................................................................................................... 9 3.2.4 Implicit objektumok ......................................................................................................... 9 3.3 AKCIÓK ................................................................................................................................ 11 3.3.1 A standard akciók .......................................................................................................... 12 3.3.1.1 A <jsp:useBean> akció...........................................................................................................................12 3.3.1.2 A <jsp:setProperty> akció ......................................................................................................................14 3.3.1.3 A <jsp:getProperty> akció......................................................................................................................15 3.3.1.4 A <jsp:include> akció ............................................................................................................................15 3.3.1.5 A <jsp:forward> akció ...........................................................................................................................16 3.3.1.6 A <jsp:plugin> akció..............................................................................................................................16 3.3.1.7 A <jsp:param> akció ..............................................................................................................................17
3.3.2 Saját elemkönyvtárak készítése...................................................................................... 17 3.3.2.1 Elemkönyvtárak használata ....................................................................................................................18 3.3.2.2 Egyszerű elemek készítése .....................................................................................................................20 3.3.2.3 A törzsüket is feldolgozó elemek készítése ............................................................................................24 3.3.2.4 Scriptváltozók bevezetése ......................................................................................................................26 3.3.2.5 Egymásbaágyazott elemek készítése ......................................................................................................29
1. Bevezetés A JavaServer Pages (a továbbiakban az egyszerűség kedvéért JSP) a Java szervletekhez hasonlóan egy klienstől érkező kérés alapján valamilyen szöveges, leggyakrabban HTML vagy XML formátumú dokumentum dinamikus, szerveroldali előállítására szolgáló technológia, a Java 2 Enterprise Edition (J2EE) része. A Sun hivatalos JSP-vel foglalkozó oldalai a http://java.sun.com/products/jsp címen érhetők el, innen letölthető a teljes specifikáció, valamint a szintaxist összefoglaló syntax card. (Ez utóbbi különösen jó szolgálatot tehet.) A JSP a szabad szoftverek szellemiségéhez közel álló módon, a Java Community Process keretein belül fejlődik, széles iparági támogatással övezve. Az írás pillanatában a legújabb elfogadott specifikáció az 1.1-es, ami a Servlet API 2.2-es verziójára épül. Ez felülről
1
kompatibilis a korábbi 1.0-ás változattal, ami viszont teljesen eltér az előzetes, 0.92 számmal kiadott változattól. (Óvakodjunk az erre épülő termékektől!) Sajnos a specifikáció meglehetősen nehezen olvasható, nyomdahibákban gazdag, helyenként az implementáció részleteibe veszik, helyenként pedig elhallgatja a nyilvánvaló megvalósítás hátterét. Az alábbiakban ezért a JSP előnyeinek rövid összefoglalása után bemutatom a nyelvi elemeket, és egy-két gyakorlati példán keresztül illusztrálom a használatukat. A szervletekkel való kapcsolat jóval szorosabb, mint azt a JSP-ről szóló prospektusok sejtetni engedik, ezért úgy vélem, nem nélkülözhető a szervletek bizonyos szintű ismerete, így a továbbiakban építeni is fogok ezekre az ismeretekre. A szervletekről magyarul a http://javasite.bme.hu oldalon, illetve a Nyékyné et al.: Java 2 útikalauz programozóknak (ELTE TTK, 1999) című könyvben olvashatunk. A JSP kipróbálásához szükség van egy ún. JSP container-t tartalmazó web- vagy alkalmazáskiszolgálóra. Ehhez a referencia implementáció gyanánt is szolgáló Tomcat-et ajánlom, melyet az Apache project keretében fejlesztenek, és ingyenesen letölthető a http://jakarta.apache.org címről. A Tomcat egy 100%-ig Javában írt webkiszolgáló, mely szervletek és JSP oldalak futtatására is képes, és mind önállóan, mind más webkiszolgálóknak (Apache, IIS, Netscape) bedolgozva is működtethető. Telepítése (egyszerű bemásolása) után a /bin könyvtárban levő startup script segítségével indítható, és az ugyanitt található shutdown nevű scripttel állítható le. A 3.1 és újabb változatokban a dokumentumhierarchia gyökere a /webapps/Root/ könyvtár, mely (alapbeállításként) a http://localhost:8080/ címen érhető el. A webszerver teljes forrása mellett a Tomcathez megkapjuk a szervlet és a JSP API-k (javax.servlet.*) JavaDoc dokumentációját, ezt sem kell tehát külön letölteni. Jelen írás szerzője biztos benne, hogy munkája nem mentes sem a hibáktól, sem a hiányosságoktól, így köszönettel fogad minden korrekciót és kiegészítést, továbbá szívesen segít minden, a szervletekkel, a JSP-vel és a Tomcattel összefüggő kérdés megválaszolásában. E-mail címe
[email protected]
2. A JSP és előnyei A JSP oldalak jó megközelítéssel visszájukra fordított szervleteknek tekinthetők: míg a szervletek esetén Java kódban elrejtve szerepelnek a szöveget a kimenetre író utasítások, addig a JSP-nél éppen ellenkezőleg, a rögzített szöveg közé rejtve szerepelnek az oldal tartalmát módosító utasítások. A JSP oldalak tehát inline kódot tartalmazó HTML/XML oldalaknak tekinthetők, ami azért is fontos, mert így az oldal ASP-re vagy JSP-re felkészített webszerkesztő programokkal is manipulálható. A JSP előnyei elsősorban az olyan oldalaknál mutatkoznak, amelyek relatíve sok fix szöveget, és kevés kódot tartalmaznak. A Sun mérnökeinek egyik bevallott célja a JSP megalkotásával az volt, hogy elrejtsék a szervletek írásának nehézségeit a programozásban kevéssé jártasak elől azáltal, hogy szétválasztják az oldal programozási részét a tartalomtól és a designtól. Ez utóbbit ugyanis manapság általában külön tartalomkészítők és webdesignerek készítik, számukra a JSP egyszerű, XML-szerű elemeken keresztül biztosít hozzáférést a szerveroldali komponensekhez (JavaBeans, EJB), elrejtve előlük a tényleges programkódot. A prezentációs- és az alkalmazáslogika ezen szétválasztása a JSP legnagyobb előnye a szervletekhez képest: nem csak munkamegosztást tesz lehetővé, de izolációt is jelent, ugyanis így nem keveredik az oldal tartalma a programkóddal. A kettő így külön-külön fejleszthető a véletlen felülírás veszélye nélkül. Ahhoz hogy megérthessük a JSP és a szervletek közti kapcsolatot, nézzük meg mi is történik egy egyszerű JSP oldalra vonatkozó kérés kiszolgálásakor. Készítsünk egy szokványos szöveges filet a minden programozó szívét megdobogtató „Hello World!” szöveggel, majd
2
mentsük el a webkiszolgáló gyökérkönyvtárába hello.jsp néven. Ha most a böngészőnkkel megpróbáljuk lekérni ezt a dokumentumot, akkor a háttérben következők történnek: 1. A kliens elküldi a kérést a szervernek 2. A web- vagy alkalmazásszerver a .jsp kiterjesztés alapján felismeri, hogy egy JSP filera vonatkozik a kérés, így továbbítja azt a JSP containernek, ami lehet a webkiszolgáló része, vagy külön plug-in. 3. Mivel ez volt az adott dokumentumra vonatkozó első kérés, a JSP fordító (ami a Tomcat esetén Jasper névre hallgat) a .jsp forrásból sorról-sorra haladva előállítja a neki megfelelő java szervlet forrását. 4. A java kódot a javac fordítóval lefordítja egy .class fileba. (Ezért szükséges, hogy a J2SE-beli tools.jar része legyen a CLASSPATH-nak.) 5. Inicializálja a szervletet, majd a szervlet a kérést megkapva előállítja az oldal végleges szövegét. 6. Ami aztán eljut a klienshez. A trükk tehát egész egyszerűen annyi, hogy a JSP egyszerűbb, szövegvezérelt formátumából egy specializált fordító szervlet kódot készít, és valójában ez szolgálja ki a kérést. Ez a plusz fordítás természetesen többletterhet, és lassabb válaszidőt jelent, de csak az első kérés kiszolgálásakor jelentkezik, a további kérések ugyanis már egyenesen a szervlethez továbbítódnak. (Van mód az oldal előfordítására is, így már az első kérés kiszolgálása sem lesz lassabb: ehhez a kérés paraméterei között szerepelnie kell egy jsp_precompile nevűnek üres vagy true értékkel. Az ilyen kérés nem továbbítódik az oldalnak.) Milyen szervlet készült a mi fileunkból? Tomcat esetén a forrás megtalálható a /work könyvtárban, _0002fhello_0002ejsphello_jsp_0.java (vagy hasonló) néven: // import public class _0002fhello_0002ejsphello_jsp_0 extends HttpJspBase {
static { } public _0002fhello_0002ejsphello_jsp_0( ) { } private static boolean _jspx_inited = false; public final void _jspx_init() throws JasperException { } public void _jspService(HttpServletRequest request, HttpServletResponse throws IOException, ServletException {
response)
JspFactory _jspxFactory = null; PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; String _value = null; try { if (_jspx_inited == false) { _jspx_init(); _jspx_inited = true; } _jspxFactory = JspFactory.getDefaultFactory(); response.setContentType("text/html;charset=8859_1");
3
pageContext = _jspxFactory.getPageContext(this, request, response, "", true, 8192, true); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); // HTML // begin [file="E:\\tomcat\\webapps\\ROOT\\hello.jsp";from=(0,0);to=(1,0)] out.write("Hello World!\r\n"); // end } catch (Exception ex) { if (out.getBufferSize() != 0) out.clearBuffer(); pageContext.handlePageException(ex); } finally { out.flush(); _jspxFactory.releasePageContext(pageContext); } } }
Látható, hogy az oldal tényleges szövegét a szervletek service metódusához hasonló _jspService metódusban lévő sor, az out.write("Hello World!\r\n");
állítja elő. Technikailag ez az osztály az absztrakt HttpJspBase osztály leszármazottja, aminek pedig a szintén absztrakt HttpServlet a szülője, innen hát a szervletes örökség. Mint látni fogjuk, a JSP oldal szövegébe helyezett Java kódrészletek változatlanul, egy az egyben bekerülnek a belőle készített szervlet-forrás szövegébe, így a JSP is megőrzi a szervletek egyik nagy előnyét: tetszőleges Java API (pl. a JDBC, az RMI, a JavaMail stb) vagy saját Java osztály meghívható. A szervletes kapcsolatnak köszönhető az is, hogy a JSP-k és a szervletek kiválóan kiegészíthetik egymást: egy JSP oldal könnyen átadhat egy kérést egy szervletnek vagy fordítva (forward), vagy pedig beillesztheti a végeredménybe egy másik szervlet vagy JSP eredményét (include) így egészen bonyolult rendszerek építhetők ki. (A JSP ezt annyiban is segíti, hogy a kimenet (JspWriter osztály) pufferelt, így hiba vagy abortálás esetén az oldalrészlet nem kerül elküldésre.) A jellegzetes architektúrákat illetően lásd a Sun Blueprint Design Guidelines for the J2EE negyedik fejezetét (The Web Tier). A JSP apró, de hasznos többletszolgáltatása a szervletekhez képest, hogy amennyiben kivétel lép fel a feldolgozás alatt lévő oldalon, akkor a kérést a kivétel (java.lang.Throwable típusú) objektumával egyetemben átadja a megadott hibakezelő oldalnak, aminek ezért szintén JSP-nek kell lennie. Nem csalódik egyébként, aki arra tippel, hogy ezt a fenti példa catch blokkjában szereplő pageContext.handlePageException(ex);
sor végzi. A JavaServer Pages értékelésekor nem szabad megfeledkeznünk a Microsoft hasonló igényt kielégítő technológiájáról, az Active Server Pagesről (ASP). Mivel a kettő összevetését már többen is megtették, itt most csak két olyan weblapot ajánlanék, amelyeken részletesen olvashatunk a témáról: az egyik az ASPToday, a másik a JSPInsider. Ez utóbbi oldalon előfizethetünk a JSPBuzz hírlevélre is, mely kétheti rendszerességgel ismerteti az egyre táguló JSP-univerzum eseményeit.
4
3. A JSP elemei A gyakorlatban minden sokkal egyszerűbb, mint ahogy az a fenti leírásból tűnhet, a JSP oldal szerzője ugyanis ideális esetben soha nem találkozik az oldalból készülő szervlettel, a JSP elemeivel pedig elvileg minden megoldható, ami szervletekkel. Miket tartalmazhat tehát egy JSP oldal? Az oldalban –néhány kivételtől eltekintve- tetszőleges számban és tetszőleges helyen előfordulhatnak JSP nyelvi elemek, melyek háromfélék lehetnek: direktívák, script-elemek, és akcióelemek. Minden olyan literál, amit a JSP fordító nem ismer fel, a whitespaceket is megőrizve egy az egyben bekerül az előállított oldal szövegébe. A JSP saját elemeinek egy rész XML-szerű tag, egy része viszont a könnyebb gépelhetőség kedvéért az XML konvencióktól eltérő, egyszerűbb jelölést használ. Éppen ezért a JSP oldalak nem XML dokumentumok, noha létezik egy szabványos átírás, mellyel az XML specifikációnak is megfelelő formára hozhatók. (Lásd a specifikáció hetedik fejezetét.) A JSP nyelvi elemeire is igaz ugyanakkor, hogy a kis- és a nagybetűk különbözőnek számítanak. Fontos különbség azonban, hogy a JSP-nek saját kommentje van, ennek formája a következő: <%-- Ez egy JSP komment --%>
Ez a fajta komment használatos magának a JSP oldalnak a kommentálására, mert az így megjelölt szövegrészek nem kerülnek bele az előállított oldalba, ellentétben például az XML kommentekkel, amik rendes szövegként továbbadódnak a kliensnek. (Aki aztán vagy felhasználja a kommenteket, vagy sem.) A kommentálás egy harmadik lehetséges módja, hogy az oldalban használt scriptnyelv kommentjeit használjuk, ezek értelemszerűen szintén nem adódnak tovább.
3.1 Direktívák A direktívák a JSP containernek szóló utasítások, közös jellemzőjük, hogy <%@ direktiva-nev attr1=”ertek1” attr2=”ertek2” … %>
alakúak, és nem módosítják az előállított oldal szövegét.
3.1.1 A page direktíva Mint azt a neve is sugallja, a page direktívával az egész oldalra vonatkozó jellemzőket állíthatjuk be. A page direktíva a fordítási egységben (ami az adott oldal és az include direktívával beillesztett oldalak egésze) akárhányszor és akárhol előfordulhat, de az import kivételével minden attribútumnak csak egyszer adhatunk értéket. Az attribútumok és lehetséges értékeik a következők: •
language: A script-részletekben, kifejezésekben, és deklarációkban használt programozási nyelv neve. Az 1.1-es specifikáció a JSP containerek számára csak a java érték elfogadását teszi kötelezővé, csak erre az esetre vonatkozóan tartalmaz előírásokat, és egyben ez az alapértelmezés is. (Noha a specifikáció terminológiáját követve scriptekről beszélek, itt nem JavaScriptről, hanem a "valódi" Javáról van szó!) Érdemes megemlíteni, hogy létezik egy, az IBM Bean Scripting Frameworkjén (BSF) alapuló kezdeményezés, amelynek révén akkor is használhatunk más nyelven (Netscape Rhino (Javascript), VBScript, Perl, Tcl, Python,
5
•
•
• •
•
•
• •
• •
NetRexx, Rexx) írt kódrészleteket, ha a JSP container ezt közvetlenül nem támogatja. További információkért lásd a Jakarta Taglibs alprojektet. extends: Megadja, hogy a JSP oldalból készült osztály melyik osztálynak legyen a leszármazotta. Lehetőleg ne használjuk, mivel a Java egyszeres öröklődése miatt ezzel erősen korlátozzuk a JSP fordító lehetőséget. (Lásd a specifikáció 3.2.4 fejezetét a tekintetben, hogy mit kell tudnia saját szülőosztálynak.) import: Ezzel az attribútummal az oldalból készülő szervlet import listáját egészíthetjük ki, aminek a specifikáció szerint eleve tartalmaznia kell a java.lang.*, javax.servlet.*, javax.servlet.jsp.* és a javax.servlet.http.* csomagokat. (Ennél többet is tartalmazhat, lásd a Hello, World! példát.) Az attribútum értéke (language="java" esetén) Java típusok, illetve csomagok vesszővel elválasztott listája. session: true vagy false értéke adja meg, hogy az oldalban akarunk-e sessiont használni. Az alapbeállítás a true, ilyenkor használhatjuk a session implicit változót. Az implicit változókról lásd 3.2.4. buffer: Ezen attribútum értéke szabja meg az implicit out változó pufferelésének módját: none esetén nincs pufferelés, minden közvetlenül kiíródik a ServletResponse PrintWriter objektumára, ha pedig egy pufferméretet adunk meg (xxxkb), akkor minden egy pufferbe íródik, mielőtt elküldésre kerülne. Az alapértelmezés 8kb. autoFlush: Amennyiben pufferelést kértünk eldönti, hogy mi történjen a puffer megtelése esetén. Az alapértelmezett true beállítás esetén a puffer automatikusan ürül, ha megtelik, false esetén ilyenkor exception váltódik ki. (Ez utóbbi például akkor lehet hasznos, ha a kliens maga is alkalmazás, és nem egy böngésző.) isThreadSafe: Ezzel adhatjuk meg, hogy oldalunk fel van-e készítve arra, hogy egyszerre több kérést szolgáljon ki párhuzamosan, vagy sem. Az alapértelmezett true esetén a JSP container egyszerre több kérést is átadhat az oldalnak, míg false választása esetén a kérések sorbaállítódnak, és egyenként kapja meg őket az oldal. (A gyakorlatban ennek megvalósítása a javax.servlet.SingleThreadModel interface implementálásával zajlik.) info: Ennek segítségével adhatjuk meg az oldal rövid leírását, a Servlet.getServletInfo megfelelője. errorPage: A bevezetőben említett kivételkezelési mechanizmus része: annak a JSP oldalnak az URL-je, amelyikhez az oldalban esetleg fellépő java.lang.Throwable osztályú kivételt továbbítani szeretnénk. Ennek gyakorlati megvalósítása is elég triviális: a kivételkezelő oldal a kivételt a ServletRequest objektum javax.servlet.jsp.jspException nevű paramétereként kapja meg. isErrorPage: A kivételkezelő oldal esetén állítsuk az értékét true-ra, és akkor a megkapott kivétel elérhetővé válik az implicit exception változón keresztül. contentType: Megadja az oldal MIME típusát és karakterkódolását, "TYPE" vagy "TYPE;charset=CHARSET" formátumban. Az alapértelmezés "text/html;charset=ISO-8859-1". Mivel a dokumentumtípust egy direktíva határozza meg, ezért dinamikusan nem állítható. Ha ilyesmit szeretnénk, akkor célszerű egy olyan JSP oldalt készíteni, amelyik a kérés paraméterei alapján a megfelelő típusú dokumentumot előállító szervletre/JSP oldalra irányítja a kérést.
3.1.2 Az include direktíva Az include direktíva adott file (nem feltétlenül JSP) fordítás előtti beillesztésére szolgál. A beillesztés statikus, a megadott file byteról bytera másolódik be. (Dinamikus, minden egyes kérés kiszolgálásakor kiértékelődő beillesztés a jsp:include akció-elemmel lehetséges. Lásd
6
3.3.1.4) A specifikáció szerint a JSP implementációk nem kötelesek automatikusan újrafordítani az oldalt az include file megváltozásakor. A direktíva egyetlen attribútuma a file, értéke egy filera mutató relatív URL.
3.1.3. A taglib direktíva A JSP által felismert akció-elem készlet kibővíthető saját tagekkel, melyek könyvtárakba rendezhetők (custom tag library). A taglib direktíva segítségével adhatjuk meg a JSP fordító számára, hogy hol keresse a saját elemeinket definiáló tag library descriptor-t. A direktívának még az első saját elem előfordulása előtt szerepelnie kell. Két attribútuma közül a uri adja meg az elemkönyvtárat azonosító szimbolikus vagy tényleges URI-t, a prefix pedig azt a prefixet, amit egy kettősponttal elválasztva a saját elemeink neve elé kell majd tennünk, ahhoz, hogy hivatkozni tudjunk rájuk, pl. <prefix:nev attrnev="ertek" … />
vagy <prefix:nev attrnev="ertek" … > törzs prefix:nev>
A prefixek célja a névterek szétválasztása, amire azért van szükség, mert egy oldalba taglib direktívákkal tetszőleges számú elemkönyvtárat "importálhatunk". A saját elemkönyvtárak készítéséről bővebben lásd a 3.3.2 fejezetet.
3.2 Script-elemek A JSP 1.1 specifikáció háromféle script-elemet definiál: használhatunk deklarációkat, script-részleteket, és kifejezéseket. Mivel mindhárom az oldal scriptnyelvén alapul, ezért ezek pontos jelentése függ a használt nyelvtől is. Továbbiakban csak a Java esetéről lesz szó. A scriptelemek szintaxisa megtévesztésig hasonló: <%! deklaráció %> <% script-részlet %> <%= kifejezés %>
3.2.1 Deklarációk A deklaráció elnevezés kissé megtévesztő, mert változókat deklarálni script-részletek belsejében is lehet, metódusokat viszont nem. Ennek magyarázata az, hogy míg a script részleteket a JSP fordító a készülő szervlet _jspService metódusába, az aktuális helyre teszi, addig a deklarációk a szervlet metódusain kívülre másolódnak, tehát osztály szintű változókat, illetve metódusokat hoznak létre. Deklaráció révén van lehetőségünk a szervlet init, illetve destroy metódusának átdefiniálására. Ehhez deklarálnunk kell a jspInit, illetve jspDestroy metódusokat. (A szervletek init és destroy metódusával ellentétben egyik sem engedi meg exception dobását.) Más szervlet metódust nem definiálhatunk át, továbbá foglaltak a jsp, _jsp,
7
jspx és _jspx kezdetű metódusnevek, így természetesen _jspService nevű metódust sem
deklarálhatunk.
3.2.2 Script-részletek Az előzőekben elmondottak alapján a script-részletek (scriptlet-ek) egy az egyben bekerülnek a _jspService metódus forrásába, mégpedig a JSP oldalban elfoglalt helyüknek megfelelő helyre. A script-részletek tetszőlegesen keveredhetnek az oldal szövegével, de egybeolvasva őket az adott nyelven értelmes kódot kell, hogy kapjunk. Érdemes megnézni a specifikáció példáját: <%@ page import="java.util.Calendar" %> <% if (Calendar.getInstance().get(Calendar.AM_PM) == Calendar.AM) { %> Kellemes delelottot! <% } else { %> Kellemes delutant! <% } %>
a kapott kód _jspService metódusa, melyet az áttekinthetőség kedvéért megtisztítottam a fordító (Tomcat/Jasper 3.2 beta) kommentjeitől: public void _jspService(HttpServletRequest request, HttpServletResponse throws IOException, ServletException {
response)
JspFactory _jspxFactory = null; PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; String _value = null; try { if (_jspx_inited == false) { _jspx_init(); _jspx_inited = true; } _jspxFactory = JspFactory.getDefaultFactory(); response.setContentType("text/html;charset=8859_1"); pageContext = _jspxFactory.getPageContext(this, request, response, "", true, 8192, true); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); out.write("\r\n\r\n"); if (Calendar.getInstance().get(Calendar.AM_PM) == Calendar.AM) { out.write("\r\nKellemes delelottot!\r\n"); } else { out.write("\r\nKellemes delutant!\r\n"); } out.write("\r\n"); } catch (Exception ex) { if (out.getBufferSize() != 0) out.clearBuffer(); pageContext.handlePageException(ex); } finally { out.flush();
8
_jspxFactory.releasePageContext(pageContext); } } }
Jól látszik, hogy az általunk írt kód egy az egyben bemásolódott, és közéékelődtek a JSP fordító által előállított egyéb programsorok, amik itt kivétel nélkül out.write hívások.
3.2.3 Kifejezések A kifejezések olyan script-nyelvi elemek, amelyeket a fordító String típusra hozhat, és közvetlenül a kimenetre írhat. Az eddigiek alapján könnyen rájöhetünk, hogy a Java esetén ez hogyan zajlik: a <%= és a záró %> közti részt a JSP fordító egy out.write hívás paramétereként használja. Például: Az ido jelenleg <%= System.currentTimeMillis () %> milliszekundum (UTC).
JSP sorból az alábbi három Java kódsor áll elő: out.write("Az ido jelenleg "); out.print( System.currentTimeMillis () ); out.write(" milliszekundum (UTC).\r\n\r\n");
A típuskonverzió sikertelensége esetén a fordításkor hibaüzenetet kapunk, vagy a futtatáskor ClassCastException váltódik ki.
3.2.4 Implicit objektumok A programozó munkáját megkönnyítendő a JSP oldalba írt script-részletekben és kifejezésekben használhatók az előre deklarált ún. implicit objektumok. (Ez természetesen azt is jelenti, hogy ilyen nevű változókat külön már nem deklarálhatunk.) Többségük a szervletekkel foglalkozók számára már bizonyára közel sem ismeretlen. Az egyes osztályok adattagjainak és metódusainak tekintetében ajánlott a javax.servlet.* és alcsomagjai dokumentációjának tanulmányozása. Röviden összefoglalva: Név request
javax.servlet.ServletRequest
Típus
response
javax.servlet.ServletResponse
pageContext
javax.servlet.jsp.PageContext
vagy valamely leszármazottja pl. javax.servlet.HttpRequest vagy valamely leszármazottja pl. javax.servlet.HttpResponse
Rövid leirás A kérés objektuma. Figyelem: a jsp kezdetű kérés-paraméternevek foglaltak! A válasz objektuma. (Figyelem: tilos közvetlenül a kimenetre írni! Helyette az out implicit objektumot kell használni.) A JSP központi objektuma, összefogja a névtereket (page, request, session, application), lásd
9
session
javax.servlet.http.HttpSession
application
javax.servlet.ServletContext
az első példát. Saját akciók definiálásakor kap jelentős szerepet. A session objektuma. Csak HTTP protokoll és session="true" esetén deklarált. Lásd 3.1.1. A szervlet környezetét jelképező objektum, az eltérő elnevezés ellenére megegyezik a getServletContext
out
javax.servlet.jsp.JspWriter
visszaadott értékével. A JSP oldal kimenete, a ServletRequest PrintWriter objektuma
config
javax.servlet.ServletConfig
helyett a programozónak is ide kell írni minden, a kliensnek szánt szöveget. Fontos tudni, hogy az akciók törzsének feldolgozása során egy ideiglenes kimenetre mutat. A szervlet ServletConfig
objektuma, megegyezik a getServletConfig
page
java.lang.Object
exception
java.lang.Throwable
visszatérési értékével. Az oldalt megvalósító szervlet objektuma, Java esetén megegyezik a this értékével. Hibakezelő oldal (isErrorPage="true") esetén a fellépett kivétel objektuma. Lásd 3.1.1.
A script-elemek használatának demonstrálására nézzünk egy gyakori problémát: az oldal látogatottságát mérő számláló készítését. A JSP eszközeivel a triviális megoldás az, hogy deklarálunk egy osztályszintű változót, amit minden kérés kiíráskor növelünk egyszer: <%! private int counter; %> A szamlalo allasa: <%= counter++ %>
A megvalósítás hibája, hogy a számláló értéke elvész, ha megváltoztatjuk az oldalt (például mert változik a szöveg vagy a design), és az oldalt újra kell fordítani. Célszerű tehát eltárolni a számláló értékét az alkalmazás-környezetben (application context), hiszen ez túléli az alkalmazás egyes szerveleteinek megsemmisítését és újraindítását: <%! public final static String counterkey = "mypage_counter";
10
private int counter; public void jspInit () { ServletContext context = getServletContext (); Object storedobject = context.getAttribute (counterkey); if (storedobject != null) { try { counter = ((Integer) storedobject).intValue (); } catch (Exception e) { System.err.println ("Invalid counter object in application scope."); } } } public void jspDestroy () { ServletContext context = getServletContext (); context.setAttribute (counterkey, new Integer (counter)); } %> A szamlalo tarolt erteke: <%= (Integer) application.getAttribute (counterkey) %> A szamlalo mostani allasa: <%= counter++ %>
A felültdefiniált init és destroy metódusokban történik a tárolt érték lekérdezése, illetve az új érték elmentése. Ezekben a metódusokban még nem hivatkozhatunk az implicit application objektumra, mert az implicit objektumok csak a _jspService eljárás törzsében deklarálódnak. A megoldás továbbfejleszthető az oldalszámláló állásának perzisztens tárolásával (fileban, adatbázisban stb.), ekkor a számláló értéke a szerver leállása esetén sem veszik el.
3.3 Akciók Az eddig leírtak alapján bizonyára a tisztelt Olvasóban is felmerült már a kétség: mi szükség van a JSP-re, ha -azon túl, hogy kényelmesebben szerkeszthető- lényegében a szervletek egyfajta átírása? Hogy fogja beváltani azt a bevezetőben még hozzá fűzött reményt, hogy segítségével a Javában, vagy általában a programozásban kevéssé jártasak is kezelhessenek különféle szerveroldali komponenseket? A választ kisebb részben a JSP saját akciói adják, az igazi előrelépést ezen a téren mégis az 1.1-es specifikáció jelentette. Ekkor jelent meg a JSP nyelvi kiterjesztésének a lehetősége: a programozók saját akciókat definiáló custom tag librarykat írhatnak, egyszerű elemek mögé rejtve a máskülönben az oldalban terpeszkedő kódrészleteket. A saját akciók mögött egy-egy Java osztály áll, mely felhasználhatja, és általában fel is használja az attribútumok értékeit valamint az implicit objektumok (például a kérés) paramétereit, és ezek alapján valamilyen szerveroldali feladatot végez el, ír a kimenetre, esetleg új script-változókat is definiál. Az akciók a kód elrejtésén túl a kód újrafelhasználásában is fontos szerepet játszanak, mivel a JSP más elemeihez hasonlóan egy oldalon belül is –azonos vagy eltérő paraméterezéssel- tetszőlegesen sokszor felhasználhatók. A standard és saját akciók (actions) használata nem programozói lelkületűek számára sem jelenthet gondot, hiszen XML-szerű szintaxist követnek. (Standard akciók esetén a prefix jsp, saját akcióinkhoz a taglib direktíva révén választhatunk prefixet. Lásd 3.1.3) Az akciók által használt szintaxist azért neveztem XML-szerűnek, mert némileg bővebb annál: lehetőséget ad arra, hogy bizonyos attribútumok értékeként kifejezést adjunk meg, amelyik csak futásidőben értékelődik ki.
11
3.3.1 A standard akciók 3.3.1.1 A <jsp:useBean> akció A jsp:useBean akció többféle funkciót egyesít, emiatt sajnos sem a szintaxisa, sem a használata nem nevezhető triviálisnak. Lényege, hogy megpróbálja megkeresni a megadott névtérben (scope) megadott néven (id) szereplő objektumot, és az oldal script nyelve számára szintén id néven elérhetővé tenni, type típusúra kényszerítve. (Az id attribútum tehát kettős szerepet játszik és az adott script-nyelv változó-elnevezési konvencióinak is meg kell felelnie!) Használható ugyanakkor objektumok, illetve JavaBeanek példányosítására is, ugyanis ha a keresés nem jár sikerrel, akkor a class vagy a beanName attribútumok alapján megkísérli példányosítani az adott osztályt, elhelyezi a kapott objektumot a scope névtérben és az id értékeként megadott néven a scriptnyelv számára is elérhetővé teszi az új objektumot. Kötelező értéket adni az id attribútumnak, valamint a type és a class attribútumok közül legalább az egyiknek. Az id a keresett, illetve deklarálandó változó neve, a scope lehetséges értékei pedig a következők: page
request
session
application
A nevezett objektumot az oldal javax.servlet.jsp.PageContext objektumában keresi, illetve az új objektumot ott tárolja. Az ilyen objektumok élettartama az oldal egyetlen lefutásának ideje. Ez a scope alapértelmezett értéke is egyben. Az objektumot a kérés javax.servlet.ServletRequest objektumában keresi, illetve az új objektumot abban tárolja el. Az ilyen objektumok élettartama a kérés kiszolgálásának befejezéséig tart. (Forward vagy include használata esetén ez eltérhet a page scope élettartamától!) Az objektumot a felhasználóhoz kötödő javax.servlet.http.HttpSession objetumban keresi, illetve az új objektumot ott tárolja el. A session élettartama a session lejártával, a session érvénytelenítésével, vagy az alkalmazás terminálásával ér véget. Az objektumot az egész web-alkalmazáshoz tartozó javax.servlet.ServletContext objektumban keresi, illetve az új objektumot ott tárolja el. Az ilyen objektumok élettartama megegyezik az egész web-alkalmazás élettartamával. Fontos, hogy distributable web-alkalmazás esetén minden JVM külön ServletContextet használ, így a globális adatok megosztására valamilyen más mechanizmust (pl. adatbázist) kell használni.
A type és a class értéke Java osztálynév lehet (nem kötelezően JavaBean!), a beanName értéke egy, a java.beans.Beans osztály instantiate metódusával példányosítható JavaBean neve kell hogy legyen. Nem adható meg egyszerre beanName és class, és ha a type és a class is meg van adva, akkor a class a Java szabályai szerint értékül adható kell, hogy legyen type-nak. A beanName értéke futásidőben kiértékelődő kifejezés is lehet.
12
A teljes káoszt megelőzendő érdemes a specifikációból pontosan is idézni a jsp:useBean teljes hatásmechanizmusát:
1. Az id és a scope alapján megpróbálja megkeresni az adott objektumot az adott névtérben. 2. Az oldal scriptnyelvében deklarál egy változót id néven és type típussal, amennyiben ez utóbbi definiált, különben pedig class típussal. 3. Ha az objektumot megtalálta az adott néven és az adott hatókörrel, akkor típuskényszerítéssel type típusúra hozza és értékül adja a létrehozott változónak. (Ha a típuskényszerítés sikertelen, akkor java.lang.ClassCastException lép fel, és a jsp:useBean feldolgozása véget ér.) A jsp:useBean elem törzse eldobódik, és ezzel az elem feldolgozása véget ér. 4. Ha az objektum nem volt megtalálható, és se a class, se a useBean attribútum nem volt megadva, akkor egy java.lang.InstantionException kivétel dobásával a feldolgozás véget ér. 5. Ha az objektum nem volt megtalálható, és a class attribútum értékeként megadott osztály rendelkezik nyilvános, argumentum nélküli konstruktorral, akkor példányosítja, az új objektumot értékül adja a script-változónak, és elhelyezi a scope által meghatározott névtérben, majd a 7. pontnál folytatja. Ha a megadott osztály nem példányosítható (absztrakt, interface, vagy nincs megfelelő konstruktora), akkor a feldolgozás egy java.lang.InstantionException dobásával ér véget. 6. Ha az objektum nem volt megtalálható, és a beanName attribútum adott, akkor meghívja a java.beans.Beans osztály instantiate metódusát a szervlet ClassLoaderével és a megadott névvel. Amennyiben sikerrel jár, akkor az előzőhöz hasonlóan az új objektumot értékül adja a script-változónak, és elhelyezi a scope által meghatározott névtérben, majd a 7. pontnál folytatja. 7. Ha a jsp:useBean törzse nem üres, akkor feldolgozza. A törzsben lévő script-elemek számára az új változó már elérhető. A jsp:useBean két jellemző felhasználására álljon itt két példa. Az egyik eset, amikor valamelyik névtérben eltárolt, már meglévő objektumot szeretnénk scriptből is elérhetővé tenni. Ilyenkor az id és scope attribútumokon kívül csak a type-nak kell értéket adni. <jsp:useBean id="vect" scope="request" type="java.util.Vector" />
Ebben az esetben a vect nevű java.util.Vector típusú objektumnak szerepelnie kell a kérés paraméterei között, ellenkező esetben hibát kapunk. Az ebből a sorból generált kód (Tomcat/Jasper 3.2 beta): // begin [file="E:\\tomcat\\webapps\\ROOT\\hello.jsp";from=(0,0);to=(0,65)] java.util.Vector vect = null; boolean _jspx_specialvect = false; synchronized (request) { vect= (java.util.Vector) pageContext.getAttribute("vect",PageContext.REQUEST_SCOPE); if (vect == null) throw new java.lang.InstantiationException ("bean vect not found within scope "); } if(_jspx_specialvect == true) { // end // begin [file="E:\\tomcat\\webapps\\ROOT\\hello.jsp";from=(0,0);to=(0,65)] }
13
A jsp:useBean másik gyakori alkalmazása, amikor egy új objektumot szeretnénk létrehozni. Az id és scope attribútumok mellett ilyenkor vagy a class-nak vagy beanName-nek és a type-nak kell szerepelnie: <jsp:useBean id="vect" scope="request" class="java.util.Vector" />
vagy <jsp:useBean id="vect" scope="request" type="java.util.List" beanName="java.util.Vector" />
A Tomcat/Jasper 3.2 beta némileg meglepő módon a kettőből azonos kódot készít, azzal a különbséggel, hogy az utóbbi esetben a type értékét felhasználja típuskonvertálásra: // begin [file="E:\\tomcat\\webapps\\ROOT\\hello.jsp";from=(0,0);to=(0,91)] java.util.List vect = null; boolean _jspx_specialvect = false; synchronized (request) { vect= (java.util.List) pageContext.getAttribute("vect",PageContext.REQUEST_SCOPE); if ( vect == null ) { _jspx_specialvect = true; try { vect = (java.util.List) Beans.instantiate(this.getClass().getClassLoader(), "java.util.Vector"); } catch (Exception exc) { throw new ServletException (" Cannot create bean of class "+"java.util.Vector"); } pageContext.setAttribute("vect", vect, PageContext.REQUEST_SCOPE); } } if(_jspx_specialvect == true) { // end // begin [file="E:\\tomcat\\webapps\\ROOT\\hello.jsp";from=(0,0);to=(0,91)] } // end
3.3.1.2 A <jsp:setProperty> akció A jsp:useBean akcióval, vagy script-elemmel létrehozott beanek tulajdonságainak beállítását szolgálja a jsp:setProperty akció. Két kötelező attribútuma a name, és a property, előbbi a kérdéses bean neve (ami megegyezik a jsp:useBean id attribútumának értékével, ha azzal hoztuk létre), utóbbi a beállítandó tulajdonság neve. A property értékeként megadható "*" is, ez esetben minden olyan property értéke automatikusan beállítódik a megfelelő setProperty () metódussal, amelyikhez a ServletRequest objektumban azonos néven tárolva van valamilyen nemüres érték. (Ez jól használható formok paramétereit tároló beanek feltöltésére). Ugyanez a mechanizmus játszódik le egyetlen propertyvel, ha a property értékeként egy konkrét property nevét adjuk meg, és nem írunk harmadik attribútumot. Mindkét esetben konvertálódik a requestben tárolt érték, ha szükséges. Harmadik attribútumként megadható a param, amit akkor kell használni, ha a ServletRequest-ben a property nevétől eltérő kulccsal van tárolva az érték. Végül, de nem utolsó sorban a value paraméter megadásával tetszőleges, akár futásidőben számolt értéket is adhatunk az adott propertynek. Összefoglalva a lehetséges szintaxisok: <jsp:setProperty name="beannev" property="*" />
14
<jsp:setProperty name="beannev" property="propertynev" /> <jsp:setProperty name="beannev" property="propertynev" param="parameternev" /> <jsp:setProperty name="beannev" property="propertynev" value="ertek" />
3.3.1.3 A <jsp:getProperty> akció A jsp:getProperty, mint a neve is mutatja, a jsp:setProperty párja: a jsp:useBean akcióval, vagy valamilyen script-elemmel létrehozott bean valamely tulajdonságának a lekérdezésére szolgál. A name attribútum értékeként megadott névvel rendelkező bean property nevű tulajdonságát kérdezi le a megfelelő getProperty () metódussal, és írja ki a println metódusnak megfelelő konverzió után a JSP oldal szabványos kimenetére (azaz az implicit out objektumba). Példa jsp:setProperty és jsp:getProperty használatára: <jsp:useBean id="date" class="java.util.Date" /> A date altal tarolt ido <jsp:getProperty name="date" property="time" /> ms (UTC). <% Thread.sleep (10); %> <jsp:setProperty name="date" property="time" value="<%= System.currentTimeMillis () %>" /> A date altal tarolt ido a beallitas utan <jsp:getProperty name="date" property="time" /> ms (UTC).
Az első sorban az alapértelmezett "page" hatókörrel létrehozunk egy date nevű, java.util.Date osztályú objektumot, a második sorban lekérdezzük ennek time nevű tulajdonságát. Majd a harmadik sorban elaltatjuk a szervletet 10 ms ideig, a negyedikben pedig a jsp:setProperty segítségével frissítjük a date által tárolt időt. Végül újra lekérdezzük a most beállított attribútum értékét, ami – ha a futtató környezet elég gyors volt – éppen 10 milliszekundummal nagyobb értéket eredményez. 3.3.1.4 A <jsp:include> akció A komplex JSP oldalak -önmagukban is funkcionális- egységekre bontásának legfontosabb eszköze a jsp:include akció: segítségével az oldal adott pontján beilleszthetjük egy másik szervlet vagy JSP oldal futásának eredményét. Az include direktívához hasonlóan a jsp:include akcióval is lehetséges statikus erőforrások beszúrása, lényeges különbség viszont, hogy míg a direktívában megjelölt file a fordítás előtt kerül be az oldal szövegébe (így a benne levő JSP elemek is érvényre jutnak), az akcióval beszúrt statikus file viszont a kérés kiszolgálásakor, utólag illesztődik bele a készülő oldal szövegébe. A jsp:include leggyakoribb felhasználása mégis a szervletek/JSP oldalak eredményének a beszúrása: az akció hatására ilyenkor kiűrítődik a puffer, majd meghívódik a RequestDispatcher osztály include metódusa, aminek visszatérése után folytatódik az oldal feldolgozása. Így a meghívott szervletre, illetve JSP oldalra az include metódusra érvényes korlátozások vonatkoznak, azaz nem állítgathatja sem a válasz headerjeit, sem a response code-ot. (Az ezzel probálkozó utasításokat a container figyelmen kívül hagyja.) További megszorítás, hogy jsp:include nem szerepelhet más akció (standard vagy saját) törzsében, az out implicit objektum ugyanis ilyenkor nem az oldal JspWriter objektumára mutat. A jsp:include kötelező page attribútumának kell értékül adni a beillesztendő erőforrás relatív URL-jét, ami lehet kifejezés futásidejű eredménye is, a szintén kötelező flush attribútum értéke minden esetben true kell, hogy legyen. Az elem törzsében lehetnek jsp:param akciók, ekkor a kérés kiegészül az ezekben megadott paraméterekkel, mielőtt a kívánt erőforrásnak
15
átadódna. (Ezeknek az értékeknek precedenciájuk van a hasonló kulccsal tárolt értékekkel szemben, de a visszatérés után törlődnek a kérésből.) A szintaxis tehát a következő: <jsp:include page="url" flush="true"/>
vagy <jsp:include page="url" flush="true"> <jsp:param name="nev" value="ertek" /> …
3.3.1.5 A <jsp:forward> akció A feladatmegosztás másik eszköze a jsp:forward akció, melynek révén a kérést továbbadhatjuk egy másik statikus erőforrásnak, szervletnek, vagy JSP oldalnak. Leggyakoribb alkalmazása egy olyan architektúra, amelyben egy ún. front component (szervlet vagy JSP) vizsgálja meg a beérkező kérést, majd annak paraméterei függvényében továbbítja azt valamelyik JSP oldalnak. (Például ha a kérés egy bejelentkezés adatait tartalmazta, és a bejelentkezés sikeres, akkor az ezt igazoló oldalnak adódik át a kérés, ha pedig sikertelen, akkor ismét a bejelentkező adatait rögzítő oldal kapja meg a kérést). A jsp:forward elem feldolgozásakor a puffer tartalma törlődik, ha pedig nem volt pufferelve az oldal, és már írt a kimenetre, akkor java.lang.IllegalStateException váltódik ki. A kérés továbbítása után az oldal feldolgozása véget ér. A jsp:forward egyetlen paramétere a page, aminek az erőforrás relatív URL-jét kell értékül, ez lehet valamilyen kifejezés futásidejű kiértékelésének eredménye is. Az elem törzsében lehetnek jsp:param akciók, ekkor a kérés kiegészül az ezekben megadott paraméterekkel, mielőtt továbbítódna. (Ezeknek az értékeknek precedenciájuk van a hasonló kulccsal tárolt értékekkel szemben.) A szintaxis tehát igen egyszerű: <jsp:forward page="url" />
vagy <jsp:forward page="url" > <jsp:param name="nev" value="ertek" /> …
3.3.1.6 A <jsp:plugin> akció A jsp:plugin már nem szorosan a JSP funkcióihoz kötődik, inkább felcsillant valamit a saját akciók lehetőségeiből: beilleszti az oldal szövegébe azt a megjegyezhetetlenül bonyolult HTML-kódot, ami ahhoz kell, hogy a böngésző letöltse egy applet vagy bean indításához szükséges plug-int, és lefutassa azt. A type attribútum értéke –"applet" vagy "bean" adja meg a futtatandó komponens típusát, a szintén kötelező code és codebase, illetve az opcionális name, title, align, archive, width, height, hspace, vspace attribútumok a HTML specifikáció szerintiek. A jreversion jelzi a futtatáshoz szükséges JRE verziószámát, alapértelmezés szerinti értéke 1.1. Az opcionális nsplugin és iepluginurl attribútumok adják meg a letöltendő plugin URL-jét Netscape Navigator és Internet Explorer esetén, ezek alapértelmezése implementációfüggő. A jsp:plugin elem törzsében lehet egy paraméterek nélküli
16
jsp:params elem, ennek a törzsében kell elhelyezni a futtatandó applet paramétereit beállító jsp:param elemeket. A jsp:plugin tartalmazhat továbbá egy jsp:fallback nevű,
attribútumokkal szintén nem rendelkező elemet, ami azt a HTML szöveget adja meg, amit a böngésző a plug-in sikertelen futtatása esetén jelenít meg. Egy példa a J2SE MoleculeViewer appletjének felhasználásával:
Molecule Viewer <jsp:plugin type="applet" code="XYZApp.class" codebase="/" width="300" height="300"> <jsp:params> <jsp:param name="model" value="models/HyaluronicAcid.xyz" /> <jsp:fallback>
Unable to start plugin!
3.3.1.7 A <jsp:param> akció A jsp:param a jsp:include, jsp:forward, és jsp:plugin akciók törzsében szerepelhet, célja kulcs-érték párok egymáshoz rendelése, az akciók paraméterezése. Két kötelező attribútuma a name és a value, ez utóbbi értéke kifejezés is lehet.
3.3.2 Saját elemkönyvtárak készítése és használata A JSP legújabb és legígéretesebb területe a saját akció-elemek, illetve elemkönyvtárak (custom tag library) készítése. Egyben ez a terület, ahová manapság a legtöbb fejlesztés összpontosul. Szükség is van rá, mivel a technikai megvalósítás meglehetősen bonyolultra sikeredett: a specifikáció kiötlői egy újabb réteget vontak a már amúgy is a sokadik absztrakciós szintet jelentő JSP oldalak fölé. Ez persze egészen addig nem jelent gondot, amíg nem szembesülünk a hibakeresés szépségeivel… Amilyen nehéz téma a saját tag-ek készítése, olyan könnyű a felhasználásuk, mint azt rövidesen látni fogjuk. Ennek a kettősségnek az oka az, hogy a specifikáció írói a szerveroldalon ténykedő programozóknak szánták az elemkönyvtárak készítésének feladatát, az internetes tartalmak szerzőinek pedig az elemek felhasználását. A kitalálók reménye az, hogy a szerveroldali termékek készítői saját elemkönyvtáraikon keresztül nyújtanak majd hozzáférést a termékeik funkcionalitásához, illetve hogy aki hasznos, gyakori problémát megoldó elemet készít az az internet révén megosztja azt a programozói közösséggel, és így ez a mechanizmus a JSP kód megosztásának és újrafelhasználásának globális eszköze lehet. Ezt segíti az elemkönyvtárak kettős hordozhatósága: az oldal programozási nyelvétől függetlenül, tetszőleges készítőtől származó JSP containerben felhasználhatóak. Mielőtt egy mély lélegzetvételt követően fejest ugranánk az elemkönyvtárak készítésének és felhasználásának rejtelmeibe, érdemes megemlíteni egy alternatív megoldást, a Resin servlet/JSP container által támogatt XML Template Pages (XTP) technológiát. Bár új nevet kapott, ez lényegében az XSL egy innovatív felhasználása, ennek segítségével helyettesítik be az XML dokumentumban elhelyezett saját elemeket JSP kóddal. Előnye az egyszerűbb
17
programozhatóság (a saját elemek kódja is JSP), hátránya az, hogy összetett, nehezen átlátható, hibakeresési szempontból problémás, kísérleti stádiumban lévő technológia. Az XTP-ről bővebben a Caucho web-oldalain lehet olvasni. Az alábbi fejezetek példáinak forráskódja letölthető innen: http://javasite.bme.hu/dokument/jsp/taglibtut.zip. További munkák a JSP elemkönyvtárak készítéséről: •
•
Szervlet és JSP témakörben az egyik legjobb munka Marty Hall idén megjelent könyve, a Core Servlets and JavaServer Pages. Ennek tizennegyedik fejezete példákon keresztül mutatja be a saját elemek készítését. Az egyetlen nem tárgyalt téma az új script változók bevezetése. Szintén erősen gyakorlatorientált mű Magnus Rydin internetes
-ja .
3.3.2.1 Elemkönyvtárak használata Könnyű helyzetben vagyunk, ha készen kapunk vagy az internetről, például a jsptags.com oldalról vagy a Jakarta project taglibs alprojectjéből töltünk le egy adott célra szolgáló elemkönyvtárat. Ilyenkor mindenképp meg kell, hogy kapjuk az elemkönyvtár tulajdonságait leíró .tld kiterjesztésű tag library descriptor-t, és az elemek funkcióját megvalósító Java osztályokat .class fileok, vagy egy .jar kiterjesztésű archívum formájában. (A TLD a jar file /meta-inf alkönyvtárában is lehet.) Általában kapunk külön használati utasítást is a könyvtár mellé, de a megfelelően megírt TLD-nek is tartalmaznia kell a felhasználáshoz szükséges összes információt, érdemes tehát megismerkedni vele. A TLD egy egyszerű szerkezetű XML dokumentum, mely a JSP containernek, a JSP-t ismerő szerkesztőeszközöknek, és a könyvtárat saját kezűleg felhasználóknak szolgál információkkal a könyvtárról és a benne szereplő elemekről. Példaként nézzünk egy egyszerű TLD filet, ami ugyan csak egyetlen akció-elemet ír le (amit a későbbiekben el is fogunk készíteni), de a TLD-k összes lehetséges elemének használatát bemutatja:
1.0 <jspversion>1.1 <shortname>tutorial http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0 JSP Taglib peldakonyvtar greeting tags.GreetingTag empty Napszaktol fuggo koszontest illeszt az oldal szovegebe. A verbose parameter 'true' erteke eseten a pontos idot is kiirja. Alapertelmezett erteke 'false'. verbose <required>false false
18
A file az XML dokumentum fejléccel kezdődik. A legkülső elem a taglib, ennek törzsében a következő elemek lehetnek: • • •
•
• •
tlibversion: a könyvtár verziószáma, a könyvtár fejlődésének a nyomonkövetésére szolgál. jspversion: a könyvtár által igényelt JSP verzió száma. Jelenleg csak az 1.1 érték érvényes. Opcionális. shortname: a könyvtár rövid neve, ez lehet például a könyvtár készítője által ajánlott prefix (amit a taglib direktívának lehet megadni). A JSP specifikáció ennek konkrét felhasználását nem említi, mindenesetre lehetőleg ne tartalmazzon whitespace-t, és ne kezdődjön aláhúzással vagy számjeggyel. uri: egy olyan nyilvános URI, amelyik egyértelműen azonosítja a könyvtárat a verziószámmal egyetemben. Érdemes megjegyezni, hogy a JSP specifikáció jelenlegi verziója nem használja ezt az értékét. (A taglib direktívának megadott URI tehát nem kell, hogy megegyezzen ezzel.) Opcionális. info: a könyvtár általános leírása. Opcionális. tag: Egy saját elem leírása, ilyenből a könyvtárban tetszőleges számú lehet. • name: A saját akció neve. Ezt a nevet kell majd a prefix után írni az elemre való hivatkozáskor. • tagclass: Az elemkezelő (tag handler) Java osztály neve. • teiclass: Az elemről kiegészítő információkat szolgáltató, TagExtraInfo-tól származtatott osztály neve. Jelenleg csak új változókat bevezető elemeknél használatos. Opcionális. • bodycontent: Az elem törzsének lehetséges tartalma. Három lehetséges értéke: tagdependent, JSP, illetve empty. Az első eset azt jelöli, amikor az elem törzse valamilyen egyéni formátumban van (az elem maga dolgozza fel a törzset), a második esetben az elem törzse is JSP (a JSP fordító dolgozza fel a törzset), a harmadik esetben az elem törzse üres kell legyen. (Hibajelzést kapunk, ha másként használjuk.) Opcionális, alapértelmezése JSP. • info: az elem funkciójának szöveges leírása, érdemes beleírni az attribútumok szerepét is. • attribute: Az elem egy attribútumának a leírása. Természetesen ebből is több lehet. • name: Az attribútum neve. • required: true, false, yes, vagy no értéke mutatja, hogy kötelező-e megadni az attribútumot. Opcionális, alapértelmezése false. • rtexprvalue: true, false, yes, vagy no aszerint, hogy az attribútum értéke lehet-e futásidőben számított kifejezés eredménye. Opcionális, alapértelmezése false.
A könyvtár használatához a következőket kell tennünk. A web-alkalmazásunk /web-inf könyvtárában hozzunk létre egy web-alkalmazás leírót web.xml néven, vagy a meglévő web.xml filehoz adjunk hozzá egy taglib elemet. (A szervleteket/JSP oldalakat használó websiteok tulajdonságait definiáló web-alkalmazás leíróról bővebben a Servlet specifikációban olvashatunk.) Példaként álljon itt egy egyszerű web.xml: <web-app>
19
http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0 /WEB-INF/tutorial.tld
Ennek a taglib elemnek csak két aleleme lehet. A taglib-uri, ami valós vagy szimbolikus URI is lehet, az elemkönyvtárat azonosítja a JSP container számára, ezért a könyvtárat használó oldalak taglib direktíváinak uri attribútumai ezt az értéket kell, hogy kapják. A taglib-location adja meg a TLD file tényleges helyét, ez a specifikáció szerint a /web-inf könyvtárban kell, hogy legyen. A .class fileokat ezek után helyezzük a /web-inf/classes könyvtárba, illetve a Java konvenciók szerint a megfelelő alkönyvtárába (a példában ez a /web-inf/classes/tags), ha .jar fileban vannak, akkor tegyük a .jar filet a /web-inf/lib könyvtárba. (Lehet a könyvtár egyik része külön fileokban, a másik része pedig .jar fileban.) Ezek után már nincs más hátra, mint hogy felhasználjuk az új elemeket a saját oldalunkban. Egy példa a fenti tutorial nevű könyvtár használatára: <%@ page language="java" %> <%@ taglib uri="http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0" prefix="tutorial" %>
Tag library pelda 1
A JSP fordító a taglib direktíva uri attribútuma alapján fogja megkeresni a web.xml fileban az elemkönyvtárhoz tartozó leíró helyét, majd a leíró alapján leellenőrzi, hogy helyesen használtuk-e az elemeket, végül befordítja a készülő szervlet kódjába azokat a hívásokat, amelyek az elemkezelő (tag handler) Java osztály megfelelő metódusait aktivizálják. (Ha a web.xml fileban nem volt megegyező URI, akkor relatív URI esetén megpróbálja közvetlenül az uri attribútum alapján megkeresni a könyvtárleírót. Ezt a lehetőséget azonban nem ajánlott kihasználni, mert csökkenti az átláthatóságot.) 3.3.2.2 Egyszerű elemek készítése Ha már kitaláltuk, hogy mit is tud majd a megálmodott elem, és milyen attribútumai lesznek, majd ezek alapján megírtuk a TLD-t, akkor már nincs más hátra, mint hogy megírjuk az elemnek szánt feladatokat elvégző elemkezelőt. Az elemkezelő egy Java osztály kell legyen, pontosabban egy megadott tulajdonságokkal (property-kkel) rendelkező JavaBean, ami közvetve vagy közvetlenül megvalósítja a javax.servlet.jsp.tagext csomag Tag, vagy BodyTag interface-ét. Ha csak nincsenek különleges igényeink, célszerű az ugyanebben a csomagban található TagSupport, illetve BodyTagSupport osztályt kiterjeszteni, ezek ugyanis implementálják a Tag, illetve a BodyTag interface-t, így már csak az egyes eljárásaikat kell az igényeink szerint felüldefiniálni. (Ezen kívül kényelmi metódusokkal is szolgálnak, erről később.) A TagSupport osztályt célszerű kiterjeszteni, ha a saját akciónknak nincs törzse, vagy lehet törzse, de azt vagy egy az egyben felhasználjuk, vagy egy az egyben kidobjuk. (Ilyenkor
20
nincs szükség a BodyTag interface plusz metódusaira). A BodyTagSupport osztályt kell kiterjeszteni, ha a törzset manipulálni is szeretnénk. A interface-ek azokat a metódusokat tartalmazzák, amiken keresztül a JSP oldalból készült szervlet az elemkezelő osztállyal kommunikál, a TagSupport és a BodyTagSupport ezekhez hozzáteszik azt a két kötelező propertyt (pageContext és parent) amivel minden elemkezelőnek rendelkeznie kell. A két property értéke rögtön az elemkezelő példányosítása után beállítódik, így minden más metódusból elérhetők. A pageContext az oldal pageContext objektumára mutat, ezen keresztül kaphatók meg a nevezetes implicit objektumok (request, response, session, application), illetve közvetlenül is elérhetők a különfélenévterek. (Lásd PageContext.getAttribute) A parent a szülő elem objektumát kapja értékül, ezen keresztül kommunikálhat a gyerek az őt közrefogó szülő-elemmel. (Lásd 3.3.2.5) Ezeken kívül az elemkezelőben minden attribútumhoz tartoznia kell egy azonos nevű propertynek (elég ha setProperty metódus van), ezek az attribútumok aktuális értékeit kapják paraméterül, szintén még más metódusok meghívása előtt. A propertyk értékeit beállító és lekérdező metódusokon túl a Tag interface három érdemi metódussal rendelkezik. Az első a doStartTag, a JSP oldalból készített szervletben ez a hívódik meg a nyitóelem helyén. A metódus törzsében a már elmondottak alapján felhasználhatjuk a pageContext, a parent és az attribútumok értékeit. Az eljárás visszatérési értéke a Tag interface megvalósítása esetén SKIP_BODY vagy EVAL_BODY_INCLUDE lehet, az előbbi, alapértelmezés szerinti esetben a törzs nem kerül feldolgozásra, az utóbbi esetben viszont igen. (A BodyTag interface esetét lásd 3.3.2.3). A metódus párja a doEndTag, ez a záró elem helyén hívódik meg. (Akkor is meghívódik, ha az elem <prefix:nev /> alakú.) Két lehetséges visszatérési értéke EVAL_PAGE, és SKIP_PAGE. Az előbbi esetben, ami egyben az alapértelmezés is, folytatódik az oldal végrehajtása, az utóbbi esetben pedig befejeződik. (Ez még nem feltétlenül jelenti a kérés kiszolgálásának végét, hiszen az oldal meghívása lehetett egy include eredménye is.) Mind a doStartTag, mind a doEndTag dobhat JspException kivételt, ilyenkor a JSP oldal szokásos kivételkezelő mechanizmusa lép életbe. (Használatos még a JSPException leszármazottja, a JSPTagException. A kettő között jelenleg semmiféle eltérés nincs.) A Tag interface harmadik, az elem életciklusához kötödő eljárása a release, ez akkor hívódik meg, ha a szervlet már végzett az elemkezelő objektummal. (Ha egy elem többször is előfordul egy oldalon, akkor a JSP fordító készíthet olyan kódot, ami egy példányt többször is felhasznál.) További magyarázatok helyett nézzük meg az előző pontbeli greeting elem példáján, hogy hogyan is néz ki mindez a gyakorlatban. Íme a TLD-ben megadott GreetingTag Java forráskódja: package tags; import import import import import import
java.util.*; java.io.*; javax.servlet.*; javax.servlet.http.*; javax.servlet.jsp.*; javax.servlet.jsp.tagext.*;
public class GreetingTag extends TagSupport { private boolean verbose = false; public void setVerbose (String verbose) { if (verbose != null && verbose.equalsIgnoreCase ("true")) this.verbose = true; } public int doStartTag() throws JspException
21
{ JspWriter writer = pageContext.getOut(); Calendar cal = Calendar.getInstance (); try { if (cal.get(Calendar.AM_PM) == Calendar.AM) { writer.print ("Kellemes delelottot!"); } else { writer.print ("Kellemes delutant!"); } if (verbose) writer.print (" A pontos ido " + cal.get(Calendar.HOUR) + " ora " + cal.get (Calendar.MINUTE) + " perc."); } catch (IOException e) { throw new JspException (e.getMessage()); } return SKIP_BODY; } }
A már elmondottak szerint a GreetingTag osztály a TagSupport leszármazottja. Egyetlen saját propertyje az egyetlen attribútumához tartozó verbose, a beállítómetódust ehhez nekünk kell megadni. A doStartTag a pageContext objektumtól lekéri a kiirásra szolgáló JspWriter-t, majd erre az idő függvényében kiírja a köszöntést, a verbose értéke szerint ehhez még esetlegesen hozzáteszi a pontos időt. Az esetleg fellépő IOExceptiont kénytelenek vagyunk elkapni és újradobni, mert az eljárás csak JspException, illetve leszármazottainak dobását teszi lehetővé. Végül SKIP_BODY értékkel térünk vissza, mert az elemhez a TLD alapján eleve nem tartozhat törzs. A doEndTag metódust nem kell felüldefiniálnunk, mivel az alapértelmezett visszatérési értéke (EVAL_PAGE) számunkra megfelel. A release metódust sem definiáltuk fölül, mert nem tároltunk semmilyen objektum-referenciát. Ha engedélyezzük a TLD-ben a futásidejű paraméterek használatát, akkor az attribútumok értékeit eltároló setProperty metódusok paramétere konkrét típus, esetünkben boolean is lehet, így egyszerűsödik az eljárás: public void setVerbose (boolean verbose) { this.verbose = verbose; }
Ilyen esetben viszont Tomcat/Jasper 3.2 beta esetén az attribútum értéke mindenképp kifejezés kell legyen. (A Tomcat 3.1 még engedékenyebb volt e téren.) Például
Tanulságos megnézni, hogy a fordító milyen kódot készít az előző pontban szereplő JSP oldalból a TLD és a mi osztályunk ismeretében. (Ez utóbbit a Java reflection segítségével deríti fel. Ellenőrzi, hogy megvannak-e az attribútumokhoz tartozó setPropertymetódusok stb.) A szervlet _jspService metódusa a kommentektől megtisztítva Tomcat/Jasper 3.2 beta esetén: public void _jspService(HttpServletRequest request, HttpServletResponse
response)
22
throws IOException, ServletException { JspFactory _jspxFactory = null; PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; String _value = null; try { if (_jspx_inited == false) { _jspx_init(); _jspx_inited = true; } _jspxFactory = JspFactory.getDefaultFactory(); response.setContentType("text/html;charset=8859_1"); pageContext = _jspxFactory.getPageContext(this, request, response, "", true, 8192, true); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); out.write("\r\n"); out.write("\r\n\r\n\r\n\t
Tag library pelda 2\r\n\r\n\r\n\r\n\t"); tags.GreetingTag _jspx_th_tutorial_greeting_0 = new tags.GreetingTag(); _jspx_th_tutorial_greeting_0.setPageContext(pageContext); _jspx_th_tutorial_greeting_0.setParent(null); _jspx_th_tutorial_greeting_0.setVerbose("true"); try { int _jspx_eval_tutorial_greeting_0 = _jspx_th_tutorial_greeting_0.doStartTag(); if (_jspx_eval_tutorial_greeting_0 == BodyTag.EVAL_BODY_TAG) throw new JspTagException("Since tag handler class tags.GreetingTag does not implement BodyTag, it can't return BodyTag.EVAL_BODY_TAG"); if (_jspx_eval_tutorial_greeting_0 != Tag.SKIP_BODY) { do { } while (false); } if (_jspx_th_tutorial_greeting_0.doEndTag() == Tag.SKIP_PAGE) return; } finally { _jspx_th_tutorial_greeting_0.release(); } out.write("\r\n\r\n\r\n"); } catch (Exception ex) { if (out.getBufferSize() != 0) out.clearBuffer(); pageContext.handlePageException(ex); } finally { out.flush(); _jspxFactory.releasePageContext(pageContext); } }
Jól látható a metódusok meghívásának már ismertetett sorrendje, valamint az, hogy a fordító intelligenciáján lenne még mit javítani: a doStartTag metódus végén nem csak SKIP_BODY-t adhattunk volna vissza (annak ellenére, hogy a TLD-ben azt szerepeltettük, hogy az elemnek nem lehet törzse). Sok értelme persze nem lenne, hisz a fordító a TLD alapján garantálja, hogy a törzs üres, így legfeljebb egy üres do{}while(false) ciklusra futhatunk rá fölöslegesen.
23
Továbblépésként nézzük meg Marty Hall könyvének egyik egyszerű példáját, a debug elemet. A nevéhez hűen ez az elem a hibakeresést hivatott elősegíteni, mégpedig oly módon, hogy a kérés egy paraméterének beállítása esetén beteszi az oldalba a törzsében szereplő szöveget, amiben tetszőleges JSP kód is lehet. Ahhoz, hogy egy elemnek törzse lehessen a TLDben a bodycontent értékét JSP-re kell állítani. Mivel az elem nem manipulálja a törzsét, ezért a kezelőosztályt most is elég a TagSupport osztályból származtatni: package tags; import import import import
javax.servlet.jsp.*; javax.servlet.jsp.tagext.*; java.io.*; javax.servlet.*;
public class DebugTag extends TagSupport { public int doStartTag() { ServletRequest request = pageContext.getRequest(); String debugFlag = request.getParameter("debug"); if ((debugFlag != null) && (!debugFlag.equalsIgnoreCase("false"))) { return(EVAL_BODY_INCLUDE); } else { return(SKIP_BODY); } } }
A fenti metódus a debug paraméter bármely, false-tól és FALSE-tól eltérő értéke esetén beilleszti az oldalba a törzsében lévőket. Használni például így érdemes: <%@ page language="java" %> <%@ taglib uri="http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0" prefix="tutorial" %>
Tag library pelda 2 Debug: - A kliens host neve: <%= request.getRemoteHost() %>
- A kliens altal ismert MIME tipusok: <%= request.getHeader ("Accept") %>
- Session ID: <%= session.getId() %>
Az oldalban lévő debug elem törzsében lévő információkat megjeleníthetjük, ha értéket adunk a debug paraméternek, például az URL-ben, azaz http://hostnev:8080/XXX.jsp?debug= alakban hivatkozunk a dokumentumra. (Bizonyos webszervereknél az egyenlőségjel is elhagyható.) 3.3.2.3 A törzsüket is feldolgozó elemek készítése
24
Az eddig megismert lehetőségek nem elegendőek, amennyiben olyan elemeket szeretnénk készíténi, amelyek felhasználják vagy megváltoztatják a törzsük tartalmát. Az elemkezelőnek ilyenkor a Tag interface-t kiterjesztő BodyTag interfacet kell implementálnia, amit legegyszerűbben a BodyTagSupport osztálytól való örökléssel érhet el. A JSP oldalból készült szervlet, mielőtt végrehajtaná az ilyen elemek törzsét, elmenti az implicit out objektum értékét, és egy BodyContent osztályú objektummal helyettesíti. (A BodyContent egy végtelen nagy pufferrel rendelkező JSPWriter.) Ezzel dolgoznak a saját akciónk törzsében lévő JSP elemek, és ez az, amit aztán a szervlet az elemkezelőnek átad. (A BodyTagSupport ezt eltárolja számunkra a bodyContent változóban.) Azt már a saját elemkezelőnk döntheti el, hogy mit tesz a bodyContent tartalmával: lekérdezheti (getReader, getString), módosíthatja, kiírhatja a külső JSPWriterre (writeOut), vagy akár el is dobhatja (clearBody). A programozók figyelmének lankadását elkerülendő a specifikáció kiötlői igyekeztek megkülönböztetni a Tag és a BodyTag interface-ek használatát. A BodyTag interface-t alkalmazó osztályok doStartTag metódusa EVAL_BODY_TAG értéket kell, hogy visszaadjon a törzs kiértékeléséhez, szemben a Tag interface alkalmazásával, ahol erre az EVAL_BODY_INCLUDE szolgál. A BodyTag interface saját metódusai közül a legfontosabbak a doInitBody és a doAfterBody. A doInitBody egyszer, a törzs első kiértékelése előtt hívódik meg, a doAfterBody viszont a törzs minden végrehajtása után meghívódik. (Ha nem volt törzs megadva, vagy a doStartTag metódus SKIP_BODY értéket adott vissza, akkor persze egyik sem hívódik meg.) Ebből már sejthető, hogy mód van a törzs többszöri kiértékelésére: amennyiben a doAfterBody EVAL_BODY_TAG értékkel tér vissza, akkor a törzs kódja újra végrehajtódik, ha pedig SKIP_BODY-t ad vissza, akkor a törzs feldolgozása véget ér. (Az out implicit objektum értéke is visszaállítódik a legkülső JspWriterre.) A törzs többszöri kiértékelésének legjobb példái a különféle ismétléseket, felsorolásokat végző elemek. Nézzük meg kezdetnek Marty Hall könyvéből a legegyszerűbb ilyen elemet, amelyik nem tesz mást, mint a megadott számban megismétli a törzsébe írtakat: package tags; import java.io.*; import javax.servlet.jsp.*; import javax.servlet.jsp.tagext.*; public class RepeatTag extends BodyTagSupport { private int reps; public void setReps(String repeats) { try { reps = Integer.parseInt(repeats); } catch(NumberFormatException nfe) { reps = 1; } } public int doAfterBody() { if (reps-- >= 1) { BodyContent body = getBodyContent(); try { JspWriter out = body.getEnclosingWriter(); out.println(body.getString()); body.clearBody(); // Clear for next evaluation } catch(IOException ioe) { System.out.println("Error in RepeatTag: " + ioe); } return(EVAL_BODY_TAG); } else {
25
return(SKIP_BODY); } } }
Az elem egyetlen reps nevű attribútumának értékét eltároljuk, és a doAfterBody minden egyes végrehajtásakor csökkentjük eggyel. Ha még nem volt meg a kellő számú ismétlés, akkor kiírjuk a bodyContent tartalmát a kimenetre, majd töröljük, és EVAL_BODY_TAG értékkel térünk vissza, különben pedig SKIP_BODY-t adunk vissza. Példa az elem használatára: <%@ page language="java" %> <%@ taglib uri="http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0" prefix="tutorial" %>
Tag library pelda 3 Egy kis szoveg, meg egy koszontes:
Az oldalban szereplő repeat tag öt teljesen azonos bekezdést állít elő, legalábbis látszólag. Valójában, még ha nem is túl valószínű, előfordulhat, hogy a kiírt pontos idő eltér az egyes ismétléseknél (megváltozik a perc értéke). Ha megnézzük az oldalból készített Java kódot, azt is látni fogjuk, hogy a beágyazott greeting elem az elmondottak szerint valóban nem a legkülső JSPWriterrel dolgozik, hanem tudta nélkül a bodyContent-re ír. 3.3.2.4 Scriptváltozók bevezetése A gyakorlatban ritkán fordul elő, hogy egy weboldalon adott számban pontosan meg kelljen ismételni valamit, ahogy azt az előző pontban szerepelt repeat elem teszi. Annál gyakrabban merül fel az igény arra, hogy valamilyen iterált típus (vektor, lista, sorozat stb.) elemein végiglépkedve egy listát vagy táblázatot jelenítsünk meg. A cél egy olyan saját akció készítése, ami annyiszor hajtja végre a törzsét, ahány feldolgozandó elem van, és a törzsében egy scriptváltozón keresztül elérhetővé teszi az aktuális elemet. Szükség van tehát valami olyan módszerre, amellyel új scriptváltozókat definiálhatunk, hasonló a standard jsp:useBean elemhez. Nehézséget jelent ugyanakkor, hogy a JSP fordítónak már fordítási időben tudnia kell, hogy milyen nevű és típusú változokat akarunk bevezetni, hiszen az erre szolgáló programsorokat el kell helyeznie a JSP oldalból készülő szervlet kódjában. Az erre vonatkozó információkat a nagyobb rugalmasság érdekében nem a TLD-ben, hanem egy külön osztályban helyeztek el: ez a TagExtraInfo (TEI), ami a javax.servlet.jsp.tagext csomagban kapott helyet. A változókat is bevezető saját akciókhoz mellékelni kell egy ebből származtatott osztályt, és ennek a nevét kell megadni a TLD teiclass elemében. A származtatáskor elég egyetlen metódust, a getVariableInfo nevűt felüldefiniálni, a JSP fordító ennek a meghívásával kérdezi le az új változók jellemzőit. Az eljárás paraméterül kapja a TagData osztály egy példányát, melyen keresztül elérhetők az attribútumok értékei, amire szükség van ha például az egyik attribútum
26
értéke adja meg, hogy milyen néven is kell a változót létrehozni. A getVariableInfo visszatérési értéke VariableInfo objektumok tömbje kell legyen, ahol is minden egyes VariableInfo egy új változó adatait tartalmazza. (A VariableInfo osztály szerepe csak az értékek tárolásában merül ki, érdemi metódusa nincsen.) A TagExtraInfo osztály metódusai közül felüldefiniálhatjuk még az isValid eljárást, ez szintén megkapja a TagData osztály egy példányát az attribútumok ellenőrzése céljából. Hátránya, hogy mivel ez is fordítási időben hívódik meg, ezért a futásidejű attribútumértékek ellenőrzésére alkalmatlan. (Ezt az elemkezelőben kell megtennünk.) A VaribleInfo konstruktorának négy dolgot kell megadni: az új változó nevét, osztályát, azt, hogy tényleg új-e, vagy csak frissíteni kell az értékét, illetve a változó láthatóságát. Tegyük fel, hogy a mi új elemünk –legyen iterate a neve- egyetlen változót hoz létre, aminek a nevét a name attribútum, a típusát pedig a type attribútum adja meg, és azt szeretnénk, hogy a változó csak a nyitó és a záró elem között legyen elérhető. Ekkor a következő osztályt kell mellékelni az elemkezelő mellé: package tags; import javax.servlet.jsp.tagext.*; public class IterateTEI extends TagExtraInfo { public IterateTEI() {} public VariableInfo[] getVariableInfo(TagData data) { return new VariableInfo[] { new VariableInfo( data.getAttributeString("name"), data.getAttributeString("type"), true, VariableInfo.NESTED ), }; } }
A visszaadott tömbnek itt egyetlen eleme van. A VariableInfo konstruktorának első két paraméterét az attribútumok értékei alapján adtuk meg, a harmadik true azt jelzi, hogy új változóról van szó, a VariableInfo.NESTED pedig azt, hogy láthatóságát a törzsre korlátozzuk. További lehetséges értékei a VariableInfo.AT_BEGIN és a VariableInfo.AT_END. Az előbbi esetben a nyitóelemtől, a második esetben a záróelemtől definiált az új változó, egészen az oldal végéig. (A jsp:useBean az AT_BEGIN szerint működik, hiszen az új változó a nyitóelemtől az oldal végéig definiált.) A VariableInfo struktúra alapján a JSP fordító már el tudja helyezni a megfelelő deklarációkat a megfelelő helyekre. De hogyan adhatunk értéket az elemkezelő kódjából ezeknek a változóknak? A megvalósításhoz a JSP megalkotói már nem akartak új mechanizmust kitalálni, így felhasználták a már adott lehetőségeket: a megoldás annyiból áll, hogy a pageContext objektumon keresztül a page névtérben letároljuk a változó neve mellé a beállítandó értéket, amit a szervlet bizonyos pontokon szinkronizál az oldal scriptváltozóinak értékével. Hogy milyen pontokon történik szinkronizáció, az a változótól függ: •
NESTED változó szinkronizációja Tag esetén a doStartTag után történik meg, BodyTag esetén pedig az doInitBody és a doAfterBody metódusok után.
27
• •
AT_BEGIN változó szinkronizációja BodyTag esetén a doInitBody, doAfterBody és a doEndTag után, Tag esetén a doStartTag és a doEndTag után történik. AT_END változó esetén értelemszerűen szinkronizáció csak a doEndTag után zajlik.
Nézzük meg az Magnus Rydin iterate elemének példáján keresztül hogyan is néz ki ez a gyakorlatban: package tags; import java.util.*; import javax.servlet.jsp.*; import javax.servlet.jsp.tagext.*; public class IterateTag extends BodyTagSupport { private String name; private Iterator iterator; private String type; public void setCollection(Collection collection) { if(collection.size()>0) iterator = collection.iterator(); } public void setName(String name) { this.name=name; } public void setType(String type) { this.type=type; }
public int doStartTag() { if(iterator==null) { return SKIP_BODY; } if(iterator.hasNext()) { pageContext.setAttribute(name,iterator.next(),PageContext.PAGE_SCOPE); return EVAL_BODY_TAG; } else { return SKIP_BODY; } } public int doAfterBody() throws JspException { if(iterator.hasNext()) { pageContext.setAttribute(name,iterator.next(),PageContext.PAGE_SCOPE); return EVAL_BODY_TAG; }
28
else { return SKIP_BODY; } } public int doEndTag() throws JspException { try { if(bodyContent != null) bodyContent.writeOut(bodyContent.getEnclosingWriter()); } catch(java.io.IOException e) { throw new JspException("IO Error: " + e.getMessage()); } return EVAL_PAGE; } }
Az attribútumok értékeit eltároló metódusok közül csak a setCollection lóg ki a sorból, ennél ugyanis rögtön itt rögtön az Iterator-t tároljuk el. A doStartTag metódusban, amennyiben van egyáltalán felsorolandó elem, az elsőt el is helyezzük a pageContext objektum page hatókörrel. Ha még mindig van elem, ugyanezt tesszük a doAfterBody eljárásban is, és kérjük a törzs újboli kiértékelését. A doEndTag-ben így már más dolgunk nincs is, mint kiiratni a valódi kimenetre a bodyContentben felgyülemlett dolgokat. Pihentetőül szemléljük meg munkánk gyümölcsét: próbáljuk ki az alábbi JSP oldalt! Ha mindent jól csináltunk, egy háromtagú felsorolást láthatunk: <%@ page language="java" %> <%@ taglib uri="http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0" prefix="tutorial" %>
Tag library pelda 4 <% java.util.Vector vect = new java.util.Vector (); vect.add ("alma"); vect.add ("korte"); vect.add ("narancs"); %>
3.3.2.5 Egymásbaágyazott elemek készítése A saját elemek együttműködésének egyik módja, hogy az egyik elem bevezet egy új scriptváltozót, amit a másik elem felhasznál. Például az egyik elem kikeresi egy táblázat adatait az adatbázisból, a másik elem pedig –ez lehet a fenti iterate- a megfelelő formában kiírja azt. Az együttműködés másik módja az elemek egymásbaágyazása, ami jobban kifejezi az elemek összetartozását, bár a programozása némi többletmunkát igényel. Sajnos a TLD jelenlegi
29
formájában sem az elemek kötelező sorrendjére, sem az egymásbaágyazás módjára vonatkozóan nem tartalmaz információkat, így az elemek helyes használatát minden esetben magunknak kell ellenőriznünk. Kissé triviális, de szemléletes példa gyanánt tekintsünk egy, a programozási nyelvek elágazásait kiváltó elemet, amit a következőképpen szeretnénk használni:
<%= kifejezes %> Ez a szoveg kerul az oldalba, ha a kifejezes igaz Ez a szoveg kerul az oldalba, ha hamis
Ez a Marty Halltól származó példa tehát négy, attribútum nélküli elemet igényel. (Lehetne a feltétel, vagy akár mind a három belső elem a külső if egy-egy attribútuma.) Ez négy különböző elemkezelő osztály megírását jelenti, cserébe viszont jól átlátható szintaxist kapunk. A Tag interface leírásánál volt szó arról, hogy minden elemkezelő megkapja a JSP oldaltól a "szülő" elem osztályának referenciáját, ahol szülőnek nevezik azt az osztályt, aminek a törzsében az elem található. A Support osztályok ezt a referenciát, ami lehet null is, eltárolják a parent osztályváltozóban. Innen már jól látszik, hogy az információátadás egyoldalú lehet csak, a gyerek meghívhatja a szülő metódusait, a szülő viszont általában nem tud a törzsében lévő elemekről. (Feldolgozhatja természetesen a törzsét karakterről karakterre, de ez igencsak körülményes lehet.) Arra sincs mód, hogy két gyerekelem –például a condition és a then – közvetlenül elérjék egymást, de a szülőn keresztül már megoszthatják az adataikat. A szülő megkeresésére a gyakorlatban a TagSupport osztály beszédes nevű findAncestorWithClass metódusa használatos, előfordulhat ugyanis, hogy további saját akciók ékelődnek a szülő és a gyerek közé. A findAncestorWithClass nem tesz mást, mint a parent értékéből kiindulva addig lépked felfelé a hierarchiában, amíg megadott osztályú metódust nem talál. Nézzük meg először a legkülső if elem osztályát: package tags; import java.io.*; import javax.servlet.*; import javax.servlet.jsp.*; import javax.servlet.jsp.tagext.*; public class IfTag extends TagSupport { private boolean condition; private boolean hasCondition = false; public void setCondition(boolean condition) { this.condition = condition; hasCondition = true; } public boolean getCondition() { return(condition); } public void setHasCondition(boolean flag) { this.hasCondition = flag; } //Beallitotta mar a beagyazott
elem a kifejezes erteket? public boolean hasCondition() { return(hasCondition); } public int doStartTag() { return(EVAL_BODY_INCLUDE); }
30
}
Bár látszólag nincs szerepe, az if elem szolgál a kifejezés eredményének (igaz vagy hamis) a tárolására, amit a condition elem állít be, és a then és az else elemek kérdeznek le. A hasCondition logikai változó mutatja, hogy be lett e már állítva a kifejezés eredménye, ezt szintén a then és else elemek használják. A condition elem kezelője már a BodyTagSupport leszármazottja, mert felhasználja a törzse tartalmát: package tags; import import import import
java.io.*; javax.servlet.*; javax.servlet.jsp.*; javax.servlet.jsp.tagext.*;
public class IfConditionTag extends BodyTagSupport { public int doStartTag() throws JspTagException { IfTag parent = (IfTag)findAncestorWithClass(this, IfTag.class); if (parent == null) { throw new JspTagException("condition not inside if"); } return(EVAL_BODY_TAG); } public int doAfterBody() { IfTag parent = (IfTag)findAncestorWithClass(this, IfTag.class); String bodyString = getBodyContent().getString(); if (bodyString.trim().equals("true")) { parent.setCondition(true); } else { parent.setCondition(false); } return(SKIP_BODY); } }
A doStartTag metódus rögtön a szülő osztályának megkeresésével kezdődik a már ismertetett findAncestorWithClass segítségével. (Mivel ez static metódus, így a keresett szülő osztálya mellett a saját objektumot is át kell adni.) Ha a szülőt nem találtuk, azaz a condition elem önmagában állt, akkor ezt a hibát egy JSPTagException dobásával jelezzük. A doAfterBody metódusban hasonló keresés után átadjuk a szülőnek a törzs kiértékelésének eredményét. A then és az else elemek kezelője teljesen hasonló, így elég az egyikkel megismerkednünk: package tags; import import import import
java.io.*; javax.servlet.*; javax.servlet.jsp.*; javax.servlet.jsp.tagext.*;
public class IfThenTag extends BodyTagSupport {
31
public int doStartTag() throws JspTagException { IfTag parent = (IfTag)findAncestorWithClass(this, IfTag.class); if (parent == null) { throw new JspTagException("then not inside if"); } else if (!parent.hasCondition()) { String warning = "condition tag must come before then tag"; throw new JspTagException(warning); } return(EVAL_BODY_TAG); } public int doAfterBody() { IfTag parent = (IfTag)findAncestorWithClass(this, IfTag.class); if (parent.getCondition()) { try { BodyContent body = getBodyContent(); JspWriter out = body.getEnclosingWriter(); out.print(body.getString()); } catch(IOException ioe) { System.out.println("Error in IfThenTag: " + ioe); } } return(SKIP_BODY); } }
Ez az osztály már nem sok újdonságot tartogat számunkra. A doStartTag annyival bővebb az IfConditionTag osztály hasonló metódusánál, hogy itt már a condition elem meglétét is ellenőrizzük. A doAfterBody eljárás a feltétel igaz értéke esetén a megszokott módon kiírja a törzs tartalmát a kimenetre, különben pedig nem csinál semmit. Legújabb szerzeményünkkel igazán elegáns megoldást adhatunk a napszaktól függő köszöntés problémájára, amint azt az alábbi JSP oldal demonstrálja: <%@ page language="java" import="java.util.Calendar" %> <%@ taglib uri="http://valerie.inf.elte.hu/~pmika/taglibs/tutorial-1.0" prefix="tutorial" %> Tag library pelda 5 <%= Calendar.getInstance().get(Calendar.AM_PM) == Calendar.AM %> Kellemes delelottot! Kellemes delutant!
32