|
11
XML |
|
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 danych. Aby zrozumieć przesłanki przemawiające za użyciem XML-a na potrzeby konfiguracji, wystarczy napisać aplikację korzystającą z obszernych plików właściwości albo serwer konfigurowany 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ślonej 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 począ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 pozbawionym 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 przeprowadzić żadnego logicznego grupowania wewnątrz takiego pliku. Co więcej, kiedy hierarchiczne 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 wszystkich tych problemów — oferował standardowy, prosty sposób reprezentacji informacji konfiguracyjnych.
Format XML nadaje się do szerokich zastosowań administracyjnych. Możliwe jest zbudowanie ogólnej aplikacji ładującej definicję DTD lub schemat, a następnie plik konfiguracyjny i umożliwiającej dodawanie, aktualizację, usuwanie i modyfikację informacji konfiguracyjnych. Jedna aplikacja, 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 shadow, użytkowników, grup i skryptów inicjalizacyjnych, to na pewno takie rozwiązanie jest przejrzystsze i prostsze w obsłudze.
Ponieważ XML jest już wykorzystywany w wielu aplikacjach, dodanie rozszerzenia do przetwarzania 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, wymagającą deskryptorów wdrożeniowych EJB w formacie XML, wykorzystanie tego języka w konfiguracji 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 wykorzystać ten język do wdrożenia obiektów biznesowych w serwerach EJB. Spowodowało to logiczną 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 sposoby wykorzystania XML-a do konfigurowania własnej aplikacji.
Najpierw zostaną omówione używane obecnie sposoby korzystania z XML-a na potrzeby konfiguracji. Przedstawione zostaną deskryptory wdrożeniowe EJB pod kątem decyzji projektowych, jakie 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 konfiguracyjnego 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ększy elastyczność omawianego serwera i jego klientów. Informacje o konfiguracji w prosty sposó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 zastosowań XML-a.
Deskryptory wdrożeniowe EJB
Zanim zbudujemy własne pliki konfiguracyjne oraz oprogramowanie pozwalające z nich korzystać, przyjrzymy się istniejącym formatom i szablonom. Tematem niniejszej książki nie jest specyfikacja 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łasny format danych konfiguracyjnych. W niniejszym podrozdziale zostaną także omówione najważ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 specyfikacji. 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ć uruchomiona 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 zgodne 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 wersjach specyfikacji definicja ta zostanie prawdopodobnie zastąpiona schematem. Tak czy inaczej, dokumenty XML używane w konfiguracji muszą tutaj (nawet bardziej niż gdzie indziej) przestrzegać 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 dowolnego dokumentu XML. Na elemencie tym spoczywa ciężar odpowiedzialności za reprezentację 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 powijakach) 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 uniknięto poprzez zastosowanie w deskryptorze wdrożeniowym przestrzeni nazwy takiej jak DeploymentDescriptor 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 znaczenie dla użyteczności danych jako źródła konfiguracji. Sposób organizacji i zagnieżdżania elementó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 odpowiadają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 wpisowi 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 niemu możliwe jest zamieszczenie informacji specyficznych dla danego pojemnika, a nie mających zastosowania do obiektów bean; informacje te nie zostaną wymieszane z tymi specyficznymi 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 macierzystego 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 nazywać i organizować pliki XML tego rodzaju we własnych aplikacjach oraz w opisywanym przykł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 stworzeniem 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 zastosowania informacji konfiguracyjnych w formacie XML; mamy już dostępny parser XML (wykorzystany 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 wszystko 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 niego odczytać:
port, na którym ma być uruchomiony serwer XML-RPC,
klasa parsera, którą serwer wykorzysta jako sterownik SAX,
procedury obsługi dla serwera XML-RPC,
identyfikator klasy,
nazwa klasy,
nazwa hosta, z którym mają się łączyć klienty XML-RPC,
port, z którym mają się łączyć klienty XML-RPC,
klasa parsera, którą klient wykorzysta jako sterownik SAX.
To wszystkie informacje, jakie trzeba przekazać serwerowi i klientom, aby te mogły zostać uruchomione 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 pamiętać o określeniu przestrzeni nazw i elementu głównego dokumentu. Choć w środowisku produkcyjnym 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ć jednolity 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 — szczególnie w złożonych dokumentach XML — że najprostsze rozwiązanie jest rozwiązaniem najlepszym. Nazewnictwo elementów XML nie stanowi tutaj wyjątku.
Po podjęciu tych wstępnych decyzji można rozpocząć budowę pliku konfiguracyjnego dla posiadanych 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 dokument oraz instrukcje przetwarzania dla aplikacji, które potem będą wykorzystywały i przetwarzały ten plik. Tutaj jednak pominiemy te elementy — nasz program będzie po prostu przetwarzał dokument „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 grupowalibyś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:
port, na którym ma być uruchomiony serwer XML-RPC,
klasa parsera, którą serwer wykorzysta jako sterownik SAX.
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:
procedury obsługi dla serwera XML-RPC,
identyfikator klasy,
nazwa klasy.
Klient XML-RPC korzysta przynajmniej z trzech różnych informacji; wiemy jednak, że port klienta 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ć tylko 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 wykorzystywanego w żądaniach XML-RPC:
nazwa hosta, z którym mają się łączyć klienty XML-RPC,
port, z którym mają się łączyć klienty XML-RPC,
klasa parsera, którą klient wykorzysta jako sterownik SAX.
Zasadnicza organizacja pliku jest już określona. Mamy dwie grupy informacji konfiguracyjnych: „informacje współużytkowane” (wykorzystywane przez klienta i serwer) oraz „informacje o procedurach 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 parserClass. Znów obowiązuje zasada — im prościej, tym lepiej. Aby zobrazować sposób wykorzystania atrybutów, do elementu port dodamy atrybut type. Atrybut ten przyjmie wartość „protected” (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 wykorzystywane 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 serwera. Nasz parser, zamiast szukać specyficznych elementów (i wymuszać zmianę kodu po dodaniu 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 informacji konfiguracyjnych. Do konfiguracji serwera dodajemy informacje opisujące klasy HelloHandler 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 obsł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 zagwarantowania, ż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 konfiguracyjnym. 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ż programista 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 elementó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 udostępniają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 korzystaliś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 kodu. Wiemy już, jakie informacje mają zostać uzyskane i możemy rozpocząć budowanie szkieletu klasy, zawierającego metody dostępu do danych. Faktyczne zawartości stworzonych zmiennych przynależ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.LightweightXmlRpcServer 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: XmlRpcConfiguration. 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 (informacje 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 kodzie 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 podstawowych 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 pobierania informacji konfiguracyjnych.
Ładowanie informacji konfiguracyjnych
Teraz można już rozpocząć wpisywanie kodu odpowiedzialnego za przetwarzanie. My akurat mamy 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 drzewiastej: „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ą wartość w aplikacji. Wcześniej konieczne jest zaimportowanie odpowiednich klas JDOM (oraz klas pomocniczych Javy), stworzenie nowej metody przetwarzającej konfigurację i uruchomienie jej poprzez konstruktor XmlRpcConfiguration. Kod ładujący informacje konfiguracyjne przedstawiony jest poniżej:
|
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ść utrzymania zwięzłości i przejrzystości kodu. Po uzyskaniu elementu głównego (doc.getRootElement()) za pomocą interfejsu JDOM lokalizujemy elementy w oparciu o strukturę drzewiastą przestawioną na rysunku 11.1; po odnalezieniu każdego elementu pobieramy jego zawartość tekstową i wstawiamy do zmiennej.
|
W naszym przykładzie do stworzenia obiektu JDOM Document użyliśmy klasy DOMBuilder. Jest to decyzjanieco dyskusyjna, ponieważ już po zbudowaniu elementu Document nie istnieją żadne związki ani z interfejsem SAX, ani z DOM. Równie łatwo (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 dowodzi elastyczności interfejsu JDOM. W ten sposób wykazujemy także, że istnieje możliwość opracowania zupełnie nowych implementacji do tworzenia hierarchii drzewiastej, 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 wykonaniu 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 pomocą 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 tego pliku określane byłoby za pomocą stałej w odpowiednim pliku lub klasie, ewentualnie podawane jako wstępny argument, gdyby aplikacja miała postać serwleta Javy.
Zmiany w serwerze
Teraz zmienimy serwer LightweightXmlRpcServer tak, aby korzystał z konfiguracji w postaci danych XML, a nie z pliku właściwości stworzonego w poprzednim rozdziale. Usuniemy również argument wiersza poleceń określający port — tę informację program pobierze teraz z pliku konfiguracyjnego. Konieczne więc będzie takie zmodyfikowanie konstruktora, aby pobierał wyłącznie 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ż metoda registerHandlers() obsługuje strukturę Hashtable zwróconą z wywołania getHandlers() z klasy XmlRpcConfiguration, a więc w tym miejscu żadne zmiany nie są potrzebne. 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 jeszcze prostsza. Po zaimportowaniu klasy XmlRpcConfiguration nasza klasa SchedulerClient 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 „prawdziwymi” aplikacjami zaczyna się coraz bardziej rozmywać. Serwer XML-RPC po zmianach wprowadzonych w tym rozdziale nadaje się niemal do wykorzystania produkcyjnego — posiada elastyczny format pliku konfiguracyjnego, dynamicznie rejestruje procedury obsługi i nie angażując potężnych zasobów, obsługuje żądania XML-RPC. Jednakże wykorzystanie XML-a do reprezentacji 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żywaniem nowej technologii. Poniżej zostaną porównane możliwości XML-a jako nośnika składowania 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 relacyjne) 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 relacyjnymi (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 systemy 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 relacyjnych 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 aplikacjach, 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 zawsze 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 wykorzystują takie systemy, dlatego zakładanie, że XML zastąpi systemy baz danych — czy nawet szybko zyska popularność — jest przesadą. Sama ilość danych składowanych w istniejących systemach (gigabajty, a często nawet terabajty) czyni XML niezbyt fortunnym sposobem reprezentacji. Nawet w idealnym 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 trzeba 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 przechowywania danych, czy też że Sybase zakończy działalność; używajmy XML do konfiguracji i transportu danych tam, gdzie jest to możliwe, ale przy dużych ilościach danych nie próbujmy szukać 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 powiemy 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 starszymi 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 uniwersytetach 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 firmie; 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 potrzeby 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 katalogowe 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 odpowiednie byłoby więc pytanie „W jakich sytuacjach należy integrować LDAP i XML?”. Odpowiedź 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 technologii 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 powinno 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 najlepsza, staje się często ważniejsze niż sama ta decyzja! Dlatego teraz Czytelnik będzie mógł poznać 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 jaki 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 endDocument()), 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 ContentHandler. Typowe dla SAX-a jest to, że większość kodu aplikacji znajdzie się w metodzie ContentHanlder:
/**
* <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 startElement() nazwa zgłaszanego elementu zostaje zachowana. Stanowi ona potem klucz do struktury Hashtable zawierającej pary element-dane, wypełnianej kolejnymi wywołaniami characters(). Po przetworzeniu każdego elementu handler identyfikator i nazwa klasy muszą zostać zachowane, ponieważ kolejne pobrania elementu handler spowodowałyby ich nadpisanie. Wreszcie, po wywoł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 dokumentacja). 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 skrajnie 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 został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 elementu, 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żywaniu DOM-a: element DOM nie posiada wartości tekstowej; może posiadać elementy potomne w postaci węzłów Text Node, a dopiero te zawierają właściwe wartości.
Wreszcie, struktura zwracana z wywołań w rodzaju getElementByTagName() czy getChildNodes() 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ć standardowych 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. Najnowsze 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 rozwią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 projekcie, 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 sposoby. 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 zagadnienie — tworzenie samych danych XML. W następnym rozdziale zostanie przedstawiona kolejna możliwość XML-a — zdolność mutacji. Tym samym warsztat programisty zostanie uzupeł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 stronie 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.)