|
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 poznaliśmy jego strukturę. W tym rozdziale rozpoczniemy programowanie w Javie od przetworzenia dokumentu XML i uzyskania dostępu do przetworzonych danych z poziomu kodu w Javie.
Najpierw będziemy musieli pobrać i przetworzyć dokument. W czasie przetwarzania dokumentu staje się on dostępny dla aplikacji wykorzystującej parser. To właśnie jest aplikacja „obsługująca XML” (w angielskim używa się określenia XML-aware, czyli „wiedząca o XML-u”). To wszystko wydaje się zbyt proste, żeby było prawdziwe — ale tak właśnie jest. W tym rozdziale poznamy szczegóły dotyczące przetwarzania dokumentu XML. Omówimy sposoby korzystania z parsera wewnątrz aplikacji i przekazywania parserowi danych. Potem omówimy wywołania wsteczne dostępne w czasie przetwarzania. Są to miejsca, w których możliwe jest wstawienie kodu specyficznego 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; istnieje 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 udostę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ą warunkiem 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 przedstawione 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 stronie producenta parsera i odwiedzić wymienione wcześniej witryny.
Zgodnie z duchem społeczności wolnego oprogramowania (open source), we wszystkich przykładach tej książki wykorzystany zostanie parser Apache Xerces. Jest on dostępny w postaci źródłowej i binarnej pod adresem http://xml.apache.org. Parser ten został napisany w C i w Javie i jest używany przez liczną grupę programistów. Poza tym, jeśli korzystamy z parsera objętego zasadą wolnego oprogramowania, mamy możliwość wysłania autorom pytań lub powiadomień o błędach — to przyczynia się do podwyższania jakości produktu. Aby zapisać się na ogólną listę adresową dotyczącą parsera Xerces, wystarczy wysłać pusty e-mail pod adres xerces-dev-subscribe@xml.apache.org. Tam uzyskamy odpowiedzi na pytania dotyczące samego parsera oraz uzyskamy pomoc w przypadku wystąpienia problemów nie ujętych w tej książce. Oczywiście, przykłady z książki będą poprawnie obsłużone przez dowolny parser wykorzystujący opisywaną tutaj implementację SAX.
Po dokonaniu wyboru i pobraniu parsera XML należy upewnić się, że nasze środowisko programistyczne — 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 powinniś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.helpers i 2 klasy org.xml.sax.ext. Jeśli brakuje którejś z tych klas, trzeba skontaktować się z producentem i sprawdzić, czy zostały one zawarte w dystrybucji. Niektóre mogą być pominięte, jeśli nie są w całości obsługiwane. Tyle samo klas istnieje w interfejsie SAX 2.0; mniej klas pojawi się w tych katalogach, gdy obsługiwany jest tylko SAX 1.0.
Warto także pobrać albo zaznaczyć zakładką przeglądarki dokumenty SAX API Javadoc na stronach 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 parserze 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 dokumentu, w tym rozdziale nasze programy będą wyłącznie przetwarzały („parsowały”) dokument. Aby zapobiec błędom, trzeba usunąć z dokumentu odwołania do zewnętrznej definicji DTD zawężającej XML oraz do arkuszy XSL powodujących jego przekształcenie. Wystarczy opatrzyć komentarzami 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 posiadając definicji DTD opisującej sposób przetworzenia tej encji, otrzymalibyśmy błędy. W następnym rozdziale zostanie przedstawione zastosowanie tej encji.
</JavaXML:Spis>
<!-- To odkładamy do rozdziału o DTD
<JavaXML:Copyright>&OReillyCopyright;</JavaXML:Copyright>
-->
</JavaXML:Ksiazka>
Czytniki SAX
Po informacjach wstępnych czas przejść do programowania. Nasz pierwszy program pobierze plik jako argument wiersza poleceń i przetworzy go. Zbudujemy wywołania wsteczne do procesu przetwarzania, dzięki czemu zdarzenia zachodzące w czasie tego procesu będą wyświetlane i umożliwią 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 zaimplementowano 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 przetworzenia danych XML:
XMLReader parser =
new SAXParser();
// Tu robimy coś z parserem
parser.parse(uri);
Osoby, które po raz pierwszy stykają się z interfejsem SAX, mogą być zaskoczone brakiem zmiennej egzemplarza o nazwie reader czy XMLReader (reader to po angielsku „czytnik”). Rzeczywiście, wydawałoby się, że tak powinny nazywać się te komponenty, ale w klasach SAX 1.0 zdefiniowano 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, funkcji i właściwości, ale konwencje nazewnicze wciąż obowiązują, a parser dobrze oddaje przeznaczenie 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. Faktyczne 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 przygotował się w opisany wcześniej sposób i klasy SAX znajdują się w ścieżce dostępu do klas. Ten prosty program nie potrafi jeszcze zbyt wiele; jeśli uruchomimy go, podając wymyśloną nazwę pliku lub URI jako argument, powinien „pobrzęczeć” dyskiem i wyświetlić komunikat „Przetwarzanie pliku XML”. Wynika to stąd, że jedynie stworzyliśmy instancję parsera, a nie zażądaliśmy jeszcze przetworzenia dokumentu XML.
|
Jeśli wynikły kłopoty z kompilacją powyższego pliku źródłowego, najprawdopodobniej problem tkwi w ścieżce dostępu do klas środowiska programistycznego lub systemu. 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 rozpakowaniu archiwum uzyskujemy plik xerces.jar — właśnie on zawiera skompilowane klasy dla tego programu. Po dodaniu tego archiwum do ścieżki dostępu do klas nie powinno być problemów z kompilacją powyższego przykładu. |
Przetwarzanie dokumentu
Kiedy parser jest już załadowany i gotowy do wykorzystania, można przekazać mu do przetworzenia 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.InputSource, albo zwykły identyfikator URI. Na razie nie będziemy zajmowali się wejściem InputSource 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 skorzystać z dokumentów umieszczonych w sieci, powinniśmy pamiętać o tym, że aplikacja musi umieć znaleźć dany adres URL (tzn. musi być zapewnione połączenie z siecią).
Do programu dodajemy więc metodę parse() oraz dwie procedury obsługi błędów. Ponieważ dokument musi zostać załadowany, czy to lokalnie, czy przez sieć, może wystąpić wyjątek java.io.IOException, który musimy przechwycić. Ponadto w czasie przetwarzania może zostać zgłoszony wyjątek org.xmlsax.SAXException (problem przy przetwarzaniu), a więc musimy dodać jeszcze dwie istotne instrukcje, kilka linijek kodu i już mamy gotową do użycia aplikację przetwarzającą XML:
import java.io.IOException;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
// Tutaj importujemy implementację czytnika XML (XML Reader)
import org.apache.xerces.parsers.SAXParser;
/**
* <b><code>SAXParserDemo</code></b> pobiera plik XML i przetwarza je
* za pomocą SAX, wyświetlając wywołania wsteczne.
*
* @author
* <a href="mailto:brettmclaughlin@earthlink.net">Brett McLaughlin</a>
* @version 1.0
*/
public class SAXParserDemo {
/**
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " + e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Użycie: java SAXParserDemo [XML URI]");
System.exit(0);
}
String uri = args[0];
SAXParserDemo parserDemo = new SAXParserDemo();
parserDemo.performDemo(uri);
}
}
Tak zmodyfikowany przykład kompilujemy i wykonujemy. Pierwszym argumentem programu powinna 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 cokolwiek się stało. Jednak dokument XML jest rzeczywiście przetwarzany, a gdybyśmy podali błędny identyfikator URI pliku, parser zgłosi wyjątek i poinformuje, że nie mógł przetworzyć pliku. Nie stworzyliśmy jednak jeszcze żadnych wywołań wstecznych, powodujących że SAX informuje w czasie przetwarzania, co się dzieje w danej chwili. Bez nich dokument jest przetwarzany w sposób niewidoczny dla użytkownika i bez interwencji aplikacji. Oczywiście, my mamy zamiar interweniować w ten proces, a więc musimy stworzyć metody wywołań wstecznych. To właśnie ta „interwencja” jest najważniejszym aspektem korzystania z parsera SAX. Wywołania wsteczne parsera umożliwiają podjęcie działania w czasie trwania programu. Przetwarzanie przestaje być takie nieciekawe jak powyżej — aplikacja zaczyna reagować na dane, elementy, atrybuty i strukturę przetwarzanego dokumentu, a w tym czasie współdziała jeszcze z innymi programami i klientami.
Korzystanie z InputSource
Zamiast korzystania z pełnego identyfikatora URI, metodę parse() można także wywołać z argumentem w postaci org.xml.sax.InputSource. Właściwie o tej klasie niewiele można powiedzieć — to raczej klasa osłonowa (ang. wrapper) i pomocnicza. Klasa InputSource po prostu kapsułkuje informacje o pojedynczym obiekcie. W naszym przykładzie nie na wiele się to przyda, ale w sytuacjach, gdzie identyfikator systemowy, identyfikator publiczny lub strumień mogą 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 identyfikatorów systemowego i publicznego, kodowania znaków, strumienia bajtów (java.io.InputStream) oraz strumienia znaków (java.io.Reader). Jeśli w ten sposób przekażemy argument do parse(), to SAX gwarantuje również, że parser nigdy nie zmodyfikuje InputSource. Dzięki temu wiemy na pewno, że po wykorzystaniu parsera lub aplikacji obsługującej XML dane wejściowe pozostaną niezmienione. Wiele aplikacji opisywanych w dalszych częściach książki będzie wykorzystywało właśnie klasy InputSource, a nie specyficznego URI.
Procedury obsługi zawartości
Aby nasza aplikacja mogła zrobić cokolwiek pożytecznego z przetwarzanymi właśnie danymi XML, w parserze SAX musimy zarejestrować procedury obsługi (ang. handlers). Procedura obsługi to po prostu grupa wywołań wstecznych zdefiniowanych w ramach interfejsu SAX i umożliwiających wywoływanie kodu aplikacji w przypadku zajścia konkretnych zdarzeń w czasie przetwarzania 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 wczytywania 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.ContentHandler, org.xml.sax.ErrorHandler, org.xml.sax.DTDHandler oraz org. xml.sax.EntityResolver. W tym rozdziale omówimy procedurę ContentHandler, umożliwiającą obsługę standardowych zdarzeń związanych z danymi dokumentu XML. Rozpoczęte zostanie również omawianie procedury ErrorHandler, za pośrednictwem której parser zgłasza znalezione w dokumencie błędy. Procedura DTDHandler zostanie omówiona w rozdziale 5. Procedura EntityResolver omawiana jest w różnych miejscach książki; teraz wystarczy zrozumieć, że działa ona dokładnie tak jak pozostałe i służy do tłumaczenia encji zewnętrznych wstawionych do dokumentu XML. Każdy z tych interfejsów może zostać zaimplementowany w klasach aplikacji wykonujących specyficzne zadania. Klasy implementacyjne rejestrowane są w parserze metodami setContentHandler(), setErrorHandler(), setDTDHandler() i setEntityResolver(). Następnie parser wykonuje wywołania wsteczne tych metod w razie uruchomienia konkretnego programu obsługi.
W naszym przykładzie zaimplementujemy interfejs ContentHandler. W interfejsie tym zdefiniowano szereg istotnych metod cyklu przetwarzania, na które nasza aplikacja może reagować. Przede wszystkim musimy dodać odpowiednie instrukcje import do pliku źródłowego (w tym klasę i interfejs org.xml.sax.Locator i org.xml.sax.Attributes — oraz nową klasę implementującą te metody wywołań wstecznych. Ta nowa klasa zostanie dodana na końcu pliku źródłowego SAXParserDemo.java:
import java.io.IOException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
// Tutaj importujemy implementację czytnika XML (XML Reader).
import org.apache.xerces.parsers.SAXParser;
/**
* <b><code>SAXParserDemo</code></b> pobiera pliki XML i przetwarza je
* za pomocą SAX, wyświetlając wywołania wsteczne.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class SAXParserDemo {
/**
* <p>
* Tutaj przetwarzamy plik za pomocą zarejestrowanych procedur obsługi SAX;
* wyświetlamy zdarzenia zachodzące w cyklu przetwarzania.
* </p>
*
* @param uri <code>String</code> URI pliku do przetworzenia.
*/
public void performDemo(String uri) {
System.out.println("Przetwarzanie pliku XML: " + uri + "\n\n");
// 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></[element name]></code>). Należy zauważyć, że parser
* nie rozróżnia pomiędzy elementami pustymi i niepustymi,
* a więc to będzie się odbywało identycznie w obu przypadkach.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z jaką skojarzony jest ten element
* @param localName <code>String</code> nazwa elementu bez przedrostka
* @param rawName <code>String</code> nazwa elementu w postaci XML. 1.0
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak.
*/
public void endElement(String namespaceURI, String localName,
String rawName)
throws SAXException {
}
/**
* <p>
* Tutaj wyświetlamy dane tekstowe (zawarte wewnątrz elementu).
* </p>
*
* @param ch <code>char[]</code> tablica znaków zawartych w elemencie.
* @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 ContentHandler 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 wymaganym metodom.
Lokalizator dokumentu
Pierwsza metoda, jaką należy zdefiniować, to ta ustawiająca org.xml.sax.Locator dla dowolnego zdarzenia. W przypadku wystąpienia wywołania wstecznego klasa implementująca procedurę 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 miejsca, 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 zmiennej 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 dokumencie 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 wszystkimi innymi zdarzeniami, a drugie po. Ten oczywisty fakt ma w aplikacjach krytyczne znaczenie — dokładnie informuje, kiedy przetwarzanie rozpoczyna się i kiedy kończy. SAX udostępnia metody 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. DTDHandler. Innymi słowy, startDocument() to nie tylko pierwsza metoda wywoływana wewną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 zatrzymany 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 ErrorHandler, po czym przetwarzanie kończy się wywołaniem metody endDocument().
W naszym przykładzie, kiedy obie te metody zostaną wywołane, wyświetlimy odpowiednie komunikaty 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 interfejs do procesu przetwarzania. Jednakże wyjątki te często pośredniczą w obsłudze innych wyjątkó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 zlokalizowano zasób XML. Aplikacja może obsługiwać tylko jeden wyjątek — SAXException. Ten pierwszy wyjątek jest przechwytywany przez parser SAX i ponownie zgłaszany, tym razem jako SAXException; pierwotny wyjątek jest „kapsułkowany” w nowym. Dzięki temu wystarczy, żeby aplikacja przechwytywała jeden wyjątek, poprzez który dostarczane są także szczegółowe informacje o naturze problemu, jaki wystąpił w procesie przetwarzania. Klasa SAXException udostępnia metodę getException(), zwracającą właściwy wyjątek.
Instrukcje przetwarzania
Instrukcje przetwarzania (PI) w dokumencie XML są elementami dość wyjątkowymi. Nie są postrzegane jako elementy samego XML-a i zamiast natychmiastowej obsługi, przekazywane są aplikacji wywołującej. Z tego względu do ich obsługi SAX udostępnia specjalne wywołanie wsteczne. Metoda 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 razie wystąpienia błędu.
Podczas omawiania instrukcji przetwarzania wspomnieliśmy także o deklaracji XML. Ta specjalna instrukcja przetwarzania udostępnia wersję, opcjonalne informacje o kodowaniu oraz o tym, czy dany dokument jest dokumentem samodzielnym:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
Instrukcja ta przeznaczona jest specjalnie dla parsera XML. Umożliwia zgłoszenie błędu (np. o nieobsł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 nigdy 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 skomplikowania 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 (opisaną przedrostkiem i odpowiednim identyfikatorem URI) a lokalną nazwą elementu. W tym przypadku, 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 JavaXML, a identyfikator URI przestrzeni nazw (w naszym przykładzie) to http://www.oreilly.com/ catalog/javaxml.
Istnieją dwa wywołania wsteczne do obsługi przestrzeni nazw (choć wywołania związane z elementami mogą także z nich korzystać). Wywołania te następują wtedy, gdy parser natrafi na począ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 element, 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 przeszkodzie, aby tę rolę spełniał dowolny inny element dokumentu XML, jawnie deklarujący przestrzeń 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 zagnieżdżenia elementów.
Wywołanie startPrefixMapping() otrzymuje przedrostek przestrzeni nazw oraz identyfikator URI skojarzony z tym przedrostkiem. Odwzorowanie uznawane jest za „zamknięte” lub „zakoń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 zazwyczaj miejsce w interfejsie SAX; odwzorowanie przedrostków odbywa się bezpośrednio przed wywołaniem związanym z elementem deklarującym przestrzeń nazw. Oto przykład wywołania:
/**
* <p>
* To oznacza początek odwzorowania przedrostka przestrzeni nazw XML.
* Zazwyczaj powinno się pojawić wewnątrz elementu głównego dokumentu XML,
* ale nie jest to reguła (może pojawić się w dowolnym miejscu).
* Odwzorowanie przedrostka danego elementu uruchamia wywołanie wsteczne
* <i>przed</i> odwołaniem odnoszącym się do samego elementu
* (<code>{@link #startElement}</code>).
* </p>
*
* @param prefix <code>String</code> przedrostek dla odnalezionej
* przestrzeni nazw
* @param uri <code>String</code> URI dla odnalezionej
* przestrzeni nazw
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void startPrefixMapping(String prefix, String uri) {
System.out.println("Początek odwzorowania dla przedrostka " + prefix +
" odwzorowanego dla URI " + uri);
}
W naszym dokumencie jedynym zadeklarowanym odwzorowaniem jest atrybut elementu głównego. Oznacza to, że powinniśmy oczekiwać tego wywołania przed wywołaniem związanym z pierwszym elementem (o którym za chwilę), ale już po wywołaniu startDocument() oraz po wszelkich instrukcjach PI, jakie znajdują się na początku dokumentu. Drugie z tej pary wywołań związanych z przestrzeniami nazw oznacza napotkanie końca odwzorowania i pojawia się bezpośrednio po znaczniku zamykającym elementu, w którym zadeklarowano odwzorowanie:
/**
* <p>
* To oznacza koniec odwzorowania przedrostka, kiedy nie jest już dostępna
* przestrzeń nazw określona w odwołaniu
* <code>{@link #startPrefixMapping}</code>.
* </p>
*
* @param prefix <code>String</code> znalezionej przestrzeni nazw
* @throws <code>SAXException</code> jeśli coś pójdzie nie tak
*/
public void endPrefixMapping(String prefix) {
System.out.println("Koniec odwzorowania dla przedrostka " + prefix);
}
W przypadku pokazanego powyżej fragmentu dokumentu XML można oczekiwać następującego komunikatu po napotkaniu znacznika element2:
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 elementami, 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 rozpoczyna 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 temat 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 zawiera oraz kiedy parser napotkał znacznik zamykający elementu. Pierwsze z wywołań, startElement(), 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.Attributes (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 przedrostkiem 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 identyfikator 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 podawana jest lokalna nazwa elementu, czyli fragment bez przedrostka. Oprócz tego (na potrzeby zgodności wstecz), podawana jest „surowa” nazwa elementu. Chodzi tu o niezmodyfikowaną nazwę elementu, zawierającą ewentualny przedrostek przestrzeni nazw — czyli to, co umieszczone było w dokumencie XML; dla naszego elementu Ksiazka byłaby to nazwa JavaXML: Ksiazka. Obecność tych trzech typów nazw umożliwia opisanie dowolnego elementu, z przestrzenią 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, informują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 startElement() trzeba jeszcze zwrócić uwagę na fakt, że nie zachowuje się kolejności atrybutów. Podczas iteracji po implementacji klasy Attributes atrybuty niekoniecznie będą pojawiały się w kolejności, w jakiej zostały przetworzone (czyli w takiej, w jakiej wpisano je do dokumentu). Nie należy więc polegać na uzyskanej kolejności atrybutów — i język XML nie wymaga od parserów zachowywania tej kolejności. W niektórych parserach kolejność jest zachowywana, ale nie należy to do udokumentowanych funkcji takich parserów.
Zamykająca część wywołania związanego z elementem to metoda endElement(). To proste wywołanie właściwie tłumaczy się samo — przekazywana mu jest tylko nazwa elementu, co umożliwia dopasowanie do odpowiedniego elementu przekazanego wcześniej do wywołania startElement(). Głównym przeznaczeniem tego wywołania jest zaznaczenie zamknięcia elementu — dzięki temu aplikacja wie, czy dalsze znaki są częścią innego zakresu, czy zamykanego właśnie elementu. W naszym przykładzie, podczas zamykania elementu wydrukujemy po prostu jego nazwę:
/**
* <p>
* Oznacza, że osiągnięto koniec elementu
* (<code></[element name]></code>). Należy zauważyć, że parser
* nie rozróżnia pomiędzy elementami pustymi i niepustymi,
* a więc to będzie się odbywało identycznie w obu przypadkach.
* </p>
*
* @param namespaceURI <code>String</code> URI przestrzeni nazw, z jaką skojarzony
* 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 kolejną istotną informacją do pobrania są same dane zawarte w elemencie. Dane składają się zazwyczaj 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 pseudorekurencyjny — elementy zagnieżdżone w elementach powodują wywołania „zagnieżdżone” w wywołaniach. 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 characters(). 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ż interfejs 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 jednym 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 algorytmów mających na celu przyspieszenie przetwarzania. Nigdy nie należy liczyć na uzyskanie wszystkich 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. Innymi 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 niespodzianki. 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 elementó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, prawda? Ale dzieje się inaczej — dla każdego zdarzenia SAX wywoływana jest metoda, co powoduje następujący wynik:
startElement: macierzysty nie posiada skojarzonej przestrzeni nazw.
znaki: To jest.
startElement: potomny nie posiada skojarzonej przestrzeni nazw.
znaki: zagnieżdżony tekst.
endElement: potomny.
znaki: dalszy tekst.
endElement: macierzysty.
SAX nie wybiega z przetwarzaniem naprzód, a wiec wynik jest dokładnie taki, jaki uzyskalibyśmy czytając dokument XML sekwencyjnie, bez „ludzkich” przyzwyczajeń. Warto o tym pamiętać.
I jeszcze jedno — metoda characters() często informuje o białych znakach. To powoduje dalsze niezrozumienie, bo przecież inne wywołanie SAX, ignorableWhitespace(), także komunikuje o białych znakach. W naszym przykładzie nie sprawdzamy poprawności składni dokumentu XML; ale możemy przecież korzystać z parsera sprawdzającego poprawność składni. To dość ważny szczegół, bo sposób komunikowania o białych znakach zależy od tego, czy parser ma funkcję sprawdzania poprawności składni, czy nie. Parsery sprawdzające poprawność składni będą komunikowały o wszystkich białych znakach poprzez metodę ignorableWhitespace() — jest to spowodowane pewnymi czynnikami związanymi ze sprawdzaniem poprawności składni, które zostaną omówione w następnych dwóch rozdziałach. Parsery nie sprawdzające poprawności składni komunikują o białych znakach albo poprzez metodę ignorableWhitespace(), albo poprzez metodę characters(). Konieczne jest więc sprawdzenie, czy mamy do czynienia z parserem sprawdzającym poprawność składni.
Należy wiedzieć, że na wiele parserów faktycznie składają się dwie implementacje parsera — jedna sprawdzająca, a druga nie sprawdzająca poprawności składni. W czasie działania parsera odpowiednia 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 faktycznie się nie odbywa (z powodu konieczności implementacji dodatkowych struktur danych w parserze sprawdzającym). Tak właśnie jest w parserze Apache Xerces. W naszym przykładzie wykorzystany 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 sprawdzają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 spacji”, gdzie liczba spacji ma znaczenie w danej aplikacji), to należy korzystać z konstrukcji CDATA. To zapewni, że dane, w których liczy się liczba spacji, nie będą w ogóle przetwarzane
— obsłużone zostaną przez aplikację nadrzędną jako duży „wycinek” danych tekstowych. W pozostałych przypadkach należy unikać stosowania spacji w reprezentowaniu danych i czynienia jakichkolwiek założeń odnośnie sposobu zwracania spacji przez wywołania.
Spokojnie, to tylko białe znaki
Zagadnienia związane z białymi znakami zostały już omówione. Teraz wystarczy dodać to ostatnie wywołanie SAX do naszej klasy MyContentHandler. Metoda ignorableWhitespace() pobiera parametry w dokładnie takim samym formacie jak metoda characters() i powinna wykorzystywać indeksy początkowy i końcowy, udostępniane w celu czytania znaków z dostarczonej 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 klamrowe. Białe znaki wyświetlane są w taki sam sposób jak zwykłe dane tekstowe; mogą zostać obsł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 — OreillyCopyright. 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 wymagamy 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 podawana 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 przypadku 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 otrzymany wynik dołącza do danych otrzymywanych po przetworzeniu. Innymi słowy, wywołanie to czeka 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 przekazany parametr nie zawiera otwierającego znaku „ampersand” ani końcowego średnika. W przypadku &OReillyCopyright; do metody skippedEntity() przekazywany jest tylko łańcuch OReillyCopyright.
Wyniki
Teraz należy zarejestrować procedurę obsługi w klasie XMLReader, której egzemplarz stworzyliśmy. Służy do tego metoda setContentHandler(), której jedynym argumentem jest implementacja ContentHandler. Do metody demo() w naszym przykładowym programie dodajemy nastę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ć klasę MyContentHandler oraz zawierający ją plik SAXParserDemo. Po udanej kompilacji można uruchomić program demonstrujący działanie parsera na stworzonym wcześniej przykładowym 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 Windows, 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 elementów. Wyraźnie widać, że parser sekwencyjnie przetwarza każdy element, atrybuty elementu, dane w nim zawarte, elementy zagnieżdżone oraz znacznik końcowy elementu. Proces powtarza się dla każdego elementu. W naszym przykładzie został zastosowany parser Apache Xerces w wersji nie sprawdzającej poprawności dokumentu, a więc białe znaki są zgłaszane za pomocą wywołań characters(). W następnych dwóch rozdziałach zostaną omówione zagadnienia związane ze sprawdzaniem poprawności i wówczas będzie można porównać wynik działania.
Teraz wiemy już, w jaki sposób parser zgodny z interfejsem SAX przetwarza dokument XML. Powinniśmy również rozumieć wywołania pojawiające się w czasie procesu przetwarzania oraz to, w jaki sposób można z nich uzyskać informacje potrzebne aplikacji. W następnych dwóch rozdziałach zostaną omówione zagadnienia związane ze sprawdzaniem poprawności dokumentu XML za pomocą dodatkowych klas SAX służących do obsługi definicji DTD. Teraz sprawdzimy, jakie błędy mogą się pojawić, gdy dokument nie będzie poprawny, co dzieje się, gdy dokument XML nie jest poprawny i jakie błędy mogą z tego wynikać.
Procedury obsługi błędów
Oprócz interfejsu ContentHandler, SAX udostępnia również interfejs ErrorHandler, służący do obsługi sytuacji awaryjnych zaistniałych w czasie przetwarzania. Klasa ta działa podobnie jak omówiona wcześniej procedura obsługi dokumentu, ale zdefiniowano w niej jedynie trzy 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ę SAXParseException. Obiekt ten zawiera numer wiersza, w którym wystąpił błąd, identyfikator URI przetwarzanego 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 — program 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 powinno 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 odpowiedniej 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 metodzie demo(). Metoda setErrorHandler() pobiera jako jedyny parametr interfejs ErrorHandler 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 wywoływana jest w zarejestrowanej procedurze obsługi błędów. Są różne sytuacje powodujące wygenerowanie ostrzeżenia; wszystkie związane są z definicjami DTD i poprawnością składniową dokumentu. Zostaną one omówione w następnych dwóch rozdziałach. Tymczasem zdefiniujemy prostą metodę wyświetlającą numer wiersza, identyfikator URI i komunikat w przypadku wystąpienia ostrzeżenia. Ponieważ chcemy, aby w razie ostrzeżenia przetwarzanie zostało przerwane, zgłaszamy wyjątek SAXException i pozwalamy aplikacji nadrzędnej elegancko zakończyć działanie i zwolnić zasoby:
/**
* <p>
* Powiadomienie o ostrzeżeniu; żadne zasady języka XML nie zostały złamane,
* ale wydaje się, że czegoś brakuje lub coś jest wpisane niepoprawnie.
* </p>
*
* @param exception <code>SAXParseException</code> -- wyjątek, jaki nastąpił.
* @throws <code>SAXException</code> gdy coś pójdzie nie tak.
*/
public void warning(SAXParseException exception)
throws SAXException {
System.out.println("**Przetwarzanie ostrzeżenia**\n" +
" Wiersz: " +
exception.getLineNumber() + "\n" +
" URI: " +
exception.getSystemId() + "\n" +
" Komunikat: " +
exception.getMessage());
throw new SAXException("Napotkano ostrzeżenie");
}
Błędy niekrytyczne
Błędy występujące w czasie przetwarzania, które można naprawić, ale które stanowią pogwałcenie fragmentu specyfikacji XML, postrzegane są jako błędy niekrytyczne. Procedura obsługi błędów powinna je przynajmniej odnotować w pliku dziennika, bo zazwyczaj są na tyle poważne, że zasługują na zwrócenie uwagi użytkownika lub administratora aplikacji; jednak nie są aż tak istotne, by przerwać jej działanie. Tak jak w przypadku ostrzeżeń, większość błędów niekrytycznych związanych jest z poprawnością dokumentu, która zostanie omówiona w odpowiednich rozdziałach. Podobnie jak w przypadku ostrzeżeń, nasza prosta procedura obsługi błędów wyświetli informacje 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 niepoprawnego 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 poinformować użytkownika lub administratora o wystąpieniu takiego błędu; pozostawienie aplikacji samej sobie może grozić całkowitym jej zawieszeniem. W naszym przykładzie będziemy emulowali zachowanie dwóch pozostałych metod i zatrzymamy przetwarzanie, wyświetlając komunikat o błędzie na ekranie:
/**
* <p>
* Tutaj komunikujemy, że nastąpił błąd krytyczny; oznacza to, że
* złamano regułę w ten sposób, że dalsze przetwarzanie
* nie jest możliwe albo jest bezcelowe.
* </p>
*
* @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 przekompilowanie 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 dokumentach 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 jednak 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 zobaczyć, 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 powinien 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łanie 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ąż istnieją 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 potrafią naprawiać takich dokumentów, a więc błąd w składni powoduje zatrzymanie procesu przetwarzania. 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ć komunikat o błędzie, należy zdać sobie sprawę z faktu, że parser „widzi” znak ukośnika przed elementem 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 elementu JavaXML:Tytul, zgłasza niepoprawność tego znacznika jako zamknięcia otwartego elementu JavaXML:Ksiazka.
Uważna analiza omawianej procedury obsługi błędów pozwala rozumieć, na jakie problemy można natknąć się w czasie przetwarzania i jak można ich unikać. W rozdziale 5. ponownie zajmiemy się procedurami obsługi błędów i przyjrzymy się problemom zgłaszanym przez parser wykonujący sprawdzanie poprawności składni.
Lepszy sposób ładowania parsera
Choć potrafimy już przetwarzać dokumenty XML z wykorzystaniem interfejsu SAX, w naszym kodzie wciąż znajduje się pewien ewidentny błąd. Przeanalizujmy powtórnie proces tworzenia egzemplarza klasy XMLReader:
try {
// Stwórz egzemplarz parsera
XMLReader parser =
new SAXParser();
// Zarejestruj procedurę obsługi zawartości
parser.setContentHandler(contentHandler);
// Zarejestruj procedurę obsługi błędów
parser.setErrorHandler(errorHandler);
// Przetwórz dokument
parser.parse(uri);
} catch (IOException e) {
System.out.println("Błąd przy wczytywaniu URI: " + e.getMessage());
} catch (SAXException e) {
System.out.println("Błąd w przetwarzaniu: " + e.getMessage());
}
Czy powyższy fragment nie budzi żadnych wątpliwości? Wskazówką może być jeszcze inny wiersz naszego kodu:
// Tutaj importujemy implementację czytnika XML (XML Reader)
import org.apache.xerces.parsers.SAXParser;
Jawnie importujemy implementację klasy XMLReader danego producenta, a następnie bezpośrednio 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 przecież 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 implementacji. W ten sposób w kodzie źródłowym możliwe jest zastosowanie rozwiązania polegającego na zmianie prostego parametru String. Wszystko to jest możliwe w interfejsie SAX 2.0, a klasa, która udostępni potrzebną nam metodę, nosi nazwę org.xml.sax.helpers.XMLReaderFactory:
/**
* 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 postaci egzemplarza rzutowanego na interfejs XMLReader (zakładając, że faktycznie implementowany jest XMLReader). Ewentualne problemy są zgłaszane do programu wywołującego za pośrednictwem 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 przechowywać 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ę klasy parsera, którą należy wykorzystać dla danej platformy. W naszych przykładach byłby to łańcuch org.apache.xerces.SAXParser. Oczywiście, można byłoby także skorzystać z systemowych właściwości Javy, ale nie są one odpowiednie dla rozproszonych aplikacji WWW przedstawionych 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 unikać 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 zgodności z najnowszą wersją SAX należy do obowiązku producenta parsera. Jeśli producent nie nadąż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 obsługi zastanego kodu lub aplikacji). Jednak nawet w takich sytuacjach nie wszystko jest stracone.
SAX 2.0 wyposażony jest w klasę pomocniczą org.xml.sax.helpers.ParserAdapter, która powoduje, że implementacja klasy Parser w SAX 1.0 zachowuje się jak implementacja XMLReader w interfejsie SAX 2.0. To poręczne narzędzie pobiera implementację Parser z wersji 1.0 jako parametr wejściowy i dalej może być używane zamiast tej implementacji. Umożliwia ustawienie ContentHandler i poprawnie obsługuje wszystkie wywołania związane z przestrzenią 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 przykł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 egzemplarza 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ż rozpoczął 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 zostanie raczej niemiły SAXException i całe przetwarzanie zostanie zatrzymane. Jaki z tego wynika 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 egzemplarza Locator poprzez wywołanie metody setDocumentLocator(). W ten sposób aplikacja 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łnianym 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ą ContentHandler. Często korzystanie z takiej zmiennej powoduje nie tylko otrzymywanie przez aplikację nieprawidłowych informacji, ale również uszkodzenie dokumentu XML. Innymi słowy, obiektu tego należy używać lokalnie, a nie globalnie. W naszej implementacji ContentHandler otrzymany Locator zachowaliśmy do zmiennej. Następnie moglibyśmy jej użyć w poprawny 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 prowadzić 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ętlach są często spotykane. Jednak w przypadku zdarzenia SAX takie postępowanie może powodować 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 wyprzedzeniem oraz wielokrotne wykorzystanie tej samej tablicy). Wszystko to jest poprawnym działaniem 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ę) przypadkowych danych. Pętla wygląda zupełnie zwyczajnie i kompiluje się „bez zająknięcia”, a więc jest to prawdziwa pułapka, na którą musi już uważać sam programista.
Co dalej?
Teraz Czytelnik powinien już dobrze rozumieć zasady działania interfejsów SAX oraz sposób, w jaki współdziałają z parserem XML i procesem przetwarzania — wszystko w kontekście parsera nie sprawdzającego poprawności dokumentu XML. Interfejsy te mają kluczowe znaczenie dla reszty zagadnień opisywanych w tej książce — w następnych rozdziałach zostaną poszerzone wiadomości o interfejsie SAX, a w programie zastosowane będą dodatkowe klasy tego interfejsu. W następnym rozdziale omówione zostaną zagadnienia związane z zawężaniem XML-a, procesem sprawdzania 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 wiersze w celu poprawnego umieszczenia wydruku w książce. O ile jednak same dane tekstowe się zgadzają, to na pewno 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