2009. augusztus 28., péntek

Munka egyszerre több projekten

Többször adódhat, hogy a munkahelyen is egyidőben több projekten dolgozunk, sőt esetleg otthon is saját hobbi projektet fejlesztünk, ugyanazon a számítógépen, pl. notebook-on. Ennek megkönnyítésére a NetBeans, és webes alkalmazások esetén a Tomcat is hatékony eszközöket biztosít.

A NetBeans 6.0 Milestone 5-ben jelent meg a Project Group fogalma. Ezzel projekt csoportokat tudunk definiálni. A File/Project Group menüpontban tudunk új projekt csoportot hozzáadni. Egy projekt csoport tartalmazhat szabadon összeválogatott projekteket, egy projektet, és a tőle függő összes projektet, valamint egy könyvtárban lévő projekteket. A szabadon összeválogatott projektek esetén létrehozáskor megadhatjuk, hogy üres projekt csoport jöjjön létre, és ahhoz adjuk hozzá a projekteket, vagy a megnyitott projekteket tegye bele alapértelmezetten a projekt csoportba. Megadhatjuk azt is, hogy új projekt megnyitásakor vagy lezárásakor mentse-e a projekt listát. Egy projekt és a tőle függő projektek esetén a projekt könyvtárát kell megadni, míg egy könyvtárban lévő projekteket tartalmazó projekt csoport esetén a könyvtárat kell megadni. Én általában a szabadon összeválogatott projektet használom, mert így egy csoporthoz szabadon adhatom hozzá, és vehetem el a projekteket.

A projekt csoportok váltása között nem kell újraindítani a NetBeans-t, egyszerűen a File/Project Group menüpontban kell kiválasztani a projekt csoportot. Ekkor azonnal vált, bazárja a bezárandó projekt csoport projektjeit, és kinyitja az új projekteket. A váltáskor megjegyzi a megnyitott állományokat is, így ott folytathatjuk a munkát, ahol abbahagytuk. Ebben a menüpontban lehet a projekt csoport tulajdonságait is szerkeszteni, vagy projekt csoportot törölni.

Ez a koncepció ismerős lehet az Eclipse felhasználóknak, ott Workspace néven fut ugyanez.

A projekt csoportok különösen hasznosak lehetnek pl. olyanoknak, akik prezentációkat tartanak, mert így könnyen lehet váltani a bemutatandó projektek között.

A projekt csoportokat a NetBeans a [user_home/].netbeans/[version/]config/Preferences/org/netbeans/modules/projectui/groups könyvtárban tárolja el, így újratelepítéskor csak ezt a könyvtárat kell átmásolni.

A projekt csoportokról egy remek videó is készült.

Webes alkalmazások fejlesztésekor a NetBeans-ben alapesetben ugyanazt a Tomcat példányt használja, melyre az alkalmazást telepíti, és melyet futtat. A Tools/Servers fülön azonban több Tomcat példányt is fel lehet venni, ha pl. a különböző alkalmazásokat különböző verziójú Tomcat-ekben akarjuk futtatni. Azonban ha több alkalmazást fejlesztünk, gyorsabb lehet, ha mindegyik alkalmazásnak külön Tomcat-et definiálunk, akár ugyanazt a verziót is, és csak azokat a dolgokat konfiguráljuk be, melyekre a különböző alkalmazásoknak szüksége lehet (pl. csak az adott alkalmazáshoz tartozó DataSource-ot inicializálja ekkor). Ezt megoldhatjuk úgy is, hogy a Tomcat-et több példányba bemásoljuk, de lehetőséget biztosít arra is (NetBeans-től függetlenül), hogy a binárisokat megtartsuk egy közös könyvtárba (ezt nevezik Catalina Home-nak), és csak a konfigurációs állományokat másoljuk le több példányba, különböző könyvtárakba (ezt nevezik Catalina Base-nek). Ez utóbbi csak a conf, logs, webapps, work, és temp könyvtárakat tartalmazza. Amennyiben a NetBeans-ben egy új Tomcat-et veszünk fel, külön megadhatjuk mindkettőt. Szerver esetén egyedül a CATALINA_BASE környezeti változót kell beállítanunk. Ez hasznos lehet akkor, ha ugyanazon a szerveren több alkalmazást is futtatni akarunk, de nem akarjuk, hogy egymástól függjenek (pl. az egyik Tomcat-jét a másiktól függetlenül akarjuk indítani/leállítani, vagy az egyik alkalmazás a másiktól egy Tomcat-ben elenné a memóriát). Ekkor a Services fülön a Servers alatt megjelenik a két Tomcat, és jobb klikk/Edit server.xml menüpontban külön tudjuk szerkesztgetni a server.xml-jüket.

Manapság egy webes alkalmazáshoz több, mint 30 JAR-t használunk (3rd party library), ami akár a 20 megát is kiteheti. Amennyiben ezt a WAR-ba csomagoljuk, és újra telepítjük az alkalmazást, ezeket mindig el kell távolítani, majd be kell tölteni, miközben a Tomcat egy 20 megás WAR állományt kezel, kitömörít, stb. Ennek elkerülésére megtehetjük azt, hogy a JAR állományokat nem a WAR-ba csomagoljuk, és a web app classloader-e tölti be, hanem a Tomcat lib könyvtárába másoljuk ezeket, így a common classloader fogja betölteni ezeket. Így fejlesztés közben az alkalmazást újraindítgatva kétszeres sebességnövekedést értem el, így jelentősen rövidült a fejlesztési iteráció hossza. Persze ennél sokkal szebb megoldás, ha a JAR-okat nem a lib könyvtárába másoljuk, hanem a ${catalina.base}/conf/catalina.properties állományában a common.loader porperty értékét kiegészítjük azzal a könyvtárral, ahol a JAR állományaink elhelyezkednek. Itt használhatjuk a ${catalina.home} és ${catalina.base} környezeti változókat is. Így indításkor ezeket is betölti a Tomcat, és nem kell minden alkalmazás telepítéskor. A NetBeans-be ehhez még a Project Properties/Libraries fülön be kell állítani, hogy ne csomagolja a WAR-ba a JAR-okat, hogy a Libraries-eknél a nevük melletti pipát (Package) kikapcsoljuk. Ezt használhatjuk akkor is, ha pl. szerverre telepítünk, és nem 20 megás WAR állományokat akarunk másolni, hanem jelentősen kisebbeket.

2009. augusztus 19., szerda

E-mail kezelés tesztelése

Többször kellett már olyan alkalmazást fejleszteni, mely elektronikus levelet küld SMTP protokollon, vagy egy mailbox tartalmát figyeli POP3, esetleg IMAP protokollon. Ilyen alkalmazás tesztelése elég nehézkes, hiszen kell hozzá egy mail szerver, megfelelő e-mail címekkel és folder-ekkel. Vagy rendelkezésre áll ilyenkor az éles környezet, amin nem szerencsés tesztelni, vagy a legrosszabb esetben még egy mail szervert is telepíteni kell.

Erre ad megoldást a Mock JavaMail projekt. Ez gyakorlatilag négy osztály, mely kihasználja a JavaMail API plugin-olhatóságát. Ezt a Session osztály teszi lehetővé, mely lehetőséget biztosít a különböző protocol provider-ekhez. A protocol provider-ek különböző osztályok, melyek a különböző protokollokat kezelik. Ezek lehetnek Transport leszármazottak pl. küldés esetén, melynek egyik leszármazottja a SMTPTransport osztály az SMTP protokoll kezelése, és lehetnek Store leszármazottak, melyek egy üzenet tárolót reprezentálnak, és az üzenetek kezelését, letöltését teszik lehetővé. A Store leszármazottja a POP3Store POP3 protokoll és az IMAPStore az IMAP protokoll kezelésére. A Session osztály a protokollokhoz tartozó provider-eket a javamail.providers vagy javamail.default.providers állományok alapján tölti be, a protokollokhoz tarozó cím típusokat a javamail.address.map vagy a javamail.default.address.map alapján tölti be. Tipikusan a cím típus SMTP esetén rfc822 (rfc-ben meghatározott), NNTP esetén news.

Ezen állományokat a Session vagy a java.home környezeti változóban meghatározott könyvtár lib alkönyvtárából, vagy a META-INF könyvtárból tölti be. A Mock JavaMail felüldefiniálja az SMTP, POP3 és IMAP protokollokhoz tartozó provider-eket, úgy, hogy a META-INF könyvtárában szerepel a javamail.providers állomány, mely a saját Transport és Store osztályait állítja be. Emiatt a Mock JavaMail nagyon egyszerűen használható, hiszen nem kell mást tenni, mint a JAR állományát (a bejegyzés írásának időpontjában mock-javamail-1.7.jar) el kell helyezni a CLASSPATH-ban.

Ezután pontosan úgy lehet levelet küldeni, mint a standard JavaMail API használatakor, azaz:

MimeMessage msg = new MimeMessage(session);
msg.addRecipient(RecipientType.TO, new InternetAddress("foo@bar.com"));
msg.setSubject("Foo subject");
msg.setText("Foo body");
Transport.send(msg);

Valamint levelet is pontosan ugyanúgy lehet fogadni:

Store store = session.getStore();
store.connect("bar.com", "foo", "bar");
Folder folder = store.getDefaultFolder();
folder.open(Folder.READ_WRITE);
Message[] messages = folder.getMessages();

Ez a kód semmiben nem tér el a JavaMail API normál használatától, azaz a kódunkban akár változatlanul is hagyhatjuk. A különbség annyi, hogy a JAR CLASSPATH-ban való elhelyezése után a teszt eset futtatásakor a JavaMail API a Mock JavaMail provider-eit fogja használni, azaz a mailbox-okat és az üzeneteket a memóriában tárolja, amit a org.jvnet.mock_javamail.Mailbox osztály valósít meg, mely a ArrayList<Message> osztály leszármazottja. Egy mailboxes statikus tagja tárolja az e-mail címekhez tartozó Mailbox példányokat, melyeket a get statikus metódussal lehet elérni. Azaz pl. az Assert-nél használhatjuk a következő kódrészletet a levélküldés után:

List inbox = Mailbox.get("foo@bar.com");
assertEquals(1, inbox.size());
assertEquals("Foo subject", inbox.get(0).getSubject());

Egy Mailbox példányon meghívhatjuk a setError(true) metódust is, ekkor a Mailbox példányhoz való hozzáférés hibát fog dobni. Ezzel tesztelhetjük az alkalmazásunk hibakezelését is.

A Session-t lehet direktben is példányosítani, de egy webes alkalmazás esetében a legszebb megoldás, ha a web konténer vagy az alkalmazásszerverben definiáljuk erőforrásként, és az alkalmazásunk JNDI-vel fér hozzá. Persze teszteléskor, ha a teszt esetet out of container futtatjuk, akkor a Session-t magunknak kell példányosítanunk. Én Spring-et használok, így a Session-t a Spring dependency injection-nel állítja be. Viszont a Session-t nem lehet közvetlenül példányosítani, hanem factory metódusai vannak, mint a getDefaultInstance. Emiatt a Spring-ben a következőképp lehet példányosítani:

<bean id="session" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass"><value>javax.mail.Session</value></property>
<property name="targetMethod"><value>getDefaultInstance</value></property>
<property name="arguments">
<list>
  <props>
      <prop key="mail.store.protocol">pop3</prop>
   <prop key="mail.smtp.host">bar.com</prop>
      <prop key="mail.smtp.user">foo</prop>      
  </props>
</list>
</property>
</bean>

Ahhoz, hogy az alkalmazásunk működjön, a JavaMail API JAR-ját is el kell helyeznünk a CLASSPATH-ban (jelenlegi verzió a 1.4.2, mail.jar), valamint Java 6 előtt a Java Activation Framework legutolsó verzióját (1.1.1, activation.jar). A Java 6-nál ez már nem szükséges, ugyanis már bekerült a javax.activation csomag.

A projekt oldalán lévő letöltés link nekem nem működött, viszont innen letölthető, forráskóddal együtt.

A projekt oldala három alternatívát is megemlít: Wiser, Dumbster, és az Aspirin, de első látásra mindegyik csak egy külön indítható, beépíthető pehelysúlyú SMTP szerver, üzenetek küldésének tesztelésére. Ezzel szemben a Mock JavaMail mock provider-eket tartalmaz, melyekkel a küldésen kívül a fogadás is tesztelhető.

2009. augusztus 12., szerda

Fa ábrázolása adatbázisban

Gyakran megesik, hogy adott egy probléma, melyet már megoldottam az általam használt eszközökkel, de egy kicsit jobban körülnézve olyan alternatív megoldásokat találok, melyekről nem is hallottam, sőt a környezetemben sem ismert, mégis egyszerű, jól használható, csak nem annyira elterjedt.

Ilyenbe futottam, mikor egy egyszerű fát akartam adatbázisban eltárolni. Mivel mostanában a perzisztenciát általában JPA-val oldom meg, nem is foglalkozom az adatbázis tervezéssel, engedem, hogy a JPA provider kigenerálja helyettem az adatbázis sémát. Definiáltam egy Node osztályt, melynek van Node típusú attribútuma (parent), mely egy referencia a szülő objektumra, és van egy List típusú attribútuma (children), mely a gyermek objektumokra mutató referenciákat tartalmazza. A következő forráskód mutatja a Node osztály vázát:

@Entity
public class Node {
...

@ManyToOne
private Node parent;

@OneToMany(mappedBy="parent")
private List children = new ArrayList();

...
}

Az első probléma akkor adódik, ha a gyermekek sorrendje nem mindegy. Ugyanis a JPA alapban nem veszi figyelembe, hogy a List-ben számít a sorrend. Amennyiben egy új gyermeket veszek fel már létező gyermekek mellé, a következő lekérdezésnél nem garantált az, hogy ugyanazon sorrendbe kapom vissza a gyermekeket, ahogy a List-ben szerepeltek. Ez abból adódik, hogy az adatbázis rekordok között nincs sorrend definiálva, így általában olyan sorrendben kapom vissza az elemeket, ahogy az adatbázisba kerültek (de erre sem szabad építeni, mert különböző műveletek, optimalizációk miatt más sorrend is lehetséges).

Ennek megoldására definiálni kell egy új mezőt, legyen a neve "rank", mely alapján a rendezettséget definiálni lehet, és használjuk a @OneToMany annotációval megjelölt mezőn az @OrderBy annotációt, ami hatására a lekérdezéskor a children lista elemei a rank szerinti sorrendben lesznek. Ennek ugye az a hátránya, hogy a rank mezőt mindig karban kell tartani beszúráskor. A rank lehet egy egész szám 0-tól indexelve. Ilyenkor beszúráskor a beszúrandó gyermek utáni elemeket "jobbra kell tolni", a rank értéküket egyel növelni kell. Persze ennél hatékonyabb algoritmusok is léteznek, de ez a legegyszerűbb. Nézzük tehát a módosított Node osztályunkat:

@Entity
public class Node{
...

private int rank;

@ManyToOne
private Node parent;

@OneToMany(mappedBy="parent")
@OrderBy("rank ASC")
private List children = new ArrayList();

...
}

Ennél a megoldásnál jött elő, hogy hogyan kell törölni pl. az összes elemet a táblából. Egy DELETE FROM Node node utasítás nem volt elegendő, mert a következő hibaüzenetet kaptam: Cannot delete or update a parent row: a foreign key constraint fails. Ez azt jelenti, hogy egy elemet nem lehet letörölni, hiszen a gyermek elemek külső kulccsal hivatkoznak rá. Ezért először az összes referenciát null-ra kell állítani (UPDATE Node node SET node.parent = null), és csak aztán lehet a törlést elvégezni.

A következő probléma akkor adódik, mikor egy teljes részfát kell megjeleníteni. Amennyiben egy listába be akarjuk tenni a részfa teljes elemét (pl. grafikusan meg akarjuk jeleníteni és listában kívánjuk átadni a megjelenítési rétegnek), a részfát preorder módon, rekurzívan be kell járnunk. Ekkor viszont minden egyes Node esetén a gyermek-ek lekérdezésekor, amennyiben LAZY fetch type-ot alkalmaztunk (és @OneToMany kapcsolat esetén ez az alapértelmezett), minden esetben lefut egy query, mely lekérdezi a gyermek Node-okat. Akkor sem jobb a helyzet, ha EAGER fetch type-ot alkalmazunk, mert ekkor nem a bejáráskor, hanem már a betöltéskor fog lefutni az összes query, azaz pontosan annyi, amennyi Node-ból áll az adott részfánk.

A háttérben a Node osztály egy NODE táblára képződik le, melynek van egy PARENT_ID külső kulcsa, és a gyermekek lekérdezése esetén egy olyan SELECT-et futtat, mely lekérdezi az összes olyan NODE sort, melynek PARENT_ID mezője megegyezik a szülő azonosítójával (SELECT * FROM node WHERE parent_id = ? ORDER BY rank ASC).

A hierarchikus adatok kezelésére bizonyos adatbázisok beépített megoldásokat tartalmaznak, pl. Oracle esetén a START WITH és CONNECT BY parancs, vagy esetleg tárolt eljárásban is megvalósíthatjuk a hierarchia elemeinek összegyűjtését. Mindkettő használható JPA-ból is, de ekkor bukjuk a platform függetlenséget.

Viszont van egy ennél sokkal hatékonyabb hierarchikus adatszerkezet reprezentáció, melyet nested tree-nek hívnak. Ez ugyan lekérdezéskor gyors, de módosításkor a több művelet miatt lassabb, így csak akkor érdemes használni, ha sok a lekérdezés, és viszonylag kevés a módosítás. Azért a legtöbb CRM rendszer ebbe a kategóriába sorolható.

A megértéshez nézzünk is egy példa fát. A trükk, hogy minden egyes Node-hoz felveszünk egy bal és egy jobb értéket. Egy létező fa esetén ezeket a számokat úgy osztjuk ki, hogy a gyökér elem bal értéke lesz az 1, és megyünk végig gyermekeken mélységi kereséssel, és a bal értékhez mindig hozzáadunk egyet. Ha levélelemhez érünk, akkor folytatjuk a számolást visszafele, a szülő fele, és kitöltjük a jobb értékeket, míg egy testvérhez érünk, és ekkor ismét a bal értékeket osztjuk ki, és így tovább. Gyakorlatilag óramutató járásával ellentétes irányban körbejárjuk a fát.

Az így nyert számozás azért nagyon hatékony részfák lekérdezésére, mert egy elem részfáját úgy kérhetjük le, ráadásul a preorder bejárással adott sorrendnek megfelelően, hogy lekérdezzük azon elemeket, melyek bal értéke a szülő bal értékénél nagyobbak és a jobb értékénél kisebbek, a bal érték alapján rendezve. Így a teljes részfa egy lekérdezéssel előállítható, szemben a rekurzív módszerrel, ahol annyi lekérdezésre volt szükség, ahány elemből állt a fa. Ez a sorrend tökéletes a fák ábrázolására, pl. egy menü vagy egy oldaltérkép megjelenítésére.

A számozás alapján látható, hogy egy elem összes leszármazottjának a számát is könnyen megkaphatjuk: (bal érték - jobb érték - 1) / 2. Ez akkor lehet különösen hasznos, ha ezt a felületen is meg szeretnénk jeleníteni.

Szintén könnyen észrevehető összefüggés, hogy a levélelemekre igaz az az állítás, hogy jobb érték - bal érték = 1.

Az egyszerűség kedvéért hagyjuk meg a szülőre mutató referenciát, valamint javasolt egy attribútum felvétele, mely megadja az adott elem mélységét a fában (level). Mindkettőt persze ki lehet számolni lekérdezésekkel is. De ezen attribútumok használatával egyszerűbbek lesznek a lekérdező műveleteink, de több helyet foglal, és módosításkor többet kell adminisztrálni. Valamint a rank attribútumra már nem lesz szükség, hiszen a bal értékek már definiálnak egyfajta sorrendet. Így néz ki tehát az osztályunk:

public class Node {
...

private int level;

@ManyToOne
private Node parent;

private int left;

private int right;
...
}

Egy adott elem és annak leszármazottainak lekérdezése tehát a következő JPA lekérdezéssel adható meg, ahol az első paraméter a kiválasztott elem bal értéke, a második paraméter a kiválasztott elem jobb értéke:

SELECT node FROM Node node WHERE node.left BETWEEN :first AND :last ORDER BY node.left

Tehát ha pl. a java.awt.Window elemet és annak összes leszármazottját akarjuk lekérdezni, akkor az összes elemet kell lekérdezni, melynek a bal értéke nagyobb, mint 3, és a jobb értéke kisebb, mint 14, a bal érték szerint rendezve. Igaz, hogy a lekérdezés nagyon egyszerű, de a beszúrás viszont bonyolultabb. Amennyiben ugyanis egy elemet akarunk beszúrni, először ki kell választani a beszúrás helyét. Ha a beszúrandó elem

  • a kiválaszott elem első gyermeke lesz, az új elem bal értéke a kiválasztott elem bal értéke + 1 lesz
  • a kiválaszott elem utolsó gyermeke lesz, az új elem bal értéke a kiválasztott elem jobb értéke lesz
  • a kiválaszott elem bal testvére lesz, az új elem bal értéke a kiválasztott elem bal értéke lesz
  • a kiválaszott elem jobb testvére lesz, az új elem bal értéke a kiválasztott elem jobb értéke + 1 lesz

Az új elem jobb értéke, mivel az elemnek még nincs gyermeke, az új elem bal értéke + 1 lesz. Ez után egy "eltoltást" kell elvégezni, ugyanis lehetséges, hogy az így kiosztott számok már foglaltak. Ehhez két művelet szükséges, egyrészt az összes olyan elem bal értékéhez, melynek a bal értéke nagyobb, vagy egyenlő, mint a beszúrandó elem bal értéke, hozzá kell adni 2-t. Valamint az összes olyan elem jobb értékéhez, melynek a jobb értéke nagyobb, vagy egyenlő, mint a beszúrandó elem bal értéke, hozzá kell adni 2-t. Ez két egyszerű JPA bulk update művelettel is elvégezhető, ahol a paraméter a beillesztendő elem bal értéke:

UPDATE Node node SET node.left = node.left + 2 WHERE node.left >= :position
UPDATE Node node SET node.right = node.right + 2 WHERE node.right >= :position

Vegyük példaként, hogy a java.awt.Dialog elem első gyermekeként egy java.awt.FileDialog elemet akarunk felvenni. Az új elem bal értéke 9 lesz és a jobb értéke 10. Majd az összes elem bal értékét 2-vel növelni kell, melynek bal értéke nagyobb vagy egyenlő, mint 9, és ugyanígy az összes elem jobb értékét 2-vel növelni kell, melynek jobb értéke nagyobb vagy egyenlő, mint 9.

Ezek alapján a további műveletek is könnyen implementálhatóak. Egy részfa törlésénél töröljük az adott elemet, majd az így keletkezett "lyukra" húzzuk vissza a mögötte elhelyezkedő elemeket. Mivel nem csak egy elemet, hanem teljes részfát is törölhetünk, az eltolás nem 2-vel történik, hanem a részfa elemszáméval, melynek kiszámolását már fentebb leírtam.

A gyökértől adott elemhez vezető utat is egy lekérdezéssel megkaphatjuk, ugyanis a felmenő elemek mindegyikére igaz, hogy bal értékük kisebb, mint a kiválasztott elem bal értéke, és a jobb értékük viszont nagyobb, mint a kiválaszott elem jobb értéke. Ezt tehát a következő JPA lekérdezéssel kapjuk meg, ahol az első paraméter a kiválasztott elem jobb, a második a bal értéke:

SELECT node FROM Node node WHERE node.left < :left AND node.right > :right ORDER BY node.left

Az elem közvetlen gyermekeinek, valamint egy elem testvéreinek lekérdezése a parent mutató miatt nagyon egyszerű. A legbonyolultabb persze a részfa mozgatása, itt is először meg kell határozni a mozgatni kívánt elem új bal és jobb értékét. Ez megegyezik a beszúrással. Ezután helyet kell csinálni a részfának, azaz a kiszámolt bal értékhez el kell tolni az elemeket annyival, amennyi elem van a részfában. Mind az eltolást, mind az elemek számának kiszámolását említettem. Eztán el kell mozgatni a részfát az üres helyre, szintén a bal és jobb elemek frissítésével, majd a keletkezett üres hely mögötti elemeket kell "visszahúzni" (bal és jobb értékek csökkentésével).

Ebből látszik, hogy ez a megoldás sem egyértelműen jobb, mint a klasszikus mutatós megoldás, de akkor jobban használható, ha sokkal több a lekérdezés, mint a módosítás. Én úgy valósítottam meg a fa műveleteket, hogy definiáltam egy interfészt, melybe felsoroltam a metódusokat, és az interfészt két osztály implementálta, az egyik a klasszikus műveletekkel, a másik a nested tree-vel. Persze az elemnek is csináltam egy közös absztrakt osztályt, melynek egyik leszármazottja mutatókat tartalmazott, másik leszármazottja pedig a bal és jobb értékeket. Persze Spring service-ként implementáltam őket, emiatt konfigurációval azonnal választani lehet a két implementáció között. Ez gyakorlatilag a strategy tervezési minta egy megvalósítása.

A témáról egy angol nyelvű cikk ír, mely egy PHP-s megoldást mutat be, bár a mozgatást nem részletezi. Sőt egy magyar cikk is megjelent, szintén PHP megvalósítással, miközben ezt a post-ot írtam (három részes, a negyedik cikk még nem jelent meg, ami a fa módosításáról fog szólni). A post írásakor ez a Java kódrészlet is nagyon hasznos volt, mely egyszerű JDBC-t használ.