C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 53
3
Przetwarzanie
kodu XML
Dwa rozdziały solidnego wprowadzenia za nami. Możemy rozpocząć kodowanie! Poznaliśmy już
wszystkie akronimy składające się na świat XML-a, zagłębiliśmy się w sam język XML i pozna-
liśmy jego strukturę. W tym rozdziale rozpoczniemy programowanie w Javie od przetworzenia do-
kumentu XML i uzyskania dostępu do przetworzonych danych z poziomu kodu w Javie.
Najpierw będziemy musieli pobrać i przetworzyć dokument. W czasie przetwarzania dokumentu
staje się on dostępny dla aplikacji wykorzystującej parser. To właśnie jest aplikacja „obsługująca
XML” (w angielskim używa się określenia XML-aware, czyli „wiedząca o XML-u”). To wszystko
wydaje się zbyt proste, żeby było prawdziwe — ale tak właśnie jest. W tym rozdziale poznamy
szczegóły dotyczące przetwarzania dokumentu XML. Omówimy sposoby korzystania z parsera
wewnątrz aplikacji i przekazywania parserowi danych. Potem omówimy wywołania wsteczne do-
stępne w czasie przetwarzania. Są to miejsca, w których możliwe jest wstawienie kodu specyficz-
nego dla aplikacji i w których może nastąpić obróbka danych.
Oprócz omówienia sposobu pracy parserów, poznamy również interfejs Simple API for XML (SAX).
To właśnie SAX udostępnia wywołania wsteczne. Interfejsy dostępne w pakiecie SAX staną się
istotnym elementem naszego warsztatu. Klasy SAX są niewielkie i nieliczne, ale w całym naszym
omówieniu XML-a będziemy się opierali właśnie na tych klasach. Dobre zrozumienie ich dzia-
łania jest niezbędne do sprawnego korzystania z XML-a w programach Javy.
Przygotowujemy się
Są pewne rzeczy, które należy przygotować jeszcze przed rozpoczęciem programowania. Po
pierwsze, trzeba zaopatrzyć się w parser XML. Napisanie parsera XML to ogromne zadanie; ist-
nieje kilka doskonałych projektów zmierzających do zbudowania takiego parsera. Nie będziemy
opisywali tutaj, jak pisze się parsery XML, ale w jaki sposób aplikacje korzystają z funkcji udo-
stępnianych przez te parsery. Do manipulacji danymi XML wykorzystamy istniejące narzędzia. Po
wybraniu parsera musimy zaopatrzyć się w klasy SAX. Nie jest trudno je znaleźć; klasy te są wa-
runkiem przetwarzania XML-a z poziomu Javy. Oczywiście potrzebny jest również dokument
XML do przetworzenia.
54
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 54
Zaopatrujemy się w parser
Najpierw należy zaopatrzyć się w odpowiedni parser (rodzaje parserów zostały pokrótce przedsta-
wione w rozdziale 1.). Aby parser działał ze wszystkimi przykładami z książki, należy sprawdzić,
czy jest zgodny ze specyfikacją XML. Ponieważ dostępnych jest wiele parserów, a w środowisku
programistów XML następują gwałtowne zmiany, opisanie poziomu zgodności różnych parserów
ze specyfikacją wykracza poza możliwości tej książki. Powinniśmy zasięgnąć informacji na stro-
nie producenta parsera i odwiedzić wymienione wcześniej witryny.
Zgodnie z duchem społeczności wolnego oprogramowania (open source), we wszystkich przykła-
dach tej książki wykorzystany zostanie parser Apache Xerces. Jest on dostępny w postaci źró-
dłowej i binarnej pod adresem http://xml.apache.org. Parser ten został napisany w C i w Javie
i jest używany przez liczną grupę programistów. Poza tym, jeśli korzystamy z parsera objętego za-
sadą wolnego oprogramowania, mamy możliwość wysłania autorom pytań lub powiadomień
o błędach — to przyczynia się do podwyższania jakości produktu. Aby zapisać się na ogólną listę
adresową dotyczącą parsera Xerces, wystarczy wysłać pusty e-mail pod adres xerces-dev-sub-
scribe@xml.apache.org. Tam uzyskamy odpowiedzi na pytania dotyczące samego parsera oraz
uzyskamy pomoc w przypadku wystąpienia problemów nie ujętych w tej książce. Oczywiście,
przykłady z książki będą poprawnie obsłużone przez dowolny parser wykorzystujący opisywaną
tutaj implementację SAX.
Po dokonaniu wyboru i pobraniu parsera XML należy upewnić się, że nasze środowisko programi-
styczne — czy to IDE (Integrated Development Environment), czy wiersz poleceń — ma dostęp do
parsera poprzez ścieżkę klas. To podstawowy wymóg działania wszystkich dalszych przykładów.
Zaopatrujemy się w klasy i interfejsy SAX
Po zdobyciu parsera musimy zaopatrzyć się w klasy SAX. Klasy te są niemal zawsze dołączone do
parserów i Xerces nie stanowi tutaj wyjątku. Jeśli tak właśnie jest w naszym przypadku, nie po-
winniśmy pobierać klas oddzielnie, ponieważ parser na pewno wyposażony jest w najświeższą
obsługiwaną wersję klas. W czasie pisania tej książki zkończono prace nad SAX 2.0. Właśnie z klas
w tej wersji korzystamy w niniejszej książce i powinna być w nie wyposażona najświeższa wersja
Apache Xerces.
Jeśli nie jesteśmy pewni, czy posiadamy klasy SAX, wystarczy spojrzeć na plik jar lub strukturę klas
wykorzystywaną przez parser. Klasy SAX znajdują się w pakietach struktury
org.xml.sax
.
Najświeższa wersja zawiera w katalogu głównym 17 klas oraz 9 klas
org.xml.sax.hel-
pers
i 2 klasy
org.xml.sax.ext
. Jeśli brakuje którejś z tych klas, trzeba skontaktować się
z producentem i sprawdzić, czy zostały one zawarte w dystrybucji. Niektóre mogą być pominięte,
jeśli nie są w całości obsługiwane
. Tyle samo klas istnieje w interfejsie SAX 2.0; mniej klas
pojawi się w tych katalogach, gdy obsługiwany jest tylko SAX 1.0.
Warto także pobrać albo zaznaczyć zakładką przeglądarki dokumenty SAX API Javadoc na stro-
nach WWW. Dokumentacja ta bardzo przydaje się podczas korzystania z klas SAX, a struktura
Javadoc zapewnia standardowy i prosty sposób znajdowania dodatkowych informacji o klasach.
1
Obsługa klas SAX w całości jest bardzo istotną cechą parsera. Oczywiście, można korzystać z dowolnego parsera, ale
jeśli nie obsługuje on w całości SAX 2.0, wiele przykładów z książki nie będzie działało. Ponadto oznacza to, że parser
nie uwzględnia nowości w technologii XML. Bez względu na powód, warto zapoznać się z parserem Xerces.
Czytniki SAX
55
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 55
Dokumentacja mieści się pod adresem http://www.megginson.com/SAX/SAX2/javadoc/index.html.
Dokumentację Javadoc można także wygenerować ze źródeł SAX-a poprzez kod zawarty w par-
serze lub poprzez pobranie pełnych źródeł z adresu http://www.megginson.com/SAX/SAX2.
Przygotowujemy dokument XML
Zawsze należy mieć pod ręką dokument XML do przetworzenia. Dane uzyskiwane w przykładach
wynikają z użycia dokumentu opisanego w rozdziale 2. Dokument ten należy zachować jako plik
contents.xml na dysku lokalnym. Zalecamy pracę właśnie na tym pliku. Można wpisać go ręcznie
albo pobrać ze strony WWW tej książki, http://www.oreilly.com/catalog/javaxml. Warto jednak
poświęcić chwilę czasu i wpisać go ręcznie, gdyż jest to praktyczna nauka składni XML-a.
Oprócz pobrania lub stworzenia pliku XML, będziemy musieli poczynić w nim kilka niewielkich
zmian. Ponieważ nie zostały jeszcze podane informacje o sposobie zawężania i przekształcania do-
kumentu, w tym rozdziale nasze programy będą wyłącznie przetwarzały („parsowały”) dokument.
Aby zapobiec błędom, trzeba usunąć z dokumentu odwołania do zewnętrznej definicji DTD zawę-
żającej XML oraz do arkuszy XSL powodujących jego przekształcenie. Wystarczy opatrzyć ko-
mentarzami te dwa wiersze dokumentu XML oraz instrukcję przetwarzania wysyłaną do modułu
Cocoon i żądającą przekształcenia:
<?xml version="1.0" encoding="ISO-8859-2"?>
<!-- To na razie się nie przyda
<?xml-stylesheet href="XSL\JavaXML.html.xsl" type="text/xsl"?>
<?xml-stylesheet href="XSL\JavaXML.wml.xsl" type="text/xsl"
media="wap"?>
<?cocoon-process type="xslt"?>
<!DOCTYPE JavaXML:Ksiazka SYSTEM "DTD\JavaXML.dtd">
-->
<!-- Java i XML -->
<JavaXML:Ksiazka xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/">
Po opatrzeniu tych wierszy komentarzami należy zanotować pełną ścieżkę dostępu do dokumentu
XML. Trzeba ją będzie przekazać programom w tym i w dalszych rozdziałach.
Na koniec należy opatrzyć komentarzami odwołanie do zewnętrznej encji
OReillyCopyright
,
ponieważ powodowałaby ona ładowanie pliku z informacjami o prawach autorskich. Nie posiada-
jąc definicji DTD opisującej sposób przetworzenia tej encji, otrzymalibyśmy błędy. W następnym
rozdziale zostanie przedstawione zastosowanie tej encji.
</JavaXML:Spis>
<!-- To odkładamy do rozdziału o DTD
<JavaXML:Copyright>&OReillyCopyright;</JavaXML:Copyright>
-->
</JavaXML:Ksiazka>
Czytniki SAX
Po informacjach wstępnych czas przejść do programowania. Nasz pierwszy program pobierze plik
jako argument wiersza poleceń i przetworzy go. Zbudujemy wywołania wsteczne do procesu prze-
twarzania, dzięki czemu zdarzenia zachodzące w czasie tego procesu będą wyświetlane i umożli-
wią lepszą obserwację całego mechanizmu.
56
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 56
Najpierw należy uzyskać egzemplarz (ang. instance) klasy zgodny z interfejsem SAX
org.xml.
sax.XMLReader
. Interfejs ten definiuje sposób przetwarzania i pozwala na ustawienie funkcji
i właściwości, które zostaną omówione w rozdziale 5., Sprawdzanie poprawności składni XML-a.
Informacja dla osób znających już SAX 1.0 — interfejs ten zastępuje dotychczasowy
org.xml.
sax.Parser
.
Instalacja czytnika
Interfejs udostępniany przez SAX powinien być zaimplementowany we wszystkich parserach XML
zgodnych z SAX-em. Dzięki temu SAX wie, jakie metody dostępne są dla wywołań wstecznych
i jakie można użyć z poziomu aplikacji. Na przykład główna klasa parsera SAX w Xerces,
org.apache.xerces.parsers.SAXParser
, implementuje interfejs
org.xml.sax.XML-
Reader
. Jeśli mamy dostęp do źródeł parsera, to zobaczymy, że ten sam interfejs zaimplemento-
wano w głównej klasie parsera SAX. Każdy parser musi posiadać jedną (a czasem więcej) klas,
które implementują ten interfejs. I właśnie egzemplarz tej klasy musimy stworzyć w celu przetwo-
rzenia danych XML:
XMLReader parser =
new SAXParser();
// Tu robimy coś z parserem
parser.parse(uri);
Osoby, które po raz pierwszy stykają się z interfejsem SAX, mogą być zaskoczone brakiem zmiennej
egzemplarza o nazwie
reader
czy
XMLReader
(reader to po angielsku „czytnik”). Rzeczy-
wiście, wydawałoby się, że tak powinny nazywać się te komponenty, ale w klasach SAX 1.0 zdefi-
niowano główny interfejs przetwarzający jako
Parser
i wiele zmiennych wywodzących się
z tamtego kodu odziedziczyło właśnie nazwę
parser
. Tamta postać interfejsu została już teraz
zaniechana, ponieważ duża liczba poczynionych zmian wymagała nowych przestrzeni nazw, fun-
kcji i właściwości, ale konwencje nazewnicze wciąż obowiązują, a
parser
dobrze oddaje prze-
znaczenie tego egzemplarza.
Mając to na uwadze, przyjrzyjmy się niewielkiemu programowi uruchamiającemu i tworzącemu
egzemplarz parsera SAX (przykład 3.1). Program ten nie przetwarza faktycznie dokumentu, ale
tworzy szkielet, w ramach którego będziemy mogli wykonać pozostałe ćwiczenia rozdziału. Fa-
ktyczne przetwarzanie rozpoczniemy w następnym rozdziale.
Przykład 3.1. Przykład użycia parsera SAX
import org.xml.sax.XMLReader;
// Tutaj importujemy implementację czytnika XML (XML Reader)
import org.apache.xerces.parsers.SAXParser;
/**
* <b><code>SAXParserDemo</code></b> pobiera plik XML i przetwarza je
* za pomocą SAX, wyświetlając wywołania wsteczne.
*
* @author
* <a href="mailto:brettmclaughlin@earthlink.net">Brett McLaughlin</a>
* @version 1.0
*/
public class SAXParserDemo {
/**
Czytniki SAX
57
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 57
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi
SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
}
/**
* <p>
* Tu obsługujemy wiersz poleceń tego programu demonstracyjnego.
* </p>
*/
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Użycie: java SAXParserDemo [XML URI]");
System.exit(0);
}
String uri = args[0];
SAXParserDemo parserDemo = new SAXParserDemo();
parserDemo.performDemo(uri);
}
}
Czytelnik powinien umieć załadować i skompilować powyższy program, o ile tylko przygotował
się w opisany wcześniej sposób i klasy SAX znajdują się w ścieżce dostępu do klas. Ten prosty
program nie potrafi jeszcze zbyt wiele; jeśli uruchomimy go, podając wymyśloną nazwę pliku lub
URI jako argument, powinien „pobrzęczeć” dyskiem i wyświetlić komunikat „Przetwarzanie pliku
XML”. Wynika to stąd, że jedynie stworzyliśmy instancję parsera, a nie zażądaliśmy jeszcze prze-
tworzenia dokumentu XML.
Jeśli wynikły kłopoty z kompilacją powyższego pliku źródłowego, najprawdopodob-
niej problem tkwi w ścieżce dostępu do klas środowiska programistycznego lub syste-
mu. Najpierw należy upewnić się, że zainstalowany został parser Apache Xerces (lub
inny). W przypadku Xerces należy po prostu pobrać odpowiedni plik jar. Po rozpako-
waniu archiwum uzyskujemy plik xerces.jar — właśnie on zawiera skompilowane klasy
dla tego programu. Po dodaniu tego archiwum do ścieżki dostępu do klas nie powinno
być problemów z kompilacją powyższego przykładu.
Przetwarzanie dokumentu
Kiedy parser jest już załadowany i gotowy do wykorzystania, można przekazać mu do prze-
tworzenia dokument. Tradycyjnie służy do tego metoda
parse()
, wchodząca w skład klasy
org.xml.sax.XMLReader
. Metoda ta przyjmuje albo wejście
org.xml.sax.Input-
Source
, albo zwykły identyfikator URI. Na razie nie będziemy zajmowali się wejściem
In-
putSource
i skorzystamy z URI. Identyfikator taki może być adresem sieciowym, ale my
58
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 58
użyjemy pełnej ścieżki dostępu do dokumentu, który przygotowaliśmy wcześniej. Gdybyśmy
jednak chcieli skorzystać z dokumentów umieszczonych w sieci, powinniśmy pamiętać o tym, że
aplikacja musi umieć znaleźć dany adres URL (tzn. musi być zapewnione połączenie z siecią).
Do programu dodajemy więc metodę
parse()
oraz dwie procedury obsługi błędów. Ponieważ
dokument musi zostać załadowany, czy to lokalnie, czy przez sieć, może wystąpić wyjątek
java.io.IOException
, który musimy przechwycić. Ponadto w czasie przetwarzania mo-
że zostać zgłoszony wyjątek
org.xmlsax.SAXException
(problem przy przetwarzaniu),
a więc musimy dodać jeszcze dwie istotne instrukcje, kilka linijek kodu i już mamy gotową do
użycia aplikację przetwarzającą XML:
import java.io.IOException;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
// Tutaj importujemy implementację czytnika XML (XML Reader)
import org.apache.xerces.parsers.SAXParser;
/**
* <b><code>SAXParserDemo</code></b> pobiera plik XML i przetwarza je
* za pomocą SAX, wyświetlając wywołania wsteczne.
*
* @author
* <a href="mailto:brettmclaughlin@earthlink.net">Brett McLaughlin</a>
* @version 1.0
*/
public class SAXParserDemo {
/**
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi
SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Użycie: java SAXParserDemo [XML URI]");
Procedury obsługi zawartości
59
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 59
System.exit(0);
}
String uri = args[0];
SAXParserDemo parserDemo = new SAXParserDemo();
parserDemo.performDemo(uri);
}
}
Tak zmodyfikowany przykład kompilujemy i wykonujemy. Pierwszym argumentem programu po-
winna być pełna ścieżka dostępu do przetwarzanego pliku:
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Przetwarzanie pliku XML: D:\prod\JavaXML\contents\contents.xml
Widząc taki nieciekawy wynik działania programu Czytelnik może zacząć wątpić, czy w ogóle co-
kolwiek się stało. Jednak dokument XML jest rzeczywiście przetwarzany, a gdybyśmy podali błędny
identyfikator URI pliku, parser zgłosi wyjątek i poinformuje, że nie mógł przetworzyć pliku. Nie
stworzyliśmy jednak jeszcze żadnych wywołań wstecznych, powodujących że SAX informuje
w czasie przetwarzania, co się dzieje w danej chwili. Bez nich dokument jest przetwarzany w spo-
sób niewidoczny dla użytkownika i bez interwencji aplikacji. Oczywiście, my mamy zamiar inter-
weniować w ten proces, a więc musimy stworzyć metody wywołań wstecznych. To właśnie ta
„interwencja” jest najważniejszym aspektem korzystania z parsera SAX. Wywołania wsteczne
parsera umożliwiają podjęcie działania w czasie trwania programu. Przetwarzanie przestaje być
takie nieciekawe jak powyżej — aplikacja zaczyna reagować na dane, elementy, atrybuty i strukturę
przetwarzanego dokumentu, a w tym czasie współdziała jeszcze z innymi programami i klientami.
Korzystanie z InputSource
Zamiast korzystania z pełnego identyfikatora URI, metodę
parse()
można także wywołać z ar-
gumentem w postaci
org.xml.sax.InputSource
. Właściwie o tej klasie niewiele można
powiedzieć — to raczej klasa osłonowa (ang. wrapper) i pomocnicza. Klasa
InputSource
po
prostu kapsułkuje informacje o pojedynczym obiekcie. W naszym przykładzie nie na wiele się to
przyda, ale w sytuacjach, gdzie identyfikator systemowy, identyfikator publiczny lub strumień mo-
gą być dowiązane do jednego identyfikatora URI, kapsułkowanie za pomocą
InputSource
może się okazać bardzo przydatne. Klasa posiada metody akcesora i mutatora do obsługi identyfi-
katorów systemowego i publicznego, kodowania znaków, strumienia bajtów (
java.io.Input-
Stream
) oraz strumienia znaków (
java.io.Reader
). Jeśli w ten sposób przekażemy argu-
ment do
parse()
, to SAX gwarantuje również, że parser nigdy nie zmodyfikuje
InputSour-
ce
. Dzięki temu wiemy na pewno, że po wykorzystaniu parsera lub aplikacji obsługującej XML
dane wejściowe pozostaną niezmienione. Wiele aplikacji opisywanych w dalszych częściach
książki będzie wykorzystywało właśnie klasy
InputSource
, a nie specyficznego URI.
Procedury obsługi zawartości
Aby nasza aplikacja mogła zrobić cokolwiek pożytecznego z przetwarzanymi właśnie danymi XML,
w parserze SAX musimy zarejestrować procedury obsługi (ang. handlers). Procedura obsługi to
po prostu grupa wywołań wstecznych zdefiniowanych w ramach interfejsu SAX i umożliwiają-
cych wywoływanie kodu aplikacji w przypadku zajścia konkretnych zdarzeń w czasie przetwarza-
nia dokumentu. Trzeba zdać sobie sprawę z tego, że wywołania te będą następowały w czasie
przetwarzania dokumentu, a nie po jego przetworzeniu. To między innymi dlatego SAX jest tak
60
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 60
potężnym interfejsem — umożliwia obsługę dokumentu sekwencyjnie, bez konieczności wczyty-
wania go całego do pamięci. Ograniczenie takie posiada model Document Object Model (DOM),
który zostanie omówiony w dalszej kolejności.
W interfejsie SAX 2.0 istnieją cztery podstawowe procedury obsługi:
org.xml.sax.Content-
Handler
,
org.xml.sax.ErrorHandler
,
org.xml.sax.DTDHandler
oraz
org.
xml.sax.EntityResolver
. W tym rozdziale omówimy procedurę
ContentHandler
,
umożliwiającą obsługę standardowych zdarzeń związanych z danymi dokumentu XML. Rozpo-
częte zostanie również omawianie procedury
ErrorHandler
, za pośrednictwem której parser
zgłasza znalezione w dokumencie błędy. Procedura
DTDHandler
zostanie omówiona w rozdziale
5. Procedura
EntityResolver
omawiana jest w różnych miejscach książki; teraz wystarczy
zrozumieć, że działa ona dokładnie tak jak pozostałe i służy do tłumaczenia encji zewnętrznych
wstawionych do dokumentu XML. Każdy z tych interfejsów może zostać zaimplementowany
w klasach aplikacji wykonujących specyficzne zadania. Klasy implementacyjne rejestrowane są
w parserze metodami
setContentHandler()
,
setErrorHandler()
,
setDTDHand-
ler()
i
setEntityResolver()
. Następnie parser wykonuje wywołania wsteczne tych me-
tod w razie uruchomienia konkretnego programu obsługi.
W naszym przykładzie zaimplementujemy interfejs
ContentHandler
. W interfejsie tym zde-
finiowano szereg istotnych metod cyklu przetwarzania, na które nasza aplikacja może reagować.
Przede wszystkim musimy dodać odpowiednie instrukcje
import
do pliku źródłowego (w tym
klasę i interfejs
org.xml.sax.Locator
i
org.xml.sax.Attributes
— oraz nową klasę
implementującą te metody wywołań wstecznych. Ta nowa klasa zostanie dodana na końcu pliku
źródłowego SAXParserDemo.java:
import java.io.IOException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
// Tutaj importujemy implementację czytnika XML (XML Reader).
import org.apache.xerces.parsers.SAXParser;
/**
* <b><code>SAXParserDemo</code></b> pobiera pliki XML i przetwarza je
* za pomocą SAX, wyświetlając wywołania wsteczne.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class SAXParserDemo {
/**
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi
SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
Procedury obsługi zawartości
61
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 61
// Stwórz egzemplarze procedur obsługi.
ContentHandler contentHandler = new MyContentHandler();
try {
// Stwórz egzemplarz parsera.
XMLReader parser =
new SAXParser();
// Zarejestruj procedurę obsługi zawartości.
parser.setContentHandler(contentHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
}
/**
* <p>
* Tu obsługujemy wiersz poleceń tego programu demonstracyjnego.
* </p>
*/
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Użycie: java SAXParserDemo [XML URI]");
System.exit(0);
}
String uri = args[0];
SAXParserDemo parserDemo = new SAXParserDemo();
parserDemo.performDemo(uri);
}
}
/**
* <b><code>MyContentHandler</code></b> implementuje interfejs SAX
* <code>ContentHandler</code> i definiuje sposób zachowania
* wywołań wstecznych SAX powiązanych z zawartością
* dokumentu XML.
*/
class MyContentHandler implements ContentHandler {
/** Zmienna locator będzie zawierała informacje o położeniu */
private Locator locator;
/**
* <p>
* Odwołania do <code>Locator</code> umożliwią uzyskanie
* informacji o miejscu, w którym wystąpiło wywołanie wsteczne.
* </p>
*
* @param locator <code>Locator</code> -- obiekt dowiązany do
* procesu wywołań wstecznych.
*/
62
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 62
public void setDocumentLocator(Locator locator) {
}
/**
* <p>
* Początek przetwarzanego dokumentu -- to dzieje się przed wszystkimi
* odwołaniami wszystkich procedur obsługi SAX oprócz
* <code>{@link #setDocumentLocator}</code>.
* </p>
*
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void startDocument() throws SAXException {
}
/**
* <p>
* Koniec przetwarzania dokumentu -- to dzieje się po wszystkich
* wywołaniach wstecznych wszystkich procedur obsługi. SAX.</code>.
* </p>
*
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void endDocument() throws SAXException {
}
/**
* <p>
* To oznaczać będzie, że napotkano instrukcję przetwarzania (nie na
deklarację XML).
* </p>
*
* @param target <code>String</code> obiekt docelowy instrukcji PI
* @param data <code>String</code> zawiera wszystkie dane wysłane do PI.
* Zazwyczaj ma to postać jednej lub więcej par
* atrybut - wartość.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void processingInstruction(String target, String data)
throws SAXException {
}
/**
* <p>
* To oznacza początek odwzorowywania przedrostka przestrzeni nazw XML.
* Zazwyczaj powinno się pojawić wewnątrz elementu głównego dokumentu
XML,
* ale nie jest to reguła (może pojawić się w dowolnym miejscu).
* Odwzorowanie przedrostka danego elementu uruchamia wywołanie wsteczne
* <i>przed</i> odwołaniem odnoszącym się do samego elementu
* (<code>{@link #startElement}</code>).
* </p>
*
* @param prefix <code>String</code> przedrostek dla odnalezionej
* przestrzeni nazw.
* @param uri <code>String</code> URI dla odnalezionej
* przestrzeni nazw.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void startPrefixMapping(String prefix, String uri) {
}
Procedury obsługi zawartości
63
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 63
/**
* <p>
* To oznacza koniec odwzorowania przedrostka, kiedy nie jest już dostępna
* przestrzeń nazw określona w odwołaniu
* <code>{@link #startPrefixMapping}</code>.
* </p>
*
* @param prefix <code>String</code> znalezionej przestrzeni nazw.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void endPrefixMapping(String prefix) {
}
/**
* <p>
* Komunikat o pojawieniu się faktycznego elementu. Podawane są atrybuty
* elementu, za wyjątkiem atrybutów specyficznych dla słownika XML,
* takich jak:
* <code>xmlns:[namespace prefix]</code> i
* <code>xsi:schemaLocation</code>.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z którą
* skojarzony jest ten element lub pusty
* <code>String</code>.
* @param localName <code>String</code> nazwa elementu (bez
* przedrostka przestrzeni nazw, jeśli taki istnieje)
* @param rawName <code>String</code> Wersja XML 1.0 nazwy elementu:
* [namespace prefix]:[localName].
* @param atts <code>Attributes</code> -- lista atrybutów tego elementu
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void startElement(String namespaceURI, String localName,
String rawName, Attributes atts)
throws SAXException {
}
/**
* <p>
* Oznacza, że osiągnięto koniec elementu.
* (<code></[element name]></code>). Należy zauważyć, że parser
* nie rozróżnia pomiędzy elementami pustymi i niepustymi,
* a więc to będzie się odbywało identycznie w obu przypadkach.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z jaką
skojarzony jest ten element
* @param localName <code>String</code> nazwa elementu bez przedrostka
* @param rawName <code>String</code> nazwa elementu w postaci XML. 1.0
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void endElement(String namespaceURI, String localName,
String rawName)
throws SAXException {
}
/**
* <p>
* Tutaj wyświetlamy dane tekstowe (zawarte wewnątrz elementu).
* </p>
*
* @param ch <code>char[]</code> tablica znaków zawartych w elemencie.
64
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 64
* @param start <code>int</code> indeks w tablicy, w którym zaczynają się
dane.
* @param end <code>int</code> indeks w tablicy, w którym kończą się dane.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void characters(char[] ch, int start, int end)
throws SAXException {
}
/**
* <p>
* Tutaj informujemy o znakach białych, ignorowanych w oryginalnym
dokumencie.
* Zazwyczaj procedura taka uruchamiana jest jedynie wtedy, gdy w
procesie
* przetwarzania odbywa się też sprawdzanie poprawności.
* </p>
*
* @param ch <code>char[]</code> tablica znaków zawartych w elemencie.
* @param start <code>int</code> indeks w tablicy, w którym zaczynają się
dane.
* @param end <code>int</code> indeks w tablicy, w którym kończą się dane.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void ignorableWhitespace(char[] ch, int start, int end)
throws SAXException {
}
/**
* <p>
* Tutaj pokazujemy encję pominiętą przez parser. Zdarzenie powinno
* pojawiać się jedynie w parserach nie sprawdzających poprawności;
* jego zachowanie zależy od konkretnej implementacji.
* </p>
*
* @param name <code>String</code> nazwa pomijanej encji.
* @throws <code>SAXException</code> gdy coś pójdzie nie tak.
*/
public void skippedEntity(String name) throws SAXException {
}
}
Dodaliśmy puste implementacje wszystkich metod zdefiniowanych w ramach interfejsu
Con-
tentHandler
i nasz plik źródłowy kompiluje się. Oczywiście, takie puste implementacje nie
umożliwiają obserwacji całego procesu, więc teraz przyjrzymy się po kolei wszystkim wymaga-
nym metodom.
Lokalizator dokumentu
Pierwsza metoda, jaką należy zdefiniować, to ta ustawiająca
org.xml.sax.Locator
dla do-
wolnego zdarzenia. W przypadku wystąpienia wywołania wstecznego klasa implementująca pro-
cedurę obsługi niejednokrotnie musi uzyskać informację o miejscu w pliku XML przetwarzanym
przez parser. Dzięki temu aplikacja będzie mogła podjąć decyzję odnośnie danego zdarzenia i miej-
sca, w którym ono wystąpiło. Klasa
Locator
udostępnia szereg przydatnych metod, takich jak
getLineNumber()
i
getColumnNumber()
, zwracających bieżące miejsce w dokumencie
XML. Ponieważ położenie to odnosi się tylko do bieżącego cyklu przetwarzania, klasy
Locator
powinno się używać wyłącznie w zakresie implementacji
ContentHandler
. Ponieważ nam
Procedury obsługi zawartości
65
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 65
może ona przydać się później, zachowujemy udostępniony egzemplarz klasy
Locator
do zmien-
nej składowej i drukujemy komunikat, informujący, że nastąpiło wywołanie wsteczne. Dzięki
temu poznamy kolejność występowania zdarzeń SAX:
/** Zmienna locator będzie zawierała informacje o położeniu */
private Locator locator;
/**
* <p>
* Odwołania do <code>Locator</code> umożliwią uzyskanie
* informacji o miejscu, w którym wystąpiło wywołanie wsteczne.
* </p>
*
* @param locator <code>Locator</code> -- obiekt dowiązany do
* procesu wywołań wstecznych.
*/
public void setDocumentLocator(Locator locator) {
System.out.println(" * setDocumentLocator() została wywołana");
// Zachowujemy do ewentualnego wykorzystania w przyszłości.
this.locator = locator;
}
Później, jeśli konieczne będzie uzyskanie informacji o miejscu zdarzenia, do metody tej można
dodać bardziej szczegółowe instrukcje. Jeśli jednak użytkownik chce pokazać, gdzie w dokumen-
cie pojawiają się dane zdarzenia (np. numer wiersza, w którym wystąpił element), musi przypisać
ten
Locator
zmiennej składowej do późniejszego wykorzystania w klasie.
Początek i koniec dokumentu
Każdy proces ma początek i koniec. Oba te zdarzenia powinny wystąpić raz — pierwsze przed wszy-
stkimi innymi zdarzeniami, a drugie po. Ten oczywisty fakt ma w aplikacjach krytyczne znaczenie
— dokładnie informuje, kiedy przetwarzanie rozpoczyna się i kiedy kończy. SAX udostępnia meto-
dy wywołań wstecznych dla obu tych zdarzeń:
startDocument()
i
endDocument()
.
Pierwsza metoda,
startDocument()
, wywoływana jest przed wszelkimi innymi wywołaniami
wstecznymi, również przed tymi znajdującymi się w innych procedurach obsługi SAX, np.
DTD-
Handler
. Innymi słowy,
startDocument()
to nie tylko pierwsza metoda wywoływana we-
wnątrz
ContentHandler
, ale także pierwsza metoda w całym procesie przetwarzania (nie
licząc metody
setDocumentLocator()
,wspomnianej przed chwilą). Dzięki temu wiemy,
kiedy przetwarzanie rozpoczyna się, a nasza aplikacja może wykonać procedury, które muszą być
uruchomione jeszcze przed tym przetwarzaniem.
Druga metoda,
endDocument()
, jest zawsze wywoływana jako ostatnia, również bez względu
na procedurę obsługi. Obejmuje to także sytuacje, w których proces przetwarzania zostaje za-
trzymany w wyniku napotkania błędów. Błędy zostaną omówione w dalszej części książki; teraz
Czytelnik powinien jednak wiedzieć, że są dwa ich rodzaje: błędy naprawialne i nienaprawialne.
W przypadku wystąpienia błędu nienaprawialnego wywoływana jest metoda procedury
Error-
Handler
, po czym przetwarzanie kończy się wywołaniem metody
endDocument()
.
W naszym przykładzie, kiedy obie te metody zostaną wywołane, wyświetlimy odpowiednie ko-
munikaty na konsoli.
/**
* <p>
* Początek przetwarzanego dokumentu -- to dzieje się przed wszystkimi
* odwołaniami wszystkich procedur obsługi SAX oprócz
* <code>{@link #setDocumentLocator}</code>.
66
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 66
* </p>
*
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void startDocument() throws SAXException {
System.out.println("Rozpoczyna się przetwarzanie...");
}
/**
* <p>
* Koniec przetwarzania dokumentu -- to dzieje się po wszystkich
* wywołaniach wstecznych wszystkich procedur obsługi SAX</code>.
* </p>
*
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void endDocument() throws SAXException {
System.out.println("...Przetwarzanie skończone.");
}
Oba te wywołania wsteczne mogą zgłosić wyjątek
SAXException
. Jest to jedyny typ wyjątków
zgłaszany przez SAX. Za pośrednictwem wyjątków udostępniany jest kolejny standardowy inter-
fejs do procesu przetwarzania. Jednakże wyjątki te często pośredniczą w obsłudze innych wyjąt-
ków, wyraźnie wskazujących na to, z jakim problemem mamy do czynienia. Na przykład, jeśli
plik XML jest przetwarzany za pośrednictwem sieci (poprzez adres URL), a połączenie nagle
zostało przerwane, to zgłoszony zostaje
IOException
. Ale aplikacja korzystająca z klas SAX
nie musi umieć przechwytywać tego wyjątku, ponieważ w ogóle nie musi wiedzieć, gdzie zlokali-
zowano zasób XML. Aplikacja może obsługiwać tylko jeden wyjątek —
SAXException
.
Ten pierwszy wyjątek jest przechwytywany przez parser SAX i ponownie zgłaszany, tym razem
jako
SAXException
; pierwotny wyjątek jest „kapsułkowany” w nowym. Dzięki temu
wystarczy, żeby aplikacja przechwytywała jeden wyjątek, poprzez który dostarczane są także
szczegółowe informacje o naturze problemu, jaki wystąpił w procesie przetwarzania. Klasa
SAXException
udostępnia metodę
getException()
, zwracającą właściwy wyjątek.
Instrukcje przetwarzania
Instrukcje przetwarzania (PI) w dokumencie XML są elementami dość wyjątkowymi. Nie są postrze-
gane jako elementy samego XML-a i zamiast natychmiastowej obsługi, przekazywane są aplikacji
wywołującej. Z tego względu do ich obsługi SAX udostępnia specjalne wywołanie wsteczne. Me-
toda ta otrzymuje obiekt docelowy (ang. target) instrukcji oraz dane przekazane do PI. Załóżmy,
że życzymy sobie wyświetlenia na ekranie informacji w momencie pojawienia się wywołania:
/**
* <p>
* To oznaczać będzie, że napotkano instrukcję przetwarzania (nie
* deklarację XML).
* </p>
*
* @param target <code>String</code> obiekt docelowy instrukcji PI
* @param data <code>String</code> zawiera wszystkie dane wysłane do PI.
* Zazwyczaj ma to postać jednej lub więcej par
* atrybut - wartość.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void processingInstruction(String target, String data)
throws SAXException {
Procedury obsługi zawartości
67
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 67
System.out.println("PI: Obiekt docelowy:" + target + " i dane:" +
data);
}
W rzeczywistej aplikacji, korzystającej z danych XML, w tym miejscu program mógłby otrzymać
instrukcje do ustawienia wartości zmiennych lub wykonania metod związanych z tą konkretną
aplikacją. Na przykład struktura publikacji Apache Cocoon mogłaby ustawić znaczniki związane z
przekształcaniem danych po ich przetworzeniu lub wyświetlić skrót XML jako określony typ
zawartości. Metoda ta, podobnie jak inne wywołania wsteczne SAX, zgłasza
SAXException
w ra-
zie wystąpienia błędu.
Podczas omawiania instrukcji przetwarzania wspomnieliśmy także o deklaracji XML. Ta specjalna
instrukcja przetwarzania udostępnia wersję, opcjonalne informacje o kodowaniu oraz o tym, czy
dany dokument jest dokumentem samodzielnym:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
Instrukcja ta przeznaczona jest specjalnie dla parsera XML. Umożliwia zgłoszenie błędu (np. o nie-
obsługiwanej wersji) na samym początku przetwarzania. Ponieważ instrukcja ta obsługiwana jest
tylko przez parser, nie powoduje wywołania
processingInstruction()
. Należy uważać,
aby nie stworzyć kodu oczekującego tej instrukcji lub informacji o wersji, ponieważ aplikacja ni-
gdy nie otrzyma wywołania wstecznego do tej instrukcji przetwarzania. Właściwie to tylko parser
powinien „interesować się” kodowaniem i wersją dokumentu XML, ponieważ elementy związane
są z samym procesem przetwarzania. Kiedy już dane dokumentu zostaną udostępnione poprzez
interfejs API Javy, szczegóły te stają się dla aplikacji nieistotne.
Wywołania związane z przestrzenią nazw
Sądząc po tym, ile czasu poświęciliśmy przestrzeniom nazw w XML-u (oraz po stopniu skom-
plikowania tej problematyki), można przypuszczać, że jest to pojęcie dość istotne i że ma duży
wpływ na przetwarzanie i obsługę danych XML. Oprócz schematu XML, przestrzenie nazw XML
to z pewnością najważniejsza cecha dodana do języka XML od czasu opublikowania oryginalnego
zalecenia XML 1.0. W interfejsie SAX 2.0 obsługę przestrzeni nazw wprowadzono na poziomie
elementów. Dzięki temu możliwe jest rozróżnienie pomiędzy przestrzenią nazwy elementu (opi-
saną przedrostkiem i odpowiednim identyfikatorem URI) a lokalną nazwą elementu. W tym przy-
padku, pojęciem lokalna nazwa określamy nazwę elementu bez przedrostka. Na przykład lokalną
nazwą dla
JavaXML:Ksiazka
jest po prostu
Ksiazka
. Przedrostek przestrzeni nazw to
Ja-
vaXML
, a identyfikator URI przestrzeni nazw (w naszym przykładzie) to http://www.oreilly.com/
catalog/javaxml.
Istnieją dwa wywołania wsteczne do obsługi przestrzeni nazw (choć wywołania związane z ele-
mentami mogą także z nich korzystać). Wywołania te następują wtedy, gdy parser natrafi na po-
czątek i koniec odwzorowania przedrostków (ang. prefix mapping). Sam termin nie jest jeszcze
znany, ale to pojęcie nie wprowadza nic nowego. Odwzorowanie przedrostków to po prostu ele-
ment, w którym za pomocą atrybutu
xmlns
deklarowana jest przestrzeń nazw. Jest to często
element główny (w którym może występować wiele różnych odwzorowań), ale nic nie stoi na prze-
szkodzie, aby tę rolę spełniał dowolny inny element dokumentu XML, jawnie deklarujący prze-
strzeń nazw. Na przykład:
<glowny>
<element1>
<mojaPrzestrzenNazw:element2 xmlns:mojaPrzestrzenNazw="http://mojUrl.pl">
<mojaPrzestrzenNazw:element3>Tu jakieś
dane</mojaPrzestrzenNazw:element3>
68
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 68
</mojaPrzestrzenNazw:element2>
</element1>
</glowny>
W tym przypadku przestrzeń nazw deklarowana jest jawnie, na głębokości kilku poziomów za-
gnieżdżenia elementów.
Wywołanie
startPrefixMapping()
otrzymuje przedrostek przestrzeni nazw oraz identyfika-
tor URI skojarzony z tym przedrostkiem. Odwzorowanie uznawane jest za „zamknięte” lub „za-
kończone”, gdy zamknięty zostaje element deklarujący to odwzorowanie. Jedyny „haczyk” tego
wywołania polega na tym, że zachowuje się ono niezupełnie sekwencyjnie — tak jak to ma za-
zwyczaj miejsce w interfejsie SAX; odwzorowanie przedrostków odbywa się bezpośrednio przed
wywołaniem związanym z elementem deklarującym przestrzeń nazw. Oto przykład wywołania:
/**
* <p>
* To oznacza początek odwzorowania przedrostka przestrzeni nazw XML.
* Zazwyczaj powinno się pojawić wewnątrz elementu głównego dokumentu
XML,
* ale nie jest to reguła (może pojawić się w dowolnym miejscu).
* Odwzorowanie przedrostka danego elementu uruchamia wywołanie wsteczne
* <i>przed</i> odwołaniem odnoszącym się do samego elementu
* (<code>{@link #startElement}</code>).
* </p>
*
* @param prefix <code>String</code> przedrostek dla odnalezionej
* przestrzeni nazw
* @param uri <code>String</code> URI dla odnalezionej
* przestrzeni nazw
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void startPrefixMapping(String prefix, String uri) {
System.out.println("Początek odwzorowania dla przedrostka " + prefix +
" odwzorowanego dla URI " + uri);
}
W naszym dokumencie jedynym zadeklarowanym odwzorowaniem jest atrybut elementu głównego.
Oznacza to, że powinniśmy oczekiwać tego wywołania przed wywołaniem związanym z pierw-
szym elementem (o którym za chwilę), ale już po wywołaniu
startDocument()
oraz po
wszelkich instrukcjach PI, jakie znajdują się na początku dokumentu. Drugie z tej pary wywołań
związanych z przestrzeniami nazw oznacza napotkanie końca odwzorowania i pojawia się bezpo-
średnio po znaczniku zamykającym elementu, w którym zadeklarowano odwzorowanie:
/**
* <p>
* To oznacza koniec odwzorowania przedrostka, kiedy nie jest już dostępna
* przestrzeń nazw określona w odwołaniu
* <code>{@link #startPrefixMapping}</code>.
* </p>
*
* @param prefix <code>String</code> znalezionej przestrzeni nazw
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void endPrefixMapping(String prefix) {
System.out.println("Koniec odwzorowania dla przedrostka " + prefix);
}
W przypadku pokazanego powyżej fragmentu dokumentu XML można oczekiwać następującego
komunikatu po napotkaniu znacznika
element2
:
Procedury obsługi zawartości
69
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 69
Początek odwzorowania dla przedrostka mojaPrzestrzenNazw odwzorowanego dla URI
http://mojUrl.pl
W ten sposób można poznać odwzorowywany przedrostek oraz skojarzony z nim identyfikator URI.
Wywołania związane z elementami
Teraz najprawdopodobniej przygotowani już jesteśmy na faktyczne pobranie danych z dokumentu
XML. To prawda, że ponad połowa wywołań wstecznych SAX nie ma nic wspólnego z elementa-
mi, atrybutami i danymi XML. Wynika to stąd, że proces przetwarzania nie polega po prostu na
przekazaniu aplikacji danych XML; aplikacja otrzymuje także instrukcje PI, dzięki którym wie,
jakie działanie ma zostać podjęte; aplikacja „dowiaduje się” także, kiedy przetwarzanie rozpoczy-
na się i kończy, a nawet gdzie znajdują się białe znaki, które można zignorować. Jeśli niektóre
z tych wywołań wydają się Czytelnikowi pozbawione sensu, to należy uzbroić się w cierpliwość
— jeszcze w tym rozdziale niektóre zagadnienia zostaną wyjaśnione; więcej wiadomości na ten te-
mat znaleźć można w rozdziale 5.
Oczywiście, istnieją wywołania SAX służące do uzyskiwania dostępu do danych XML zawartych
w dokumentach. Pierwsze trzy zdarzenia, jakie zostaną omówione, to początek i koniec elementu
oraz wywołanie
characters()
. Informują one, kiedy przetwarzany jest element, jakie dane za-
wiera oraz kiedy parser napotkał znacznik zamykający elementu. Pierwsze z wywołań,
start-
Element()
, informuje aplikację o elemencie XML i ewentualnych jego atrybutach. Parametry
wywołania to nazwa elementu (w różnych postaciach) oraz egzemplarz klasy
org.xml.sax.At-
tributes
(Czytelnik powinien przypomnieć sobie opisywaną wcześniej instrukcję importującą).
Ta klasa pomocnicza zawiera referencje do wszystkich atrybutów elementu. Umożliwia proste
przetwarzanie kolejnych atrybutów elementu w postaci podobnej do
Vector
. Oprócz możliwości
odwołania się do atrybutu za pomocą indeksu (kiedy przetwarzamy wszystkie atrybuty po kolei),
możliwe jest również odwołanie się poprzez nazwę. Oczywiście, w tej chwili Czytelnik powinien
już być ostrożniejszy, słysząc słowo „nazwa” związane z elementem lub atrybutem XML — może
ono znaczyć różne rzeczy. W tym przypadku można użyć albo pełnej nazwy atrybutu (z przed-
rostkiem przestrzeni nazw, o ile taki tam jest) — to nazwiemy „surową” nazwą; albo połączenia
nazwy lokalnej i identyfikatora URI, jeśli wykorzystano przestrzeń nazw. Istnieją także metody
pomocnicze, takie jak
getURI(int index)
i
getLocalName(int index)
, dzięki któ-
rym można uzyskać dodatkowe informacje o przestrzeni nazw związanej z danym atrybutem. Cały
interfejs
Attributes
stanowi więc wszechstronne źródło danych o atrybutach elementu.
Jak wspomnieliśmy, nie tylko atrybuty, ale również nazwa samego elementu może przyjmować
różne formy. I znów chodzi tu o przestrzeń nazw w XML-u. Najpierw dostarczany jest identyfika-
tor URI przestrzeni nazw danego elementu. Dzięki temu element umieszczany jest w poprawnym
kontekście i odpowiedniej relacji do pozostałych przestrzeni nazw w dokumencie. Następnie po-
dawana jest lokalna nazwa elementu, czyli fragment bez przedrostka. Oprócz tego (na potrzeby
zgodności wstecz), podawana jest „surowa” nazwa elementu. Chodzi tu o niezmodyfikowaną naz-
wę elementu, zawierającą ewentualny przedrostek przestrzeni nazw — czyli to, co umieszczone
było w dokumencie XML; dla naszego elementu
Ksiazka
byłaby to nazwa
JavaXML:
Ksiazka
. Obecność tych trzech typów nazw umożliwia opisanie dowolnego elementu, z prze-
strzenią nazw, czy też bez niej. Skoro wiemy już, jak udostępnia się element i jego atrybuty,
spójrzmy na implementację wywołania wstecznego SAX, wyświetlającą informację na ekranie.
W tym przykładzie sprawdzamy, czy nazwa elementu posiada skojarzony z nią identyfikator URI
przestrzeni nazw; jeśli tak — drukujemy przestrzeń nazw; jeśli nie — drukujemy komunikat, in-
formujący, że z elementem nie skojarzono żadnej przestrzeni nazw:
/**
70
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 70
* <p>
* Komunikat o pojawieniu się faktycznego elementu. Podawane są atrybuty
* elementu, za wyjątkiem atrybutów specyficznych dla słownika XML,
* takich jak:
* <code>xmlns:[namespace prefix]</code> i
* <code>xsi:schemaLocation</code>.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z którą
* skojarzony jest ten element lub pusty.
* <code>String</code>
* @param localName <code>String</code> nazwa elementu (bez
* przedrostka przestrzeni nazw, jeśli taki istnieje)
* @param rawName <code>String</code> Wersja XML 1.0 nazwy elementu:
* [namespace prefix]:[localName]
* @param atts <code>Attributes</code> -- lista atrybutów tego elementu.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void startElement(String namespaceURI, String localName,
String rawName, Attributes atts)
throws SAXException {
System.out.print("startElement: " + localName);
if (!namespaceURI.equals("")) {
System.out.println(" w przestrzeni nazw " + namespaceURI +
" (" + rawName + ")");
} else {
System.out.println(" nie posiada skojarzonej przestrzeni nazw");
}
for (int i=0; i<atts.getLength(); i++)
System.out.println(" Atrybut: " + atts.getLocalName(i) +
"=" + atts.getValue(i));
}
Wywołanie SAX bardzo upraszcza cały ten proces. W przypadku wywołania
startElement()
trzeba jeszcze zwrócić uwagę na fakt, że nie zachowuje się kolejności atrybutów. Podczas iteracji
po implementacji klasy
Attributes
atrybuty niekoniecznie będą pojawiały się w kolejności,
w jakiej zostały przetworzone (czyli w takiej, w jakiej wpisano je do dokumentu). Nie należy więc
polegać na uzyskanej kolejności atrybutów — i język XML nie wymaga od parserów zachowy-
wania tej kolejności. W niektórych parserach kolejność jest zachowywana, ale nie należy to do
udokumentowanych funkcji takich parserów.
Zamykająca część wywołania związanego z elementem to metoda
endElement()
. To proste
wywołanie właściwie tłumaczy się samo — przekazywana mu jest tylko nazwa elementu, co umoż-
liwia dopasowanie do odpowiedniego elementu przekazanego wcześniej do wywołania
start-
Element()
. Głównym przeznaczeniem tego wywołania jest zaznaczenie zamknięcia elementu
— dzięki temu aplikacja wie, czy dalsze znaki są częścią innego zakresu, czy zamykanego właśnie
elementu. W naszym przykładzie, podczas zamykania elementu wydrukujemy po prostu jego nazwę:
/**
* <p>
* Oznacza, że osiągnięto koniec elementu
* (<code></[element name]></code>). Należy zauważyć, że parser
* nie rozróżnia pomiędzy elementami pustymi i niepustymi,
* a więc to będzie się odbywało identycznie w obu przypadkach.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z jaką
skojarzony
Procedury obsługi zawartości
71
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 71
* jest ten element.
* @param localName <code>String</code> nazwa elementu bez przedrostka
* @param rawName <code>String</code> nazwa elementu w postaci XML. 1.0
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void endElement(String namespaceURI, String localName,
String rawName)
throws SAXException {
System.out.println("endElement: " + localName + "\n");
}
Dane elementu
Po określeniu początku i końca bloku elementu oraz po „wyłuskaniu” atrybutów tego elementu ko-
lejną istotną informacją do pobrania są same dane zawarte w elemencie. Dane składają się za-
zwyczaj z kolejnych elementów, danych tekstowych lub obu jednocześnie. Kiedy pojawiają się
kolejne elementy, następują dla nich odpowiednie wywołania i rozpoczyna się proces pseudoreku-
rencyjny — elementy zagnieżdżone w elementach powodują wywołania „zagnieżdżone” w wywoła-
niach. W pewnej chwili parser napotyka dane tekstowe. Te są zazwyczaj dla klienta najważniejsze
— informacje takie przeważnie są wyświetlane lub służą do zbudowania odpowiedzi.
W XML-u dane tekstowe w elementach przesyłane są do aplikacji za pomocą wywołania
cha-
racters()
. Metoda ta udostępnia aplikacji tablicę znaków oraz indeksy początkowy i końcowy,
które wskazują, gdzie należy odczytać odpowiednie dane:
/**
* <p>
* Tutaj wyświetlamy dane tekstowe (zawarte wewnątrz elementu).
* </p>
*
* @param ch <code>char[]</code> tablica znaków zawartych w elemencie.
* @param start <code>int</code> indeks w tablicy, w którym zaczynają się
dane.
* @param end <code>int</code> indeks w tablicy, w którym kończą się dane.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void characters(char[] ch, int start, int end)
throws SAXException {
String s = new String(ch, start, end);
System.out.println("znaki: " + s);
}
To, wydawałoby się, proste wywołanie niejednokrotnie powoduje spore zamieszanie, ponieważ in-
terfejs i standard SAX nie definiują ściśle, jak korzystać z tego wywołania przy przetwarzaniu
większych ilości danych tekstowych. Innymi słowy, parser może zwrócić długi ciąg danych w je-
dnym wywołaniu lub rozbić dane na wiele wywołań. Dla danego elementu metoda ta może zostać
wywołana zero razy (jeśli element nie zawiera danych tekstowych) albo jeden lub więcej razy.
W różnych parserach będzie się to odbywało inaczej, niejednokrotnie z wykorzystaniem algoryt-
mów mających na celu przyspieszenie przetwarzania. Nigdy nie należy liczyć na uzyskanie wszy-
stkich danych tekstowych po jednym wywołaniu metody; podobnie nigdy nie należy zakładać, że
wiele wywołań zwróci nam ciąg danych pochodzący z jednego tylko elementu.
Przy tworzeniu procedur obsługi SAX trzeba także pamiętać o postępowaniu hierarchicznym. In-
nymi słowy, nie wolno uważać, że dowolny element posiada swoje dane oraz elementy potomne,
należy traktować go jako element macierzysty. Pamiętać należy również, że parser „porusza się”,
72
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 72
obsługując elementy, atrybuty i dane w miarę ich osiągania. Mogą z tego wynikać pewne niespo-
dzianki. Spójrzmy na taki fragment dokumentu XML:
To jestzagnieżdżony tekstdalszy tekst
Jeśli zapomnimy, że SAX przetwarza sekwencyjnie i wywołuje metody w miarę napotykania ele-
mentów i danych, oraz nie uwzględnimy faktu, że XML jest konstrukcją hierarchiczną, możemy
spodziewać się następującego rezultatu:
startElement: macierzysty nie posiada skojarzonej przestrzeni nazw
znaki: To jest dalszy tekst.
startElement: potomny nie posiada skojarzonej przestrzeni nazw
znaki: zagnieżdżony tekst.
endElement: potomny.
endElement: macierzysty.
To wydawałoby się logiczne, bo
macierzysty
całkowicie „zawiera” element
potomny
, praw-
da? Ale dzieje się inaczej — dla każdego zdarzenia SAX wywoływana jest metoda, co powoduje
następujący wynik:
startElement: macierzysty nie posiada skojarzonej przestrzeni nazw.
znaki: To jest.
startElement: potomny nie posiada skojarzonej przestrzeni nazw.
znaki: zagnieżdżony tekst.
endElement: potomny.
znaki: dalszy tekst.
endElement: macierzysty.
SAX nie wybiega z przetwarzaniem naprzód, a wiec wynik jest dokładnie taki, jaki uzyskalibyśmy
czytając dokument XML sekwencyjnie, bez „ludzkich” przyzwyczajeń. Warto o tym pamiętać.
I jeszcze jedno — metoda
characters()
często informuje o białych znakach. To powoduje
dalsze niezrozumienie, bo przecież inne wywołanie SAX,
ignorableWhitespace()
, także
komunikuje o białych znakach. W naszym przykładzie nie sprawdzamy poprawności składni do-
kumentu XML; ale możemy przecież korzystać z parsera sprawdzającego poprawność składni. To
dość ważny szczegół, bo sposób komunikowania o białych znakach zależy od tego, czy parser ma
funkcję sprawdzania poprawności składni, czy nie. Parsery sprawdzające poprawność składni będą
komunikowały o wszystkich białych znakach poprzez metodę
ignorableWhitespace()
— jest
to spowodowane pewnymi czynnikami związanymi ze sprawdzaniem poprawności składni, które
zostaną omówione w następnych dwóch rozdziałach. Parsery nie sprawdzające poprawności
składni komunikują o białych znakach albo poprzez metodę
ignorableWhitespace()
, albo
poprzez metodę
characters()
. Konieczne jest więc sprawdzenie, czy mamy do czynienia z par-
serem sprawdzającym poprawność składni.
Należy wiedzieć, że na wiele parserów faktycznie składają się dwie implementacje parsera — je-
dna sprawdzająca, a druga nie sprawdzająca poprawności składni. W czasie działania parsera odpo-
wiednia klasa ładowana jest dynamicznie. Wynika to stąd, że parser nie sprawdzający poprawności
często pracuje bardziej wydajnie niż ten sprawdzający, nawet jeśli sprawdzanie poprawności faktycz-
nie się nie odbywa (z powodu konieczności implementacji dodatkowych struktur danych w parserze
sprawdzającym). Tak właśnie jest w parserze Apache Xerces. W naszym przykładzie wykorzysta-
ny zostanie egzemplarz parsera nie sprawdzającego składni, ale gdyby określono definicję DTD
lub schemat i gdyby zażądano sprawdzania poprawności, użyta zostałaby klasa parsera sprawdza-
jącego poprawność.
Aby uniknąć tego całego zamieszania, najlepiej w ogóle nie czynić żadnych założeń dotyczących
białych znaków. Rzadko konieczne jest wykorzystanie białych znaków jako danych w dokumencie
Procedury obsługi zawartości
73
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 73
XML. Jeśli zachodzi taka potrzeba (np. w przypadku konstrukcji typu „kilka spacji + dane + kilka
spacji”, gdzie liczba spacji ma znaczenie w danej aplikacji), to należy korzystać z konstrukcji
CDATA
. To zapewni, że dane, w których liczy się liczba spacji, nie będą w ogóle przetwarzane
— obsłużone zostaną przez aplikację nadrzędną jako duży „wycinek” danych tekstowych. W po-
zostałych przypadkach należy unikać stosowania spacji w reprezentowaniu danych i czynienia ja-
kichkolwiek założeń odnośnie sposobu zwracania spacji przez wywołania.
Spokojnie, to tylko białe znaki
Zagadnienia związane z białymi znakami zostały już omówione. Teraz wystarczy dodać to ostatnie
wywołanie SAX do naszej klasy
MyContentHandler
. Metoda
ignorableWhitespace()
pobiera parametry w dokładnie takim samym formacie jak metoda
characters()
i powinna
wykorzystywać indeksy początkowy i końcowy, udostępniane w celu czytania znaków z dostar-
czonej tablicy:
/**
* <p>
* Tutaj informujemy o znakach białych, ignorowanych w oryginalnym
dokumencie.
* Zazwyczaj procedura taka uruchamiana jest jedynie wtedy, gdy w
procesie
* przetwarzania odbywa się też sprawdzanie poprawności.
* </p>
*
* @param ch <code>char[]</code> tablica znaków zawartych w elemencie.
* @param start <code>int</code> indeks w tablicy, w którym zaczynają się
dane.
* @param end <code>int</code> indeks w tablicy, w którym kończą się dane.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void ignorableWhitespace(char[] ch, int start, int end)
throws SAXException {
String s = new String(ch, start, end);
System.out.println("ignorableWhitespace: [" + s + "]");
}
Oczywiście, nasz program nie wydrukuje nic widzialnego, ponieważ utworzony
String
będzie
się składał wyłącznie z białych znaków — dlatego dane wyjściowe ujmujemy w nawiasy klamro-
we. Białe znaki wyświetlane są w taki sam sposób jak zwykłe dane tekstowe; mogą zostać ob-
służone przez jedno wywołanie, proces ten może również zostać rozbity w parserze SAX na kilka
wywołań. W każdym razie w celu uniknięcia błędów w aplikacji należy przestrzegać tych samych
zasad, które obowiązują w przypadku danych tekstowych.
Pominięte encje
Jak już wspomnieliśmy, w naszym dokumencie pojawiła się jedna encja —
OreillyCopy-
right
. Przetworzenie i przetłumaczenie takiej encji oznacza załadowanie innego pliku, albo
z lokalnego systemu plików, albo z identyfikatora URI. Jednakże w tym dokumencie nie wyma-
gamy takiego przetworzenia. Z parserami nie sprawdzającymi poprawności składni wiąże się
jedna, często pomijana cecha — nie muszą one także tłumaczyć encji — po prostu pomijają je.
W interfejsie SAX 2.0 sprawę rozwiązano bardzo elegancko — kiedy encja jest pomijana przez
parser nie sprawdzający poprawności, wywoływana jest specjalna metoda. W wywołaniu podawa-
na jest nazwa encji — my wykorzystamy tę właściwość do wyświetlenia odpowiedniej informacji
74
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 74
na ekranie (parser Apache Xerces nie działa w ten sposób, ale nie znaczy to, że będzie tak w przy-
padku parsera zastosowanego przez Czytelnika):
/**
* <p>
* Tutaj pokazujemy encję pominiętą przez parser. Zdarzenie powinno
* pojawiać się jedynie w parserach nie sprawdzających poprawności;
* jego zachowanie zależy od konkretnej implementacji.
* </p>
*
* @param name <code>String</code> nazwa pomijanej encji.
* @throws <code>SAXException</code> gdy coś pójdzie nie tak.
*/
public void skippedEntity(String name) throws SAXException {
System.out.println("Pomijam encję " + name);
}
Zanim spróbujemy wykorzystać powyższą metodę, musimy pamiętać, że większość istniejących
parserów nie pomija encji, nawet jeśli nie są to parsery sprawdzające poprawność. Na przykład
parser Apache Xerces nigdy nie skorzysta z tego wywołania. Parser ten tłumaczy encję, a otrzyma-
ny wynik dołącza do danych otrzymywanych po przetworzeniu. Innymi słowy, wywołanie to cze-
ka na wykorzystanie przez parser, ale niełatwo będzie nam znaleźć parser, który z niego skorzysta!
Jeśli jednak posiadamy parser, który korzysta z powyższego wywołania, należy pamiętać, że prze-
kazany parametr nie zawiera otwierającego znaku „ampersand” ani końcowego średnika. W przy-
padku
&OReillyCopyright;
do metody
skippedEntity()
przekazywany jest tylko łańcuch
OReillyCopyright
.
Wyniki
Teraz należy zarejestrować procedurę obsługi w klasie
XMLReader
, której egzemplarz stworzy-
liśmy. Służy do tego metoda
setContentHandler()
, której jedynym argumentem jest implemen-
tacja
ContentHandler
. Do metody
demo()
w naszym przykładowym programie dodajemy na-
stępujące wiersze:
/**
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi
SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
// Stwórz egzemplarze procedur obsługi
ContentHandler contentHandler = new MyContentHandler();
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Przetwórz dokument
parser.parse(uri);
Procedury obsługi zawartości
75
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 75
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
}
Jeśli Czytelnik dołączył wszystkie omawiane wyżej wywołania do pliku, powinien skompilować
klasę
MyContentHandler
oraz zawierający ją plik
SAXParserDemo
. Po udanej kompilacji
można uruchomić program demonstrujący działanie parsera na stworzonym wcześniej przykłado-
wym pliku XML. Pełne polecenie wywołujące Javę powinno mieć postać:
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Polecenie to powinno zwrócić dużo danych wyjściowych. Jeśli Czytelnik korzysta z systemu Win-
dows, może się okazać konieczne zwiększenie bufora dla okna DOS-owego w takim stopniu, aby
możliwe było przewinięcie ekranu i obejrzenie wszystkich danych wyjściowych. Dane te powinny
mieć postać podobną do tych przedstawionych w przykładzie 3.2
Przykład 3.2. Dane wyjściowe zwracane przez program SAXParserDemo
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana.
Rozpoczyna się przetwarzanie...
Początek odwzorowania dla przedrostka JavaXML odwzorowanego dla URI.
http://www.oreilly.com/catalog/javaxml/
startElement: Ksiazka w przestrzeni nazw.
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Ksiazka)
znaki:
startElement: Tytul w przestrzeni nazw.
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Tytul)
znaki: Java i XML
endElement: Tytul
znaki:
startElement: Spis w przestrzeni nazw.
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Spis)
znaki:
startElement: Rozdzial w przestrzeni nazw.
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Rozdzial)
Atrybut: tematyka=XML
znaki:
startElement: Naglowek w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Naglowek)
znaki: Wprowadzenie
endElement: Naglowek
2
W tym oraz innych przykładach danych zwracanych przez programy możliwe jest, że zostały dodatkowo przełamane
wiersze w celu poprawnego umieszczenia wydruku w książce. O ile jednak same dane tekstowe się zgadzają, to na pew-
no wszystko jest w porządku!
76
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 76
znaki:
startElement: Temat w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Temat)
Atrybut: podRozdzialy=7
znaki: Co to jest?
endElement: Temat
znaki:
startElement: Temat w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Temat)
Atrybut: podRozdzialy=3
znaki: Jak z tego korzystać?
endElement: Temat
znaki:
startElement: Temat w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Temat)
Atrybut: podRozdzialy=4
znaki: Dlaczego z tego korzysta?
endElement: Temat
znaki:
startElement: Temat w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Temat)
Atrybut: podrozdzialy=0
znaki: Co dalej?
endElement: Temat
...
Takich danych pojawi się na wyjściu sporo, ponieważ przetwarzany dokument XML ma wiele ele-
mentów. Wyraźnie widać, że parser sekwencyjnie przetwarza każdy element, atrybuty elementu,
dane w nim zawarte, elementy zagnieżdżone oraz znacznik końcowy elementu. Proces powtarza
się dla każdego elementu. W naszym przykładzie został zastosowany parser Apache Xerces w wersji
nie sprawdzającej poprawności dokumentu, a więc białe znaki są zgłaszane za pomocą wywołań
characters()
. W następnych dwóch rozdziałach zostaną omówione zagadnienia związane ze
sprawdzaniem poprawności i wówczas będzie można porównać wynik działania.
Teraz wiemy już, w jaki sposób parser zgodny z interfejsem SAX przetwarza dokument XML. Po-
winniśmy również rozumieć wywołania pojawiające się w czasie procesu przetwarzania oraz to,
w jaki sposób można z nich uzyskać informacje potrzebne aplikacji. W następnych dwóch rozdzia-
łach zostaną omówione zagadnienia związane ze sprawdzaniem poprawności dokumentu XML za
pomocą dodatkowych klas SAX służących do obsługi definicji DTD. Teraz sprawdzimy, jakie
błędy mogą się pojawić, gdy dokument nie będzie poprawny, co dzieje się, gdy dokument XML
nie jest poprawny i jakie błędy mogą z tego wynikać.
Procedury obsługi błędów
Oprócz interfejsu
ContentHandler
, SAX udostępnia również interfejs
ErrorHandler
, słu-
żący do obsługi sytuacji awaryjnych zaistniałych w czasie przetwarzania. Klasa ta działa podobnie
jak omówiona wcześniej procedura obsługi dokumentu, ale zdefiniowano w niej jedynie trzy
Procedury obsługi błędów
77
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 77
wywołania wsteczne. Przy pomocy tych trzech metod SAX obsługuje i powiadamia o wszystkich
możliwych błędach.
Każda metoda otrzymuje informacje o błędzie lub ostrzeżeniu poprzez klasę
SAXParseExcep-
tion
. Obiekt ten zawiera numer wiersza, w którym wystąpił błąd, identyfikator URI przetwarza-
nego dokumentu (dokument przetwarzany lub zewnętrzne odwołanie wewnątrz tego dokumentu)
oraz zwykłe informacje o błędzie, takie jak komunikat i dane ze śledzenia stosu. Ponadto każda
metoda może zgłosić
SAXException
. Na początku może się to wydawać nieco dziwne — pro-
gram obsługi wyjątków zgłaszający wyjątek? Należy jednak pamiętać, że każda z procedur obsługi
błędów otrzymuje wyjątki związane z przetwarzaniem. Może to być ostrzeżenie, które nie powin-
no przerwać procesu przetwarzania, lub błąd, który należy rozwiązać, aby to przetwarzanie mogło
być kontynuowane. Jednak wywołanie takie może także wykonywać operacje wejścia-wyjścia lub
inne, które mogą spowodować zgłoszenie wyjątku — i wyjątek ten należy przekazać aż „na samą
górę”, do aplikacji. Służy do tego właśnie wyjątek
SAXException
.
Załóżmy, że procedura obsługi błędów otrzymuje powiadomienia o błędzie i zapisuje je do pliku
dziennika błędów. Metoda taka musi być w stanie albo dopisać informacje do pliku istniejącego,
albo stworzyć nowy plik. Gdyby w czasie przetwarzania dokumentu XML wystąpiło ostrzeżenie,
także „dowiadywałaby” się o nim ta metoda. Ostrzeżenie miałoby na celu wywołanie odpowie-
dniej procedury i nie przerywałoby przetwarzania. Jednakże, jeśli procedura obsługi błędów nie
byłaby w stanie zapisywać do pliku dziennika, musiałaby zakomunikować parserowi i aplikacji, że
przetwarzanie ma zostać przerwane. Efekt taki można uzyskać poprzez przechwycenie wszystkich
wyjątków wejścia-wyjścia i ponowne zgłoszenie ich aplikacji wywołującej, powodując przerwanie
przetwarzania. Powyższy przykład wyjaśnia, dlaczego procedury obsługi błędów muszą mieć
możliwość zgłaszania wyjątków (patrz przykład 3.3).
Przykład 3.3. Procedura obsługi, która może zgłosić SAXException
public void warning(SAXParseException exception)
throws SAXException {
try {
FileWriter fw = new FileWriter("error.log");
BufferedWriter bw = new BufferedWriter(fw);
bw.write("Ostrzeżenie: " + exception.getMessage() + "\n");
bw.flush();
bw.close();
fw.close();
} catch (Exception e) {
throws new SAXException("Brak możliwości zapisu do pliku dziennika",
e);
}
}
Teraz można już zdefiniować szkielet procedury obsługi błędów i zarejestrować ją w parserze
w taki sam sposób, jak w przypadku procedury obsługi zawartości. Najpierw trzeba zaimportować
klasę
SAXParseException
i
ErrorHandler
:
import java.io.IOException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
78
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 78
Teraz w tym samym pliku z programem w Javie (znów na dole, po klasie
MyContentHandler
)
należy zaimplementować interfejs
ErrorHandler
, zdefiniowany w ramach SAX-a. Podobnie jak
przy omawianiu klasy
ContentHandler
, poniżej są przedstawione puste implementacje, które
wkrótce zostaną wypełnione treścią:
/**
* <b><code>MyErrorHandler</code></b> implementuje interfejs SAX
* <code>ErrorHandler</code> i definiuje zachowanie wywołań
* wstecznych powiązanych z błędami w XML-u.
*/
class MyErrorHandler implements ErrorHandler {
/**
* <p>
* Powiadomienie o ostrzeżeniu; żadne zasady XML nie zostały "złamane",
* ale wydaje się, że czegoś brakuje lub coś jest wpisane niepoprawnie.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, jaki
nastąpił.
* @throws <code>SAXException</code> gdy coś pójdzie nie tak.
*/
public void warning(SAXParseException exception)
throws SAXException {
}
/**
* <p>
* Tutaj komunikujemy o błędzie, jeśli taki się pojawi; błąd oznacza, że
* złamano regułę i zazwyczaj pojawia się w czasie sprawdzania składni;
* przetwarzanie może jeszcze być kontynuowane.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, który
nastąpił.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void error(SAXParseException exception)
throws SAXException {
}
/**
* <p>
* Tutaj komunikujemy, że nastąpił błąd krytyczny; oznacza to, że
* złamano regułę w ten sposób, że dalsze przetwarzanie
* nie jest możliwe albo jest bezcelowe.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, który
nastąpił.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void fatalError(SAXParseException exception)
throws SAXException {
}
}
Aby procedura obsługi błędów mogła zostać użyta, należy ją zarejestrować w parserze SAX. Służy
do tego metoda
setErrorHandler()
interfejsu
XMLReader
w naszej przykładowej metodzie
Procedury obsługi błędów
79
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 79
demo()
. Metoda
setErrorHandler()
pobiera jako jedyny parametr interfejs
ErrorHand-
ler
lub jego implementację:
// Stwórz egzemplarze procedur obsługi
ContentHandler contentHandler = new MyContentHandler();
ErrorHandler errorHandler = new MyErrorHandler();
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
parser.setErrorHandler(errorHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
...
A teraz spróbujmy sprawić, żeby na ekranie pojawiały się komunikaty w przypadku wywołania
tych metod.
Ostrzeżenia
Za każdym razem, gdy pojawi się ostrzeżenie (wynikające ze specyfikacji XML 1.0), metoda ta wy-
woływana jest w zarejestrowanej procedurze obsługi błędów. Są różne sytuacje powodujące wygene-
rowanie ostrzeżenia; wszystkie związane są z definicjami DTD i poprawnością składniową dokumentu.
Zostaną one omówione w następnych dwóch rozdziałach. Tymczasem zdefiniujemy prostą metodę wy-
świetlającą numer wiersza, identyfikator URI i komunikat w przypadku wystąpienia ostrzeżenia.
Ponieważ chcemy, aby w razie ostrzeżenia przetwarzanie zostało przerwane, zgłaszamy wyjątek
SAXException i pozwalamy aplikacji nadrzędnej elegancko zakończyć działanie i zwolnić zasoby:
/**
* <p>
* Powiadomienie o ostrzeżeniu; żadne zasady języka XML nie zostały złamane,
* ale wydaje się, że czegoś brakuje lub coś jest wpisane niepoprawnie.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, jaki nastąpił.
* @throws <code>SAXException</code> gdy coś pójdzie nie tak.
*/
public void warning(SAXParseException exception)
throws SAXException {
System.out.println("**Przetwarzanie ostrzeżenia**\n" +
" Wiersz: " +
exception.getLineNumber() + "\n" +
" URI: " +
exception.getSystemId() + "\n" +
80
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 80
" Komunikat: " +
exception.getMessage());
throw new SAXException("Napotkano ostrzeżenie");
}
Błędy niekrytyczne
Błędy występujące w czasie przetwarzania, które można naprawić, ale które stanowią pogwałcenie
fragmentu specyfikacji XML, postrzegane są jako błędy niekrytyczne. Procedura obsługi błędów
powinna je przynajmniej odnotować w pliku dziennika, bo zazwyczaj są na tyle poważne, że za-
sługują na zwrócenie uwagi użytkownika lub administratora aplikacji; jednak nie są aż tak istotne,
by przerwać jej działanie. Tak jak w przypadku ostrzeżeń, większość błędów niekrytycznych
związanych jest z poprawnością dokumentu, która zostanie omówiona w odpowiednich rozdzia-
łach. Podobnie jak w przypadku ostrzeżeń, nasza prosta procedura obsługi błędów wyświetli infor-
macje przesłane do wywołania wstecznego i zakończy proces przetwarzania:
/**
* <p>
* Tutaj komunikujemy o błędzie, jeśli taki się pojawi; błąd oznacza, że
* złamano regułę i zazwyczaj pojawia się w czasie sprawdzania składni;
* przetwarzanie może jeszcze być kontynuowane.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, który
nastąpił.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void error(SAXParseException exception)
throws SAXException {
System.out.println("**Przetwarzanie błędu**\n" +
" Wiersz: " +
exception.getLineNumber() + "\n" +
" URI: " +
exception.getSystemId() + "\n" +
" Komunikat: " +
exception.getMessage());
throw new SAXException("Napotkano błąd");
}
Błędy krytyczne
Błędy krytyczne to te, które wymuszają zatrzymanie działania parsera. Zazwyczaj wynikają z niepo-
prawnego sformatowania dokumentu i ich pojawienie się oznacza, że albo dalsze przetwarzanie nie ma
sensu, albo jest technicznie niemożliwe. Procedura obsługi błędu powinna niemal zawsze poinformo-
wać użytkownika lub administratora o wystąpieniu takiego błędu; pozostawienie aplikacji samej sobie
może grozić całkowitym jej zawieszeniem. W naszym przykładzie będziemy emulowali zachowanie
dwóch pozostałych metod i zatrzymamy przetwarzanie, wyświetlając komunikat o błędzie na ekranie:
/**
* <p>
* Tutaj komunikujemy, że nastąpił błąd krytyczny; oznacza to, że
* złamano regułę w ten sposób, że dalsze przetwarzanie
* nie jest możliwe albo jest bezcelowe.
* </p>
*
Procedury obsługi błędów
81
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 81
* @param exception <code>SAXParseException</code> -- wyjątek, który
nastąpił.
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void fatalError(SAXParseException exception)
throws SAXException {
System.out.println("**Przetwarzanie błędu krytycznego**\n" +
" Wiersz: " +
exception.getLineNumber() + "\n" +
" URI: " +
exception.getSystemId() + "\n" +
" Komunikat: " +
exception.getMessage());
throw new SAXException("Napotkano błąd krytyczny");
}
Po zakodowaniu procedury obsługi tego trzeciego typu błędu powinno być możliwe przekompilo-
wanie pliku źródłowego i powtórne uruchomienie go na naszym dokumencie XML. Uzyskany
wynik nie powinien różnić się od poprzedniego, bo dokument XML nie zawiera błędów, o których
aplikacja mogłaby poinformować. W kolejnych podrozdziałach zostaną omówione błędy w doku-
mentach XML, w których nie jest wykonywane sprawdzanie poprawności.
Rozbijanie danych
Skoro posiadamy już procedury obsługi błędów, to warto zobaczyć, jak one działają. Jak już to by-
ło wspomniane, większość ostrzeżeń i błędów niekrytycznych związanych jest z poprawnością
składni dokumentu — zagadnienia te zostaną omówione w kilku następnych rozdziałach. Jest je-
dnak jeden błąd niekrytyczny, który pojawia się w dokumentach XML nie sprawdzonych pod
względem poprawności. Chodzi tutaj o wersję języka XML zgłaszaną przez dokument. Aby zoba-
czyć, jak ten błąd jest zgłaszany, zmieńmy nasz przykładowy dokument w następujący sposób:
<?xml version="1.2"?>
<!-- To na razie nie jest potrzebne.
<?xml-stylesheet href="XSL\JavaXML.html.xsl" type="text/xsl"?>
<?xml-stylesheet href="XSL\JavaXML.wml.xsl" type="text/xsl"
media="wap"?>
<?cocoon-process type="xslt"?>
<!DOCTYPE JavaXML:Ksiazka SYSTEM "DTD\JavaXML.dtd">
-->
Teraz należy uruchomić przykładowy program na takim zmienionym pliku XML. Wynik powi-
nien być zbliżony do przedstawionego w przykładzie 3.4.
Przykład 3.4. Program SAXParserDemo wyświetlający błąd
D:\prod\JavaXML>java SAXParserDemo D:\prod\JavaXML\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
**Przetwarzanie błędu**
Wiersz: 1
URI: file:/D:/prod/JavaXML/contents/contents.xml
Komunikat: XML version "1.2" is not supported.
Błąd w przetwarzaniu: Napotkano błąd
82
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 82
Kiedy parser XML operuje na dokumencie, którego wersja języka XML została określona jako
nowsza niż te obsługiwane przez parser, zgłaszany jest błąd niekrytyczny, zgodnie ze specyfikacją
XML 1.0. W ten sposób aplikacja zostaje poinformowana o tym, że nowe funkcje, które mogły
zostać użyte w dokumencie, nie muszą być rozpoznawane przez parser w tej wersji. Przetwarzanie
może być kontynuowane, a więc taki błąd nie jest postrzegany jako krytyczny. Jednakże może on
mieć istotny wpływ na dokument (np. nowa składnia może powodować dalsze błędy), a więc ma
wyższą rangę niż ostrzeżenie. To dlatego wywoływana jest metoda
error()
, powodująca wysła-
nie komunikatu informującego o błędzie i zatrzymanie przetwarzania w naszym przykładowym
programie.
Wszystkie inne istotne ostrzeżenia będą omawiane w następnych dwóch rozdziałach; wciąż istnie-
ją jednak błędy krytyczne, które mogą się pojawiać w dokumencie nie sprawdzonym pod kątem
poprawności. Są one związane z niepoprawnym formatowaniem dokumentu. Parsery XML nie po-
trafią naprawiać takich dokumentów, a więc błąd w składni powoduje zatrzymanie procesu prze-
twarzania. Najprostszym sposobem demonstracji tego zagadnienia jest wprowadzenie błędów do
dokumentu XML. Ponownie zmieńmy deklarację wersji na 1.0 i wprowadźmy następujące zmiany
w naszym dokumencie:
<?xml version="1.0" encoding="ISO-8859-2"?>
<!-- Tego jeszcze nie potrzebujemy.
<?xml-stylesheet href="XSL\JavaXML.html.xsl" type="text/xsl"?>
<?xml-stylesheet href="XSL\JavaXML.wml.xsl" type="text/xsl"
media="wap"?>
<?cocoon-process type="xslt"?>
<!DOCTYPE JavaXML:Ksiazka SYSTEM "DTD\JavaXML.dtd">
-->
<!-- Java i XML -->
<JavaXML:Ksiazka xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/">
</JavaXML:Tytul>Java i XML</JavaXML:Tytul>
<!-- Zwróćmy uwagę na niepoprawnie wstawiony ukośnik przed elementem
JavaXML:Tytul -->
Taki dokument nie jest poprawnie sformatowany. Uruchamiamy program
SAXParserDemo
na
tak zmodyfikowanym pliku. Wynik przestawiony jest w przykładzie 3.5.
Przykład 3.5. Program SAXParserDemo wyświetlający błąd krytyczny
D:\prod\JavaXML>java SAXParserDemo D:\prod\JavaXML\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
startElement: Ksiazka w przestrzeni nazw
http://www.oreilly.com/catalog/javaxml/ (JavaXML:Ksiazka)
znaki:
**Przetwarzanie błędu krytycznego**
Wiersz: 13
URI: file:/D:/prod/JavaXML/contents/contents.xml
Komunikat: The element type "JavaXML:Ksiazka" must be terminated by the
matching end-tag "</JavaXML:Ksiazka>".
Parser zgłasza niepoprawne zakończenie elementu
JavaXML:Ksiazka
. Aby zrozumieć komu-
nikat o błędzie, należy zdać sobie sprawę z faktu, że parser „widzi” znak ukośnika przed ele-
mentem
JavaXML:Tytul
i zakłada, że element, który musi zostać zamknięty, to
JavaXML:
Lepszy sposób ładowania parsera
83
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 83
Ksiazka
— czyli ten, który jest obecnie „otwarty”. Kiedy znajduje znacznik zamykający ele-
mentu
JavaXML:Tytul
, zgłasza niepoprawność tego znacznika jako zamknięcia otwartego ele-
mentu
JavaXML:Ksiazka
.
Uważna analiza omawianej procedury obsługi błędów pozwala rozumieć, na jakie problemy moż-
na natknąć się w czasie przetwarzania i jak można ich unikać. W rozdziale 5. ponownie zajmiemy
się procedurami obsługi błędów i przyjrzymy się problemom zgłaszanym przez parser wykonujący
sprawdzanie poprawności składni.
Lepszy sposób ładowania parsera
Choć potrafimy już przetwarzać dokumenty XML z wykorzystaniem interfejsu SAX, w naszym
kodzie wciąż znajduje się pewien ewidentny błąd. Przeanalizujmy powtórnie proces tworzenia
egzemplarza klasy
XMLReader
:
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
parser.setErrorHandler(errorHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
Czy powyższy fragment nie budzi żadnych wątpliwości? Wskazówką może być jeszcze inny
wiersz naszego kodu:
// Tutaj importujemy implementację czytnika XML (XML Reader)
import org.apache.xerces.parsers.SAXParser;
Jawnie importujemy implementację klasy
XMLReader
danego producenta, a następnie bezpośre-
dnio tworzymy egzemplarz tej implementacji. Problem nie wynika z trudności tego zadania, ale
z faktu, że gubimy jedną z najważniejszych cech Javy — przenośność. Takiego kodu nie da się
uruchomić, a nawet skompilować na platformie nie wykorzystującej parsera Apache Xerces. A prze-
cież możliwe jest nawet, że w zaktualizowanej wersji Xerces nazwa używanej klasy może ulec
zmianie! A zatem nasz kod nie jest przenośny.
W związku z tym zaleca się stworzenie egzemplarza klasy o nazwie klasy pobranej z implemen-
tacji. W ten sposób w kodzie źródłowym możliwe jest zastosowanie rozwiązania polegającego na
zmianie prostego parametru
String
. Wszystko to jest możliwe w interfejsie SAX 2.0, a klasa,
która udostępni potrzebną nam metodę, nosi nazwę
org.xml.sax.helpers.XMLReader-
Factory
:
/**
* Próba utworzenia egzemplarza czytnika XML z nazwy klasy.
84
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 84
*
* <p>Po otrzymaniu nazwy klasy metoda ta usiłuje załadować
* i stworzyć egzemplarz klasy jako czytnika XML.</p>
*
* @return Nowy czytnik XML.
* @exception org.xml.sax.SAXException Jeśli klasa nie może zostać
* załadowana; nie można stworzyć egzemplarza lub
* wykonać rzutowania na XMLReader.
* @see #createXMLReader()
*/
public static XMLReader createXMLReader (String className)
throws SAXException {
// Implementacja
}
Metody tej w naszym kodzie użyjemy następująco:
try {
// Stwórz egzemplarz parsera
XMLReader parser =
XMLReaderFactory.createXMLReader(
"org.apache.xerces.parsers.SAXParser");
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
parser.setErrorHandler(errorHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
Ta statyczna metoda pobiera nazwę klasy parsera do załadowania, a zwraca wersję tej klasy w postaci
egzemplarza rzutowanego na interfejs
XMLReader
(zakładając, że faktycznie implementowany
jest
XMLReader
). Ewentualne problemy są zgłaszane do programu wywołującego za pośredni-
ctwem
SAXException
. Teraz wystarczy dodać jedną instrukcję importującą, usunąć odwołanie
do parsera konkretnego producenta, wprowadzić powyższe zmiany i już można przekompilować
nasz kod źródłowy:
import java.io.IOException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.ErrorHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
// To usuwamy
// import org.apache.xerces.parsers.SAXParser;
No i znów mamy przenośny kod! Aby jeszcze bardziej rozszerzyć przenośność, warto przechowy-
wać nazwę klasy parsera w pliku właściwości. Umożliwia to ładowanie klasy „na gorąco”, a kod
Uwaga! Pułapka!
85
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 85
można przenosić pomiędzy platformami bez konieczności rekompilacji — zmianie ulega jedynie
plik właściwości. Tak przebiega proces odczytywania plików właściwości w programie:
try {
// Stwórz egzemplarz parsera
XMLReader parser =
XMLReaderFactory.createXMLReader(
PropertiesReader().getInstance()
.getProperty("parserClass"));
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
parser.setErrorHandler(errorHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
Klasa narzędziowa
PropertiesReader
posłużyła tutaj do odczytania pliku właściwości i zwró-
cenia wartości odpowiadającej kluczowi
parserClass
(wartość ta będzie zawierała nazwę klasy
parsera, którą należy wykorzystać dla danej platformy. W naszych przykładach byłby to łańcuch
org.apache.xerces.SAXParser
. Oczywiście, można byłoby także skorzystać z systemo-
wych właściwości Javy, ale nie są one odpowiednie dla rozproszonych aplikacji WWW przedsta-
wionych w tej książce (obsługiwane są z poziomu wiersza poleceń). Często aplikacje rozproszone
uruchamiane są jako całość, a nie indywidualnie, a więc określenie właściwości systemowych dla
jednego tylko komponentu byłoby trudne.
Uwaga! Pułapka!
Programista w trakcie pracy może napotkać wiele pułapek. Czytelnik powinien wiedzieć, jak uni-
kać typowych błędów programistycznych związanych z używaniem interfejsu SAX; w dalszych
rozdziałach zostaną także omówione pułapki związane z innymi interfejsami API.
Mój parser nie obsługuje interfejsu SAX 2.0. Co robić?
Nie załamywać się. Przede wszystkim zawsze można zmienić parser na inny — utrzymanie zgod-
ności z najnowszą wersją SAX należy do obowiązku producenta parsera. Jeśli producent nie na-
dąża za standardami, to znaczy, że w naszym parserze mogą też istnieć inne błędy. Istnieją jednak
przypadki, kiedy zmuszeni jesteśmy korzystać z danego parsera (np. z powodu konieczności ob-
sługi zastanego kodu lub aplikacji). Jednak nawet w takich sytuacjach nie wszystko jest stracone.
SAX 2.0 wyposażony jest w klasę pomocniczą
org.xml.sax.helpers.ParserAdapter
,
która powoduje, że implementacja klasy
Parser
w SAX 1.0 zachowuje się jak implementacja
XMLReader
w interfejsie SAX 2.0. To poręczne narzędzie pobiera implementację
Parser
z wersji 1.0 jako parametr wejściowy i dalej może być używane zamiast tej implementacji. Umoż-
liwia ustawienie
ContentHandler
i poprawnie obsługuje wszystkie wywołania związane z prze-
strzenią nazw. Jedyna funkcja, do jakiej nie będziemy mieli dostępu, to obsługa pomijania encji
86
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 86
(nie była ona po prostu dostępna w implementacji 1.0 w jakiejkolwiek formie, więc nie może być
emulowana). Sposób użycia tej klasy przedstawiony jest w przykładzie 3.6.
Przykład 3.6. Korzystanie z klasy Parser z SAX 1.0 jako klasy XMLReader z 2.0.
try {
// Zarejestruj parser w SAX
Parser parser =
ParserFactory.makeParser(
"org.apache.xerces.parsers.SAXParser");
ParserAdapter myParser = new ParserAdapter(parser);
// Zarejestruj procedurę obsługi zawartości
myParser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
myParser.setErrorHandler(errorHandler);
// Przetwórz dokument
myParser.parse(uri);
} catch (ClassNotFoundException e) {
System.out.println(
"Nie znaleziono klasy parsera.");
} catch (IllegalAccessException e) {
System.out.println(
"Niewystarczające przywileje do załadowania klasy parsera.");
} catch (InstantiationException e) {
System.out.println(
"Niemożliwe utworzenie egzemplarza klasy parsera.");
} catch (ClassCastException e) {
System.out.println(
"Parser nie ma zaimplementowanego org.xml.sax.Parser");
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " +
e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
Jeśli Czytelnik dopiero zaczyna poznawać interfejs SAX i ma kłopoty ze zrozumieniem tego przy-
kładu, nie powinien się martwić — w przypadku korzystania z najnowszej i najlepszej wersji SAX
(2.0) prawdopodobnie nigdy nie będzie trzeba używać kodu podobnego do powyższego. Przydatny
jest on tylko tam, gdzie konieczne jest korzystanie z parsera 1.0.
SAX XMLReader — wielokrotne użycie a współbieżność
Jedną z najciekawszych cech Javy jest fakt, że w niezwykle prosty sposób można wielokrotnie
używać tych samych obiektów. Cechę tę posiadają również parsery SAX. Po utworzeniu egzem-
plarza
XMLReader
możliwe jest ciągłe jego używanie i przekazywanie mu wielu (nawet setek)
dokumentów. Kolejne dokumenty lub źródła
InputSources
przekazywane są parserowi, zatem
może być on wykorzystany do różnych zadań. Parsery nie są jednak współbieżne. Kiedy już roz-
począł się proces przetwarzania, parsera nie będziemy w stanie wykorzystać ponownie dopóty,
dopóki to pierwsze przetwarzanie nie zostanie zakończone. Dla tych z Czytelników, którzy lubią
korzystać z algorytmów rekurencyjnych, to właśnie może stanowić pułapkę. Kiedy spróbujemy
użyć parsera wtedy, gdy ten jest akurat w trakcie przetwarzania innego dokumentu, zgłoszony zo-
stanie raczej niemiły
SAXException
i całe przetwarzanie zostanie zatrzymane. Jaki z tego wy-
Uwaga! Pułapka!
87
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 87
nika wniosek? Dokumenty należy przetwarzać jeden po drugim, albo — godząc się na wszelkie
tego skutki — tworzyć więcej egzemplarzy parsera.
Locator w złym miejscu
Kolejną niebezpieczną (a z pozoru niewinną) cechą zdarzeń SAX jest fakt udostępniania egzem-
plarza
Locator
poprzez wywołanie metody
setDocumentLocator()
. W ten sposób aplika-
cja poznaje źródło zdarzenia SAX, co umożliwia podjęcie decyzji o dalszym przetwarzaniu oraz
sposobie reagowania na inne zdarzenia. Jednakże to miejsce jest określone poprawnie jedynie na
czas istnienia egzemplarza
ContentHandler
. Po ukończeniu przetwarzania
Locator
nie jest
już poprawny (szczególnie wtedy, gdy rozpoczyna się następne przetwarzanie). Błędem popełnia-
nym przez nowicjuszy jest przechowywanie referencji do obiektu
Locator
wewnątrz zmiennej
należącej do klasy spoza wywołania wstecznego:
public void setDocumentLocator(Locator locator) {
// Zachowanie Locator w klasie poza ContentHandler
mojaInnaKlasa.setLocator(locator);
}
...
public metodaInnejKlasy() {
// Próba użycia poza ContentHandler
System.out.println(locator.getLineNumber());
}
To bardzo zły pomysł —
Locator
traci znaczenie, bo jesteśmy już poza implementacją
Con-
tentHandler
. Często korzystanie z takiej zmiennej powoduje nie tylko otrzymywanie przez
aplikację nieprawidłowych informacji, ale również uszkodzenie dokumentu XML. Innymi słowy,
obiektu tego należy używać lokalnie, a nie globalnie. W naszej implementacji
Content-
Handler
otrzymany
Locator
zachowaliśmy do zmiennej. Następnie moglibyśmy jej użyć w po-
prawny sposób (np. do podania numerów wierszy, w których napotkaliśmy poszczególne elementy):
public void startElement(String namespaceURI, String localName,
String rawName, Attributes atts)
throws SAXException {
System.out.print("startElement: " + localName +
" w wierszu " + locator.getLineNumber());
if (!namespaceURI.equals("")) {
System.out.println(" w przestrzeni nazw " + namespaceURI +
" (" + rawName + ")");
} else {
System.out.println(" nie posiada skojarzonej przestrzeni nazw");
}
for (int i=0; i<atts.getLength(); i++)
System.out.println(" Atrybut: " + atts.getLocalName(i) +
"=" + atts.getValue(i));
}
Wyprzedzanie danych
Wywołanie
characters()
przyjmuje tablicę znaków oraz parametry
start
i
end
, które
wskazują, gdzie należy rozpocząć, a gdzie skończyć odczytywanie z tej tablicy. Może to prowa-
dzić do pomyłek — typowym błędem jest odczytywanie znaków z tablicy w następujący sposób:
88
Rozdział 3. Przetwarzanie kodu XML
C:\WINDOWS\Pulpit\Szymon\Java i XML\03-08.doc — strona 88
public void characters(char[] ch, int start, int end)
throws SAXException {
for (int i=0; i<ch.length; i++)
System.out.println(i);
}
Błąd polega na odczytaniu tablicy od początku do końca. To naturalne zachowanie programisty,
wynikające z przyzwyczajeń programowania w Javie, C czy innym języku, gdzie iteracje po pęt-
lach są często spotykane. Jednak w przypadku zdarzenia SAX takie postępowanie może powo-
dować poważny błąd. Parsery SAX przekazują początek i koniec tablicy, z której należy korzystać
we wszystkich pętlach czytających z tej tablicy. W ten sposób dane tekstowe przetwarzane są na
niższym poziomie, co umożliwia optymalizację pracy parsera (np. odczytywanie danych z wyprze-
dzeniem oraz wielokrotne wykorzystanie tej samej tablicy). Wszystko to jest poprawnym działa-
niem w interfejsie SAX, ponieważ zakłada się, że aplikacja nie będzie próbowała „wybiegać
przed” parametr
end
przekazany do wywołania.
Tego typu błąd powoduje wyświetlenie na ekranie (lub przetworzenie przez aplikację) przypadko-
wych danych. Pętla wygląda zupełnie zwyczajnie i kompiluje się „bez zająknięcia”, a więc jest to
prawdziwa pułapka, na którą musi już uważać sam programista.
Co dalej?
Teraz Czytelnik powinien już dobrze rozumieć zasady działania interfejsów SAX oraz sposób,
w jaki współdziałają z parserem XML i procesem przetwarzania — wszystko w kontekście parsera
nie sprawdzającego poprawności dokumentu XML. Interfejsy te mają kluczowe znaczenie dla reszty
zagadnień opisywanych w tej książce — w następnych rozdziałach zostaną poszerzone wiadomości
o interfejsie SAX, a w programie zastosowane będą dodatkowe klasy tego interfejsu. W następ-
nym rozdziale omówione zostaną zagadnienia związane z zawężaniem XML-a, procesem spraw-
dzania poprawności dokumentu, a także przedstawione zostaną definicje DTD i schematy.