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.