XML und .NET - Software and Systems Engineering

Transcrição

XML und .NET - Software and Systems Engineering
XML und .NET
Tuan Duc Nguyen
[email protected]
Abstract: XML ist heutzutage eine sehr verbreitete Technologie, um Daten zu beschreiben, zu speichern und im Internet zu transportieren. Daher gibt es in der .NET
Framework drei unterschiedliche Möglichkeiten mit XML-Dateien zu arbeiten. Hier
werden wir über die Vor- und Nachteile dieser Methoden diskutieren, um herauszufinden für welche praktische Situation welche Methode am besten geeignet ist.
1
1.1
Einleitung
Überblick über XML
XML steht für Extensible Markup Language, eine vereinfachte Version vom Standard General Markup Language. Das Ziel von XML ist eine einfache Methode für Beschreibung,
Sicherung und Transportierung von Dateien zwischen verschiedenen Systemen und Applikationen zu entwickeln.
Im Jahr 1998 wurde die erste Spezifikation von XML von W3C veröffentlicht. Heute ist
XML das wichtigste Datei-Format, nicht nur im Desktop-Bereich sondern auch im Internet. Ein Grund für die schnelle Verbreitung von XML ist die Fähigkeit, Informationen über
Informationen zu geben. Mit anderen Worten können XML-Daten sich selbst beschreiben.
Unten ist ein Beispiel einer einfachen XML-Datei:
<?xml version="1.0" encoding="UTF-8" ?>
<book>
<title>C# 4.0 in a Nutshell</title>
<author>Joseph Albahari</author>
<isbn>978-0596800956</isbn>
<price currency="EURO">34,95</price>
</book>
Aus dem Beispiel können wir sofort erkennen, worum es sich bei dieser Datei handelt.
Mit Hilfe der Auszeichner (Tag) wie book, title, author, usw. erkennen wir die
Bedeutung der einzelnen Informationen. Die Schachtelung dieser Auszeichner beschreibt
die Strukturen und Beziehungen zwischen diesen Informationen.
XML definiert aber keine feste Anzahl von Auszeichnern. Je nach Bedarf können mehr
oder weniger Auszeichner deklariert werden. Das ist der größte Vorteil von XML: Erwei-
terungsfähigkeit. Man kann daher XML nutzen, um fast alle Arten von Daten zu beschreiben und zu speichern.
Eigentlich ist XML jedoch nur einfacher Text. Das bedeutet, man kann ein bestehendes
XML-Dokument einfach lesen und untersuchen, wie die Daten in dem Dokument strukturiert und definiert sind. Danach ist es möglich, die gewünschten Informationen aus der
XML-Datei zu extrahieren.
Seit der ersten Version wurde XML von vielen Softwares und Systemen genutzt um Daten zu speichern. Eine große Menge von Daten wird auch im Internet als XML-Format
transportiert. Fast jede Software-Plattform unterstützt das Lesen und Schreiben von XML.
1.2
XML-Klassen in .NET
Die .NET-Plattform von Microsoft hat in der aktuellen Version 4.0 drei unterschiedliche
Methoden um XML-Dateien zu bearbeiten: Die leichtgewichtige XmlReader/XmlWriter,
das standardisierte DOM (Document Object Model) und die neue LINQ to XML. In den
folgenden Kapiteln werden wir diese drei Möglichkeiten genauer betrachten.
Schon seit der ersten Version von .NET gibt es 2 Familien von Klassen, die das Lesen und
Schreiben von XML-Datei in der .NET-Umgebung realisieren: XmlReader/XmlWriter
und DOM. Mit .NET 3.5 wurde LINQ to XML zusammen mit LINQ eingeführt und
als empfohlene Methode vorgestellt, denn LINQ to XML hat nicht nur ein einfaches
Programmier-Konzept wie DOM, sondern auch eine akzeptable Performanz im Vergleich
zu XmlReader.
Die Abbildung 1 zeigt einen Überblick über die Hierarchie von XML-Klassen in der .NETPlattform.
2
XmlReader
XmlReader bildet die Grundlage für alle Lese-Aktivitäten in der .NET-Plattform und
definiert den grundlegenden Leistungsumfang anderer abgeleiteten Klassen.
Im Vergleich zu den zwei anderen Möglichkeiten in .NET auf XML-Dateien zuzugreifen,
sind die wesentlichen Vorteile von XmlReader seine Einfachheit, seine gute Performanz
und sein geringer Speicherbedarf. Natürlich hat XmlReader auch seine eigenen Nachteile.
2.1
Pull-Model vs. Push-Model
XmlReader implementiert den Pull-Model-Parser, während einige sehr populäre APIBibliotheke wie zum Beispiel SAX (Simple API for XML) in Java das Push-Model ver-
XmlReader
System.XML
XmlWriter
XmlDocument
XmlNode
XmlAttribute
XmlCharacterData
XmlLinkedNode
XmlElement
XDocument
XNode
XContainer
System.XML.Linq
(ab .NET 3.5)
XElement
XAttribute
Abbildung 1: Struktur der XML-Klassen in .NET
wenden.
Der Push-Parser liest die Datei und löst beim Auftreten eines bestimmten Elements des
XML-Dokuments, zum Beispiel eines Auszeichners oder eines Kommentares, das entsprechende Event aus. Die Applikationen müssen dauerhaft und passiv auf Events des Parsers
warten und die Callback-Funktion aufrufen. Die Applikationen haben daher keine Kontrolle über den Ablauf des Lesens und müssen für die Bearbeitung der XML-Datei eine
kleine Zustandsmaschine aufbauen, um den aktuellen Zustand des Lesens zu verwalten.
Im Gegensatz zum Push-Model löst der Pull-Parser kein Event aus. Stattdessen hält er
selbst die aktuellen Informationen über den Ablauf des Lesens der XML-Datei. Die Applikationen fragen den Parser aktiv nach seinem aktuellen Status ab und können dann
selbst entscheiden, ob der Parser das momentan gelesene Element detaillierter untersuchen
oder zum nächsten Knoten überspringen sollte. Eine Zustandsmaschine und einen EventHandler aufzubauen ist daher unnötig für einen Pull-Parser. Ein Parser mit Pull-Model ist
aus diesem Grund für die meisten Entwickler angenehmer als ein mit Push-Model.
2.2
Parsen von XML-Dateien mit XmlReader
XmlReader ist ein Pull-Parser, das bedeutet XmlReader bietet eigene Methoden und
Eigenschaften, um das aktuelle Element des Dokuments mitzuteilen. Die zwei wichtigsten Eigenschaften von XmlReader sind Name und NodeType, welche den Titel und
den Knotentyp des aktuellen Elements zurückliefern. Es gibt verschiedene Knotentypen,
die in der Aufzählung XmlNodeType definiert werden. Die am häufigsten betrachteten
Knotentypen sind Element, EndElement, Text und Comment.
Mit den Eigenschaften Name und NodeType kann man feststellen, ob das aktuelle Element des Dokuments die relevante Information enthält. Der Inhalt dieses Elements kann
dann mit Hilfe der Eigenschaft Value als Zeichenketten zurückgeliefert werden. Diese
Eigenschaft ist aber nur für Text-Knoten, Kommentare oder Attribute verfügbar. Für Knoten aus anderen Typen bietet XmlReader die Methode ReadElementContentAsXXXX()
oder ReadContentAsXXXX() um den Inhalt des Knotens zu lesen.
Falls das momentane Element für die Applikation uninteressant ist, kann XmlReader mit
Hilfe der Methoden Skip() oder Read() den aktuellen Knoten überspringen. Die Methoden ReadToFollowing(string nodeName), ReadToDescendant(string
nodeName) oder ReadToNextSibling(string nodeName) können den LeserCursor zum nächsten Knoten mit dem gegebenen Name verschieben.
Leider ist Navigation mit XmlReader nur vorwärts möglich. XmlReader ist daher nur
geeignet für die Situation, in der man die XML-Datei nur einmal durchlesen sollte. Ein
weiterer Nachteil von XmlReader ist das Fehlen von der Fähigkeit, ein bestehendes Dokument zu manipulieren.
2.3
Beispiel
Folgendes ist ein Beispiel, das demonstriert, wie man Informationen in einer XML-Datei
mit Hilfe XmlReader einlesen kann.
Die Test-Datei Book.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<book>
<title>C# 4.0 in a Nutshell</title>
<author>Joseph Albahari</author>
<isbn>978-0596800956</isbn>
<!-- Some comment -->
<price currency="EURO">34,95</price>
</book>
Der Code-Abschnitt:
XmlReader reader = XmlReader.Create("book.xml");
while (reader.Read()) {
switch (reader.NodeType) {
case XmlNodeType.Element:
if (reader.Name == "title") {
reader.ReadStartElement();
Console.WriteLine("title: " + reader.ReadContentAsString());
}
if (reader.Name == "price") {
if (reader.MoveToAttribute("currency"))
Console.WriteLine(reader.Name + " = " + reader.Value);
reader.ReadStartElement();
Console.WriteLine("price: " + reader.ReadContentAsFloat());
}
break;
case XmlNodeType.Comment:
Console.WriteLine("comment: " + reader.Value);
break;
}
}
3
Das Document Object Model (DOM)
Eine weitere Möglichkeit, XML-Datei in .NET zu bearbeiten, ist DOM. Im Vergleich
zu XmlReader hat DOM schlechtere Performanz und hohen Speicherbedarf. Trotzdem
ermöglicht DOM beliebige Navigation in der Baum-Struktur des Dokuments und kleine
Veränderungen der bestehenden Daten, welche unmöglich bei XmlReader sind.
3.1
Was ist DOM
DOM (Document Object Model) ist eine von W3C definierte Schnittstelle, die beschreibt,
wie man ein XML-Dokument einlesen, bearbeiten und wieder speichern sollte. Die Idee
von DOM ist es, ein XML-Dokument im Zwischenspeicher als Baum von Objekten verschiedener Klassen zu modellieren. Wenn wir mit diesen Baum-Strukturen und Objekten arbeiten, verändern wir auch die Strukturen und den Inhalt des Dokuments. DOM
in .NET wird durch die Klasse XmlDocument und ihre Familie unter dem Namespace
System.Xml implementiert.
Da DOM eine Schnittstelle ist, ist die Standardisierung der Implementierung von DOM
in verschiedenen Programmiersprachen ein Vorteil. Wenn man DOM-API schon in einer
Entwicklerplattform kennt, kann man sich schnell an die Implementierung vom DOM in
einer anderen Sprache gewöhnen.
3.2
XML-Dateien mit DOM einlesen und arbeiten
Der Kern von DOM in .NET ist die Klasse XmlNode und ihre geerbten Klassen wie
XmlElement, XmlAttribute und XmlDocument.
Um ein bestehendes XML-Dokument in den Speicher zu laden bietet eine Instanz der
Klasse XmlDocument die Methode Load(). Diese Methode ist vielfach überladen, da-
mit das Dokument aus einer angegebenen URL, aus einem Stream oder mit Hilfe eines
bestehenden XmlReader-Objekts gelesen werden kann. Falls die XML-Daten bereits in
der Form eines Strings sind, kann die Methode LoadXml(string xml) verwendet
werden. Nach dem Laden des Dokuments kann das Wurzelelement mit Hilfe der Eigenschaft DocumentElement abgerufen werden.
XmlNode ist die wichtigste Klasse des DOM, denn sie bietet die nötige Methode, um
im DOM-Baum zu navigieren, um nach einem Knoten zu suchen oder um die Struktur des Baumes zu ändern. Die Navigation im XML-Baum wird von den Eigenschaften
FirstChild, LastChild, NextSibling, PreviousSibling und ParentNode
ermöglicht. Die Eigenschaft ChildNodes einer XmlNode-Instanz liefert die Menge aller untergeordneten Knoten zurück. Die Eigenschaften Name und NodeType von XmlNode
haben die gleiche Funktionalität wie die von XmlReader.
Um nach einem Element mit dem gegebenen Auszeichner zu suchen bieten die Klassen
XmlDocument und XmlElement die Methode GetElementsByTagName(string
nodeName). Außerdem ist es auch möglich, Knoten mit Hilfe eines XPath-Ausdrucks
zu lokalisieren. Diese Funktionalität wird in der Klasse XmlNode von den Methoden
SelectSingleNode(string xpathAusdruck) und SelectNodes(string
xpathAusdruck) implementiert. XPath ist ein mächtiges Werkzeug, um nach einem
Knoten mit gegebenen Eigenschaften zu suchen. Trotzdem fordert XPath auch großen Rechenaufwand wie wir später im Abschnitt über Performanz zeigen werden. XPath sollte
daher nicht übermäßig gebraucht werden.
Nachdem wir die relevanten Knoten erhalten, kann der Inhalt des Knotens mit Hilfe von
den Eigenschaften Value oder InnerText als Zeichenketten zurückgeliefert oder festgelegt werden. Der XML-Code lässt sich einfach verändern, indem man die Eigenschaft
InnerXml einstellt.
Wenn ein neues Element des Dokuments erzeugt werden soll, stehen die Methode CreateAttribute(),
CreateComment(), CreateElement() oder CreateTextNode() einer XmlDocumentInstanz zur Verfügung. Der neu erzeugte Knoten kann danach mit Hilfe der Methoden
AppendChild(), PrependChild() oder ReplaceChild() zu einem bestehenden Knoten hinzugefügt werden. Die Methoden RemoveChild() und RemoveAll()
ermöglichen dagegen das Entfernen von einem bzw. allen Kindknoten eines Knotens.
3.3
Beispiel
Unten ist ein Code-Abschnitt, der das Lesen eines XML-Dokuments mit Hilfe von DOM
in .NET beschreibt. In diesem Beispiel wird XPath verwendet um die Knoten mit relevanten Informationen schnell zu finden. Die Test-Datei ist dieselbe Book.xml wie im
Beispiel mit XmlReader.
XmlDocument doc = new XmlDocument();
doc.Load("book.xml");
Console.WriteLine(doc.SelectSingleNode("book/title").InnerText);
Console.WriteLine(doc.SelectSingleNode("book/price/@currency").Value);
float price = float.Parse(doc.SelectSingleNode("book/price"));
4
LINQ to XML
Zusammen mit der Version 3.5 von .NET hat Microsoft die neue Technologie LINQ vorgestellt. Um die Vorteile von LINQ auch in der XML-Bearbeitung zu nutzen wurde eine
neue Familie von Klassen unter dem Namespace System.Xml.Linq entwickelt, die ein
ähnliches Konzept wie DOM implementieren. Diese Klassen sind nicht nur LINQ freundlich sondern wurden auch sehr gut optimiert und sie liefern bessere Performanz als die
alten DOM-Klassen.
4.1
Vorteil zum alten DOM
LINQ to XML hat im Vergleich zum DOM ein ähnliches Konzept. Für die Bearbeitung
von XML-Daten mit LINQ to XML wird eine Hierarchie von Objekten aus verschiedenen LINQ to XML-Klassen auch im Zwischenspeicher aufgebaut. LINQ to XML bietet
trotzdem einige stärke Vorteile zum DOM.
Der erste Vorteil ist die wesentlich bessere Performanz. LINQ to XML wurde neu entwickelt und muss den DOM-Standard nicht entsprechen, deshalb kann sie sehr intensiv
optimiert werden. Einige veraltete oder uninteressante Komponenten des DOM-Konzepts
wurden nicht in LINQ to XML eingeplant. Außerdem ist es mit LINQ to XML möglich,
nur mit einem Teil des Dokuments zu arbeiten, deshalb ist der Speicherbedarf und der
Rechenaufwand von LINQ to XML geringer als von DOM.
Eine weitere Stärke von LINQ to XML ist die Möglichkeit, einfacheren Code zu schreiben. Die Klassen von LINQ to XML implementieren einige neue Programmier-Konzepte
und ermöglichen eine sauberere Codierung und einfachere Test- und Debuggen-Verfahren.
Daraus folgt eine bessere Produktivität beim Entwicklungsprozess.
Obwohl die Klassen von LINQ to XML auch ohne LINQ verwendet werden können, ist
der größte Vorteil von LINQ to XML die Möglichkeit, LINQ-Abfrage direkt mit XMLDaten zu nutzen. Diese Abfragefunktion von LINQ to XML ist von ihrer Funktionalität
her mit der von XPath und XQuery vergleichbar, die Syntax ist allerdings eine andere.
Die Integration von LINQ mit dem Entwicklerwerkzeug Visual Studio bietet eine stärkere Typisierung, Syntaxüberprüfung bei der Kompilierung und verbesserte Debuggerunterstützung.
4.2
Bearbeitung von XML-Datei mit LINQ to XML
In LINQ to XML kann nicht nur die Klasse XDocument sondern auch die Klasse XElement
eines XML-Dokuments zum Bearbeiten verwendet werden. Die beiden Klassen verfügen
über die mehrfach übergeladene Methode Load(), die das Lesen eines Dokuments aus
einer URL, aus einem Stream oder aus einem XmlReader-Objekt realisiert. Im Gegensatz zu DOM ist es in LINQ to XML nicht verpflichtet, eine XML-Datei immer mit
XDocument zu laden, denn XElement ist allein für die Bearbeitung von XML-Dateien
ausreichend.
Zur Navigation im Dokument-Baum bieten XNode und ihre abgeleiteten Klassen die Eigenschaften NextNode, PreviousNode und Parent. Außerdem verfügen die Klassen XElement und XDocument über einige Methoden, die die Suche nach einem oder
mehreren untergeordneten Knoten mit einem gegebenen Namen ermöglichen: Descendants
(XName name) und Elements(XName name). Der Inhalt des Knotens kann danach
einfach mit “Type-Casting” erhalten werden. Dieses Design der LINQ to XML-Klassen
ermöglicht die Verwendung von LINQ-Abfragen.
Folgende ist Beispiel einer LINQ-Abfrage, die den Titel aller Bücher zurückliefert, deren
Preis kleiner oder gleich 10 ist:
var buecher = XElement.Load("buecher.xml");
var billige_buecher =
from buch in buecher.Descendants("buch")
where (float)buch.Element("preis") <= 10
select (string)buch.Element("titel");
4.3
LINQ to XML mit riesiger Datei
Ein wichtiger Vorteil von LINQ to XML zum DOM ist die Möglichkeit, ein großes Dokument nur mit geringer Speicherbeanspruchung zu bearbeiten, indem man das Dokument mit XmlReader liest und teilweise mit LINQ to XML verarbeitet. Die statische
Methode ReadFrom(XmlReader reader) der abstrakten Klasse XNode kann das
momentan von XmlReader gelesene Element aus dem Dokument extrahieren und ein
XNode-Objekt davon aufbauen. Diese Fähigkeit ist nicht verfügbar in DOM.
Unten ist der Beispiel-Code mit dem normalen DOM-Ansatz:
XDocument doc = XDocument.Load("beispiel.xml");
// Hier wird das gesamte Dokument in den Zwischenspeicher geladen.
Foreach (XElement e in doc.Root.Elements("item"))
ItemBearbeiten(e);
Die Datei beispiel.xml kann wegen einer großen Anzahl von den Kind-Elementen
item so riesig sein, dass der Zwischenspeicher nicht ausreichen kann, um den gesamten
DOM-Baum zu lagern. Hier ist es empfehlenswert, die Datei als Stream mit XmlReader
zu lesen und jedes einzelnes item-Element mit LINQ to XML zu bearbeiten.
XmlReader reader = XmlReader.Create("beispiel.xml");
while (reader.Read())
if (reader.Name == "item" &&
reader.NodeType == XmlNodeType.Element) {
XElement e = XNode.ReadFrom(reader);
// Hier wird nur das momentan gelesene "item"-Element geladen.
ItemBearbeiten(e);
};
5
5.1
Vergleichen von Performanz
Implementierung des Tests
Der Test lässt sich einfach konstruieren. Die Test-Datei wird automatisch generiert und hat
einen Wurzelknoten root mit verschiedenen Kindknoten child. Die Kindknoten haben
keinen Inhalt, sondern nur eine zufällig erzeugte Zeichenkette als id-Attribut. Verschiedene Methoden werden dann genutzt, um dieses id-Attribut aller Kindknoten zu lesen.
Für jede Methode wird der Test 100-mal durchgeführt und die durchschnittliche Laufzeit
wird in Millisekunden zurückgegeben. Das Test-Ergebnis wird in der Tabelle 1 angezeigt.
Unten ist ein Beispiel der Testdatei mit zwei Kindknoten:
<root>
<child id=’1259287450’/>
<child id=’2334165534’/>
</root>
5.2
Das Test-Ergebnis
Vom Test-Ergebnis können wir ausgehen, dass XmlReader die beste Performanz hat und
DOM die schlechteste Performanz hat. Ein Grund dafür ist, bei DOM und LINQ to XML
muss die gesamte Datei erst fertig eingelesen und in den Zwischenspeicher gelegt werden.
Außerdem lässt sich auch erkennen, dass Suchen und Navigation zu einem bestimmten
Knoten bei der Bearbeitung eines XML-Dokuments sehr teuer ist, insbesondere wenn man
XPath verwendet.
Mit der Zunahme der Größe der Test-Datei geht der Zuwachs der Laufzeit der Varianten
mit DOM und mit LINQ to XML schneller einher als mit XmlReader.
Hier wird die Memory-Belastung nicht abgeschätzt, aber trotzdem lässt sich leicht erkennen, dass DOM und LINQ to XML für große Dokumente sehr viel Zwischenspeicher
verbrauchen.
Anzahl von Kindknoten
XmlReader
XmlDocument
XmlDocument mit XPath
XDocument
XDocument mit Navigation
1
0,074830
0,095433
0,111558
0,089083
0,125736
10
0,071740
0,104943
0,104753
0,082286
0,096984
100
0,130494
0,208655
0,230719
0,178986
0,171954
1.000
0,590924
1,316731
1,477319
0,937331
1,047099
10.000
6,324870
13,803712
15,511574
10,170527
10,232822
100.000
57,057659
389,614392
421,267870
156,225456
159,162281
Tabelle 1: Laufzeit verschiedener Methoden in Millisekunde
6
Zusammenfassung
Die .NET-Plattform von Microsoft bietet eine große Unterstützung um mit XML-Dateien
zu arbeiten. XmlReader und XmlWriter sind sehr effektiv für große Dateien oder für
Dateien, deren Strukturen schon vorher bekannt sind. Wenn Performanz und Speichereffizienz eine große Rolle spielen, sollten wir uns für XmlReader und XmlWriter entscheiden. Man muss trotzdem damit rechnen, den Code oft komplizierter schreiben zu
müssen und die Applikationen schwieriger zu debuggen und zu testen.
Im Gegensatz zu XmlReader ist LINQ to XML sehr gut für kleine Dateien oder Dateien mit komplexeren Strukturen geeignet. Diese Methode hilft auch, die Produktivität zu
erhöhen, denn das Schreiben von Code ist mit LINQ to XML sehr leicht und überschaubar.
Es wird häufig eine Kombination von LINQ to XML und XmlReader verwendet werden.
XmlReader wird zuerst genutzt, um die Datei-Ströme einzulesen und die XML-Stücke
werden dann teilweise mit LINQ to XML manipuliert.
DOM hat die gleichen Vor- und Nachteile wie LINQ to XML, trotzdem ist die Performanz
von DOM am schlechtesten. LINQ to XML und XmlReader/XmlWriter bieten außerdem genügend Werkzeug, um effizient mit XML-Dateien zu arbeiten. Daher ist es nicht
empfehlenswert DOM weiter zu verwenden. Allerdings kann man DOM auch weiter nutzen, wenn Kompatibilitätsprobleme mit älterer Version der .NET-Plattform vorliegen oder
wenn nicht alle Mitglieder des Entwicklerteams die andere Alternative kennen.
Literaturverzeichnis
1. Joseph, A., Ben, A., C# 4.0 in a Nutshell, O‘Reilly, 2010.
2. David, H., Jeff, R., Joe, F., Eric, V., Danny, A., Jon, D., Andrew, W., Linda, M.,
Beginning XML, 4. Edition, Wrox, 2007.
3. MSDN, XML-Dokumente und XML-Daten, http://msdn.microsoft.com/
de-de/library/2bcctyt8(v=VS.100).aspx.
250.000
143,452533
997,281160
1070,537216
584,460105
597,603840
4. MSDN, LINQ to XML, http://msdn.microsoft.com/de-de/library/
bb387098.aspx.
5. Joe Ferner, Performance: LINQ to XML vs XmlDocument vs XmlReader, http:
//www.nearinfinity.com/blogs/joe_ferner/performance_linq_
to_sql_vs.html, Near Infinity‘s blog, 05/2008.
6. James Newton-King, LINQ to XML over large documents, http://james.newtonking.
com/archive/2007/12/11/linq-to-xml-over-large-documents.
aspx, 12/2007.
7. Michael Jervis, DTDs vs XML Schema, http://articles.sitepoint.com/
article/xml-dtds-xml-schema, Sitepoint,11/2002.

Documentos relacionados