6.2Dateisysteme und Pfade
Im Zentrum von NIO.2 stehen die Typen FileSystem und Path:
FileSystem beschreibt ein Datensystem und ist eine abstrakte Klasse. Es wird von konkreten Dateisystemen, wie dem lokalen Dateisystem oder einem ZIP-Archiv, realisiert. Um an das aktuelle Dateisystem zu kommen, deklariert die Klasse FileSystems eine statische Methode: FileSystems.getDefault().
Path repräsentiert einen Pfad zu einer Datei oder einem Verzeichnis, wobei die Pfadangaben relativ oder absolut sein können. Die Methoden erinnern ein wenig an die alte Klasse File, doch der große Unterschied ist, dass File selbst die Datei oder das Verzeichnis repräsentiert und Abfragemethoden wie isDirectory() oder lastModified() deklariert, während Path nur den Pfad repräsentiert und nur pfadbezogene Methoden anbietet. Modifikationsmethoden gehören nicht dazu; dazu dienen Extra-Typen wie BasicFileAttributes für Attribute.
6.2.1FileSystem und Path
Ein Path-Objekt lässt sich nicht wie File über einen Konstruktor aufbauen, da die Klasse abstrakt ist. File und Path haben aber dennoch einiges gemeinsam, etwa dass sie immutable sind. Das FileSystem-Objekt bietet die entsprechende Methode getPath(…), und ein FileSystem wird über eine Fabrikmethode von FileSystems erfragt.
[zB]Beispiel
Baue ein Path-Objekt auf:
Path p = fs.getPath( "C:/Windows/Fonts/" );
Da der Ausdruck FileSystems.getDefault().getPath(…) etwas unhandlich ist, existiert eine Utility-Klasse, und der Aufruf von Paths.get() kürzt das Ganze ab. Auch aus einem File-Objekt lässt sich mit toPath() ein Path ableiten. Wir werden die Vereinfachung mit Paths.get(…) im Folgenden nutzen.
static Path get(String first, String... more)
Erzeuge einen Pfad aus Segmenten. Wenn etwa »\« der Separator ist, dann ist Paths.get("a", "b", "c") gleich Paths.get("a\\b\\c").static Path get(URI uri)
Erzeugt einen Pfad aus einem URI.
Jedes Path-Objekt hat auch eine Methode getFileSystem(), um wieder an das FileSystem zu kommen.
Abbildung 6.1Abhängigkeiten der Klassen Paths und Path
[»]Hinweis
Der Pfad-String darf unter Java kein \u0000 enthalten, andernfalls gibt es eine Ausnahme. So führt Paths.get("my.php\u0000.jpg") zur Ausnahme »java.nio.file.InvalidPathException: Illegal char < > at index 6: my.php«. Das ist ein wichtiges Sicherheitsmerkmal, um zum Beispiel einen Webserver zu schützen, keine falschen Dateien anzunehmen. Der Dateiname sieht über "my.php\u0000.jpg".endsWith(".jpg") wie eine JPG-Datei aus, aber würde alles nach dem Null-String abgeschnitten (was einige Dateisysteme machen), wäre plötzlich eine Datei my.php angelegt.
Path-Eigenschaften erfragen
Die Path-Klasse deklariert diverse getXXX(…)-Methoden (und eine isAbsolute()-Methode), die eine gewisse Ähnlichkeit mit den Methoden aus File haben. Ein paar Beispiele zur Anwendung:
Listing 6.1com/tutego/insel/nio2/FileSystemPathFileDemo1.java, main()
System.out.println( p.toString() ); // C:\Windows\Fonts
System.out.println( p.isAbsolute() ); // true
System.out.println( p.getRoot() ); // C:\
System.out.println( p.getParent() ); // Fonts
System.out.println( p.getNameCount() ); // 2
System.out.println( p.getName(p.getNameCount()-1) ); // Fonts
Methoden wie getPath(), getRoot() und getParent() liefern alle wiederum Path-Objekte aus den Bestandteilen eines gegebenen Pfades. Es gibt drei Methoden, um das Ergebnis nicht als Path weiterzuverarbeiten:
toString() liefert eine String-Repräsentation,
die Methode toUri() einen URI und
toFile() ein traditionelles File-Objekt.
Dadurch, dass Path eine hierarchische Liste von Namen für den Pfad speichert, lässt sich jedes Segment des Pfades erfragen; das ist die Aufgabe von getName(int n), das wiederum einen Path liefert. Die Methode subpath(int beginIndex, int endIndex) liefert einen Path mit den Segmenten des angegebenen Bereichs. Path implementiert die Iterable-Schnittstelle, was eine Methode iterator() vorschreibt – das wiederum bedeutet, dass Path rechts vom Doppelpunkt im erweiterten for auftauchen kann.
Abbildung 6.2Klassendiagramm von Path
Praktisch sind die Prüfmethoden startsWith(Path other) und endsWith(Path other), die feststellen, ob der Pfad mit einem bestimmten anderen Pfad beginnt oder endet. Aus Object wird equals(…) überschrieben. Da Path die Schnittstelle Comparable<Path> realisiert, wird zudem compareTo(Path) implementiert. Die Methode equals(…) löst die Pfade nicht auf, sondern betrachtet nur den Namen; die statische Methode isSameFile(Path, Path) der Klasse Files macht diesen Test und löst relative Bezüge auf. Neben equals(…) überschreibt Path auch hashCode().
extends Comparable<Path>, Iterable<Path>, Watchable
String toString()
File toFile()
URI toUri()
Path getFileName()
Path getParent()
Path getRoot()
boolean isAbsolute()
int getNameCount()
Path getName(int index)
Iterator<Path> iterator()
Path subpath(int beginIndex, int endIndex)
boolean endsWith(Path other)
boolean endsWith(String other)
boolean startsWith(Path other)
boolean startsWith(String other)
boolean equals(Object other)
int compareTo(Path other)
int hashCode()
Abbildung 6.3Vererbungsbeziehung von Path
[»]Hinweis
Die Methode getFileName() liefert keinen String, sondern ein Path-Objekt nur mit dem Dateinamen. Daher führt path.getFileName().endsWith(".xml") zum Testen, ob ein Dateiname auf ».xml« endet, nicht zum Ziel, sondern nur path.getFileName().toString().endsWith(".xml"). Das kann für Umsteiger schnell zu einer Falle werden.
Neue Pfade aufbauen
Die Methode resolve(…) baut neue Pfade zusammen und akzeptiert die Parametertypen String und Path.
[zB]Beispiel
Hänge das Benutzerverzeichnis mit dem Bilderverzeichnis zusammen:
.resolve( "Pictures" )
.resolve( "Cora" );
System.out.println( picturePath ); // z. B. C:\Users\Chris\Pictures\Cora
Eine interessante Methode ist auch relativize(Path) – sie liefert aus einer Basisangabe einen relativen Pfad, der zu einem anderen Pfad führt.
[zB]Beispiel
Von c:/Windows/Fonts nach c:/Windows/Cursors führt der relative Pfad ..\Cursors:
.relativize( Paths.get("C:/Windows/Cursors") )
); // ..\Cursors
Der Pfad kann unter Windows mit »/« oder mit »\« angegeben werden.
extends Comparable<Path>, Iterable<Path>, Watchable
Path relativize(Path other)
Path resolve(Path other)
Path resolve(String other)
Path resolveSibling(Path other)
Path resolveSibling(String other)
Normalisierung und Pfadauflösung
Genauso wie die File-Klasse symbolisiert die Path-Klasse einen Pfad, aber dieser muss nicht auf eine konkrete Datei oder ein konkretes Verzeichnis zeigen. Daher liefern die vorgestellten Methoden lediglich Informationen, die sich aus dem vorgegebenen Namen erschließen lassen, ohne auf das Dateisystem zurückzugreifen. Bei relativen Pfaden liefern die Anfragemethoden daher wenig Spannendes:
Listing 6.2com/tutego/insel/nio2/FileSystemPathFileDemo2.java, main()
System.out.println( p.toString() ); // ..\..
System.out.println( p.isAbsolute() ); // false
System.out.println( p.getRoot() ); // null
System.out.println( p.getParent() ); // ..
System.out.println( p.getNameCount() ); // 2
System.out.println( p.getName(p.getNameCount()-1) ); // ..
Um ein wenig Ordnung in relative Pfadangaben zu bringen, bietet die Path-Klasse die Methode normalize(), die ohne Zugriff auf das Dateisystem die Bezüge ».« und »..« entfernt.
Zum Auflösen der relativen Adressierung mit Zugriff auf das Dateisystem bietet die Path-Klasse die beiden Methoden toAbsolutePath() bzw. toRealPath(…) an.
Listing 6.3com/tutego/insel/nio2/RealAndAbsolutePath.java, main()
System.out.println( p2.toAbsolutePath() );
// C:\Users\Christian\Documents\Insel\programme\2_06_Files\..\..
try {
System.out.println( p2.toRealPath( LinkOption.NOFOLLOW_LINKS ) );
// C:\Users\Christian\Documents\Insel
}
catch ( IOException e ) { e.printStackTrace(); }
Die erste Methode toAbsolutePath() normalisiert nicht, sondern löst einfach nur den relativen Pfad in einen absoluten Pfad auf. Die Auflösung vom ../.. erledigt toRealPath(LinkOption...), wobei das (optionale) Argument ausdrückt, ob Verknüpfungen verfolgt werden sollen oder nicht.
[zB]Beispiel
Die Methode toRealPath(…) löst eine Ausnahme aus, wenn eine Auflösung eines Pfades zu einer Datei versucht wird, die nicht existiert.
So führt zum Beispiel
zur »java.nio.file.NoSuchFileException: C:\Users\Chris\0x«. Paths.get( "../0x" ).toAbsolutePath() führt zu keinem Fehler.
extends Comparable<Path>, Iterable<Path>, Watchable
Path normalize()
Path toAbsolutePath()
Path toRealPath(LinkOption... options)
Es gibt noch zwei register(…)-Methoden in Path, doch die haben etwas mit dem Anmelden eines Horchers zu tun, der auf Änderungen im Dateisystem reagiert. Sie werden später vorgestellt.
6.2.2Die Utility-Klasse Files
Da die Klasse Path nur Pfade, aber keine Dateiinformationen wie die Länge oder Änderungszeit repräsentiert, und da Path auch keine Möglichkeit bietet, Dateien anzulegen und zu löschen, übernimmt die Klasse Files diese Aufgaben.
Einfaches Einlesen und Schreiben von Dateien
Mit den Methoden readAllBytes(…), readAllLines(…), lines(…) bzw. write(…) kann Files einfach einen Dateiinhalt einlesen oder Strings bzw. ein Byte-Feld schreiben.
Listing 6.4com/tutego/insel/nio2/ListAllLines.java, main()
Path p = Paths.get( uri );
System.out.printf( "Datei '%s' mit Länge %d Byte(s) hat folgende Zeilen:%n",
p.getFileName(), Files.size( p ) );
int lineCnt = 1;
for ( String line : Files.readAllLines( p /*, StandardCharsets.UTF_8 vor Java 8 */) )
System.out.println( lineCnt++ + ": " + line );
static long size(Path path) throws IOException
Liefert die Größe der Datei.static byte[] readAllBytes(Path path) throws IOExceptionLiest die Datei komplett in ein Byte-Feld ein.
static List<String> readAllLines(Path path) throws IOException(Java 8)
static List<String> readAllLines(Path path, Charset cs) throws IOException
Liest die Datei Zeile für Zeile ein und liefert eine Liste dieser Zeilen. Optional ist die Angabe einer Kodierung, standardmäßig ist es StandardCharsets.UTF_8.static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException
Schreibt ein Byte-Feld in eine Datei.static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) throws IOException (Java 8)
static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) throws IOException
Schreibt alle Zeilen aus dem Iterable in eine Datei. Optional ist die Kodierung, die StandardCharsets.UTF_8 ist, so nicht anders angegeben.static Stream<String> lines(Path path)
Stream<String> lines(Path path, Charset cs)
Liefert einen Stream von Zeilen einer Datei. Optional ist die Angabe der Kodierung, die sonst standardmäßig StandardCharsets.UTF_8 ist. Beide Methoden sind neu in Java 8.
Die Aufzählung OpenOption ist ein Vararg, und daher sind Argumente nicht zwingend nötig. StandardOpenOption ist eine Aufzählung vom Typ OpenOption mit Konstanten wie APPEND, CREATE usw.
[»]Hinweis
Auch wenn es naheliegt, die Files-Methode zum Einlesen mit einem Path-Objekt zu füttern, das einen HTTP-URI repräsentiert, funktioniert dies nicht. So liefert schon die erste Zeile des Programms eine Ausnahme des Typs »java.nio.file.FileSystemNotFoundException: Provider "http" not installed«.
Path path = Paths.get( uri ); //
List<String> content = Files.readAllLines( path );
System.out.println( content );
Vielleicht kommt in der Zukunft ein Standard-Provider von Oracle, doch es ist davon auszugehen, dass quelloffene Lösungen diese Lücke schließen werden. Schwer zu programmieren sind Dateisystem-Provider nämlich nicht.
Datenströme kopieren
Sollen die Daten nicht direkt aus einer Datei in ein byte-Feld/eine String-Liste gehen bzw. aus einem byte-Feld/einer String-Sammlung in eine Datei, sondern von einer Datei in einen Datenstrom, so bieten sich zwei copy(…)-Methoden an:
static long copy(InputStream in, Path target, CopyOption... options)
Entleert den Eingabestrom und kopiert die Daten in die Datei.static long copy(Path source, OutputStream out)
Kopiert alle Daten aus der Datei in den Ausgabestrom.
Im Zusammenhang mit Datenströmen kommen wir noch einmal auf diese beiden Methoden zurück.
6.2.3Dateien kopieren und verschieben
Zum Kopieren und Verschieben (bzw. Umbenennen) von Dateien und Verzeichnissen bietet die Files-Klasse eine copy(…)- und eine move(…)-Methode:[ 70 ](Damit ist ein mv bin/laden /dev/null nun auch in Java möglich.)
static Path copy(Path source, Path target, CopyOption... options) throws IOException
static Path move(Path source, Path target, CopyOption... options) throws IOException
Kopiert oder verschiebt eine Datei. Die Rückgabe ist der Zielpfad.
Da CopyOption in einem Vararg ist, ist der Aufruf ohne Zusatzoptionen sehr einfach – nur ein Zielort muss angegeben werden:
Listing 6.5com/tutego/insel/nio2/FilesCopyAndMoveDemo.java, main()
Path copyTargetPath = Paths.get( "src/lyrics – Kopie.txt" );
Files.copy( copySourcePath, copyTargetPath );
Path moveSourcePath = Paths.get( "src/lyrics.txt" );
Path moveTargetPath = Paths.get( "bin/lyrics.txt" );
Files.move( moveSourcePath, moveTargetPath );
Die Methoden versuchen die Dateiattribute auf das Ziel zu übertragen; unterstützt das Ziel ein Attribut nicht, kann es sein, dass nur einige Attribute übertragen werden.
Das Kopieren und Verschieben sind Betriebssystemoperationen, und es ist zu erwarten, dass dies schneller ist, als von Hand die Bytes von einer Stelle zur anderen zu kopieren.
[»]Hinweis
Die Methoden copy(…) und move(…) werden nicht parallel im Hintergrund ausgeführt, sondern blockieren so lange, bis die Operation durchgeführt wurde. Wer es parallel haben möchte, der muss einen Hintergrund-Thread nutzen.
Kopier- und Verschiebeoptionen
copy(…) und move(…) führen zu einer IOException, wenn die Dateien/Verzeichnisse nicht kopiert oder verschoben werden konnten. Das ist insbesondere dann wichtig, wenn sich die Dateiattribute nicht übertragen lassen. Das bringt uns zu den optionalen CopyOption-Elementen. CopyOption ist eine Schnittstelle, die von zwei Aufzählungen StandardCopyOption und LinkOption wie folgt implementiert wird:
Listing 6.6java/nio/file/StandardCopyOption.java, StandardCopyOption
REPLACE_EXISTING,
COPY_ATTRIBUTES,
ATOMIC_MOVE;
}
Listing 6.7java/nio/file/LinkOption.java, LinkOption
NOFOLLOW_LINKS;
}
StandardCopyOption und LinkOption stellen somit gültige Argumente für copy(…) und move(…) dar. Die Bedeutung der Aufzählungselemente ist wie folgt:
Argument | Bedeutung bei copy(…) | Bedeutung bei move(…) |
---|---|---|
REPLACE_EXISTING | Ersetzt, falls vorhanden, die Datei bzw. das Verzeichnis am Zielort. Ist das Ziel eine existierende symbolische Verknüpfung, so wird nur die Verknüpfung selbst ersetzt, aber nicht die Datei bzw. das Verzeichnis, auf die/das die Verknüpfung zeigt. | |
COPY_ATTRIBUTES | Versucht, alle Attribute zu kopieren. | – |
NOFOLLOW_LINKS | Ist der Path eine Verknüpfung, so wird nur die Verknüpfung selbst kopiert, aber nicht die Datei, auf die die Verknüpfung zeigt. | – |
ATOMIC_MOVE | – | Führt das Verschieben (also das Anlegen der Kopie und das Löschen des Originals) atomar durch. Führt zu AtomicMoveNotSupportedException, falls das Dateisystem dies nicht unterstützt. |
Tabelle 6.1Konstanten aus StandardCopyOption
Beim Kopieren von symbolischen Verknüpfungen wird standardmäßig die referenzierte Datei kopiert, aber nicht die Verknüpfung. Daher gibt es die Option NOFOLLOW_LINKS. Falls REPLACE_EXISTING nicht angegeben ist und im Zielordner schon eine Datei bzw. ein Verzeichnis existiert, lösen copy(…) und move(…) eine FileAlreadyExistsException aus. Das Kopieren von Dateien ist nicht automatisch atomar, und eine Option lässt sich auch nicht setzen.
Im Fall von Verzeichnissen wird copy(…) nur ein leeres Verzeichnis anlegen, aber nicht die Dateien eines Quellverzeichnisses automatisch mit kopieren. Das muss per Hand übernommen werden; Files.walkFileTree(…) ist für diesen Fall ganz gut geeignet und hilft beim Ablaufen von Verzeichnisbäumen. Die Semantik bei move(…) und nicht leeren Verzeichnissen ist komplizierter, da es hier darauf ankommt, ob es sich um ein Verschieben auf dem lokalen Dateisystem handelt (also um eine Art »Umbenennen«) oder um ein Verschieben auf zum Beispiel ein anderes Laufwerk. Wenn die Einträge in einem Verzeichnis wirklich auf ein anderes Dateisystem verschoben werden müssen, so übernimmt move(…) diese Arbeit nicht; hier müssen wir selbst per copy(…) auf der Ebene der einzelnen Einträge kopieren.
[»]Hinweis
Kommt es während einer Kopier- oder Verschiebeoperation zu Problemen und tritt eine IOException auf, so müssen wir uns um eventuell übrig gebliebene Reste und halb kopierte Dateien kümmern.
6.2.4Dateiattribute *
Für das Attribut-Management wurde das Paket java.nio.file.attribute geschaffen. Wer einen Blick auf das Paket wirft, dem fallen eine ganze Reihe von Schnittstellen und ein Namensmuster auf. Zunächst ist es wichtig zu verstehen, dass es auf die Attribute eine Reihe von Sichten für unterschiedliche Betriebssysteme gibt, etwa eine DOS-Sicht oder eine Unix-Sicht. Das gab es bisher unter Java nicht.
Für diese Sichten gibt es jeweils Schnittstellen, die die Dateisystem-Attribute für diese Sicht lesen oder schreiben. So ist es zum Beispiel die Aufgabe der Schnittstelle BasicFileAttributeView, Methoden zu deklarieren, die grundlegende Attribute einlesen und modifizieren, oder die Aufgabe von DosFileAttributeView, einer Unterschnittstelle von BasicFileAttributeView, die Attribute eines DOS-Dateisystems zu lesen und zu schreiben.
Nun kommt für die XXXAttributeView-Schnittstellen je eine Tochterschnittstelle XXXAttributes hinzu. Die Aufgabe dieser XXXAttributes-Schnittstellen ist es, Methoden zum Lesen der Eigenschaften vorzuschreiben. So ist zum Beispiel BasicFileAttributeView mit BasicFileAttributes verbunden und DosFileAttributeView mit BasicFileAttributes. Die angebotenen Methoden bei XXXAttributes sind nicht nach der JavaBeans-Notation als Getter aufgebaut, sondern kurz und knapp. BasicFileAttributes deklariert zum Beispiel creationTime() und size().
Abbildung 6.4Vererbungsbeziehungen bei den AttributeView-Klassen
Dateiattribute mit XXXFileAttributes-Typen lesen
Um Dateiattribute zu modifizieren bzw. einzulesen, müssen wir an ein XXXAttributeView- bzw. ein XXXAttributes-Objekt kommen.
Für die XXXAttributes steht die Files.readAttributes(…)-Methode bereit. Ihr wird ein Class-Typ für die Attribut-Schnittstelle (etwa BasicFileAttributes.class) als Typ-Token mitgegeben, und die Rückgabe ist eine Implementierung der Schnittstelle. Damit lassen sich dann die Abfragemethoden aufrufen.
Ein Beispiel soll die einfachen Attribute ausgeben:
Listing 6.8com/tutego/insel/nio2/BasicFileAttributesDemo.java, main()
BasicFileAttributes attrs = Files.readAttributes( p, BasicFileAttributes.class );
System.out.println( attrs.isRegularFile() ); // true
System.out.println( attrs.isDirectory() ); // false
System.out.println( attrs.isSymbolicLink() ); // false
System.out.println( attrs.isOther() ); // false
System.out.println( attrs.lastModifiedTime() ); // 2006-05-23T12:36:54Z
System.out.println( attrs.lastAccessTime() ); // 2009-07-17T12:24:33Z
System.out.println( attrs.creationTime() ); // 2006-05-23T12:36:54Z
System.out.println( attrs.size() ); // 14
Abbildung 6.5Klassendiagramm von BasicFileAttributes
Zusammenfassung der XXXFileAttributes-Schnittstellen und ihrer Methoden
BasicFileAttributes ist eine Oberschnittstelle von DosFileAttributes und PosixFileAttributes mit allgemeinen Methoden wie isDirectory() oder lastAccessTime(). Die Unterschnittstelle DosFileAttributes schreibt Methoden vor, die nur auf DOS-Dateisystemen sinnvoll sind, etwa isSystem(), und PosixFileAttributes fügt insbesondere Operationen zur Abfrage von Rechten hinzu.
Operationen | Funktion |
---|---|
BasicFileAttributes | |
long size() | Größe der Datei in Bytes |
boolean isDirectory() | Verzeichnis? |
boolean isOther() | Datei/Verzeichnis/Verknüpfung oder etwas anderes? |
boolean isRegularFile() | Normale Datei? |
boolean isSymbolicLink() | Symbolische Verknüpfung? |
FileTime creationTime() | Erstellungszeit |
FileTime lastAccessTime() | Zeit des letzten Zugriffs |
FileTime lastModifiedTime() | Zeit der letzten Änderung |
Object fileKey() | stabiler eindeutiger Identifizierer der Datei |
DosFileAttributes | |
boolean isArchive() | Archiv-Bit gesetzt? |
boolean isHidden() | Hidden-Bit gesetzt? |
boolean isReadOnly() | Nur-Lese-Bit gesetzt? |
boolean isSystem() | Systemdatei-Bit gesetzt? |
PosixFileAttributes | |
UserPrincipal owner() | Eigentümer |
GroupPrincipal group() | Gruppeneigentümer |
Set<PosixFilePermission> permissions() | Rechte der Datei |
Tabelle 6.2Operationen auf unterschiedlichen XXXFileAttributes-Schnittstellen
Bei Posix-Systemen (Windows wird nicht als solches erkannt) lassen sich die Rechte und der Benutzer erfragen:
Listing 6.9com/tutego/insel/nio2/PosixFileAttributesDemo.java, main()
PosixFileAttributes attrs = Files.readAttributes( p, PosixFileAttributes.class );
System.out.println( attrs.group() );
System.out.println( attrs.permissions() );
System.out.println( attrs.owner() );
Die Methode permissions() liefert ein Set<PosixFilePermission>, wobei ein PosixFilePermission ein Aufzählungstyp mit den Konstanten GROUP_EXECUTE, GROUP_READ, GROUP_WRITE, OTHERS_EXECUTE, OTHERS_READ, OTHERS_WRITE, OWNER_EXECUTE, OWNER_READ und OWNER_WRITE ist.
Um die Mengen mit PosixFilePermission-Elementen leicht ausgeben und aufbauen zu können, gibt es eine Utility-Klasse java.nio.file.attribute.PosixFilePermissions. Sie dient der Konvertierung von Rechteangaben in der Unix-Notation.
System.out.println( PosixFilePermissions.toString( perms ) ); // rwxr-x---
Anstatt in unserem Beispiel PosixFileAttributesDemo also einfach nur
zu nutzen, ist eine andere Schreibweise zu empfehlen:
FileTime-Klasse
java.nio.file.attribute.FileTime ist eine finale Klasse und repräsentiert Zeitstempel, die von den BasicFileAttributes Folgendes liefern (nicht jedes Betriebssystem liefert alle Werte):
creationTime(): Wann wurde die Datei angelegt?
lastAccessTime(): Wann wurde die Datei zuletzt angefasst/gelesen?
lastModifiedTime(): Wann gab es den letzen Schreibzugriff?
Mit dem FileTime in der Hand können wir die Zeit auslesen mit to(TimeUnit) oder toInstant() (liefert das neu in Java 8 eingeführte java.time.Instant). Sonst lässt sich mit FileTime nicht viel anstellen, es überschreibt aber equals(…) und hashCode() und implementiert Comparable<FileTime>.
FileTime-Objekte lassen sich über statische fromXXX(…)-Methoden aufbauen und dann den Dateiattributen zuweisen, wie wir gleich sehen werden.
Attribute über XXXAttributeView-Schnittstellen schreiben
Die XXXAttributes-Schnittstellen bieten nur Lesemethoden (und das auch nicht in der Getter-Konvention, wie wir gesehen haben), aber Modifikationen müssen auf einem anderen Weg durchgeführt werden. Einfach ist zum Beispiel das Setzen der letzten Zugriffszeit, da Files mit setLastModifiedTime(Path path, FileTime time) für diesen speziellen Fall schon eine Methode mitbringt – das ist aber neben ein paar anderen Methoden eine Besonderheit.
Der Weg, um Attribute zu modifizieren, führt über die XXXAttributeView-Schnittstellen, da sie die entsprechenden Methoden wie setReadOnly() bereithalten. Zur Erinnerung: Bei den XXXAttributes-Typen brauchen wir nicht zu suchen, dort finden sich nur Lesemethoden, wie isReadOnly(). Bleibt die Frage, wer Exemplare dieser XXXAttributeView-Typen zurückgibt. Das ist die Aufgabe einer Methode getFileAttributeView() in Files:
static <V extends FileAttributeView>
V getFileAttributeView(Path path, Class<V> type, LinkOption... options)
Erfragen und modifizieren wir zum Beispiel das Archiv-Flag einer Datei:
Listing 6.10com/tutego/insel/nio2/FileAttributeViewDemo.java, main()
DosFileAttributeView fileAttributeView = Files.getFileAttributeView(
p, DosFileAttributeView.class );
System.out.println( fileAttributeView.readAttributes().isArchive() ); // true
fileAttributeView.setArchive( false );
System.out.println( fileAttributeView.readAttributes().isArchive() ); // false
fileAttributeView.setArchive( true );
System.out.println( fileAttributeView.readAttributes().isArchive() ); // true
Alle XXXAttributeView-Implementierungen bieten die Methode readAttributes(), die die Attribute liefert. Es entspricht also
p, BasicFileAttributeView.class ).readAttributes();
der Variante:
Das heißt, von einem XXXAttributeView kommen wir zu XXXAttributes, aber nicht umgekehrt.
[»]Hinweis
In BasicFileAttributeView (und somit auch in DosFileAttributeView und PosixFileAttributeView) gibt es zum Setzen der Dateizeiten eine Megamethode setTimes(FileTimelastModifiedTime, FileTime lastAccessTime, FileTime createTime) statt drei kleiner Methoden wie setLastModifiedTime(). Soll ein Glied nicht gesetzt werden, ist hier null zu übergeben.
Attribute-Strings als Schlüssel für einen Attributspeicher
Die Anzahl möglicher Attribute ist im Prinzip unbeschränkt, und Java bietet den Zugriff auf die wichtigsten. Doch da neue Dateisystem-Provider im System angemeldet werden können, die wiederum Attribute ganz anderer Art einführen können, gibt es neben den bekannten XXXAttributes-Schnittstellen auch einen alternativen Weg, um Attributzustände über einen String-Schlüssel zu erfragen und zu verändern. Das ist so, als ob mit der Datei ein Assoziativspeicher verbunden wäre, der sich anfragen lässt. Das gibt große Flexibilität, da eine Implementierung neue Attribute veröffentlichen kann, ohne dass die API (also die XXXAttributes) geändert werden muss. Konkrete Plattformen können etwa die Information anbieten,
mit welcher Software eine Datei erstellt wurde (Apple tut das),
was das zugewiesene Icon ist,
ob die Datei indexiert wurde,
und neue Dateisysteme etwa für ZIP-Archive können Detailinformationen wie die CRC-Prüfsumme anbieten, ohne dafür über Methoden einer Java-Schnittstelle gehen zu müssen.
Die bekannten XXXAttributeView-Schnittstellen (nicht XXXAttributes) dokumentieren einen String für den Schlüssel zum Zugriff auf den Attributspeicher. Allerdings sind die Strings nicht als Konstante deklariert, sondern einfach in der Dokumentation genannt. Drei Schnittstellen schauen wir uns näher an:
Schlüssel | Typ |
---|---|
BasicFileAttributeView (basic) | |
isRegularFile | Boolean |
isDirectory | Boolean |
isSymbolicLink | Boolean |
isOther | Boolean |
fileKey | Object |
lastModifiedTime | FileTime |
lastAccessTime | FileTime |
creationTime | FileTime |
size | Long |
DosFileAttributeView (dos) | |
readonly | Boolean |
Hidden | Boolean |
System | Boolean |
Archive | Boolean |
PosixFileAttributeView (posix) | |
Permissions | Set<PosixFilePermission> |
Group | GroupPrincipal |
Tabelle 6.3Schlüssel für Attributspeicher und erwarteter Typ der Rückgabe
Eine Anfrage an den Schlüssel liefert einen Wert mit unterschiedlichen Typen; Object, Boolean, Long und Set sind klar. Neu ist eine spezielle Klasse java.nio.file.attribute.FileTime für Zeiten, die Comparable<FileTime> erweitert, aber keine Erweiterung von Date oder Calendar ist. Nur mit der Methode long to(TimeUnit unit) lassen sich die Dateizeiten konvertieren. GroupPrincipal repräsentiert einen Benutzernamen bzw. eine Gruppe, und ein UserPrincipalLookupService bietet einen Anfragedienst.
[zB]Beispiel
Chris soll der Besitzer der Datei path sein:
.getUserPrincipalLookupService();
UserPrincipal principal = upls.lookupPrincipalByName( "chris" );
Files.setOwner( path, principal );
Zum Lesen der Attribute gibt es einmal eine Methode Files.getAttribute(Path path, String attribute, LinkOption... options), die genau ein Attribut erfragt, und zwei überladene Methoden Files.readAttributes(…), die gleich mehrere Attribute auf einmal laden.
Dazu ein Beispiel, wie die Methode Files.getAttribute(…) diverse Attribute erfragt:
Listing 6.11com/tutego/insel/nio2/AttributesDemo.java, main()
System.out.println( Files.getAttribute( p, "basic:isRegularFile" ) ); // true
System.out.println( Files.getAttribute( p, "isDirectory" ) ); // false
System.out.println( Files.getAttribute( p, "isSymbolicLink" ) ); // false
System.out.println( Files.getAttribute( p, "isOther" ) ); // false
System.out.println( Files.getAttribute( p, "fileKey" ) ); // null
System.out.println( Files.getAttribute( p, "lastModifiedTime" ) ); // 2006-05-23T12:36:54Z
System.out.println( Files.getAttribute( p, "lastAccessTime" ) ); // 2009-07-17T12:24:33Z
System.out.println( Files.getAttribute( p, "creationTime" ) ); // 2006-05-23T12:36:54Z
System.out.println( Files.getAttribute( p, "size" ) ); // 14
System.out.println( Files.getAttribute( p, "dos:readonly" ) ); // false
System.out.println( Files.getAttribute( p, "dos:hidden" ) ); // false
System.out.println( Files.getAttribute( p, "dos:system" ) ); // false
System.out.println( Files.getAttribute( p, "dos:archive" ) ); // true
Der Attribut-String hat ein Präfix dos: oder posix:, das, falls es sich nicht gerade um die Standardparameter handelt, mit angegeben wird.
[»]Hinweis
Falls ein Präfix nicht unterstützt wird, ist eine Ausnahme die Folge und die Rückgabe von getAttribute(…) nicht einfach nur null. So löst unter Windows ein Files.getAttribute(p, "posix:permissions") eine »java.lang.UnsupportedOperationException: View 'posix' not available« aus. Welche Sichten das System unterstützt, sagt supportedFileAttributeViews(). So liefert
System.out.println( fs );
unter einem Windows-System die Zeichenfolgen acl, basic, owner, user, dos.
Auch das Setzen der Werte ist möglich; dazu dient Files.setAttribute(…). Ein Beispiel:
Listing 6.12com/tutego/insel/nio2/SetDosAttribute.java, main()
System.out.println(
Files.readAttributes( p, DosFileAttributes.class ).isArchive() ); // true
Files.setAttribute( p, "dos:archive", false );
System.out.println(
Files.readAttributes( p, DosFileAttributes.class ).isArchive() ); // false
Files.setAttribute( p, "dos:archive", true );
System.out.println(
Files.readAttributes( p, DosFileAttributes.class ).isArchive() ); // true
Benutzerdefinierte Datei-Attribute
Der Typ UserDefinedFileAttributeView fällt ein wenig aus dem Rahmen, da er für benutzerdefinierte Schlüssel-Wert-Eigenschaften steht, die als Metadaten mit einem Dateisystemelement assoziiert werden können. Ohne den Dateiinhalt selbst zu verändern, lassen sich ganz neue Metadaten vermerken. Ein Java-Programm könnte zum Beispiel speichern, wer der letzte zugreifende Benutzer war, oder einen Screenshot der Anwendung anhängen.
Listing 6.13com/tutego/insel/nio2/UserDefinedFileAttributeViewDemo.java, main()
UserDefinedFileAttributeView attrs = Files.getFileAttributeView( p,
UserDefinedFileAttributeView.class );
List<String> attrsList = attrs.list();
System.out.println( attrsList ); // []
attrs.write( "last-user",
ByteBuffer.wrap( "chris".getBytes( StandardCharsets.UTF_8 ) ) );
System.out.println( attrs.list() ); // [last-user]
ByteBuffer attrValue = ByteBuffer.allocate( attrs.size( "last-user" ) );
attrs.read( "last-user", attrValue );
attrValue.rewind();
String value = StandardCharsets.UTF_8.decode( attrValue ).toString();
System.out.println( value ); // chris
Der mit dem Schlüssel assoziierte Wert kann ein beliebiges Byte-Feld sein, etwa aus einem String erzeugt oder einer Grafik. Ein wenig ungelenk ist es jedoch, dass hier nicht der Typ byte[] Verwendung findet, sondern ByteBuffer, eine andere NIO-Klasse. Die Nutzung wird an dieser Stelle nicht weiter vertieft, das Beispiel zeigt eine einfache Nutzung für eine Zeichenfolge.
6.2.5Neue Dateien, Verzeichnisse, symbolische Verknüpfungen anlegen und löschen
Die Klasse Files deklariert zum Anlegen von neuen Dateien und Verzeichnissen die zwei Methoden createFile(…) und createDirectory(…):
static Path createFile(Path path) throws IOException
static Path createDirectory(Path dir) throws IOException
Beide Methoden liefern das Path-Objekt zurück, das createFile(…) bzw. createDirectory(…) übergeben wurde. Die Methoden deklarieren throws IOException, wenn die Datei bzw. das Verzeichnis nicht angelegt werden kann, und eine java.nio.file.FileAlreadyExistsException (eine Unterklasse von IOException), wenn die Datei bzw. das Verzeichnis schon existiert.
FileAttribute
Beim Erzeugen neuer Dateien oder Verzeichnisse (auch temporärer) spielen Attribute eine wichtige Rolle. Daher lassen sich den createXXX(…)-Methoden FileAttribute-Argumente übergeben. Sie kommen über ein Vararg, sodass entweder überhaupt keine Attribute mitgegeben werden (das haben wir im oberen Fall so geschrieben), oder eben beliebig viele. Die genaue Deklaration der Methoden lautet:
static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException
static Path createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException
FileAttribute ist eine Schnittstelle, und bisher gibt es nur eine Stelle in der NIO-API, die Objekte vom Typ FileAttribute liefert: Die Utility-Klasse PosixFilePermissions bietet eine statische Hilfsmethode, die die Menge mit PosixFilePermission-Objekten zurückgibt:
static FileAttribute<Set<PosixFilePermission>> asFileAttribute(
Set<PosixFilePermission> perms)
Das Argument für die Methode asFileAttribute(…) ist vom Typ Set<PosixFilePermission>, und dies kann wiederum über EnumSet aufgebaut werden oder über die statische Utility-Methode PosixFilePermissions.fromString(string), die wir schon kennengelernt haben.
Temporäre Dateien und Verzeichnisse anlegen
Vier Methoden der Klasse Files erlauben das Anlegen von temporären Dateien bzw. Verzeichnissen:
static Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs)
throws IOExceptionstatic Path createTempDirectory(String prefix, FileAttribute<?>... attrs)
throws IOExceptionstatic Path createTempFile(Path dir, String prefix, String suffix,
FileAttribute<?>... attrs) throws IOExceptionstatic Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs)
throws IOException
Misslingt dies, gibt es eine Ausnahme, etwa wenn das Dateisystem keine temporären Dateien zulässt oder die Attribute falsch sind.
Rekursiv alle Verzeichnisse anlegen
Gilt es ein Verzeichnis rekursiv mit vorangehenden Verzeichnissen anzulegen, übernimmt die statische Methode Files.createDirectories(…) diese Aufgabe:
static Path createDirectories(Path dir, FileAttribute<?>... attrs)
throws IOException
Symbolische Verknüpfungen anlegen *
Dem Anlegen von symbolischen Verknüpfungen (symbolischer Link) dient die Methode Files.createSymbolicLink(…):
static Path createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs)
throws IOException
Legt eine neue Verknüpfung an. Der Parameter link ist der Ort, an dem die Verknüpfung angelegt werden soll, und der Parameter target bestimmt den Zielpunkt der Verknüpfung. Die Angabe kann absolut oder relativ sein.
Dem Lesen der symbolischen Verknüpfung dient die Files-Methode readSymbolicLink(Path). Die Methode wie auch createSymbolicLink(…) sind optional, müssen also vom Dateisystem-Provider nicht angeboten werden – nicht jedes Dateisystem hat symbolische Verknüpfungen.
Dateien und Verzeichnisse löschen
Die Klasse Files bietet zum Löschen die zwei Methoden delete(…) und deleteIfExists(…):
static void delete(Path path) throws IOException
static boolean deleteIfExists(Path path) throws IOException
Steht der Pfad für eine Datei, wird diese gelöscht. Steht er für eine symbolische Verknüpfung, wird nur diese gelöscht, aber nicht die Datei bzw. das Verzeichnis, auf die/das er zeigt. Nur leere Verzeichnisse können gelöscht werden. Nicht leere Verzeichnisse bei einem delete(…)-Aufruf können eine DirectoryNotEmptyException auslösen, allerdings ist dieser Ausnahmetyp von der Implementierung abhängig. Anders als delete() testet deleteIfExists() vorher, ob die Datei bzw. das Verzeichnis existiert. Dieser Test ist allerdings nicht atomar. delete() sollte eine NoSuchFileException auslösen, wenn die Datei bzw. das Verzeichnis nicht vorhanden bzw. nicht gelöscht werden konnte. Gibt es grundsätzliche Probleme, wird immer eine Ausnahme vom Typ IOException ausgelöst und nicht einfach wie bei der File-Methode delete() die Rückgabe false geliefert.
6.2.6MIME-Typen herausfinden *
Die Erkennung von Dateitypen spielt eine wichtige Rolle, etwa dann, wenn für einen Dateityp ein Programm zum Betrachten oder Bearbeiten aufgerufen werden soll oder ein passendes Icon für einen Dateityp auf der grafischen Oberfläche auftauchen soll. Relativ früh wurde daher der MIME-Typ (Internet Media Type) eingeführt, der Medientypen kennzeichnet. Die wichtigsten Medientypen sind:
Medientyp | Beispiel mit Subtyp | Bedeutung |
---|---|---|
text | text/plain, text/xml, text/html | Text |
image | image/gif, image/png | Bilder |
video | video/mpeg, video/quicktime | Videos |
audio | audio/mid, audio/mpeg | Audios |
application | application/msword, application/octet-stream | Binärdaten |
Tabelle 6.4Einige MIME-Typen
Der MIME-Typ wird im Idealfall nicht nach Dateiendungen ermittelt, da es Systeme gibt, die nicht mit Dateiendungen arbeiten, und Dateiendungen auch mehrdeutig sind. Ein guter MIME-Typ-Erkenner (MIME-Sniffer genannt) schaut daher in die Datei und ermittelt den korrekten Typ – einige Dateisysteme speichern den MIME-Typ auch als Metainformation ab.
NIO.2 bietet FileTypeDetector-Klassen, die MIME-Typen identifizieren können. Alle FileTypeDetector-Objekte werden in einer Liste gesammelt, und wenn es darum geht, den MIME-Typ einer bestimmten Datei zu ermitteln, geht eine Schleife über alle angemeldeten Detektoren und fragt jeden Detektor, ob er den MIME-Typ ermitteln kann. Zugang zu diesem Suchalgorithmus bietet die einfache statische Methode Files.probeContentType(Path).
Listing 6.14com/tutego/insel/nio2/MimeTypeDetector.java, main()
System.out.println( Files.probeContentType( path1 ) ); // text/plain
Path path2 = Paths.get( "C:/Windows/Web/Wallpaper/img1.jpg" );
System.out.println( Files.probeContentType( path2 ) ); // image/jpeg
[»]Hinweis
Wir haben schon gesehen und diskutiert, dass Path-Objekte über URI-Objekte aufgebaut werden können. Doch MIME-Typen von Webressourcen lassen sich so nicht ermitteln:
System.out.println( Files.probeContentType( p ) );
Wie schon erwähnt wurde, führt Paths.get(…) zu einer Ausnahme, da es standardmäßig keinen Provider für das Protokoll HTTP gibt.
Eigene Detektoren können über den Service-Mechanismus eingebunden werden. Die API-Dokumentation bei probeContentType(…) in der Klasse Files gibt dazu Hinweise.
[»]Hinweis
Die Files-API gibt es seit Java 7, und für Java 6 bietet etwa new MimetypesFileTypeMap().getContentType(fileName) eine Alternative – das zu den Bordmitteln von Java. Quelloffene Ergänzungen für MIME-Erkennungen gibt es viele. Eine Empfehlung kann für Apache Tika ausgesprochen werden, Details unter http://tika.apache.org/1.5/detection.html.
6.2.7Verzeichnislistings (DirectoryStream/Stream) und Filter *
In der Klasse Files finden sich vier Methoden (eine mehr unter Java 8), um zu einem gegebenen Verzeichnis alle Dateien und Unterverzeichnisse aufzulisten:
static Stream<Path> list(Path dir) throws IOException
Neu in Java 8.static DirectoryStream<Path> newDirectoryStream(Path dir) throws IOException
static DirectoryStream<Path> newDirectoryStream(Path dir,
DirectoryStream.Filter<? super Path> filter) throws IOExceptionstatic DirectoryStream<Path> newDirectoryStream(Path dir, String glob) throws IOException
Die Rückgabe DirectoryStream<T> ist ein Closeable (und somit AutoCloseable) sowie Iterable<T>, und so unterscheidet sich die Möglichkeit zur Anfrage der Dateien im Ordner grundsätzlich von der Methode list(…) in der Klasse File, die immer alle Dateien in einem Feld auf einmal zurückliefert. Bei einem DirectoryStream wird Element für Element über den Iterator geholt; trotz des Namensanhangs »Stream« ist der DirectoryStream kein Strom im Sinne von java.util.stream. Ein Stream<String> hingegen liefert die kompakte Methode list(Path), sie nutzt intern einen DirectoryStream.
Listing 6.15com/tutego/insel/nio2/Dir.java, main()
Files.newDirectoryStream( Paths.get( "c:/" ) ) ) {
for ( Path path : files )
System.out.println( path.getFileName() );
}
Aus der Tatsache, dass die Dateien und Unterverzeichnisse nicht in einem Rutsch geholt werden, leitet sich die Konsequenz ab, dass der DirectoryStream/Stream<String> geschlossen werden muss, da nicht klar ist, ob der Benutzer wirklich alle Dateien abholt oder nach den ersten zehn Einträgen aufhört. Die Schnittstelle DirectoryStream erweitert die Schnittstelle Closeable (und die ist AutoCloseable, weshalb unser Beispiel ein try mit Ressourcen nutzt), und Stream implementiert AutoCloseable, daher ist es guter Stil, den DirectoryStream/Stream am Ende zu schließen, um blockierte Ressourcen freizugeben. try mit Ressourcen gibt immer etwaige Ressourcen frei, auch wenn es beim Ablaufen des Verzeichnisses zu einer Ausnahme kam.
Filtern
Die Methoden list(Path) bzw. newDirectoryStream(Path) bilden die einfachsten Varianten für ein Ablaufen eines Ordnerinhalts; sie liefern immer ungefiltert alle Inhalte. Doch lässt sich die Ergebnisliste filtern, und zwar:
über bekannte reguläre Ausdrücke
über eine spezielle Syntax, mit der sich zum Beispiel *.txt schreiben lässt
über eigene Filterklassen
Neben newDirectoryStream(Path) erlauben zwei weitere parametrisierte newDirectoryStream(…)-Methoden zusätzliche Filter, für list(…) gibt es keine Variante mit Filter. In der Version newDirectoryStream(Path, String) ist das Filterkriterium durch eine Zeichenkette beschrieben. Oracle nutzt hier die so genannte Globbing-Syntax, die an reguläre Ausdrücke erinnert. In der API-Dokumentation von FileSystem sind bei der Methode getPathMatcher(String syntaxAndPattern) einige Beispiele gegeben.
Nutzen wir newDirectoryStream(Path, String), um GIF-, JPG- und PNG-Dateien in einem bestimmten Verzeichnis aufzulisten:
Listing 6.16com/tutego/insel/nio2/ListAllImages.java, main()
Path picturePath = Paths.get( userHomeDir ).resolve( "Pictures" );
try ( DirectoryStream<Path> files =
Files.newDirectoryStream( picturePath, "*.{gif,jpg,png}" ) ) {
for ( Path path : files )
System.out.println( path.getFileName() );
}
Noch weiter geht beim Filtern die Methode newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter). Hier lässt sich ein Filter frei programmieren. Nutzen wir als Beispiel einen Filter, der alle leeren Dateien in einem Verzeichnis aufspürt:
Listing 6.17com/tutego/insel/nio2/ListZeroFiles.java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class ListZeroFiles {
static final class EmptyFilesFilter implements DirectoryStream.Filter<Path> {
@Override public boolean accept( Path path ) throws IOException {
BasicFileAttributes attrib =
Files.readAttributes( path,BasicFileAttributes.class );
return attrib.isRegularFile() && attrib.size() == 0;
}
}
public static void main( String[] args ) throws IOException {
try ( DirectoryStream<Path> files = Files.newDirectoryStream(
Paths.get( "c:/Windows" ), new EmptyFilesFilter() ) ) {
for ( Path path : files )
System.out.println( path.getFileName() );
}
}
}
Alle drei newDirectoryStream(…)-Methoden arbeiten nichtrekursiv. Für die Suche und den rekursiven Abstieg tief in den Verzeichnisbaum gibt es mit FileVisitor eine andere Möglichkeit. Unschön ist auch, dass bei Files.newDirectoryStream(…) nicht mehrere Filter übergeben werden können und dass die Java-API auch keine Klasse mitbringt, die mehrere Filter zu einem Filter zusammenfasst. Das Gleiche gilbt im Übrigen auch für andere Dateifilter.
Abbildung 6.6DirectoryStream und Filter als innerer Typ
6.2.8Rekursives Ablaufen des Verzeichnisbaums *
Die Utility-Klasse Files bietet vier statische Methoden (zwei mehr in Java 8), die, bei einem Startordner beginnend, die Verzeichnisse rekursiv ablaufen:
static Stream<Path> walk(Path start, FileVisitOption... options) throws IOException
Neu in Java 8.static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) throws IOException
Neu in Java 8.static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth,
FileVisitor<? super Path> visitor)
Bei allen Varianten bestimmt der erste Parameter den Startordner. Während walk(…)einen java.util.stream.Stream liefert (die Methoden sind neu in Java 8), erwarten die anderen beiden walkFileTree(…)-Methoden ein Objekt mit Callback-Methoden, die walkFileTree(…) beim Ablaufen des Verzeichnisbaums aufruft. Dieses Objekt implementiert die Schnittstelle FileVisitor und hat folgende Methodendeklarationen:
FileVisitResult postVisitDirectory(T dir, IOException exc)
FileVisitResult preVisitDirectory(T dir)
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
FileVisitResult visitFileFailed(T file, IOException exc)
Die Operation visitFile(…) ist die wichtigste. Ihr übergibt walkFileTree(…) beim internen Ablaufen den Pfad auf die gefundene Datei bzw. den Ordner (es wird sich in der Regel immer um FileVisitor<Path> handeln) sowie die BasicFileAttributes, die es einfach machen, Attribute wie die Dateigröße ohne große Umwege auszuwerten.
Die aufgerufenen Methoden bestimmen über die Rückgabe, ob der Durchlauf fortgeführt oder abgebrochen wird. FileVisitResult ist eine Aufzählung mit den vier folgenden Konstanten: CONTINUE, SKIP_SIBLINGS, SKIP_SUBTREE, TERMINATE.
Von FileVisitor<T> gibt es mit SimpleFileVisitor<T> eine Standardimplementierung mit folgendem Verhalten:
Methode | Implementierung |
---|---|
preVisitDirectory | return FileVisitResult.CONTINUE; |
visitFile | return FileVisitResult.CONTINUE; |
visitFileFailed | throw exc; |
postVisitDirectory | if (exc != null) throw exc; |
Tabelle 6.5Methoden aus SimpleFileVisitor
Kommt es also beim Ablaufen zu einem Fehler, führt dies beim SimpleFileVisitor zur IOException, und der Durchlauf bricht ab.
Abbildung 6.7SimpleFileVisitor ist eine Unterklasse von FileVisitor
Geschichtliches
In den ersten Versionen von NIO.2 hatte die Schnittstelle FileVisitor eine weitere Operation, preVisitDirectoryFailed(…), die immer dann aufgerufen wurde, wenn ein Verzeichnis nicht betreten werden konnte. Diese Methode ist in der finalen Java-API nicht mehr zu finden, denn auch im Fall eines nicht besuchbaren Verzeichnisses ruft der File-Visitor visitFileFailed(…) auf.
Finde alle Bilder (und auch Nemo)
In einem Beispiel wollen wir einen Verzeichnisbesucher schreiben, der alle Bilder ab einem Startverzeichnis findet:
Listing 6.18com/tutego/insel/nio2/CrawlForImages.java, main()
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFileFailed( Path file, IOException exc ) {
return FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult visitFile( Path path, BasicFileAttributes attribs ) {
try {
String mime = Files.probeContentType( path );
if ( mime != null && mime.startsWith( "image/" ) )
System.out.println( path );
}
catch ( IOException e ) { }
return FileVisitResult.CONTINUE;
}
} );
Vom SimpleFileVisitor überschreiben wir zwei Methoden. In visitFile(…) testen wir mit dem MIME-Typ-Erkenner, ob es sich um eine Grafik handelt. In diesem Fall beginnt der String mit »image/«. Dass wir auch visitFileFailed(…) überschreiben, hat den Hintergrund, dass das Standardverhalten von SimpleFileVisitor mit dem Auslösen einer Ausnahme bei Fehlern zu einschränkend ist. Damit verhindern wir zum Beispiel einen Komplettabbruch bei einer java.nio.file.AccessDeniedException, die immer dann ausgelöst wird, wenn Zugriffsrechte fehlen. Wir wollen nicht, dass die Abarbeitung gänzlich beendet wird und die JVM mit einem Error abbricht, sondern wir wollen diesen Unterbaum, den wir nicht besuchen können, einfach überspringen. Daher liefert unser visitFileFailed(…) im Fehlerfall nur SKIP_SUBTREE, was in unserer Implementierung auch bedeutet, dass die Suche gleich im Baum abgebrochen wird, wenn nur ein Dateizugriff zu einem Fehler führt. Sollte die Suche bei einem Fehler komplett beendet werden, ist die Rückgabe TERMINATE zu setzen.
Zyklen erkennen, Verknüpfungen verfolgen, Tiefen angeben
Die einfachen statischen Methoden walk(Path) und Files.walkFileTree(Path, FileVisitor) laufen den Verzeichnisbaum bis in eine beliebige Tiefe ab.[ 71 ](Nur theoretisch durch Integer.MAX_VALUE beschränkt.) Zudem folgen sie standardmäßig keinen Verknüpfungen. Für einfache Durchsuchungen ist die Methode gut geeignet, aber wer mehr Gestaltungsraum sucht, der greift zu einer alternativen statischen Methode:
Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options)(seit Java 8) bzw.
Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)
Das Startverzeichnis und die Rückgabe Stream bzw. der Parameter FileVisitorbleiben, neu sind die maximale Suchtiefe in Verzeichnisebenen (nicht in der Anzahl der Dateien) und Aufzählungselemente, die bestimmen, ob Zyklen erkannt werden und ob symbolischen Verknüpfungen gefolgt werden soll.
FileVisitOption ist eine Aufzählung mit den Konstanten DETECT_CYCLES und FOLLOW_LINKS. Bisher kommt in der Java-API der Argumenttyp »Menge von Aufzählungselementen« (Set<FileVisitOption> bei walkFileTree(…)) selten vor. Hier müssen sich Entwickler zurückerinnern, wie ein EnumSet (Menge bestehend aus Aufzählungen) einfach aufgebaut werden kann. Dazu einige Beispiele:
FileVisitor<? super Path> v = …;
Files.walkFileTree( p, EnumSet.of( FileVisitOption.DETECT_CYCLES ), 2, v );
Files.walkFileTree( p, EnumSet.of( FileVisitOption.DETECT_CYCLES,
FileVisitOption.FOLLOW_LINKS ), 2, v );
Files.walkFileTree( p, EnumSet.allOf( FileVisitOption.class ), 2, v );
Die einfache Methode walkFileTree(Path start, FileVisitor<? super Path> visitor) ist übrigens auch nur eine Weiterleitung mit walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor). Die einfachere walk(…)-Methode nutzt ein Vararg statt der Menge.
6.2.9Rekursiv nach Dateien/Ordnern suchen mit Files.find(…) *
Neu in Java 8 ist die Methode find(…) in Files. Damit sind Dateien nach gewissen Kriterien zu finden:
static Stream<Path> find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
throws IOException
Sucht einen Verzeichnisbaum rekursiv ab und wendet auf jeden Path den Filter (Prädikat) an. Falls der Filter zusagt, kommt der Path in den Ergebnis-Stream.
[zB]Beispiel und Hinweis
Finde alle Ordner unter dem Windows-Standard-Bilderverzeichnis, und gib sie aus:
.resolve( "Pictures" ),
Integer.MAX_VALUE,
(p,attr) -> Files.isReadable( p ) && attr.isDirectory()
).forEach( System.out::println );
Intern greift find(…) auf den gleichen Mechanismus wie walk(…) zurück, doch ist eine Eigenimplementierung mithilfe von walk(…) mitunter besser, da wir beim visitFileFailed(…) Fehler ignorieren können – bei find(…) führen Fehler direkt zum Abbruch. Bei Windows führt eine rekursive Suche schnell zu einer java.nio.file.AccessDeniedException durch einen Ordner, an den Java nicht heran darf, und dann ist mit find(…) sofort Schluss.
6.2.10Dateisysteme und Dateisystemattribute *
Dateisysteme werden durch den Typ FileSystem beschrieben, und die Utility-Klasse FileSystems bietet die wichtige Methode getDefault(), die das Standard-Dateisystem zurückgibt. Da es aber unterschiedliche Dateisysteme geben kann – und jedes durch eine Implementierung von FileSystemProvider realisiert wird –, müssen sie unterscheidbar sein. Dafür gibt es einen URI, bei dem das Protokoll ausschlaggebend ist.
[zB]Beispiel
Laufe alle installierten FileSystemProvider ab, und gib die unterstützten Protokolle aus:
System.out.println( p.getScheme() );
Die Ausgabe ergibt »file« und »jar«, woran wir ablesen können, dass NIO.2 das Standard-Dateisystem sowie Java- bzw. ZIP-Archive als Dateisystem unterstützt. Die internen realisierenden Klassen sind sun.nio.fs.WindowsFileSystemProvider und com.sun.nio.zipfs.ZipFileSystemProvider, wobei der ZIP-Provider aus jdk1.8.0\jre\lib\ext\zipfs.jar stammt.
Neue Dateisysteme lassen sich zum Beispiel für die Protokolle http, svn, memory[ 72 ](https://github.com/marschall/memoryfilesystem) usw. aufbauen.
Implementierung
Die Aufrufe FileSystems.getDefault() und FileSystems.getFileSystem(new URI("file:/")) führen unter Windows zum gleichen Ergebnis: zur Klasse sun.nio.fs.WindowsFileSystem.
Mit einem FileSystem lässt sich dann über die bekannte Methode getPath(String first, String… more) ein Pfad erfragen. Ob ein Dateisystem nur lesbar ist, beantwortet isReadOnly(). Da es unterschiedliche Pfadtrenner je nach Dateisystem geben kann, liefert getSeparator() einen String mit dem Separator.
Eine weitere Methode ist getRootDirectories(), die ein Iterable<Path> für die Wurzelverzeichnisse liefert.
[zB]Beispiel
Gib alle Wurzelverzeichnisse aus:
System.out.println( root );
Die Ausgabe könnte C:\ und D:\ sein.
Eine ZIP-Datei komplett ablaufen
Da das JDK einen Dateisystem-Provider für ZIP-Archive mitbringt, lassen sich ZIP-Archive genauso ablaufen wie »normale« Dateisysteme. Unser nächstes Beispiel soll das rt.jar – Java-Archive sind ZIP-Archive – ablaufen. Das sieht so aus:
Listing 6.19com/tutego/insel/nio2/WalkZipFile.java, main()
try ( FileSystem fs = FileSystems.newFileSystem( path, null ) ) {
Files.walkFileTree( fs.getPath( "/" ), new SimpleFileVisitor<Path>() {
String indent = "";
@Override
public FileVisitResult visitFile( Path path, BasicFileAttributes attrs ) {
System.out.println( " " + indent + path );
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory( Path path, BasicFileAttributes attrs ) {
System.out.println( "cd " + indent + path );
indent += " ";
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory( Path path, IOException ioe ) {
indent = indent.substring( 2 );
return FileVisitResult.CONTINUE;
}
} );
}
Die ZIP-Datei bildet eine Ressource, die geschlossen werden muss. Daher kommt der Aufbau in ein try mit Ressourcen. Der Ausdruck Paths.get(System.getProperty("java.home"), "lib", "rt.jar") liefert über eine Systemeigenschaft einen kompletten Pfad zur rt.jar-Datei.
Der Ablauf führt zur folgenden (gekürzten) Ausgabe:
cd /sunw/
cd /sunw/util/
/sunw/util/EventObject.class
/sunw/util/EventListener.class
cd /sunw/io/
/sunw/io/Serializable.class
cd /sun/
cd /sun/util/
/sun/util/PreHashedMap.class
...
cd /com/oracle/
cd /com/oracle/net/
/com/oracle/net/Sdp.class
/com/oracle/net/Sdp$SdpSocket.class
/com/oracle/net/Sdp$1.class
cd /META-INF/
/META-INF/MANIFEST.MF
[»]Hinweis
Damit NIO.2 eine ZIP-Datei auch anlegen kann, muss eine spezielle Eigenschaft[ 73 ](Bisher gibt es nur zwei Eigenschaften: create und encoding (http://docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/zipfilesystemproviderprops.html). Kompressionsgrad oder Ähnliches lässt sich nicht angeben.) gesetzt werden:
URI uri = URI.create( "jar:" + p );
Map<String,String> env = new HashMap<>();
env.put( "create", "true" );
try ( FileSystem zipfs = FileSystems.newFileSystem( uri, env ) ) {
Files.write( zipfs.getPath( "/j1.txt" ),
"The truth is out there. Anybody got the URL?".getBytes() );
Files.write( zipfs.getPath( "/j2.txt" ),
"The more I C, the less I see.".getBytes() );
}
FileStore und Attribute eines Dateisystems
Die Methode getRootDirectories() liefert nur Path-Objekte, aber sonst keine weiteren Informationen zum Dateisystem. Die physikalischen Eigenschaften lassen sich auch nicht über das FileSystem-Objekt erfragen, sondern sind in eine Extraklasse FileStore ausgelagert. Die FileSystem-Methode getFileStores() liefert eine Iteration über die FileStore-Objekte.
Abbildung 6.8Klassendiagramm von FileStore
Listing 6.20com/tutego/insel/nio2/FileStoreDemo.java, main()
long total = store.getTotalSpace() >> 30;
long available = store.getUsableSpace() >> 30;
System.out.println( store + " " + store.name() + " " +
available + " GiB frei von " + total + " GiB" +
", Typ " + store.type() );
}
Mit dem Verschiebeoperator >> 30 bekommen wir gerade die Umrechnung von Byte nach Gibibyte (also Gigabyte, aber binär gesehen), denn 2^30 Byte = 1.073.741.824 Byte. Die Ausgabe kann dann etwa sein:
(D:) 13 GiB frei von 122 GiB, Typ NTFS
6.2.11Verzeichnisse im Dateisystem überwachen *
Schreibt eine Anwendung etwa Log-Dateien in ein Verzeichnis und soll ein anderes Programm dies erkennen, so ist eine Lösung, ständig das Verzeichnis abzulaufen und nach Änderungen zu suchen. Allerdings ist das ziemlich informant, und es gibt in der Java-Bibliothek eine besser Lösung, die (im Idealfall) mit Betriebssystemunterstützung aufwarten kann: Ein WatchService überwacht ein bestimmtes Verzeichnis, arbeitet aber nicht rekursiv. Die Implementierung bekommt diese Ereignisse vom Betriebssystem, welches die Änderungen an Java weitergibt. Daher ist es auch nicht möglich, entfernte Dateisysteme zu überwachen – unter Windows etwa die, die über einen UNC-Pfad angegeben sind –, da die Dateisystem-Ereignisse nicht an andere Rechner verteilt werden.
Ein Beispiel, um auf Änderungen im Verzeichnis C:\ zu reagieren:
Listing 6.21com/tutego/insel/nio2/WatchServiceDemo.java, main()
Paths.get( "C:/" ).register( watcher, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY );
while ( true ) {
WatchKey key = watcher.take();
System.out.println( "Änderung" );
for ( WatchEvent<?> event : key.pollEvents() )
System.out.println( "Kind: " + event.kind() + ", Path: " + event.context() );
key.reset();
}
Ein Ablauf kann so aussehen:
Kind: ENTRY_CREATE, Path: tutego – Kopie.log
Kind: ENTRY_MODIFY, Path: tutego – Kopie.log
Änderung
Kind: ENTRY_MODIFY, Path: tutego – Kopie.log
Änderung
Kind: ENTRY_DELETE, Path: tutego – Kopie.log
Kind: ENTRY_CREATE, Path: tutego2.log
Kind: ENTRY_MODIFY, Path: tutego2.log
Änderung
Kind: ENTRY_DELETE, Path: tutego2.log
Die WatchService-API
Ein WatchService überwacht Änderungen an Watchable-Objekten. Path ist bisher die einzige Klasse, die die Schnittstelle Watchable implementiert. Grundsätzlich ist der Überwachungsdienst nicht an Dateien und Verzeichnisse gebunden, doch Oracle hat WatchService und Watchable in das Paket java.nio.file gelegt. Es bleibt abzuwarten, ob Oracle diese API auch noch für andere Dinge nutzen wird.
Ein Watchable-Objekt, wie also unser Path, meldet sich mit register(…) an einem WatchService an. Der WatchService stammt von FileSystems.getDefault().newWatchService(). Da es verschiedene Ereignistypen gibt, ist register(…) mit einem Varargs-Parametertyp deklariert, der WatchEvent.Kind fordert: die Aufzählung StandardWatchEventKinds deklariert drei mögliche WatchEvent.Kind-Typen.
Nach der Registrierung wird der Watcher nach den angefallenen Ereignissen gefragt. Hier gibt es eine Variante mit take(), die blockierend wartet, oder poll(), das null liefert, falls keine Ereignisgruppe vorliegt. Mit close() lässt sich der WatchService beenden.
Das mit take() oder poll() entnommene Element ist vom Typ WatchKey. Ein WatchKey ist eine Art Gruppe aus einzelnen WatchEvents. Auf einem WatchKey-Objekt liefert pollEvents() genau diese List<WatchEvent<?>>. Vom WatchEvent erfragt kind() den WatchEvent.Kind, liefert also etwa StandardWatchEventKinds.ENTRY_CREATE, und context() liefert das Objekt, auf das es sich bezog. Bei Dateisystemen dürfte context() immer ein Path liefern, aber der Rückgabetyp von context() ist mit T parametrisiert, was uns allerdings nicht hilft, denn key.pollEvents() liefert nur einen WatchEvent<?>, also ohne Typ. Hier zeigt sich, dass Wildcards in der Rückgabe nicht besonders hilfreich sind.
[»]Hinweis
Der Watch-Service hat mit ENTRY_CREATE, ENTRY_DELETE und ENTRY_MODIFY nur drei Anzeigemöglichkeiten. Das heißt, dass gewisse Dinge nicht kodiert werden können. Ein Umbenennen ergibt zum Beispiel eine Sequenz von drei Ereignissen, aber es gibt kein StandardWatchEventKinds.LÖSCHEN:
Kind: ENTRY_DELETE, Path: Datei
Kind: ENTRY_CREATE, Path: umbenannte Datei
Kind: ENTRY_MODIFY, Path: umbenannte Datei
Wird eine Datei verschoben, so entspricht das einem Löschen, und es gibt nur ein Ereignis:
Kind: ENTRY_DELETE, Path: zu löschende Datei
Der Watch-Service kann nicht sagen, wohin die Datei verschoben wurde, da er immer mit einem konkreten Verzeichnis assoziiert ist – die Lösung, an jedes Verzeichnis einen Watch-Service anzuhängen, ist auch nicht optimal. Wenn Dateien auf dem Dateisystem mit (Strg) + (X) ausgeschnitten werden, ist noch keine Dateioperation geschehen, denn das Ausschneiden ist nur eine Explorer-Aktion; daher gibt es auch kein Ereignis, sondern erst, wenn die Datei mit (Strg) + (V) dann tatsächlich verschoben wird. Das Gleiche gilt bei Drag & Drop. Das Ziehen selbst ist nur ein »Trick« vom Explorer, aber am Dateisystem gibt es erst dann eine Operation, wenn die Datei wirklich fallen gelassen wird.