Als ich begonnen habe, Java zu lernen und mich die ersten Mal mit den Klassen auseinandergesetzt habe, die zum Filehandling bereitgestellt wurden, fand ich ein großes Wirrwarr vor und fragte mich, wer bitte ein solches nichtsnutziges Chaos fabriziert hatte, wo in anderen Sprachen kurze, knappe Befehle für die gleiche Aufgabe ausreichten.
Der Aha-Effekt kam erst später, als ich plötzlich feststellte, wie einfach es ist, statt aus einer Datei aus einem String oder einem Datenstrom zu lesen respektive zu schreiben.
Als ich dann "Entwurfsmuster von Kopf bis Fuß" gelesen hatte, wusste ich spätestens, warum die Javaentwickler dieses "Chaos" fabriziert hatten, wie das zugehörige Entwurfsmuster heißt und wozu all das gut ist.
Gerade in der Verquickung mit den Checked Exceptions ist die Verwendung der von Java vorgesehenen Klassen zum Filehandling alles andere als eine Freude. So kommt man über kurz oder lang auf die Idee, diese ganzen weniger schönen Dinge zu kapseln, wie man auch eine fremde API kapseln würde, um sie für die eigenen Programme glatter verwenden zu können. So kann man sogar die verwendete Bibliothek austauschen, ohne Anpassungen an unzähligen Stellen in den verschiedensten Projekten vornehmen zu müssen.
Ein solches Vorgehen hat natürlich nicht nur Vorteile:
Für mich überwiegen hier klar die Vorteile. Im weiteren will ich meine Lösung vorstellen.
Writer |
package de.duehl.basics.io; import java.util.List; import de.duehl.basics.io.exceptions.IORuntimeException; public interface Writer { /** * Schreibt den übergebenen String auf den Writer. * * @param text * Der auszugebende Text. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ void write(String text); /** * Schreibt die übergebene Zahl auf den Writer. * * @param number * Die auszugebende Zahl. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ void write(int number); /** Gibt einen Zeilenumbruch aus. */ void writeln(); /** * Schreibt den übergebenen String auf den Writer und fügt einen * Zeilenumbruch an. * * @param text * Der auszugebende Text. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ void writeln(String text); /** * Schließt den Writer. * * @throws IORuntimeException * Falls beim Schließen ein Fehler auftrat. */ void close(); /** * Veranlasst den Writer alle Daten im Zwischenspeicher in die Datei zu * schreiben. * * @throws IORuntimeException * Falls beim flush ein Fehler auftrat. */ void flush(); /** * Schreibt alle Zeilen der übergebenen Liste auf den Writer und fügt * jeweils einen Zeilenumbruch an. * * @param list * Die Liste mit den auszugebenden Zeilen. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ void writeAllLines(List |
FineFileWriter |
package de.duehl.basics.io; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; import de.duehl.basics.io.exceptions.IORuntimeException; import de.duehl.basics.text.Text; /** * Diese Klasse stellt einen Writer dar, im Falle von Fehlern werden Ausnahmen geworfen. * * @version 1.03 2016-10-14 * @author Christian Dühl */ public class FineFileWriter implements Writer { /** Der Schreiberling. */ private final OutputStreamWriter writer; /** Name der Ausgabedatei. */ private final String fileName; /** Gibt an ob an eine bestehende Datei angehängt wird. */ private final boolean append; /** Zeichensatz der einzulesenden Datei (z.B. "ISO-8859-1", "UTF-8"). */ private final Charset charset; /** * Konstruktor aus einem File. * * @param file * Zu schreibende Datei. * @throws UnsupportedEncodingException * @throws FileNotFoundException */ public FineFileWriter(File file) { this(file.getAbsolutePath(), false); } /** * Konstruktor aus einem Dateinamen. * * @param fileName * Der Name der auszugebenden Datei. * @throws UnsupportedEncodingException * @throws FileNotFoundException */ public FineFileWriter(String fileName) { this(fileName, false); } /** * Konstruktor zum Anlegen oder Anhängen an eine Datei im Format "ISO-8859-1". * * @param fileName * Der Name der auszugebenden Datei. * @param append * Gibt an, ob an die Datei angehängt wird. * @throws IORuntimeException * Wenn die Datei (der Pfad) nicht gefunden wurde oder falls eine unbekannte * Kodierung an den OutputStreamWriter übergeben wurde. */ public FineFileWriter(String fileName, boolean append) { this(fileName, Charset.ISO_8859_1, append); } /** * Konstruktor zum Anlegen einer neuen Datei mit Angabe des Zeichensatz. * * @param fileName * Der Name der auszugebenden Datei. * @param charset * Zeichensatz der zu schreibenden Datei. * @throws IORuntimeException * Wenn die Datei (der Pfad) nicht gefunden wurde oder falls eine unbekannte * Kodierung an den OutputStreamWriter übergeben wurde. */ public FineFileWriter(String fileName, Charset charset) { this(fileName, charset, false); } /** * Konstruktor * * @param file * Der Name der auszugebenden Datei. * @param charset * Zeichensatz der zu schreibenden Datei. * @param append * Gibt an, ob an die Datei angehängt wird. * @throws IORuntimeException * Wenn die Datei (der Pfad) nicht gefunden wurde oder falls eine unbekannte * Kodierung an den OutputStreamWriter übergeben wurde. */ public FineFileWriter(String file, Charset charset, boolean append) { this.fileName = file; this.append = append; this.charset = charset; this.writer = openWriter(); } /** * Öffnet den Writer. Die Fehlerbehandlung erfolgt hier. * * @return Writer-Objekt. * * @throws IORuntimeException * Wenn die Datei (der Pfad) nicht gefunden wurde oder falls eine unbekannte * Kodierung an den OutputStreamWriter übergeben wurde. */ private OutputStreamWriter openWriter() { try { return tryToOpenWriter(); } catch (FileNotFoundException e) { throw new IORuntimeException("Der Pfad zur Ausgabedatei '" + fileName + "' wurde " + "nicht gefunden. (" + e.getMessage() + ")"); } catch (UnsupportedEncodingException e) { throw new IORuntimeException("Bei der Erzeugung des OutputStreamWriters für die " + "Eingabedatei '" + fileName + "' wurde eine nicht unterstützte Kodierung " + "verwendet. (" + e.getMessage() + ")"); } } /** * Öffnet den Writer. * * @return Writer-Objekt. * * @throws FileNotFoundException * Wenn die Datei (der Pfad) nicht gefunden wurde. * @throws UnsupportedEncodingException * Falls eine unbekannte Kodierung an den OutputStreamWriter * übergeben wurde. */ private OutputStreamWriter tryToOpenWriter() throws FileNotFoundException, UnsupportedEncodingException { OutputStream outputStream = new FileOutputStream(fileName, append); return new OutputStreamWriter(outputStream, charset.getCharsetAsString()); } /** * Schreibt den übergebenen String auf den Writer. * * @param text * Der auszugebende Text. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ @Override public void write(String text) { try { writer.write(text); //writer.flush(); /* bremst */ } catch (IOException e) { throw new IORuntimeException("Beim Schreiben in die Datei '" + fileName + "' trat ein " + "Fehler auf. (" + e.getMessage() + ")"); } } /** * Schreibt die übergebene Zahl auf den Writer. * * @param number * Die auszugebende Zahl. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ @Override public void write(int number) { write(Integer.toString(number)); } /** Gibt einen Zeilenumbruch aus. */ @Override public void writeln() { write(Text.LINE_BREAK); } /** * Schreibt den übergebenen String auf den Writer und fügt einen Zeilenumbruch an. * * @param text * Der auszugebende Text. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ @Override public void writeln(String text) { write(text + Text.LINE_BREAK); } /** * Schließt den Writer. * * @throws IORuntimeException * Falls beim Schließen ein Fehler auftrat. */ @Override public void close() { try { writer.close(); } catch (IOException e) { throw new IORuntimeException("Beim Schließen der Datei '" + fileName + "' trat ein " + "Fehler auf. (" + e.getMessage() + ")"); } } /** * Veranlasst den Writer alle Daten im Zwischenspeicher in die Datei zu schreiben. * * @throws IORuntimeException * Falls beim flush ein Fehler auftrat. */ @Override public void flush() { try { writer.flush(); } catch (IOException e) { throw new IORuntimeException("Beim Flushen der Datei '" + fileName + "' trat ein " + "Fehler auf. (" + e.getMessage() + ")"); } } /** * Schreibt alle Zeilen der übergebenen Liste auf den Writer und fügt jeweils einen * Zeilenumbruch an. * * @param list * Die Liste mit den auszugebenden Zeilen. * @throws IORuntimeException * Falls beim Schreiben ein Fehler auftrat. */ @Override public void writeAllLines(List |
Reader |
package de.duehl.basics.io; import java.util.List; import de.duehl.basics.io.exceptions.IORuntimeException; public interface Reader { /** * Liest die nächste Zeile aus der Datei ein. * * @return Die nächste Zeile oder null, falls das Ende der Datei erreicht * wurde. * @throws IORuntimeException * Falls die Zeile nicht eingelesen werden konnte. */ String readNextLine(); /** Liefert die aktuelle Zeilennummer zurück. */ int getLineNumber(); /** * Schließt den Reader. * * @throws IORuntimeException * Wenn es beim Schließen zu Problemen kam. */ void close(); /** * Gibt den Dateiinhalt (falls schon gelesen wurde: ab der momentanen * Stelle) als Liste von Strings zurück. * * @return Liste mit dem Dateiinhalt als Strings. */ List |
FineFileReader |
package de.duehl.basics.io; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import de.duehl.basics.io.exceptions.IORuntimeException; /** * Diese Klasse öffnet, liest aus und schließt Dateien. Ehemals 'OpenDataFile', dann 'FileReader'. * * @version 1.05 2016-10-14 * @author Christian Dühl */ public class FineFileReader implements Reader { /** Die Datei, die geöffnet und gelesen werden soll. */ private final String fileName; /** Das LineNumberReader-Objekt. */ private final LineNumberReader lineReader; /** Hält fest, ob der Reader geschlossen wurde. */ private boolean closed; /** Zeichensatz der einzulesenden Datei (z.B. "ISO-8859-1", "UTF-8"). */ private final Charset charset; /** * Der Konstruktor aus einem File. * * @param file * Zu öffnende Datei. * @throws IORuntimeException * Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine * unbekannte Kodierung vorliegt. */ public FineFileReader(File file) { this(file.getAbsolutePath()); } /** * Der Konstruktor aus einem Dateinamen. * * @param fileName * Name der zu öffnenden Datei. * @throws IORuntimeException * Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine * unbekannte Kodierung vorliegt. */ public FineFileReader(String fileName) { this(fileName, Charset.ISO_8859_1); } /** * Der Konstruktor aus einem File und einem Charset. * * @param file * Zu öffnende Datei. * @param charset * Zeichensatz der einzulesenden Datei. * @throws IORuntimeException * Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine * unbekannte Kodierung vorliegt. */ public FineFileReader(File file, Charset charset) { this(file.getAbsolutePath(), charset); } /** * Der Konstruktor aus einem Dateinamen und einem Charset. * * @param fileName * Name der zu öffnenden Datei. * @param charset * Zeichensatz der einzulesenden Datei. * @throws IORuntimeException * Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine * unbekannte Kodierung vorliegt. */ public FineFileReader(String fileName, Charset charset) { this.fileName = fileName; this.charset = charset; lineReader = openReader(); closed = false; } /** * Bildet das Reader-Objekt. Hier werden die Ausnahmen behandelt und in Laufzeitausnahmen * umgewandelt. * * @return Das Reader-Objekt. * @throws IORuntimeException * Wenn die Datei nicht gefunden wurde oder eine unbekannte Kodierung vorliegt. */ private LineNumberReader openReader() { try { return tryToOpenReader(); } catch (FileNotFoundException e) { throw new IORuntimeException("Die Eingabedatei '" + fileName + "' wurde nicht gefunden. (" + e.getMessage() + ")"); } catch (UnsupportedEncodingException e) { throw new IORuntimeException("Bei der Erzeugung des " + "InputStreamReaders für die Eingabedatei '" + fileName + "' wurde eine nicht unterstützte Kodierung verwendet. (" + e.getMessage() + ")"); } } /** * Bildet das Reader-Objekt. * * @return Das Reader-Objekt. * @throws FileNotFoundException * Wenn die angegebene Datei nicht gefunden wurde. * @throws UnsupportedEncodingException * Wenn eine unbekannte Kodierung für den InputStreamReader verwendet wurde. */ private LineNumberReader tryToOpenReader() throws FileNotFoundException, UnsupportedEncodingException { InputStream inputStream = new FileInputStream(fileName); InputStreamReader reader = new InputStreamReader(inputStream, charset.getCharsetAsString()); return new LineNumberReader(reader); } /** Erzeugt eine Stringrepräsentation des Objekts. */ @Override public String toString() { return "FineFileReader[fileName:" + fileName + ", charset:" + charset.getCharsetAsString() + "]"; } /** * Liest die nächste Zeile aus der Datei ein. * * @return Die nächste Zeile oder null, falls das Ende der Datei erreicht wurde. * @throws IORuntimeException * Falls die Zeile nicht eingelesen werden konnte. */ @Override public String readNextLine() { if (closed) { throw new IORuntimeException("Es wurde versucht, aus der geschlossenen Datei '" + fileName + "' zu lesen"); } else { try { return lineReader.readLine(); } catch (IOException e) { throw new IORuntimeException("Es trat ein Fehler beim Einlesen der nächsten Zeile " + "aus der Datei '" + fileName + "' auf. (" + e.getMessage() + ")"); } } } /** Liefert die aktuelle Zeilennummer zurück. */ @Override public int getLineNumber() { return lineReader.getLineNumber(); } /** * Schließt den Reader. * * @throws IORuntimeException * Wenn es beim Schließen zu Problemen kam. */ @Override public void close() { if (closed) { throw new IORuntimeException("Es trat ein Fehler beim Schließen der Datei '" + fileName + "' auf."); } else { tryToClose(); closed = true; } } /** * Schließt den Reader. * * @throws IORuntimeException * Wenn es beim Schließen zu Problemen kam. */ private void tryToClose() { try { lineReader.close(); } catch (IOException e) { throw new IORuntimeException("Es trat ein Fehler beim " + "Schließen der Datei '" + fileName + "' auf. (" + e.getMessage() + ")"); } } /** * Gibt den Dateiinhalt (falls schon gelesen wurde: ab der momentanen Stelle) als Liste von * Strings zurück. * * @return Liste mit dem Dateiinhalt als Strings. */ @Override public List |
Charset |
package de.duehl.basics.io; /** * Enum für die Charsets. * * @see java.nio.charset.Charset * * @version 1.03 2016-10-14 * @author Christian Dühl */ public enum Charset { US_ASCII( "US-ASCII", "Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode " + "character set"), ISO_8859_1( "ISO-8859-1", "ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1"), UTF_8( "UTF-8", "Eight-bit UCS Transformation Format"), UTF_16BE( "UTF-16BE", "Sixteen-bit UCS Transformation Format, big-endian byte order"), UTF_16LE( "UTF-16LE", "Sixteen-bit UCS Transformation Format, little-endian byte order"), UTF_16( "UTF-16", "Sixteen-bit UCS Transformation Format, byte order identified by an optional " + "byte-order mark"), ; // "Cp1252" ? private final String charsetAsString; private final String description; private Charset(String charsetAsString, String description) { this.charsetAsString = charsetAsString; this.description = description; } public String getCharsetAsString() { return charsetAsString; } public String getDescription() { return description; } } |
FileHelper |
package de.duehl.basics.io; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import javax.swing.filechooser.FileSystemView; import de.duehl.basics.io.exceptions.DirectoryNotCreatedRuntimeException; import de.duehl.basics.io.exceptions.FileNotFoundRuntimeException; import de.duehl.basics.io.exceptions.IORuntimeException; /** * Diese Klasse enthält Hilfsmethoden rund um das Dateisystem. * * @version 1.01 2016-10-14 * @author Christian Dühl */ public class FileHelper { /** Eine Instanz soll hiervon nicht angelegt werden! */ private FileHelper() {} /** * Löscht ein Verzeichnis mit Inhalt. * * @param path * Verzeichnis * @throws IORuntimeException * Wenn sich eine Datei oder Verzeichnis nicht löschen lässt. */ public static void deleteTree(File path) { if (!path.exists()) { return; } for (File file : path.listFiles()) { if (file.isDirectory()) { deleteTree(file); } else if (!file.delete()) { throw new IORuntimeException(file + " could not be deleted!"); } } if (!path.delete()) { throw new IORuntimeException(path + " could not be deleted!"); } } /** * Löscht ein Verzeichnis mit Inhalt. * * @param path * Verzeichnis * @throws IORuntimeException * Wenn sich eine Datei oder Verzeichnis nicht löschen lässt. */ public static void deleteTree(String path) { deleteTree(new File(path)); } /** * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis und * seinen Unterverzeichnissen. * * @param start * Verzeichnis, in dem die Suche beginnt. * @param extensions * Liste aller Endungen der gesuchten Dateien (etwa ".txt" oder ".java") * @return Liste mit den gefundenen Dateien. */ public static List |
Die unzähligen Tests führe ich an dieser Stelle nicht auf. Zu diesen gehören auch Testdateisysteme, das würde den Umfang hier sprengen.
Außerdem gehören noch selbst geschriebene Ausnahmen dazu, die die checked exceptions ersetzen. Exemplarisch hier die FileNotFoundException:
FileNotFoundRuntimeException |
package de.duehl.basics.io.exceptions; import java.io.FileNotFoundException; /** * Ausnahmeklasse, die statt einer FileNotFoundException eine * FileNotFoundRuntimeException darstellt. * * @version 1.01 2013-12-12 * @author Christian Dühl */ public class FileNotFoundRuntimeException extends RuntimeException { private static final long serialVersionUID = 2795730444012714039L; /** Konstruktor */ public FileNotFoundRuntimeException() { super(); } /** * Konstruktor * * @param message * Text der Fehlermeldung. */ public FileNotFoundRuntimeException(String message) { super(message); } /** * Konstruktor * * @param exception * Die FileNotFoundException, die nun in Form einer * RuntimeException verpackt werden soll. */ public FileNotFoundRuntimeException(FileNotFoundException exception) { super(exception); } } |