9Dateiformate
»Ein Gramm Information wiegt schwerer als tausend Tonnen Meinung.«
– Gerd Bacher (*1925)
Beim Austausch von Dokumenten ist das Dateiformat entscheidend, damit die Darstellung und Wiedergabe optimal gelingt. Bei jedem Dokument – sei es binär oder textuell, eine Grafik oder eine Musikdatei – muss das Format der Zeichen oder Bytes definiert sein, damit das Einlesen und die Verarbeitung möglich sind. Die Definition vom Format ist je nachdem offen beschrieben (wie bei XML, PNG, OpenDocument), proprietär, aber dokumentiert (RTF, DOC) oder auch intern nur den Unternehmen bekannt (wie das Skype-Datenformat).
Einige Textformate sind gleichzeitig Auszeichnungssprachen, denn wenn alles Text ist, muss irgendwie unterschieden werden, was Auszeichnungen sind und was die eigentliche Information ist. Bei XML zum Beispiel stehen die Auszeichnungen in spitzen Klammern. Die bekannteste Auszeichnungssprache ist sicherlich HTML; Akademikern war oder ist TeX vertraut. Mittlerweile sind viele moderne Auszeichnungssprachen XML-basiert. Dazu zählen DocBook für Dokumentationen, Atom für Web-Feeds, ebXML für elektronische Geschäftsprozesse, MathML für mathematische Formeln, OpenDocument und Office Open XML (die jedoch in einem ZIP-Archiv standardmäßig komprimiert werden).
Oft gehen Betriebssysteme über die Dateiendung, um den Typ bzw. das Format einer Datei herauszulesen, oder nutzen spezielle Metadaten bzw. interpretieren die ersten Bytes, um das Format zu erkennen. Wenn Dokumente etwa in einer Datenbank abgelegt oder vom Webserver verschickt werden, haben sie keinen Dateinamen, und so werden zusätzliche Metadaten gespeichert. Im Internet beschreibt der MIME-Typ die Mediendaten. NIO.2 kann versuchen, den MIME-Typ zu ermitteln.
Da die Grundlage jedes Dokuments ein Byte ist, lässt sich natürlich immer ein Bytestrom öffnen und lassen sich die Daten selbst verarbeiten. Wir suchen jedoch eine andere, höhere Abstraktion. Die fängt schon in Java selbst damit an, dass Textdokumente nicht mit InputStream/OutputStream verarbeitet und die Strings dann von Hand in Unicode konvertiert werden, sondern dass ein Reader/Writer die Arbeit verrichtet. Wenn wir es mit XML-Dateien zu tun haben, so holen wir uns auch keinen Reader zum Lesen, sondern greifen auf elegante XML-Klassen zurück, die uns die Arbeit beim Lesen der Elemente abnehmen.
Dieses Kapitel stellt unterschiedliche Aspekte mit Dateiformaten und Java heraus. Zum einen werden unterschiedliche Dateien und Dokumentenformate vorgestellt, und zum anderen werden auch Aspekte wie Darstellung, Wiedergabe und Konvertierung angesprochen.
9.1Einfache Dateiformate für strukturierte Daten
Daten sind in der Regel immer strukturiert. Diese Strukturierung bekommen die Daten durch das Format oder eingebettete besondere Symbole bzw. Symbolfolgen. Das kann etwa ein Zeilenumbruchzeichen sein oder ein Token-Separator wie »=« oder XML für hierarchische Dokumente.
9.1.1Property-Dateien mit java.util.Properties lesen und schreiben
Dateien, die Schlüssel-Wert-Paare als String repräsentieren und die Schlüssel und Wert durch ein Gleichheitszeichen trennen, nennen sich Property-Dateien. Sie kommen zur Programmkonfiguration häufig vor, und Java bietet mit der Klasse Properties die Möglichkeit, die Property-Dateien einzulesen und zu schreiben. In Kapitel 4, »Datenstrukturen und Algorithmen«, kam die Klasse mit Beispielen schon zur Sprache.
store() und load()-Methoden vom Properties-Objekt
Die Methode store(…) dient dem Speichern der Zustände und load(…) dem Initialisieren eines Properties-Objekts aus einem Datenstrom. Die Schlüssel und Werte trennt ein Gleichheitszeichen. Die Lade-/Speichermethoden sind:
extends Hashtable<Object,Object>
void store(OutputStream out, String comments)
void store(Writer writer, String comments)
Schreibt die Properties-Liste in den Ausgabestrom. An den Kopf der Datei kann ein Kommentar gesetzt werden, wenn das Argument comments nicht null ist; bei null kommt kein eigener Kommentar.void load(InputStream inStream)
void load(Reader reader) throws IOException
Liest eine Properties-Liste aus einem Eingabestrom.
Ist der Typ ein Binärstrom also OutputStream/InputStream, so behandeln die Methoden die Zeichen in der ISO-8859-1-Kodierung. Reader/Writer erlauben eine freie Kodierung. Eine ähnliche Methode list(…) ist nur für Testausgaben gedacht, sie sollte nicht mit store(…) verwechselt werden.
Das folgende Beispiel initialisiert ein Properties-Objekt mit den Systemeigenschaften und fügt dann einen Wert hinzu. Anschließend macht store(…) die Daten persistent, load(…) liest sie wieder, und list(…) gibt die Eigenschaften auf dem Bildschirm aus:
Listing 9.1com/tutego/insel/util/map/SaveProperties.java, main()
try ( Writer writer = Files.newBufferedWriter( path, StandardCharsets.UTF_8 ) ) {
Properties prop1 = new Properties( System.getProperties() );
prop1.setProperty( "MeinNameIst", "Forrest Gump" );
prop1.store( writer, "Eine Insel mit zwei Bergen" );
try ( Reader reader = Files.newBufferedReader( path, StandardCharsets.UTF_8 ) ) {
Properties prop2 = new Properties();
prop2.load( reader );
prop2.list( System.out );
}
}
catch ( IOException e ) {
e.printStackTrace();
}
Besonderheiten des Formats
Beginnt eine Zeile mit einem »#« oder »!«, gilt sie als Kommentar und wird überlesen. Da der Schlüssel selbst aus einem Gleichheitszeichen bestehen kann, steht in dem Fall ein »\« voran, folglich liefert Properties p = new Properties(); p.setProperty("=", "="); p.store(System.out, null); neben dem Kommentar die Zeile »\==\=«. Beim Einlesen berücksichtigen die Lesemethoden auch Zeilenumbrüche: Eine Zeile darf mit »\« enden, und dann führt die folgende Zeile die vorangehende fort. Die Property »cars« ist also »Honda, Mazda, BMW«, wenn steht:
Honda, Mazda, \
BMW
Properties im XML-Format speichern
Die Properties-Klasse kann die Eigenschaften auch im XML-Format speichern und laden. Dem Speichern dienen zwei überladene Methoden storeToXML(…), und das Laden übernimmt loadFromXML(…). Das XML-Format hat einen festen Aufbau, wie es der Einzeiler System.getProperties().storeToXML(System.out, ""); zeigt:
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment/>
<entry key="java.runtime.name">Java(TM) SE Runtime Environment</entry>
…
<entry key="sun.cpu.isalist">amd64</entry>
</properties>
Die Methode loadFromXML(…) liest aus einem InputStream und löst im Fall eines fehlerhaften Dateiformats eine InvalidPropertiesFormatException aus. Beim Speichern kann so ein Fehler natürlich nicht auftreten. Und genauso, wie bei store(…) ein OutputStream mit einem Kommentar gespeichert wird, macht das auch storeToXML(…). Die Methode ist mit einem zusätzlichen Parameter überladen, der eine XML-Kodierung erlaubt. Ist der Wert nicht gesetzt, so ist die Standardkodierung UTF-8.
extends Hashtable<Object,Object>
void store(OutputStream out, String header)
Speichert die Properties-Liste mithilfe des Ausgabestroms ab. Am Kopf der Datei wird eine Kennung geschrieben, die im zweiten Argument steht. Die Kennung darf null sein.void load(InputStream inStream)
Lädt eine Properties-Liste aus einem Eingabestrom.void storeToXML(OutputStream os, String comment, String encoding) throws IOException
Speichert die Properties im XML-Format. comment kann null sein, wenn ein Kommentar erwünschst ist. encoding steht für die Zeichenkodierung, etwa »Latin-1« oder »UTF-8«.void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException
Liest Properites im XML-Format von einem Eingabestrom ein.
9.1.2CSV-Dateien
Eine CSV-(Comma-separated-Values-)Datei bildet die Zeilen und Spalten einer Tabelle in einer ASCII-Datei ab. Die Zellen sind dabei durch ein Komma oder ein anderes Trennzeichen separiert. Texte können in Anführungszeichen gesetzt werden, um etwa Weißraum zu berücksichtigen.
Bryant,Allen,"Gast auf dem Rücksitz"
Auch Microsoft Excel kann Tabellen in das CSV-Format exportieren, nutzt aber in der deutschen Version als Trenner ein Semikolon – CSV wird bei Microsoft also zu einer sprachabhängigen Datei (außer der Export wird über ein englischsprachiges Makro angestoßen, da ist es wieder ein Komma). In der ersten Zeile stehen die Tabellenköpfe.
Sollten CSV-Dateien in Java verarbeitet werden, fallen einem zum Einlesen spontan die Klassen Scanner und vielleicht StringTokenizer ein, allerdings sind diese zum Einlesen nicht besonders gut geeignet. Welches Trennsymbol sollte gewählt werden? Sicherlich das Semikolon. Doch was passiert, wenn dieses im Text vorkommt? Dann wird der Text in zwei Tokens aufgeteilt – was falsch ist. Des Weiteren kann der Java-StringTokenizer nicht mit Leer-Strings umgehen, also auf Zeilenfolgen wie ;; im Datenstrom reagieren; er würde sie überlesen, aber nicht einen leeren String zurückgeben.
Die Java-Standardbibliothek hilft bei CSV-Daten nicht weiter, wohl aber diverse quelloffene Bibliotheken.
Ostermiller Java Utilities
Eine freie Java-Bibliothek stammt von Stephen Ostermiller unter http://tutego.de/go/ostermillercsv. Mit dem CSVParser lassen sich leicht CSV-Daten einlesen, und er behandelt auch Fluchtsymbole korrekt.
[zB]Beispiel
Lies aus einem Eingabestrom die CSV-Daten, und gib Element für Element auf der Konsole aus:
for ( String s; (s = csvParser.nextValue()) != null; )
System.out.println( csvParser.lastLineNumber() + " " + s );
Jsefa (Java Simple Exchange Format API)
Der Reiz der freien Bibliothek Jsefa (http://jsefa.sourceforge.net/) liegt in der Abbildung von Objekten auf das Datenformat, wobei Annotationen die Daten beschreiben.
[zB]Beispiel
Annotiere eine Person, und Jsefa soll ein Objekt auf das CSV-Format abbilden und zurückkonvertieren. Beispiel aus dem Tutorial[ 88 ](http://jsefa.sourceforge.net/quick-tutorial.html):
public class Person {
@CsvField(pos = 1)
String name;
@CsvField(pos = 2, format = "dd.MM.yyyy")
Date birthDate;
}
Geschrieben wird auf diese Weise:
serializer.open( writer ); // so oft ausführen wie Objekte vorhanden sind
serializer.close( true );
Zum Einlesen Folgendes:
StringReader reader = new StringReader( writer.toString() );
deserializer.open( reader );
while ( deserializer.hasNext() ) {
Person p = deserializer.next();
…
}
deserializer.close( true );
9.1.3JSON-Serialisierung mit Jackson
Im Internet hat JSON das XML-Format zwecks Objektübertragung zwischen Server und Browser fast vollständig verdrängt. Das liegt daran, dass ein Browser JSON-Strings direkt in JavaScript-Objekte konvertieren kann, XML-Dokumente aber erst aufwändiger verarbeitet werden müssen. Neben dem Einsatzgebiet im Internet bietet JSON auch ein kompaktes Format, um etwa lokale Konfigurationsdateien zu kodieren.
Nehmen wir folgende Zeile JavaScript-Code, die ein Person-Objekt mit zwei Properties für Name und Alter definiert. Eine Property wird über ein Schlüssel-Wert-Paar beschrieben:
Die Definition eines Objekts geschieht in der JSON (JavaScript Object Notation). Als Datentypen unterstützt JSON Zahlen, Wahrheitswerte, Strings, Arrays, null und Objekte – wie unser Beispiel zeigt. Die Deklarationen können geschachtelt sein, um Unterobjekte aufzubauen.
Zum Zugriff auf die JSON-Daten kommt der Punkt zum Einsatz, sodass der Name nach der Auswertung durch person.name zugänglich ist.
Eine Personenbeschreibung wie diese kann auch in einem String stehen, der von JavaScript zur Laufzeit ausgewertet wird.
eval( json );
Der Zugriff auf person.name liefert wie vorher den Namen, denn nach der Auswertung mit eval(…) wird JavaScript ein neues Objekt mit person im Kontext anlegen.
JSON ist besonders praktisch, wenn es darum geht, Daten zwischen einem Server und einem Browser mit JavaScript-Interpreter auszutauschen. Denn wenn der String json nicht von Hand mit einem String initialisiert wurde, sondern ein Server die Zeichenkette person = { ... }; liefert, haben wir das, was heutzutage in modernen Ajax-Webanwendungen passiert. Die letzte Frage ist nun, wie elegant der Server Zeichenketten im Datenaustauschformat JSON erzeugt und so Objekte überträgt. Den String per Hand aufzubauen ist eine Lösung, aber es geht besser.
JSON-Verarbeitung mit JSON
Die Open-Source-Bibliothek Jackson (http://tutego.de/go/jackson) gehört zu den populärsten Lösungen. Sie liest JSON-Daten ein, gibt sie aus und überträgt sie auf JavaBeans, sodass eine unkomplizierte Serialisierung in JSON möglich wird:
MyClass myObject = mapper.readValue( input, MyClass.class );
mapper.writeValue( output, myObject );
Der ObjectMapper übernimmt das Lesen/Schreiben. In der zweiten Zeile wird aus der Eingabequelle input gelesen und ein Objekt vom Typ MyClass rekonstruiert. In der dritten Zeile wird es in die Ausgabe output geschrieben.
JSON mit JavaScript-Engine von Java
Praktischerweise liefert jede (aktuelle) Java-Installation eine JavaScript-Engine mit aus, und so lassen sich JSON-Daten auch direkt über die Java-API einlesen. In Kapitel 18, »Dynamische Übersetzung und Skriptsprachen«, kommt das genauer zur Sprache, daher hier nur kurz ein Beispiel:
[zB]Beispiel
Ein String enthält JSON-kodierte Daten. Dieser wird von der JavaScript-Engine eingelesen und dann ein Element erfragt. Es bedarf wenig Fantasie, sich die Daten aus einer Datei vorzustellen und hier die Flexibilität für Konfigurationsdateien zu erkennen:
Listing 9.2com/tutego/insel/json/JavaSciptJSONDemo.java
engine.eval( "person = { 'name' : 'Michael Jackson', 'age' : 2*25 }" );
JSObject obj = (JSObject) engine.get( "person" );
System.out.println( obj.getMember( "age" ) ); // 50