|
5
Sprawdzanie poprawności składni XML-a |
|
Po lekturze poprzednich rozdziałów Czytelnik potrafi już stworzyć dokument XML, przetworzyć go za pomocą klas SAX oraz zawęzić. W tym rozdziale zostanie omówione kolejne zagadnienie — sprawdzanie poprawności dokumentu XML za pomocą Javy. Bez takiej możliwości tworzenie aplikacji firma-firma oraz komunikacji międzyaplikacyjnej staje się o wiele trudniejsze. Zawężenie zwiększa przenośność danych; natomiast sprawdzanie poprawności — spójność. Innymi słowy, możliwość zawężenia dokumentu nie zda się na wiele, jeśli stworzonych zawężeń nie przeforsujemy w aplikacji XML.
W tym rozdziale przedstawione zostaną klasy i interfejsy SAX służące do sprawdzania poprawności dokumentów XML względem ich zawężeń. Czytelnik dowie się, jak ustawić cechy i właściwości parsera zgodnego z SAX, aby możliwe było proste sprawdzanie poprawności, obsługa przestrzeni nazw i wykonywanie innych czynności. Ponadto szczegółowo omówione zostaną błędy i ostrzeżenia zgłaszane przez parsery sprawdzające poprawność.
Konfiguracja parsera
W obliczu bogactwa specyfikacji i technologii autorstwa konsorcjum W3C związanych z językiem XML, dodanie obsługi nowej cechy lub właściwości nie jest proste. W wielu implementacjach parserów dodano własne rozszerzenia lub metody kosztem przenośności kodu. W pakietach tych mógł zostać zaimplementowany interfejs XMLReader, ale metody do ustawiania sprawdzania poprawności, obsługi przestrzeni nazw i innych kluczowych cech są odmienne w różnych implementacjach parserów. W związku z tym w interfejsie SAX 2.0 zdefiniowano standardowy mechanizm do ustawiania istotnych właściwości i cech parsera. Umożliwia on dodawanie nowych właściwości i cech w miarę przyjmowania ich przez W3C, bez konieczności stosowania własnych metod lub rozszerzeń.
Ustawianie właściwości i cech
Na szczęście dla nas, interfejs SAX 2.0 wyposażono w metody służące do ustawiania właściwości i cech interfejsu XMLReader. Oznacza to, że w celu zażądania sprawdzania poprawności, ustawienia separatora przestrzeni nazw czy obsługi innych cech nie trzeba zmieniać zbyt wiele w istniejącym kodzie. Odpowiednie metody przedstawione są w tabeli 5.1.
Tabela 5.1. Metody do obsługi właściwości i cech parsera
Metoda |
Zwraca |
Parametry |
Składnia |
setProperty() |
void |
String IDwlasciwosci, Object wartosc |
parser.setProperty(„[URI wlasciwosci]”, „[Parametr obiektu]”); |
setFeature() |
void |
String IDcechy, boolean stan |
parser.setFeature(„[URI cechy]”, true); |
getProperty() |
Object |
String IDwlasciwosci |
String separator = (Stringparser.getProperty(„[URI wlasciwosci]”); |
getFeature() |
boolean |
String IDcechy |
if (parser.getFeature(„[URI cechy]”)) {robiCos();} |
W każdej z powyższych metod identyfikatorem ID określonej właściwości lub cechy jest identyfikator URI. Lista najważniejszych właściwości i cech jest zamieszczona w dodatku B. Producent parsera XML powinien udostępnić dodatkową dokumentację informujacą o obsługiwanych cechach i właściwościach. Pamiętajmy jednak, że identyfikatory te, podobnie jak identyfikatory URI przestrzeni nazw, służą wyłącznie do kojarzenia odpowiednich cech. Dobry parser umożliwi korzystanie z nich bez posiadania połączenia z siecią. W tym sensie identyfikatory URI można postrzegać jako proste stałe, które akurat mają format URI. Po skorzystaniu z takiej metody następuje lokalne przetworzenie identyfikatora, często jako stałej reprezentującej odpowiednią czynność, którą należy podjąć.
W kontekście konfiguracji parsera właściwość wymaga obecności obiektu, z którego można skorzystać. Na przykład w celu obsługi leksykalnej jako wartość właściwości można dostarczyć klasę LexicalHandler. Cecha natomiast ma postać znacznika wykorzystywanego przez parser w celu określenia, czy ma nastąpić przetwarzanie określonego typu. Typowe cechy to sprawdzanie poprawności, obsługa przestrzeni nazw i dołączanie encji zewnętrznych.
Najwygodniejsza własność powyższych metod polega na tym, że umożliwiają one łatwe dodawanie i zmianę różnych cech. Nowe lub zaktualizowane właściwości wymagają obsługi ze strony parsera, ale dostęp do nich uzyskuje się wciąż za pomocą tych samych metod — konieczne jest tylko zdefiniowanie nowego identyfikatora URI. Bez względu na złożoność nowych koncepcji związanych z XML, ich implementacja w parserach przebiega bezproblemowo, właśnie dzięki tym metodom.
Włączanie sprawdzania poprawności
Czytelnik dowiedział się już, jak ustawiać cechy i właściwości, ale nie poznał jeszcze samych cech i właściwości. W tym rozdziale interesujemy się przede wszystkim sprawdzaniem poprawności w czasie przetwarzania. Aby zilustrować, jak ważne są wspomniane wyżej metody, trzeba odwołać się do historii. W interfejsie SAX 1.0 implementacje parserów musiały udostępniać własne rozwiązania do obsługi przetwarzania ze sprawdzaniem poprawności lub bez. Nie było możliwości włączenia lub wyłączenia sprawdzania poprawności w sposób standardowy, więc najprostszy sposób polegał na dostarczeniu dwóch niezależnych klas przetwarzających. Na przykład, aby wykonać przetwarzanie bez sprawdzania poprawności we wczesnych wersjach parsera Project X firmy Sun, trzeba było skorzystać z przedstawionego poniżej fragmentu kodu.
Przykład 5.1. Uruchamianie parsera nie sprawdzającego poprawności w interfejsie SAX 1.0
try {
// Rejestrujemy parser w SAX
Parser parser =
ParserFactory.makeParser(
"com.sun.xml.parser.Parser");
// Przetwarzamy dokument
parser.parse(uri);
} catch (Exception e) {
e.printStackTrace();
}
Ponieważ nie istniał standardowy mechanizm włączania sprawdzania poprawności, trzeba było załadować inną klasę; ta nowa klasa jest niemal identyczną implementacją interfejsu Parser w SAX 1.0, ale wykonującą sprawdzanie poprawności. Kod pozwalający na użycie parsera jest niemal identyczny (przykład 5.2), różnica jest tylko w klasie załadowanej w celu przetwarzania.
Przykład 5.2. Uruchamianie parsera sprawdzającego poprawność w interfejsie SAX 1.0
try {
// Rejestrujemy parser w SAX
Parser parser =
ParserFactory.makeParser(
"com.sun.xml.parser.ValidatingParser");
// Przetwarzamy dokument
parser.parse(uri);
} catch (Exception e) {
e.printStackTrace();
}
Włączając lub wyłączając sprawdzanie poprawności trzeba więc było zmieniać i kompilować kod. Ponadto wynikał z tego dodatkowy problem z przetwarzaniem. Standardowe środowisko programistyczne wykorzystuje kod, który sprawdza poprawność danych XML wytwarzanych przez aplikację. To sprawdzanie poprawności, choć obniża wydajność, zapewnia, że aplikacja tworzy zawsze poprawny kod XML albo że otrzymuje poprawne dokumenty XML na wejściu. Często takie zawężenia po intensywnym testowaniu można usunąć, dzięki czemu odzyskuje się wysoką wydajność działania aplikacji. Pozbawienie parsera możliwości sprawdzania poprawności jest uzasadnione, ponieważ dogłębne testy potwierdziły poprawność tworzonego dokumentu XML; ale zmiana ta wymaga modyfikacji i rekompilacji kodu. Wydaje się to być sprawą banalną, ale wiele firm nie pozwala na wdrożenie do produkcji kodu, który był modyfikowany później niż przed jakimś określonym czasem — często kilka dni, a nawet tygodni. Taka niewielka zmiana może więc spowodować dodatkowy cykl testowy — niejednokrotnie niepotrzebny, a dodatkowo wydłużający czas wdrożenie aplikacji.
Ale przecież nazwę parsera można pobrać z pliku właściwości (zostało to już powiedziane w rozdziale 2. przy okazji opisywania aspektów przenośności aplikacji XML). Tak, ale zmiana całej implementacji parsera tuż przed wdrażaniem aplikacji do produkcji to zmiana poważna, która powinna być należycie przetestowana. Jeśli porównamy to ze zmianą wartości zestawu cech (zakładając, że wartość do ustawiania tej cechy także pobierana jest z pliku właściwości), to łatwo zgadnąć, które rozwiązanie jest lepsze.
Z tych wszystkich powodów w interfejsie SAX 2.0 do XMLReader dodano omawiane metody. Dzięki nim włączenie sprawdzania poprawności polega na wykorzystaniu odpowiedniego identyfikatora URI: http://xml.org/sax/features/validation. Moglibyśmy zażądać również przetwarzania encji zewnętrznych i przestrzeni nazw, ale tymczasem „włączymy” tylko sprawdzanie poprawności (przykład 5.3).
Przykład 5.3. Włączanie sprawdzania poprawności
// Stwórz egzemplarze procedur obsługi
ContentHandler contentHandler = new MyContentHandler();
ErrorHandler errorHandler = new MyErrorHandler();
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);
parser.setFeature("http://xml.org/sax/features/validation",
true);
// 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());
}
Po tych prostych zmianach możemy już zmodyfikować nasz przykładowy plik XML tak, by znów zawierał odwołanie do definicji DTD i zewnętrzną encję (zostały one opatrzone komentarzami w poprzednim rozdziale).
<?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>
<JavaXML:Spis>
...
<!-- z encji także usuwamy komentarz -->
<JavaXML:Copyright>&OReillyCopyright;</JavaXML:Copyright>
Upewniamy się, czy w podanym tutaj katalogu mamy stworzoną wcześniej definicję DTD. Zanim uruchomimy przykład, musimy połączyć się z Internetem — pamiętajmy, że encje muszą zostać „przetłumaczone”. W naszym przykładowym pliku mamy taką encję — OReillyCopyright. W definicji DTD odwołujemy się do identyfikatora URI http://www.oreilly.com/catalog/javaxml/ docs/copyright.xml. Jeśli w czasie sprawdzania poprawności identyfikator URI nie jest dostępny, pojawią się błędy przetwarzania. Jeśli nie mamy dostępu do Internetu albo nie chcemy z niego korzystać, możemy odwołanie sieciowe zastąpić odwołaniem do pliku lokalnego. Na przykład tworzymy jednowierszowy plik jak w przykładzie 5.4.
Przykład 5.4. Lokalny plik z opisem praw autorskich
Przykładowy współdzielony plik z opisem praw autorskich.
Plik ten zachowujemy w katalogu dostępnym z poziomu programu parsera i zamieniamy deklarację encji w DTD na ścieżkę do tego pliku:
<!ENTITY OReillyCopyright SYSTEM
"encje/copyright.txt">
W tym przykładzie plik tekstowy zachowany jest pod nazwą copyright.txt w katalogu encje/. Po takiej zmianie możemy już uruchomić nasz program na przykładowym pliku XML.
Wynik sprawdzania poprawności
Sprawdzamy, czy pliki XML, DTD, plik z prawami autorskimi (o ile taki stworzyliśmy) oraz odpowiednie klasy Javy znajdują się w jednym miejscu, i uruchamiamy przykładowy program. Wynik może być zaskakujący (przykład 5.5).
Przykład 5.5. Wynik działania programu SAXParserDemo
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
**Przetwarzanie błędu**
Wiersz: 11
URI: file:/D:/prod/JavaXML/contents/contents.xml
Komunikat: Document root element "JavaXML:Ksiazka", must match DOCTYPE
root "JavaXML:Ksiazka".
Ten dość enigmatyczny komunikat wynika z istnienia poważnego problemu związanego z używaniem definicji DTD i przestrzeni nazw. Z informacji wynikałoby, że element główny określony w deklaracji DOCTYPE (JavaXML:Ksiazka) jest różny od elementu głównego samego dokumentu. Ale przecież elementy te są identyczne, prawda? Otóż nie! Domyślnie SAX 2.0 wymaga od parserów włączenia przetwarzania przestrzeni nazw, chyba że jawnie wyłączymy tę cechę. My ustawienia domyślnego nie zmienialiśmy, a więc nasza implementacja interfejsu XMLReader jest „świadoma” przestrzeni nazw. Nieoczekiwany rezultat wynika stąd, że nasz element główny jest postrzegany (przez parser) jako Ksiazka z przedrostkiem przestrzeni nazw JavaXML. Ale pamiętajmy, że XML 1.0 oraz definicje DTD nie rozróżniały pomiędzy przedrostkiem a nazwą elementu, a więc element główny DTD oczekuje nazwy JavaXML:Ksiazka. Kiedy znajduje Ksiazka, zgłasza błąd.
Jedynym sposobem obejścia tej nieco uciążliwej cechy SAX-a jest wyłączenie „świadomości” przestrzeni nazw w dokumentach, w których sprawdzanie poprawności odbywa się za pomocą DTD. Dodajemy następujący fragment kodu:
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);
// Włącz sprawdzanie poprawności
parser.setFeature("http://xml.org/sax/features/validation",
true);
// Wyłącz "świadomość" przestrzeni nazw
parser.setFeature("http://xml.org/sax/features/namespaces",
false);
// 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());
}
Po tej zmianie wszystkie nazwy traktowane są jako zawierające zarówno przedrostek przestrzeni nazw, jak i nazwę lokalną elementu. Do korzystania z przestrzeni nazw powrócimy w dalszej części rozdziału. Tymczasem spróbujmy ponownie uruchomić program z zaimplementowanymi zmianami. Powinniśmy uzyskać wynik widoczny w przykładzie 5.6.
Przykład 5.6. Wynik działania programu SAXParserDemo po wyłączeniu obsługi przestrzeni nazw
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
startElement: nie posiada skojarzonej przestrzeni nazw
Atrybut: =http://www.oreilly.com/catalog/javaxml/
ignorableWhitespace: [
]
startElement: nie posiada skojarzonej przestrzeni nazw
znaki: Java i XML
endElement:
...
Wynik może nas nieco rozczarować. Nie różni się niczym od wyniku uzyskanego w przypadku parsera nie sprawdzającego poprawności (zob. rozdział 3.)! To dlatego, że nasz dokument jest poprawny i poprawnie sformatowany — parser sprawdzający poprawność nie ma o czym komunikować. To ważne — zachowanie parsera w przypadku sprawdzania poprawnego dokumentu niemal nie różni się od zachowania parsera nie sprawdzającego poprawności. Jeśli to wydaje się dziwne, to należy przypomnieć, że sprawdzanie poprawności ma na celu zagwarantowanie, że dokument nie łamie zdefiniowanych wcześniej zasad. Jeśli wszystkie reguły są przestrzegane, aplikacja użyje dokumentu XML zgodnie z jego przeznaczeniem. Tylko kiedy nastąpi złamanie zasad, parser sprawdzający poprawność wkracza do akcji — poniżej zostaną omówione takie przypadki.
Wcześniej jednak trzeba zauważyć, że mimo wszystko powyższy wynik nieco różni się od poprzednich. Wcześniej wszystkie znaki białe pomiędzy elementami w dokumencie XML zgłaszane były poprzez wywołanie metody characters() w implementacji ContentHandler. W dokumencie XML, którego poprawność nie jest sprawdzana, parser ma możliwość zgłoszenia białych znaków albo przez tę metodę, albo przez ignorableWhitespace().Wynika to z faktu, że parser nie może zakładać określonego przeznaczenia obecności białych znaków pomiędzy elementami bez definicji DTD zawężającej XML. Kiedy w programie włączamy sprawdzanie poprawności, widzimy, że wszystkie znaki białe zgłaszane są za pomocą ignorableWhitespace(). Przy sprawdzaniu poprawności wszystkie białe znaki są ignorowane i traktowane tak, jakby ich nie było — chyba że zostanie to inaczej ujęte w definicji DTD. Dzięki temu parser potrafi określić, czy zawartość elementu XML zgodna jest z definicją DTD bez analizowania białych znaków otaczających tę zawartość. Innymi słowy, parser potraktuje taki fragment XML:
<dokument>
<element1>
<element2>Cześć!</element2>
</element1>
</dokument>
dokładnie tak samo jak:
<dokument><element1><element2>Cześć!</element2></element1></dokument>
Drugi fragment jest co prawda mniej miły dla ludzkiego oka, ale zgodny jest z tymi samymi zawężeniami co fragment pierwszy i powinien być identycznie traktowany w czasie sprawdzania poprawności. Brak wcięć nie może wpłynąć na działanie aplikacji korzystających z danych XML. Gdyby białe znaki tworzące wcięcia zgłaszane były poprzez wywołanie characters(), aplikacja monitorująca to wywołanie uznałaby, że dokumenty nie są identyczne.
Ostrzeżenia
W wyniku żądania sprawdzania poprawności nie mogą się pojawić niemal żadne ostrzeżenia. Jakiekolwiek dane XML niezgodne z odpowiednią definicją DTD są postrzegane jako błąd. Niepoprawność dokumentu została uznana przez W3C za rzecz na tyle ważną, że zawsze zgłaszany jest wtedy błąd. Dlatego trudno jest, szczególnie za pomocą parsera SAX 2.0, wywołać ostrzeżenie. Jednak istnieją parsery SAX 1.0, w których jest to możliwe. Na przykład parser Project X firmy Sun zawiera obecnie klasę służącą do sprawdzania poprawności XML-a w czasie przetwarzania i klasę nie wykonującą takiego sprawdzania. Jest to rozwiązanie podobne do omawianego wcześniej. Jeśli parser sprawdzający poprawność przetwarza dokument nie deklarujący definicji DTD w sposób jawny, generowane jest ostrzeżenie. Dotyczy to tylko niektórych implementacji parserów SAX 1.0, dlatego w niniejszej książce nie zostanie przedstawiony kod powodujący zgłoszenie ostrzeżenia. Czytelnik może natomiast obejrzeć wynik działania takiego ostrzeżenia (przykład 5.7) — podobny do tych opisywanych w rozdziale 3.
Przykład 5.7. Program SAXParserDemo zgłaszający ostrzeżenie
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
**Przetwarzanie ostrzeżenia**
Wiersz: 6
URI: file:/D:/prod/JavaXML/contents/contents.xml
Komunikat: Valid documents must have a <!DOCTYPE declaration.
Taki komunikat pojawiłby się, gdyby konstrukcja <!DOCTYPE> była opatrzona komentarzami lub gdyby została pominięta. Ponieważ niemal wszystkie parsery XML (w tym Project X firmy Sun) ewoluują w kierunku zgodności z SAX 2.0, takiego ostrzeżenia najprawdopodobniej nigdy nie zobaczymy.
Błędy niekrytyczne
Najbardziej typowym problemem z interfejsem SAX, jaki będzie zgłaszany w czasie sprawdzania poprawności XML, jest błąd o statusie „niekrytyczny”. Błąd taki generowany jest za każdym razem, gdy pogwałcone zostaną zawężenia XML. Aby to zademonstrować, wykonajmy następującą zmianę w naszym przykładowym dokumencie XML (czyniąc go niepoprawnym):
<?xml version="1.0" encoding="ISO-8859-2"?>
<!-- to jeszcze 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">
<!-- Java i XML -->
<JavaXML:Ksiazka xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"
dataPublikacji="czerwiec 2000">
<JavaXML:Tytul>Java i XML</JavaXML:Tytul>
<JavaXML:Spis>
Taka zmiana, choć nie wpływa na poprawność danych XML, nie jest zgodna z narzuconymi regułami — atrybut dataPublikacji nie został zadeklarowany w definicji DTD i w czasie przetwarzania zgłoszony zostanie błąd (przykład 5.8).
Przykład 5.8. Błąd zgłaszany przez program SAXParserDemo
D:\prod\JavaXML> java SAXParserDemo D:\prod\JavaXML\contents\contents.xml
Przetwarzanie pliku XML: contents.xml
* setDocumentLocator() została wywołana
Rozpoczyna się przetwarzanie...
**Przetwarzanie błędu**
Wiersz: 14
URI: file:///mnt/teksty_zdalny/biura/helion/javaxml/cwiczenia/contents.xml
Komunikat: Attribute "dataPublikacji" must be declared for element type
"JavaXML:Ksiazka".
Błąd w przetwarzaniu: Napotkano błąd
Analizator przesyła błąd do implementacji ErrorHandler, która zgłasza błąd — w tym przypadku mówiący, że atrybut nie został zadeklarowany dla zamykającego go elementu. Oczywiście, poprawność dokumentu można zburzyć na wiele sposobów — to tylko jeden z nich; zawsze jednak zgłoszony zostanie ten sam błąd, warto więc poeksperymentować. Należy po prostu pamiętać, że jakiekolwiek pogwałcenie DTD spowoduje zgłoszenie błędu. Dotyczy to przypadków, w których zawartość dokumentu nie jest poprawna, elementy nie są prawidłowo zagnieżdżone, atrybuty znajdują się nie tam, gdzie trzeba, lub są niepoprawne itd.
Błędy krytyczne
Co ciekawe, dokument niezgodny z zawężeniami DTD nigdy nie spowoduje wygenerowania błędu krytycznego. W czasie przetwarzania dokumentu nie mogą zajść takie warunki, które spowodują przerwanie tego procesu. Może nam się wydawać, że kontynuacja przetwarzania niepoprawnego dokumentu kłóci się z celowością sprawdzania poprawności. Należy jednak pamiętać o tym, że w większości przypadków dokumenty XML są generowane przez aplikacje. Innymi słowy, aplikacja otrzymuje dane wejściowe XML z innego programu lub podprogramu. Jeśli dane te są niepoprawne, aplikacja usiłująca z nich skorzystać powinna zgłosić aplikacji klienta błąd, a nie przerywać przetwarzanie. Co więcej, przetwarzanie to niejednokrotnie powinno być kontynuowane, aby możliwe było zakończenie procesu w sposób elegancki — w ten sposób aplikacja jest w stanie precyzyjnie zakomunikować, jakie błędy wystąpiły. Dokumenty błędnie sformatowane spowodują zatrzymanie przetwarzania, a dokumenty niepoprawne wskażą, że albo nastąpił błąd, który można poprawić, albo zaszła sytuacja, o której klient powinien wiedzieć, np. wprowadzono niepoprawne dane. Zastanówmy się, jak trudno byłoby obsługiwać edytor XML lub środowisko programistyczne IDE, jeśli za każdym razem, gdy nie do końca przestrzegaliśmy zawężeń DTD, edytor kończyłby działanie z błędem krytycznym, lub też odmawiałby w ogóle przetwarzania dokumentu; a przecież niektóre edytory mogłyby takie błędy poprawiać za nas! Dlatego właśnie niepoprawne dokumenty powodują zgłaszanie ostrzeżeń i błędów, ale nigdy błędów krytycznych.
Jedyny błąd krytyczny, na jaki możemy się natknąć przy używaniu definicji DTD w dokumentach, których poprawność nie jest sprawdzana, to błąd składniowy w wykorzystywanej definicji DTD. To nie powinno Czytelnika dziwić, jako że błędy składniowe w dokumencie XML także powodują błędy krytyczne. Ma tutaj miejsce to samo rozumowanie — nie jest możliwe dalsze przetwarzanie lub sprawdzanie poprawności, jeśli nie można określić zawężeń (a tak jest, gdy składnia DTD nie jest poprawna). Powinniśmy zdawać sobie sprawę, że to nie to samo co błąd generowany, gdy mamy do czynienia z niepoprawnie sformatowanym dokumentem XML — główna różnica polega na
tym, że definicja DTD nigdy nie jest postrzegana jako „poprawnie sformatowana”, bo przecież nie jest to tak naprawdę dokument XML. Ale rezultat wystąpienia błędów składniowych w DTD jest taki sam, jak rezultat przetwarzania niepoprawnie sformatowanych danych XML.
Interfejs DTDHandler
Ostatnia ważna procedura obsługi udostępniania przez SAX rejestruje metody wywołań wstecznych w czasie procesu czytania i przetwarzania definicji DTD dokumentu. W interfejsie tym nie są zdefiniowane zdarzenia, jakie występują w czasie sprawdzania poprawności, a jedynie te, które występują w czasie czytania DTD. W podrozdziale dotyczącym pułapek zostaną omówione problemy wywoływane przez to zróżnicowanie. Ta procedura obsługi zachowuje się w taki sam sposób jak interfejsy ContentHandler i ErrorHandler, omawiane w rozdziale 3. — definiowane są dwie metody wywoływane w czasie przetwarzania.
Sprawdzanie poprawności dokumentu XML jest bardzo ważne, ale zdarzenia związane z odczytywaniem dokumentu DTD — już mniej. Są tylko dwie metody, obie nie są wykorzystywane tak często, a więc prawdopodobnie rzadko Czytelnik będzie musiał korzystać z interfejsu DTDHandler (chyba że pisząc edytor lub IDE dla XML-a będzie chciał tworzyć lub przetwarzać dokumenty DTD w celu sprawdzenia poprawności zapisu i składni). Dlatego pokrótce omówimy tutaj te dwie metody udostępniane przez SAX, ale nie będziemy poświęcali im zbyt dużo czasu. O opcjonalnej procedurze obsługi SAX, służącej do pobierania innych informacji z DTD, można przeczytać w części DeclHandler, w dodatku A (pakiet org.xml.sax.ext).
Nieprzetwarzane deklaracje encji
Pierwsze wywołanie wsteczne, unparsedEntityDecl(), wywoływane jest, kiedy deklaracja encji w DTD mówi, iż encja ta ma nie być przetwarzana przez parser XML. Choć nie omawialiśmy jeszcze przykładu takiego zachowania, nieprzetwarzane encje są dość typowe w dokumentach XML, w których istnieją odwołania do plików graficznych lub innych danych binarnych (często plików multimedialnych). Metoda ta pobiera nazwę encji, identyfikatory, publiczny i systemowy, oraz nazwę notacyjną encji. Nazwy notacyjne w XML-u nie były jeszcze omawiane. Spójrzmy na przykład dokumentu XML odwołującego się do pliku graficznego (np. do logo firmy) — przykład 5.9.
Przykład 5.9. Dokument XML z nieprzetwarzaną encją
<dokument>
<mojeLogo>&LogoFirmy;</mojeLogo>
</dokument>
Podczas przetwarzania parser stara się przetłumaczyć wszystkie encje i wstawić w ich miejsce przetworzoną wartość. Jednakże parser nie potrafi przetwarzać plików graficznych i powinien pozostawić dane binarne w postaci nieprzetworzonej. Można go o tym „poinformować” poprzez następującą definicję typu dokumentu:
<!ENTITY LogoFirmy SYSTEM "obrazki/logo.gif" NDATA gif>
Słowo kluczowe NDATA powoduje, że parser XML nie przetworzy tej encji. Gdyby ta definicja DTD była przetwarzana za pośrednictwem zarejestrowanej implementacji DTDHandler, informacja w deklaracji encji zostałaby przekazana do wywołania. Inną ważną sprawą jest tutaj fakt, że wywołanie następuje w miejscu deklaracji w DTD, a nie w chwili, gdy deklaracja ta jest prze-
twarzana w dokumencie XML. Oznacza to, że nawet jeśli encja nie znajduje się w dokumencie XML, to i tak wywołanie nastąpi. To ma sens — przecież wywołanie to stanowi część interfejsu DTDHandler, a nie ContentHandler.
Deklaracje notacji
Deklaracje notacji zawsze skojarzone są z nieprzetwarzanymi deklaracjami i stanowią przedmiot działania drugiej metody wchodzącej w skład DTDHandler. Ostatnia część powyższej deklaracji nieprzetwarzanej encji to słowo „gif”. Słowo to określa typ nieprzetwarzanej encji i musi stanowić odwołanie do typu zdefiniowanego w innym miejscu DTD, za pośrednictwem konstrukcji NOTATION. W ten sposób parser poznaje identyfikator URI dla danego typu — często jest to odwołanie publiczne dla typów binarnych. Przypomina to odwoływanie się do DTD z dokumentu XML, ponieważ specyficzne dane (w tym przypadku plik GIF) są kojarzone z publicznym identyfikatorem lub URI. Przetwarzanie definicji NOTATION i deklaracji nieprzetwarzanej encji powoduje w rezultacie, że parser XML nie przetwarza danych binarnych. A o to właśnie chodziło.
<!NOTATION gif SYSTEM "http://www.gif.com">
Wystąpienia takich deklaracji zgłaszane są zarejestrowanej procedurze obsługi poprzez wywołanie notationDecl(). Po wywołaniu metoda ta otrzymuje deklarację notacji, identyfikator systemowy i dostępny identyfikator publiczny. Podobnie jak w przypadku nieprzetwarzanych encji, metoda ta wywoływana jest podczas odczytywania definicji DTD, a nie samego dokumentu XML.
Rejestrowanie procedury obsługi
Rejestrowanie implementacji DTDHandler w naszym parserze XML nie różni się niczym od rejestrowania procedur obsługi błędów i zawartości. Egzemplarz klasy przekazywany jest metodzie setDTDHandler(), a parser rejestruje odpowiednie zdarzenia związane z metodami SAX w tej klasie:
import java.io.IOException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
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;
...
// Stwórz egzemplarze procedur obsługi
ContentHandler contentHandler = new MyContentHandler();
ErrorHandler errorHandler = new MyErrorHandler();
DTDHandler dtdHandler = new MyDTDHandler();
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);
// Zarejestruj procedurę obsługi DTD
parser.setDTDHandler(dtdHandler);
// Włącz sprawdzanie poprawności
parser.setFeature("http://xml.org/sax/features/validation",
true);
// Wyłącz "świadomość" przestrzeni nazw
parser.setFeature("http://xml.org/sax/features/namespaces",
false);
// 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());
}
I cała reszta...
Czytelnik może odnieść wrażenie, że interfejs procedury obsługi DTD został omówiony dość pobieżnie, szczególnie w porównaniu z innymi procedurami. Jednakże aplikacje wykorzystujące XML rzadko muszą rejestrować tego typu procedury. W systemach XML-owych często sprawdza się poprawność dokumentu, ale szczegółowe informacje odnośnie nieprzetworzonych encji rzadko przydają się na wyższym poziomie wykorzystania XML-a. Dlatego właśnie przejdziemy do innych szczegółowych informacji, bardziej przydatnych przy tworzeniu aplikacji korzystających z XML-a.
Czytelnik mógłby oczekiwać, że teraz zostanie omówione sprawdzanie poprawności za pomocą XML Schema; schematy są jednakże w coraz większym stopniu wykorzystywane nie tylko do sprawdzania poprawności, ale także do zwykłej reprezentacji danych. Dlatego właśnie omawianie schematów z poziomu Javy zostało przeniesione do rozdziału 14. — wtedy Czytelnik będzie już znał XSL i różnego rodzaju praktyczne sposoby użycia XML-a w aplikacjach. Interfejsy SAX do obsługi schematów są o wiele potężniejsze niż DTDHandler i będą wymagały od Czytelnika większej wiedzy z zakresu XML-a. W niniejszym rozdziale omawianie sprawdzania poprawności danych XML zostanie zakończone przedstawieniem typowych problemów, na jakie można się natknąć w czasie tego procesu.
Uwaga! Pułapka!
Podczas sprawdzania poprawności dokumentu XML na programistę czyha wiele pułapek. Poniżej zostaną omówione problemy, z którymi najczęściej mają do czynienia początkujący programiści XML, a także te, których rozwiązanie wcale nie jest łatwe. Warto dobrze się zapoznać z poniższym materiałem, bo opisywane trudności spędziły sen z oczu już niejednemu programiście.
Obsługa sprawdzania poprawności a obsługa definicji DTD
Jednym z typowych nieporozumień odnośnie SAX-a i sprawdzania poprawności jest błędne założenie, że sprawdzanie poprawności dokumentu XML następowało automatycznie po zarejestrowaniu implementacji DTDHandler w parserze XML. Często zdarza się, że programista implementuje ten interfejs i rejestruje go w parserze, ale nie włącza funkcji sprawdzania poprawności parsera. Ten błąd wynika z zagubienia różnicy pomiędzy obsługą samego DTD a wykorzystaniem DTD do sprawdzania poprawności. W tym przypadku definicja DTD zostanie przetworzona, wystąpią też wszystkie wywołania związane z DTD (o ile są konieczne), jednakże nie nastąpi sprawdzenie poprawności dokumentu XML — zostanie on wyłącznie przetworzony. Należy pamiętać, że wynik sprawdzania poprawności poprawnego dokumentu XML wygląda niemal tak samo jak wynik przetwarzania dokumentu bez sprawdzania jego poprawności; zawsze należy śledzić, kiedy występuje sprawdzanie poprawności — w ten sposób można uniknąć błędów w aplikacji:
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);
// Z tego nie wynika włączenie sprawdzania poprawności
parser.setDTDHandler(dtdHandler);
// Włącz sprawdzanie poprawności
parser.setFeature("http://xml.org/sax/features/validation",
true);
// Wyłącz "świadomość" przestrzeni nazw
parser.setFeature("http://xml.org/sax/features/namespaces",
false);
// 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());
}
Trzeba koniecznie pamiętać o tym, że zarejestrowanie DTD nie ma nic wspólnego z procesem sprawdzania poprawności; parser z zarejestrowaną implementacją DTDHandler nie zawsze sprawdza poprawność XML-a; zaś parser bez zarejestrowanej tej procedury może sprawdzić poprawność. To od cech (w implementacjach SAX 2.0) lub klasy (w implementacjach sprzed SAX 2.0) parsera XML zależy, czy sprawdzanie poprawności będzie się odbywało — a nie od zarejestrowanej procedury obsługi. Pamiętajmy o tym — implementacja DTDHandler do sprawdzania poprawności nie jest potrzebna; potrzebne jest natomiast ustawienie odpowiednich cech parsera lub użycie innej klasy.
W czasie tworzenia priorytetem jest poprawność;
w gotowej aplikacji liczy się szybkość
Programista powinien wiedzieć, kiedy należy korzystać ze sprawdzania poprawności, a kiedy nie. Jednym z największych problemów związanych ze sprawdzaniem poprawności jest często niezwykle powolne działanie aplikacji produkcyjnej. Programista analizuje przyczyny takiego spadku wydajności pracy aplikacji, nie pamiętając o tym, że w środowisku produkcyjnym pozostawił włączone sprawdzanie poprawności. Zazwyczaj sprawdzanie poprawności powinno mieć miejsce tylko w czasie testowania lub w czasie działania procesu kontrolowania jakości (ang. Quality Assurance). Jeśli część aplikacji generuje lub modyfikuje pliki XML, sprawdzanie poprawności wynikowego dokumentu XML powinno odbywać się właśnie w czasie testowania. To proces kosztowny — parser przetwarza o wiele więcej danych i musi podjąć więcej decyzji dotyczących dokumentu.
Po sprawdzeniu poprawności wyników generowanych przez aplikację zazwyczaj dobrze jest wyłączyć sprawdzanie poprawności. Aplikacja produkcyjna zyskuje dzięki temu na wydajności, a jeśli testowanie przeprowadzono zgodnie z zasadami tej sztuki, to nie powinny pojawić się żadne problemy. Jedyną sytuacją, w której nie należy wyłączać sprawdzania poprawności w aplikacji produkcyjnej, jest ta, w której w tworzeniu danych XML bierze aktywny udział klient — np. w narzędziach XML IDE lub GUI, albo ta, w której dane XML uzyskuje się od innych aplikacji — np. w e-biznesie. Wynika to stąd, że dane wejściowe są poza naszą kontrolą i mogą być niepoprawne — sprawdzanie poprawności jest więc zalecane, a nawet oczekiwane przez klienta. Jednak w większości przypadków gotowa aplikacja może pracować bez sprawdzania poprawności.
Co dalej?
Czytelnik powinien już dobrze znać zasady tworzenia dokumentów XML oraz sposoby ich zawężania. W niniejszym rozdziale zostały omówione wszystkie najważniejsze aspekty korzystania z interfejsów i klas SAX. Czytelnik poznał już cykle przetwarzania i sprawdzania poprawności oraz dostępne wywołania. Powinien potrafić skonfigurować i użyć parsera XML oraz zarejestrować w nim różne procedury obsługi SAX. W następnym rozdziale zostanie przedstawiona kolejna specyfikacja — XSL, czyli rozszerzalny język arkuszy stylów. Omówienie przekształcania za pomocą XSL to wstęp do dyskusji o obiektowym modelu dokumentu i mechanizmach publikacyjnych, a także do bardziej dogłębnego poznawania zagadnień programowania aplikacji.
Domyślne włączanie obsługi przestrzeni nazw miało miejsce w czasie pisania tej książki, jednakże niektórzy programiści związani z XML-em (np. zespół programistów Apache Xerces) postulowali wyłączenie tej funkcji. Jeśli Czytelnik nie otrzymuje wspomnianego wyżej komunikatu, to być może domyślne ustawienie zostało już zmienione. (W czasie tłumaczenia książki, w grudniu 2000 r., obsługa przestrzeni nazw była już domyślnie wyłączona — przyp. tłum.).
130 Rozdział 5. Sprawdzanie poprawności składni XML-a
Uwaga! Pułapka! 129
C:\Helion\Java i XML\jAVA I xml\05-08.doc — strona 130
C:\Helion\Java i XML\jAVA I xml\05-08.doc — strona 129
C:\Helion\Java i XML\jAVA I xml\05-08.doc — strona 117