2010. április 19., hétfő

GWT-s portlet fejlesztése Development Mode-ban

A Google Web Toolkit a legjobb javascript framework, amivel eddig dolgoztam, egyszerű, gyors, és alapvetően java. Jó lenne, ha ezt fel lehetne használni portletekben is. Mivel a GWT egyszerű javascripteket készít, ezért elvileg nincs akadálya, le kell fordítani a forrást, és a portletben egy egyszerű <script> tag-ben hivatkozni rá. A 2.0-s verziótól a GWT-ben megjelent a Development Mode mint böngésző plugin, ami azt ígéri, hogy újrafordítás nélkül lehet deployolni, és debuggolható a java kód. Ha ez portletes környezetben is belőhető, akkor az nagyon gyors fejlesztést tesz lehetővé egy nagyon kényelmes eszközzel.

Először fel kell tenni az Eclipse-hez a GWT-s plugint, ehhez fel kell vennünk ezt az Update Site-ot:
http://dl.google.com/eclipse/plugin/3.5
Innen fel kell telepíteni a GWT-hez kapcsolódó plugineket (2db), majd újraindítani az Eclipse-t. Ezután valamilyen portletre van szükségünk, aminek a javascriptjét meg akarjuk írni, próbaképpen generáltathatunk egy Hello World-öt a Liferay Plugins SDK-val. Ha a portlet nincs felvéve a Workspace-be, akkor tegyük meg.

Ezután meg kell mondanunk a pluginnek, hogy ez egy GWT-s project. Ehhez jobbklikk a projectre->Properties->Google/Web Toolkit/Use Web Toolkit-et pipáljuk be, majd Web Application alatt a This project has a WAR directory-t is, és a WAR directory-hoz írjuk be azt a könyvtárat, ami a portletben direkt elérhető (tipikusan a docroot). A fejlesztéshez szükséges egy GWT modul, ezt jobbklikk->New->Module ponton tehetjük meg, valamint a modulhoz szükséges egy Entry Point, ezt a jobbklikk->New->Entry Point pont alatt. Ezután fordítsuk le a projectet, ezt a GWT Compile Project gombbal tudjuk a legegyszerűbben megtenni a menüsorból.

Fel kell vennünk még egy Run configuration-t. Hozzunk létre egy Web Application-ös configot, majd értelemszerűen töltsük ki a Main fület, ha nem lenne kitöltve magától, a Main-Class legyen com.google.gwt.dev.DevMode! Itt jön egy kis trükközés.

Ha nincs feltelepítve a böngészőplugin, akkor a legegyszerűbben úgy tehetjük meg, hogy a Server fülön bepipáljuk a Run built-in server-t, majd elindítjuk, és ellátogatunk a megjelenő url-re. Itt fel fogja ajánlani a böngésző a plugin feltelepítését, amit tegyünk is meg. Amennyiben már fentvan korábbról, ezt a bekezdést kihagyhatjuk.

Amennyiben már van böngésző plugin telepítve, akkor a Server fülön állítsuk be, hogy ne indítsa el, majd a GWT fülön az URL-hez írjuk be a tesztelni kívánt címet (tipikusan azt az oldalt, ahol a portlet van). Ezután indítsuk el, és hozzuk be a böngészőben a kiírt url-t. Ha ezután változtatunk valamit a java kódban, mentés és újratöltés után egyből látszanak a módosítások, valamint debuggolni is lehet.

Sajnos Linux alatt nem műküdik, annyit találtam róla, hogy az Eclipse-s Jdt-vel akad össze. Majd a Heliossal megnézem újra a nyáron, hátha addigra már megcsinálják.

2010. március 5., péntek

Liferay portlet fejlesztése SDK és ServiceBuilder segítségével

A téma bemutatásához egy minta portletet fogunk elkészíteni. Ez egy egyszerű üzenőfal lesz, ami listázza az eddig beírt üzeneteket, és lehet újat is felvenni.

Első teendő az SDK beszerzése, ezt az alábbi utasítással tehejük meg:
svn co svn://svn.liferay.com/repos/public/plugins/trunk  --username guest
A jelszó üres. Ezzel letöltöttük a legfrissebb forrást. Mielőtt használatba vennénk, szükséges beállítani az eléréseket. Ehhez hozzuk létre a gyökerébe egy build.{username}.properties fájlt, az alábbi tartalommal:
app.server.dir=${user.home}/workspace/portal/bundles/tomcat-6.0.24
Értelemszerűen átírva, hogy egyezzen a használt rendszerrel. Ezután a portlet könyvtárban kiadva az alábbi parancsot, létrejön a portletünk váza:
./create.sh messagewall "Message Wall"
Ki is próbálhatjuk, ha a portlet mappájában buildelünk ant-al, akkor lefordítja, összepakolja és bemásolja a beállított deploy mappába, tehát azonnal használható is lesz.

Szükségünk lesz egy service.xml fájlra, ez fogja leírni az adatbázis modelljét, amiből a ServiceBuilder le fogja generálni a szükséges fájlokat. Tehát a WEB-INF-be hozzunk létre egy service.xml fájlt, az alábbi tartalommal:

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_0_0.dtd">

<service-builder package-path="hu.example.messagewall">
<namespace>MessageWall</namespace>
<entity name="Message" local-service="true" remote-service="false">

<!-- PK fields -->

<column name="messageId" type="long" primary="true" />

<!-- Audit fields -->

<column name="message" type="String" />

<!-- Order -->

<order by="desc">
<order-column name="message" />
</order>
</entity>
</service-builder>
Az osztályok legenerálásához az alábbi utasítás használható:
ant build-service
Ez nem történik meg általános buildelésnél, de csak akkor van rá szükség, ha a service.xml vagy az Impl oszályok változnak.

Mivel szeretnénk Eclipse-el fejleszteni, ezért létre kell hoznunk egy projectet neki. Ehhez indítsunk egy Dynamic Web Project-et, írjuk be a nevét, töröljük a Project Contents pontot, majd válasszuk ki a portlet mappáját. Válasszunk Runtime-t, majd Next. Az src legyen docroot\WEB-INF\src, a build pedig docroot\WEB-INF\classes, majd Next. Itt a webcontent-et írjuk át docroot-ra, majd finish. A path errorok megoldására adjuk hozzá,hogy függjön a portál projecttől, majd a view.jsp-ben a taglib URI végéről a 2_0-t szedjük le, tehát így nézzen ki a sor:
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
Végül adjuk hozzá a service mappát forrásként, ezzel megszűnt az összes error. Ezután fel kell venni egy új üzenetet létrehozó metódust a service-be. Ehhez írjuk át a MessageLocalServiceImpl osztályt:
public class MessageLocalServiceImpl extends MessageLocalServiceBaseImpl  {
public void createMessage(String msg) {
try {
Message m = createMessage(counterLocalService.increment());
m.setMessage(msg);
updateMessage(m);
} catch (SystemException e) {
e.printStackTrace();
}

}
}
Ezután generáljuk le újra a service osztályokat. Végül fel kell vennünk egy portletet, ami majd az action-t kezeli:
public class MessageWallPortlet extends MVCPortlet {
@Override
public void processAction(ActionRequest actionRequest, ActionResponse actionResponse) throws IOException, PortletException {
MessageLocalServiceUtil.createMessage(ParamUtil.getString(actionRequest, "newMessage"));
}
}
Majd meg kell mondanunk, hogy ezt a portletet használja, ehhez a portet.xml-t írjuk át:
<portlet-class>hu.example.messagewall.portlet.MessageWallPortlet</portlet-class>
Ezután meg kell még írnunk a megjelnítést, ez a view.jsp-be kerül:
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>


<%@page import="java.util.List"%>
<%@page import="hu.example.messagewall.model.Message"%>
<%@page import="hu.example.messagewall.service.persistence.MessageUtil"%>
<%@page import="hu.example.messagewall.service.impl.MessageLocalServiceImpl"%>
<%@page import="hu.example.messagewall.service.MessageLocalServiceUtil"%><portlet:defineObjects />

This is the <b>Message Wall2</b>.
Messages:
<%

List<Message> messages=MessageLocalServiceUtil.getMessages(-1,-1);
for(Message m:messages){
%>
<p><%=m.getMessage() %></p>
<%
}
%>

Create new message:
<form action="<portlet:actionURL/>" method="post">
Message:<input type="text" name="<portlet:namespace/>newMessage"/><input type="submit"/>
</form>
Ezzel készen is vagyunk, lefordítva, kideployolva már használható is az üzenőfal.

Kiegészítés:


Eclipse project létrehozásához használhatjuk az ant setup-eclipse utasítást, ezután importálhatjuk a workspace-be.

2010. március 3., szerda

Liferay portál fejlesztése

Előkerülhet az igény, hogy magát a portált fejlesszük, és nem csak témákat vagy portleteket hozzá. Mivel nyílt forrású programról van szó, ezért nem áll utunkba semmi a cél eléréséhez. Ehhez először is be kell szereznünk a forrást. Ezt az SVN tárolókból tudjuk megtenni, a következő utasítással:
svn co svn://svn.liferay.com/repos/public/portal/trunk --username guest
A kért jelszót hagyjuk üresen, és máris jön lefele a forrás. Ez eltart egy darabig, mivel elég nagy kódbázisról van szó. Ezután buildeljük le:
ant all
Ez létrehozza a szükséges könyvtárakat. Töltsünk egy Tomcat 6-ot, és tegyük be a most létrejött bundles/tomcat mappába. Telepítéshez újra buildeljünk:
ant all
Ezután ha elindíthatjuk, felállt a portál. Fontos, hogy minden buildnél le legyen állítva a Tomcat! Ha Exceptionökkel indul, mert nem sikerült a Lucene-nek lock-ot szereznie, akkor leállítás után töröljük le a data/lucene/0/write.lock fájlt!

Mivel szeretnénk IDE-vel fejleszteni, ezért Eclipse-ben importáljuk be a projectet, majd vegyük fela szerverek közé a Tomcat-et. Ezután duplaklikk a szerverre és áálítsuk át a Server location-t a 2. választási lehetőségre, és a Deploy path-ot webapps-ra! Ezután a run configuration-t megnyitva adjuk hozzá az argumentumlistához a -Dexternal-properties=portal-developer.properties -t!

Voilá, készen is vagyunk, van egy fejlesztőkörnyezetünk, amivel a Liferayt tudjuk fejleszteni.

2010. január 25., hétfő

Webalkalmazások teljesítménytesztje

Háttér

Nagy terhelés alatt hajlamosabbak a hibák előjönni, ezért is szoktak összeomlani az előre beharangozott rendszerek az élesítéskor. Emiatt viszont fejlesztés közben is érdemes időnként egy kicsit megküldeni az alkalmazást, hogy lássuk, milyen teljesítményre képes, valamint egyes esetekben kiderülhetnek a szűk keresztmetszetek is.

Webalkalmazásoknál a sebesség csak másodlagos fontosságú. Míg desktopon a 0.2 másodpercnél további választ tekinthetjük lassúnak, a web sokkal lomhább, emiatt a felhasználók is türelmesebbek. Még ha nagyon gyorsan állítjuk is elő az oldalt, a hálózati késleltetés, utána pedig a rendering bőven efölé viszi a tapasztalt válaszidőket. Persze a végtelenségig nem lehet a felhasználót várakoztatni, de néhány másodperc még belefér. Sokkal fontosabb viszont az áteresztőképesség, tehát sok párhuzamos lekérés esetén hányat tud kiszolgálni másodpercenként.

Érdekes kérdés, hogy ebből mennyi is az elég, vagy legalábbis az elvárható, ha egy saját alkalmazást tesztelünk. Persze annyi, amennyi a majdani látogatókat ki tudja szolgálni. 'Educated guess' alapján úgy gondolom, hogy egy jól belőtt, megfelelő cachelést alkalmazó és egy átlagos extranet bonyolultságát felmutató oldalból 50 körül lehet kihozni, de 20-30 még jónak számít. Természetesen ez sokban függ a tesztelendő gép hardverétől is, ezek csak irányszámok.

A tesztelés

A teszteléshez JMeter-t használok, ezzel grafikusan lehet összehúzogatni a teszt lépéseit, valamint az eredményeket is táblázatok és/vagy grafikonok segítségével tudjuk megnézni. A tesztek lelke a Thread Group, itt állíthatjuk be, hogy hány párhuzamos szálon és szálanként hányszor tesztelünk. Ennek a gyerekeként HTTP Request-eket tegyünk le, és állítsuk be a tesztelt URL-eket. Érdemes Assertion-öket adni a kérésekhez, hogy valamilyen szinten a válasz helyességét is meg tudjuk ítélni. Legegyszerűbb a Size Assertion, ezzel már ki tudjuk szűrni, ha mondjuk a terhelésre üres oldalt vagy rövid hibaüzenetet kapnánk vissza. Természetesen nem érdemes a terheléstesztet összetéveszteni az funkcionális teszteléssel, itt feltételezzük, hogy maguk a funkciók működnek.

Érdemes Controller-be venni fel a kéréseket, pl. egy Random Controller-be. Ezzel a benne lévő lekérések közül az egyik fog csak végrehajtódni, de több szál esetén ezzel hatékonyan tudjuk szimulálni azt, hogy párhuzamosan több oldalt is látogatnak. Végül egy Assertion Results-ot és egy Aggregate Report-ot tegyünk le, ezeket figyelve majd láthatjuk az eredményeket, valamint, hogy bukott-e meg az ellenőrzésen valamelyik válasz. Ezután Start, majd idővel megkapjuk az eredményeket. Futás közben érdemes figyelni a rendszer memóriahasználatát, az egyes folyamatok processzorhasználatát, valamint az I/O-t is. Ezzel ki tudjuk szűrni, ha pl. adatbázishoz túl sokszor fordul, vagy ír a lemezre.

A Glassfish minden felhasználóhoz rendel egy session-t, melyekből tesztelés alatt túl sok is létrejöhet, így ronthatja a mért teljesítményt. Ennek elkerülésére maximalizáljuk a számukat, vagy az elévülési idejüket vegyük le.

Ez a tesztelés nem tartalmaz bejelentkezést. Ehhez fel kell vennünk egy Cookie Manager-t, valamint szimulálnunk kell a belépést, valamint a későbbi felhasználói akciókat. A POST-okat kinyerhetjük a böngészőből pl. Firebug-gal, így kényelmesen felépíthetünk bonyolultabb bejárásokat is.

2010. január 23., szombat

Cachelés

Szabvány szerinti portlet cache

A portlet szabvány szerint alapvetően kétféleképpen lehet portletet cachelni. Egyrészt expiration time alapján, másrészt ETag alapon. Mindkettő nagyjából megegyezik a HTTP-ben is alkalmazott módokkal. Egy kicsit az előbbire azért kitérek, mert a kiejtése speciális módon is történhet.

Az alapgondolat az, hogy a portlet egy önálló webalkalmazás, mely kezeli a saját megjelenítését, valamint a hozzá tartozó felhasználói akciókat. Ennek alapján cacheljük el a portletet, és majd csak akkor ejtsük ki, ha történt valami akció (vagy ha lejárt az expiration timer, persze). Probléma abból lehet, hogy ha más portlet is meg tudja változtatni az állapotát egynek, akkor a másik felé történt akcióra is ki kell ejteni. Erre az esemény-alapú IPC nyújt megoldást, mellyel lazán kapcsolhatjuk az összetartozó portleteket.

Fontos még a cachelendő portlet hatálya. Ez lehet publikus vagy privát. Előbbi esetben minden felhasználónak ugyanúgy jelenik meg, így csak egyetlen példányt elég tárolni, utóbbiban azonban minden sessionhöz kell egyet-egyet.

Ez az egyszerű megoldás sok esetben akár elégséges is lehet, így kevés fejfájással sokat tudunk dobni a portál teljesítményén, és ha figyelünk az eseményekre, akkor a cachehibákat is elég jól el tudjuk kerülni. A Liferay egyik sajátossága, hogy jelenleg a vendég felhasználóknak nem gyorsítótáraz. Nem tudom, hogy ez miért jó, de jelenleg így van.

Liferay cachelési megoldása

A Liferay lehetőséget ad a portál cache használatára, melyet magától karbantart, és a Vezérlőpultból lehet is törölgetni, valamint elvileg a fürtözést is támogatja. A használata egyszerű, mint a faék. Vannak cachek, melyekbe betehetünk és kivehetünk objektumokat, kulcs alapján. Az eléréséhez a MultiVMPoolUtil-t tudjuk használni, ennek a getCache(név) metódusával tudunk cache-t elkérni, és a get/put-al az értékeket kezelni. Használhatjuk a SingleVMPoolUtil-t is ugyanígy, a kettő között elvileg a fürtözés a különbség, de ezt még nem próbáltam ki. Az egyes cacheknek a tulajdonságait az ehcache beállító xml-jeiben tudjuk kezelni, itt megadhatunk maximális méretet is a táraknak.

Ez egy elég egyszerű megoldás, amire végülis bármilyen bonyolult egyedi gyorsítótárazást rá tudunk építeni. Mielőtt azonban nekiállnánk valami nagyon általánosnak, érdemes figyelembe venni, hogy JPA szinten az adatbázis entitások már cachelve vannak, ezért egyszerű portleteknél nem biztos, hogy szükség van további gyorsítótárazásra. Először érdemes kimérni, hogy valóban bottleneck-e az adott portlet megjelenítése, és csak utána optimalizálni. Ahogy a mondás tartja: Premature optimalization is the root of all evil.

2010. január 22., péntek

Liferay portlet fejlesztése

Már régóta szemezgettem a Liferay portállal, többször is nekiálltam megismerkedni vele, de most jött el az ideje, hogy egy egyszerű, de a legtöbbször szükséges technológiákat használó portletet fejlesszek. Hétfőn kezdtem, ma péntek van, de tegnapra már készenvolt, ez 4 nap. Ennyi idő volt szükséges ahhoz, hogy minden szívást és kezdeti lépést kiismerjek és megoldjak, így a következőt már fél óra alatt is sikerülne elkészíteni.

A portlet egyszerű adatbáziskezelést valósít meg, elneveztem EmotionMeterPortletnek. A funkcionalitási kimerül abban, hogy egy 1-10 közé eső értéket megmutat, és ad 2 gombot, amelyekkel le vagy felfelé piszkálhatjuk, az aktuális értéket pedig adatbázisba teszi.

Telepítések

Szükségünk lesz egy Glassfish-re, Eclipse-re, valamint a Liferay-re, meg egy választott adatbáziskezelőre (esetemben ez MySql, de lehetne bármilyen más relációs is).
Glassfish telepítése: Töltsük le a v3 Web profilt (tölthetjük a teljeset is), telepítsük fel valahova, az alaéprtelmezett beállítások általában jók, én a java elérési utat állítottam csak át.
Eclipse telepítése: Töltsünk egy EE-s Eclipse-t, opcionálisan tegyük fel a Portal Pack-ot (
https://eclipse-portalpack.dev.java.net/install.html), valamint a Glassfish adaptert. Ubuntu-n az Eclipse fagyások elkerülésére ki kellett kapcsolni az Assistive Technologiest(+relogin).
Liferay telepítése: Szükségünk lesz a liferay war-os változatára, valamint a dependenciákra. Az utóbbit bontsuk ki a Glassfish lib-jébe, az előbbit meg majd telepítsük az AS-be.
Egyebek telepítése: Szükségünk lesz egy XML parserre, ehhez töltsük le a Xerxes-t (http://www.apache.org/dist/xerces/j/), é saf az xml-apis, xerxesImpl, serializer és resolver jarokat tegyük a lib-be. Szükséges még egy JPA provider telepítése is, esetemben ez az EclipseLink lesz. Töltsük le, és az eclipselink és jpa2-es jarokat tegyük a lib-be. Végül az adatbázisunknak megfelelő connector-t is tegyük be a többi jar közé.

Beállítások

Érdemes a fejlesztéshez a Liferay development módot bekapcsolni. Ehhez a domain.xml-be kell egy új jvm opciót felvenni, tehát szúrjuk be az alábbi sort a megfelelő helyre:
-Dexternal-properties=portal-developer.properties
Ezután indítsuk el az AS-t, telepítsük a liferay.war-t, állítsuk be alapértelmezett web modulnak a virtuális szerveren, valamint vegyünk fel egy JDBC Connection Pool-t és Resource-t. Én ezt jdbc/emotion név alá tettem.

Portlet fejlesztése

Hozzunk létre egy új projectet Eclipse-ben, ha fent van a Portal Pack, akkor a Dynamic Web Projectnél válasszük ki a Portlet 2.0-ás konfigurációt, ekkor létre gof hozni néhány szükséges fájlt magától.

Összesen 8 fájlt kell megírnunk.
WebContent/WEB-INF/portlet.xml: Ide kerülnek a portletet leíró beállítások.
<?xml version='1.0' encoding='UTF-8' ?>
<portlet-app xmlns='http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd'
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:schemaLocation='http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd'
version='2.0'>
<portlet>
<description>EmotionMeterPortlet</description>
<portlet-name>EmotionMeterPortlet</portlet-name>
<display-name>EmotionMeterPortlet</display-name>
<portlet-class>hu.test.BaseRedirectorPortlet</portlet-class>
<init-param>
<name>target-servlet</name>
<value>EmotionServlet</value>
</init-param>
<expiration-cache>0</expiration-cache>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>VIEW</portlet-mode>
<portlet-mode>EDIT</portlet-mode>
<portlet-mode>HELP</portlet-mode>
</supports>
<resource-bundle>hu.test.Language</resource-bundle>
<portlet-info>
<title>EmotionMeterPortlet</title>
<short-title>EmotionMeterPortlet</short-title>
</portlet-info>
<security-role-ref>
<role-name>administrator</role-name>
</security-role-ref>
<security-role-ref>
<role-name>guest</role-name>
</security-role-ref>
<security-role-ref>
<role-name>power-user</role-name>
</security-role-ref>
<security-role-ref>
<role-name>user</role-name>
</security-role-ref>
</portlet>
</portlet-app>
WebContent/WEB-INF/liferay-portlet.xml: Ide a Liferay egyedi beállításai kerülnek.

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 4.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_4_2_0.dtd">

<liferay-portlet-app>
<portlet>
<portlet-name>EmotionMeterPortlet</portlet-name>
<instanceable>true</instanceable>
</portlet>
<role-mapper>
<role-name>administrator</role-name>
<role-link>Administrator</role-link>
</role-mapper>

<role-mapper>
<role-name>guest</role-name>
<role-link>Guest</role-link>
</role-mapper>

<role-mapper>
<role-name>power-user</role-name>
<role-link>Power User</role-link>
</role-mapper>

<role-mapper>
<role-name>user</role-name>
<role-link>User</role-link>
</role-mapper>
</liferay-portlet-app>
WebContent/WEB-INF/liferay-display.xml: Ide a Liferay portlet megjelenítését szabályozó beállítások kerülnek:
<?xml version="1.0"?>
<!DOCTYPE display PUBLIC "-//Liferay//DTD Display 5.2.0//EN" "http://www.liferay.com/dtd/liferay-display_5_2_0.dtd">

<display>
<category name="category.emotion">
<portlet id="EmotionMeterPortlet" />
</category>
</display>
src/META-INF/persistence.xml: Magáért beszél, figyeljünk arra, hogy az src-n belül létre kell hoznunk ezt a könyvtárat.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="default" transaction-type="JTA">

<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<jta-data-source>jdbc/emotion</jta-data-source>

<properties>
<property name="eclipselink.ddl-generation" value="drop-and-create-tables" />

</properties>

</persistence-unit>
</persistence>
src/hu/test/Language.properties: A nyelvi beállítások vannak benne.
javax.portlet.title.EmotionMeterPortlet=EmotionMeter Portlet
category.emotion=Emotion

src/hu/test/BaseRedirectorPortlet.java: Ez egy általános portlet, mely egy megadott szervletre irányítja tovább a kérést. Mivel a portlet feldolgozása közben nincsen CDI (nem tudom, hogy miért, de nincsen), szervleteknél viszont van, ezért átirányítjuk a kérést, és utána dolgozzuk fel.
/** Servlet initparameter: target-servlet */
public class BaseRedirectorPortlet extends GenericPortlet {

public static final String parameterName = "target-servlet";

public static final String modeParameter = "BaseRedirector.mode";

public static enum modes {
VIEW, EDIT, HELP, ACTION
};

public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException {
String servletName = getInitParameter(parameterName);
response.setContentType("text/html");
if (request.getAttribute(modeParameter) == null) {
request.setAttribute(modeParameter, modes.VIEW);
}
getPortletContext().getRequestDispatcher("/" + servletName).include(request, response);
}

public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException {
request.setAttribute(modeParameter, modes.EDIT);
doView(request, response);
}

public void doHelp(RenderRequest request, RenderResponse response) throws PortletException, IOException {
request.setAttribute(modeParameter, modes.HELP);
doView(request, response);
}

public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException {
request.setAttribute(modeParameter, modes.ACTION);
String servletName = getInitParameter(parameterName);
getPortletContext().getRequestDispatcher("/" + servletName).include(request, response);
}
}
src/hu/test/EmotionServlet.java: Ide kerül a portlet logikája.
@WebServlet(urlPatterns = { "/EmotionServlet" })
public class EmotionServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

@PersistenceContext(unitName = "default")
EntityManager manager;

@EJB
EmotionEJB ejb;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String actionUrl = ((RenderResponse) request.getAttribute("javax.portlet.response")).createActionURL().toString();
try {
List<Emotion> ems = manager.createQuery(manager.getCriteriaBuilder().createQuery(Emotion.class)).getResultList();
int pos = 5;
if (ems.size() != 0) {
pos = ems.get(0).getPosition();
}
response.getWriter().write("<span><form method=\"POST\" action=\"" + actionUrl + "\"><input type=\"hidden\" name=\"vote\" value=\"minus\"><input type=\"submit\" value=\"-\"></form>TEST:" + pos + "<form method=\"POST\" action=\"" + actionUrl + "\"><input type=\"hidden\" name=\"vote\" value=\"plus\"><input type=\"submit\" value=\"+\"></form></span>");
} catch (Exception e) {
e.printStackTrace();
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
boolean up = true;
String param = request.getParameter("vote");
if (param == null) {
return;
}
if (param.compareTo("minus") == 0) {
up = false;
}
ejb.modifyEmotion(up);
}

}
src/hu/test/EmotionEJB.java: A CMP-hez szükséges ez az EJB.
@LocalBean
@Stateless
public class EmotionEJB {

@PersistenceContext(unitName = "default")
EntityManager manager;

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void modifyEmotion(boolean up) {
int lastPos = 5;
for (Emotion e : manager.createQuery(manager.getCriteriaBuilder().createQuery(Emotion.class)).getResultList()) {
lastPos = e.getPosition();
manager.remove(e);
}
Emotion e = new Emotion();
e.setPosition(Math.min(Math.max(lastPos + (up ? 1 : -1), 1), 10));
manager.persist(e);
}

}
src/hu/test/entities/Emotion.java: Az entitás.
@Entity
public class Emotion implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

private int position;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public int getPosition() {
return position;
}

public void setPosition(int position) {
this.position = position;
}

}
Portlet telepítése

A portlet elkészítése után szeretnénk kitelepíteni. Az Eclipse-s deploy megoldás sajnos nem alkalmazható, mivel az nem futtat le Liferay-specifikus eseményeket, ezért a Liferay autodeploy funkcióját fogjuk használni. Mivel kényelmetlen minden buildelés után másolgatni, ezért érdemes ezt automatizálni. Ebben segít ez a build.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="EmotionMeterPortlet" basedir=".">
<property name="warfile" value="EmotionMeterPortlet" />
<property name="deploy.dir" value="/home/sashee/glassfishv3/deploy"/>

<target name="create">
<war destfile="${warfile}.war" webxml="WebContent/WEB-INF/web.xml"
update="true" compress="false">
<classes dir="build\classes" />
<fileset dir="WebContent">
<exclude name="WEB-INF/web.xml" />
</fileset>
</war>
</target>

<target name="copy">
<copy todir="${deploy.dir}" overwrite="true">
<fileset dir=".">
<include name="*.war" />
</fileset>
</copy>
</target>

<target name="deploy">
<antcall target="create" />
<antcall target="copy" />
</target>
</project>
Természetesen az elérési utat írjuk át használat előtt. Eclipse-ben beállíthatjuk ezt a project builderei közé, és akkor minden mentés (vagy buildelés) után magától ki is fogja telepíteni a változtatásokat. Érdemes a Liferay-en belül a check interval-t levenni 5s-re, valamint Eclipse-ben az Allocate Console-t kivenni, hogy ne ugorjon el minden mentésnél.

Ezzel készen is vagyunk, van egy egyszerű portletünk, valamint be van lőve egy hatékony fejlesztői környezet is. Sajnos az exploded deploymentet nem tudjuk használni így, azonban a mentéstől számított kitelepítés 4-5 másodperc nálam, ez elfogadható. Bár azt nem próbáltam, de elvileg az állapot is megmarad.