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.

2011. augusztus 15., hétfő

Collections.emptyList() és a generikus típuskényszerítés

Sajnos mostanában nem jut annyi időm blogolásra, mint szeretném, és ez a bejegyzés is mindössze egy szösszenet lesz.
A Collections osztály, mint nevéből is következik, a Collection példányok manipulálására, és előre definiált Collection-ök hozzáféréséhez nyújt kényelmes eszközt. Az osztály részleteibe nem szeretnék belemenni, ezt az olvasóra bízom. Kiszemelt áldozatom az emptyList metódus. A metódus forrását megnézve láthatjuk, hogy nem csinál egyebet, mint az osztályban statikus paraméterként deklarált nyers típusú EMPTY_LIST-et adja vissza a kívánt típusra alakítva. Természetesen ez a lista egy változtathatatlan lista, tehát elemeket ebbe nem tudunk tenni. Használata végtelenül egyszerű.
List<String> stringList = Collections.emptyList();
A fordító megpróbálja kitalálni, hogy milyen típusra kell alakítani az EMPTY_LIST példányt, teszi ezt úgy, hogy megnézi milyen típusba szeretnénk belepasszírozni a listát. Az esetek többségében sikerül is ez a művelet, ám mit tehetünk a következő esetben:
public static void bar() {
        foo(Collections.emptyList());
    }
    
    public static void foo(List<String> stringList) {}
Már az IDE is jelzi, hogy ez így nem lesz jó, nem tudja megállapítani milyen típusra kell alakítani a listát. Netbeans fel is ajánlja, hogy létrehoz egy foo(List<Object> stringList) metódust, de mi ezt nem szeretnénk. Egyik lehetőségünk, hogy a paramétert egy lokális változóba kimozgatjuk, és a helyes típussal adjuk át a metódusnak. Ez a megoldás célra vezető, viszont feleslegesen növeli kódunk méretét. A következő eshetőség, hogy a nyers EMPTY_LIST-et adjuk át paraméterként, viszont így hazavágjuk a típusbiztonságot. Nyers típusoknál a JVM egyszerűen levágja a típusellenőrzést. Ennek oka a visszafelé kompatibilitás, a generikusok megjelenése óta nem javallott a nyers típusok használata. Az elegáns megoldás egyszerű, segítsük a fordítót, és mondjuk meg neki milyen típusra alakítsa a listát.
foo(Collections.<String>emptyList());
Az itt említett módszer nem a Collections osztály sajátossága, alkalmazható minden generikus metódusra. Választásom azért esett erre az osztályra, mert ezt eléggé gyakran használja mindenki , aki Java-ban programozik, és része a Java SE-nek.

2011. április 28., csütörtök

JDK7 újdonság: java.util.Objects

A java.util.Arrays és java.util.Collections mintájára a Java következő verziója elhozza számunkra a java.util.Objects "eszközt". Lényegében semmi újdonság nincs az osztályban, egyszerűen olyan kényelmi funkciókat tartalmaz, melyek megkönnyítik a fejlesztők életét. Az objektumnak mindössze 9 statikus metódusa van, melyek az alábbiak:
  • compare(T a, T b, Comparator c):int - Mint a neve is mutatja objektumokat tudunk vele összehasonlítani, az egyetlen érdekessége, hogy Objects.compare(null, null) visszatérési értéke 0
  • equals(Object a, Object b):boolean - Szintén zenész Objects.equals(null, null) igaz értékkel tér vissza
  • deepEquals(Object a, Object b):boolean - Igazzal tér vissza, ha a két objektum "mélységében" egyenlő, minden más esetben hamis. Szintén elmondható, hogy Objects.deepEquals(null, null) igaz. Amennyiben mindkét átadott paraméter tömb, a metódus meghívja az Arrays.deepEquals-t ellenkező esetben az első objektum equals metódusának adja paraméterül a második paramétert
  • hashCode(Object o):int - Visszatér az átadott objektum hashCode-jával, null paraméter esetén 0-val
  • hash(Object... values):int - Az Arrays.hashCode(Object[]) mintájára az átadott paramétereknek kiszámolja a hashCode-ját. Gyakorlati haszna leginkább a DTO osztályoknál mutatkozik meg, ahol több objektum összessége alkot egy egységet.
    public class Student {
        private Integer id;
        private Mother mother;
        private Father father;
        ...
        @Override public int hashCode() {
            return Objects.hash(id, mother, father);
        }
    }
    
  • requireNonNull(T obj):T - Ez a metódus egy egyszerű validáció, amely NullPointerException-t dob, amennyiben az átadott referencia értéke null. A metódusnak átadhatunk még egy String paramétert, ekkor ez lesz a kivétel szövege
  • toString(Object o): String - Az átadott objektum toString metódusával tér vissza, null érték esetén "null" Stringgel. Második paraméternek egy String is átadható, ez a paraméter lesz a null érték toString-je
    logger.info(Objects.toString(bar, “Bar is null”));
    
Kettős érzelmekkel viseltetek az "újjítás" iránt. Egyrészt némi iróniával kijelenthető, hogy most aztán kitettek magukért a srácok, nem hiába csúszik a kiadás. Másfelöl viszont elég sok felesleges kódolástól szabadít meg az eszköz, és mivel része lesz a JDK-nak egy "szabványos" megoldást kínál hétköznapi problémákra.

2011. április 1., péntek

Liferay "gyári" Servicek és a DynamicQuery használata

Egy előző bejegyzésben foglalkoztam már a Service Builderrel, amivel saját szolgáltatásokat illeszthetünk a Liferay (továbbiakban LR) portleteinkbe. A LR természetesen a ServiceBuildert nem csak külső integrációhoz használja, hanem saját adatbázis-rétegének eléréséhez is. A szolgáltatások közül leginkább a ...LocalServiceUtil osztályokra lesz szükségünk. Ezen osztályok használatára nem szeretnék kitérni, statikus metódusaik lekérdezésével "egyértelmű", hogy melyik mire való. Amennyiben a "gyári" metódusokat használjuk legtöbb esetben sajnos számolnunk kell a LR sajátosságaival, szerencsére azonban van lehetőségünk saját SQL futtatására is, természetesen ebben az esetben nem ússzuk meg az adatbázis szerkezetének megismerését. A saját SQL-ek rendszerbe juttatásáról a DynamicQuery osztály gondoskodik. Ennek használata nem túl bonyolult, de mitől is lenne az.
DynamicQuery query = DynamicQueryFactoryUtil
    .forClass(User.class, PortalClassLoaderUtil.getClassLoader());
query.add(PropertyFactoryUtil.forName("email").eq("foo@bar.hu"));
query.addOrder(OrderFactoryUtil.asc("name"));
List fullResult = UserLocalServiceUtil.dynamicQuery(query);
List someResult = UserLocalServiceUtil.dynamicQuery(query, start, end);
long resultsNo = UserLocalServiceUtil.dynamicQueryCount(query);
Fontos tudnunk, hogy LR osztályok 2 fő csoportba sorolhatók. Az egyik a portal-impl, amely kívülről nem érhető el, ezek a LR saját belső osztályai, és a portal-service, amik pedig a kifelé ajánlott szolgáltatások. Sajnos nem sikerült a szétválasztást tökéletesen megvalósítani, legalábbis szerintem, így ha a PortalClassLoaderUtil.getClassLoader() paramétert nem adjuk meg a DynamicQueryFactoryUtilnak, akkora ClassNotFoundException-t kapunk mint a ház :). Ezt az egy "apró" részletet leszámítva viszonylag jól dokumentált a DynamicQuery használata, és az alapvető műveletekre elégséges.