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.

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.