2012. január 21., szombat

Weak és Soft referenciák a Javaban

Több éves Java tapasztalattal a hátam mögött hallottam először a gyenge (weak) referenciák létezéséről a nyelvben, és úgy gondoltam érdemel pár szót a téma kör. Javaslom, akinek nem világos a JVM szemétgyűjtőjének (továbbiakban GC) működése, ezen a ponton olvassa el Viczián István idevágó bejegyzését. Tehát mint tudjuk a GC kidob a memóriából minden olyan objektumot, melyre nem mutat már egyetlen referencia sem. Amikor létrehozunk egy objektumot, Integer foo = new Integer(0), akkor a foo egy strong reference lesz rá, és amíg az objektum "strongly reachable", addig a GC-nek tabu. Mivel ezeket az objektumokat nem tudja a GC felszabadítani, a memória beteltével jön a fránya OutOfMemotyError, és az alkalmazás kilép. Egy darabig persze lehet növelni a memóriát, majd a fizikai határok elérésével lehet elosztani az alkalmazást, de előbb álljunk meg egy szóra! Vajon minden objektumra szükségünk van a memóriában? Kézenfekvő megoldás, hogy bizonyos objektumokat azonnal megszüntessünk, amint nincs rájuk szükség, viszont az objektumok újbóli létrehozása is nagy költség, ebben az esetben pedig értékes processzoridőt fecsérelünk az állandó memóriaallokációra, példányosításra, stb. Arany középútként az 1.2-es, igen az 1.2-es Java verzióban bevezették a weak referenciákat, melynek implementációja a WeakReference osztály. A WeakReference referenciát tárol az adott objektumra, annyi különbséggel, hogy ezt az objektumot a GC első futáskor, szó nélkül eltávolítja a memóriából, felszabadítva ezzel a helyet mások számára.

WeakReference<StringBuilder> weakString = new WeakReference<StringBuilder>(new StringBuilder());

int i = 0;
while (weakString != null && weakString.get() != null) {
 weakString.get().append(i++);
 System.out.println(i);
}

System.out.println("Finish");

Kezdetként futási paraméterként állítsuk be a JVM-nek a maximális memóriát mondjuk 2 Mb-ra (-Xmx2m), majd az értékkel játszva láthatjuk, hogy mennyit változik programunk kimenete. Ez a technika kiválóan alkalmas memória gyorsítótárak készítésére, ám amennyiben kulcs-érték-pár alapú gyorsítótárat készítünk a new WeakReference(myMapInstance) helyett használjuk, az erre a célra készített WeakHashMap implementációt. A WeakHashMap a kulcsokat őrzi weak referenciával, és a MapEntry automatikusan eltávolításra kerül, amikor a kulcs már nincs rendszeres használat alatt.

A WeakReference mellett létezik a SoftReference osztály is, amely azt garantálja, hogy az OutOfMemoryError előtt felszámolásra kerülnek az objektumok, helyet biztosítva az újonan létrejövőknek.

SoftReference softObj = new SoftReference(new Serializable() {
 @Override public void finalize() throws Throwable {
  System.out.print("Finalize runned");

  super.finalize();
 } 
});

StringBuilder sb = new StringBuilder("foobar");
while (true) {
  sb.append(sb.toString());
}
A kimenetben láthatjuk, hogy miközben az alkalmazás akkut memóriahiányban elhalálozott, még utolsó leheletével felszámolta osztályunkat.
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Finalize runned at java.util.Arrays.copyOf(Arrays.java:2882)
 at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:100)
 at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:390)
 at java.lang.StringBuilder.append(StringBuilder.java:119)
 at com.blogspot.jpattern.Main.run(Main.java:36)
 at com.blogspot.jpattern.Main.main(Main.java:22)
Java Result: 1
Természetesen ennél complexebb program esetében az alkalmazás tovább tud dolgozni a felszabadult memóriával.A Java specifikáció szerint semmi garancia nincs a finalize() metódus futására!

Evezzünk egy kicsit sötétebb vizekre. Ha jól megnézzük a Reference API dokumentációját, láthatjuk, hogy van még egy ismert implementáció, a PhantomReference. Az első különbség társaihoz képest, hogy a bele helyezett objektumra soha nem tudunk referenciát kérni, ugyanis konstans null értékkel válaszol a get() hívására. Másik nagy eltérés lényege tömören, hogy az ilyen objektumokat a GC csak azelőtt rendezi sorba, mielőtt a fizikai memóriából kitakarítaná. Mivel úgy tudom Darth Vader nagyurat is ennek használata állította át az erő sötét oldalára, én magam nem merészkedtem ennél tovább (esetleg írhatnátok valami konkrét használati esetet).

Előfordulhat, hogy az alkalmazásunkban szükséges tudni, hogy mely objektumokat dobta már ki a GC, és melyeket nem. Ilyen esetben egy ReferenceQueue osztályt kell példányosítanunk, és a queue példányt átadni a WeakReference vagy SoftReference konstruktorának. A bejegyzésben említett referenciákkal óvatosan bánjunk, és csak alapos tervezés és tesztelést követően alkalmazzuk éles bevetésben őket.

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. október 30., vasárnap

OWASP AntiSamy Javaban

A webes biztonság kérdése egyidős magával az internettel, hiszen a hálózaton elérhető adatokat felhasználók milliói tekintik meg napról-napra. Mivel a weboldalak végeredményben a kliensek gépein futnak, hamar beláthatjuk, hogy a biztonsági kérdések nem elhanyagolhatóak, és mind a klienseknek, mind a weboldalak üzemeltetőinek fontos, hogy a kiszolgált tartalom hiteles és biztonságos legyen. Tipikus támadási forma az un. XSS, amikor pl. egy az oldalba ágyazott külső Javascript próbál szenzitív információkhoz hozzájutni a böngésző valamely biztonsági hibáját kihasználva. Az ilyen és ehhez hasonló problémák megoldására jött létre 2001-ben OWASP Antisamy Project, melynek célja nyílt forrású alternatívát, és szabványt kínálni az alkalmazások megvédésére.

Mint minden 3rd party fejlesztés, ez is úgy kezdődik, hogy letöltjük a programkönyvtár lehetőleg legfrissebb verzióját. AntiSamy jelenleg az 1.4.4-es verziónál tart. A dokumentációval ellentétbe a csomag függőségeit nem lehet letölteni, rákeresve a antisamy-required-libs.zip kulcsszavakra találtam egy oldalt. A készítők szerint deprecated ami a zip-be van, de a próba erejéig megteszi, éles használatra össze kell vadászni a függőségeket. A szoftver konfigurációja eléggé bonyolult, hosszas dokumentáció olvasással és tanulással egész biztos össze lehet dobni egy normális konfig XML-t, de szerencsére több nagyobb felhasználó is rendelkezésre bocsátotta saját összeállítását, így nyugodtan mazsolázgathatunk az eBay, MySpace, vagy akár Slashdot beállításaiból.

Az általam készített tesztprogram az alábbi (belecsempészve kis Java 7-et):

import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.owasp.validator.html.*;


public class AntiSamyTest {

    public static void main(String[] args) {
        try {
            URL dirtyHtmlUrl = AntiSamyTest.class.getResource("dirtyHtml.html");
            Path dirtyHtmlPath = FileSystems.getDefault().getPath(dirtyHtmlUrl.getFile());
            List<String> lines = Files.readAllLines(dirtyHtmlPath, Charset.defaultCharset());
   
            URL configXmlUrl = AntiSamyTest.class.getResource("antisamy-ebay-1.4.4.xml");
            Policy policy = Policy.getInstance(configXmlUrl.getFile());
            
            AntiSamy as = new AntiSamy();
            CleanResults cleanResult = as.scan(concatString(lines), policy);

            System.out.println(cleanResult.getCleanHTML());
        } catch (IOException | PolicyException | ScanException ex) {
            throw new RuntimeException(ex);
        }
    }
 
    private static String concatString(List<String> input) {
        StringBuilder output = new StringBuilder();
        for(String line : input) output.append(line);
        return output.toString();
    }
}
A dirtyHtml.html fájl tartalma szabvány HTML (HTML, HEAD, BODY). A programot futtatva láthatjuk, hogy a body tartalmán kívül minden HTML taget kidobott az AntiSamy. Azért mondom, hogy minden HTML taget, mert ha van pl. title a headbe, a tartalma bizony ottmarad, tehát csak a sallang kerül ki. Szerintem ez utóbbi működés konfigurálható (fixme).

Az elméleti ismerkedés után ideje valami komolyabb megbizatást adni Samykének. Személy szerint Liferay fejlesztő vagyok, így szinte evidens, hogy erre esett a választásom. A Liferay 6-os verziója óta létezik egy Sanitizers-nek nevezett funkcionalitás, amely, bár még nem teljeskörű, mégis segít az igényes programozónak, a kritikus user inputokat szűrni. A funkcionalitás mint írtam nem teljeskörű, ugyanis egyelőre csak a Blog bejegyzéseket tudjuk kontrollálni out-of-the-box. A dolgunk egyszerű, a portal.properties-ben van egy sanitizer.impl paraméter, amit a portal-ext.properties-ben felül tudunk definiálni. Az alapbeállítás a com.liferay.portal.sanitizer.DummySanitizerImpl osztályra mutat, ami jóformán semmit nem csinál, viszont jó kiindulási pont lehet saját Saniterünk elkészítéséhez. Létezik a Liferaynek beépített osztálya com.liferay.portal.kernel.sanitizer.SanitizerUtil képében, választhatjuk ezt is, de saját megoldást is minden további nélkül.

Miután Blog bejegyzéseinket megvédtük a sokszor óvatlan bloggerektől, sajnos nem dőlhetünk hátra nyugodtan, mivel a Liferayben is, mint minden CMS-ben, nem csak Blogot szerkesztenek a felhasználók, hanem számtalan egyéb módon is lehetőségük van HTML szöveget a rendszerbe juttatni. Mivel "gyári" támogatás még nincs ezen bejegyzésekre, nincs más lehetőségünk, mint hook-ot írni. A Plugin tárolóban van egy "antisamy hook", amely alapján könnyedén megírhatjuk saját kiterjesztésünket. A legjobb módszer un. Model Wrapper Hook készítése. Hozzunk létre egy hook-ot, majd a liferay-hook.xml fájlba írjuk be a felülírandó szervíz definícióját

<hook>
    <service>
        <service-type>com.liferay.portlet.wiki.service.WikiPageLocalService</service-type>
        <service-impl>com.test.hooks.SaniterWikiPageLocalService</service-impl>
    </service>
</hook>
Majd írjuk meg saját osztályjunkat
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.sanitizer.SanitizerUtil;
import com.liferay.portal.kernel.util.ContentTypes;
import com.liferay.portal.service.ServiceContext;
import com.liferay.portlet.wiki.model.WikiPage;
import com.liferay.portlet.wiki.service.WikiPageLocalService;
import com.liferay.portlet.wiki.service.WikiPageLocalServiceWrapper;

public class SaniterWikiPageLocalService extends WikiPageLocalServiceWrapper {

    public SaniterWikiPageLocalService(WikiPageLocalService wikiPageLocalService) {
        super(wikiPageLocalService);
    }
 
    public WikiPage addPage(
            long userId, long nodeId, String title, double version,
            String content, String summary, boolean minorEdit, String format,
            boolean head, String parentTitle, String redirectTitle,
            ServiceContext serviceContext)
            throws PortalException, SystemException {

        String sanitizedContent = SanitizerUtil.sanitize(
            serviceContext.getCompanyId(), serviceContext.getScopeGroupId(),
            userId, WikiPage.class.getName(), 0, ContentTypes.TEXT_HTML, content);

        return super.addPage(userId, nodeId, title, version,
            sanitizedContent, summary, minorEdit, format,
            head, parentTitle, redirectTitle,
            serviceContext);
    }
}
Bár én a Wiki-t választottam példának, ez alapján bármely más szervízre megírható a szűrés.

2011. szeptember 24., szombat

Liferay Portál OSGi bundle támogatással

Liferay és OSGi témák már többször is előkerültek, de most egy olyan egyedülálló kombináció bemutatását tűztem ki célul, mely egyesíti a két platform minden előnyét. A bemutatott módszer aktív fejlesztés alatt áll, éles használatát nem ajánlom senkinek. A fejlesztés Raymond Augé keze alatt történik, jelenleg a teljes kódbázis branch repóból érhető el. A cikk megírását az érdeklődés ihlette, és a remény, hogy másoknak is megtetszik a kezdeményezés, és programozók tucatjai állnak be Ray mögé, hogy minél előbb core feature legyen a dolog.

Első lépésként szükségünk lesz a módosított Liferay forrására, amit itt tudunk beszerezni. A letöltött forrást egyszerűen tömörítsük ki valahová. A fordításhoz egy Apache Antot is be kell üzemelnünk, ennek lépéseit nem részletezném. A kibontott Liferay gyökérében találunk egy app.server.properties állományt, melyben tudjuk konfigurálni, hogy milyen konténert szeretnénk használni. Amennyiben valami "különleges" igény nem szól közbe, én az Apache Tomcat verziót preferálom, ez egyébként az alapértelmezett. A properties fájlban 7.0.21-es verzió van beállítva, letöltés után a Liferay forrásunkkal párhuzamosan lévő bundles könyvtárba csomagoljuk is ki. Vagy az app.server.properties-ben írjuk át az útvonalat, vagy az apache-tomcat-7.0.21 könyvtárat nevezzük át tomcat-7.0.21-re.

A fordításhoz Antunkat is meg kell kicsit piszkálni. Először is az ecj.jar-t be kell másolni az Ant libjei közé (vagy szimlinkelni, vagy egyéb úton a classpath-ra tenni). Az ecj.jar beszerezhető a Plugin SDK lib könyvtárából. Be kell állítanunk az Ant memóriahasználatát is az alábbi módon.

ANT_OPTS="-Xmx1024m -XX:MaxPermSize=256m"
export ANT_OPTS
Nincs más hátra, mint elindítani a fordítást a Liferay gyökérkönyvtárában.
ant all

A fordítás befejeztével a ../bundles/tomcat-7.0.21/bin/startup.sh parancs futtatásával kelthetjük életre friss Liferayünket. Miután elindult, a Control Panel Server szekcióban találunk egy OSGi Admin menüpontot. Belépve láthatjuk, ha minden jól ment, hogy van egy OSGi System Bundle nevű batyu, ami aktív, tehát az OSGi konténer fut rendben.Az általam próbált verzióba Eclipse Equinox 3.7 volt integrálva, Ray elmondása szerint minden >=4.1-es implementációval változtatás nélkül működik.

Amit a rendszer tud jelenlegi állás szerint:

  • Az adminisztrátor portleten keresztül lehetséges OSGi batyuk hozzáadása, eltávolítása, frissítése, elindítása, újraindítása, és leállítása.
  • Egy saját deploy listenerrel a deploy könyvtárba másolt batyukat a Liferay megpróbálja telepíteni és elindítani.

Most, hogy működik a portálunk, nincs más dolgunk, mint nekiállni első OSGi Servletünk megalkotásához. A batyu készítésről itt írtam részletesen, így az alapokba nem is mennék bele. Hozzunk létre egy OSGi Plugin Projectet. A MANIFEST.MF állomány tartalma legyen a következő:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: OSGiOnLiferay
Bundle-SymbolicName: OSGiOnLiferay;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Import-Package: javax.servlet;version="2.5.0",
 javax.servlet.http;version="2.5.0",
 org.eclipse.equinox.http;resolution:=optional,
 org.osgi.framework;version="1.6.0"

Hozzunk létre a gyökér-könyvtárban egy plugin.xml-t, amelybe regisztráljuk a Servletünket.

   
   

Végezetül írjunk egy egyszerű Servletet.
public class TestServlet extends HttpServlet {
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp)
   throws ServletException, IOException {
  PrintWriter out = resp.getWriter();
  out.print("Java'nother blog");
  out.close();
 }
}
A plugin buildelése után egyszerűen másoljuk be a kapott JAR-t a deploy könyvtárba. A Liferay logot nézve az alábbi sor jelöli a sikeres telepítést.
12:14:29,798 INFO  [AutoDeployDir:167] Processing sample.jar
Az admin felületre látogatva remélhetőleg meg is találjuk batyunkat az OSGi Admin felületen, nincs más dolgunk, mint aktiválni.

És itt jön a szomorú, ám reményteli vég. Mint említettem eléggé fejlesztési fázisban van a projekt, ami sajnálatosan azt is jelenti, hogy ezen a ponton nem tudunk túljutni. Ray a közeljövőben publikál egy birdge Servletet, aminek a segítségével a Liferay a HTTP kéréseket az OSGi HttpService rétegén keresztül bármely, a keretrendszerbe regisztrált Servletnek vagy JSPnek továbbítani tudja. Tervezi a Plugin SDK bővítését is, hogy OSGi batyukat lehessen vele kényelmesen fejleszteni. Véleményem szerint ígéretes a kezdeményezés, hiszen a Liferay egy jól gyúrható platform, az OSGi, pedig égy végtelenül rugalmas keretrendszer.

2011. augusztus 27., szombat

Validálás, ahogy én csinálom

Egy programozó életében rendkívül gyakran előfordul, hogy entitásokat kell validálnia. Lényegében az üzleti logika mindig úgy kezdődik, hogy a bemeneti paramétereket, melyek legtöbbször felhasználói interakció által kerülnek be, validálni kell, hogy a hibás entitások a későbbiekben ne okozzanak hibát a program működésébe. A validálás az a dolog, amit egy kicsit mindenki másként csinál, mindenkinek megvan a saját módszere. Ebben az írásban bemutatnám, én hogyan is szoktam ezt a problémát orvosolni. A bemutatott megoldás nem tökéletes, de remélem gondolat-ébresztőnek megteszi.

A validálás alapproblémája szerintem a következő: az entitások validitását az üzleti réteg végzi el, amelyhez különböző kliensek kapcsolódnak. A különböző kliensek különböző nyelveken, és módon kívánják megjeleníteni a hibaüzeneteket, tehát az üzleti rétegbe írt return "Hibás a név!" és hasonlók megengedhetetlenek (később úgyis kiderül, hogy többnyelvű lesz a megjelenítés). A következő probléma, amivel szembesülünk, hogy egyszerre több hibát is tudni kell kezelni, hiszen egy bonyolult form kitöltésekor nem szerencsés egyesével untatni a felhasználót a hibák közlésével, meg egyébként sem tudjuk klienseink miként működnek. A nyelvi problémát orvosolni viszonylag egyszerű, vezessünk be jól dokumentált hibakódokat, klienseink, meg feldolgozzák saját belátásuk szerint. Az egyidejű több hibajelzés kérdésköre már egy kicsit rafináltabb dolog. A legegyszerűbb megoldás, egy hibakódokkal feltöltött tömb, vagy Collection visszaadása. Ezzel a megoldással a legnagyobb problémám, hogy rengeteg favágó programozással jár. Ha mindenhová, mind kliensben mind az üzleti rétegben kézzel írogatjuk a hibakódokat, akkor a hibaszázalékunk igencsak megnő, mert óhatatlanul elgépeljük valahol a kódot. Ha bevezetünk konstansokat, vagy Enumokat, akkor legalább a hibákat kiküszöböljük, de a ThisIsMyEntity.ERROR_NAME_REQ gépelésébe fogunk megőszülni.

Nekem a következő megoldás vált be: mivel 2db kettő hatványa sosem lesz egy harmadik kettő hatványával egyenlő, a hibakódot adjuk vissza kettő hatványaival. Tehát, ha hiányzik a név adjunk vissza 2-t, amennyiben a lakcím hiányzik adjunk vissza 4-et, ha pedig mindkettő hiányzik adjunk vissza 6-ot. A metódus implementációját személy szerint magába az Entitást megvalósító osztályba szoktam tenni, de ez ízlés dolga. Van aki szerint az Entitásnak csak egy DTO-nak kell lennie, és a validitás eldöntése az üzleti logika feladata. Véleményem szerint egy entitás tudja leginkább magáról eldönteni, hogy megfelelőek-e a benne foglalt adatok.

public int validate() {
            int valid = 0;
            
            if (foo == null || foo.isEmpty()) valid |= 1 << 1;
            if (bar == null) valid |= 1 << 2;
            if (param == null || param.isEmpty()) valid |= 1 << 3;
            
            return valid;
        }
Mivel későbbiekben is bitműveletekkel fogunk operálni, elegánsabbnak tartottam bitműveletekkel megoldani az összeadást, természetesen dolgozhatunk "hagyományos" számokkal is.

Kliens oldalon a hibakód lekezelése pofon egyszerű. Kiválasztunk egy tetszőleges 2 hatványt (érdemes a dokumentációban is említettet választani :)), mondjuk a 4-et, és ellenőrizzük, hogy adott bit, 2^2 az 100, tehát a harmadik, egyezik-e a visszakapott érték harmadik bitjével.

boolean somethingIsValid = (4 & entity.validate()) == 4;
Ez a módszer sem tökéletes, hiszen a hibákat csak bővíteni lehet, egyéb változtatás esetén már meglévő klienseink fognak hibásan működni. A teljes példaprogram az alábbi:
public class ValidatorTest {

    private static final Map<Integer, String> ERRORS = new HashMap<Integer, String>();

    static {
        ERRORS.put(2, "Foo is required!");
        ERRORS.put(4, "Bar is required!");
        ERRORS.put(8, "Param is required!");
    }

    public static void main(String[] args) {
        List<Entity> entities = Arrays.asList(new Entity[] {
            new Entity(null, null, null),
            new Entity("foo", null, null),
            new Entity("", null, "param")
        });

        for(Entity entity : entities) {
            System.out.println(entity);
            int valid = entity.validate();
            
            for(Integer code : ERRORS.keySet()) {
                if ((code & valid) == code) {
                    System.out.println(ERRORS.get(code));
                }
            }
        }
    }

    static class Entity {

        private String foo;
        private Integer bar;
        private String param;
        
        public Entity(String foo, Integer bar, String param) {
            this.foo = foo;
            this.bar = bar;
            this.param = param;
        }

        public int validate() {
            int valid = 0;
            
            if (foo == null || foo.isEmpty()) valid |= 1 << 1;
            if (bar == null) valid |= 1 << 2;
            if (param == null || param.isEmpty()) valid |= 1 << 3;
            
            return valid;
        }
    }
}

Nagyon kíváncsi vagyok a véleményetekre, illetve, hogy nektek milyen módszer vált be ebben a témakörbe.