2010. november 28., vasárnap

Liferay Service Builder

A Liferay az egyedi portletek fejlesztését saját MVC keretrendszerével kívánja élvezetesebbé tenni, az un. Liferay MVC-vel. A keretrendszer legizgalmasabb része a Model-t előállító Service Builder, ennek bemutatására teszek most egy kísérletet. Felépítését tekintve az adatbázis réteg fölött Hibernate biztosítja az entitások adatbázissal való megfeleltetését. A Hibernatet a Liferay saját perzisztencia rétege burkolja be. A perzisztencia réteggel a Servicek tartják a kapcsolatot, melyek lehetnek lokálisak és távoliak egyaránt, és általuk szerezhetünk referenciát Entitásainkra, melyeket a Liferay analógia szerint Modeleknek hívunk. A könnyebb megértést segíti a mellékelt ábra.
Első lépésként, ha még nem tettük volna, hozzunk létre egy New -> Liferay Plug-in Projectet kedvenc Eclipse fejlesztői környezetünkben. Eclipshez létezik egy Liferay IDE nevű plugin, ami azért nagyban megkönnyíti a fejlesztést, természetesen a Liferay SDK-t is használhatjuk, én a pluginra támaszkodom. Következő dolog amit létre kell hoznunk, az egy New -> Liferay Service. A beállító-panelen állítsuk be a Plug-in Projectet, Package path paraméternek adjuk meg a service réteg ős-csomagjának nevét, ez tetszőleges, Namespace paraméternek a tábla prefixet tudjuk megadni, és végül saját nevünket. A Namespace paraméter üresen is hagyható jelentősége annyi, hogy mivel az érintett táblákat a Liferay  alap értelmezetten saját táblái mellé hozza létre könnyebb átlátni, ha prefixeljük a táblaneveket. Ha végeztünk találunk egy service.xml állományt a docroot/WEB-INF mappában. Dolgunk mindössze annyi, hogy XML formában rögzítsük Modelljeink struktúráját.
<!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.jpattern.model">
 <author>mhmxs</author>
 <namespace>library</namespace>
 <entity name="Book" local-service="true" remote-service="false">
  <column name="bookId" type="long" primary="true" />
  <column name="title" type="String" />
  <column name="author" type="Collection" entity="Author" mapping-key="authorId" />
  <column name="description" type="String" />
  <column name="price" type="long" />
  
  <column name="groupId" type="long" />
  <column name="companyId" type="long" />
  
  <order by="asd">
   <order-column name="title" />
   <order-column name="price" order-by="desc" />
  </order>
  
  <finder return-type="Collection" name="title">
   <finder-column name="title" comparator="LIKE" />
  </finder>
 </entity>
 <entity name="Author" local-service="true" remote-service="false">
  <column name="authorId" type="long" primary="true" />
  <column name="name" type="String" />
  
  <column name="groupId" type="long" />
  <column name="companyId" type="long" />
 </entity>
</service-builder>
Az XMl-ben az entity tag írja le az entitásokat. Először is egy kötelező név attribútumot kell megadnunk, majd a szerviz hívására vonatkozóakat. A példában csak a lokális Servicekkel foglalkozok, de tudni érdemes, hogy létezik remote-service attribútum is, alapértelmezett true!!, mely SOAP szolgáltatásokat valósít meg a Liferayyel együtt szállított Axison keresztül. Következnek a mezőleírások, melyek típusa primitív, egyszerű adat típus (String), vagy Date lehet. A primary="true" paraméterrel a PK-t határozhatjuk meg, értelemszerűen entitásonként legfeljebb 1-et. Lehetőségünk van entitások közötti kapcsolat létrehozására is, erre példa a Book author mezője, ahol a kapcsolat típusát, módját kell megadni, és az összekapcsolás kulcsát. A mapping-key OTM (@OneToMany) a mapping-table pedig MTM (@ManyToMany) kapcsolatot hoz létre entitásaink között. Liferay "poliszi", hogy nem használnak idegen kulcsokat az adatbázisban, minden kapcsolatot Java kódból kell kezelni, ezért a Service Builder sem támogatja ezt a dolgot. Személy szerint nekem ez roppant unszimpatikus, hiszen rengeteg hibát rejt magában a koncepció, a fejlesztők emberek, és óhatatlan, hogy bonyolultabb adat-struktúra esetén valami mégis kimarad a kódból. Az egyetlen "elfogadható" érv a Liferay döntése mellett, hogy így tudják biztosítani a minél szélesebb adatbázis-paletta támogatását. Érdemes felvenni a groupId és companyId mezőket, mert a Liferay ezen mezők szerint fogja tudni összekötni entitásunkat saját jogosultság-kezelő rendszerével. A Service Builder lehetőséget ad egy rendezési elv meghatározására az order mező megadásával, példánkban cím szerint ASC, és ár szerint DESC rendezést valósítunk meg. Sajnos order-ből egyetlen egy szerepelhet.  Kereséseket könnyítendő meghatározhatunk előre generált kereső metódusokat a finder mezővel, ez eléggé hasznos, ugyanis tetszőleges számú lehet, és rögtön megadható a comperatorlt, =, !=, <, <=, >, >=,  és LIKE lehet, értelemszerűen a = az alapértelmezett.  Bár a példában nem szerepel, létezik egy tx-required mező, melynek szöveges tartalmában kell azon metódusokat felsorolni, amikre szeretnénk tranzakciókezelést használni. Ennek értéke alapértelmezetten add*, check*, clear*,
delete*, set*, és update*, így az alap Service metódusok le is vannak vele fedve. További lehetőségekről a liferay-service-builder_6_0_0.dtdből tájékozódhatunk :). Végezetül a service.xml szerkesztőjében nyomjuk meg a Build services gombot, aminek hatására az IDE legenerálja a szükséges osztályokat.
A létrejött struktúrát megvizsgálva az alábbi osztályok "érdekesek" számunkra:
  • hu.jpattern.model.service.base.xxxLocalServiceBaseImpl (xxx az adott entitás osztály) ez az osztály tartalmazza az alap szerviz metódusainkat. Ezt az osztályt ne módosítsuk!
  • hu.jpattern.model.service.impl.xxxLocalServiceImpl osztályban tudjuk megvalósítani saját szerviz metódusainkat, melyek nincsenek benne a xxxLocalServiceBaseImpl osztályban. Fontos tudni, hogy az ebben az osztályba írt publikus metódusokat a Service Builder beleteszi az interfészbe következő buildeléskor.
  • hu.jpattern.model.model.impl.xxxModelImpl az alap entitásunkat reprezentálja. Ezt az osztályt ne módosítsuk, és ne szerezzünk rá referenciát közvetlenül!
  • hu.jpattern.model.model.impl.xxxImpl tartalmazza az entitás egyéb, általunk megírt metódusait. Ezt az osztályt szintén beolvasztja a következő build futtatásakor a Liferay.
  • Egy állományt emelnék még ki a többi közül, mégpedig a docroot/WEB-INF/lib könyvtárban létrejött ?.service.jar-t, mely az interfészeket és a szerviz "végleges" osztályait tartalmazza. Ezekkel a fejlesztőnek nem kell foglalkoznia.
Használata pofon egyszerű, az xxxLocalServiceUtil osztály statikus metódusain keresztül érhetjük el a Service réteget.
BookLocalServiceUtil.getBooksCount();

2010. szeptember 1., szerda

Forrásgenerálás CodeModel segítségével


Minden programozó életében egyszer elérkezik a pont, amikor valamilyen előre egyeztetett séma alapján forrás-álloményokat, azaz működő Java osztályokat kell generálnia. Jelenleg is számtalan eszközzel tudunk forrást generálni, gondoljunk csak az adatbázisból létrejövő entitásokra, vagy egy WSDL alapján generált osztály-struktúrára, sőt a legtöbb IDE alapértelmezetten segítséget nyújt ezen a területen, képes konstruktort, getter-setter metódusokat, stb készíteni pár kattintással. Ha saját magunknak szeretnénk készíteni egy forrás-generátort, természetesen arra is megvan a lehetőség. A JAXB-nek van egy al-projektje, a CodeModel, aminek segítségével megoldható a probléma, ha van elég kitartásunk megérteni a mikéntjét, ugyanis dokumentálva mondhatni egyáltalán nincs az eszköz. Rövid írásom célja, hogy ízelítőt adjon a CodeModel lehetőségeiből, ezért kézenfekvőnek tűnik, hogy ismerkedés gyanánt készítsünk egy olyan generátort, ami átadott paraméter-lista alapján összeállít egy DTO osztályt. Először is szükségünk lesz a jaxb-xjc csomagra, ugyanis ez tartalmazza a szükséges osztályokat. Magam részéről a DTO típusú osztály-generátort egy saját osztályba csomagoltam.
import com.sun.codemodel.JAnnotationUse;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JMod;
import java.io.Serializable;
import java.util.Map;

/**
 * @author mhmxs
 */
public final class DTO {

    private final String name;
    private final Map<String, Class> parameters;

    public DTO(String name, Map<String, Class> parameters) {
        this.name = name;
        this.parameters = parameters;
    }

    public JCodeModel generateSource() throws JClassAlreadyExistsException {
        JCodeModel codeModel = new JCodeModel();

        //define class header
        JDefinedClass clazz = codeModel._class(name);
        clazz._implements(Serializable.class);
        JAnnotationUse annotation = clazz.annotate(SuppressWarnings.class);
        annotation.param("value", "serial");

        //Add Java-doc to class header
        JDocComment jDocComment = clazz.javadoc();
        jDocComment.add("Simple DTO class : " + name + "\n");
        jDocComment.add("@author mhmxs");

        //Create constructor
        JMethod constr = clazz.constructor(JMod.PUBLIC);

        //Generate getter and setter methods for all parameters
        JMethod method;
        for(String param : parameters.keySet()) {
            //Add parameter to class declaration
            clazz.field(JMod.PRIVATE, parameters.get(param), param);

            //Add parameter to constructor
            constr.param(parameters.get(param), param);
            constr.body().directStatement("this." + param + " = " + param + ";");

            String methodName = param.replaceFirst(String.valueOf(param.charAt(0)),
                String.valueOf(param.charAt(0)).toUpperCase());

            //Create getter method
            method = clazz.method(JMod.PUBLIC, parameters.get(param), "get" + methodName);
            method.body()._return(JExpr._this().ref(param));

            //Create setter method
            method = clazz.method(JMod.PUBLIC, Void.TYPE, "set" + methodName);
            method.param(parameters.get(param), param);
            method.body().directStatement("this." + param + " = " + param + ";");
        }

        return codeModel;
    }
}
Az osztály-t az alábbi módon tudjuk meghívni.
Map<String, Class> parameters = new HashMap<String, Class>();
parameters.put("parameter", String.class);
parameters.put("parameter2", Integer.class);
        
DTO dto = new DTO("a.b.Clazz", parameters);

OutputStream out = new FileOutputStream(new File("/a/b/Clazz.java"));
dto.generateSource().build( new SingleStreamCodeWriter(out));
out.close();
Végül az eredmény.
package a.b;

import java.io.Serializable;


/**
 * Simple DTO class : a.b.Clazz
 * @author mhmxs
 * 
 */
@SuppressWarnings("serial")
public class Clazz
    implements Serializable
{

    private String parameter;
    private Integer parameter2;

    public Clazz(String parameter, Integer parameter2) {
        this.parameter = parameter;
        this.parameter2 = parameter2;
    }

    public String getParameter() {
        return this.parameter;
    }

    public void setParameter(String parameter) {
        this.parameter = parameter;
    }

    public Integer getParameter2() {
        return this.parameter2;
    }

    public void setParameter2(Integer parameter2) {
        this.parameter2 = parameter2;
    }

}

2010. augusztus 19., csütörtök

META-INF könyvtár kicsit közelebbről

A META-INF könyvtáron belül található MANIFEST.MF (továbbiakban MF) állománnyal mindenki találkozik, aki Java binárist készít, vagy felhasznál egy mások által fordított Java forrást. Amikor létrehozunk egy JAR-t vagy valamely módozatát (WAR, EAR), akkor automatikusan belekerül egy példány az állományba, melynek tartalma alapértelmezetten csak a Manifest verziószámát tartalmazza "Manifest-Version: 1.0". A MF állomány JDK 1.2 verziója óta lényegesen egyszerűsödött, korára való tekintettel az ez előtti verzió nem kerül tárgyalásra. A MF állomány kulcs-érték pár alapú metaadat-tárolást valósít meg, és egy JAR-ban (WAR, EAR) csak egy példány lehet belőle, így értelem szerűen az egész JAR-ra, és a benne lévő csomagokra vonatkozó információkat is tárol. Rövid írásom célja, hogy bemutassa a lehetséges alap-beállítások egy részét. Mivel a MF funkcionalitása eléggé széleskörű, kezdve a verzió-követéstől, az elektronikus aláíráson át, a szerzői információkig, egy írásban talán össze sem lehetne foglalni az összes területet, ráadásként az egyes keretrendszerek (Spring, OSGi, stb.) is előszeretettel használják saját csomag-információk tárolására.

A JAR-ra vonatkozó paraméterek


Main-Class

Amennyiben önállóan futtatható JAR-t készítünk, a JVM-nek meg kell adnunk paraméterként, hogy mely osztály "main" metódusa a belépési pont. Mivel ezt körülményes minden indításnál megadni, ezért lehetőségünk van a MF-ben  tárolni ezt a beállítást.
Main-Class: my.package.startApplication

Class-Path

Az osztály-betöltő ha még nem töltött be egy hivatkozott osztályt, akkor az alapértelmezett útvonalakon elkezdi keresni azt. Ha szerencsések vagyunk, akkor sikerrel jár és az osztály bekerül a memóriába. Amennyiben az alapértelmezettől eltérő útvonalon található az osztály, akkor annak pontos helyet specifikálhatjuk a MF-ben. Lehetőség van több JAR-t is felsorolni szóközzel elválasztva. A fejlesztés könnyítése érdekében helyettesítő karaktereket is használhatunk az útvonal megadásakor.

Class-Path: ./lib/first.jar /usr/lib/secound.jar /usr/lib/java/*

Egyéb opcionális paraméterek

A teljeség igénye nélkül.

Archiver-Version: Plexus Archiver
Created-By: Apache Ant
Built-By: Joe
Build-Jdk: 1.6.0_04
Signature-Version: 1.0

Bejegyzésekre vonatkozó paraméterek


Name

Lehetőségünk van nevet adnia a JAR-on belül az egyes csomagoknak, osztályoknak vagy erőforrásoknak. A névadás konvenciója, hogy a név értéke a csomag esetén relatív útvonala a fájlstruktúrában, és egy lezáró "/"-karakter, osztály vagy egyéb erőforrás-fájl esetén a teljes relatív elérés. Minden további attribútum ami üres-sor nélkül a név után következik, érvényes lesz a Name-ben hivatkozott elemre.

Name: my/package/

Sealed

A névvel ellátott csomagokat opcionálisan le lehet zárni, ami azt jelenti, hogy a lezárt csomagban lévő összes definiált osztály egyazon JAR-ban található. A lezárásnak biztonsági és verziózási okai lehetnek.

Name: my/package/
Sealed: true

Package Versioning Specification

Verzió-kezelési specifikációban a csomagnak többféle attribútum is megadható opcionálisan, az egyetlen megkötés, hogy ezen beállítások mindegyikének szintén a Name attributumot kell követniük üres-sor hagyása nélkül.

Name: my/package/
Specification-Title: "MF Sample"
Specification-Version: "0.1"
Specification-Vendor: "jpattern"
Implementation-Title: "my.package"
Implementation-Version: "build1"
Implementation-Vendor: "jpattern"

Content-Type

A Content-Type segítségével az adott elem MIME tipusát határozhatjuk meg.

Name: my/package/startApplication.properties
Content-Type: text/plain

Java-Bean

A Java-Bean attribútum segítségével adható meg, hogy az adott elem Java Bean vagy sem.

Name: my/package/startApplication.class
Java-Bean: true

*-Digest

Az aláírással ellátott állományoknál kötelező elemként meg kell adni az állomány hashének base64 dekódolt reprezentációját. Az aláírás készítésről később.

Name: my/package/startApplication.class
SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=

MANIFEST.MF állomány készítése Ant taskból

Természetesen a MF-et nem kell minden alkalommal manuálisan megszerkeszteni, hanem lehetőségünk van Ant taskbol előállítani tartalmát (Mavenből szintén).

    
        
            
            
        
    

Aláírás és hitelesítés


A digitális aláírás fogalmát, és lényegét azt hiszem senkinek sem kell bemutatnom. Mivel a Java eléggé fejlett biztonsággal rendelkezik, elengedhetetlen, hogy az egyes csomagokat ne lehessen hitelesíteni. A Java a művelethez szükséges állományokat szintén a META-INF könyvtárban tárolja, tipikusan *.SF, *.DSA, *.RSA és SIG-* fájlokban. A hitelesítés menete tömören:

  • Az aláíró egy titkos kulcs segítségével aláírja a JAR-t (pontosabban minden benne lévő állományt).
  • A felhasználó a publikus kulccsal ellenőrzi az aláírást.

Aláírás készítése

  • Aláírás készítésének első lépése, hogy generálnunk kell egy privát kulcsot.
    keytool -genkey -alias jarSigner -keystore storedkeys -keypass passwd -dname "cn=jpatter" -storepass stpasswd
    Mint is csinál ez a parancs? Generál egy kulcsot jarSigner névvel, és a keypass jelszóval védetté teszi. A keystore paraméterben megadott adatbázis-állományban eltárolja a kulcsot, mely állomány ha nem létezik a keytool létrehozza azt, és a storepass-ban megadott jelszóval védi. A dname paraméter specifikál egy un. "X.500 Distinguished Name" bejegyzést, a cn paraméterben megadott egyszerűsített névvel. Az "X.500 Distinguished Name azonosítja a bejegyzéseket a X.509 hitelesítéshez, értéke tetszőleges lehet.
  • Az elkészült JAR-unk aláírásához a jarsigner eszközt tudjuk segítségül hívni.
    jarsigner -keystore storedkeys -storepass stpasswd -keypass passwd -signedjar myapp_signed.jar myapp.jar jarSigner
    A parancs lényegében kiszedi az előzőekben létrehozott kulcsot, és a bemeneti JAR állomány minden elemén elvégzi az aláírást, a végeredményt pedig a kimeneti JAR-ba teszi. A JAR-ba pillanva máris megjelent a JARSIGNE.SF és JARSIGNE.DSA, melyek közül az előbbi az egyes állományok hitelesítéséért felel, az utóbbi a publikus kulcs. A JARSIGN.SF állományt megnyitva valami hasonló tárul a szemünk elé:
    Signature-Version: 1.0
    SHA1-Digest-Manifest-Main-Attributes: FPLdz3FFeWLdgX0fUdHTqjUNkpE=
    Created-By: 1.6.0_20 (Sun Microsystems Inc.)
    SHA1-Digest-Manifest: AJwzuuyn2mg59fYB2qEhUL0PPgI=
    
    Name: my/resources/logo.png
    SHA1-Digest: oVtyix/BpiM9iq1fG/nMpy4Xy4Q=
    
    Name: my/package/startApplication.class
    SHA1-Digest: TD1GZt8G11dXY2p4olSZPc5Rj64=
    A MF állományunk tartalma is automatikusan megváltozott, az SHA1-Digest bejegyzések kerültek bele.

Aláírás hitelesítése


A hitelesítés szintén a jarsigner eszközzel történik.
jarsigner -verify myapp_signed.jar
eredményként pedig "jar verified" vagy "jar is unsigned" értékeket kaphatjuk.

2010. július 30., péntek

OSGi távoli kapcsolódás CoolRMI segítségével

Az előző bejegyzésben láthattuk miként készíthetünk OSGi keret-rendszer alá szolgáltatást, és hogyan kapcsolódhatunk ehhez a szolgáltatáshoz a keret-rendszeren belül. A mostani írás célja, hogy segédletet nyújtson távoli kapcsolat kialakítására, hiszen elengedhetetlen, hogy az OSGi konténeren kívül eső kód is használni tudja a szolgáltatásokat. Választásom a CoolRMI nevű eszközre esett, egyrészt mert magyar fejlesztés, és nyílt-forráskóddal lett publikálva, másrészt mert a környezetem ezt használja, és kipróbált alkalmazás-komponensnek minősült. A CoolRMI két részből tevődik össze. Az egyik fele az OSGi konténerben fut mint szolgáltatás, és várja a beérkező kéréseket, a másik fele pedig az alkalmazásunkban a kapcsolódásért felel.
  • Szerezzük be a CoolRMI legfrissebb verzióját.
  • Indítsuk el az OSGi konténert.
  • Installáljuk majd indítsuk el a CoolRMI-t az OSGi konténerben.
    Valami ilyesmit kell látnunk:

    osgi> ss
    
    Framework is launched.
    
    id      State       Bundle
    0       ACTIVE      org.eclipse.osgi_3.6.0.v20100517
    2       ACTIVE      com.rizsi.coolrmi_1.1.0
    
  • Hozzunk létre egy "Plug-in Project"-et az Eclipsben CoolRMI néven, majd a letöltött jar-ból a MANIFEST.MF-et és a forrás-fájlokat másoljuk a projektbe, végül pedig frissítsük a projektet. (nekem a MANIFEST.MF állományba az alábbi sort fel kellett venni: Import-Package: org.osgi.framework;version="1.3.0")
  • Nyissuk meg az előzőekben service néven futó projektünket, és a MANIFEST.MF állomány Import-Package szekcióját bővítsük a CoolRMI exportjaival.
  • Szerver oldalon a a szolgáltatás regisztrációjában van némi különbség. Jelen esetben nem az OSGi konténerbe kell regisztrálnunk a szolgáltatást, hanem a CoolRMI kontextusába, ezért az Activator.start metódusunkat az alábbira kell alakítani:

    InetSocketAddress addr = new InetSocketAddress(9001);
      
    CoolRMIServer server = new CoolRMIServer(Activator.class.getClassLoader(), addr, true);
      
    //Register a service on the id: "service"
    //This id an be used client side to find the service on this server.
    server.getServiceRegistry().addService(new CoolRMIService("service", SimpleService.class, new SimpleServiceImpl()));
      
    server.start();
    System.out.println("CoolRMI Service started.");
    
  • Fordítsuk és telepítsük a szolgáltatást.
  • Hozzunk létre egy "Java Project"-et, és importáljuk be az imént letöltött jar-t.
  • Tegyük elérhetővé a service.SimpleService interfészt, hogy a kliens programunk tudja milyen metódusokat tud meghívni.
  • A kliens osztály kódja az alábbi:

    import java.net.InetSocketAddress;
    import service.SimpleService;
    import com.rizsi.coolrmi.CoolRMIClient;
    import com.rizsi.coolrmi.ICoolRMIProxy;
    
    public class Caller {
     public static void main(String[] args) throws Exception {
      // Create a client that connects to the server on the specified address.
      CoolRMIClient client = new CoolRMIClient( Caller.class.getClassLoader(), new InetSocketAddress("localhost", 9001), true);
      
      try {
       // Get a proxy for the remote service object
       // To access the remote object the remote interface
       // and the service Id must be known
       ICoolRMIProxy remoteService = client.getService(SimpleService.class, "service");
       
       try {
        SimpleService service = (SimpleService) remoteService;
        service.echo("CoolRMI calling.");
       } finally {
        remoteService.disposeProxy();
       }
      } finally {
       client.close();
      }
     }
    }
  • Futtassuk a klienset, és ellenőrizzük a kimenetet az OSGi konzolon.

    osgi> CoolRMI calling.
    

2010. július 10., szombat

OSGi szárnypróbálgatás

Bár biztosan sokan ismeritek, de legalább hallottatok az Open Services Gateway Initiative-ról, ismertebb nevén az OSGi-ről, gondoltam én is kísérletet teszek rendszer a bemutatására. A témában kutakodva azonban találtam egy rövid írást Paller Gábor kollégától, melyben olyan érthetően megfogalmazza a technológia lényegét, hogy bizton állíthatom nekem sem sikerülne jobban, ezért meg sem próbálom. Az elméleti alapok elsajátítása után a gyakorlati megvalósításra szeretném helyezni a hangsúlyt.
OSGi pluginek fejlesztésére Eclipse IDE-t választottam, mely amellett, hogy ismeri a MANIFES.MF állomány függőségeit kezelni, rendelkezik beépített OSGi konténerrel (Equinox) is. Szerencsére a hazai bloggerek már több ízben is feszegették a témát, ennek köszönhetően pl. a jTechnics hasábjain olvashatunk a különféle konténer-implementációkról.
Megvalósítandó feladatként készítsünk egy bundlet, amely kiajánl egy szolgáltatást, amit egy másik bundeből meghívunk.

A szerviz:

  • Első lépésként hozzunk létre az Eclipsben egy "Plug-in Project"-et service néven, és az "on OSGi framework" opciónál állítsuk be az Equinox-ot.
  • Hozzuk létre a service.SimpleService interfészt:
    package service;
    
    public interface SimpleService {
     public void echo(Object message);
    }
    
  • Majd hozzuk létre service.SimpleServiceImpl néven az szolgáltatás implementációját:
    package service;
    
    public class SimpleServiceImpl implements SimpleService {
    
     @Override
     public void echo(Object message) {
      System.out.println(message);
     }
    }
    
  • Szerkesszük a service.Activator osztályt a következőre:
    package service;
    
    import org.osgi.framework.BundleActivator;
    import org.osgi.framework.BundleContext;
    import org.osgi.util.tracker.ServiceTracker;
    
    public class Activator implements BundleActivator {
    
     @Override
     public void start(BundleContext context) throws Exception {
      context.registerService(SimpleService.class.getName(), new SimpleServiceImpl(), new java.util.Hashtable());
      
      //Check service
      ServiceTracker serviceTracker = new ServiceTracker(context, SimpleService.class.getName(), null);
      serviceTracker.open();
      
      SimpleService simpleLogService = (SimpleService) serviceTracker.getService();
      if(simpleLogService != null)
       System.out.println("Service started.");
      else
       System.out.println("Service init failed!");
      
      serviceTracker.close();
     }
    
     @Override
     public void stop(BundleContext context) throws Exception {  
      System.out.println("Service stopped.");
     }
    }
    
  • A META-INF/MANIFEST.MF állomány szerkesztésével készíthetjük fel bundlet a konténerrel való együttműködésre. Eclipsben az Import-Package szerkesztésével tudjuk a "Plug-in Dependencies"-eket szerkeszteni, az IDE automatikusan importálja az itt megadott csomagokat. Fontos továbbá, hogy az Export-Packagenél kiajánljuk a service csomagot, mert ellenkező esetben a kliens oldalon "class SimpleServiceImpl cannot be cast to class SimpleService" exceptiont fogunk kapni.
    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Service
    Bundle-SymbolicName: service
    Bundle-Version: 1.0.0
    Bundle-Activator: service.Activator
    Bundle-RequiredExecutionEnvironment: JavaSE-1.6
    Import-Package: org.osgi.framework;version="1.3.0",org.osgi.util.tracker;version="1.3.1"
    Export-Package: service;version="1.0.0"
    
  • A MANIFEST.MF állomány->jobbklikk->Run As->Run Configuration ablakban tudjuk beállítani a konténert a teszteléshez.
  • Fordításhoz vagy írunk saját ant/maven scriptet, vagy Exportáljuk a bundlet JAR formájában. Szeretném megjegyezni, hogy az Eclipse alapértelmezetten generálja a MANIFEST.MF állományt a jar-ba, viszont ebben az esetben a konténer nem fog tudni mit kezdeni a bundleval, ezért a "Use existing manifest from workspace" opció alatt (utolsó lépés) adjuk meg a megfelelő állományt.

A kliens:

  • Ismét egy hasonlóan paraméterezett "Plug-in Projct"-re lesz szükségünk, de ezúttal caller néven.
  • A caller.Activate osztályt hozzuk az alábbi formára:
    package caller;
    
    import org.osgi.framework.BundleActivator;
    import org.osgi.framework.BundleContext;
    import org.osgi.framework.ServiceReference;
    
    import service.SimpleService;
    
    public class Activator implements BundleActivator {
    
     @Override
     public void start(BundleContext context) throws Exception {
      //Another way to connect a service.
      ServiceReference reference = context.getServiceReference(SimpleService.class.getName());
      SimpleService service = (SimpleService) context.getService(reference);
      
      if (service != null)
       service.echo("Service called.");
      else
       System.out.println("Service call error!");
     }
    
     @Override
     public void stop(BundleContext context) throws Exception {
     }
    }
    
  • A service.SimpleService interfészt mindenképpen tegyük elérhetővé ebben a projektben, az egyszerűség kedvéért hozzuk létre az objektumot.
  • A META-INF/MANIFEST.MF állomány tartalma legyen a következő:
    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: Caller
    Bundle-SymbolicName: caller
    Bundle-Version: 1.0.0
    Bundle-Activator: caller.Activator
    Bundle-RequiredExecutionEnvironment: JavaSE-1.6
    Import-Package: org.osgi.framework;version="1.3.0",service;version="1.0.0"
    
    Fontos, hogy a szervizben exportált service csomagot itt importáljuk.
  • A szerviznél leírtakkal azonos módon tesztelhetjük és fordíthatjuk a bundlet (nekem voltak problémák, amikor az IDE-ből próbáltam tesztelni a két bundlet együtt).

A konténer:

Utolsó egységként a konténert kell megismernünk, melynek vezérlése eltérő a különböző implementációk esetén. Az Equinoxot választva az alábbiakat kell tennünk.
  • Szerezzük be a nekünk megfelelő verziót, jelenleg a 3.6-os a legfrissebb.
  • A `java -jar org.eclipse.osgi_3.6.0(.*?).jar -console` parancs futtatásával indíthatjuk el a konténert.
  • Installáljuk a bundlekat:
    osgi> install file:///path/to/file/service.jar
    Bundle id is 6
    
    osgi> install file:///path/to/file/caller.jar
    Bundle id is 7
    
    Eltávolítani az uninstall paranccsal, frissíteni pedig az updatetel lehet.
  • Az ss paranccsal lekérdezhetjük a telepített bundlek állapotát, mindkettőnek INSTALLED állapotban kell lennie:
    osgi> ss
    
    Framework is launched.
    
    id      State       Bundle
    0       ACTIVE      org.eclipse.osgi_3.6.0.v20100517
    6       INSTALLED   service_1.0.0.qualifier
    7       INSTALLED   caller_1.0.0.qualifier
    
  • Utolsó lépésként indítsuk el mindkét bundlet:
    osgi> start service
    Service started.
    
    osgi> start caller
    Service called.
    
    A bundle neve helyett használhatjuk az azonosítóját.
  • A stop paranccsal állítjuk le a bundlekat:
    osgi> stop service
    Service stopped.
    

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.

2010. május 16., vasárnap

Adott pont vonzáskörzetében való keresés MySQL segítségével

Azt a feladatot kaptam, hogy saját adatbázisból vendéglátó-egységeket tegyek kereshetővé, és a találatokat jelenítsem meg egy térképen. Hogy ne legyen olyan egyszerű a feladat, további kérés volt, hogy Budapesten a szomszédos kerületek találatait is mutassam meg, egyébként pedig 50 km-es körzetben lévőket. Bár a megoldás nem kapcsolódik közvetlenül a Java-hoz, mégis úgy gondoltam talán érdekes lehet. Igyekeztem úgy kivitelezni a dolgot, hogy a MySQL adatbázisból ne kerüljön ki olyan érték, amelyet nem kell megjeleníteni.
Lássuk először az 50 km-es körzet problémáját:
Először is létrehoztam két triggert, amik a vendéglátó-egység beszúrása vagy módosítása után futnak le, és egész egyszerűen annyit tesznek, hogy a beállított longitude és latitude értékből a coordinate mezőben eltárolnak egy POINT objektumot. A POINT beépített MySQL objektum, így értelem-szerűen a mező típusának is az van deklarálva.
DELIMITER //

CREATE TRIGGER RefreshHorecaPointInsert BEFORE INSERT ON `horeca`
FOR EACH ROW
BEGIN
    SET NEW.`coordinate` = POINT(NEW.`longitude`, NEW.`latitude`);
END//

CREATE TRIGGER RefreshHorecaPointUpdate BEFORE UPDATE ON `horeca`
FOR EACH ROW
BEGIN
    SET NEW.`coordinate` = POINT(NEW.`longitude`, NEW.`latitude`);
END//

DELIMITER ;
A következő lépés már igen egyszerű, mivel a MySQL (is) rendelkezik beépített geometriai funkciókkal. Feladatunk annyi, hogy a POLYGONE objektumot felhasználva az első találat koordinátái köré egy tetszőleges sokszöget rajzoljunk. Bár az 50 km-es körzet meghatározásához egy kört kellene rajzolni, de annak bonyolultsága és gyakorlati haszna között fennálló aránytalanság miatt, én a négyszöget választottam.
String polygone = "POLYGON((" + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() - 0.22) + ", " + (horeca.getLongitude() + 0.32) + " " + (horeca.getLatitude() - 0.22) + ", " + (horeca.getLongitude() + 0.32) + " " + (horeca.getLatitude() + 0.22) + ", " + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() + 0.22) + ", " + (horeca.getLongitude() - 0.32) + " " + (horeca.getLatitude() - 0.22) + "))";
String query = "SELECT h FROM Horeca h WHERE MBRContains(PolygonFromText('" + polygone + "'), coordinate)";
Megjegyzem a geometriai funkciók elméletileg 5.0-ás verzió óta léteznek MySQL-ben, elég sok helyen olvastam, hogy hibásan működik, meg, hogy aki ilyet akar használjon PostgreSQL-t. Én is belefutottam olyan kellemetlenségbe, ami miatt frissíteni kellett a szervert 5.1.x-re (a SELECT POINT(12, 12) lefutott, de a UPDATE horeca SET coordinate = POINT(longitude, latitude) már nem).
A Budapest szomszédos kerületeinek kérdés-köre nem egy bonyolult dolog, pusztán azért tárgyalom, hogy teljes legyen a kép. Az adatbázis kímélése érdekében ismét egy triggert írtam, ami budapesti cím beszúrása vagy szerkesztése esetén az irányító számból eltárolja a középső 2 karaktert, mint kerületet. Az egységes kereshetőség érdekében hagytam minden kerületet két karakteresre.
DELIMITER //

CREATE TRIGGER SetDistrict BEFORE INSERT ON `horeca`
FOR EACH ROW
BEGIN
    IF NEW.`postal_code` < 2000 THEN
        SET NEW.`district` = SUBSTR(NEW.`postal_code`, 2, 2);
    END IF;
END;

DELIMITER ;
Ezek után létrehoztam egy táblát, amely a szomszédos kerületek mátrixát tartalmazza.
INSERT INTO `district_neighbors` (`reference`, `neighbor`) VALUES
('01', '02'),('01', '12'),('01', '11'),('01', '05'),('02', '12'),('02', '03'),('02', '05'),('02', '13'),('03', '04'),('03', '13'),('04', '13'),('04', '14'),('04', '15'),('05', '06'),('05', '07'),('05', '13'),('06', '07'),('06', '13'),('06', '14'),('07', '08'),('07', '09'),('07', '14'),('08', '09'),('08', '10'),('08', '14'),('09', '10'),('09', '11'),('09', '19'),('09', '20'),('09', '21'),('10', '14'),('10', '16'),('10', '17'),('10', '18'),('10', '19'),('11', '12'),('11', '21'),('11', '22'),('13', '18'),('13', '19'),('13', '20'),('13', '21'),('14', '15'),('14', '16'),('15', '16'),('17', '18'),('18', '19'),('18', '20'),('18', '23'),('19', '20'),('20', '21'),('20', '23'),('21', '22'),('21', '23');
Az alábbi lekérdezéssel pedig könnyedén kinyerhető adott kerület és a vele szomszédosak az adatbázisból.
String query = "SELECT h FROM Horeca h WHERE district = :district OR district IN (SELECT IF(reference = :district, neighbor, reference) FROM district_neighbors WHERE reference = :district OR neighbor = :district)";

2010. április 28., szerda

Weboldal beüzemelése Linux, Apache, Glassfish alapon

Java-s web-alkalmazások fejlesztése során gyorsan felmerül az igény arra, hogy az oldal elérhető legyen mindenféle portszám megadása nélkül, hiszen vég-felhasználóink általában nem szakavatott fejlesztők, vagy rendszer tervező mérnökök, akiktől nem idegen az efféle címzés. A probléma megoldására több lehetséges megoldás is létezik, az általam felvázolt lehetőség csak egy a sok közül. Operációs rendszernek Linuxot választottam, kedvenc alkalmazás-szerverem pedig a Glassfish.
A beállítás lépései:
  • Glassfish domain létrehozása.
# asadmin create-domain --adminport 4848 --savemasterpassword domain1
A savemasterpassword opcióra azért van szükség, hogy a szerver indításakor és leállításakor ne kelljen a masterjelszavat megadni. Ellenkező esetben csak kézzel tudjuk a szervert indítani és/vagy leállítani.
  • Indító script megírása, és a megfelelő runlevel-be helyezése.
#!/bin/sh
ase "$1" in
start)
    ulimit -Hn 10240
    ulimit -Sn 10240
    su glassfish /path_to_glassfish/bin/asadmin start-domain domain1
    ;;
stop)
    su glassfish /path_to_glassfish/bin/asadmin stop-domain domain1
    ;;
restart)
    su glassfish /path_to_glassfish/bin/asadmin stop-domain domain1
    ulimit -Hn 10240
    ulimit -Sn 10240
    su glassfish /path_to_glassfish/bin/asadmin start-domain domain1
    ;;
*)
    echo $"usage: $0 {start|stop|restart}"
    exit 1
esac
Az első említésre méltó dolog, hogy én létrehoztam egy glassfish felhasználót a rendszerben, és annak nevében/jogosultságával telepítettem az alkalmazás-szervert. Ennek elsősorban biztonsági okai vannak, hiszen így az alkalmazás-szerver csak a "mezei" felhasználó hatáskörében tud tevékenykedni. A második dolog ami szemet szúrhat az ulimit parancs. A Linux kernelben meg van határozva, hogy mekkora darab-számú állományt nyithat meg egy alkalmazás/felhasználó. Ez a szám alapértelmezetten 1024, amiből a Glassfish indulás után elhasznál 8-900-at, így eléggé kis terhelés esetén is átlépi a határt. A kiadott ulimit parancs az adott processre, és az abból induló alprocessekre vonatkozik, ezért nem érdemes rendszer-szinten növelni a limitet, elég az init scriptben beállítani a kívánt értéket. A pontos érték megállapítása terheléses teszt után hangolható, ám kezdésnek érdemes 10240-re venni.
  • Apache web-szerver beállítása
NameVirtualHost *:80

<VirtualHost *:80>
    ServerName www.foo.bar
    ProxyPass / http://localhost:8080/foo.bar-war/
    ProxyPassReverse / http://localhost:8080/foo.bar-war/
    ProxyPassReverseCookieDomain localhost:8080/foo.bar-war www.foo.bar
    ProxyPassReverseCookiePath / /
    ProxyVia Off
    ProxyPreserveHost On
</VirtualHost>
Mivel a felhasználók az URL begépelésével a szerver 80-as portjára csatlakoznak, ésszerű megoldás, ha egy web-szervert telepítünk erre a portra, és a web-szerverből proxyzzuk át a megfelelő kéréseket az alkalmazás-szerver felé. A proxyzáshoz a mod_proxy és mod_proxy_http modulokat kell betölteni. A ProxyPass és ProxyPassReverse opciókkal magát a proxyzás útvonalát állítjuk be, míg a ProxyPassReverseCookieDomain és ProxyPassReverseCookiePath opciókkal a Cookie-k tárolásának módját írjuk elő. Az utóbbi 2 opció elhagyása esetén nem tudjuk a tárolt Cookie-kat elérni, mivel azok a www.foo.bar domainen lesznek bejegyezve, ebből kifolyólag Session azonosítót sem tudunk Cookie-ban tárolni.