6744


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 po­zna­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 wew­nątrz aplikacji i przekazywania parserowi danych. Potem omówimy wywołania wstecz­ne do­stęp­­ne w czasie przetwarzania. Są to miejsca, w których możliwe jest wstawienie kodu spe­cy­ficz­ne­go 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­nie­je 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.

Zaopatrujemy się w parser

Najpierw należy zaopatrzyć się w odpowiedni parser (rodzaje parserów zostały pokrótce przed­sta­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ę­te­go za­sa­dą 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ę adre­sową 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, przy­­kłady z książki będą poprawnie obsłużone przez dowolny parser wykorzystujący opisywaną tu­taj implementację SAX.

Po dokonaniu wyboru i pobraniu parsera XML należy upewnić się, że nasze środowisko pro­gra­mi­sty­czne — 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 pro­du­cen­tem 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.


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­ku­mentu, 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 za­wę­ża­ją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 po­sia­da­jąc definicji DTD opisującej sposób przetworzenia tej encji, otrzymalibyśmy błędy. W następ­nym 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­twa­rzania, dzięki czemu zdarzenia zachodzące w czasie tego procesu będą wyświetlane i umoż­li­wią lepszą obserwację całego mechanizmu.

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 zaim­ple­men­to­wa­no 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 prze­two­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 zmien­nej egzemplarza o nazwie reader czy XMLReader (reader to po angielsku „czytnik”). Rze­czy­wiście, wydawałoby się, że tak powinny nazywać się te komponenty, ale w klasach SAX 1.0 zde­fi­nio­wa­no 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ś­ci­woś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 {

/**

* <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 przy­go­to­wał się w opisany wcześniej sposób i klasy SAX znajdują się w ścieżce dostępu do klas. Ten pro­sty pro­gram 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 „Prze­twa­rzanie pliku XML”. Wynika to stąd, że jedynie stworzyliśmy instancję parsera, a nie zażądaliśmy jeszcze prze­two­rzenia dokumentu XML.

0x01 graphic

Jeśli wynikły kłopoty z kompilacją powyższego pliku źródłowego, najpraw­do­po­do­b­niej problem tkwi w ścieżce dostępu do klas środowiska programistycznego lub sys­te­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 roz­pa­ko­wa­niu archiwum uzyskujemy plik xerces.jar — właśnie on zawiera skompilowane kla­sy 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­two­rze­nia 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.­In­put­Sour­ce, 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 użyjemy pełnej ścieżki dostępu do dokumentu, który przygotowaliśmy wcześniej. Gdybyśmy jednak chcieli sko­rzy­stać 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ż do­kument 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 zo­stać zgłoszony wyjątek org.xmlsax.SAXException (problem przy przetwarzaniu), a więc mu­si­my 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]");

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łęd­ny identyfikator URI pliku, parser zgłosi wyjątek i poinformuje, że nie mógł przetworzyć pli­ku. 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­we­niować 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 par­sera 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 stru­ktu­rę 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­gu­mentem w postaci org.xml.sax.InputSource. Właściwie o tej klasie niewiele można po­wie­dzieć — 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 iden­ty­fi­ka­torów sys­temowego i publicznego, kodowania znaków, strumienia bajtów (java.io.In­put­Stream) oraz strumienia znaków (java.io.Reader). Jeśli w ten sposób przekażemy argu­ment do par­se(), to SAX gwarantuje również, że parser nigdy nie zmodyfikuje Input­Sour­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 wy­ko­rzy­stywał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ż­li­wia­ją­cych wywoływanie kodu aplikacji w przypadku zajścia konkretnych zdarzeń w czasie prze­twa­rza­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 potężnym interfejsem — umożliwia obsługę dokumentu sekwencyjnie, bez konieczności wczy­ty­wa­nia 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­Han­dler, org.xml.sax.ErrorHandler, org.xml.sax.DTDHandler oraz org. xml.­sax.En­tityResolver. W tym rozdziale omówimy procedurę ContentHandler, umoż­liwiającą obsługę standardowych zdarzeń związanych z danymi dokumentu XML. Roz­po­częte zostanie również omawianie procedury ErrorHandler, za pośrednictwem której parser zgła­sza znalezione w dokumencie błędy. Procedura DTDHandler zostanie omówiona w roz­dziale 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 kla­sę 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");

// 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.

*/

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) {

}

/**

* <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>&lt;/[element name]&gt;</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.

* @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­tent­Handler 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 wyma­ga­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 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 doku­men­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 udo­stęp­nia me­to­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>.

* </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 in­ter­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 zloka­li­zo­wano zasób XML. Aplikacja może obsługiwać tylko jeden wyjątek — SAXException. Ten pierw­szy 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, że­by aplikacja przechwytywała jeden wyjątek, poprzez który dostarczane są także szczegółowe in­formacje 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ą po­strze­gane jako elementy samego XML-a i zamiast natychmiastowej obsługi, przekazywane są apli­kacji 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 {

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 da­ny 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.oreil­ly.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>

</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 identy­fi­ka­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łów­ne­go. 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 wszel­kich 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ę bez­po­ś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 ko­mu­ni­ka­tu po napotkaniu znacznika element2:

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 ele­men­ta­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 rozpo­czy­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­wie­ra oraz kiedy parser napotkał znacznik zamykający elementu. Pierwsze z wywołań, start­Ele­ment(), 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 identy­fi­ka­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 zgo­dnoś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 Java­XML: Ksiaz­ka. 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:

/**

* <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 startEle­ment() 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 zacho­wy­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 wy­woł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­Ele­ment(). 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>&lt;/[element name]&gt;</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 {

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 pseudo­re­ku­ren­cyjny — elementy zagnieżdżone w elementach powodują wywołania „zagnieżdżone” w wy­wo­ł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­ter­fejs 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ę”, obsługując elementy, atrybuty i dane w miarę ich osiągania. Mogą z tego wynikać pewne nie­spo­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 dal­sze niezrozumienie, bo przecież inne wywołanie SAX, ignorableWhitespace(), także ko­munikuje 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 zo­staną 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 po­przez 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 od­po­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 fakty­cz­nie się nie odbywa (z powodu konieczności implementacji dodatkowych struktur danych w par­serze sprawdzającym). Tak właśnie jest w parserze Apache Xerces. W naszym przykładzie wyko­rzy­sta­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 spraw­dza­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 XML. Jeśli zachodzi taka potrzeba (np. w przypadku konstrukcji typu „kilka spacji + dane + kilka spa­cji”, 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 wy­korzystywać indeksy początkowy i końcowy, udostępniane w celu czytania znaków z do­star­czo­nej 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 klam­ro­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 — OreillyCo­py­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 wy­ma­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 po­da­wa­na jest nazwa encji — my wykorzystamy tę właściwość do wyświetlenia odpowiedniej informacji 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 otrzy­ma­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­ka­zany 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 stwo­rzy­liśmy. Służy do tego metoda setContentHandler(), której jedynym argumentem jest im­ple­men­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);

} 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ć kla­sę MyContentHandler oraz zawierający ją plik SAXParserDemo. Po udanej kompilacji moż­na uruchomić program demonstrujący działanie parsera na stworzonym wcześniej przykła­do­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

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 wer­sji nie sprawdzającej poprawności dokumentu, a więc białe znaki są zgłaszane za pomocą wywołań cha­racters(). 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 roz­dzia­ł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 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ę SAXPar­se­Ex­ce­p­tion. Obiekt ten zawiera numer wiersza, w którym wystąpił błąd, identyfikator URI prze­twa­rza­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 po­win­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 od­po­wie­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;

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 me­to­dzie 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 wyge­ne­rowanie ostrzeżenia; wszystkie związane są z definicjami DTD i poprawnością składniową do­kumentu. 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 wy­stąpienia ostrzeżenia. Ponie­waż chcemy, aby w razie ostrzeżenia przetwarzanie zostało przerwane, zgłaszamy wyjątek SAXException i pozwalamy aplikacji nadrzędnej elegancko zakończyć dzia­ła­nie 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" +

" 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 po­winna 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 roz­dzia­łach. Podobnie jak w przypadku ostrzeżeń, nasza prosta procedura obsługi błędów wyświetli in­for­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 nie­po­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 poinfor­mo­wać użytkownika lub administratora o wystąpieniu takiego błędu; pozostawienie apli­kacji samej sobie może grozić całkowitym jej zawieszeniem. W naszym przykładzie będziemy emu­lowali zachowanie dwóch pozostałych metod i zatrzymamy przetwarzanie, wyświetlając ko­mu­nikat 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>

*

* @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 prze­kom­pi­lo­wa­nie 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 do­ku­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

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­tra­fią 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­ni­kat 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: 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 bez­po­ś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 imple­men­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.help­ers.XMLRead­er­Factory:

/**

* Próba utworzenia egzemplarza czytnika XML z nazwy klasy.

*

* <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 po­staci egzemplarza rzutowanego na interfejs XMLReader (zakładając, że faktycznie imple­men­to­wa­ny 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 prze­cho­wy­wać nazwę klasy parsera w pliku właściwości. Umożliwia to ładowanie klasy „na gorąco”, a kod 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ę kla­sy 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 sy­ste­mo­wych właściwości Javy, ale nie są one odpowiednie dla rozproszonych aplikacji WWW przed­sta­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łu­gi 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 (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­pla­rza 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­po­czął 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­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­pla­rza Locator poprzez wywołanie metody setDocumentLocator(). W ten sposób apli­ka­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­tent­Handler. Często korzystanie z takiej zmiennej powoduje nie tylko otrzymywanie przez apli­kację 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 pro­wa­dzić do pomyłek — typowym błędem jest odczytywanie znaków z tablicy w następujący sposób:

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 wy­prze­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ę) przy­pad­ko­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 resz­ty zagadnień opisywanych w tej książce — w następnych rozdziałach zostaną poszerzone wia­do­moś­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­dza­nia poprawności dokumentu, a także przedstawione zostaną definicje DTD i schematy.

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.

W tym oraz innych przykładach danych zwracanych przez programy możliwe jest, że zostały dodatkowo przełamane wier­sze 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!

88 Rozdział 3. Przetwarzanie kodu XML

Co dalej? 89

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

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

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



Wyszukiwarka

Podobne podstrony:
6744
6744
6744
6744
6744
07 Wilgotnosc optymalna instrukcjaid 6744 (2)
6744
6744

więcej podobnych podstron