A következő címkéjű bejegyzések mutatása: Portlet. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: Portlet. Összes bejegyzés megjelenítése

2011. december 12., hétfő

Liferay Service Builder, a sötét oldal

Korábban már volt szó a Liferay Service Builderéről, akkor inkább a tudásáról esett szó, most pedig az árny oldalát szeretném taglalni. A Service Builder feladata, hogy egy egységes perzisztens réteget biztosítson anélkül, hogy a különböző WAR-ok eltérő kontextusaival, vagy az eltérő adatbázisok problémájával kellene bajlódnunk. A kezdetben kézenfekvő megoldások mára sajnos nem nyújtják azt a kényelmet, amit egy JPA/JTA-hoz szokott fejlesztő elvár egy efféle megoldástól. Lássuk mik azok a pontok, amiket én személy szerint fájlalok:

  • Az entitásokat leíró XML-ben csak primitív, String, vagy Date típust jelölhetünk meg az adatok tárolására, viszont osztály szinten a primitív adattagok inicializálódnak, tehát nem tudunk olyan mezőt felvenni, amelynek az értéke üres, vagyis NULL. Képzeljük el a User entitásunkat, egy opcionális életkor mezővel. Mivel az életkor alapértelmezetten 0 értéket vesz fel, nem tudjuk megkülönböztetni a csecsemő felhasználóinkat azoktól, akik nem töltötték ki az életkorukat. A példa légből kapott, de jól szemlélteti a primitív típusok hiányosságát. A Liferay készített egy workaroundot a probléma orvoslására, és használhatunk wrapper osztályokat is a primitívek helyett, viszont ebben az esetben szembesülnünk kell két dologgal, mégpedig, hogy a DTD miatt kedvenc IDE-nk figyelmeztetni fog, hogy nem megfelelő adatot írtunk be, másrészt hivatalosan a Liferay nem támogatja ezt a megoldást.
  • Rengetegszer futottam bele, hogy a  String mezők hossza alapértelmezetten 75 karakter széles. Ezen a ponton lehet vitatkozni, hogy valahol meg kellett húzni a határt az adatbázis méretének és a programozói munkának az optimalizációja közben, de véleményem szerint a 75 karakter a legtöbb esetben nem elég. Lehetett volna egy kicsit a felesleges adatbázis helyfoglalás felé billenteni a mérleget. A programozók feledékenyek, a tesztelők meg hanyagok, és ebből majdnem egyenesen következik, hogy 100 String mezőből legalább 1-nél csak túl későn derül ki, hogy nem elegendő a 75 karakter. Természetesen van megoldás, mégpedig az src/META-INF könyvtárban lévő  portlet-model-hints.xml fájlban kell a mezőkre "tippeket" adni a Liferaynek. Persze itt nem csak a mező szélességére adhatunk hasznos tanácsokat, egyéb dolgokat is beállíthatunk, ezeket most nem részletezném.
    A megoldás árny oldala, hogy EE verzió esetén megcsinálja az adatbázis módosítását a rendszer, azonban az általam próbált CE verziók nem módosították, az adatbázist, és a módosítást végrehajtó SQL-t is magamnak kellett megírnom. Ezen a ponton bukott meg az adatbázis-függetlenség. Nem ennyire elkeserítő a helyzet, mert írhatunk olyan általános SQL-t, amit a Liferay is használ a Service Builder tábláinak legenerálásához, és létezik egy osztály a Liferayben, ami az általános SQL-ből adatbázisnak megfelelő SQL-t generál, amit egy startup hookba ágyazva futtathatunk, de azt hiszem ez túl nagy ár a függetlenségért.
  • A tranzakciók kezelése sem túl kifinomult szerintem a Liferayben. A Liferay filozófiája, hogy a tranzakciók kezelését teljesen a Spring AOP-re bízza, amely a Liferay szívében van konfigurálva. Az alapértelmezett működés, hogy az add*, check*, clear*, delete*, set*, és update* karakter-lánccal kezdődő metódusok egy tranzakciót indítanak. Ettől eltérően csak annyit tehetünk, hogy a service.xml-ben az entitásnál a tx-required mezővel megadhatjuk, hogy ezen felül milyen metódusok indítsanak tranzakciót. Nekem nagyon kényelmes és kézenfekvő, hogy szabadon válogathatok a 6 féle tranzakció-kezelés közül, és bármikor eldönthetem, hogy becsatlakozok-e a meglévő tranzakcióba, hogy indítok-e újat, stb. Megint csak egy légből kapott példa, banki tranzakciót indítok a rendszerben, és a folyamat közben szeretném loggolni, hogy pontosan mi történt a tranzakcióval, de sajnos a log szerver timeoutol/betelik a lemez/stb. Ebben az esetben a loggolást végző kódrészlet hibája miatt gördül vissza az egész tranzakció, holott csak egy harmadlagos funkció nem teljesült. Létezik a Liferayben EXT plugin, amivel felül lehet írni a Spring konfigot, de a 7-es verziótól ezek egyáltalán nem lesznek támogatottak, így én sem javaslom senkinek, hogy ezt az utat válassza.
  • A service réteg deleteAll metódusai a perzisztes rétegen keresztül egyesével törlik az entitásokat, ami egy 10M+ rekordszámú táblánál órákba is telhet. Ezt különösen azért nem értem, mert a Liferay nem kezel entitás-kapcsolatokat, tehát feleslegesnek érzem az adatbázisból egyesével kikérni a rekordokat, és törlést végrehajtani rajtuk. Szerencsére erre is van egy kiskapu, közvetlenül el tudunk kérni egy JDBC kapcsolatot, ahol szabadon garázdálkodhatunk:
    DataAccess.getConnection.createStatement().execute("delete from MY_Entity");
    Természetesen a kapcsolat élet ciklusáról magunknak kell gondoskodnunk.
  • Mint fentebb is írtam a Liferay nem igazán kezeli az entitás relációkat. Meg lehet ugyan adni kapcsolatokat, de a OneToOne kapcsolaton kívül, amit kezel a rendszer, minden kapcsolat OneToMany esetén  csak egy long típusú mező, ManyToOne esetén pedig egy üres Collection lesz. a OTO kapcsolatot az service.xml-ben a reference tag segítségével adhatjuk meg. Ennek célja egyébként a minél szélesebb adatbázis paletta támogatása, de szerény véleményem szerint ez akkor plusz terhet ró a fejlesztőkre, és annyi hiba lehetőséget, és adatbázis-szemetet eredményez, ami nem biztos, hogy megéri ezt az árat. További hátránya a szemléletnek, hogy így az entitások közötti kapcsolatot View oldalon kell lekezelni, onnan kell az adatbázishoz fordulni minden kapcsolat esetén. A Liferay egyébként MVC patternt követi, de vallja azt a nézetet, hogy lekérdezés esetén lehet közvetlenül a Model-hez fordulni, egyszerűsítve a logikát, így viszont egy standard Liferay portlet plugin jsp-je tele van scriptletekkel, olvashatatlanná téve a kódot. Ízlések és pofonok, én személy szerint szeretem, ha jól el vannak szeparálva a dolgok egymástól, a jsp szerkesztésekor az IDE tud hasznos tanácsokat adni, és nincs tele a Problems fül felesleges figyelmeztetésekkel, ha a Model önállóan végzi a dolgát, és nem a View fejlesztőnek kell gondolkodnia a különböző service hívásokról.
Röviden ennyi jutott most eszembe a Liferay Service Builderének árny oldalairól, remélem ezzel nem vettem el senki kedvét a használattól, nem ez volt a célom. Véleményem szerint ha viszonylag nem túl nagy, hordozható, és/vagy SOAP-os megoldásra van szükség, jó választás lehet, de mielőtt nagy projekteket építenénk erre a komponensre mindenféleképpen végezzünk kutatásokat, kísérletezzünk, hogy kielégíti-e üzleti igényeinket maximálisan.

2011. január 29., szombat

Liferay FriendyUrlMapping vs. IPC

Amikor először futottam bele a címben nevezett probléma-együttesbe, még azt gondoltam, hogy csak egyedi az eset, így a megolldást nem jegyeztem meg, és sajnos le sem. Viszont mikor másodjára is szembetalálkoztam vele, és újra végigjártam a szamárlétrát, gondoltam megkímélem az emberiséget, vagy legalább az olvasóimat ezen gyötrelmektől.
A portletek világában sajnos állandó problémát jelent a felhasználóbarát URL-ek kezelése. Ennek oka, hogy egy portletnek két fázisa létezik, az egyik a render-fázis, a másik pedig az action-fázis. Álltalános eset, hogy amíg egy portlet action-fázisban van, addig a többi render-fázisba. A böngészőből átvitt adatoknak a szerver felé tartalmazniuk kell valamiféle hivatkozást az adott portlet-példányról, hogy a portlet-konténer el tudja dönteni kinek is címezték a kérést. Paraméterben szokott szerepelni a portlet nézet beállítása (minimalizált, normál, maximalizált, exclusive, ez utóbbi liferay specifikus), valamint az életciklusra, és portlet módra (view|edit|etc) vonatkozó adat. Ennek eredménye valami ehhez hasonló URN: ?p_p_id=searchaction_WAR_fakeportletshoppingportlet&p_p_lifecycle=1&p_p_state=normal&p_p_mode=view&p_p_col_id=column-2&p_p_col_count=1&_searchaction_WAR_fakeportletshoppingportlet_javax.portlet.action=search. A probléma megoldására a Liferay többféle megoldást is kínál, de erről később. A probléma másik része, hogy a portlet-szabvány második verziója óta lehetőség van portletek közötti kommunikációra, röviden IPC-re. Az IPC lényege tömören, hogy az egyik portletünk kivált, publikál egy eseményt, míg egy vagy több másik portlet, amelyek előzőleg feliratkoztak az eseményre, elkapják azt. Az eseményben lehetőség van adatok átpasszíroázára is, így válik teljessé a kommunikáció. A dolog hátulütője, hogy kiváltani eseményt csak action fázisban lehet, és a kiváltott eseményt a másik portlet render fázisának egy bizonyos szakaszában képes elkapni, tehát érezhetően nem teljes a fejlesztői szabadság. Összegezve a problémát egy friendly url-en (továbbiakban FU) "figyelő" portletnek kell jeleznie állapotáról egy másik portletnek.

IPC kialakítása

A megvalósításhoz nincs más dolgunk, mint bejegyezni a portlet.xml-ben egy globális eseményleírást, ezzel rögzítve magát az eseményt.
<event-definition>
    <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname>
    <value-type>java.lang.String</value-type>
</event-definition>
Ezzel művelettel a http://jpattern.blogspot.hu/events névtéren bejegyeztünk egy ipc.myEvent eseményt.
Ezután mindkét portletünket szerepkörüknek megfelelően felvértezzük a kommunikációval.
<supported-publishing-event>
    <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname>
</supported-publishing-event>

<supported-processing-event>
    <qname xmlns:x="http://jpattern.blogspot.hu/events">x:ipc.myEvent</qname>
</supported-processing-event>
Az esemény publikálása az alábbi módon történik:
QName qName = new QName("http://jpattern.blogspot.hu/events", "ipc.myEvent");
response.setEvent(qName, "param"); //ActionResponse
Az esemény elkapásához portlet osztályunkban kell delegálnunk egy metódust:
@ProcessEvent(qname = "{http://jpattern.blogspot.hu/events}ipc.myEvent")
public void myEvent(EventRequest request, EventResponse response) {
    Event event = request.getEvent();
    String param = (String) event.getValue();
    response.setRenderParameter("param", param);
}
Ezek után a JSP-ben nincs más dolgunk mint kiszedni a requestből a paramétert:
renderRequest.getParameter("param");
Ennyi a varázslat, kipróbálásához házi feladat a portlet taglib actionUrl használata.

A FriendlyUrlMapping

Az említett több lehetőség közül nekem szimpatikusabb volt az XML szerkesztgetésnél, hogy létrehozok egy osztályt, amely beállítja a portletet az URL alapján. Az osztállyal szemben támasztott követelmény, hogy a com.liferay.portal.kernel.portlet.BaseFriendlyURLMapper osztály leszármazottja legyen.
public class MyFriendlyUrlMapper extends BaseFriendlyURLMapper {

private static final String MAPPER = "mappingstring"; //erre az URL darabra fog aktiválódni a mapper
 
@Override
public void populateParams(String friendlyURLPath,
        Map<String, String[]> params, Map<String, Object> requestContext) {
    addParameter(params, "p_p_id", getPortletId());
    addParameter(params, "p_p_lifecycle", "1"); //ez a paraméter kényszeríti action-fázisba a portletet
    addParameter(params, "p_p_mode", PortletMode.VIEW);
    addParameter(params, "p_p_state", WindowState.NORMAL);
    addParameter(getNamespace(), params, "javax.portlet.action", "actionMethod"); //Ez az action-metódus fog meghívódni
 }
 
@Override
public boolean isCheckMappingWithPrefix() {
    return false;
}
 
@Override
public String getMapping() {
    return MAPPER;
}
 
@Override
public String buildPath(LiferayPortletURL portletURL) {
    return null;
}

}
Miután elkészítettük az osztályt, be kell jegyeznünk a liferay-portlet.xml fájlban a megfelelő portletnél azt:
<friendly-url-mapper-class>com.blogger.jpattern.MyFriendlyUrlMapper</friendly-url-mapper-class>
Érdemes megjegyezni, hogy a FU kezelés a LR-ben úgy működik, hogy felveszünk egy oldalt, pl.: /oldal, amire kihelyezzük a portletet, és a LR /oldal/mappingstring URL esetén aktiválja a Mapper osztályt.
Amennyiben a portletünket render fázisban szeretnénk használni p_p_lifecycle=0, nincs más dolgunk, de mint említettem IPC esemény kiváltására csak action-fázisban van lehetőség. A LR-ben ki kell kapcsolnunk az "Auth token check"-et, ami annyit tesz, hogy minden action típusú kéréshez a LR generál egy egyedi azonosítót, és ennek hiányában nem enged hozzáférést az erőforráshoz. Nyilván FU használata esetén nem rendelkezünk ezzel az azonosítóval, ezért a portal-ext.properties fájlban vegyük fel a "auth.token.check.enabled=false" paramétert.

2010. június 24., csütörtök

Barátkozás a Liferay Portlettel, telepítés

Az utóbbi időben sajnos, vagy inkább szerencsére több időt szántam a tanulásra, mint az írásra. Egy munka-hely váltás kapcsán  találkoztam a Liferay Portletek téma-körével, és mivel hosszas küzdelem árán sikerült beüzemelni a dolgot, gondoltam talán másoknak is hasznos lehet, ha lejegyzem.
A Liferay egy Java alapú, nyílt forrás-kódú, "Enterprise" CMS rendszer, ami annyiba tér el más hasonló alkalmzásoktól, hogy a tartalmat Portletek segítségével állítja elő. A Portletek lényegét röviden úgy foglalhatnánk össze, hogy az egyes hagyományos értelembe vett modulok külön életet élnek, és egy HTML oldal legenerálása a szerveren nem a megszokott módon történik, hanem a Portlet konténer, a megjelenítendő HTML-t "össze-ollózza" a Portletektől, azaz a Portleteket megkéri, hogy az állapotuknak megfelelő HTML kimenetet biztosítsák számára. Portlet konténerből számtalan implementáció létezik, ki jobban, ki kevésbé tér el a specifikációtól, a Liferay fejlesztői úgy döntöttek, hogy saját megoldást dolgoznak ki, így született meg a Liferay Portlet. Számomra a megismerést nagyban nehezítette a név-választás, ami egyébként logikus, ugyanis a netet böngészve, linkre kattintva nehéz hirtelen eldönteni, hogy vajon a portálról, vagy a portletről van éppen szó.
  • A tanulás/fejlesztés első lépése, hogy letöltjük a conténert. Bár széles palettán válogathatunk a társított szerverek terén - Tomcat, JBoss, Glassfish...-, mégiscsak a Liferay fejlesztők száj-íze, és verziói szerint kell dolgoznunk. Személy szerint nekem nem szimpatikus, hogy csak egyben tudjuk beszerezni a web-konténert, a Portlet-konténert, és ráadásként a Liferay Portalt, találkoztam olyan Portlet-konténerrel, amely meglévő alkalmazás-szerverünket frissítette, meghagyva a választás szabadságát, illetve futó alkalmazásainkat.
  • Kicsomagolás közben bőven lesz időnk tanulmányozni a dokumentációt.
  • Miután elindítottuk a szervert azonmód elérhető a Lifery Portal rendszer (jó esetben a http://localhost:8080 címen), amire Portlet fejlesztőként egyelőre nem lesz szükségünk.
  • A dokumentáció szerint függőségként telepítenünk kell az Apache Antot (Mavennel is működésre lehet bírni, de ezt nem próbáltam), valamint a Jikes fordítót.
  • Az első Portletünk létrehozását érdemes a Liferay Plugins SDKn keresztül elvégezni, amely számtalan mintával könnyíti meg a fejlsztést.
  • Az SDK gyökér-könyvtárában található build.properties állományban állítsuk be a kívánt web-konténert, a megfelelő elérésekkel. Amennyiben több felhasználó is használja ugyanazt az SDK-t, a buil.properties mellé hozzunk létre felhasználónként egy-egy build.${username}.properties fájlt, amelyben perszonalizálni tudjuk a beállításokat.
  • Következő lépésként az SDK portlets könyvtárában hozzunk létre egy hello-world projektet.
    ./create.sh hello-world "Hello World"
    Buildfile: build.xml
    
    create:
        [unzip] Expanding: /media/data/code/liferay/sdk/portlets/portlet.zip into /media/data/code/liferay/sdk/portlets/hello-world-portlet
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/tld
         [copy] Copying 6 files to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/tld
    
    BUILD SUCCESSFUL
    Total time: 1 second
    
  • A script által létrehozott hello-world-portlet könyvtárba lépve vegyük rá az Antot, hogy készítsen egy buildet.
    ant deploy
    Buildfile: build.xml
    
    compile:
    
    merge:
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/classes
        [mkdir] Created dir: /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/lib
         [copy] Copying 5 files to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/lib
        [javac] Compiling 1 source file to /media/data/code/liferay/sdk/portlets/hello-world-portlet/docroot/WEB-INF/classes
    
    merge:
    
    war:
    
    clean-portal-dependencies:
          [zip] Building zip: /media/data/code/liferay/sdk/dist/hello-world-portlet-5.2.3.1.war
    
    deploy:
         [copy] Copying 1 file to /media/data/code/liferay/deploy
    
    BUILD SUCCESSFUL
    Total time: 4 seconds
    
  • Utolsó simításként annyi van hátra, hogy Bruno (Admin) nevében jelentkezzünk be az alapértelmezett Portal oldalon (email: bruno@7cogs.com, password: bruno, de szerencsére kattintós módszer is van), és a Welcome -> Add Application menüpont alatt adjuk hozzáadjuk a "Hello World"-öt az oldalhoz (érdemes rákeresni).
Szeretném megjegyezni, hogy eléggé sok fejfájást okozott az, hogy eleinte a megszokott módon szerettem volna az alkalmazás-fejlesztést vezérelni az IDE-ből. Ennek érdekében először a Liferay Eclipse Pluginjével próbálkoztam (elég sokat), amit végül nem sikerült rávenni, hogy tegye a dolgát. Ezután megpróbáltam olyan projektet létrehozni az IDE-ben, ami kapcsolatban áll a web-konténerrel, és vezérli a szinkronizációt és magát a szervert (itt is próbálkoztam pár kombinációval hátha csak én vagyok a hüje), de sajnos ez sem vezetett eredményre. Az járható út egyelőre, hogy az IDE-ből fejlesztés közben/után WAR fájlba csomagolva a konténer auto-deploy könyvtárába exportáljuk a produktumot, és a konzolból indított szerver pedig teszi a dolgát. Szerény véleményem, hogy ez eléggé fa-pados, és időigényes módszer, hiszen a konténer az egész alkalmazást újra húzza, és nem csak a szükséges részt frissíti. Szerk.: A Liferay Eclipse Plugin sajnos csak 6-os verziónál >= Liferayyel kompatibilis, így csak az új fejlesztések tehetők kényelmesebbé vele. Régebbi verzió esetén eléggé fapados sajnos a fejlesztés.
Pozitívumként tapasztaltam, hogy létezik Magyar Liferay közösség, továbbá egy hazai Liferay specialista az I-Logic képében, és az elengedhetetlen Facebook csoport.