2010. április 14., szerda

HTTPS kapcsolódás self-signed és/vagy lejárt tanusítvánnyal ellátott szolgáltatáshoz

Egy on-line fizetéses alkalmazás fejlesztése közben futottam bele abba a problémába, hogy https protokollon keresztül kell kapcsolódni a banki rendszerhez. Első hallásra nem is tűnik olyan bonyolultnak a dolog, azonban a megvalósítás közben belefutottam egy igen kellemetlen körülménybe. A teszt rendszer tanusítványa lejárt. Az ügyeletes rendszer-gazdával konzultálva megnyugtatott, hogy az éles rendszerben nem lesz probléma,... egy darabig,... még jó, hogy szóltam! Álmomban sem gondoltam volna, hogy ilyen előfordulhat egy banknál, mégha tesztrendszerről is van szó.
A kapcsolódáshoz a jakarta.commons.httpclient-et választottam, abból is a 3.1-es verziót.
HttpClient httpclient = new HttpClient();
Protocol myhttps = new Protocol("https", new SSLProtocolSocketFactory(), 443);
httpclient.getHostConfiguration().setHost("somehost.tld", 443, myhttps);
GetMethod method = new GetMethod("/some_uri"); //PostMethod to post data

try {
    httpclient.executeMethod(method);
    String response = new String(method.getResponseBody());
} finally {
    method.releaseConnection();
}
A kódot futtatva az alábbi Exception-t kaptam:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Első teendőnk ebben az esetben, hogy megszerezzük a tanusítvány publikus részét, majd letöltve hozzáadjuk egy kulcs-tartóhoz, az alábbi paranccsal:
keytool -importcert -trustcacerts -alias untrustedCert -file somecert.crt -keystore /usr/lib/jvm/java/jre/lib/security/cacerts
Mivel én fejlesztői környezetben szeretnék kapcsolódni, a Java alapértelmezett kulcs-tartójához adtam hozzá a tanusítványt, az alapértelmezett "changeit" jelszó segítségével. Ezek után nincs más dolgunk, mint a JVM-nek átadni paraméterként, hogy a kiszemelt kulcs-tartót fogadja el mindenképpen hitelesnek.
-Djavax.net.ssl.trustStore=/usr/lib/jvm/java/jre/lib/security/cacerts

2010. április 4., vasárnap

Képek átméretezése, első nekifutás

Ebben a témában rengeteg bejegyzés található a neten, én személy szerint mégsem találtam elfogadható megoldás, ami egyben megoldaná minden problémámat, kivéve a Java Advanced Imaging-ot. Utóbbival elég nagy problémám volt, hogy nincsen agyondokumentálva, vannak ugyan példák, de azokon eligazodni is felér egy rém-álommal. A rossz tapasztalatok sarkalltak arra, hogy belekezdjek egy, az én igényeimet kielégítő, eszköz megírásába, mely az alapvető webes környezetben előforduló problémákat orvosolja. A projekt elérkezett az első "mérföld-kőhöz", így gondoltam szánok rá pár sort.
Jelenlegi stádiumban képes átméretezni jpg, png, gif, anim gif formátumokat, alkalmazza az élsimítás technológiát, viszont a transzparens képekbe még beletörik a foga. A művelet pilléreit szeretném itt bemutatni, a teljes forráskódot elérhetővé teszem, de mivel felhasználtam pár osztályt, amit letöltöttem és javítottam, ezek pontos liszenszelését még egyeztetem.
Első esetben nézzük az egyszerűbb oldalát a dolognak, amikor nem animált gif-et kell átméretezni. Mivel a legtöbb példa csak a Graphics2D beépített élsimítását használta fel, aminek minősége finoman szólva is vacak, így átméretezés előtt egy blur effektet húzok a képre. A példa csak a kicsinyítés problémájával foglalkozik, mivel a jelentős minőség-romlás ilyen esetben érzékelhető igazán, és a gyakorlatban is ritkán kell nagyítani egy felhasználó által feltöltött képet.
OutputStream out;
BufferedImage bufferedImage;
String extension;
//...
if (!extension.equals("gif")) {
//Első lépésként, amenniyben jpg-a forrás kép, át kell állítanunk a típusát, mivel az alapértelmezett típus nem támogatja a blur készítését, minden más esetben, kompatibilissé tesszük a képet, szintén a blur-ozhatóság érdekében.
    if (mimeType.equals("image/jpeg") || mimeType.equals("image/pjpeg")) {
        bufferedImage = convert(bufferedImage, BufferedImage.TYPE_INT_RGB);
    } else {
        bufferedImage = createCompatibleImage(bufferedImage);
    }

//Elvégezzük a blur-ozást, és átméretezést, majd mehet a kép a kimenetre.
    ImageIO.write(resize(blur(bufferedImage), newWidth.intValue(), newHeight.intValue()), extension, out);

    out.flush();
    out.close();
}
A meghívott metódusok:
//Átméretezi a képet.
private BufferedImage resize(BufferedImage image, int width, int height) {
    BufferedImage result = new BufferedImage(width, height, image.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : image.getType());
    Graphics2D g = result.createGraphics();
    g.setComposite(AlphaComposite.Src);
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g.drawImage(image, 0, 0, width, height, null);
    g.dispose();

    return result;
}

//Blur-ozza a képet.
private BufferedImage blur(BufferedImage image) {
    float ninth = 1.0f / 9.0f;
    float[] blurKernel = {
        ninth, ninth, ninth,
        ninth, ninth, ninth,
        ninth, ninth, ninth
    };

    Map map = new HashMap();
    map.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, new RenderingHints(map));

    return op.filter(image, null);
}

//Átalakítja a típusát a képnek.
private BufferedImage convert(BufferedImage image, int type) {
    BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), type);
    Graphics2D g = result.createGraphics();
    g.drawRenderedImage(image, null);
    g.dispose();

    return result;
}

//Kompatibilissé teszi a képet.
private static BufferedImage createCompatibleImage(BufferedImage image) {
    GraphicsConfiguration gc = BufferedImageGraphicsConfig.getConfig(image);
    BufferedImage result = gc.createCompatibleImage(image.getWidth(), image.getHeight(), Transparency.BITMASK);
    Graphics2D g = result.createGraphics();
    g.drawRenderedImage(image, null);
    g.dispose();

    return result;
}
A nehezebb eset, mint említettem, animált gifek átméretezése, mivel a Graphics2D nem támogatja alapértelmezetten azokat. A megoldás nem egyedüli érdemem, bár a letöltött kódokat javítanom kellett. A lényeg, hogy a gif kép-kockáin egyesével végig kell iterálni, és az előbb említett műveletek végrehajtása után, a cél képbe beletenni az immár átméretezett példányokat.
try {
    GifDecoder d = new GifDecoder();
    d.read(image.getAbsolutePath());

    AnimatedGifEncoder e = new AnimatedGifEncoder();
    e.setRepeat(d.getLoopCount());
    e.start(out);

    int type = bufferedImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : bufferedImage.getType();

    for (int i = 0; i < d.getFrameCount(); i++) {
        e.setDelay(d.getDelay(i));
                    
        BufferedImage frameBuffer = new BufferedImage(origWidth, origHeight, BufferedImage.TYPE_BYTE_INDEXED);
        frameBuffer.getGraphics().drawImage(d.getFrame(i), 0, 0, null);

        if (mimeType.equals("image/jpeg") || mimeType.equals("image/pjpeg")) {
            frameBuffer = convert(frameBuffer, BufferedImage.TYPE_INT_RGB);
        } else {
            frameBuffer = createCompatibleImage(frameBuffer);
        }

        e.addFrame(resize(blur(frameBuffer), newWidth.intValue(), newHeight.intValue()));
    }

    e.finish();
    out.flush();
    out.close();
} catch (Exception e) {}
A javított osztályokat egyenlőre pastebin.com-ra töltöttem fel.

Nem állítom, hogy ez a legjobb és legszebb megoldás, és azt sem, hogy tökéletes eredményt hoz minden típusú képnél, csak azt tudom, hogy a transzparens képek kivételével eddig bevált.

2010. március 23., kedd

Egységesített "Global JNDI name" kezelés EJB 3.1 környezetben

Az alkalmazás-szerver, amikor létrehozunk egy Session Bean-t, automatikusan regisztrálja azt a JNDI kontextusban. Az regisztrált Bean-ekre referenciát legegyszerűbben a függőség-injektálással szerezhetünk, a szerver leveszi a direkt név-feloldás terhét vállunkról. Az említett módszer egységesen működik minden alkalmazás-szerveren, viszont a megvalósítása már korántsem egységes. Minden gyártó saját elképzelése szerint oldotta meg a feladatot.
  • JBoss global JNDI name: ejbapp/BeanName/local
  • GlassFish global JNDI name: InterfaceClass
  • WebSphere Community Edition global JNDI name: ejbapp-ejb/BeanName/InterfaceClass
  • Oracle Application Server (OC4J) global JNDI name: BeanName
Mivel az alkalmazás-szerver elrejti előlünk a feloldás problémáját, egészen addig amíg van lehetőségünk a függőség-injektálásra támaszkodni, kódunk hordozható marad. Viszont abban a pillanatban, amikor pl. egy nem menedzselt objektumból, vagy egy unit-tesztelőből kényszerülünk direkt JNDI név-feloldásra, kénytelenek vagyunk gyártó specifikus kódot készíteni.
Egy egyszerű példa szemlélteti a problémát EJB 3 környezetben:
@Remote
public interface JNDITestBeanInterface {
    String test();
}
@Stateless(name="JNDITestBean")
public class JNDITestBean implements JNDITestBeanInterface {
    public String test() {
        return "ok";
    }
}
public class JNDITestBeanClient {
    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        JNDITestBeanInterface testBean = (JNDITestBeanInterface) context.lookup("JNDITest/JNDITestBeanInterface/remote");
        System.out.print(testBean.test());
    }
}
Az EJB 3.1 specifikáció megoldást kínál, mégpedig úgy, hogy egységesíti a név-regisztráció módját, így azon alkalmazás-szerverek, melyek implementálni szeretnék a specifikációt kénytelenek követni is azt.


java:global[/<application-name>]/<module-name>/<bean-name>#<interface-name>
@Stateless
@LocalBean
public class JNDITestBean {
    public String test() {
        return "ok";
    }
}
public class JNDITestBeanClient {
    public static void main(String[] args) throws Exception {
        Context context = new InitialContext();
        JNDITestBean testBean = (JNDITestBean) context.lookup("java:global/JNDITest/JNDITest-ejb/JNDITestBean");
        System.out.print(testBean.test());
    }
}
Az "interface-name" elhagyható, amenniyben interface nélküli Bean-re szeretnénk referenciát szerezni, továbbá ha az egyszerűsített deployment technológiát használva a WAR-ba helyeztük az EJB réteget az "application-name" azaz az alkalmazás neve opcionálissá válik.

2010. március 17., szerda

Cassandra

Több irányból is érkeznek a hírek, hogy nagyobb tartalom-szolgáltatók sorra lecserélik adatbázis megoldásaikat Cassandrára. A Cassandra program arany fokozatú Apache projektté nőtte ki magát, melyet kezdetben a Facebook fejlesztett és tett közzé nyílt forrással. Az adatbázis egy jól skálázható, második generációs, strukturált kulcs-érték tárolására alkalmas Java nyelven írt eszköz, amit kifejezetten több-gépes, clusterezett rendszerekre terveztek.
Néhány alap fogalom:

  • Column: Az oszlop, a legkisebb egysége az adat-tárolásnak, melyben kulcs, érték,és egy idő-bélyeg található.
  • ColumnFamily: Adattábla, mely az oszlopokat fogja össze.
  • SuperColumns: Olyan oszlopokat tartalmaz, melyek értékei további oszlopok.
  • Row: Minden adattábla elszeparált fájlban tárolódik, és ezek a fájlok tárolódnak az un.: Row-ban. Minden Row-nak van egy kulcsa, amivel hivatkozni lehet rá.
  • Keyspace: Lényegében az adatbázis név-tér az adattábláknak, tipikusan egy alkalmazáshoz egy név-tér tartozik.
  • Cluster: A cluster kezeli az elosztott név-tereket.
Ez az egyszerű JSON példa jól szemlélteti a Row, ColumnFamily, és Column struktúráját:
{
   "mccv":{
      "Users":{
         "emailAddress":{"name":"emailAddress", "value":"foo@bar.com"},
         "webSite":{"name":"webSite", "value":"http://bar.com"}
      },
      "Stats":{
         "visits":{"name":"visits", "value":"243"}
      }
   },
   "user2":{
      "Users":{
         "emailAddress":{"name":"emailAddress", "value":"user2@bar.com"},
         "twitter":{"name":"twitter", "value":"user2"}
      }
   }
}
Az adat-szerkezet részletesebb leírása megtalálható itt.
Fontos különbség a relációs adatbázisokhoz képest, hogy míg utóbbiakban az entitások és a közöttük fennálló kapcsolat alapján keresünk, és a keresést index-ek tesszük élhetővé, addig a Cassandra fordított paradigmát igényel, és a tervezésnél azt kell figyelembe vennünk, hogy milyen lekérdezéseket szeretnénk kiszolgálni, mert mert egy adattábla lényegében egy lekérdezésnek "felel meg".
Nézzük, hogyan is működik.
A telepítést részletesen leírja a wiki, ezért arra nem térnék ki. Miután sikeresen feltelepítettük a lánykát a conf/storage-conf.xml állományban tudjuk hangolni a rendszert, illetve itt van lehetőség Keyspace-k és ColumnFamily-k létrehozására, az alábbi módon.
<Keyspaces>
    <Keyspace Name="testKeyspace">
        <KeysCachedFraction>0.01</KeysCachedFraction>
        <ColumnFamily CompareWith="UTF8Type" Name="testColumnFamily"/>
    </Keyspace>
</Keyspaces>
Mentsük el az XML-t indítsuk el a Cassandrát, majd a "cassandra-cli -host localhost -port 9160" paranccsal csatlakozzunk az adatbázishoz. Ha minden jól ment a cassandra> felirat jelenik meg a képernyőn jelezve, ahol is megkezdhetjük a munkát. Első kérésként ellenőrizzük, hogy sikeresen létre hoztuk-e a Keyspace-t.
cassandra>show keyspaces
testKeyspace
system
Ne lepődjünk meg van egy system Keyspace, amit a rendszer maga használ. Jelen esetben létrehoztuk egy testColumnFamily-t amiben az adatokat szeretnénk tárolni UTF-8 string formában, de van mód ASCIIType, BytesType, TimeUUIDType, Super tárolási formákra.
Következő lépésként helyezzünk be egy Column-ot a familybe.
cassandra>set testKeyspace.testColumnFamily ['testUser']['foo'] = 'bar'
Value inserted.
És végül kérdezzük le a felvitt értéket.
cassandra>get testKeyspace.testColumnFamily ['testUser']['foo']
=> (column=foo, value=bar, timestamp=1268818855201)
Mivel leggyakrabban nem parancssorból szeretnénk használni az adatbázist, számtalan programozási nyelvhez létezik már implementáció, köztük természetesen Java-hoz is. Az említett kliensben az előbbi példa e-képpen valósítható meg:
CassandraClient cl = pool.getClient();
KeySpace ks = cl.getKeySpace("testKeyspace");

//insert value
ColumnPath cp = new ColumnPath("testColumnFamily" , null, "testUser".getBytes("utf-8"));
ks.insert("foo", cp , "bar".getBytes("utf-8"));

//get value
Column col = ks.getColumn("foo", cp);
String value = new String(col.getValue(),"utf-8");

pool.releaseClient(cl);
pool.close();
Összegzésként elmondható, hogy Cassandra sem váltja meg a világot, mégis megvan a maga helye és szerepe az adatbázisok területén. Lényeges működésbeli sajátossága hátrány, ha bonyolult adastruktúrákban gondolkozunk, viszont előnye, hogy rendkívül gyorsan képes kiszolgálni a kéréseket. Véleményem szerint relációs adatbázissal vegyesen, mindkettő erősségét (ki)használva optimális megoldáshoz jutunk.

2010. március 9., kedd

JRebel, avagy hogyan tegyük egyszerűbbé az EEletet

Bár elkötelezett híve vagyok a nyílt forrású szoftverfejlesztésnek,a címben említett eszköz mellett mégsem bírtam elmenni csukott szemmel. Mentségükre legyen mondva, a fizetős terméküket bármely szabad szoftver fejlesztéséhez ingyen biztosítják. A JRebel projekt célja, hogy olyan megvalósításokkal támogassa meg a fejlesztőket, melyek nem része a JVM-nek, így hagyományos módon vagy sehogy, vagy igen körülményesen oldhatóak csak meg. A fejlesztők 5 fő pontot neveznek meg, melyek az alábbiak:
1. Új osztályok hozzáadása
A HotSwap protokoll csak meglévő osztályok frissítését tesz lehetővé futás-időben, mivel egy egyedi megfeleltetés van az osztály neve és tartalma között. Ahhoz, hogy új osztályt adhassunk hozzá, kell válasszunk egy class loadert, mivel minden osztály egy specializált loaderben töltődik be. Sajnos ezt a szolgáltatást nem támogatja a HotSwap.
2. Erő-forrás-állományok frissítése
Az osztályaink mellé gyakran teszünk erő-forrás-állományokat melyek leggyakrabban xml vagy properties fájlok. A HotSwap technológia nem támogatja ezen állományok dinamikus módosítását, azaz minden változtatást deployolni kell a szerverre. Ez elég nagy probléma, főleg ha a szerveren van már pl. feltöltött állomány, mert a deploy folyamat esetlegesen törli azokat.
3. Webes erő-források
Minden weben megjelenő alkalmazásban vannak képek, HTML, CSS, JS, stb állományok, melyek az előző ponthoz hasonlóan nem frissülnek automatikusan a web-konténerben.
4. Gyorsító-tárak
Enterprise környezet nagy előnye, hogy a konténer, legyen szó web-konténerről vagy alkalmazás-konténerről, gyorsító-tárrazza osztályainkat. Ez ez előny egyben egy hátrány is, hiszen új elemek, pl. metódusok hozzáadása után a tár törléséig az előző állapot érhető el. Megjegyzem a legtöbb IDE támogatja a "deploy on save" metódust, mely az osztály mentésekor automatikusan frissíti a szerveren az osztályt.
5. Menedzselt komponensek
Egy alkalmazásban számtalan un. menedzselt komponens (EJB-k, Spring bean-ek, stb.) vannak. Az ilyen osztályokra jellemző, hogy általában van függőség-injektálással a konténer által behelyezett osztály, némely állapota társítva van un. instance vagy instance pool-lal, a funkcionalitás megvalósítása különböző rétegeken történik. Frissítésénél nem elegendő magát az osztályt frissíteni, hanem a különböző rétegeket, injectált osztályokat is.
 
Mindezen problémák feloldására a JRebel egy sajátságos metodikát ajánl, amit egy egyszerű, mégis hétköznapi példán mutatnék be. Jelenleg a 2.2.1-es verzió a legfrissebb, amit a letöltés oldalról szerezhetünk meg, 30 napos próbára. A telepítés grafikusan történik pár lépésben. A telepítő felajánlja, hogy befejezés után futtatja is a varázslót, amely megjegyzem példásan kivitelezett alkalmazás, minek segítségével pofon egyszerűvé válik a konfigurálás. A varázslót "UNIX like" környezetben a bin/jrebel-config.sh script futtatásával "varázsolhatjuk" elő.
  • Nincs más dolgunk mint első lépésben a megfelelő IDE-t kiválasztani, én Netbeans-el próbálkoztam, de támogatott az Eclipse, IntelliJ is.
  • Második lépésben felajánlja a varázsló az IDE-knek megfelelő plugint, Netbeans esetén a beépített pluginek közül a legegyszerűbb beszerezni.
  • Következő, sorban a harmadik lépés, hogy alkalmazásunkat felkészítsük a JRebel használatára. A segédlet mindent érthetően elmagyaráz, EJB projekt esetén a források gyökerébe, web projekt esetén a WEB-INF/classes mappában kell egy-egy rebel.xml állományt elhelyeznünk, és az xml-ben specifikálni, hogy mely osztályokat és/vagy erő-forrásokat szeretnénk ha a JRebel kezelné. Itt térnék ki kicsit a működésre. A JRebel egy újabb rétegként rátelepszik az alkalmzásunkra, és a betöltendő osztályokat vagy erő-forrásokat a rebel.xml alapján közvetlenül a fejlesztőkörnyezetből tölti be. Biztos mindenkinek volt már problémája a dinamikusan fordított web-oldalakkal, amikor is a WEB-INF/classes-ban elhelyezett translate.properties-t minden apró változtatás után deployolni kellett. Mivel ez egy gyakori eset, ebből a problémából indultam ki.
<?xml version="1.0" encoding="UTF-8"?>
<application
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.zeroturnaround.com"
xsi:schemaLocation="http://www.zeroturnaround.com http://www.zeroturnaround.com/alderaan/rebel-2_0.xsd">
<web>
<link target="/"><dir name="~/NetBeansProjects/webapp/webapp-war/web"/></link>
</web>
</application>
  • Negyedik lépéséként választanunk kell Java verziót, operációs rendszert, és konténert, utóbbiból eléggé széles a paletta, én Glassfish 3-at használok, ezért azt választottam. Glassfish esetén az adminisztrációs felületen a JVM Setting / JVM Options alatt 2 paramétert kell megadnunk a JVM-nek, mégpedig a -noverify és -javaagent:/utvonal/jrebel.jar.
  • Ötödik lépésben az ügynök, na jó az agent beállításait kell megadnunk. Az advanced gombra kattintva állíthatjuk be pl. azt, ha nem akarunk a rebel.xml-ben abszolút eléréseket megadni, ennek módját részletesen tárgyalja a varázsló. A beállító-panelt "UNIX like" környezetben a bin/agent-settings.sh script futtatásával indíthatjuk.
  • Nincs más dolgunk, mint újraindítani az alkalmazás-szervert, deployolni a rebel.xml-el megfejelt alkalmazásunkat, és máris élvezhetjük a produktumot.
Bár a JRebel fizetős eszköz, a honlapján elhelyezett kalkulátor segítségével gyorsan kiszámolható megéri-e az árát, viszont a pénzben nem mérhető könnyebbséget még a kalkulátor sem tudja számításba venni.