|
4
Zawężanie danych XML |
|
Nauka XML-a, zarówno jako reprezentacji danych, jak i materiału wykorzystywanego przez aplikację w Javie, to proces wieloetapowy. Kolejne poznawane cechy XML-a lub technologii siostrzanych przyczyniają się do odkrywania całych pokładów wiedzy o tym języku. Istnieje wiele projektów i specyfikacji związanych z XML-em; pojawia się pokusa „poznania wszystkiego”. A kiedy w końcu wydaje nam się, że poznaliśmy wszystkie strony zagadnienia, pojawiają się nowe wersje... Im lepiej jednak rozumiemy zasadę działania poszczególnych komponentów składających się na krajobraz XML-a, tym lepiej przygotowani jesteśmy na wszelkie nowinki w naszym programistycznym warsztacie. Pamiętając o tym, na jakiś czas porzucimy teraz język Java i wrócimy do specyfikacji związanych z samym XML-em.
W rozdziałach 2. i 3. uzyskaliśmy wiedzę, która powinna wystarczyć do stworzenia poprawnie sformatowanego dokumentu oraz do późniejszej manipulacji (w ograniczonym zakresie) tym dokumentem z poziomu Javy. Powinniśmy także rozumieć, jak przetwarzane są dokumenty XML i jak w tym procesie można wykorzystać klasy SAX. W tym rozdziale zostanie omówione zawężanie dokumentów XML, a w następnym — wpływ zawężania na przetwarzanie takiego dokumentu w Javie.
Po co zawężać dane XML?
Wypada najpierw wytłumaczyć, do czego potrzebne są Czytelnikowi wiadomości dotyczące definicji DTD i schematów. Niektórzy użytkownicy XML-a twierdzą, że w ogóle nie ma potrzeby zawężania dokumentów XML i sprawdzania ich poprawności. Jak już powiedzieliśmy, poprawny dokument XML spełnia wymogi zawężeń na niego narzuconych przez odpowiednią definicję DTD lub schemat. Napisaliśmy również, że choć dokument jest poprawnie sformatowany, to nie musi jeszcze być poprawny. Po cóż więc zadawać sobie trud tworzenia definicji DTD lub schematu, który narzuca dodatkowe reguły na dane XML?
Komentarze w programach
Jako programujący w Javie, Czytelnik zapewne potrafi już dokumentować swój kod za pomocą narzędzia Javadoc lub komentarzy w samym kodzie. Prawdopodobnie wie także, jak ważne jest dokumentowanie własnej pracy — być może na kogoś spadnie obowiązek czytania naszego kodu, poprawiania, czy po prostu zrozumienia. Komentowanie kodu jest jeszcze ważniejsze w projektach open source. Być może zdarzyło nam się kiedyś, że poganiani terminami nie byliśmy zbyt rozlewni w komentowaniu programu. A trzy miesiące później programista, któremu powierzono zadanie kontynuowania projektu, zasypywał nas telefonami o przeznaczenie danego fragmentu kodu. Dobrze, jeśli jeszcze pamiętaliśmy; gorzej, jeśli dawno już zapomnieliśmy, jak udało nam się zrobić „tę sztuczkę”. Właśnie w takich chwilach poznajemy wartość dokumentacji.
Dane XML to z pewnością nie kod. W wyniku zagnieżdżania i innych reguł składniowych niemal zawsze łatwiej jest zrozumieć dokument XML niż fragment kodu w Javie. Jednakże nie należy zakładać, że to, jak my widzimy naszą reprezentację danych, będzie identycznie postrzegane przez innych. Świetnie obrazuje to plik XML z przykładu 4.1.
Przykład 4.1. Dwuznaczny plik XML
<?xml version="1.0" encoding="ISO-8859-2"?>
<strona>
<ekran>
<nazwa>Sprzedaż</nazwa>
<kolor>#CC9900</kolor>
<font>Arial</font>
</ekran>
<zawartosc>
<p>Tu idzie cała zawartość.</p>
</zawartosc>
</strona>
Przeznaczenie powyższego pliku wydaje się zupełnie oczywiste. Za jego pomocą aplikacja otrzymuje informacje o sposobie wyświetlenia konkretnego ekranu u klienta. Podany jest kolor, rodzaj czcionki oraz zawartość ekranu. Gdzież tutaj dwuznaczność? Cóż, staje się ona ewidentna dopiero po obejrzeniu innego dokumentu wykorzystywanego w tej samej aplikacji (przykład 4.2).
Przykład 4.2. Mniej dwuznaczny plik XML
<?xml version="1.0" encoding="ISO-8859-2"?>
<strona>
<ekran>
<nazwa>Sprzedaż</nazwa>
<kolor>#CC9900</kolor>
<font>Arial</font>
</ekran>
<ekran>
<nazwa>Komunikaty</nazwa>
<kolor>#9900FF</kolor>
<font>Arial</font>
</ekran>
<ekran>
<nazwa>Nowości</nazwa>
<kolor>#EECCEE</kolor>
<font>Helvetica</font>
</ekran>
<zawartosc>
<p>Tu idzie cała zawartość.</p>
</zawartosc>
</strona>
I nagle nasza interpretacja pierwszego pliku nie wydaje się już poprawna. Element ekran nie może reprezentować bieżącego ekranu, bo w drugim przykładzie widzimy aż trzy takie elementy. W rzeczywistości aplikacja tworzy na górze strony odsyłacze do dostępnych ekranów i właśnie element ekran opisuje, jak odsyłacze te mają wyglądać — podana jest nazwa odsyłacza, kolor fragmentu ekranu i czcionka. W pierwszym przypadku tak się złożyło, że był tylko jeden ekran, do którego tworzono odsyłacz, i stąd to zamieszanie. Tylko autor dokumentu XML lub programista aplikacji od razu zrozumieliby, o co tutaj chodzi.
Zawężanie dokumentów XML umożliwia udokumentowanie takich dwuznacznych sytuacji. Gdybyśmy wiedzieli, że w danej stronie XML dozwolony jest tylko jeden element ekran, moglibyśmy bezpiecznie poczynić takie założenie, jakie zrobiliśmy odnośnie pierwszego przykładu. Gdybyśmy jednak wiedzieli, że dozwolonych jest wiele elementów ekran, to nawet patrząc tylko na pierwszy przykład moglibyśmy trafniej odgadnąć przeznaczenie dokumentu. Innymi słowy, poprawnie sformatowany dokument XML zawiera słowa występujące w słowniku. Słowa te mają pewne znaczenia, ale mogą być wykorzystywane w różny sposób. Jako przykład weźmy słowa: „Lis kot bieg chleb ser”. Poprawność (ang. validity) dokumentu daje nam gwarancje, że „słowa” te (elementy i atrybuty dokumentu XML) zostaną złożone w sensowny sposób: „Lisy i koty biegną w kierunku chleba z serem”.
Udokumentowanie „poprawnych” czy „właściwych” połączeń elementów i atrybutów to właśnie zadanie definicji DTD lub schematu. Umożliwiają one samodokumentowanie się danych XML — teraz już nie tylko my będziemy wiedzieli, co właściwie chcieliśmy przekazać za pomocą danych XML.
Przenośność
Zawężanie XML-a pomaga nie tylko zrozumieć sposób reprezentacji danych innym osobom; pomaga także zrozumieć dane innym aplikacjom. Wspomnieliśmy o tym już wcześniej — jeśli weźmiemy dwie dowolne aplikacje, to nie możemy zakładać, że korzystają one ze wspólnych zasobów. Mówiąc inaczej, program, który utworzył dokument XML w jednej aplikacji, może nie być dostępny w drugiej; ta druga aplikacja „nie rozumie” logiki, według której powstał dokument XML. Druga aplikacja musi więc określić sama, jaki typ danych jest przekazywany za pośrednictwem dokumentu XML. Nie mając żadnych wskazówek, druga aplikacja mogłaby tylko zakładać, co „autor miał na myśli” — i często byłyby to założenia błędne.
Przypomina to nieco problemy z językiem C, których starali się uniknąć twórcy Javy. Java, niezależna od platformy i nie polegająca na kodzie własnym, jest obecnie najbardziej przenośnym językiem programowania. Wynika to stąd, że nałożono szereg zawężeń na to, co Java może robić i zawężenia te działają na wszystkich platformach. Takie szczegóły implementacyjne jak zarządzanie pamięcią czy wątkami pozostawiono do rozwiązania poszczególnym platformom, ale interfejs do obsługi tych zadań przez programistę jest zawsze taki sam.
Zawężanie dokumentów za pomocą definicji DTD lub schematów gwarantuje podobną przenośność w języku XML. Przypomnijmy sobie pierwszy z dwóch powyższych przykładów — gdyby inna aplikacja mogła uzyskać dostęp do zasobu opisującego dozwolone formaty przetwarzanych danych, mogłaby przetworzyć te dane za pomocą narzędzi XML-a. Ponieważ zawężenia dokumentu nie zostały zakodowane bezpośrednio w aplikacji (pierwszej czy drugiej), logika aplikacji nie zmieni się po zmianie formatu dokumentu. Definicja DTD lub schemat mógłby ulec zmianie, ale ponieważ pracujemy na tekstowym opisie zawężeń, aplikacja mogłaby natychmiast wykorzystać dokument o zmienionej strukturze, bez konieczności modyfikacji samej aplikacji. W ten sposób uzyskuje się przenośność danych XML bez konieczności ingerencji w kod aplikacji — tak jak próbuje się unikać kodu własnego w programach w Javie.
Czy to na potrzeby dokumentacji, przenośności pomiędzy aplikacjami i systemami, czy po prostu ze względu na bardziej restrykcyjne sprawdzanie poprawności danych XML, zawężanie przydaje się w niemal wszystkich przypadkach. Przeciwnicy zawężania mają rację właściwie tylko w jednym — sprawdzanie poprawności danych ma duży wpływ na wydajność całego procesu. Jednakże w wielu strukturach publikacji, np. takich jak Apache Cocoon, można określić, czy dany dokument ma być sprawdzany pod kątem poprawności, czy nie. Oznacza to, że tworzenie dokumentu można przeprowadzać przy włączonym sprawdzaniu poprawności i wyłączyć je dopiero wtedy, gdy struktura dokumentu została już solidnie przetestowana. Aplikacje otrzymujące dane również mogą sprawdzać lub nie sprawdzać poprawności dokumentu. W razie potrzeby zawsze jest to możliwe, bo dokument ten zawierać będzie odwołanie do definicji DTD lub schematu. A więc możliwe jest korzystanie ze sprawdzania składni bez jednoczesnego zmniejszania wydajności aplikacji. Wcześniej należy jednak sprawdzić, czy funkcja włączania-wyłączania sprawdzania jest dostępna w wykorzystywanej strukturze publikacji.
W systemach produkcyjnych sprawdzanie poprawności oznacza wyższą jakość aplikacji typu firma-firma (ang. business-to-business). Sprawdzanie poprawności daje gwarancję, że dane otrzymane z innych aplikacji — często takich, na które nie mamy wpływu — są poprawnie sformatowane. Dzięki temu unikamy błędów wynikających z niepoprawnego formatu danych wejściowych. Tutaj definicje DTD i schematy są wprost nieocenione.
Definicje typu dokumentu
Jak już to zostało powiedziane, dokument XML ma niewielką wartość bez towarzyszącej mu definicji DTD. XML w wydajny sposób opisuje dane, a DTD przygotowuje te dane do użycia w wielu różnych programach poprzez zdefiniowanie ich struktury. W tej części książki zajmiemy się konstrukcjami DTD. W funkcji przykładowego pliku XML znów zostanie wykorzystany fragment spisu treści niniejszej książki, dla którego zbudujemy definicję DTD.
Zadaniem definicji DTD jest określenie sposobu formatowania danych. Zdefiniowany musi zostać każdy element dozwolony w danym dokumencie XML, sposoby występowania i zagnieżdżania elementów, a także zewnętrzne encje. Tak naprawdę w definicji DTD można określić jeszcze wiele innych aspektów dokumentu, ale tutaj skoncentrujemy się na tych podstawowych. Poznamy konstrukcje oferowane przez DTD — za ich pomocą zawęzimy nasz przykładowy plik z rozdziału 2. Ponieważ do pliku tego będziemy się często odwoływali w tym rozdziale, warto przytoczyć go tutaj jeszcze raz (przykład 4.3).
Przykład 4.3. Plik XML zawierający spis treści
<?xml version="1.0" encoding="ISO-8859-2"?>
<?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:Title>Java i XML</JavaXML:Title>
<JavaXML:Spis>
<JavaXML:Rozdzial tematyka="XML">
<JavaXML:Naglowek>Wprowadzenie</JavaXML:Naglowek>
<JavaXML:Temat podrozdzialy="7">Co to jest?</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="3">Jak z tego korzystać?</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="4">Dlaczego z tego korzystać?</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="0">Co dalej?</JavaXML:Temat>
</JavaXML:Rozdzial>
<JavaXML:Rozdzial tematyka="XML">
<JavaXML:Naglowek>Tworzenie dokumentów XML</JavaXML:Naglowek>
<JavaXML:Temat podrozdzialy="0">Dokument XML</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="2">Nagłówek</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="6">Zawartość</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="1">Co dalej?</JavaXML:Temat>
</JavaXML:Rozdzial>
<JavaXML:Rozdzial tematyka="Java">
<JavaXML:Naglowek>Przetwarzanie kodu XML</JavaXML:Naglowek>
<JavaXML:Temat podrozdzialy="3">Przygotowujemy się</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="3">Czytniki SAX</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="9">Procedury obsługi zawartości</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="4">Procedury obsługi błędów</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="0">
Lepszy sposób ładowania parsera
</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="4">"Pułapka!"</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="0">Co dalej?</JavaXML:Temat>
</JavaXML:Rozdzial>
<JavaXML:PodzialSekcji/>
<JavaXML:Rozdzial tematyka="Java">
<JavaXML:Naglowek>Struktury publikacji WWW</JavaXML:Naglowek>
<JavaXML:Temat podrozdzialy="4">Wybór struktury</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="4">Instalacja</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="3">
Korzystanie ze struktury publikacji
</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="2">XSP</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="3">Cocoon 2.0 i dalej</JavaXML:Temat>
<JavaXML:Temat podrozdzialy="0">Co dalej?</JavaXML:Temat>
</JavaXML:Rozdzial>
</JavaXML:Spis>
<JavaXML:Copyright>&OReillyCopyright;</JavaXML:Copyright>
</JavaXML:Ksiazka>
Określanie elementów
Najpierw zajmiemy się określaniem, które elementy są w naszym dokumencie dozwolone. Chcemy, aby definicja DTD umożliwiała autorom umieszczanie w naszym dokumencie takich elementów jak JavaXML:Ksiazka i JavaXML:Spis, ale nie JavaXML:jas czy JavaXML:malgosia. Określenie zestawu dozwolonych elementów oznacza nadanie takiemu dokumentowi znaczenia semantycznego; innymi słowy określamy dopuszczalny (sensowny) kontekst. Najpierw należy więc stworzyć listę dopuszczalnych elementów. Najprostszym sposobem jest przejrzenie dokumentu i odnotowanie każdego wykorzystanego elementu. Dobrze jest także zdefiniować przeznaczenie poszczególnych znaczników. Co prawda na określenie przeznaczenia elementów
nie pozwala bezpośrednio DTD (chyba że za pomocą komentarzy — to całkiem niezłe rozwiązanie), ale coś takiego uprościłoby pracę autora DTD. W tabeli 4.1 przedstawiono pełne zestawienie elementów dokumentu contents.xml.
Tabela 4.1. Elementy dozwolone w naszym dokumencie XML
Nazwa elementu |
Znaczenie |
JavaXML:Ksiazka |
element główny |
JavaXML:Tytul |
tytuł dokumentowanej książki |
JavaXML:Spis |
oznacza spis treści książki |
JavaXML:Rozdzial |
rozdział książki |
JavaXML:Naglowek |
nagłówek (tytuł) rozdziału |
JavaXML:Temat |
tematyka rozdziału |
JavaXML:PodzialSekcji |
podział pomiędzy rozdziałami oznaczający nową część książki |
JavaXML:Copyright |
informacje o prawach autorskich |
Teraz poszczególne elementy możemy zdefiniować w DTD. Wykonujemy to za pomocą następujących konstrukcji:
<!ELEMENT [Nazwa elementu] [Definicja/typ elementu]>
Konstrukcja [Nazwa elementu] to faktyczna nazwa elementu, taka jak w tabelce powyżej. Powinna ona, podobnie jak w tabeli, zawierać przedrostek przestrzeni nazw. W DTD nie istnieje pojęcie elementu z przedrostkiem przestrzeni nazw ani odwzorowania przestrzeni nazw według identyfikatora URI. Nazwa elementu ma postać albo samej nazwy (gdy nie użyto przestrzeni nazw), albo nazwy poprzedzonej przedrostkiem i dwukropkiem.
Najbardziej użytecznym fragmentem jest konstrukcja [Definicja/typ elementu]. Tutaj definiujemy element, nadając mu „typ” i określając, czy są to „czyste dane”, czy też typ złożony z danych i innych elementów. Najmniej restrykcyjnym typem elementu jest ten określony za pomocą słowa kluczowego ANY. Wtedy element może zawierać dane tekstowe, elementy zagnieżdżone, lub też dowolną (prawidłową) kombinację ich obu. Ta wiedza umożliwia już zdefiniowanie wszystkich elementów dokumentu XML, choć może w niezbyt użyteczny sposób. W przykładzie 4.4 jest przedstawiony początek definicji DTD naszego dokumentu XML.
Przykład 4.4. „Gołe” definicje elementów w DTD
<!ELEMENT JavaXML:Ksiazka ANY>
<!ELEMENT JavaXML:Tytul ANY>
<!ELEMENT JavaXML:Spis ANY>
<!ELEMENT JavaXML:Rozdzial ANY>
<!ELEMENT JavaXML:Naglowek ANY>
<!ELEMENT JavaXML:Temat ANY>
<!ELEMENT JavaXML:PodzialSekcji ANY>
<!ELEMENT JavaXML:Copyright ANY>
Oczywiście, taka prosta definicja DTD, nie opisująca ani atrybutów, ani encji, jest niezbyt przydatna. Określony jest każdy dozwolony element, ale nie ma mowy ani o typach tych dokumentów, ani o dozwolonym zagnieżdżaniu. Można stworzyć dokument zgodny z tą DTD, a jednak zupełnie bezsensowny (przykład 4.5).
Przykład 4.5. Dokument zgodny z DTD, ale bezużyteczny
<?xml version="1.0" encoding="ISO-8859-2"?>
<?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">
<JavaXML:Temat>
<JavaXML:Ksiazka>Oto moja książka</JavaXML:Ksiazka>
<JavaXML:Copyright>
<JavaXML:Rozdzial>Rozdział pierwszy</JavaXML:Rozdzial>
</JavaXML:Copyright>
<JavaXML:PodzialSekcji>A tu inna część</JavaXML:PodzialSekcji>
</JavaXML:Temat>
W powyższym fragmencie zostały użyte wyłącznie elementy dozwolone w DTD, a jednak jego struktura nie jest poprawna. Wynika to stąd, że nasza definicja DTD nie określa, w jaki sposób mogą być zagnieżdżane elementy i które z nich mogą zawierać dane tekstowe.
Zagnieżdżanie elementów
Jedną z najważniejszych cech struktury dokumentu XML jest zagnieżdżanie elementów. Naszą wcześniejszą tabelkę rozszerzymy teraz o informację o dozwolonym zagnieżdżaniu elementów. W ten sposób powstanie hierarchia elementów, którą następnie zdefiniujemy w DTD. Spójrzmy na tabelę 4.2.
Tabela 4.2. Hierarchia elementów
Nazwa elementu |
Dozwolone zagnieżdżone elementy |
Znaczenie |
JavaXML:Ksiazka |
JavaXML:Tytul |
element główny |
JavaXML:Tytul |
brak |
tytuł dokumentowanej książki |
JavaXML:Spis |
JavaXML:Rozdzial |
oznacza spis treści książki |
JavaXML:Rozdzial |
JavaXML:Naglowek |
rozdział książki |
JavaXML:Naglowek |
brak |
nagłówek (tytuł) rozdziału |
JavaXML:Temat |
brak |
tematyka poruszana w rozdziale |
JavaXML:PodzialSekcji |
brak |
podział pomiędzy rozdziałami oznaczający nową część książki |
JavaXML:Copyright |
brak |
informacje o prawach autorskich |
Teraz możemy już określić sposób zagnieżdżania w DTD. Robimy to za pomocą następującej konstrukcji:
<!ELEMENT [Nazwa elementu] ([Zagniezdzony element] [,Zagniezdzony element]...)>
W tym przypadku typem elementu staje się lista elementów rozdzielonych przecinkami. Istotna jest kolejność elementów, z czego wynika dalsze zawężenie dokumentu XML. W ten sposób gwarantujemy, że np. element copyright znajdzie się na końcu książki, albo że tytuł występuje przed spisem. Teraz możemy naszą definicję DTD wzbogacić o opis zagnieżdżania (przykład 4.6).
Przykład 4.6. Definicja DTD określająca hierarchię elementów
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ELEMENT JavaXML:Tytul ANY>
<!ELEMENT JavaXML:Spis (JavaXML:Rozdzial, JavaXML:PodzialSekcji)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek, JavaXML:Temat)>
<!ELEMENT JavaXML:Naglowek ANY>
<!ELEMENT JavaXML:Temat ANY>
<!ELEMENT JavaXML:PodzialSekcji ANY>
<!ELEMENT JavaXML:Copyright ANY>
Choć niektóre elementy — te, które zawierają przetwarzane dane — nie uległy zmianie, stworzyliśmy hierarchię, która nadaje znaczenie zawężonemu dokumentowi. Wcześniejszy bezsensowny przykład teraz traktowany byłby już jako niepoprawny. Wciąż jednak jest tutaj sporo problemów związanych z dozwolonymi typami danych w pozostałych elementach.
Przetwarzane dane
Typ elementu wykorzystywany do określania danych tekstowych to #PCDATA. Nazwa tego typu wywodzi się ze słów Parsed Character Data (przetwarzane dane tekstowe). Za jego pomocą oznaczamy elementy zawierające dane tekstowe, które mają być w zwyczajny sposób traktowane przez parser XML. Zastosowanie tego typu wyklucza jednak możliwość obecności w elemencie innych, zagnieżdżonych elementów. Tego typu sytuacje zostaną przedstawione dalej. Tymczasem możemy już zmienić elementy określające tytuł, nagłówek i temat — w nich mogą być zawarte dane tekstowe (przykład 4.7).
Przykład 4.7. Definicja DTD opisująca hierarchię elementów i elementy zawierające dane tekstowe
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis (JavaXML:Rozdzial, JavaXML:PodzialSekcji)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek, JavaXML:Temat)>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ELEMENT JavaXML:PodzialSekcji ANY>
<!ELEMENT JavaXML:Copyright ANY>
Elementy puste
Oprócz elementów zawierających dane tekstowe oraz tych, które zawierają inne elementy, mamy jeden element, JavaXML:PodzialSekcji, który nie ma zawierać żadnych danych. Oczywiście, można by określić, że element ten zawiera przetwarzane dane tekstowe i nigdy ich tam nie wstawiać, ale nie tak należy korzystać z zawężeń. Lepiej jawnie określić, że element ma być pusty — w ten sposób zapobiegamy przypadkowym błędom. Tym razem odpowiednie słowo kluczowe
to EMPTY. Nie musi ono pojawiać się w nawiasach, ponieważ określa pewien typ i nie może być łączone z innymi elementami, na co — jak wkrótce zobaczymy — pozwalają właśnie nawiasy. Wprowadźmy więc kolejną poprawkę do definicji DTD (przykład 4.8).
Przykład 4.8. Definicja DTD z określonym elementem EMPTY
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis (JavaXML:Rozdzial, JavaXML:PodzialSekcji)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek, JavaXML:Temat)>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ELEMENT JavaXML:PodzialSekcji EMPTY>
<!ELEMENT JavaXML:Copyright ANY>
Encje
Ostatnim elementem, który należy uściślić, jest JavaXML:Copyright. Jak już to było wspomniane, element ten faktycznie zawiera encję odwołującą się do innego pliku, który w tym miejscu ma zostać zawarty. Kiedy dokument XML „zobaczy” &OReillyCopyright;, będzie usiłował odnaleźć encję OReillyCopyright w DTD, która w naszym przypadku powinna stanowić odwołanie do zewnętrznego pliku. Plik ten zawiera informacje o prawach autorskich wszystkich książek udokumentowanych za pomocą języka XML. Zadaniem DTD jest określenie miejsca położenia pliku oraz sposobu uzyskania dostępu do niego. Tutaj zakładamy, że plik z prawami autorskimi znajduje się na dysku lokalnym i że do niego właśnie będziemy się odwoływali. Encje określane są w ramach definicji DTD za pomocą następującego zapisu:
<!ENTITY [Nazwa encji] "[Znaki podstawiane/identyfikator]">
Zauważmy, że umożliwia się podanie zestawu znaków podstawianych (zamiast pliku zewnętrznego). To właśnie w ten sposób określane są „sekwencje unikowe” w dokumentach XML:
<!ENTITY & "&">
<!ENTITY < "<">
<!ENTITY > ">">
...
Tak więc jeśli nasza informacja o prawach autorskich byłaby krótka, moglibyśmy napisać:
<!ENTITY &OReillyCopyright;
"Copyright O'Reilly and Associates, 2000">
Jednakże my zamierzamy użyć dłuższego tekstu, a więc wygodniej będzie pobrać go z zewnętrznego pliku. W ten sposób możemy z niego korzystać w wielu dokumentach bez konieczności duplikowania danych w poszczególnych definicjach DTD. Wymaga to podania identyfikatora zasobu systemowego, dlatego korzystamy tutaj z następującego zapisu:
<!ENTITY [Encja] SYSTEM "[URI]">
Tak jak w przypadku dokumentu XML, URI może być zarówno zasobem lokalnym, jak i sieciowym. W naszym przypadku odwołujemy się do pliku znajdującego się na serwerze zewnętrznym, więc musimy zastosować adres URL:
<!ENTITY OReillyCopyright SYSTEM
"http://www.oreilly.com/catalog/javaxml/docs/copyright.xml">
Dzięki takiemu zapisowi parser XML rozpozna referencję OReillyCopyright w dokumencie i odpowiednio przetłumaczy ją w czasie przetwarzania. Z tego właśnie powodu musieliśmy ten fragment oznaczyć jako komentarz w rozdziale 3. W następnym rozdziale usuniemy symbole komentarza i zobaczymy, w jaki sposób parser sprawdzający poprawność składni obsłuży tę encję z wykorzystaniem definicji DTD.
Na koniec w elemencie zawierającym encję musimy podać, że oczekujemy w tym miejscu przetwarzanych danych tekstowych:
<!ELEMENT JavaXML:Copyright (#PCDATA)>
Powtórz to, a...
Ostatnią ważną konstrukcją, jaką omówimy w związku z definicjami DTD, jest opis grupowania, wielokrotnego występowania i dopuszczalnych kombinacji w ramach elementu. Innymi słowy, chodzi tu np. o sytuacje, w których element X może wystąpić tylko raz, lub też po elemencie Y musi wystąpić element Z. To niezwykle istotne konstrukcje DTD. Domyślnie, jeśli nie podaliśmy w elemencie specjalnych modyfikatorów, element może wystąpić dokładnie raz:
<!ELEMENT MojElement (ZagniezdzonyElement, InnyElement)>
W tym przypadku ZagniezdzonyElement ma wystąpić tylko raz, a po nim koniecznie musi znajdować się InnyElement. Jeśli struktura dokumentu XML jest inna, to dokument ten nie jest poprawny. Aby zmienić to domyślne zawężenie, można zastosować specjalne modyfikatory.
Zero, jeden lub więcej
Najczęściej do elementu dodawany jest operator rekurencji. Umożliwia on określenie, czy dany element ma się pojawić zero lub więcej razy, jeden lub więcej razy, wcale lub (ustawienie domyślne) dokładnie raz. W tabeli 4.3 przedstawione są operatory rekurencyjne i ich znaczenia.
Tabela 4.3. Operatory rekurencyjne
Operator |
Opis |
[Domyślnie] |
musi wystąpić dokładnie raz |
? |
musi wystąpić raz albo wcale |
+ |
musi wystąpić przynajmniej raz (1 ... n razy) |
* |
może wystąpić dowolną liczbę razy (0 ... n razy) |
Modyfikatory dodawane są na końcu nazwy elementu. W naszym poprzednim przykładzie, aby zezwolić na jedno lub więcej wystąpień elementu ZagniezdzonyElement, a potem na tylko jedno (lub wcale) wystąpienie elementu InnyElement, należałoby użyć następującej konstrukcji:
<!ELEMENT MojElement (ZagniezdzonyElement+, InnyElement?)>
Wtedy taki zapis XML byłby zupełnie poprawny:
<MojElement>
<ZagniezdzonyElement>Jeden</ZagniezdzonyElement>
<ZagniezdzonyElement>Dwa</ZagniezdzonyElement>
</MojElement>
W definicji DTD, którą tworzymy, z taką sytuacją mamy do czynienia w przypadku elementu JavaXML:Rozdzial. Chcemy, aby nagłówek rozdziału (JavaXML:Naglowek) mógł wystąpić raz lub wcale, chcemy również pozwolić na wystąpienie jednego lub więcej elementów JavaXML:Temat. Wykorzystujemy modyfikatory rekurencyjne:
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
Ta prosta zmiana czyni naszą reprezentację rozdziału w XML bardziej realistyczną. Musimy także dokonać zmian w definicji elementu JavaXML:Spis. Może się w nim pojawić jeden lub więcej rozdziałów, a potem, ewentualnie, podział sekcji. Podział jest opcjonalny, bo książka może się składać wyłącznie z rozdziałów. Rekurencję rozdziałów i podziałów sekcji zapiszemy następująco:
<!ELEMENT JavaXML:Spis (JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)>
Wciąż jednak definicja DTD „nie wie”, że po elemencie JavaXML:PodzialSekcji może wystąpić więcej rozdziałów. Chcemy, aby cała ta struktura mogła pojawić się kilkakrotnie. Po rozdziale podział sekcji, potem znów rozdziały i znów podziały sekcji. Musimy zastosować grupowanie.
Grupowanie
Grupowanie umożliwia rozwiązywanie problemów takich jak ten z zagnieżdżaniem elementów wewnątrz JavaXML:Spis. Często rekurencja dotyczy bloku elementów, a nie jednego elementu. Dlatego operatory rekurencji można dodawać także do grup elementów. Grupę elementów tworzymy poprzez ujęcie elementów w nawiasy. Jeśli w tej chwili Czytelnikowi przypomniały się zajęcia z języka LISP, to niech się nie zamartwia — w naszych przykładach wszystko będzie dosyć proste, a liczba nawiasów nie wymknie się spod kontroli. Oczywiście, zagnieżdżanie nawiasów jest dozwolone. Tak więc, aby stworzyć grupę elementów, zastosujemy następującą konstrukcję:
<!ELEMENT PrzykladGrupowania ((Grupa1El1, Grupa1El2),
(Grupa2El1, Grupa2El2))>
Do każdej grupy (a nie tylko do poszczególnych jej elementów) można dopisać modyfikator. W opisywanym scenariuszu musimy tak zmienić zapis, aby możliwe było wielokrotne wystąpienie grupy zawierającej elementy rozdziału i podziału sekcji:
<!ELEMENT JavaXML:Spis (JavaXML:Rozdzial+,JavaXML:PodzialSekcji?)+>
Teraz dozwolone są już różne kombinacje — kilka rozdziałów plus jeden podział sekcji i to wszystko powtórzone wiele razy lub też nie powtórzone wcale. Możliwy jest także przypadek, w którym wystąpią same tylko rozdziały, bez podziałów sekcji. Jednakże tutaj sprawa nie jest zupełnie jasna (według naszego opisu w DTD). Lepiej byłoby określić, że pojawić się może jeden lub więcej rozdziałów lub taka struktura. Zachowanie DTD się nie zmieni, ale z pewnością definicja będzie bardziej czytelna. Aby to uzyskać, należy wprowadzić pojęcie „lub”.
To lub tamto
W definicjach DTD możliwe jest stosowanie pojęcia „lub”, oznaczanego symbolem kreski pionowej. Symbol ten jest często wykorzystywany w połączeniu z grupowaniem. Częstym, choć niekoniecznie udanym przykładem użycia operatora „lub” jest umożliwienie wystąpienia wewnątrz elementu pewnego innego elementu (elementów) lub danych:
<!ELEMENT ElementKumulujacy (#PCDATA|(Element 1, Element2))>
W przypadku tej definicji DTD oba poniższe fragmenty będą poprawne:
<ElementKumulujacy>
<Element1>Jeden</Element1>
<Element2>Dwa</Element2>
</ElementKumulujacy>
<ElementKumulujacy>
Dane tekstowe
</ElementKumulujacy>
Nie zaleca się jednak stosowania takiego rozwiązania, gdyż wówczas staje się niejasne znaczenie elementu zamykającego. Zazwyczaj element powinien zawierać dane tekstowe, dane przetwarzane lub inne elementy — ale nie wszystko razem.
W naszym dokumencie zależy nam na bardziej przejrzystej reprezentacji elementu JavaXML: Spis. Zastosujemy następujące rozwiązanie:
<!ELEMENT JavaXML:Spis ((JavaXML:Rozdzial+)|
(JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)+)>
Teraz jest już jasne, że może pojawić się tutaj albo pewna liczba rozdziałów, albo rozdziały, po których następuje podział sekcji. W ten sposób tworzy się przejrzyście udokumentowaną definicję, a jednocześnie zapewnia odpowiednie zawężenie dokumentu XML.
Teraz nasze elementy XML są już poprawnie określone i zawężone. Definicja przedstawiona w przykładzie 4.9 powinna działać bezproblemowo na naszym przykładowym dokumencie. Pozostały nam jeszcze definicje argumentów, które omawiamy poniżej.
Przykład 4.9. Opis elementów w definicji DTD
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis ((JavaXML:Rozdzial+)|
(JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)+)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ELEMENT JavaXML:PodzialSekcji EMPTY>
<!ELEMENT JavaXML:Copyright (#PCDATA)>
<!ENTITY OReillyCopyright SYSTEM
"http://www.oreilly.com/catalog/javaxml/docs/copyright.xml">
Definiowanie atrybutów
Po tym dość dokładnym omówieniu sposobu definiowania elementów możemy przejść do definiowania atrybutów. Ponieważ tutaj nie będziemy mieli do czynienia z zagnieżdżaniem, czynność ta jest nieco prostsza niż opisywanie elementów. Nie będziemy też potrzebowali operatorów rekurencji, ponieważ o tym, czy obecność danego atrybutu jest wymagana, mówi określone słowo kluczowe. Definicje atrybutów przyjmują następującą formę:
<!ATTLIST [Element zamykajacy]
[Nazwa atrybutu] [typ] [modyfikator]
...
>
Pierwsze dwa parametry, nazwa elementu i atrybutu, nie powinny sprawiać trudności. Za pomocą jednej konstrukcji ATTLIST dowolnemu elementowi możemy przypisać wiele atrybutów. Taki schemat jak powyżej zostanie dodany teraz do naszej definicji DTD, dzięki czemu lepiej będzie przedstawiona struktura opisywania atrybutów. Najlepiej definicje atrybutów dodawać zaraz po opisie elementu (to kolejny krok w kierunku utworzenia samodokumentującego się opisu DTD). Spójrzmy na przykład 4.10.
Przykład 4.10. Definicja DTD opisująca elementy i zawierająca ogólny szkielet opisów atrybutów
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ATTLIST JavaXML:Ksiazka
xmlns:JavaXML [typ] [modyfikator]
>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis ((JavaXML:Rozdzial+)|
(JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)+)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ATTLIST JavaXML:Rozdzial
tematyka [typ] [modyfikator]
czesc [typ] [modyfikator]
>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ATTLIST JavaXML:Temat
podRozdzialy [typ] [modyfikator]
>
<!ELEMENT JavaXML:PodzialSekcji EMPTY>
<!ELEMENT JavaXML:Copyright (#PCDATA)>
<!ENTITY OReillyCopyright SYSTEM
"http://www.oreilly.com/catalog/javaxml/docs/copyright.xml">
Teraz zdefiniujmy typy dozwolone dla poszczególnych atrybutów.
Typy atrybutów
Wartością wielu atrybutów będą dane tekstowe. To najprostszy typ wartości atrybutu, ale też najmniej zawężony. Typ ten określamy za pomocą słowa kluczowego CDATA (Character Data — dane tekstowe). Jest to ta sama konstrukcja CDATA, która w samym dokumencie XML reprezentuje dane wchodzące w skład „sekwencji unikowej”. Typ ten jest zazwyczaj używany wtedy, gdy atrybut może przyjąć dowolną wartość i służy jako komentarz lub dodatkowa informacja o danym elemencie. Wkrótce Czytelnik przekona się, że lepszym rozwiązaniem jest zdefiniowanie zestawu wartości dozwolonych dla atrybutu danego elementu. W naszym dokumencie typem tekstowym jest atrybut xmlns. Co prawda xmlns to słowo kluczowe XML oznaczające deklarację przestrzeni nazw, ale wciąż jest to atrybut, którego poprawność ma zostać sprawdzona — i musi zostać opisany w definicji DTD. Atrybut podRozdzialy elementu JavaXML:Temat przyjmuje także wartość tekstową:
<!ATTLIST JavaXML:Ksiazka
--> xmlns:JavaXML CDATA [modyfikator][Author:AJ]
>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis ((JavaXML:Rozdzial+)|
(JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)+)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ATTLIST JavaXML:Rozdzial
tematyka [typ] [modyfikator]
>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ATTLIST JavaXML:Temat
--> podRozdzialy CDATA [modyfikator][Author:AJ]
>
Następny typ atrybutu (jeden z najczęściej używanych) to wyliczenie. Umożliwia określenie konkretnych wartości atrybutu. Wartości nie wymienione w wyliczeniu powodują, że dokument XML jest niepoprawny. Rozwiązanie to przydaje się wszędzie tam, gdzie zestaw wartości atrybutu znamy już w czasie tworzenia dokumentu i umożliwia on bardzo ścisłe zawężenie definicji elementu. Taki typ ma nasz atrybut tematyka, ponieważ dozwolone są dla niego jedynie wartości „Java” lub „XML”. Wartości dozwolone umieszczane są w nawiasach i rozdzielane znakiem operatora „lub”, podobnie jak przy określaniu zagnieżdżania elementów:
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ATTLIST JavaXML:Rozdzial
tematyka (XML|Java) [modyfikator]
>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
Być albo nie być ...
W definicji atrybutu należy jeszcze określić, czy atrybut ten jest w danym elemencie wymagany. Służą do tego trzy słowa kluczowe: #IMPLIED, #REQUIRED lub #FIXED. Atrybut niejawny (ang. implied) nie jest wymagany. Takim modyfikatorem można określić atrybut podRozdzialy, jako że nie jest on wymagany do stworzenia poprawnego dokumentu:
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ATTLIST JavaXML:Temat
podRozdzialy CDATA #IMPLIED
>
Jeśli chodzi o atrybut xmlns, to chcemy zagwarantować, żeby autor zawartości zawsze określał przestrzeń nazw dla danej książki. W przeciwnym razie nasze przedrostki określające przestrzeń nazw na nic by się nie zdały. W takim przypadku należy zastosować słowo #REQUIRED (wymagany). Gdyby atrybut ten nie został zawarty w elemencie JavaXML:Ksiazka, dokument nie byłby poprawny:
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ATTLIST JavaXML:Ksiazka
xmlns:JavaXML CDATA #REQUIRED
>
Ostatnie słowo kluczowe, #FIXED, nie jest zbyt często używane w aplikacjach. Najczęściej stosuje się je w systemach bazowych; określa ono, że użytkownik nie może nigdy zmienić wartości danego atrybutu. Format jest tutaj następujący:
<!ATTLIST [Nazwa elementu]
[Nazwa atrybutu] #FIXED [Stala wartosc]
>
Ponieważ atrybut ten nie przydaje się w aplikacjach dynamicznych (nie jest możliwa jego zmiana), nie będziemy się nim tutaj zajmować.
Do omówienia pozostał jeszcze atrybut tematyka. Wymieniliśmy wszystkie możliwe wartości, jakie może przyjmować, ale ponieważ w tej książce koncentrujemy się głównie na Javie, chcielibyśmy, aby autor nie musiał jawnie definiować atrybutu jako „Java” w tych rozdziałach, w których rzeczywiście Java jest głównym tematem. W książce składającej się z dwudziestu czy trzydziestu rozdziałów ciągłe wpisywanie atrybutu może okazać się męczące. Wyobraźmy sobie listę książek w bibliotece naukowej, w której jako temat każdej książki należałoby podać „nauka”! Duplikowanie danych wydłuża proces, a więc wymaganie podawania atrybutu za każdym razem nie jest dobrym rozwiązaniem. Chcemy więc przypisać atrybutowi pewną domyślną wartość, kiedy żadna wartość nie zostanie podana przez autora (i nie załatwia tego słowo #IMPLIED, które powoduje, że atrybutowi nie zostaje przypisana żadna wartość). Domyślną wartość można określić poprzez wpisanie jej w cudzysłowach zamiast wpisania słowa kluczowego modyfikatora. Jeśli typem atrybutu jest wyliczanie (enumeration), wartość ta powinna być jednym z wymienionych łańcuchów znaków. Zdefiniujmy więc nasz atrybut tematyka:
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ATTLIST JavaXML:Rozdzial
tematyka (XML|Java) "Java"
>
Nasza definicja DTD jest już gotowa! Składnia definicji DTD może wydawać się nieco dziwaczna, ale mam nadzieję, że Czytelnikowi nie sprawiło trudności śledzenie kolejnych etapów definiowania elementów, atrybutów i encji. Nie omówiliśmy oczywiście wszystkich zagadnień związanych z definicjami DTD, ponieważ niniejsza książka nie traktuje o samym XML-u, ale o Javie i XML-u. Mam jednak nadzieję, że przykładowa definicja DTD jest zrozumiała i że Czytelnik będzie potrafił stworzyć proste definicje dla własnych dokumentów. Zanim przejdziemy do omawiania schematów, spójrzmy raz jeszcze na gotową definicję DTD (przykład 4.11).
Przykład 4.11. Gotowa definicja DTD
<!ELEMENT JavaXML:Ksiazka (JavaXML:Tytul,
JavaXML:Spis,
JavaXML:Copyright)>
<!ATTLIST JavaXML:Ksiazka
xmlns:JavaXML CDATA #REQUIRED
>
<!ELEMENT JavaXML:Tytul (#PCDATA)>
<!ELEMENT JavaXML:Spis ((JavaXML:Rozdzial+)|
(JavaXML:Rozdzial+, JavaXML:PodzialSekcji?)+)>
<!ELEMENT JavaXML:Rozdzial (JavaXML:Naglowek?,JavaXML:Temat+)>
<!ATTLIST JavaXML:Rozdzial
tematyka (XML|Java) "Java"
>
<!ELEMENT JavaXML:Naglowek (#PCDATA)>
<!ELEMENT JavaXML:Temat (#PCDATA)>
<!ATTLIST JavaXML:Temat
podRozdzialy CDATA #IMPLIED
>
<!ELEMENT JavaXML:PodzialSekcji EMPTY>
<!ELEMENT JavaXML:Copyright (#PCDATA)>
<!ENTITY OReillyCopyright SYSTEM
"http://www.oreilly.com/catalog/javaxml/docs/copyright.xml">
Po dokładnym przyjrzeniu się tej definicji DTD można uznać, że jest ona zbyt złożona. Struktura definicji DTD rządzących organizacją plików XML zupełnie nie przypomina samych plików XML. Struktura DTD jest odmienna od struktury schematu, arkusza stylu XSL i niemal każdego innego dokumentu związanego z językiem XML. Definicje DTD opracowano jako część specyfikacji XML 1.0. Niektóre decyzje projektowe podjęte w ramach tej specyfikacji odbiły się piętnem na użytkownikach i programistach korzystających z XML. Podstawy definicji DTD w standardzie XML oparte są w dużej części na SGML-u — specyfikacji o wiele starszej. Jednakże struktura definicji DTD z czasów SGML-a stanowi niezbyt dobry wybór w odniesieniu do XML-a. Te różnice strukturalne stara się naprawić standard XML Schema, który skoncentrowany jest właśnie na XML-u i nie narusza stylu poprawnego programowania. Schematy XML Schema zostaną wkrótce omówione. Najprawdopodobniej schematy całkowicie zastąpią definicje DTD, ale zastępowanie to przebiega powoli — w wielu aplikacjach XML został już wdrożony w systemach produkcyjnych, w których są używane dokumenty zawężane definicjami DTD. To dlatego właśnie zrozumienie definicji DTD jest tak istotne — nawet jeśli mają one odejść do lamusa.
Czego brak w definicji DTD?
Co ciekawe, aż całą sekcję poświęcić trzeba sprawom, które nie zostały ujęte w definicji DTD. W DTD podać trzeba wszystkie elementy i zdefiniować wszystkie atrybuty dokumentu XML, ale nie muszą się tam znaleźć instrukcje przetwarzania (PI). Co więcej, w ogóle nie ma możliwości określenia instrukcji PI, podobnie jak deklaracji XML znajdującej się na samym początku dokumentu XML. Definicja DTD rozpoczyna się wraz z pierwszym wystąpieniem pierwszego elementu w pliku XML. Takie podejście może się wydawać naturalne — po co określać, że dokument ma zawierać taką, a nie inną instrukcję przetwarzani?. Odpowiedzią jest przenośność.
Są powody, dla których określanie instrukcji przetwarzania przydałoby się w definicjach DTD. Na przykład możliwa jest sytuacja, w której autor zawartości chciałby mieć gwarancję, że jego dokument XML zostanie zawsze przekształcony — a więc wymaga instrukcji opisującej arkusz stylu xml-stylesheet. Ale który arkusz chcemy wykorzystać? To trzeba również określić. A jaki mechanizm ma wykonać przekształcenia? Cocoon? Servlet Jamesa Clarka? Inna struktura? To wszystko również trzeba zdefiniować. Jednak kiedy wszystko zostanie dokładnie określone, dokument utraci przenośność. Będzie mógł być wykorzystany już tylko do jednego konkretnego celu i w jednej specyficznej strukturze publikacji. Nie będzie go łatwo przenieść na inną platformę, strukturę czy aplikację. Dlatego instrukcje PI i deklaracje XML pozostają niezawężone w ramach definicji DTD. Zajmujemy się tylko elementami i atrybutami dokumentu, począwszy od elementu głównego.
XML Schema
XML Schema to nowy projekt roboczy grupy W3C, uwzględniający dotychczasowe problemy i ograniczenia definicji DTD. Umożliwia on bardziej precyzyjną reprezentację zawężeń struktury XML i, co istotne, wykorzystuje w tym celu sam język XML. Schematy to właściwie dokumenty XML — dobrze sformatowane i poprawne. Mogą więc być obsługiwane przez parsery i inne aplikacje „znające XML” w sposób podobny jak zwykłe dane XML. Nie trzeba stosować specjalnych technik, jak przy obsłudze definicji DTD.
Ponieważ XML Schema to jeszcze „młoda” i niepełna specyfikacja, zostanie omówiona pokrótce. Szczegóły implementacji mogą w każdej chwili ulec zmianie; jeśli więc Czytelnik ma problemy z omawianymi przykładami, może przejrzeć najnowszą wersję projektu pod adresami http://www.w3.org/TR/xmlschema-1/ i http://www.w3.org/TR/xmlschema-2/. Należy także pamiętać, że wiele parserów XML nie obsługuje schematów XML, bądź też obsługuje je tylko częściowo. Stopień obsługi XML Schema opisany jest w instrukcji parsera.
Istnieje różnica pomiędzy dokumentem poprawnym a dokumentem poprawnym z punktu widzenia schematu. XML Schema nie stanowi części specyfikacji XML 1.0, a więc dokument zgodny z danym schematem może być niepoprawny. Tylko dokument XML zgodny z definicją DTD określoną w deklaracji DOCTYPE może być uważany za poprawny. W związku z tym w środowisku użytkowników XML-a rodzą się wątpliwości dotyczące sposobu obsługi sprawdzania poprawności poprzez XML Schema. Ponadto, nawet jeśli dokument posiada odpowiadający mu schemat, to nie znaczy jeszcze, że jego poprawność będzie sprawdzana — ponieważ XML Schema nie jest opisany w specyfikacji XML 1.0, aplikacja lub parser wcale nie muszą dokonywać takiego sprawdzania (bez względu na poziom obsługi schematów przez parser). Dlatego zawsze trzeba sprawdzić, czy parser będzie sprawdzał poprawność dokumentu, oraz jak obsługuje schematy. Na potrzeby przejrzystości opisu będziemy zakładać, że sprawdzanie poprawności to jedno pojęcie, obejmujące zarówno schematy, jak i definicje DTD; znaczenie tego pojęcia będzie zaś musiało być interpretowane w odpowiednim kontekście. Na ewentualne dwuznaczności zawsze będziemy zwracali uwagę.
Najważniejsze przy budowaniu schematu jest to, że w trakcie tego procesu powstaje po prostu nowy dokument XML. W przeciwieństwie do definicji DTD, korzystających z zupełnie odmiennego formatu opisu elementów i definiowania atrybutów, schemat jest zwyczajnym dokumentem XML. Składnia nie będzie więc znacząco odbiegała od tej opisanej w rozdziale 2. Co ciekawe, sam schemat XML jest zawężony za pomocą definicji DTD. Jeśli to wydaje się nieco osobliwe, to należy przypomnieć, że przed powstaniem XML Schema jedynym sposobem zawężania dokumentów było właśnie zastosowanie definicji DTD. Aby poprzez XML Schema można było sprawdzać poprawność, trzeba najpierw określić zawężenia samego schematu, korzystając z „zewnętrznego mechanizmu”. A tym zewnętrznym mechanizmem musi być definicja DTD. Jednak już po stworzeniu tej początkowej definicji wszystkie inne dokumenty XML w ogóle nie będą korzystały z DTD. Ta nieco dziwna kolej rzeczy jest zjawiskiem dość pospolitym w ewoluującym świecie specyfikacji — nowe wersje powstają na bazie starych.
Przestrzeń nazw schematu
Można oczekiwać, że dokumenty XML Schema rozpoczynają się standardową deklaracją XML, po czym następuje odwołanie do przestrzeni nazw schematu. I tak jest w rzeczywistości. Ponadto istnieją standardy nazywania elementu głównego. Przyjęło się, że element główny nosi nazwę schema. Przy tworzeniu elementu głównego buduje się również definicje przestrzeni nazw — podobnie jak to robiliśmy w naszym przykładowym dokumencie XML. Przede wszystkim konieczne jest podanie domyślnej deklaracji przestrzeni nazw:
<xsd:schema xmlns:xsd="http://www.w3.org/1999/XMLSchema">
Jak to zostało przedstawione w rozdziale 2., pominięcie identyfikatora po atrybucie xmlns powoduje zastosowanie w dokumencie domyślnej przestrzeni nazw. W naszym wcześniejszym dokumencie XML definicja przestrzeni nazw odpowiadała konkretnie przestrzeni JavaXML:
<JavaXML:Ksiazka xmlns:JavaXML="http://www.w3.org/1999/XMLSchema">
W ten sposób informowaliśmy parser XML, że wszystkie dokumenty z przedrostkiem JavaXML należą do tej konkretnej przestrzeni nazw, skojarzonej z podanym adresem URL. W naszym dokumencie XML dotyczyło to wszystkich elementów, bo wszystkie opatrzone były tym przedrostkiem. Ale przecież mogły tam pojawić się także elementy bez takich przedrostków. Elementy takie nie są, oczywiście, skazane na niebyt — one również muszą zostać przypisane jakiejś przestrzeni nazw. Przypisuje się im przestrzeń domyślną, która nie jest zdefiniowana w dokumencie. Można zdefiniować ją za pomocą dodatkowej deklaracji przestrzeni nazw w elemencie głównym:
<JavaXML:Ksiazka xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml"
xmlns="http://www.jakisInnyUrl.com"
>
Spowodowałoby to, że dowolny element nie poprzedzony przedrostkiem JavaXML lub innym kojarzony byłby z domyślną przestrzenią nazw, identyfikowaną za pomocą adresu http://www.jakisInnyUrl.com. Tak więc w następującym fragmencie dokumentu elementy Ksiazka, Spis i Tytul skojarzono z przestrzenią JavaXML, zaś element1 i element2 z domyślną przestrzenią nazw:
<JavaXML:Ksiazka xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml"
xmlns="http://www.jakisInnyUrl.com"
>
<JavaXML:Tytul>Mój tytuł</JavaXML:Tytul>
<JavaXML:Spis>
<element1>
<element2 />
</element1>
</JavaXML:Spis>
</JavaXML:Ksiazka>
Ponieważ w naszym schemacie opisujemy inny dokument, wszystkie elementy związane z konstrukcjami samego XML Schema powinny należeć do domyślnej przestrzeni nazw — i dlatego podajemy na początku jej deklarację. Jednakże konstrukcje te oddziaływają na przestrzeń nazw w zawężanym dokumencie XML. Innymi słowy, konstrukcje XML Schema stanowią część przestrzeni nazw XML Schema, ale wykorzystywane są do zawężenia elementów w innych przestrzeniach nazw — w tych, które opisują dokument lub dokumenty XML opisywane przez ten schemat. W naszym przykładzie dotyczyłoby to przestrzeni nazw JavaXML. Tak więc do elementu schema trzeba dodać następującą deklarację:
<schema xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"
>
Teraz jeszcze należy „poinformować” schemat, że przedmiotem zawężania jest druga przestrzeń nazw. W tym celu należy zastosować atrybut targetNamespace (docelowa przestrzeń nazw), którego działanie jest zgodne z nazwą:
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"
>
W ten sposób otrzymaliśmy definicje dwóch przestrzeni nazw (domyślna oraz JavaXML) oraz określiliśmy przedmiot zawężania w postaci drugiej przestrzeni nazw (JavaXML). A skoro jest już zdefiniowany element główny, można rozpocząć nakładanie zawężeń na tę przestrzeń nazw. Należy pamiętać jeszcze o jednej rzeczy: w świecie protokołu HTTP i serwerów WWW możliwe jest, że adres URL skojarzony z daną przestrzenią nazw może być prawdziwym adresem; w naszym przypadku, gdybyśmy do przeglądarki wpisali http://www.oreilly.com/catalog/javaxml, otrzymalibyśmy odpowiedź HTML. Ale zwracany dokument tak naprawdę nie jest wykorzystywany; adres URL wcale nie musi być osiągalny — służy wyłącznie jako element kojarzony z przestrzenią nazw. Nie zawsze jest to łatwo zrozumieć, dlatego Czytelnik nie powinien zaprzątać sobie głowy tym, na co wskazuje podany identyfikator URI; ważniejsze jest skoncentrowanie się na deklarowanej przestrzeni nazw oraz na sposobie, w jaki wykorzystywana jest ona w dokumencie.
Uwaga! Zagadnienia omawiane w dalszej części rozdziału nie są proste, więc Czytelnik nie powinien przejmować się ewentualnymi trudnościami z ich zrozumieniem. Koncepcje związane ze schematami XML Schema nie są „łatwe i przyjemne”, a cała ta specyfikacja jest dopiero w fazie rozwoju. Wielu autorów zawartości korzysta z technologii XML Schema, my zaś staramy się tutaj zrozumieć ją — to subtelna, ale istotna różnica. Dzięki zrozumieniu zagadnień związanych z XML Schema Czytelnik będzie potrafił bardziej efektywnie projektować dokumenty XML i tworzyć lepsze aplikacje. Szczególnie złożoną sprawą jest sposób wykorzystania w schematach definicji DTD i przestrzeni nazw; na szczęście większość pozostałych konstrukcji nie jest już tak skomplikowana. Tak więc warto teraz zrelaksować się, napić kawy i uważnie, bez pośpiechu kontynuować lekturę. To na pewno zaowocuje w przyszłości.
Określanie elementów
Zagadnienie określania elementów zostało już poruszone przy omawianiu definicji DTD. W schematach określanie elementów okaże się nieco bardziej logiczne. Proces ten dość dokładnie odzwierciedla strukturę deklaracji w Javie (plus kilka dodatkowych opcji). Do określania elementów posłużymy się elementem... element:
<element name="[Nazwa elementu]"
type="[Typ elementu]"
[Opcje...]
>
[Nazwa elementu] określa element w zawężanym dokumencie XML. Jednakże w przeciwieństwie do definicji DTD, nie powinno być tutaj przedrostka przestrzeni nazw. Czytelnik winien przypomnieć sobie uwagi na temat docelowej przestrzeni nazw. Zostało tam powiedziane, że przestrzenią docelową (ang. target) jest JavaXML, a więc specyfikacje wszystkich elementów, a także typy zdefiniowane przez użytkownika, są kojarzone i przypisywane właśnie tej docelowej przestrzeni nazw. To przyczynia się do utworzenia bardziej przejrzystego dokumentu XML Schema, ponieważ najpierw definiowane są wszystkie elementy, a potem narzuca się przestrzeń nazw. Konstrukcja [Typ elementu] to albo wstępnie zdefiniowany typ danych w schemacie, albo typ zdefiniowany przez użytkownika. W tabeli 4.4 zestawiono typy danych obsługiwane przez bieżącą wersję standardu XML Schema.
Tabela 4.4. Typy danych w XML Schema
Typ |
Podtypy |
Znaczenie |
string |
NMTOKEN, language |
łańcuchy znaków |
boolean |
brak |
binarna wartość logiczna (prawda lub fałsz) |
float |
brak |
32-bitowy typ zmiennoprzecinkowy |
double |
brak |
64-bitowy typ zmiennoprzecinkowy |
decimal |
integer |
standardowy zapis dziesiętny, dodatni i ujemny |
timeInstant |
brak |
połączenie daty i czasu, wskazujące konkretny moment w czasie |
timeDuration |
brak |
czas trwania |
recurringInstant |
date, time |
specyficzny czas, powtarzający się przez okres timeDuration |
binary |
brak |
dane binarne |
uri |
enumeration |
jednolity identyfikator zasobów (URI). |
W naszych przykładach wykorzystamy tylko niektóre typy. Widać jednak od razu, że wybór jest tutaj większy niż w DTD.
Zaczynamy od dołu
Możliwe jest także tworzenie złożonych typów danych, definiowanych przez użytkownika. Typy takie budowane są z kombinacji elementów. Na przykład można określić, że typ Ksiazka składa się z elementu Tytul, Spis i Copyright (zwróćmy uwagę, że przy opisywaniu elementów nie jest już podawany przedrostek przestrzeni nazw — w schematach najpierw odczytywana jest tylko nazwa elementu, dopiero potem definiuje się odpowiednią przestrzeń). Te zaś elementy mogą znów być typami zdefiniowanymi przez użytkownika, składającymi się z dalszych elementów. Tak powstaje piramida hierarchii; na jej spodzie znajdują się elementy o podstawowych typach XML Schema; wyżej — warstwy elementów zdefiniowanych przez użytkownika; na samej górze — element główny.
Taka struktura wymusza określony sposób działania — najlepiej zaczynać budowanie schematu od określenia elementów znajdujących się u podstaw tej hierarchii — czyli tych, które można zdefiniować jako standardowe typy XML Schema. Sytuacja jest więc tutaj nieco inna niż w definicjach DTD, gdzie zazwyczaj postępuje się zgodnie z kolejnością elementów w dokumencie XML. Ale dzięki temu budowanie schematu jest prostsze. Jeśli spojrzymy na nasz dokument XML, to możemy określić, które elementy stanowią typy „prymitywne” — (patrz tabela 4.5).
Tabela 4.5. Elementy „prymitywne”
Nazwa elementu |
Typ |
Tytul |
string |
Naglowek |
string |
Temat |
string |
Teraz elementy te można wpisać do schematu (przykład 4.12). Aby budowany schemat nie utracił przejrzystości, zostaną w nim pominięte deklaracje XML i DOCTYPE; znajdą się one w wersji ostatecznej, ale teraz tylko zmniejszałyby czytelność przykładów.
Przykład 4.12. Schemat z elementami „prymitywnymi”
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"
>
<element name="Tytul" type="string" />
<element name="Naglowek" type="string" />
<element name="Temat" type="string" />
</schema>
Zbyt proste? Cóż, to jest proste. Po zdefiniowaniu tych „podstawowych”, czy też „prymitywnych” elementów możemy przejść do budowania elementów bardziej złożonych.
Typy danych definiowane przez użytkownika
Podobnie jak w przypadku elementów niepodzielnych, elementy bardziej złożone należy budować także „od dołu” piramidy. Niemal zawsze oznacza to, że programista rozpoczyna tworzenie od elementów najgłębiej zagnieżdżonych i posuwa się „w górę”, aż do elementu głównego. W naszym przypadku najbardziej zagnieżdżone są Naglowek i Temat. Ponieważ elementy te już zostały określone jako prymitywne, można przenieść się o poziom wyżej — do elementu Rozdzial. To właśnie będzie pierwszy element zdefiniowany przez użytkownika. W definicji musi zostać zawarta informacja, że składa się on z jednego elementu Naglowek oraz jednego lub więcej elementów Temat. Typy złożone definiujemy za pomocą elementu complexType:
<complexType name="[Nazwa typu">
<[Specyfikacja elementu]>
<[Specyfikacja elementu]>
...
</complexType>
Naszemu elementowi przypisujemy nowy typ. Dla elementu Rozdzial stworzymy typ TypRozdzial:
<complexType name="TypRozdzial">
...
</complexType>
Powstały w ten sposób typ staje się, oczywiście, częścią docelowej przestrzeni nazw JavaXML. Tak więc aby przypisać ten typ naszemu elementowi Rozdzial, użyjemy następującej specyfikacji dokumentu:
<element name="Rozdzial" type="JavaXML:TypRozdzial" />
Teraz, jakąkolwiek strukturę określimy wewnątrz typu TypRozdzial, będzie ona zawężała element Rozdzial. Należy zauważyć, że typ elementu określany jest jako JavaXML:TypRozdzial, a nie po prostu jako TypRozdzial. Typ ten był tworzony w ramach docelowej przestrzeni nazw, JavaXML. Jednak elementy, jakie wykorzystujemy w schemacie (element, complexType itd.), nie mają przedrostka przestrzeni nazw, ponieważ należą do przestrzeni domyślnej schematu. Gdybyśmy więc próbowali określić typ jako TypRozdzial, parser przeszukałby przestrzeń domyślną (tę odnoszącą się do schematu), a nie znajdując tam naszego elementu, zgłosiłby wyjątek. Aby parser „wiedział”, gdzie szukać definicji typu, należy wskazać poprawną przestrzeń nazw — w tym przypadku JavaXML.
Ogólną strukturę Czytelnik już zna, pora zająć się szczegółami. W przypadku tego elementu trzeba zdefiniować w schemacie dwa elementy, które są w tym typie zagnieżdżane. Ponieważ już zostały określone dwa zagnieżdżane elementy (prymitywne Naglowek i Temat), należy się do nich dowołać w nowym typie:
<complexType name="TypRozdzial">
<element ref="JavaXML:Naglowek" />
<element ref="JavaXML:Temat" />
</complexType>
Atrybut ref informuje parser XML, że definicja danego elementu znajduje się w innej części schematu. Tak jak przy określaniu typu i tutaj również trzeba wskazać parserowi przestrzeń nazw, w jakiej znajdują się elementy (zazwyczaj jest to przestrzeń docelowa). Ale można tutaj dopatrzyć się pewnej nadmiarowości i „przegadania”. Dwa elementy definiujemy jako prymitywy, a potem się do nich odwołujemy — to daje cztery linijki. Elementy te jednak nie są wykorzystywane nigdzie indziej w naszym dokumencie, a więc czy nie logiczniej byłoby zdefiniować element od razu wewnątrz typu? Nie trzeba byłoby wtedy odwoływać się do elementu i wymuszać na czytających nasz schemat przeszukiwanie dokumentu w celu odnalezienia elementu wykorzystanego tylko raz. Tak można zrobić. Specyfikacja elementu może zostać zagnieżdżona w typie zdefiniowanym przez użytkownika, a więc nieco przerobimy teraz nasz schemat — tak, aby był bardziej czytelny:
<element name="Tytul" type="string" />
<element name="Rozdzial" type="JavaXML:TypRozdzial" />
<complexType name="TypRodzial">
<element name="Naglowek" type="string" />
<element name="Temat" type="string" />
</complexType>
Oprócz usunięcia niepotrzebnych wierszy, pozbyliśmy się także niepotrzebnych odwołań do przestrzeni nazw JavaXML. To na pewno zwiększy czytelność, szczególnie wśród odbiorców nie mających doświadczenia w pracy z XML-em. Znając sposób tworzenia typów definiowanych przez użytkownika, możemy teraz określić pozostałe elementy, tak jak w przykładzie 4.13.
Przykład 4.13.
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/"
>
<element name="Ksiazka" type="JavaXML:TypKsiazka" />
<complexType name="TypKsiazka">
<element name="Tytul" type="string" />
<element name="Spis" type="JavaXML:TypSpis" />
<element name="Copyright" type="string" />
</complexType>
<complexType name="TypSpis">
<element name="Rozdzial" type="JavaXML:TypRozdzial" />
<element name="PodzialSekcji" type="string" />
</complexType>
<complexType name="TypRozdzial">
<element name="Naglowek" type="string">
<element name="Temat" type="string">
</complexType>
</schema>
Dzięki temu każdy użyty element XML zostanie zdefiniowany, a schemat będzie czytelny. Nie znaczy to jednak, że wszystkie problemy zostały rozwiązane.
Typy niejawne i pusta zawartość
Do tej pory korzystaliśmy wyłącznie z typów nazwanych, często określanych mianem jawnych. Typ jawny to taki, w którym podano nazwę typu; element wykorzystujący ten typ zazwyczaj znajduje się w innym miejscu pliku. To bardzo obiektowe podejście — ten sam typ jawny może być zastosowany dla różnych elementów. Jednakże istnieją sytuacje, kiedy taka struktura jest rozwiązaniem na wyrost; innymi słowy, typ jest tak specyficzny względem elementu, że jego nazywanie nie jest potrzebne. W naszym przykładzie definicję elementu Rozdzial moglibyśmy skonsolidować poprzez zdefiniowanie typu w samej definicji elementu. Służą do tego typy niejawne, zwane także nienazwanymi:
<complexType name="TypSpis">
<element name="Rozdzial">
<complexType>
<element name="Naglowek" type="string" />
<element name="Temat" type="string" />
</complexType>
</element>
<element name="PodzialSekcji" type="string" />
</complexType>
Zastosowanie typu niejawnego powoduje, że schemat jest jeszcze bardziej czytelny. Jednakże żaden inny element nie może być tego samego typu, co zdefiniowany w ten sposób typ niejawny — chyba że zdefiniowano kolejny typ niejawny. Innymi słowy, typów niejawnych używamy tylko wtedy, gdy jesteśmy zupełnie pewni, że typ ten nie będzie wykorzystywany przez wiele elementów.
Typy niejawne mogą także służyć do podawania informacji o definiowanych elementach. Na przykład, do tej pory definiowaliśmy element PodzialSekcji jako string. Faktycznie nie jest to określenie zbyt precyzyjne, ponieważ element ten ma być pusty. Aby określić element jako pusty, użyjemy typu niejawnego:
<element name="PodzialSekcji">
<complexType content="empty" />
</element>
To może wydawać się dziwne — dlaczego nie można po prostu przypisać temu elementowi „pustego” (ang. empty) typu danych? Czy twórcy specyfikacji XML Schema coś przeoczyli? Wręcz przeciwnie — we wcześniejszych wersjach specyfikacji istniał typ danych empty, ale go usunięto. Zrobiono tak, aby wymóc definicje typu elementu. Aby to zrozumieć, zauważmy, że większość pustych elementów może przyjmować atrybuty do określania danych:
<img src="obrazki/takiGif.gif" />
<komentarz tekst="A tu komentarz" />
W takich przypadkach określenie elementu jako empty nie pozwoliłoby na intuicyjne określenie, jakie atrybuty są w nim dozwolone. Sprawa jest natomiast prosta, jeśli użyjemy typu elementu:
<element name="img">
<complexType content="empty">
<attribute name="src" type="string" />
</complexType>
</element>
Sposoby definiowania atrybutów zostaną omówione dalej. Tymczasem warto pamiętać, że typy niejawne pomagają budować schemat w sposób bardziej intuicyjny oraz umożliwiają definiowanie właściwości elementu — np. tego, że jest pusty.
Ile?
Oprócz powyższych, w definicji elementu należy określić rodzaj rekurencji (lub zdefiniować jej brak). Schemat zachowuje się tutaj podobnie jak DTD — jeśli elementu nie opatrzono modyfikatorami, może pojawić się tylko raz. Jak już wcześniej wspomniano, nie zawsze akurat tak musi być. Nasza książka może mieć wiele rozdziałów, ale może nie mieć podziału sekcji; niektóre rozdziały mogą mieć nagłówki, a inne nie. Musimy umieć zdefiniować te wszystkie szczegóły w naszym schemacie. Podobnie jak w definicji DTD, służy do tego odpowiedni mechanizm. Ale w przeciwieństwie do DTD stworzono intuicyjny zestaw atrybutów (zamiast nieco zagadkowych operatorów rekurencji typu ?, + czy *). W XML Schema wykorzystuje się atrybuty minOccurs i maxOccurs:
<element name="[Nazwa elementu]"
type="[Typ elementu]"
minOccurs="[Ile razy najmniej może się pojawić]"
maxOccurs="[Ile razy najwięcej może się pojawić]"
>
Jeśli atrybuty te nie zostaną określone, przyjmują domyślną wartość 1 — czyli jeden dozwolony element na definicję. Jeśli maksymalna wartość nie jest określona, wykorzystuje się symbol wieloznaczny. Konstrukcje te pozwalają w prosty sposób narzucić zawężenia związane z rekurencją (przykład 4.14).
Przykład 4.14. Schemat XML z definicjami elementów
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/">
<element name="Ksiazka" type="JavaXML:TypKsiazka" />
<complexType name="TypKsiazka">
<element name="Tytul" type="string" />
<element name="Spis" type="JavaXML:TypSpis" />
<element name="Copyright" type="string" />
</complexType>
<complexType name="TypSpis">
<element name="Rozdzial" maxOccurs="*">
<complexType>
<element name="Naglowek" type="string" minOccurs="0" />
<element name="Temat" maxOccurs="*">
</complexType>
</element>
<element name="PodzialSekcji" minOccurs="0" maxOccurs="*">
<complexType content="empty" />
</element>
</complexType>
</schema>
Zdefiniowaliśmy jeden element główny, Ksiazka, typu TypKsiazka. Element ten zawiera trzy bezpośrednio potomne elementy: Tytul, Spis i Copyright. Z tych trzech dwa są prymitywnymi łańcuchami XML, a trzeci (Spis) to kolejny typ zdefiniowany przez użytkownika, TypSpis. Typ ten zawiera z kolei element potomny Rozdzial, który może pojawić się raz lub więcej razy, oraz element potomny PodzialSekcji, który wcale nie musi się pojawić. Element Rozdzial zawiera dwa elementy zagnieżdżone, Naglowek i Temat. Każdy z nich jest prymitywnym łańcuchem XML; Naglowek może pojawić się zero lub więcej razy, a Temat jeden lub więcej razy. Element PodzialSekcji może wystąpić zero lub więcej razy i jest elementem pustym. Teraz w schemacie zdefiniowane są już wszystkie elementy — pozostaje dodać atrybuty.
Definiowanie atrybutów
Proces definiowania atrybutów jest o wiele prostszy od definiowania elementów, przede wszystkim dlatego, że nie trzeba zwracać uwagi na tyle spraw, co w przypadku elementów. Domyślnie atrybut nie musi występować, a zagnieżdżanie w ogóle atrybutów nie dotyczy. Choć istnieje wiele zaawansowanych konstrukcji, których można użyć do zawężania atrybutów, my przyjrzymy się tylko tym podstawowym, koniecznym do zawężenia omawianego dokumentu XML. W razie potrzeby zastosowania tych trudniejszych konstrukcji Czytelnik powinien zajrzeć do specyfikacji XML Schema.
Czego brakuje?
Przy zawężaniu atrybutów na potrzeby dokumentu XML wchodzą w grę pewne pominięcia; wszystkie mają związek z różnymi definicjami przestrzeni nazw opisywanego dokumentu. Dokument XML, jak to już było powiedziane, musi zawierać definicje przestrzeni nazw w odniesieniu do schematu oraz definicje przestrzeni nazw „na własne potrzeby” — tj. odnoszące się do jego zawartości. Definicje te tworzy się za pomocą atrybutu xmlns:[PrzestrzenNazw] elementu głównego. Atrybuty te nie powinny być definiowane w schemacie. Próba zdefiniowania każdej dozwolonej przestrzeni nazw dałaby bardzo zagmatwany schemat. Położenie deklaracji przestrzeni nazw nie musi być stałe; można ją przemieszczać, o ile tylko pozostaje dostępna dla wszystkich odpowiednich elementów. Dlatego w schemacie zezwala się na pominięcie definicji atrybutów przestrzeni nazw.
Pora przypomnieć sobie wiadomości dotyczące definicji DTD. Aby zezwolić na deklaracje przestrzeni nazw w dokumencie XML, trzeba było wstawić taką definicję atrybutu jak poniżej:
<!ATTLIST JavaXML:Ksiazka
xmlns:JavaXML CDATA #REQUIRED
>
Aby można było korzystać z DTD, wystarczyło określić przestrzeń nazw w danym dokumencie XML, bo definicje DTD nie posiadają „świadomości” istnienia przestrzeni nazw w XML. W schematach XML Schema sprawa jest trochę bardziej skomplikowana.
Czytelnik wie już, że istnieją trzy różne atrybuty służące do określenia schematu dla dokumentu:
<?xml version="1.0"?>
<ksiazkaAdresowa xmlns:xsi="http://www.w3.org/1999/XMLSchema/instance"
xmlns="http://www.oreilly.com/catalog/javaxml/"
xsi:schemaLocation="http://www.oreilly.com/catalog/javaxml/
mySchema.xsd"
Gdyby Czytelnik miał napisać schemat w oparciu o poznane dotąd wiadomości o DTD, prawdopodobnie zadeklarowałby, że xmlns:xsi, xmlns i xsi:schema-Location to poprawne atrybuty elementu głównego. Jednakże deklaracje te można pominąć, ponieważ schematy są „świadome” istnienia przestrzeni nazw i wystarczająco „inteligentne”, żeby nie wymagać wstawiania takich deklaracji w zawężanym dokumencie XML.
Definicja
Atrybuty definiuje się za pomocą elementu XML Schema attribute (ciekawe, prawda?). Innymi słowy, oprócz elementu element, w schematach zdefiniowany jest element attribute, za pomocą którego określa się atrybuty dozwolone dla bieżącej definicji elementu lub typu. Oto format tego elementu:
<attribute nazwa="[Nazwa atrybutu]"
typ="[Typ atrybutu]"
[Opcje atrybutu]
>
Format ten jest podobny do definicji elementów — właściwie jest niemal identyczny. Przy definiowaniu atrybutów dostępne są te same typy danych, co przy definiowaniu elementów. Oznacza to, że dodanie atrybutów do naszego schematu nie będzie przedstawiało trudności. Dla każdego elementu posiadającego zdefiniowany typ dodamy wymagane atrybuty wewnątrz definicji tego typu. W przypadku elementów nie posiadających zdefiniowanego typu musimy typ taki dodać. W ten sposób „poinformujemy” schemat, że atrybuty, które deklarujemy, „należą” do opisywanego typu elementu. W tych nowych typach elementów można określić typ zawartości za pomocą atrybutu content elementu complexType — zachowując oryginalne zawężenia i dodając definicje atrybutów. Po tych zmianach uzyskamy schemat taki jak w przykładzie 4.15.
Przykład 4.15. Schemat XML z definicjami atrybutów
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/">
<element name="Ksiazka" type="JavaXML:TypKsiazka" />
<complexType name="TypKsiazka">
<element name="Tytul" type="string" />
<element name="Spis" type="JavaXML:TypSpis" />
<element name="Copyright" type="string" />
</complexType>
<complexType name="TypSpis">
<element name="Rozdzial" maxOccurs="*">
<complexType>
<element name="Naglowek" type="string" minOccurs="0" />
<element name="Temat" maxOccurs="*">
<complexType content="string">
<attribute name="podRozdzialy" type="integer" />
</complexType>
</element>
<attribute name="tematyka" type="string" />
</complexType>
</element>
<element name="PodzialSekcji" minOccurs="0" maxOccurs="*">
<complexType content="empty" />
</element>
</complexType>
</schema>
Po przyjrzeniu się elementowi Temat Czytelnik z pewnością zauważy, że na potrzeby zdefiniowania atrybutu podRozdzialy konieczne jest stworzenie nowego typu. Wewnątrz tego typu, za pomocą atrybutu content, wymagamy, aby atrybut był typu integer. Z tego samego mechanizmu korzystaliśmy wcześniej, przy przypisywaniu elementowi PodzialSekcji typu empty — w ten sposób zapewnialiśmy, że element ten pozostanie pusty. Pozostałe dodane atrybuty nie wymagają już tak wielu modyfikacji, bo odpowiednie typy dla złożonych elementów już istniały.
Atrybuty wymagane, wartości domyślne i wyliczenia
Pozostaje już tylko „dostroić” definicje atrybutów. W definicjach DTD, w celu określenia, czy atrybuty mają się pojawić oraz czy posiadają wartości domyślne (jeśli ich nie podano), używaliśmy słów kluczowych #IMPLIED, #FIXED i #REQUIRED. Podobnie jak w przypadku operatorów rekurencji w elementach, w schematach zdefiniowano prostszy sposób zapisu tego samego. Jeśli atrybut jest wymagany, wykorzystuje się ten sam atrybut minOccurs, który używany był przy definiowaniu elementów, przypisując mu wartość 1. W naszym przykładzie chcemy zdefiniować, że w elemencie Rozdzial istnieje atrybut o nazwie sekcja. Użyjemy takiego zapisu:
<attribute name="sekcja" type="string" minOccurs="1" />
Jak wspomnieliśmy, domyślnie każdy element wymagany jest w jednym egzemplarzu (domyślna wartość minOccurs to 1). Atrybuty natomiast nie są wymagane (minOccurs ma wartość domyślną 0).
W XML Schema nie istnieje pojęcie wartości stałej atrybutów (#FIXED); jak to zostało powiedziane wcześniej, wartość ta jest wykorzystywana rzadko i nie stanowi zapisu „intuicyjnego”. Przydaje się za to możliwość określenia wartości domyślnej i służy do tego atrybut default. Na przykład, aby określić domyślną wartość atrybutu tematyka elementu Rozdzial jako „Java”, skorzystamy z zapisu:
<attribute nazwa="tematyka" type="string" default="Java" />
Mam nadzieję, że Czytelnik zdążył już polubić prostotę XML Schema! Jest to standard bardziej intuicyjny i przez to prostszy niż DTD. Kolejnym dowodem na to są wyliczenia.
Dla atrybutu tematyka określiliśmy za pomocą DTD tylko dwie możliwe wartości: Java i XML. Skorzystaliśmy z operatora LUB (OR):
<!ATTLIST JavaXML:Rozdzial
tematyka (XML|Java) "Java"
>
Nie jest to może zapis trudny, ale nie jest też najbardziej intuicyjny. Dozwolone wartości nie są nawet umieszczane w cudzysłowach — a to jest właśnie de facto standard reprezentacji wartości. W XML Schema, aby uzyskać to samo, trzeba napisać więcej — ale za to zrozumienie zapisu jest o wiele prostsze. Należy otworzyć definicję atrybutu, po czym skorzystać z elementu simpleType. Element ten umożliwia zawężenie istniejącego typu danych (np. string) do podanych wartości. W tym przypadku zależy nam na podaniu dwóch wartości (wyliczenia) i każdą z nich określimy za pomocą elementu enumeration. Typ bazowy elementu określimy za pomocą słowa kluczowego base. Zmieńmy więc definicję atrybutu tematyka:
<attribute name="tematyka" default="Java">
<simpleType base="string">
<enumeration value="XML" />
<enumeration value="Java" />
</simpleType>
</attribute>
Tak, tekstu jest więcej niż w przypadku bliźniaczej konstrukcji w DTD, a jednak powyższy zapis jest prostszy w zrozumieniu, szczególnie dla nowych użytkowników standardu XML. Po wprowadzeniu tej zmiany nasz schemat jest już gotowy; zawarto w nim wszystkie zawężenia, które są zamieszczone w stworzonej wcześniej definicji DTD. Spójrzmy na przykład 4.16.
Przykład 4.16. Gotowy dokument XML Schema
<?xml version="1.0"?>
<schema targetNamespace="http://www.oreilly.com/catalog/javaxml/"
xmlns="http://www.w3.org/1999/XMLSchema"
xmlns:JavaXML="http://www.oreilly.com/catalog/javaxml/">
<element name="Ksiazka" type="JavaXML:TypKsiazka" />
<complexType name="TypKsiazka">
<element name="Tytul" type="string" />
<element name="Spis" type="JavaXML:TypSpis" />
<element name="Copyright" type="string" />
</complexType>
<complexType name="TypSpis">
<element name="Rozdzial" maxOccurs="*">
<complexType>
<element name="Naglowek" type="string" minOccurs="0" />
<element name="Temat" maxOccurs="*">
<complexType content="string">
<attribute name="podRozdzialy" type="integer" />
</complexType>
</element>
<attribute name="tematyka" default="Java">
<simpleType base="string">
<enumeration value="XML" />
<enumeration value="Java" />
</simpleType>
</attribute>
</complexType>
</element>
<element name="PodzialSekcji" minOccurs="0" maxOccurs="*">
<complexType content="empty" />
</element>
</complexType>
</schema>
Co dalej?
W niniejszym rozdziale Czytelnik poznał dwa sposoby zawężania dokumentów XML: zawężanie z wykorzystaniem definicji DTD oraz sposób nowszy — zawężanie z wykorzystaniem XML Schema. Czytelnik zapewne dostrzegł już, jak ważne jest zawężanie dokumentów, szczególnie jeśli mają one być wykorzystane przez aplikacje. Jeśli aplikacja nie rozpoznaje typu informacji zawartych w dokumencie, manipulacja i przetwarzanie takich danych stają się o wiele trudniejsze. W następnym rozdziale zostaną omówione klasy interfejsu SAX. Czytelnik dowie się, jak z programu w Javie uzyskać dostęp do definicji DTD oraz schematów. Do parsera dodamy przykładowy program, który zbudowaliśmy w rozdziale 3. — będzie on odczytywał zawężenia dokumentu i zwracał błędy, jeśli dokument XML nie będzie poprawny; będzie również wykorzystywał odwołania wsteczne dostępne w procesie sprawdzania poprawności składni.
116 Rozdział 4. Zawężanie danych XML
XML Schema 115
C:\Helion\Java i XML\jAVA I xml\04-08.doc — strona 116
C:\Helion\Java i XML\jAVA I xml\04-08.doc — strona 115
C:\Helion\Java i XML\jAVA I xml\04-08.doc — strona 89
W.D.: Bold powinien być w linii xmlns (39) i w linii podRozdziały CDATA (52).
W.D.: jw.