Java i XML, programowanie, Java


11

XML
na potrzeby konfiguracji

Niniejszy rozdział zawiera informacje dotyczące sposobów zastosowania języka XML w danych służących do konfiguracji. W poprzednich rozdziałach Czytelnik poznawał sposoby zastosowania tego języka do przesyłania danych pomiędzy aplikacjami i do tworzenia warstwy prezentacyjnej — w tym rozdziale zostaną omówione sposoby wykorzystania XML-a do przechowywania da­nych. Aby zrozumieć przesłanki przemawiające za użyciem XML-a na potrzeby konfiguracji, wy­star­czy napisać aplikację korzystającą z obszernych plików właściwości albo serwer konfigu­ro­wa­ny na podstawie plików, a nie argumentów wiersza poleceń. W obu tych przypadkach format plików dostarczających informacje do aplikacji jest umowny i zazwyczaj odpowiada tylko okreś­lo­nej aplikacji. Programista ustala format pliku, buduje moduł odczytujący taki plik i aplikacja zostaje związana ze swoim plikiem konfiguracyjnym na zawsze. Oczywiście, takie postępowanie nie uwzględnia dalekowzrocznych planów związanych z rozwijaniem tej aplikacji.

Programiści i inżynierowie systemów zdali sobie sprawę z kłopotów z pielęgnacją tak napisanego oprogramowania (zapominanie o przecinkach, niepewność dotycząca znaków wskazujących po­czą­­tek komentarza itd.). Jasne stało się, że potrzebny jest standard reprezentacji tego typu danych niezwiązany z konkretną aplikacją. Jednym z takich standardów, wciąż wykorzystywanym, ale poz­­ba­wionym wielu użytecznych cech, jest klasa java.util.Properties i odpowiednie pliki właściwości. Konstrukcje te, wprowadzone w pakiecie Java Development Kit (JDK) 1.0, umoż­liwiają wygodne z punktu widzenia Javy przechowywanie informacji konfiguracyjnych, jednakże nie udostępniają żadnego sposobu grupowania lub budowania hierarchii. Aplikacja klienta ma taki dostęp do informacji serwera, jak serwer do informacji klienta i programista nie jest w stanie prze­pro­wadzić żadnego logicznego grupowania wewnątrz takiego pliku. Co więcej, kiedy hierarchi­czne parametry konfiguracyjne zyskały na popularności, takie zagnieżdżanie było trudne do osią­gnięcia za pomocą innych rozwiązań bez wprowadzania jeszcze bardziej złożonych (a wciąż współ­pracujących tylko z daną aplikacją) formatów plików. XML przyniósł rozwiązanie wszyst­kich tych problemów — oferował standardowy, prosty sposób reprezentacji informacji konfiguracyjnych.

Format XML nadaje się do szerokich zastosowań administracyjnych. Możliwe jest zbudowanie ogól­nej aplikacji ładującej definicję DTD lub schemat, a następnie plik konfiguracyjny i umoż­li­wia­jącej dodawanie, aktualizację, usuwanie i modyfikację informacji konfiguracyjnych. Jedna apli­kacja, wykorzystująca jeden lub wiele plików konfiguracyjnych XML, mogłaby służyć jako jeden interfejs administracyjny. Jeśli porównamy to z całą mnogością plików haseł, plików sha­dow, użytkowników, grup i skryptów inicjalizacyjnych, to na pewno takie rozwiązanie jest przej­rzystsze i prostsze w obsłudze.

Ponieważ XML jest już wykorzystywany w wielu aplikacjach, dodanie rozszerzenia do prze­twa­rzania i obsługi plików konfiguracyjnych było tylko kwestią czasu. Aplikacje nie wykorzystujące jeszcze XML-a mogą wprowadzać pliki konfiguracyjne oparte na tym języku; jest to o wiele prostsze niż dodanie obsługi przesyłania danych XML lub przekształcania XML-a do innych formatów. Tak czy inaczej, konfiguracja za pomocą XML-a okazała się dobrym rozwiązaniem w wielu sytuacjach. Od kiedy przedstawiono specyfikację Enterprise JavaBeans(EJB) 1.1, wyma­ga­jącą deskryptorów wdrożeniowych EJB w formacie XML, wykorzystanie tego języka w kon­fi­guracji stało się bardzo popularne. Wielu programistów obawiających się obciążenia wynikającego z uruchomienia parsera XML lub wątpiących w perspektywy standardu XML nagle musiało wy­ko­rzystać ten język do wdrożenia obiektów biznesowych w serwerach EJB. Spowodowało to lo­gicz­ną migrację wszystkich danych konfiguracyjnych do tego formatu i przyczyniło się nawet do zmniejszenia złożoności niejednej aplikacji. W niniejszym rozdziale zostaną przedstawione spo­soby wykorzystania XML-a do konfigurowania własnej aplikacji.

Najpierw zostaną omówione używane obecnie sposoby korzystania z XML-a na potrzeby kon­fi­gu­ra­cji. Przedstawione zostaną deskryptory wdrożeniowe EJB pod kątem decyzji projektowych, ja­kie podjęto przy tworzeniu specyfikacji tego rodzaju plików. To stanowić będzie przygotowanie do stworzenia własnych plików konfiguracyjnych XML. Zaczniemy od zbudowania pliku kon­fi­guracyjnego dla serwera, który stworzyliśmy w poprzednim rozdziale. Następnie napiszemy klasy narzędziowe przetwarzające i ładujące te informacje do naszych klas XML-RPC — to znacznie zwięk­szy elastyczność omawianego serwera i jego klientów. Informacje o konfiguracji w prosty spo­sób załadujemy za pomocą interfejsów JDOM. W zakończeniu rozdziału zostaną porównane możliwości XML-a i innych mechanizmów składowania danych — baz danych i serwerów usług katalogowych. Dzięki temu Czytelnik będzie mógł wyrobić sobie opinię na temat praktycznych za­stosowań XML-a.

Deskryptory wdrożeniowe EJB

Zanim zbudujemy własne pliki konfiguracyjne oraz oprogramowanie pozwalające z nich korzys­tać, przyjrzymy się istniejącym formatom i szablonom. Tematem niniejszej książki nie jest spe­cy­fikacja EJB, ale krótki opis deskryptorów wdrożeniowych EJB potrzebny jest do zrozumienia zasady działania plików konfiguracyjnych XML; opis ten podpowie również, jak zaprojektować włas­ny for­mat danych konfiguracyjnych. W niniejszym podrozdziale zostaną także omówione naj­ważniejsze decyzje projektowe leżące u podstaw plików konfiguracyjnych deskryptorów.

Jednak przed omówieniem deskryptorów wdrożeniowych warto zastanowić się, dlaczego w ogóle programiści EJB „przeszli” na XML. Specyfikacja EJB 1.0 wymagała stosowania uszeregowanych deskryptorów wdrożeniowych; niestety, to była jedyna wskazówka oferowana przez autorów spe­cyfikacji. Każdy producent EJB wymagał więc własnego formatu deskryptorów wdrożeniowych dla swojego serwera i zmuszał programistę do uruchamiania narzędzia (a nawet pisania własnych narzędzi) szeregujących deskryptor. Specyfikacja EJB (a także ogólnie język Java) utraciła prawo do miana WORA (Write Once Run Anywhere — aplikacja raz napisana może zostać uru­cho­mio­na gdziekolwiek). XML udostępniał standardowy sposób obsługi deskryptorów wdrożeniowych, a także zniwelował potrzebę korzystania z własnych narzędzi szeregujących. Co więcej, firma Sun
udostępniła definicję DTD EJB, dzięki której deskryptory wdrożeniowe dowolnego producenta zgo­dne są z jedną specyfikacją; system EJB został w dużym stopniu uniezależniony od platformy i produktów określonego producenta.

Podstawy

Podobnie jak w przypadku dowolnego dokumentu XML, deskryptor wdrożeniowy EJB posiada definicję DTD, z którą utrzymuje zgodność. Jak już wcześniej wspomniano, w przyszłych wer­sjach specyfikacji definicja ta zostanie prawdopodobnie zastąpiona schematem. Tak czy inaczej, dokumenty XML używane w konfiguracji muszą tutaj (nawet bardziej niż gdzie indziej) prze­strzegać narzuconych na nie reguł. Bez zawężeń informacje w nich zamieszczone mogłyby okazać się niepoprawne lub bezużyteczne, a na tym cierpiałaby cała aplikacja. Po określeniu zawężeń deskryptor wdrożeniowy rozpoczyna się od elementu głównego ejb-jar. Niniejsza uwaga może wydawać się banalna, ale nazwanie elementu głównego stanowi istotny element budowania do­wol­nego dokumentu XML. Na elemencie tym spoczywa ciężar odpowiedzialności za reprezen­ta­cję wszystkich informacji zawartych w danym dokumencie XML. Szczególnie w przypadku, kiedy inne osoby będą musiały zarządzać naszymi dokumentami, właściwe ich nazwanie może oszczę­dzić im wiele kłopotów. Poniżej pokazano istotne fragmenty deskryptora wdrożeniowego XML zgodnego ze specyfikacją EJB:

<?xml version="1.0"?>

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise

JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd">

<ejb-jar>

<description>

Ten plik ejb-jar zawiera obiekty enterprise beans będące

częścią aplikacji samoobsługowej dla pracowników.

</description>

...

</ejb-jar>

Zastosowanie przestrzeni nazw (które w czasie tworzenia specyfikacji EJB 1.1 były jeszcze w po­wijakach) powoduje, że nazewnictwo elementu głównego i pozostałych jest bardziej przejrzyste. To zaś upraszcza określenie celu dokumentu; należy zauważyć, jak wiele dwuznaczności unik­nięto poprzez zastosowanie w deskryptorze wdrożeniowym przestrzeni nazwy takiej jak Dep­loy­mentDescriptor lub nawet EJB-DD. Wystarczy spojrzeć na przedrostek elementu, a wszystkie wątpliwości znikają.

Organizacja

Podobnie jak nazwy są istotne dla przejrzystości dokumentu, jego organizacja ma zasadnicze zna­czenie dla użyteczności danych jako źródła konfiguracji. Sposób organizacji i zagnieżdżania ele­men­tów i atrybutów nie tylko pomaga w zrozumieniu przeznaczenia dokumentu, ale gwarantuje również, że zawarte w nim informacje konfiguracyjne będą mogły być wykorzystane w różnych aplikacjach. Równie ważna jest umiejętność ustalenia, kiedy nie składować informacji tak, by była ona współużytkowana. Szczególnie istotny jest tutaj przypadek deskryptora wdrożeniowego; każ­dy obiekt EJB ma działać niezależnie od innych i posiadać tylko te informacje, które uzyskał z od­po­­wiadającego mu pojemnika (ang. containter). Może to stanowić problem, jeśli obiekty bean współdziałają ze sobą poza ścisłymi zasadami narzucanymi przez programistę — istnieje wtedy możliwość zburzenia logiki biznesowej i obniżenia wydajności. Dlatego właśnie każdy wpis EJB jest niezależny od pozostałych.

W poniższym przykładzie za pomocą dokumentu XML opisano obiekt bean sesji:

<enterprise-beans>

<session>

<description>

Obiekt bean EmployeeServiceAdmin stanowi implementację sesji

wykorzystywanej przez administratora aplikacji.

</description>

<ejb-name>EmployeeServiceAdmin</ejb-name>

<home>com.wombat.empl.EmployeeServiceAdminHome</home>

<remote>com.wombat.empl.EmployeeServiceAdmin</remote>

<ejb-class>com.wombat.empl.EmployeeServiceAdmin-Bean</ejb-class>

<session-type>Stateful</session-type>

<transaction-type>Bean</transaction-type>

<resource-ref>

<description>

Referencja do bazy JDBC.

EmployeeService zawiera dziennik wszystkich transakcji

dokonywanych poprzez bean EmployeeService na potrzeby

audytu.

</description>

<res-ref-name>jdbc/EmployeeAppDB</res-ref-name>

<res-type>javax.sql.DataSource</res-type>

<res-auth>Containter</res-auth>

</resource-ref>

</session>

</enterprise-bean>

Elementy nie tylko izolują ten obiekt bean od wszelkich innych, ale również umożliwiają logiczne grupowanie danych. Element resource-ref zawiera informacje odpowiadające określonemu wpi­­sowi związanemu ze środowiskiem. W ten sposób aplikacja przetwarzająca i wykorzystująca dane, a także programiści i administratorzy systemu zarządzający aplikacją mogą w prosty sposób zlokalizować i zaktualizować informacje o obiekcie bean lub serwerze EJB.

Większą grupą — i nie tak łatwo rozpoznawalną — jest element enterprise-beans. Dzięki nie­mu możliwe jest zamieszczenie informacji specyficznych dla danego pojemnika, a nie ma­jących zastosowania do obiektów bean; informacje te nie zostaną wymieszane z tymi specy­fi­cz­nymi dla EJB. Jest to ważne rozgraniczenie; za jego pomocą w dalszej części rozdziału oddzielimy informacje konfiguracji serwera i klientów XML-RPC. Wreszcie — do tego elementu macie­rzys­te­go można dodać dowolną liczbę obiektów bean; tutaj zamieszczony jest przykład tylko jednego obiektu sesji bean, ale można ich wstawić dowolnie wiele, tak by odpowiadały wielu obiektom bean w tworzonym pliku jar.

Choć pokazany plik XML został opisany pobieżnie, Czytelnik może już domyślać się, jak na­zy­wać i organizować pliki XML tego rodzaju we własnych aplikacjach oraz w opisywanym przy­kładzie XML-RPC. Niemal każda aplikacja ma odmienne potrzeby, co pociąga za sobą odmienną organizację dokumentu XML oraz inny zestaw zawężeń. Skoro przyjrzeliśmy się już przykładowi konfigurowania serwera aplikacji za pomocą języka XML i zaczęliśmy zastanawiać się nad stwo­rzeniem własnego pliku konfiguracyjnego, spróbujmy podejść do tematu praktycznie i zbudować właśnie taki plik dla naszych klas XML-RPC.

Tworzenie pliku konfiguracyjnego XML

Aby zastosować naszą wiedzę w praktyce, zbudujemy plik konfiguracyjny XML dla klas XML-RPC, które napisaliśmy w poprzednim rozdziale. Posłuży nam to jako doskonały przykład za­sto­so­wania informacji konfiguracyjnych w formacie XML; mamy już dostępny parser XML (wy­korzystany w serwerze XML-RPC) i możliwe jest użycie tego samego pliku konfiguracyjnego dla klientów i serwera. Co więcej, plik konfiguracyjny mógłby być edytowany za pomocą interfejsu IDE XML, a nie za pomocą własnego interfejsu do edycji pliku we własnym formacie. To wszy­stko przyczynia się do zmniejszenia ilości kodu koniecznego do budowania złożonych aplikacji.

Zanim zaczniemy pisać plik konfiguracyjny, musimy określić, jakie informacje będzie można z nie­go odczytać:

To wszystkie informacje, jakie trzeba przekazać serwerowi i klientom, aby te mogły zostać uru­cho­mione z tylko jednym parametrem — położeniem pliku konfiguracyjnego. Pamiętając o tym, zabierzmy się do pisania samego pliku konfiguracyjnego.

Od czego zacząć?

Podobnie jak w przypadku deskryptora wdrożeniowego EJB, nasz plik musi zawierać standardowy nagłówek XML. To chyba nie powinno przedstawiać trudności; poza tym trzeba jeszcze tylko pa­miętać o określeniu przestrzeni nazw i elementu głównego dokumentu. Choć w środowisku pro­duk­cyjnym użylibyśmy przestrzeni nazw kojarzącej się z przeznaczeniem dokumentu (np. XMLRPC lub XmlRpcConfig), tutaj wciąż będziemy używać nazwy JavaXML, starając się zachować je­dnolity schemat we wszystkich przykładach w tej książce. Deklarujemy przestrzeń nazw tak jak we wcześniejszych rozdziałach. Element główny pełni rolę identyfikatora przeznaczenia dokumentu; tak więc nazwa xmlrpc-config będzie tutaj odpowiednia. Często jest tak — szcze­gólnie w złożonych dokumentach XML — że najprostsze rozwiązanie jest rozwiązaniem naj­lep­szym. Nazewnictwo elementów XML nie stanowi tutaj wyjątku.

Po podjęciu tych wstępnych decyzji można rozpocząć budowę pliku konfiguracyjnego dla po­siadanych klas XML-RPC. Poniżej przedstawiona jest początkowa deklaracja XML oraz element główny:

<?xml version="1.0"?>

<JavaXML:xmlrpc-config

xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"

>

</JavaXML:xmlrpc-config>

W tym miejscu można jeszcze dodać odwołanie do definicji DTD lub schematu zawężającego do­kument oraz instrukcje przetwarzania dla aplikacji, które potem będą wykorzystywały i prze­twa­rzały ten plik. Tutaj jednak pominiemy te elementy — nasz program będzie po prostu przetwarzał do­kument „tak jak jest” i zwracał wymagane dane konfiguracyjne serwerowi i klientom XML-RPC.

Organizacja

Mając już taki szkielet pliku konfiguracyjnego, pora zastanowić się nad jego organizacją. Chodzi tu zarówno o grupowanie elementów, jak i o określenie, czy informacje konfiguracyjne będą współ­­­użytkowane przez serwery i klienty. Najlepszym wyjściem będzie pogrupowanie pliku tak, jak gru­powalibyśmy informacje konfiguracyjne „ręcznie”. Nasze wymagania nie są zbyt wielkie, więc proces ten będzie łatwo przeprowadzić.

Sam serwer wymaga przede wszystkim następujących informacji:

Te natomiast będą powtarzane wielokrotnie i można je pogrupować; w zestawie procedur obsługi każda procedura obsługi będzie zawierała identyfikator i nazwę klasy:

Klient XML-RPC korzysta przynajmniej z trzech różnych informacji; wiemy jednak, że port klien­ta będzie taki sam jak serwera XML-RPC. Najprawdopodobniej to samo dotyczy sterownika SAX. Sensownie więc byłoby współdzielić tę informację tak, żeby ewentualne zmiany wprowadzać tyl­ko w jednym elemencie XML, a nie oddzielnie dla klienta i serwera. Ponieważ port i klasa SAX są współ­użytkowane, sensownie jest z kolei dodać do tych współużytkowanych informacji nazwę hosta; co prawda korzysta z niej tylko klient, ale „dobrze pasuje” do numeru portu wyko­rzys­ty­wa­ne­go w żą­daniach XML-RPC:

Zasadnicza organizacja pliku jest już określona. Mamy dwie grupy informacji konfiguracyjnych: „informacje współużytkowane” (wykorzystywane przez klienta i serwer) oraz „informacje o pro­ce­­du­rach obsługi” z wpisami typu „procedura obsługi” dla każdej procedury wykorzystanej w XML-RPC. Powstaną więc dwa podstawowe podzbiory w pliku konfiguracyjnym; ten ostatni bę­dzie zawierał elementy zagnieżdżone, opisujące dalsze podzbiory.

Informacje współdzielone

Niewiele pozostało do dodania odnośnie nazwy hosta, numeru portu i klasy parsera SAX, które nasz serwer oraz klienty wykorzystają w momencie uruchamiania i w czasie połączenia. Nawet nad nazwami odpowiednich elementów nie ma się co długo zastanawiać: hostname, port i par­serClass. Znów obowiązuje zasada — im prościej, tym lepiej. Aby zobrazować sposób wyko­rzys­tania atrybutów, do elementu port dodamy atrybut type. Atrybut ten przyjmie wartość „pro­tected” (chroniony) lub „unprotected” (niechroniony). Kiedy port jest chroniony, aby uzyskać połączenie, trzeba przedsięwziąć pewne dodatkowe czynności, np. zakodować żądanie protokołem SSL. W naszych przykładowych klasach XML-RPC serwer nasłuchuje na porcie niechronionym; jednak zastosowanie takiego atrybutu przyczynia się do zwiększenia elastyczności rozwiązania — pozwala w przyszłości dodać obsługę portów chronionych:

<?xml version="1.0"?>

<JavaXML:xmlrpc-config

xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"

>

<!-- Informacje o konfiguracji serwera i klientów -->

<JavaXML:hostname>localhost</JavaXML:hostname>

<JavaXML:port type="unprotected">8585</JavaXML:port>

<JavaXML:parserClass>

org.apache.xerces.parsers.SAXParser

</JavaXML:parserClass>

</JavaXML:xmlrpc-config>

Procedury obsługi XML-RPC

Definiując procedury obsługi przede wszystkim musimy zagwarantować, że będą one wyko­rzys­ty­wa­ne tylko przez serwer XML-RPC. Choć nie mamy żadnych innych informacji poza konfiguracją procedur obsługi rozumianą tylko przez serwer, to jest możliwe — a nawet dość prawdopodobne — że kiedyś do pliku konfiguracyjnego zostaną dodane informacje bardziej specyficzne dla ser­we­ra. Nasz parser, zamiast szukać specyficznych elementów (i wymuszać zmianę kodu po do­da­niu nowych informacji konfiguracyjnych), może szukać elementu specyficznego dla serwera, np. xmlrpc-server. Aplikacja serwera odczyta zawarte w nim informacje, zaś klienty zignorują je i w ogóle nie będą musiały znać ich składni. Oprócz tego tak przygotowany plik konfiguracyjny jest czytelniejszy dla człowieka. Tak więc elementem xmlrpc-server „otoczymy” informacje o procedurach obsługi.

Ponadto konieczne jest pogrupowanie procedur obsługi — wykorzystamy tutaj element o nazwie handlers. I znów — grupowanie upraszcza określenie przeznaczenia i sposobu wykorzystania in­formacji konfiguracyjnych. Do konfiguracji serwera dodajemy informacje opisujące klasy Hel­loHandler i Scheduler jako procedury obsługi XML-RPC:

<?xml version="1.0"?>

<JavaXML:xmlrpc-config

xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"

>

<!-- Informacje o konfiguracji serwera i klientow -->

<JavaXML:hostname>localhost</JavaXML:hostname>

<JavaXML:port type="unprotected">8585</JavaXML:port>

<JavaXML:parserClass>

org.apache.xerces.parsers.SAXParser

</JavaXML:parserClass>

<!-- Informacje o konfiguracji serwera -->

<JavaXML:xmlrpc-server>

<!-- Lista procedur XML-RPC do zarejestrowania -->

<JavaXML:handlers>

<JavaXML:handler>

<JavaXML:identifier>hello</JavaXML:identifier>

<JavaXML:class>HelloHandler</JavaXML:class>

</JavaXML:handler>

<JavaXML:handler>

<JavaXML:identifier>scheduler</JavaXML:identifier>

<JavaXML:class>Scheduler</JavaXML:class>

</JavaXML:handler>

</JavaXML:handlers>

</JavaXML:xmlrpc-server>

</JavaXML:xmlrpc-config>

Nawet w tak niewielkim dokumencie można zastosować zamienne sposoby reprezentacji danych. Możliwe byłoby opisanie procedur obsługi za pomocą następującej struktury:

<handler id="hello" class="HelloHandler" />

W niemal każdym dokumencie będziemy musieli wybrać nie tylko to, jakie dane przechowywać, ale również jak je przechowywać. W naszym przykładzie wykorzystujemy element handler; wynika to z faktu, że w przyszłości być może zechcemy dodać nowe informacje o procedurze ob­sługi (np. opis lub lokalizacja klasy w sieci). Element z zagnieżdżonymi elementami potomnymi pozwala na bezproblemowe dodawanie takich informacji jako nowych elemenówy potomnych; dodawanie ich jako atrybuty pogorszyłoby czytelność zapisu (element byłby bardzo długi).

Zawężenia dokumentu

Wspomnieliśmy, że w pliku deskryptora wdrożeniowego EJB definicja DTD służyła do za­gwarantowania, że nie zostaną wykorzystane nieprawidłowe elementy i atrybuty — dzięki temu plik jest „rozumiany” przez dowolny serwer. To samo trzeba zrobić z naszym plikiem kon­fi­gu­racyjnym. Stworzenie definicji DTD (w przypadku tak skromnego pliku będzie ona bardzo prosta) gwarantuje, że nasza aplikacja może oczekiwać od dokumentu zgodności z pewnymi narzuconymi zasadami. W przykładzie 11.1 przedstawiono kompletną definicję DTD dla stworzonego pliku XML.

Przykład 11.1 Definicja DTD dla pliku konfiguracyjnego XML-RPC

<!ELEMENT JavaXML:xmlrpc-config (JavaXML:hostname,

JavaXML:port,

JavaXML:parserClass,

JavaXML:xmlrpc-server)>

<!ATTLIST JavaXML:xmlrpc-config

xmlns:JavaXML CDATA #REQUIRED

>

<!ELEMENT JavaXML:hostname (#PCDATA)>

<!ELEMENT JavaXML:port (#PCDATA)>

<!ATTLIST JavaXML:port

type (protected|unprotected) "unprotected"

>

<!ELEMENT JavaXML:parserClass (#PCDATA)>

<!ELEMENT JavaXML:xmlrpc-server (JavaXML:handlers)>

<!ELEMENT JavaXML:handlers (JavaXML:handler)+>

<!ELEMENT JavaXML:handler (JavaXML:identifier,

JavaXML:class)>

<!ELEMENT JavaXML:identifier (#PCDATA)>

<!ELEMENT JavaXML:class (#PCDATA)>

Teraz do takiej definicji należy odwołać się z pliku konfiguracyjnego:

<?xml version="1.0"?>

<!DOCTYPE JavaXML:xmlrpc-config SYSTEM "DTD/XmlRpc.dtd">

<JavaXML:xmlrpc-config

xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"

>

...

</JavaXML:xmlrpc-config>

Tutaj odwołujemy się do definicji XmlRpc.dtd umieszczonej w podkatalogu DTD/.

Ostatnie przygotowania

Stworzenie pliku konfiguracyjnego i jego zawężeń okazało się dość łatwym zadaniem. Kiedy już pro­gramista zrozumie mechanikę działania XML, jedyną trudnością będzie podjęcie odpowiednich decyzji projektowych. Ważne jest użycie zrozumiałych i prostych nazw, logiczne pogrupowanie ele­mentów i określenie, kiedy informacja ma być współdzielona przez wiele aplikacji. Po podjęciu takich decyzji samo zbudowanie pliku XML to kwestia kilku minut. W przypadku naszej aplikacji informacje zawarte w pliku konfiguracyjnym są naprawdę skromne, co dodatkowo upraszcza całą operację. Kompletny plik konfiguracyjny przedstawiony jest w przykładzie 11.2.

Przykład 11.2. Kompletny plik konfiguracyjny XML dla klas XML-RPC

<?xml version="1.0"?>

<!DOCTYPE JavaXML:xmlrpc-config SYSTEM "DTD/XmlRpc.dtd">

<JavaXML:xmlrpc-config

xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"

>

<!-- Informacje o konfiguracji serwera i klientow -->

<JavaXML:hostname>localhost</JavaXML:hostname>

<JavaXML:port type="unprotected">8585</JavaXML:port>

<JavaXML:parserClass>

org.apache.xerces.parsers.SAXParser

</JavaXML:parserClass>

<!-- Informacje o konfiguracji serwera -->

<JavaXML:xmlrpc-server>

<!-- Lista procedur XML-RPC do zarejestrowania -->

<JavaXML:handlers>

<JavaXML:handler>

<JavaXML:identifier>hello</JavaXML:identifier>

<JavaXML:class>HelloHandler</JavaXML:class>

</JavaXML:handler>

<JavaXML:handler>

<JavaXML:identifier>scheduler</JavaXML:identifier>

<JavaXML:class>Scheduler</JavaXML:class>

</JavaXML:handler>

</JavaXML:handlers>

</JavaXML:xmlrpc-server>

</JavaXML:xmlrpc-config>

Powyższy plik należy zachować pod nazwą xmlrpc.xml i sprawdzić, czy jest dostępny dla kodu naszej aplikacji. Teraz zajmiemy się stworzeniem klasy SAX odczytującej te informacje i udo­stęp­niającej je serwerowi i klientom XML-RPC.

Odczytywanie pliku konfiguracyjnego XML

Aby klasy XML-RPC mogły korzystać z utworzonego pliku konfiguracyjnego, trzeba zbudować klasę pomocniczą przetwarzającą te informacje i udostępniającą je serwerowi i klientom. Można byłoby wbudować ten mechanizm w metody zawarte w klasach XML-RPC (podobnie jak ko­rzys­taliśmy z metody getHandlers() w klasie LightweightServer), ale użycie oddzielnej klasy umożliwia współużytkowanie jej przez serwer i klienty, dzięki czemu unikamy duplikacji ko­du. Wiemy już, jakie informacje mają zostać uzyskane i możemy rozpocząć budowanie szkie­le­tu kla­sy, zawierającego metody dostępu do danych. Faktyczne zawartości stworzonych zmiennych przy­należnych zostaną uzyskane z procesu przetwarzania, który wkrótce skonstruujemy.

Pobieranie informacji o konfiguracji

Kod przetwarzający można byłoby dodać bezpośrednio do klasy com.oreilly.xml.Ligh­tweight­XmlRpcServer i do klientów. Jednak w ten sposób powielony zostałby ten sam kod. Zamiast tego budujemy nową klasę narzędziową z pakietu com.oreilly.xml: XmlRpcCon­figuration. Zalążek tej klasy przedstawiony jest w przykładzie 11.3. Konstruktor pobiera plik albo strumień InputStream, z którego zostaną odczytane informacje o konfiguracji. Stworzono również proste metody dostępu do danych po ich załadowaniu. Odizolowanie wejścia i wyjścia klas od specyficznych konstrukcji XML umożliwia zmianę mechanizmu przetwarzania (infor­ma­cje na temat są zawarte w kolejnym podrozdziale) bez przebudowywania serwera i klienta XML-RPC. To o wiele bardziej obiektowe rozwiązanie niż zagnieżdżanie kodu przetwarzającego w ko­dzie klientów i serwera.

Przykład 11.3. Klasa XmlRpcConfiguration, odczytująca dane konfiguracyjne XML

package com.oreilly.xml;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.InputStream;

import java.io.IOException;

import java.util.Hashtable;

/**

* <b><code>XmlRpcConfiguration</code></b> to klasa narzędziowa

* ładująca dane konfiguracyjne dla serwerów i klientów XML-RPC.

*

* @author Brett McLaughlin

* @version 1.0

*/

public class XmlRpcConfiguration {

/** Z tego strumienia odczytujemy dane konfiguracyjne XML */

private InputStream in;

/** Port, na którym działa serwer */

private int portNumber;

/** Nazwa hosta, na którym uruchomiono serwer */

private String hostname;

/** Klasa sterownika SAX do załadowania */

private String driverClass;

/** Procedury obsługi do zarejestrowania w serwerze XML-RPC */

private Hashtable handlers;

/**

* <p>

* Tutaj określamy plik, z którego odczytamy

* informacje o konfiguracji.

* </p>

*

* @param filename <code>String</code> nazwa pliku

* zawierającego konfigurację XML.

*/

public XmlRpcConfiguration(String filename)

throws IOException {

this(new FileInputStream(filename));

}

/**

* <p>

* Tutaj określamy plik, z którego odczytamy

* informacje o konfiguracji.

* </p>

*

* @param in <code>InputStream</code> z którego

* odczytamy informacje o konfiguracji.

*/

public XmlRpcConfiguration(InputStream in)

throws IOException {

this.in = in;

portNumber = 0;

hostname = "";

handlers = new Hashtable();

// Przetwarzamy dane XML o konfiguracji

}

/**

* <p>

* Zwraca numer portu, na którym nasłuchuje serwer.

* </p>

*

* @return <code>int</code> - port serwera.

*/

public int getPortNumber() {

return portNumber;

}

/**

* <p>

* Zwraca nazwę hosta, w którym uruchomiono serwer.

* </p>

*

* @return <code>String</code> - nazwa hosta serwera.

*/

public String getHostname() {

return hostname;

}

/**

* <p>

* Zwraca klasę sterownika SAX do załadowania.

* </p>

*

* @return <code>String</code> - nazwa klasy sterownika SAX.

*/

public String getDriverClass() {

return driverClass;

}

/**

* <p>

* Zwraca procedury obsługi do zarejestrowania przez serwer.

* </p>

*

* @return <code>Hashtable</code> - struktura procedur obsługi.

*/

public Hashtable getHandlers() {

return handlers;

}

}

Po zbudowaniu takiego szkieletu dodamy kod wypełniający zmienne danymi konfiguracyjnymi za pośrednictwem interfejsu JDOM. Aby zagwarantować, że informacje te będą dostępne w razie potrzeby, metodę przetwarzającą umieścimy w konstruktorze klasy. Stworzenie tych podsta­wo­wych metod dostępu pozwoli ukryć mechanizm samego pobierania informacji konfiguracyjnych przed klasami i aplikacjami korzystającymi z tych informacji. Zmiany w wersji JDOM, a nawet wykorzystanie zupełnie innych metod dostępu do danych, wpłyną wyłącznie na tę klasę; klienty i serwer XML-RPC pozostaną niezmienione. To bardzo elastyczny i perspektywiczny sposób po­bie­rania informacji konfiguracyjnych.

Ładowanie informacji konfiguracyjnych

Teraz można już rozpocząć wpisywanie kodu odpowiedzialnego za przetwarzanie. My akurat ma­my zadanie ułatwione — znamy strukturę wejściowego dokumentu XML (dzięki definicji DTD i narzucanym przez nią zawężeniom). Możemy więc bezpośrednio uzyskać dostęp do elementów dokumentu, których wartości zamierzamy pobrać. Najlepiej znów przywołać obraz hierarchii drze­wiastej: „przechodzimy” po drzewie i pobieramy wartości tych elementów, na których nam zależy. Na rysunku 11.1. przedstawiono taką właśnie reprezentację pliku konfiguracyjnego XML.

Mając taki model przed oczami, łatwiej będzie użyć metod getChildren() oraz getChild(), udostępnianych przez JDOM i za ich pomocą nawigować po elementach XML; na „znalezionym” w ten sposób elemencie wywołujemy metodę getContent() i wykorzystujemy uzyskaną war­tość w aplikacji. Wcześniej konieczne jest zaimportowanie odpowiednich klas JDOM (oraz klas pomocniczych Javy), stworzenie nowej metody przetwarzającej konfigurację i uruchomienie jej po­przez konstruktor XmlRpcConfiguration. Kod ładujący informacje konfiguracyjne przed­stawiony jest poniżej:

0x01 graphic

Rysunek 11.1. Drzewiasta struktura pliku konfiguracyjnego XML

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.InputStream;

import java.io.IOException;

import java.util.Hashtable;

import java.util.Iterator;

import java.util.List;

import org.jdom.Document;

import org.jdom.Element;

import org.jdom.JDOMException;

import org.jdom.Namespace;

import org.jdom.input.Builder;

import org.jdom.input.DOMBuilder;

...

/**

* <p>

* Tutaj określamy plik, z którego odczytamy

* informacje o konfiguracji.

* </p>

*

* @param in <code>InputStream</code> z którego

* odczytamy informacje o konfiguracji.

*/

public XmlRpcConfiguration(InputStream in)

throws IOException {

this.in = in;

portNumber = 0;

hostname = "";

handlers = new Hashtable();

// Przetwarzamy dane XML o konfiguracji

parseConfiguration();

}

...

/**

* <p>

* Przetwarzamy informacje o konfiguracji XML i

* udostępniamy je klientom.

* </p>

*

* @throws <code>IOException</code> w razie wystąpienia błędu.

*/

private void parseConfiguration() throws IOException {

try {

// Żądamy implementacji DOM i parsera Xerces.

Builder builder =

new DOMBuilder("org.jdom.adapters.XercesDOMAdapter");

// Pobieramy dokument konfiguracyjny, wykonując sprawdzanie poprawności.

Document doc = builder.build(in);

// Pobieramy element główny.

Element root = doc.getRootElement();

// Uzyskujemy przestrzeń nazw JavaXML.

Namespace ns = Namespace.getNamespace("JavaXML",

"http://www.oreilly.com/catalog/javaxml/");

// Ładujemy nazwę hosta, port i klasę procedury obsługi.

hostname = root.getChild("hostname", ns).getContent();

driverClass = root.getChild("parserClass", ns).getContent();

portNumber =

Integer.parseInt(root.getChild("port", ns).getContent());

// Pobieramy procedury obsługi

List handlerElements =

root.getChild("xmlrpc-server", ns)

.getChild("handlers", ns)

.getChildren("handler", ns);

Iterator i = handlerElements.iterator();

while (i.hasNext()) {

Element current = (Element)i.next();

handlers.put(current.getChild("identifier", ns).getContent(),

current.getChild("class", ns).getContent());

}

} catch (JDOMException e) {

throw new IOException(e.getMessage());

}

}

}

Po stworzeniu egzemplarza klasy, informacje konieczne do skonfigurowania serwerów i klientów XML-RPC są przetwarzane i ładowane do zmiennych przynależnych. Jedyna funkcja, jaka nie została tutaj zaimplementowana to rejestrowanie komunikatów o błędach; funkcja taka powinna znaleźć się w aplikacji produkcyjnej — tutaj pomijamy tę sprawę ze względu na konieczność utrzy­mania zwięzłości i przejrzystości kodu. Po uzyskaniu elementu głównego (doc.getRoot­Ele­ment()) za pomocą interfejsu JDOM lokalizujemy elementy w oparciu o strukturę drzewiastą prze­stawioną na rysunku 11.1; po odnalezieniu każdego elementu pobieramy jego zawartość te­k­stową i wstawiamy do zmiennej.

0x01 graphic

W naszym przykładzie do stworzenia obiektu JDOM Document użyliśmy klasy DOM­Builder. Jest to decyzjanieco dyskusyjna, ponieważ już po zbudowaniu elementu Do­cument nie istnieją żadne związki ani z interfejsem SAX, ani z DOM. Równie łat­wo (i tak naprawdę szybciej) można byłoby wykorzystać w tym celu klasę SAX SAXBuilder; w tej książce wykorzystywane są na przemian oba te modele, co do­wo­­dzi elastyczności interfejsu JDOM. W ten sposób wykazujemy także, że istnieje możliwość opracowania zupełnie nowych implementacji do tworzenia hierarchii drze­wiastej, nie opartej ani na interfejsie SAX, ani na DOM.

--> Ponieważ elementów opisujących procedury obsługi jest wiele, do uzyskania ich listy List wykorzystujemy metodę getChildren().[Author:AJ] Następnie przetwarzamy każdy element listy. Po wy­konaniu tych czynności skompilowana klasa może już zostać użyta w klasach XML-RPC z poprzedniego rozdziału.

Korzystanie z informacji konfiguracyjnych

Niełatwe zadanie wykorzystania danych XML na potrzeby konfiguracji mamy już za sobą. Za po­mocą stworzonej klasy XmlRpcConfiguration w prosty sposób pobierzemy te dane do naszej aplikacji. Serwer i klienty powinny tylko znać położenie pliku konfiguracyjnego, który następnie przekażą klasie pomocniczej XmlRpcConfiguration. W aplikacji produkcyjnej położenie te­go pliku określane byłoby za pomocą stałej w odpowiednim pliku lub klasie, ewentualnie poda­wa­ne jako wstępny argument, gdyby aplikacja miała postać serwleta Javy.

Zmiany w serwerze

Teraz zmienimy serwer LightweightXmlRpcServer tak, aby korzystał z konfiguracji w po­staci danych XML, a nie z pliku właściwości stworzonego w poprzednim rozdziale. Usuniemy rów­nież argument wiersza poleceń określający port — tę informację program pobierze teraz z pliku kon­figuracyjnego. Konieczne więc będzie takie zmodyfikowanie konstruktora, aby pobierał wy­łącz­nie plik konfiguracyjny, a następnie za pomocą klasy XmlRpcConfiguration uzyskiwał informacje o porcie i procedurach obsługi do zarejestrowania. Z klasy serwera usuniemy również metodę getHandlers(). Zmiany przedstawione są w przykładzie 11.4.

Przykład 11.4. Klasa LightweightXmlRpcServer wykorzystująca plik konfiguracyjny XML

/**

* <b><code>LightweightXmlRpcServer</code></b> to klasa narzędziowa

* uruchamiająca serwer XML-RPC nasłuchujący żądań HTTP

* i rejestrująca procedury obsługi zdefiniowane w pliku konfiguracyjnym.

*

* @author Brett McLaughlin

* @version 1.0

*/

public class LightweightXmlRpcServer {

/** Klasa narzędziowa serwera XML-RPC */

private WebServer server;

/** Plik konfiguracyjny */

private XmlRpcConfiguration config;

// Usunięto informacje o porcie i nazwie pliku

/**

* <p>

* Tutaj będziemy przechowywać plik konfiguracyjny

* wykorzystywany przez serwer.

* </p>

*

* @param configFile <code>String</code> nazwa pliku do

* pobrania informacji o konfiguracji.

* @throws <code>IOException</code> kiedy serwer nie może

* pobrać informacji konfiguracyjnych

*/

public LightweightXmlRpcServer(String configFile)

throws IOException {

config = new XmlRpcConfiguration(configFile);

}

/**

* <p>

* Uruchomienie serwera.

* </p>

*

* @throws <code>IOException</code> zgłaszany w razie problemów.

*/

public void start() throws IOException {

try {

// Korzystamy z parsera SAX Apache Xerces

XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");

System.out.println("Uruchamianie serwera XML-RPC...");

server = new WebServer(config.getPortNumber());

// Register handlers

registerHandlers(config.getHandlers());

} catch (ClassNotFoundException e) {

throw new IOException("Błąd ładowania parsera SAX: " +

e.getMessage());

}

}

// Metoda getHandlers() została usunięta z kodu źródłowego.

/**

* <p>

* Statyczny punkt rozpoczęcia programu.

* </p>

*/

public static void main(String[] args) {

if (args.length < 1) {

System.out.println(

"Użycie: " +

"java com.oreilly.xml.LightweightXmlRpcServer " +

"[plikKonfiguracyjny]");

System.exit(-1);

}

// Utworzenie serwera przeniesiono do bloku try/catch,

// dzięki czemu klient "dowiaduje się" o ewentualnych błędach

// przy uruchamianiu

try {

// Ładowanie informacji konfiguracyjnych

LightweightXmlRpcServer server =

new LightweightXmlRpcServer(args[0]);

// Uruchomienie serwera.

server.start();

} catch (IOException e) {

System.out.println(e.getMessage());

}

}

}

Dzięki tym zmianom nasz serwer pobierze informacje o konfiguracji z pliku XML i będzie potrafił zakomunikować o ewentualnych błędach, jakie wystąpiły w czasie tego procesu. Istniejąca już me­to­da registerHandlers() obsługuje strukturę Hashtable zwróconą z wywołania get­Handlers() z klasy XmlRpcConfiguration, a więc w tym miejscu żadne zmiany nie są po­trzebne. Co prawda w czasie uruchamiania serwera nie widać nic nadzwyczajnego (przykład 11.5), ale nasza aplikacja jest teraz naprawdę o wiele lepsza.

Przykład 11.5. Komunikaty w czasie uruchamiania zmodyfikowanej klasy LightweightXmlRpcServer

$ java com.oreilly.xml.LightweightXmlRpcServer conf/xmlrpc.xml

Uruchamianie serwera XML-RPC...

Port: 8585

Zarejestrowana w serwerze nazwa scheduler odpowiada klasie Scheduler

Zarejestrowana w serwerze nazwa hello odpowiada klasie HelloHandler

Zmiany w kliencie

Modyfikacja klienta pod kątem korzystania z nowego formatu danych konfiguracyjnych jest jesz­cze prostsza. Po zaimportowaniu klasy XmlRpcConfiguration nasza klasa Scheduler­Client potrafi już pobrać nazwę hosta i numer portu, pod które można zgłaszać żądania XML-RPC. Wprowadźmy zmiany pokazane w przykładzie 11.6.

Przykład 11.6. Klasa SchedulerClient wykorzystująca plik konfiguracyjny XML

import java.io.IOException;

import java.net.MalformedURLException;

import java.util.Calendar;

import java.util.Date;

import java.util.Enumeration;

import java.util.Hashtable;

import java.util.Vector;

import com.oreilly.xml.XmlRpcConfiguration;

import helma.xmlrpc.XmlRpc;

import helma.xmlrpc.XmlRpcClient;

import helma.xmlrpc.XmlRpcException;

public class SchedulerClient {

// implementacje metod addEvents() i listEvents().

public static void main(String args[]) {

if (args.length < 1) {

System.out.println(

"Usage: java SchedulerClient [configFile]");

System.exit(-1);

}

try {

// Ładowanie pliku konfiguracyjnego.

XmlRpcConfiguration config =

new XmlRpcConfiguration(args[0]);

// Ładowanie klasy sterownika SAX.

XmlRpc.setDriver(config.getDriverClass());

// Łączenie z serwerem.

XmlRpcClient client =

new XmlRpcClient("http://" +

config.getHostname() + ":" +

config.getPortNumber());

// Dodanie zdarzeń.

addEvents(client);

// Wyświetlenie zdarzeń.

listEvents(client);

} catch (MalformedURLException e) {

System.out.println(

"Niepoprawny format URL serwera: " +

e.getMessage());

} catch (XmlRpcException e) {

System.out.println("Wyjątek XML-RPC: " +

e.getMessage());

} catch (IOException e) {

System.out.println("Wyjątek IO: " + e.getMessage());

} catch (ClassNotFoundException e) {

System.out.println("Odnalezienie parsera SAX nie jest możliwe: " +

e.getMessage());

}

}

...

}

Podobne zmiany można równie łatwo wprowadzić w przykładzie HelloClient. Oba klienty zwró­cą (co może nie jest pasjonujące) dokładnie takie same wyniki, jak w poprzednim rozdziale; jednak jeśli chodzi o serwer, to zrobiliśmy rzeczywiście ogromny krok naprzód. Zmiana nazwy hosta czy numeru portu wymaga tylko wprowadzenia jednej tekstowej poprawki do pliku XML
i zrestartowania klas serwera i klientów. Zanim przejdziemy do następnego rozdziału, przejrzymy alternatywne sposoby przechowywania tego typu informacji konfiguracyjnych i porównamy je z XML-em.

Rzeczywistość

Uważny Czytelnik z pewnością zaobserwował, że granica pomiędzy przykładami w książce a „praw­dziwymi” aplikacjami zaczyna się coraz bardziej rozmywać. Serwer XML-RPC po zmianach wpro­wadzonych w tym rozdziale nadaje się niemal do wykorzystania produkcyjnego — posiada ela­styczny format pliku konfiguracyjnego, dynamicznie rejestruje procedury obsługi i nie anga­żu­jąc potężnych zasobów, obsługuje żądania XML-RPC. Jednakże wykorzystanie XML-a do repre­zen­tacji danych w „czystej postaci” to idea bardzo nowa — jak zresztą większość innych zagadnień związanych z XML-em. To tak jak z RMI i RPC — łatwo można zapędzić się i przesadzić z uży­waniem nowej technologii. Poniżej zostaną porównane możliwości XML-a jako nośnika składo­wa­nia danych z innymi, bardziej tradycyjnymi formatami. Czytelnik będzie mógł sam podejmować decyzje, kiedy lepiej stosować taki, a nie inny sposób zapisu, oraz porównać JDOM z innymi drogami dostępu do danych XML.

XML kontra bazy danych

W zależności od tego, z kim rozmawiamy na ten temat, bazy danych (a szczególnie bazy re­la­cyjne) albo są absolutnie nie do zastąpienia, albo lada chwila znikną z powierzchni naszej planety na rzecz baz obiektowych i składnic danych XML. Jak to zwykle bywa, prawda leży gdzieś po­środku. Dziesięć lat temu osoba wątpiąca w perspektywy systemów administracji bazami relacyj­ny­mi (RDBMS) narażała się na śmieszność. Pięć lat temu, kiedy pojawiły się systemy administracji bazami obiektowymi (OODBMS), taka osoba miała już prawo głosu, ale wciąż była traktowana sceptycznie. W ciągu ostatnich dwóch lat wszystko się zmieniło — dzięki OODBMS „wypłynął” XML, a niektórzy poważni informatycy przewidują, że XML całkowicie zastąpi tradycyjne syste­my baz danych.

Jaka jest prawda? Systemy RDBMS będą jeszcze przez wiele lat stosowane, o ile w ogóle kiedyś wyjdą z użycia. Nawet jeśli zignorujemy tak poważne sprawy, jak reprezentacja danych rela­cyj­nych w XML-u, to jednak technologia RDBMS stanowi podstawę bardzo wielu używanych współ­cześnie aplikacji. Zastosowanie XML-a stanowi realistyczne rozwiązanie w mniejszych apli­kacjach, nie korzystających z własnych formatów lub własnych baz danych; jednak większość tzw. dużych aplikacji produkcyjnych musi korzystać z istniejących już danych. Dane te niemal za­wsze znajdują się w relacyjnej bazie danych (zazwyczaj jest to komercyjny Oracle lub darmowy MySQL). Niemal wszystkie duże firmy mające wpływ na rozwój technologii wyko­rzy­stują takie sys­temy, dlatego zakładanie, że XML zastąpi systemy baz danych — czy nawet szybko zyska popu­larność — jest przesadą. Sama ilość danych składowanych w ist­nie­ją­cych systemach (giga­baj­ty, a często nawet terabajty) czyni XML niezbyt fortunnym sposobem re­pre­zentacji. Na­wet w ide­alnym projekcie XML, zupełnie nowym i nie korzystającym z żadnych istniejących danych czy aplikacji i niezależnym od systemów DBMS, możliwe jest, że na pewnym etapie trze­ba będzie nawiązać komunikację ze starszymi rozwiązaniami. Przekonanie kadry zarządzającej do przejścia na format XML, kiedy dotychczasowe rozwiązanie spisuje się bez zarzutu, może również sprawiać kłopoty. Nie powinniśmy więc oczekiwać, że nagle Oracle wprowadzi XML jako format prze­cho­wy­wania danych, czy też że Sybase zakończy działalność; używajmy XML do konfiguracji i tran­sportu danych tam, gdzie jest to możliwe, ale przy dużych ilościach danych nie próbujmy szu­kać lepszych rozwiązań niż istniejące.

Warto jednak odnotować, że istnieje pewne rozwiązanie dla tych, którzy tak bardzo chcieliby wykorzystać XML wyłącznie do składowania danych. Istnieją produkty tworzące warstwę danych XML „na” danych relacyjnych i innych formatach (np. usługach katalogowych, o których po­wiemy za chwilę). W miarę dojrzewania tych narzędzi odwzorowujących, budowanie XML-owej reprezentacji baz danych staje się coraz bardziej niezawodne i proste. Nowsze firmy decydujące się na składowanie danych w czystej postaci XML-owej mają możliwość komunikacji ze star­szymi systemami — pozwalają na to te same narzędzia. Najbardziej obiecującym produktem jest obecnie Castor, projekt open source grupy ExoLab. Więcej informacji o systemie Castor i narzę­dziach dowiązywania danych ML można znaleźć na stronie http://castor.exolab.org.

XML kontra usługi katalogowe i LDAP

Kolejną nowością w obszarze technologii składowania danych jest protokół Lightweight Directory Access Protocol (LDAP) i usługi katalogowe. Protokół ten, początkowo opracowywany na uni­wersytetach w Berkeley i Michigan, a teraz obecny w popularnym serwerze usług katalogowych firmy Netscape (http://www.netscape.com), stał się tematem wielu dyskusji. Po upowszechnieniu XML-a wiele osób waha się: kiedy lepiej korzystać z usług katalogowych niż z XML-a? Usługi katalogowe to doskonały sposób administracji katalogami, pocztą, adresami i kalendarzami w fir­mie; protokół LDAP zyskał także popularność jako sposób administrowania konfiguracjami. Przechowywanie informacji o konfiguracji aplikacji oraz sposobie reagowania na podstawowe zdarzenia związane z aplikacją (np. autoryzacja) jest bardzo często wykonywane właśnie przez serwer usług katalogowych. Umożliwia on szybsze przeszukiwanie i pobieranie informacji niż baza danych; oferuje dane w formacie hierarchicznym i tym samym świetnie nadaje się na po­trzeby konfiguracji. Po przeczytaniu niniejszego rozdziału, w którym przedstawiony jest sposób przechowywania informacji za pomocą XML-a, rodzi się pytanie, które z rozwiązań jest lepsze w konkretnych sytuacjach.

I tu niespodzianka — samo pytanie jest źle postawione! Tak naprawdę technologii LDAP i XML nie można porównywać, bo zostały stworzone w zupełnie innych celach. LDAP i usługi kata­lo­gowe służą do udostępniania technologii lub komponentów pod określonymi nazwami; natomiast XML służy do składowania i przesyłania danych zawartych w tych komponentach. Bardziej od­po­wiednie byłoby więc pytanie „W jakich sytuacjach należy integrować LDAP i XML?”. Od­po­wiedź leży w tych samych technologiach dowiązywania danych XML, które zostały wspomniane przy okazji poruszania tematu baz danych; projekt Castor oferuje kompletne powiązanie tech­nologii XML i LDAP. Co więcej, usługi katalogowe ewoluują w kierunku jednolitego sposobu
przechowywania danych; takim nośnikiem z pewnością mógłby być XML. Ponieważ zarówno LDAP, jak i XML są formatami hierarchicznymi, połączenie usług LDAP i języka XML nie po­winno nikogo dziwić.

JDOM, SAX czy DOM

Kiedy rozpatruje się alternatywy XML-a, ważne jest także rozważenie różnych sposobów dostępu do danych XML. Czytelnik widział, w jak prosty sposób można pobrać informacje o konfiguracji poprzez interfejs JDOM; tutaj zobaczy, jak można to zrobić inaczej. Kiedy aplikacje XML wdra­-
żane są w środowiskach produkcyjnych, zrozumienie, dlaczego taka, a nie inna decyzja jest naj­lepsza, staje się często ważniejsze niż sama ta decyzja! Dlatego teraz Czytelnik będzie mógł poz­nać inne sposoby uzyskania dostępu do danych XML z poziomu Javy.

Do pobrania danych konfiguracyjnych XML zastosowany został interfejs JDOM:

private void parseConfiguration() {

try {

// Żądamy implementacji DOM i parsera Xerces

Builder builder =

new DOMBuilder("org.jdom.adapters.XercesDOMAdapter");

// Pobieramy dokument konfiguracyjny, wykonując sprawdzanie poprawności.

Document doc = builder.build(in);

// Pobieramy element główny.

Element root = doc.getRootElement();

// Uzyskujemy przestrzeń nazw JavaXML.

Namespace ns = Namespace.getNamespace("JavaXML",

"http://www.oreilly.com/catalog/javaxml/");

// Ładujemy nazwę hosta, port i klasę procedury obsługi.

hostname = root.getChild("hostname", ns).getContent();

driverClass = root.getChild("parserClass", ns).getContent();

portNumber =

Integer.parseInt(root.getChild("port", ns).getContent());

// Pobieramy procedury obsługi.

List handlerElements =

root.getChild("xmlrpc-server", ns)

.getChild("handlers", ns)

.getChildren("handler", ns);

Iterator i = handlerElements.iterator();

while (i.hasNext()) {

Element current = (Element)i.next();

handlers.put(current.getChild("identifier", ns).getContent(),

current.getChild("class", ns).getContent());

}

} catch (JDOMException e) {

throw new IOException(e.getMessage());

}

}

Aby dobrze zilustrować różnicę pomiędzy JDOM-em, SAX-em i DOM-em, należy pokazać, w ja­ki sposób można uzyskać te same informacje poprzez SAX i DOM.

SAX

Największą trudnością przy pisaniu kodu z wykorzystaniem interfejsu SAX jest to, że stosowane tutaj podejście nie jest w tak dużym stopniu obiektowe, w jakim jest hierarchiczne. Ponieważ zdarzenia SAX występują sekwencyjnie, nie jest możliwe bezpośrednie operowanie na elementach potomnych. Konieczne jest zarezerwowanie pamięci do przechowania przetwarzanego elementu — dane tego elementu zostaną uzyskane dopiero w następnym wywołaniu. Przetwarzanie w tym interfejsie wymaga zazwyczaj odczytania dokumentu i przechowania danych przekazanych do wywołania characters() pod nazwą ostatniego przetwarzanego elementu (poprzez wywołanie startElement()). Następnie, pod koniec przetwarzania elementu (endElement() lub end­Document()), informacje te ładowane są z pamięci i wykorzystywane. SAX i jego sekwencyjny mechanizm są czasem szybsze od DOM-a (lub JDOM-a z implementacją DOM), ale uzyskany kod nie jest tak przejrzysty i trudniej usunąć z niego błędy. Metoda parseConfiguration(), przepisana do obsługi SAX-a, wyglądałaby następująco:

private void parseConfiguration() {

try {

XMLReader parser =

XMLReaderFactory.createXMLReader(

"org.apache.xerces.parsers.SAXParser");

parser.setContentHandler(new ConfigurationHandler());

parser.parse(new InputSource(in));

} catch (JDOMException e) {

// Komunikat o błędzie

}

}

Implementacja SAX XMLReader jest ładowana po zarejestrowaniu implementacji Con­tent­Handler. Typowe dla SAX-a jest to, że większość kodu aplikacji znajdzie się w metodzie Con­tentHanlder:

/**

* <p>

* Ta klasa wewnętrzna obsłuży wywołania

* odczytujące dane konfiguracyjne.

* </p>

*/

class ConfigurationHandler extends DefaultHandler {

/** Miejsce składowania zawartości elementu */

private Hashtable storage;

/** Nazwa ostatnio zgłoszonego elementu */

private String currentElement;

/** Stałe odpowiadające nazwom elementów */

private static final String HOSTNAME_ELEMENT = "hostname";

private static final String PORTNUMBER_ELEMENT = "port";

private static final String DRIVER_CLASS_ELEMENT = "parserClass";

private static final String HANDLER_ELEMENT = "handler";

private static final String HANDLER_ID_ELEMENT = "identifier";

private static final String HANDLER_CLASS_ELEMENT = "class";

/**

* <p>

* Tutaj inicjalizujemy miejsce składowania.

* </p>

*/

public ConfigurationHandler() {

storage = new Hashtable();

}

/**

* <p>

* Przechwytujemy nazwę zgłoszonego elementu.

* </p>

*/

public void startElement(String namespaceURI, String localName,

String rawName, Attributes atts)

throws SAXException {

currentElement = localName;

}

/**

* <p>

* Dodajemy zgłaszane dane znakowe do tych już

* znajdujących się w pamięci.

* </p>

*/

public void characters(char[] ch, int start, int end)

throws SAXException {

String data = new String(ch, start, end).trim();

if (storage.containsKey(currentElement)) {

data =

(String)storage.get(currentElement) +

data.trim();

}

storage.put(currentElement, data);

}

/**

* <p>

* Ponieważ informacje zagnieżdżone składowane są w elemencie

* procedury obsługi, a element ten może wystąpić wielokrotnie,

* obsługujemy dane składowane z takiego elementu za każdym razem,

* gdy osiągniemy koniec tego elementu.

* </p>

*

*/

public void endElement(String namespaceURI, String localName,

String rawName) throws SAXException {

// Dodanie procedury obsługi po zakończeniu.

if (localName.equals(HANDLER_ELEMENT)) {

String handlerName =

(String)storage.get(HANDLER_ID_ELEMENT);

String handlerClass =

(String)storage.get(HANDLER_CLASS_ELEMENT);

// Dodanie do miejsca składowania klasy zewnętrznej.

handlers.put(handlerName, handlerClass);

storage.remove(HANDLER_ID_ELEMENT);

storage.remove(HANDLER_CLASS_ELEMENT);

}

}

/**

* <p>

* Zachowujemy zgromadzone informacje na końcu dokumentu;

* wtedy mamy gwarancję, że wszystkie elementy zostały przetworzone.

* </p>

*/

public void endDocument() throws SAXException {

hostname = (String)storage.get(HOSTNAME_ELEMENT);

driverClass = (String)storage.get(DRIVER_CLASS_ELEMENT);

try {

portNumber =

Integer.parseInt(

(String)storage.get(PORTNUMBER_ELEMENT));

} catch (NumberFormatException e) {

// Komunikujemy o błędzie

}

}

}

Kolejne etapy opisano za pomocą dokumentacji Javadoc. Po uruchomieniu metody start­Ele­ment() nazwa zgłaszanego elementu zostaje zachowana. Stanowi ona potem klucz do struktury Hashtable zawierającej pary element-dane, wypełnianej kolejnymi wywołaniami charac­ters(). Po przetworzeniu każdego elementu handler identyfikator i nazwa klasy muszą zo­stać zachowane, ponieważ kolejne pobrania elementu handler spowodowałyby ich nadpisanie. Wreszcie, po wy­wołaniu endDocument() zachowane zostają także: nazwa hosta, numer portu i klasa parsera.

Ten kod oczywiście działa i nawet nie jest bardzo skomplikowany (w czym dużą rolę odgrywa do­kumentacja). A jednak jest on mniej czytelny niż ten wykorzystujący JDOM. Co więcej, kiedy liczba elementów w dokumencie XML wzrośnie do pięćdziesięciu, stu czy więcej, kod SAX zupełnie straci czytelność — konieczne jest wtedy dopisywanie kolejnych stałych tekstowych oraz operacji logicznych. Ten sam kod używający JDOM-a nie będzie „rósł” nawet w połowie tak szybko, ponieważ w tego typu rozwiązaniu dostęp do całego dokumentu XML uzyskujemy właś­nie poprzez interfejs JDOM.

DOM

Wykorzystanie interfejsu DOM do obróbki danych XML to, w pewnym sensie, rozwiązanie skraj­nie odmienne od zastosowania SAX-a. DOM pozwala uzyskać pełny obraz dokumentu XML, ale wymaga to zawsze wczytania całego dokumentu do pamięci, zanim jeszcze w ogóle uzyskamy do niego dostęp programowo. W przypadku niewielkich plików nie stanowi to problemu, ale może dać się we znaki przy większych dokumentach.

Co więcej, DOM nie oferuje standardowego interfejsu do uzyskiwania obiektu DOM Document. W wyniku tego konieczne jest jawne importowanie klas określonego producenta lub — jeśli chcemy tego uniknąć — stosowanie zaawansowanych refleksji. Interfejs DOM charakteryzuje się również bardzo formalną reprezentacją struktury drzewiastej; zawartość tekstowa dokumentu dostępna jest tylko jako potomny węzeł Node (a nie bezpośrednio z elementu), z czym wiąże się określony sposób jej uzyskiwania. Metoda parseConfiguration() wykorzystująca DOM zo­stała zaprezentowana poniżej:

private void parseConfiguration() {

org.apache.xerces.parsers.DOMParser parser =

new.org.apache.xerces.parsers.DOMParser();

handlers = new Hashtable();

parser.setFeature("http://xml.org/sax/features/namespaces", true);

try {

parser.parse(uri);

doc = parser.getDocument();

Element root = doc.getDocumentElement();

// Pobierz nazwę hosta.

NodeList nodes =

doc.getElementByTagNameNS(NAMESPACE_URI, "hostname");

if (nodes.getLength() > 0) {

hostname = nodes.item(0).getFirstChild()

.getNodeValue();

} else {

hostname = "";

}

// Pobierz numer portu.

nodes =

root.getElementByTagNameNS(NAMESPACE_URI, "port")

if (nodes.getLength() > 0) {

portNumber = Integer.parseInt(

nodes.item(0).getFirstChild()

.getNodeValue();

} else {

portNumber = 0;

}

// Pobierz procedury obsługi.

nodes =

root.getElementByTagNameNS(NAMESPACE_URI, "handler");

for (int i=0; i<nodes.getLength(); i++) {

Element handlerNode = (Element)nodes.item(i);

NodeList handlerNodes =

handlerNode.getElementByTagNameNS(

NAMESPACE_URI, "identifier");

String handlerID =

handlerNodes.item(0).getFirstChild()

.getNodeValue();

handlerNodes =

handlerNode.getElementByTagNameNS(

NAMESPACE_URI, "class");

String handlerClass =

handlerNodes.item(0).getFirstChild()

.getNodeValue();

handlers.put(handlerID, handlerClass);

}

} catch (Exception e) {

// Ustawienie wartości domyślnych.

portNumber = 0;

hostname = "";

}

}

Powyższy przykład jest na pewno krótszy niż ten wykorzystujący SAX, ale i tak kodu jest całkiem sporo. Trzeba bezpośrednio korzystać z parsera Apache Xerces (lub innego budującego drzewo DOM); są też inne mało intuicyjne struktury. Oto sposób uzyskania wartości tekstowej węzła:

hostname = nodes.item(0).getFirstChild()

.getNodeValue();

Ponieważ element hostname ma posiadać węzły potomne, zawierające wartości tekstowe tego ele­mentu, konieczne jest uzyskanie wartości pierwszego elementu potomnego, a nie wartości
węz­ła Node reprezentującego sam element hostname. Oto główna przyczyna błędów przy uży­wa­niu DOM-a: element DOM nie posiada wartości tekstowej; może posiadać elementy potomne w pos­taci węzłów Text Node, a dopiero te zawierają właściwe wartości.

Wreszcie, struktura zwracana z wywołań w rodzaju getElementByTagName() czy get­Child­Nodes() to lista DOM NodeList, a nie Java Vector czy List. Obiekt ten posiada własne metody dostępu (getLength() i item()), różniące się od odpowiednich klas Javy. Przez to DOM nie jest tak bliski Javie jak JDOM — w tym ostatnim zwracane typy mają postać stan­dardowych obiektów Javy.

Oczywiście, zarówno SAX, jak i DOM umożliwiają wykonanie tych samych zadań co JDOM; pytanie brzmi jednak, czy jakakolwiek oferowana przez nie cecha czyni je na tyle atrakcyjnymi, że warto przymknąć oko na małą czytelność kodu. Ponadto, dzięki klasie JDOM SAXBuilder, JDOM może wykonywać operacje na poziomie porównywalnym z SAX-em, bez konieczności rezygnacji z korzyści płynących z posiadania w pamięci struktury drzewiastej dokumentu (poprzez obiekt JDOM Document); ponadto jest to rozwiązanie „lżejsze” niż zastosowanie DOM-a. Naj­nowsze wersje i implementacje JDOM można znaleźć pod adresem http://www.jdom.org. JDOM umożliwia także separację od parserów i implementacji, emulowaną w interfejsie SAX poprzez XMLReaderFactory i zupełnie ignorowaną przez DOM. JAXP firmy Sun ma zalążki roz­wiązania tego problemu, ale wciąż nieodpowiednio obsługuje nowsze wersje SAX-a i DOM-a. Tak czy inaczej, to programista zdecyduje, który interfejs najlepiej sprawdzi w określonym pro­jekcie, a przy tym będzie najprostszy w użyciu dla nowych programistów, którzy do tego projektu mogą dołączyć w przyszłości.

Co dalej?

Czytelnik potrafi już odczytywać, przetwarzać, przekształcać i wykorzystywać XML na różne spo­soby. Język XML został użyty do tworzenia zawartości i prezentacji, do wywoływania procedur poprzez sieć oraz do konfiguracji aplikacji. Trzeba uzupełnić te informacje o jeszcze jedno za­gadnienie — tworzenie samych danych XML. W następnym rozdziale zostanie przedstawiona ko­lej­na możliwość XML-a — zdolność mutacji. Tym samym warsztat programisty zostanie uzu­pełniony o umiejętność dynamicznego generowania dokumentów XML przy różnych rodzajach danych wejś­ciowych.

Książka nie zawiera szczegółowych informacji na temat ExOffice i grupie ExoLab; warto jednak zauważyć, że grupa ta silnie wspiera technologie open source, szczególnie te związane z XML-em i Javą. Więcej informacji można znaleźć na stro­nie http://www.exolab.org.

298 Rozdział 11. XML na potrzeby konfiguracji

Rzeczywistość 297

C:\Helion\Java i XML\jAVA I xml\11-08.doc — strona 298

C:\Helion\Java i XML\jAVA I xml\11-08.doc — strona 297

C:\Helion\Java i XML\jAVA I xml\11-08.doc — strona 273

W.D.: Opuszczenie na stronie 334 oryginału; wiersze pod tekstem uwagi. (tłumacz: teraz jest ok.)



Wyszukiwarka

Podobne podstrony:
developerWorks Tutorial XML programming in Java (1999)
Java i XML, programowanie, Java
Java i XML, programowanie, Java
05-08, Programowanie, ! Java, Java i XML
02-08, Programowanie, ! Java, Java i XML
12-08, Programowanie, ! Java, Java i XML
00-08-orig, Programowanie, ! Java, Java i XML
01-08, Programowanie, ! Java, Java i XML
14-08, Programowanie, ! Java, Java i XML
zasady grupy, java, javascript, oprogramowanie biurowe, programowanie, programowanie 2, UTK, systemy
r12-05, Programowanie, ! Java, Java Server Programming
Programowanie współbieżne i rozproszone w języku Java stpiczynski
Java Sztuka programowania jaszpr
JAVA 03 konstrukcja programu
r20-05, Programowanie, ! Java, Java Server Programming
wyklad5.cpp, JAVA jest językiem programowania obiektowego
Java Zadania z programowania z przykładowymi rozwiązaniami
Java Programowanie Sieciowe Podstawy Javy id 226331

więcej podobnych podstron