IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]


 

14. NIO 2

 

chapitre 1 4

 

Niveau : niveau 3 Intermédiaire 

 

L'API NIO 2 a été développée sous la JSR 203 et a été ajoutée au JDK dans la version 7 de Java SE. NIO 2 est une API plus moderne et plus complète pour l'accès au système de fichiers. Son but est en partie de remplacer la classe File de la très ancienne API IO.

NIO 2 propose d'étendre les fonctionnalités relatives aux entrées/sorties : l'utilisation du système de fichiers de manière facile et les lectures/écritures asynchrones.

L'API FileSystem simplifie grandement la manipulation de fichiers et répertoires d'un système de fichiers et ajoute des fonctionnalités attendues depuis longtemps. La nouvelle API de gestion et d'accès au système de fichiers est contenue dans le package java.nio.file et ses sous-packages.

Parmi les nouvelles fonctionnalités proposées par NIO2, on peut trouver :

  • le support des liens physiques et symboliques s'ils sont pris en charge par le système de fichiers
  • la gestion des attributs sur les fichiers des systèmes Dos et POSIX
  • Le support de notifications en cas de changement dans le contenu d'un répertoire (ajout, suppression, modification d'un fichier du répertoire) en utilisant l'API WatchService
  • le support du parcours d'un répertoire avec la possibilité de filtrer les fichiers obtenus
  • l'utilisation de channels asynchrones avec lesquels les opérations de lecture/écriture sont réalisées en utilisant un pool de threads
  • l'ajout de fonctionnalités de base comme la copie ou le déplacement de fichiers
  • l'utilisation de fabriques pour permettre à l'API d'être extensible : il est par exemple possible de créer sa propre implémentation d'un système de fichiers. Une implémentation permettant de gérer les fichiers zip est d'ailleurs fournie en standard.

NIO2 est probablement l'API de Java 7 qui sera la plus utilisée par les développeurs tant elle facilite la mise en oeuvre de fonctionnalités courantes d'entrées/sorties sur un système de fichiers.

Ce chapitre contient plusieurs sections :

 

14.1. Les entrées/sorties avec Java

Depuis les débuts de Java, l'API java.io est essentiellement composée de classes et d'interfaces pour réaliser des opérations sur les flux d'octets ou de caractères. Seule la classe File permet des opérations sur les fichiers et les répertoires du système de fichiers.

L'utilisation du système de fichiers se fait donc en utilisant l'API java.io et notamment la classe java.io.File qui présente de nombreux inconvénients :

  • Plusieurs méthodes ne lèvent pas une exception en cas de problème mais renvoient un booléen. Ceci ne respecte pas ce qu'il est possible de faire pour gérer les erreurs avec Java, rend l'API incohérente et ne permet pas de connaître l'origine du problème mais seulement de savoir que la fonctionnalité a échouée
  • La méthode rename() n'a pas le même comportement sur toutes les plates-formes
  • Il n'y a pas de réel support pour les liens symboliques (symbolic links)
  • Les métadonnées (attributs de permissions, propriétaire, sécurité, ...) sont peu ou mal supportées
  • Certaines fonctionnalités de base sont absentes de l'API comme la copie ou le déplacement d'un fichier
  • Certaines fonctionnalités sont peu performantes comme par exemple la méthode listFiles() avec un répertoire contenant de nombreux fichiers
  • ...

La classe java.io.File existe depuis Java 1.0 mais elle a évoluée dans plusieurs versions de Java :

Version 

Méthodes ajoutées

1.1 

getCanonicalPath()

1.2 

getParentFile(), getAbsoluteFile(),getCanonicalFile(), toURL(), isHidden(), createNewFile(), deleteOnExit(), listFiles(), setLastModified(), setReadOnly(), listRoots(), createTempFile(), compareTo()

1.4 

création à partir d'une URI, toURI()

setWritable(), setReadable(), setExecutable(), canExecute(), getTotalSpace(), getFreeSpace(), getUsableSpace()


L'API NIO a été introduite dans Java 1.4 : elle propose entre autres l'utilisation de channels, buffers et charsets notamment pour permettre de réaliser des opérations de lectures/écritures non bloquantes (non blocking I/O).

L'API NIO 2 est une API plus moderne qui propose plusieurs caractéristiques :

  • la séparation des responsabilités : un chemin (Path) représente un élément du système de fichiers (FileSystem) stocké dans un système de stockage (FileStorage) et est manipulé en utilisant la classe Files
  • la gestion de toutes les erreurs se fait avec des exceptions
  • l'utilisation de fabriques permet de créer les différentes instances de l'API et de la rendre extensible

La classe java.io.File n'est pas deprecated mais à partir de Java 7, il est recommandé d'utiliser les classes et interfaces de l'API NIO 2 dans la mesure du possible : ceci doit être le cas dans les nouveaux développements d'autant que, pour faciliter l'intégration dans le code existant, il existe des fonctionnalités pour convertir un objet de type File en un objet de type Path et vice versa.

 

14.2. Les principales classes et interfaces

NIO 2 repose sur plusieurs classes et interfaces dont les principales sont :

  • Path : encapsule un chemin dans le système de fichiers
  • Files : contient des méthodes statiques pour manipuler les éléments du système de fichiers
  • FileSystemProvider : service provider qui interagit avec le système de fichiers sous-jacent
  • FileSystem : encapsule un système de fichiers
  • FileSystems : fabrique qui permet de créer une instance de FileSystem

Ces classes et interfaces sont regroupées dans le package java.nio.file et ses sous-packages :

  • java.nio.file
  • java.nio.file.attribute

L'interface Path décrit les fonctionnalités d'une classe qui encapsule un chemin sur le système de fichiers. Ce chemin est représenté sous la forme d'une séquence de noms qui compose la hiérarchie des répertoires du chemin. Cette séquence peut inclure le nom d'un fichier ou d'un répertoire comme dernier élément mais pas obligatoirement car un objet de type Path peut simplement encapsuler un sous-chemin.

Les méthodes de l'interface Path permettent uniquement de manipuler les éléments qui composent le chemin : elles n'ont aucune action sur le système de fichiers sous-jacent du chemin.

La classe FileStorage encapsule un système de stockage de fichiers. Elle permet d'obtenir des informations sur le système de stockage comme l'espace total ou l'espace libre. Une instance de type FileStorage est obtenue en invoquant la méthode Files.getFileStore() en lui passant en paramètre un objet de type Path encapsulant un élément du système de stockage.

La classe FileSystem est une fabrique pour créer des objets relatifs à un système de fichiers. La méthode getPath() permet d'obtenir une instance d'un chemin dans le système de fichiers. La méthode getFileStores() permet d'obtenir une collection de tous les systèmes de stockage utilisables.

La classe FileSystems permet de créer des objets de type FileSystem. La méthode statique getDefault() permet d'obtenir une instance du FileSystem par défaut. La classe FileSystems permet aussi de créer des instances personnalisées de classes de type FileSystem.

 

14.3. L'interface Path

La classe Path est une des interfaces principales de NIO 2 : elle encapsule tout ou partie d'un chemin vers un élément du système de fichiers de manière dépendante du système d'exploitation sous-jacent.

Le chemin peut concerner plusieurs types d'éléments :

  • Un fichier
  • Un répertoire
  • Un lien symbolique : permet de faire référence à un fichier ou un autre répertoire
  • Un sous-chemin

Ce chemin peut être absolu (le chemin contient une racine) ou relatif (en combinaison avec le chemin courant pour obtenir le chemin absolu). La représentation d'un chemin dépend du système de fichiers sous-jacent : par exemple, tous les systèmes d'exploitation n'utilisent pas tous le même séparateur entre les éléments d'un chemin. Un objet de type Path encapsule le chemin d'un élément du système de fichiers composé d'un ensemble d'éléments organisés de façon hiérarchique grâce à un séparateur spécifique au système.

Une instance de type Path encapsule les informations sur le chemin permettant de localiser un fichier ou un répertoire dans un système de fichiers. Elle peut contenir la racine ou le nom du fichier mais aucun des deux n'est obligatoire : elle peut contenir un sous-chemin ou uniquement le nom du fichier.

Le chemin d'un fichier ou d'un répertoire encapsulé dans une instance de type Path n'a pas forcément d'existence physique sur le système de fichiers sous-jacents.

Les instances de type Path sont immuables et utilisables dans un contexte multithread.

Elle hérite de plusieurs interfaces : Comparable<Path>, Iterable<Path> et Watchable.

La classe Path possède plusieurs méthodes qui peuvent être utilisées pour obtenir des informations sur le chemin, accéder aux éléments du chemin, convertir le chemin ou extraire des sous-chemins, ... Ces méthodes traitent le chemin lui-même mais sont sans action sur le système de fichiers sous-jacents. Aucune méthode ne concerne la gestion des extensions des fichiers.

 

14.3.1. L'obtention d'une instance de type Path

Il n'est pas possible de créer une instance de type Path sans utiliser une fabrique ou un helper qui invoque une fabrique.

Il existe plusieurs manières de créer un objet de type Path :

  • invoquer la méthode getPath() d'une instance de type FileSystem
  • invoquer la méthode Paths.get() qui invoque la méthode FileSystems.getDefault().getPath()
  • invoquer la méthode toPath() sur un objet de type java.io.File

La méthode getPath() de la classe FileSystem permet d'obtenir une instance de type Path.

Exemple ( code Java 7 ) :
   
Path chemin = FileSystems.getDefault()
  .getPath("C:/Users/jm/AppData/Local/Temp/monfichier.txt");

La classe Paths est un helper qui permet de créer facilement des instances de type Path : c'est une fabrique proposant deux surcharges de sa méthode get() qui attendent respectivement en paramètres un nombre variable d'objets de type String qui sont les éléments du chemin ou une URI.

Exemple ( code Java 7 ) :
   
Path chemin1 = Paths.get("C:/Users/jm/AppData/Local/Temp/monfichier.txt");
   
Path chemin2 = Paths.get(URI.create("file:///C:/Users/jm/AppData/Local/Temp/monfichier.txt"));
   
Path chemin3 = Paths.get(System.getProperty("java.io.tmpdir"), "monfichier.txt");

Le chemin précisé peut utiliser le séparateur du système sous-jacent.

Exemple ( code Java 7 ) :
   
Path chemin1 = Paths.get("C:\\Users\\jm\\AppData\\Local\\Temp\\monfichier.txt");

 

14.3.2. L'obtention d'éléments du chemin

Une instance de type Path stocke les éléments de la hiérarchie du chemin sous une forme séquentielle, l'élément le plus haut dans la hiérarchie (après la racine) ayant l'index 0 et l'élément le plus bas ayant l'index n-1, n étant le nombre d'éléments du chemin.

L'interface Path propose plusieurs méthodes pour retrouver un élément particulier ou un sous-chemin composé de plusieurs éléments en utilisant les index.

Méthode

Rôle

String getFileName()

Retourner le nom du dernier élément du chemin. Si le chemin concerne un fichier alors c'est le nom du fichier qui est retourné

Path getName(int index)

Retourner l'élément du chemin dont l'index est fourni en paramètre. Le premier élément possède l'index 0

int getNameCount()

Retourner le nombre d'éléments du chemin

Path getParent()

Retourner le chemin parent ou null s'il n'existe pas (dans ce cas, le chemin correspond à une racine)

Path getRoot()

Retourner la racine d'un chemin absolu (par exemple C:\ sous Dos ou / sous Unix) ou null pour un chemin relatif

String toString()

Retourner le chemin sous la forme d'une chaîne de caractères

Path subPath(int beginIndex, int endIndex)

Retourner un sous-chemin correspondant aux deux index fournis en paramètres


Exemple ( code Java 7 ) :
    Path path = Paths.get("C:/Users/jm/AppData/Local/Temp/monfichier.txt");   
    System.out.println("toString()     = " + path.toString());
    System.out.println("getFileName()  = " + path.getFileName());
    System.out.println("getRoot()      = " + path.getRoot());
    System.out.println("getName(0)     = " + path.getName(0));
    System.out.println("getNameCount() = " + path.getNameCount());
    System.out.println("getParent()    = " + path.getParent());
    System.out.println("subpath(0,3)   = " + path.subpath(0,3));

Résultat :
toString()     = C:\Users\jm\AppData\Local\Temp\monfichier.txt
getFileName()  = monfichier.txt
getRoot()      = C:\
getName(0)     = Users
getNameCount() = 6
getParent()    = C:\Users\jm\AppData\Local\Temp
subpath(0,3)   = Users\jm\AppData

Le chemin peut aussi être relatif.

Exemple ( code Java 7 ) :
    Path path = Paths.get("jm/AppData/Local/Temp/monfichier.txt");   
    System.out.println("toString()     = " + path.toString());
    System.out.println("getFileName()  = " + path.getFileName());
    System.out.println("getRoot()      = " + path.getRoot());
    System.out.println("getName(0)     = " + path.getName(0));
    System.out.println("getNameCount() = " + path.getNameCount());
    System.out.println("getParent()    = " + path.getParent());
    System.out.println("subpath(0,3)   = " + path.subpath(0, 3));

Résultat :
toString()     = jm\AppData\Local\Temp\monfichier.txt
getFileName()  = monfichier.txt
getRoot()      = null
getName(0)     = jm
getNameCount() = 5
getParent()    = jm\AppData\Local\Temp
subpath(0,3)   = jm\AppData\Local

Une instance de type Path implémente l'interface Iterator qui permet de réaliser une itération sur les éléments du chemin.

Exemple ( code Java 7 ) :
    Path path = Paths.get("C:/Users/jm/AppData/Local/Temp/monfichier.txt");
    for (Path name : path) {
      System.out.println(name);
    }

Résultat :
Users
jm
AppData
Local
Temp
monfichier.txt

 

14.3.3. La manipulation d'un chemin

L'interface Path propose plusieurs méthodes pour manipuler les chemins :

Méthode

Rôle

Path normalize()

Nettoyer le chemin en supprimant les éléments « . » et « .. » qu'il contient

Path relativize(Path other)

Retourner le chemin relatif à celui fourni en paramètres

Path resolve(Path)

Combiner deux chemins


La méthode normalize() permet d'expliciter un chemin en éliminant les éléments comme « . » et « .. »

Exemple ( code Java 7 ) :
    Path path = Paths.get("C:/Users/jm/AppData/Local/Temp/./monfichier.txt");

    System.out.println("normalize()   = " + path.normalize());
    path = Paths.get("C:/Users/admin/  ./../jm/AppData/Local/Temp/./monfichier.txt");

    System.out.println("normalize()   = " + path.normalize());

Résultat :
normalize()   = C:\Users\jm\AppData\Local\Temp\monfichier.txt
normalize()   = C:\Users\jm\AppData\Local\Temp\monfichier.txt

La méthode normalize() effectue une opération purement syntaxique : elle ne vérifie pas dans le système de fichiers le chemin qu'elle produit.

La méthode resolve() permet de combiner deux chemins. Elle attend en paramètre un chemin partiel qui ne doit pas commencer par un élément racine du système de fichiers. Si le chemin fourni en paramètre contient un élément racine, alors la méthode resolve() renvoie le chemin fourni en paramètre.

Exemple ( code Java 7 ) :
    Path path = Paths.get("C:/Users/jm/AppData/Local/");
    Path nouveauPath = path.resolve("Temp/monfichier.txt");
    System.out.println(nouveauPath);
    nouveauPath = path.resolve("C:/Temp");
    System.out.println(nouveauPath);

Résultat :
C:\Users\jm\AppData\Local\Temp\monfichier.txt
C:\Temp

La méthode Path.relativize() permet d'obtenir le chemin relatif à celui encapsulé dans l'instance de type Path. Ceci permet de définir le chemin relatif entre deux Path du système de fichiers.

La méthode relativize() effectue l'inverse de la méthode resolve() : elle ajoute au besoin dans le chemin qu'elle renvoie des éléments ./ ou ../

Exemple ( code Java 7 ) :
    Path path1 = Paths.get("C:/Users/jm");
    Path path2 = Paths.get("C:/Users/test");
    Path path1VersPath2 = path1.relativize(path2);
    System.out.println(path1VersPath2);
    Path path2VersPath1 = path2.relativize(path1);
    System.out.println(path2VersPath1);

Résultat :
..\test
..\jm

Dans cet exemple, les deux chemins ont le même répertoire père : le résultat de l'invocation de la méthode relativize() renvoie simplement un chemin qui remonte au répertoire père et descend au répertoire cible.

Exemple ( code Java 7 ) :
    Path path1 = Paths.get("C:/");
    Path path2 = Paths.get("C:/Users/test");
    Path path1VersPath2 = path1.relativize(path2);
    System.out.println(path1VersPath2);
    Path path2VersPath1 = path2.relativize(path1);
    System.out.println(path2VersPath1);

Résultat :
Users\test
..\..

Une exception est levée si un chemin relatif et un chemin absolu sont utilisés lors de l'invocation de la méthode relativize().

Exemple ( code Java 7 ) :
    Path path1 = Paths.get("test");
    Path path2 = Paths.get("C:/Users/test");
    Path path1VersPath2 = path1.relativize(path2);
    System.out.println(path1VersPath2);
    Path path2VersPath1 = path2.relativize(path1);
    System.out.println(path2VersPath1);

Résultat :
Exception in thread "main" java.lang.IllegalArgumentException: 'other' is different type
of Path
      at sun.nio.fs.WindowsPath.relativize(Unknown Source)
      at sun.nio.fs.WindowsPath.relativize(Unknown Source)
      at com.jmdoudoux.test.nio2.TestNIO2.testRelativize3(TestNIO2.java:33)
      at com.jmdoudoux.test.nio2.TestNIO2.main(TestNIO2.java:9)

 

14.3.4. La comparaison de chemins

Une instance de type Path redéfinit la méthode equals() pour permettre de tester l'égalité de l'instance avec une autre instance.

L'interface Path hérite de l'interface Comparable, ce qui permet de trier des objets de type Path.

L'interface Path propose également des méthodes permettant de comparer le début ou la fin de deux chemins

Méthode

Rôle

int compareTo(Path other)

Comparer le chemin avec celui fourni en paramètre

boolean endsWith(Path other)

Comparer la fin du chemin avec celui fourni en paramètre

boolean endsWith(String other)

Comparer la fin du chemin avec celui fourni en paramètre

boolean startsWith(Path other)

Comparer le début du chemin avec celui fourni en paramètre

boolean startsWith(String other)

Comparer le début du chemin avec celui fourni en paramètre


Attention : une instance de type Path est dépendante du système de fichiers : il n'est donc pas possible de comparer deux instances de type Path associées à deux systèmes de fichiers différents.

L'interface Path propose les méthodes startsWith() et endsWith() qui permettent respectivement de tester si le chemin commence ou se termine par la chaîne de caractères fournie en paramètre.

Exemple ( code Java 7 ) :
    Path path1 = Paths.get("C:/Users/jm");
    Path path2 = Paths.get("C:/");
   
    System.out.println(path1.startsWith("C:/"));
    System.out.println(path1.startsWith("C:/Users"));
    System.out.println(path1.startsWith(path2));
    System.out.println(path1.startsWith("C:"));
    System.out.println(path1.startsWith("Users"));
    System.out.println(path1.startsWith("/Users"));

Résultat :
true
true
true
false
false
false

 

14.3.5. La conversion d'un chemin

Les chemins encapsulés dans une instance de type Path ne sont pas toujours complets ou linéaires : par exemple un chemin relatif ne possède pas de racine ou un chemin peut contenir un lien symbolique qui fera dévier le cheminement lors de l'accès à la ressource encapsulée par le chemin.

L'interface Path propose donc plusieurs méthodes pour convertir un chemin.

Méthode

Rôle

Path toAbsolutePath()

Retourner le chemin absolu du chemin

Path toRealPath(LinkOption...)

Retourner le chemin physique du Path notamment en résolvant les liens symboliques selon les options fournies. Peut lever une exception si le fichier n'existe pas ou s'il ne peut pas être accédé

URI toUri()

Retourner le chemin sous la forme d'une URI


La méthode Path.toAbsolutePath() permet d'obtenir le chemin absolu du chemin encapsulé dans l'instance de type Path.

La méthode toRealPath() renvoie un chemin dans lequel les liens symboliques du chemin fourni en paramètre ont été résolus par rapport au système de fichiers.

Exemple ( code Java 7 ) :
    path = Paths.get("C:/Users/jm/AppData/Local/Temp/monfichier.txt");   
    System.out.println("toUri()          = " + path.toUri());
    path = Paths.get("src/monfichier.txt"); 
    System.out.println("toAbsolutePath() = " + path.toAbsolutePath());
    try {
      System.out.println("toRealPath()    = " + path.toRealPath(LinkOption.NOFOLLOW_LINKS));
    } catch (IOException ex) {
      ex.printStackTrace();
    }

Résultat :
toUri()          = file:///C:/Users/jm/AppData/Local/Temp/monfichier.txt
toAbsolutePath() = C:\Users\jm\Documents\NetBeansProjects\JavaApplication1\src\monfichier.txt
toRealPath()     = C:\Users\jm\Documents\NetBeansProjects\JavaApplication1\src\monfichier.txt

 

14.4. Glob

Un glob est un pattern qui est appliqué sur des noms de fichiers ou de répertoires : c'est une version simplifiée des expressions régulières adaptée aux noms d'éléments d'un système de fichiers.

Plusieurs méthodes de la classe Files attendent un glob en paramètre.

L'interface PathMatcher définit une méthode pour des objets dont le but est de réaliser des comparaisons sur des chemins.

Méthode

Rôle

Boolean matches(Path path)

Renvoie une booléen qui précise si le chemin correspond au pattern


Pour obtenir une instance de type PathMatcher, il faut invoquer la méthode getPathMatcher() de la classe FileSystem qui attend en paramètre une chaîne de caractères précisant la syntaxe et le pattern.

Exemple ( code Java 7 ) :
      PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
      if (matcher.matches(path)) {
        System.out.println(path);
      }

La définition d'un glob utilise une syntaxe qui lui est propre :

Motif

Rôle

*

Aucun ou plusieurs caractères

**

Aucun ou plusieurs sous-répertoires

?

Un caractère quelconque

{}

Un ensemble de motifs
exemple : {htm, html}

[]

Un ensemble de caractères.

Exemple :
[A-Z] : toutes les lettres majuscules

[0-9] : tous les chiffres

[a-z,A-Z] : toutes les lettres indépendamment de la casse

Chaque élément de l'ensemble est séparé par un caractère virgule

Le caractère - permet de définir une plage de caractères

A l'intérieur des crochets, les caractères *, ? et / ne sont pas interprétés

\

Il permet d'échapper des caractères pour éviter qu'ils ne soient interprétés.
Il sert notamment à échapper le caractère \ lui-même

Les autres caractères

Ils se représentent eux-mêmes sans être interprétés


Exemples :

Glob 

Explication

*.phpl 

tous les fichiers ayant l'extension .phpl

??? 

trois caractères quelconques

*[0-9]* 

tous les fichiers qui contiennent au moins un chiffre

*.{htm, html} 

tous les fichiers dont l'extension est htm ou html

I*.java 

tous les fichiers dont le nom commence par un i majuscule et possède une extension .java


Chaque implémentation de type FileSystem permet d'obtenir une instance de type PathMatcher en utilisant la méthode getPathMatcher() qui attend en paramètre un objet de type String contenant la syntaxe et le motif.

Exemple ( code Java 7 ) :
      String pattern = "glob:*.{text}";
      PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);

Le paramètre contient la syntaxe du motif suivi du caractère deux-points et du motif qui sera utilisé pour vérifier la correspondance. Dans l'exemple ci-dessus, la syntaxe utilisée est de type glob.

La syntaxe glob est simple mais il est aussi possible d'utiliser une expression régulière en précisant la syntaxe regex.

Une implémentation peut proposer le support d'autres syntaxes. Il est aussi possible de définir sa propre implémentation de l'interface PathMatcher.

L'interface PathMatcher ne possède qu'une seule méthode nommée matches() qui attend en paramètre un objet de type Path et renvoie un booléen.

Exemple ( code Java 7 ) :
      PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.{java,class}");
      Path filename = ...;
      if (matcher.matches(filename)) {
          System.out.println(filename);
      }

Il faut être vigilent lors de la définition du motif utilisé par le glob car le motif s'applique sur l'ensemble du chemin.

Exemple ( code Java 7 ) :
  public static void testGlob() throws IOException {
    final Path file1 = Paths.get("C:/java/test/test.java");
    final Path file2 = Paths.get("C:/java/test/test.txt");
    final Path file3 = file1.getFileName();
    
    String pattern = "glob:**/*.{java,class}";
    System.out.println("Pattern " + pattern);
    
    PathMatcher matcher = FileSystems.getDefault().getPathMatcher(pattern);
    System.out.println(file1 + " " + matcher.matches(file1));
    System.out.format("%-22s %b\n", file2, matcher.matches(file2));
    System.out.format("%-22s %b\n", file3, matcher.matches(file3));
    System.out.println("");
    
    pattern = "glob:*.java";
    System.out.println("Pattern " + pattern);
    matcher = FileSystems.getDefault().getPathMatcher(pattern);
    System.out.println(file1 + " " + matcher.matches(file1));
    System.out.format("%-22s %b\n", file3, matcher.matches(file3));
  }

Résultat :
Pattern glob:**/*.{java,class}
C:\java\test\test.java true
C:\java\test\test.txt  false
test.java              false
Pattern glob:*.java
C:\java\test\test.java false
test.java              true

 

14.5. La classe Files

La classe java.nio.file.Files est un helper qui contient une cinquantaine de méthodes statiques permettant de réaliser des opérations sur des fichiers ou des répertoires dont le chemin est encapsulé dans un objet de type Path.

La classe Files permet de réaliser des opérations de base sur les fichiers et les répertoires : création, ouverture, suppression, test d'existence, changement des permissions, ...

Ces méthodes concernent notamment :

  • La création d'éléments : createDirectory(), createFile(), createLink(), createSymbolicLink(), createTempFile(), createTempDirectory(), ...
  • La manipulation d'éléments : delete(), move(), copy(), ...
  • L'obtention du type d'un élément : isRegularFile(), isDirectory(), probeContentType(), ...
  • L'obtention de métadonnées et la gestion des permissions : getAttributes(), getPosixFilePermissions(), isReadable(), isWriteable(), size(), getFileAttributeView(), ...

NIO 2 propose une API qui facilite la manipulation des éléments du système de fichiers pour par exemple créer, supprimer, déplacer, renommer ou copier un fichier. La manipulation des fichiers et des répertoires est assurée par la classe java.nio.file.Files..

Les méthodes de la classe Files attendent généralement en paramètre au moins une instance de type Path. Certaines méthodes de la classe Files effectuent des opérations atomiques qui doivent être réalisées dans leur entièreté ou pas du tout : elles réussissent ou échouent.

 

14.5.1. Les vérifications sur un fichier ou un répertoire

La classe Files propose deux méthodes pour vérifier l'existence d'un élément dans le système de fichier :

Méthode

Rôle

boolean exists(Path)

vérifier l'existence sur le système de fichiers de l'élément dont le chemin est encapsulé dans le paramètre de type Path fourni

boolean notExists(Path)

vérifier que l'élément dont le chemin est encapsulé dans l'instance de type Path fournie en paramètre n'existe pas sur le système de fichiers


Lors d'un test d'existence d'une instance de type Path, le résultat peut avoir plusieurs valeurs :

  • L'existence de l'élément est vérifiée
  • L'inexistence de l'élément est vérifiée
  • La vérification n'a pas pu être réalisée car le statut de l'élément est inconnu : c'est par exemple le cas si l'élément n'est pas accessible

La vérification n'a pas pu être réalisée si les méthodes exists() et notExists() pour une même instance de type Path renvoient toutes les deux false.

Attention : !Files.exists(path) n'est donc pas équivalent à Files.notExists(path)

La classe Files propose plusieurs méthodes pour vérifier les droits d'accès ou le type d'un élément de type Path :

Méthode

Rôle

boolean isReadable(Path path)

Retourner true si le fichier peut être lu

boolean isWritable(Path path)

Retourner true si le fichier peut être modifié

boolean isHidden(Path path)

Retourner true si le fichier est caché

boolean isExecutable(Path path)

Retourner true si le fichier est exécutable

boolean isRegularFile(Path path)

Retourner true si l'objet encapsulé dans le Path est un fichier

boolean isDirectory(Path path)

Retourner true si l'objet encapsulé dans le Path est un répertoire

boolean isSymbolicLink(Path path)

Retourner true si l'objet encapsulé dans le Path est un lien symbolique


Exemple ( code Java 7 ) :
  public static void testAttributs() throws IOException {
    Path monFichier = Paths.get("C:/java/temp/monfichier.txt");
    boolean estLisible = Files.isRegularFile(monFichier) &
                         Files.isReadable(monFichier);
    System.out.println(monFichier + " est lisible : "+estLisible);
  }

Résultat :
C:\java\temp\monfichier.txt est lisible : true

La classe Files propose aussi plusieurs méthodes pour faire d'autres vérifications sur des éléments de type Path.

Méthode

Rôle

isSamePath(Path, Path)

Comparer les deux instances de Path pour déterminer si elles correspondent aux mêmes éléments dans le système de fichiers.

Ceci est pratique si l'un des deux Path est un lien symbolique.


Exemple ( code Java 7 ) :
  public static void sontIdentiques(String cheminCible, String cheminLien)
      throws IOException {
    Path lien = Paths.get(cheminLien);
    Path cible = Paths.get(cheminCible);
    if (Files.isSameFile(lien, cible)) {
      System.out.println("Fichiers identiques");
    } else {
      System.out.println("Fichiers différents");
    }
  }

 

14.5.2. La création d'un fichier ou d'un répertoire

L'API permet la création de fichiers, de répertoires permanents ou temporaires en utilisant plusieurs méthodes de la classe File :

Méthode

Rôle

Path createFile(Path path, FileAttribute<?>... attrs)

Créer un fichier dont le chemin est encapsulé par l'instance de type Path fournie en paramètre

Path createDirectory(Path dir, FileAttribute<?>... attrs)

Créer un répertoire dont le chemin est encapsulé par l'instance de type Path fournie en paramètre

Path createDirectories(Path dir, FileAttribute<?>... attrs)

Créer dans le répertoire dont le chemin est fourni en paramètre un sous-répertoire avec les attributs fournis

Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs)

Créer dans le répertoire dont le chemin est fourni en paramètre un sous-répertoire temporaire dont le nom utilisera le préfixe fourni

Path createTempDirectory(String prefix, FileAttribute<?>... attrs)

Créer dans le répertoire temporaire par défaut du système, un sous-répertoire temporaire dont le nom utilisera la préfixe fourni

Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs)

Créer dans le répertoire dont le chemin est fourni en paramètre un fichier temporaire dont le nom utilisera le préfixe fourni

Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs)

Créer dans le répertoire temporaire par défaut du système un fichier temporaire dont le nom utilisera le préfixe et le suffixe fournis


La méthode Files.createFile() permet de créer un fichier dont le chemin est encapsulé dans son paramètre de type Path.

La méthode createFile() attend en paramètres un objet de type Path et un varargs de type FileAttribute< ?> qui permet de préciser les attributs du fichier créé.

Exemple ( code Java 7 ) :
Pathfichier=Paths.get("/home/jm/test.txt");
Set<PosixFilePermission> perms=PosixFilePermissions.fromString("rw-rw-rw-");
FileAttribute<Set<PosixFilePermission>>attr=PosixFilePermissions.asFileAttribute(perms);
Files.createFile(fichier,attr);

Si le chemin est uniquement fourni en paramètre de la méthode createFile(), le fichier est créé avec les attributs par défaut du système.

Exemple ( code Java 7 ) :
Path monFichier = Paths.get("C:/temp/monfichier.txt");
Path file = Files.createFile(monFichier);

Par défaut, une exception de type FileAlreadyExistsException est levée si le fichier à créer existe déjà.

La méthode createTempFile() permet de créer un fichier temporaire.

Elle possède deux surcharges :

Méthode

Rôle

createTempFile(Path dir, String prefix, String suffix, FileAttribute< ?>... attrs) 

Créer un fichier temporaire dans le répertoire dont le chemin est fourni en paramètre

createTempFile(String prefix, String suffix, FileAttribute< ?>... attrs) 

Créer un fichier temporaire dans le répertoire par défaut du système


Les deux surcharges attendent en paramètres un préfixe et un suffixe qui seront utilisés pour déterminer le nom du fichier et les attributs à utiliser lors de la création du fichier. Le préfixe et le suffixe peuvent être null : s'ils sont fournis, ils seront utilisés par l'implémentation de manière spécifique pour déterminer le nom du fichier. Le format du nom du fichier créé est dépendant de la plate-forme.

Exemple ( code Java 7 ) :
 
public static void testCreateTempFile() throws IOException {
  Path tempFile = Files.createTempFile("monapp_", ".tmp");   
  System.out.format("Fichier créé : %s%n", tempFile);
}

Résultat :
Fichier créé : C:\DOCUME~1\jm\LOCALS~1\Temp\monapp_242180026059597956.tmp

La méthode createDirectory() permet de créer un répertoire : elle attend en paramètre un objet de type Path qui encapsule le chemin ou le sous-chemin du répertoire et un varargs de type FileAttribute< ?> qui permet de préciser les attributs du nouveau répertoire.

Si aucun attribut n'est fourni en paramètre, alors le répertoire est créé avec les attributs par défaut du système.

Exemple ( code Java 7 ) :
 
public static void testCreateDirectory() throws IOException {
  Path monRepertoire = Paths.get("C:/temp/mon_repertoire");
  Path file = Files.createDirectory(monRepertoire);
}

Si le répertoire à créer existe déjà alors une exception de type FileAlreadyExistsException est levée.

La méthode createDirectory() ne permet que de créer un seul sous-répertoire : le chemin ou le sous-chemin fourni ne doit donc correspondre qu'à un nouveau sous-répertoire à créer dans un répertoire existant. Dans le cas contraire, une exception de type NoSuchFileException est levée.

Exemple ( code Java 7 ) :
 
public static void testCreateDirectory() throws IOException {
  Path monRepertoire = Paths.get("C:/temp/niveau1/niveau2/mon_repertoire"); 
  Path file = Files.createDirectory(monRepertoire);
}

Résultat :
java.nio.file.NoSuchFileException:
C:\temp\niveau1\niveau2\mon_repertoire
      at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
      at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
      at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
      at sun.nio.fs.WindowsFileSystemProvider.createDirectory(Unknown Source)
      at java.nio.file.Files.createDirectory(Unknown Source)
      at com.jmdoudoux.test.nio2.TestNIO2.testCreateDirectory(TestNIO2.java:199)
      at com.jmdoudoux.test.nio2.TestNIO2.main(TestNIO2.java:28)

Pour créer toute l'arborescence fournie dans le chemin, incluant la création d'un ou plusieurs sous-répertoires manquants dans l'arborescence, il faut utiliser la méthode createDirectories().

Exemple ( code Java 7 ) :
 
public static void testCreateDirectories() throws IOException {
  Path monRepertoire = Paths.get("C:/temp/niveau1/niveau2/mon_repertoire");
  Path file = Files.createDirectories(monRepertoire);
}

Pour créer un répertoire temporaire, il faut utiliser la méthode createTempDirectory() qui possède deux surcharges :

  • createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs)
  • createTempDirectory(String prefix, FileAttribute<?>... attrs)

La surcharge qui attend en paramètre un objet de type Path permet de préciser le sous-répertoire dans lequel le répertoire temporaire va être créé. La seconde surcharge crée le sous-répertoire temporaire dans le répertoire temporaire par défaut du système d'exploitation.

Le paramètres varargs de type FileAttributs<?> permet de préciser les attributs qui seront associés au nouveau répertoire. Si aucun attribut n'est précisé alors ce sont les attributs par défaut du système qui seront utilisés.

Le paramètre prefix, qui peut être null, sera utilisé de manière dépendante de l'implémentation pour construire le nom du répertoire.

Exemple ( code Java 7 ) :
public static void testCreateTempDirectory() throws IOException {
  Path repertoireTemp = Files.createTempDirectory(null);
  System.out.println(repertoireTemp);
  repertoireTemp = Files.createTempDirectory("monApp_");
  System.out.println(repertoireTemp);
}

Résultat :
C:\DOCUME~1\jm\LOCALS~1\Temp\2626334559178550265
C:\DOCUME~1\jm\LOCALS~1\Temp\monApp_404075526480225045

 

14.5.3. La copie d'un fichier ou d'un répertoire

Ecrire sa propre méthode pour une fonctionnalité aussi basique que la copie d'un fichier ne présente pas beaucoup d'intérêt. Il est préférable d'utiliser une bibliothèque tierce comme Apache Commons IO ou Google Guava car cette fonctionnalité n'est pas proposée par l'API Java Core avant Java 7.

La classe Files propose plusieurs surcharges de la méthode copy() pour copier un fichier ou un répertoire.

Méthode

Rôle

Path copy(Path source, Path target, CopyOption... options)

Copier un élément avec les options précisées

long copy(InputStream in, Path target, CopyOption... options)

Copier tous les octets d'un flux de type InputStream vers un fichier

long copy(Path source, OutputStream out)

Copier tous les octets d'un fichier dans un flux de type OutputStream


La méthode Files.copy() permet de copier un fichier dont les chemins source et cible sont encapsulés dans ses deux paramètres de type Path.

Exemple ( code Java 7 ) :
Path monFichier =
Paths.get("C:\\temp\\monfichier.txt");
Path monFichierCopie = Paths.get("C:\\temp\\monfichier - copie.txt");
Path file = Files.copy(monFichier, monFichierCopie);

Une surcharge de la méthode copy() permet de préciser les options de copie du fichier en utilisant son troisième paramètre qui est un varargs de type CopyOption.

Plusieurs valeurs des énumérations StandardCopyOption et LinkOption qui implémentent l'interface CopyOption peuvent être utilisées avec la méthode copy() :

Valeur

Rôle

StandardCopyOption.COPY_ATTRIBUTES

La copie se fait en conservant les attributs du fichier : ceux-ci sont dépendants du système sous-jacent

StandardCopyOption.REPLACE_EXISTING

Remplacer le fichier cible s'il existe. Si le chemin cible est un répertoire non vide, une exception de type FileAlreadyExistsException est levée

LinkOption.NOFOLLOW_LINKS

Ne pas suivre les liens symboliques. Si le chemin à copier est un lien symbolique, c'est le lien lui-même qui est copié


Exemple ( code Java 7 ) :
import static java.nio.file.StandardCopyOption.*;
      
// ... 

Path monFichier = Paths.get("C:\\temp\\monfichier.txt");
Path monFichierCopie = Paths.get("C:\\temp\\monfichier - copie.txt");
Path file = Files.copy(monFichier, monFichierCopie, REPLACE_EXISTING);

Faute d'option indiquée, une exception est levée si le fichier cible existe déjà. La copie échoue si la destination existe sauf si l'option StandardCopyOption.REPLACE_EXISTING est utilisée.

La copie d'un lien symbolique duplique sa cible si l'option LinkOption.NOFOLLOW_LINKS est utilisée : dans ce cas, c'est le lien lui-même qui est copié.

Si l'option StandardCopyOption.ATOMIC_MOVE est utilisée avec la méthode copy(), alors une exception de type UnsupportedOperationException est levée.

Attention : il est possible d'utiliser la méthode copy() sur un répertoire cependant, le répertoire sera créé sans que les fichiers et les sous-répertoires ne le soient : quoi que contienne le répertoire, la méthode copy ne créé qu'un répertoire vide. Pour copier le contenu du répertoire, il faut parcourir son contenu et copier chacun des éléments un par un.

La méthode copy() possède deux surcharges qui permettent d'utiliser respectivement un objet de type InputStream comme source et un objet de type OutputStream comme cible.

Exemple ( code Java 7 ) :
public static void copierFichier2() throws IOException {
  Path cible = Paths.get("c:/java/test/monfichier_copie.txt");
  URI uri = new File("c:/java/test/monfichier.txt").toURI();
  try (InputStream in = uri.toURL().openStream()) {
    Files.copy(in, cible);
  }
}

 

14.5.4. Le déplacement d'un fichier ou d'un répertoire

Avant Java 7, la méthode rename() de la classe java.io.File ne fonctionnait pas sur tous les systèmes d'exploitation et généralement pas au travers du réseau. Bien que peu performante, la solution la plus sûre était de copier chaque octet du fichier source puis de supprimer ce fichier.

La méthode Files.move() permet de déplacer ou de renommer un fichier dont les chemins source et cible sont encapsulés dans ses deux paramètres de type Path.

Méthode

Rôle

move(Path source, Path target, CopyOption... options)

Déplacer ou renommer un élément avec les options précisées


Exemple ( code Java 7 ) :
Path monFichier = Paths.get("C:\\temp\\monfichier.txt");
Path monFichierCopie = Paths.get("C:\\temp\\monfichier.old");
Path file = Files.move(monFichier, monFichierCopie);

Les options de déplacement du fichier peuvent être précisées en utilisant son troisième paramètre de type CopyOption.

Plusieurs valeurs de l'énumération StandardCopyOption qui implémente l'interface CopyOption peuvent être utilisées avec la méthode move() :

Valeur

Rôle

StandardCopyOption.REPLACE_EXISTING

Remplacement du fichier s'il existe

StandardCopyOption.ATOMIC_MOVE

Assure que le déplacement est réalisé sous la forme d'une opération atomique. Si l'atomicité de l'opération ne peut être garantie alors une exception de type AtomicMoveNotSupportedException est levée


Exemple ( code Java 7 ) :
Path monFichier = Paths.get("C:\\temp\\monfichier.txt");
Path monFichierCopie = Paths.get("C:\\temp\\monfichier.old");
Path file = Files.move(monFichier, monFichierCopie, REPLACE_EXISTING, COPY_ATTRIBUTES);

Si la méthode move() est invoquée avec l'option StandardCopyOption.COPY_ATTRIBUTES alors une exception de type UnsupportedOperationException est levée.

Par défaut, l'invocation de la méthode move() dont le chemin cible existe déjà lève une exception de type FileAlreadyExistException. Pour écraser le fichier existant, il faut utiliser l'option StandardCopyOption.REPLACE_EXISTING.

Si le chemin source est un lien alors c'est le lien lui-même et non sa cible qui est déplacé.

Si les chemins cible et source fournis en paramètres de la méthode move() sont identiques alors l'invocation de la méthode n'a aucun effet.

Exemple ( code Java 7 ) :
  public static void testMove() throws IOException {
    Path source = Paths.get("C:/java/temp/monfichier.txt");
    Path cible = Paths.get("C:/java/temp/monfichier.txt");
    Files.move(source, cible);
  }

La méthode move() peut être utilisée sur un répertoire vide ou sur un répertoire non vide dont la cible est sur le même système de fichiers. Dans ce cas le répertoire est simplement renommé et il n'est pas nécessaire de déplacer récursivement le contenu du répertoire.

Exemple ( code Java 7 ) :
  public static void testMoveRepertoireVide() throws IOException {
    Path source = Paths.get("C:/java/temp/mon_repertoire");
    Path cible = Paths.get("C:/temp/mon_repertoire_copie");
    Files.move(source, cible);
  }

Exemple ( code Java 7 ) :
  public static void testRenommerRepertoire() throws IOException {
    Path source = Paths.get("C:/java/temp/mon_repertoire");
    Path cible = source.resolveSibling("mon_repertoire_copie");
    Files.move(source, cible);
  }

Si le répertoire cible existe déjà, même vide, alors une exception de type FileAlreadyExistsException est levée. Pour forcer le remplacement, il faut utiliser l'option REPLACE_EXISTING.

Exemple ( code Java 7 ) :
  public static void testMoveRepertoireVide() throws IOException {
    Path source = Paths.get("C:/java/temp/mon_repertoire");
    Path cible = Paths.get("C:/java/temp/mon_repertoire_copie");
    Files.move(source, cible, StandardCopyOption.REPLACE_EXISTING);
  }

Si le répertoire cible existe et n'est pas vide, alors une exception de type DirectoryNotEmptyException est levée.

Une exception de type AtomicNotSupportedException est levée si le déplacement du répertoire implique deux systèmes de fichiers différents entre la cible et la source et que l'option ATOMIC_MOVE est utilisée.

Exemple ( code Java 7 ) :
    // déplacer un fichier dans une autre unité de stockage
    source = Paths.get("c:/temp/cible.txt");
    cible = Paths.get("s:/cible.txt");
    try {
      Files.move(source, cible, ATOMIC_MOVE);
    } catch (final IOException ioe) {
      ioe.printStackTrace();
    }

Résultat :
java.nio.file.AtomicMoveNotSupportedException:
c:\temp\cible.txt -> s:\cible.txt: Impossible de déplacer le fichier vers un
lecteur de disque différent.
      at sun.nio.fs.WindowsFileCopy.move(WindowsFileCopy.java:296)
      at sun.nio.fs.WindowsFileSystemProvider.move(WindowsFileSystemProvider.java:286)
      at java.nio.file.Files.move(Files.java:1339)

Les répertoires vides peuvent être déplacés. Si le répertoire n'est pas vide alors il est possible de le déplacer à condition que son contenu n'est pas besoin de l'être : ceci dépend du système d'exploitation sous-jacent qui peut simplement renommer le répertoire si celui-ci reste sur la même unité de stockage.

Sur la plupart des systèmes, le déplacement d'un répertoire vers une cible sur le même système de stockage se fait simplement en modifiant des entrées dans la table d'allocations des fichiers.

Par contre, le déplacement vers une autre unité de stockage implique forcément le déplacement du contenu du répertoire.

Pour tout autre problème lors de l'invocation de la méthode move(), comme pour toute opération d'entrée/sortie, une erreur peut survenir : dans ce cas, la méthode lève une exception de type IOException.

L'exécution de la méthode move() se fait de manière synchrone et bloquante.

Par défaut, lors de la copie ou du déplacement d'un fichier :

  • la copie échoue si le fichier cible existe déjà
  • les attributs du fichier peuvent être conservés entièrement, partiellement ou pas du tout
  • lors de la copie d'un lien symbolique, c'est la cible du lien qui est copiée et non le lien lui-même
  • lors du déplacement d'un lien symbolique, le lien lui-même est déplacé mais le fichier cible n'est pas déplacé
  • un répertoire est déplacé seulement s'il est vide ou si le déplacement consiste simplement à le renommer

 

14.5.5. La suppression d'un fichier ou d'un répertoire

L'API permet la suppression de fichiers, de répertoires ou de liens en utilisant l'une des deux méthodes de la classe Files :

Méthode

Rôle

void delete(Path path)

Supprimer un élément du système de fichiers

boolean deleteIfExist(Path path)

Supprimer un élément du système de fichiers s'il existe


La méthode Files.delete() permet de supprimer un fichier dont le chemin est encapsulé dans son paramètre de type Path. Elle lève une exception si la suppression échoue. Par exemple, une exception de type NoSuchFileException est levée si le fichier à supprimer n'existe pas dans le système de fichiers.

La suppression d'un lien symbolique supprime le lien mais ne supprime pas le fichier cible.

La suppression d'un répertoire échoue si le répertoire n'est pas vide.

Exemple ( code Java 7 ) :
Path path = Paths.get("c:/java/test.txt");
try {
  Files.delete(path); 
} catch (NoSuchFileException nsfe) {
  System.err.println("Fichier ou repertoire " + path + " n'existe pas");
} catch (DirectoryNotEmptyException dnee) {
  System.err.println("Le repertoire " + path + " n'est pas vide");
} catch (IOException ioe) {
  System.err.println("Impossible de supprimer " + path + " : " + ioe);
}

La méthode deleteIfExist() permet de supprimer un élément du système de fichiers sans lever d'exception si celui-ci n'existe pas.

Exemple ( code Java 7 ) :
Path path = Paths.get("c:/java/test.txt");
try {
  Files.deleteIfExists(path);
} catch (DirectoryNotEmptyException dnee) {
  System.err.println("Le repertoire " + path + " n'est pas vide");
} catch (IOException ioe) {
  System.err.println("Impossible de supprimer " + path + " : " + ioe);
}

 

14.5.6. L'obtention du type de fichier

NIO2 propose une fonctionnalité pour obtenir le type du contenu d'un fichier en utilisant la méthode probeContentType() de la classe Files

Méthode

Rôle

String probeContentType(Path path)

Retourner le type du contenu du fichier dont le chemin est passé en paramètre


Exemple ( code Java 7 ) :
package com.jmdoudoux.test.nio2;
      
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class TestNIO2 {
 
  public static void main(String[] args) { 
    try {
      Path source = Paths.get("c:/java/temp/monfichier.txt");
      testProbeContent(source);
      source = Paths.get("c:/java/temp/monfichier.bin");
      testProbeContent(source);
      source = Paths.get("c:/java/temp/monfichier");
      testProbeContent(source);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 
  public static void testProbeContent(Path fichier) throws IOException {
    String type = Files.probeContentType(fichier);
    if (type == null) {
      System.out.println("Impossible de déteminer le type du fichier :"
         + fichier);
    } else {
      System.out.println("le fichier " + fichier + " est du type : " + type);
    }
  }
}

Résultat :
le fichier c:\java\temp\monfichier.txt
est du type : text/plain
Impossible de déteminer le type du
fichier : c:\java\temp\monfichier.bin
Impossible de déteminer le type du
fichier : c:\java\temp\monfichier

La méthode probeContentType() renvoie null si le type de contenu ne peut pas être déterminé.

Si le type a pu être déterminé, il est renvoyé sous la forme d'une chaîne de caractères dont le contenu respecte la norme MIME (Multipurpose Internet Mail Extensions) définit par la RFC 2045.

L'implémentation de cette méthode est dépendante de la plate-forme : sa fiabilité n'est donc pas garantie.

Il est possible de fournir une implémentation du type FileTypeDetector pour déterminer le type du contenu d'un fichier.

Si aucune implémentation de type FileTypeDetector ne peut déterminer le type, alors la méthode probeContentType() va demander au système de déterminer le type du contenu.

Pour définir sa propre implémentation, il faut créer une classe qui hérite de la classe abstraite FileTypeDetector et redéfinir sa méthode abstraite probeContentType() qui attend en paramètre un objet de type Path et renvoie une chaîne de caractères.

L'implémentation doit avoir un constructeur sans argument.

L'enregistrement de FileTypeDetector doit se faire un utilisant le service Provider de la JVM : le nom pleinement qualifié de la classe doit être dans un fichier java.nio.file.spi.FileTypeDetector contenu dans le sous-répertoire META-INF/services.

La détermination du type du contenu est généralement spécifique au système d'exploitation sous-jacent : utilisation de l'extension, de métadonnées dans un fichier associé ou lecture de tout ou partie du contenu du fichier.

 

14.6. Le parcours du contenu de répertoires

Les solutions proposées par NIO2 pour le parcours du contenu d'un répertoire remplacent avantageusement les méthodes list() et listfiles() de la classe java.io.File. Ces méthodes offraient de piètres performances notamment avec des répertoires contenant de nombreux fichiers et consommaient beaucoup de ressources.

NIO2 propose plusieurs solutions pour parcourir le contenu d'un répertoire : elles sont plus complexes à mettre en oeuvre par rapport à la classe java.io.File mais sont aussi beaucoup plus performantes surtout avec des répertoires qui contiennent de nombreux fichiers.

 

14.6.1. Le parcours d'un répertoire

Il est possible d'utiliser une instance de l'interface java.nio.file.DirectoryStream qui permet de parcourir un répertoire en réalisant une itération sur les éléments qu'il contient.

La méthode newDirectoryStream() de la classe Files attend en paramètre un objet de type Path qui correspond au répertoire à parcourir et permet d'obtenir une instance de type DirectoryStream<Path>.

La méthode iterator() retourne une instance d'un itérateur sur les éléments du répertoire : fichiers, liens, sous-répertoires, ...

L'itération sur les éléments permet de meilleures performances et une consommation réduite en ressources pour obtenir les mêmes résultats que l'invocation des méthodes list() et listFiles() de la classe java.io.File.

Attention : il est très important d'invoquer la méthode close() de l'instance de type DirectoryStream pour libérer les ressources utilisées.

Exemple ( code Java 7 ) :
  public static void testDirectoryStream() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0_02");
    DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath);
    try { 
      Iterator<Path> iterator = stream.iterator();
      while(iterator.hasNext()) {
        Path p = iterator.next();
        System.out.println(p); 
      }
    } finally { 
      stream.close(); 
    } 
  }

Résultat :
C:\Program Files\Java\jdk1.7.0\bin
C:\Program Files\Java\jdk1.7.0\COPYRIGHT
C:\Program Files\Java\jdk1.7.0\db
C:\Program Files\Java\jdk1.7.0\demo
C:\Program Files\Java\jdk1.7.0\include
C:\Program Files\Java\jdk1.7.0\jre
C:\Program Files\Java\jdk1.7.0\lib
C:\Program Files\Java\jdk1.7.0\LICENSE
C:\Program Files\Java\jdk1.7.0\README.phpl
C:\Program Files\Java\jdk1.7.0\register.phpl
C:\Program Files\Java\jdk1.7.0\register_ja.phpl
C:\Program Files\Java\jdk1.7.0\register_zh_CN.phpl
C:\Program Files\Java\jdk1.7.0\release
C:\Program Files\Java\jdk1.7.0\sample
C:\Program Files\Java\jdk1.7.0\src.zip
C:\Program Files\Java\jdk1.7.0\THIRDPARTYLICENSEREADME.txt

L'ordre dans lequel les éléments sont fournis lors de l'itération n'est pas garanti. Des éléments spécifiques à certains systèmes ne sont pas retournés dans l'itération : c'est par exemple le cas des éléments « . » (le répertoire courant) et « .. » (le répertoire parent) sur un système de type Unix.

Attention : l'implémentation de l'interface Iterable de l'instance de type DirectoryStream ne propose pas le support de la méthode remove() et son invocation lève une exception de type UnsupportedOperationException.

Exemple ( code Java 7 ) :
  public static void testDirectoryStream() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0_02");
    DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath);
    try { 
      Iterator<Path> iterator = stream.iterator();
      while(iterator.hasNext()) {
        Path p = iterator.next();
        System.out.println(p); 
        Iterator.remove();
      }
    } finally { 
      stream.close();
    } 
  }

Résultat :
C:\Program Files\Java\jdk1.7.0\bin
Exception in thread "main"
java.lang.UnsupportedOperationException
      at sun.nio.fs.WindowsDirectoryStream$WindowsDirectoryIterator.remove(Unknown
Source)
      at com.jmdoudoux.test.nio2.TestNIO2.testDirectoryStream(TestNIO2.java:138)
      at com.jmdoudoux.test.nio2.TestNIO2.main(TestNIO2.java:25)

L'interface DirectoryStream hérite des interfaces Closeable et Iterable. Il est donc pratique de déclarer l'instance de type DirectoryStream<Path> dans une instruction try avec ressources qui se chargera d'invoquer automatiquement sa méthode close(). Le parcours des éléments peut se faire dans une instruction for.

Exemple ( code Java 7 ) :
  public static void utilisationDirectoryStream() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0");
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath)) {
      for (Path entry : stream) {
        System.out.println(entry);
      }
    }
  }

Si une exception est levée durant l'itération, alors elle est encapsulée dans une exception unchecked de type DirectoryIteratorException.

Exemple ( code Java 7 ) :
  public static void testDirectoryStream3() {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0_02");
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath)) {
      for (Path entry : stream) {
        System.out.println(entry);
      }
    } catch (IOException | DirectoryIteratorException e) {
      e.printStackTrace();
    }
  }

Il est aussi possible de fournir un paramètre qui est une chaîne de caractères au format glob pour filtrer la liste des éléments retournés en fonction de leurs noms.

Exemple ( code Java 7 ) :
  public static void utilisationDirectoryStream() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0");
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath,"*.{zip,html}")) {
      for (Path entry : stream) {
        System.out.println(entry);
      }
    }
  }

Résultat :
C:\Program Files\Java\jdk1.7.0\README.phpl
C:\Program Files\Java\jdk1.7.0\register.phpl
C:\Program Files\Java\jdk1.7.0\register_ja.phpl
C:\Program Files\Java\jdk1.7.0\register_zh_CN.phpl
C:\Program Files\Java\jdk1.7.0\src.zip

Attention : il n'est possible de n'obtenir qu'un seul itérateur d'une même instance de type DirectoryStream. Une seconde invocation de la méthode iterator() lève une exception de type IllegalStateException.

Exemple ( code Java 7 ) :
  public static void utilisationDirectoryStream() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0_02");
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath)) {
      for (Path entry : stream) {
        System.out.println(entry);
        Iterator<Path> secondIterator = stream.iterator();
      }
    }
  }

Résultat :
Exception in thread "main"
java.lang.IllegalStateException: Iterator already obtained
      at sun.nio.fs.WindowsDirectoryStream.iterator(Unknown Source)
      at com.jmdoudoux.test.nio2.TestNIO2.utilisationDirectoryStream(TestNIO2.java:134)
      at com.jmdoudoux.test.nio2.TestNIO2.main(TestNIO2.java:24)

Il est possible de définir un filtre qui sera appliqué sur chacun des éléments du répertoire pour déterminer s'il doit être retourné ou non lors du parcours.

Pour cela, il faut créer une instance de type DirectoryStream.Filter<Path> et la fournir en paramètre à la méthode newDirectoryStream(). Le code du filtre doit se trouver dans la méthode accept() qui prend en paramètre un objet de type Path et renvoie un boolean qui est le résultat de l'application du filtre.

Exemple ( code Java 7 ) :
  public static void utilisationDirectoryStreamAvecFiltre() throws IOException {
    Path jdkPath = Paths.get("C:/Program Files/Java/jdk1.7.0_02");
    DirectoryStream.Filter<Path> filtre = new DirectoryStream.Filter<Path>() {
      public static final long HUIT_MEGABYTES = 8*1024*1024;
     
      @Override 
      public boolean accept(Path element) throws IOException {
        return Files.size(element) >= HUIT_MEGABYTES;
      }
    };
    
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(jdkPath, filtre)) {
      for (Path entry : stream) {
        System.out.println(entry);
      }
    }
  }

Résultat :
  C:\Program Files\Java\jdk1.7.0_02\src.zip

 

14.6.2. Le parcours d'une hiérarchie de répertoires

La méthode Files.walkFileTree() permet de parcourir la hiérarchie d'un ensemble de répertoires en utilisant le motif de conception visiteur. Ce type de parcours peut être utilisé pour rechercher, copier, déplacer, supprimer, ... des éléments de la hiérarchie parcourue.

Il faut écrire une classe qui implémente l'interface java.nio.file.FileVisitor<T>. Cette interface définit des méthodes qui seront des callbacks lors du parcours de la hiérarchie.

Méthode 

Rôle

FileVisitResult postVisitDirectory(T dir, IOException exc) 

Le parcours sort d'un répertoire qui vient d'être parcouru ou une exception est survenue durant le parcours

FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) 

Le parcours rencontre un répertoire, cette méthode est invoquée avant de parcourir son contenu

FileVisitResult visitFile(T file, BasicFileAttributes attrs) 

Le parcours rencontre un fichier

FileVisitResult visitFileFailed(T file, IOException exc) 

La visite d'un des fichiers durant le parcours n'est pas possible et une exception a été levée


Il est possible de contrôler les traitements du parcours en utilisant les objets de type FileVisitResult retournés par les méthodes de l'interface FileVisitor.

Les méthodes de l'interface FileVisitor renvoient toutes une valeur qui appartient à l'énumération FileVisitResult. Cette valeur permet de contrôler le processus de parcours de l'arborescence :

  • CONTINUE : poursuite du parcours
  • TERMINATE : arrêt immédiat du parcours
  • SKIP_SUBTREE : inhibe le parcours de la sous-arborescence. Si la méthode preVisitDirectory() renvoie cette valeur, le parcours du répertoire est ignoré
  • SKIP_SIBLING : inhibe le parcours des répertoires frères. Si la méthode preVisitDirectory() renvoie cette valeur alors le répertoire n'est pas parcouru et la méthode postVisitDirectory() n'est pas invoquée. Si la méthode postVisitDirectory() renvoie cette valeur, alors les autres répertoires frères qui n'ont pas encore été parcourus sont ignorés
Exemple ( code Java 7 ) :
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
  if (dir.getFileName().toString().equals("target")) {
    return SKIP_SUBTREE;
  }
  return CONTINUE;
}

L'exemple ci-dessous parcourt l'arborescence et s'arrête dès que le fichier test.txt est trouvé.

Exemple ( code Java 7 ) :
public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
  if (file.getFileName().equals("test.txt")) {
    System.out.println("Fichier trouve");
    return TERMINATE;
  }
  return CONTINUE;
}

L'API propose la classe java.nio.file.SimpleFileVisitor qui est une implémentation de l'interface FileVisitor. Le plus simple est donc de créer une classe fille qui hérite de la classe SimpleFileVisitor et de redéfinir les méthodes utiles selon les besoins.

L'exemple ci-dessous affiche tous les fichiers .java en ignorant les répertoires target.

Exemple ( code Java 7 ) :
  public static void testWalkFileTree() throws IOException {
    final Path repertoire = Paths.get("C:/java/projets");
    Files.walkFileTree(repertoire, new SimpleFileVisitor<Path>() {
      
      @Override
      public FileVisitResult visitFile(final Path file,
          final BasicFileAttributes attrs) throws IOException {
        final String nom = file.getFileName().toString();
        System.out.println("Fichier : " + nom);
        return FileVisitResult.CONTINUE;
      }
      
      @Override
      public FileVisitResult preVisitDirectory(final Path dir,
      final BasicFileAttributes attrs) throws IOException {
        FileVisitResult result = FileVisitResult.CONTINUE;
        System.out.println("Répertoire : " + dir);
        return result;
      }
    });
  }

Pour lancer le parcours de la hiérarchie d'un répertoire, il faut utiliser la méthode walkFileTree() de la classe Files qui propose deux surcharges :

  • Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
  • Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)

La première surcharge attend en paramètres le chemin du répertoire qui doit être parcouru et une instance de type FileVisitor qui va encapsuler les traitements du parcours.

La seconde surcharge attend deux paramètres supplémentaires qui permettent de préciser des options sous la forme d'un ensemble de type FileVisitOption et un entier qui permet de limiter le niveau de profondeur du parcours dans la hiérarchie.

L'énumération FileVisitOption ne contient que la valeur FOLLOW_LINKS qui permet de demander de suivre les liens rencontrés lors du parcours. Par défaut, les liens symboliques ne sont pas suivis par le WalkFileTree. Pour suivre les liens symboliques, il faut préciser l'utilisation de l'option FOLLOW_LINKS.

Exemple ( code Java 7 ) :
final Path repertoire = Paths.get("C:/java/projets");
   
EnumSet<FileVisitOption> options = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
   
Files.walkFileTree(repertoire, options, Integer.MAX_VALUE, new
SimpleFileVisitor<Path>() {
  // ...
});

Si l'option FOLLOW_LINK est utilisée, le walkFileTree est capable de détecter les références circulaires lors du parcours. Dans ce cas, la méthode visitFileFailed() sera invoquée et elle aura une exception de type FileSystemLoopException en paramètre.

Exemple ( code Java 7 ) :
      @Override
      public FileVisitResult visitFileFailed(Path file, IOException ioe) {
        if (ioe instanceof FileSystemLoopException) {
          System.err.println("Reference circulaire detectee : " + file);
        } else {
          ioe.printStackTrace();
        }
        return FileVisitResult.CONTINUE;
      }

Important : il n'est pas possible de présumer de l'ordre de parcours des répertoires.

Si les traitements modifient le système de fichiers, il est important de faire particulièrement attention dans l'implémentation du FileVisitor. Par exemple :

  • Si le parcours est utilisé pour supprimer une sous-arborescence, il est nécessaire de supprimer les fichiers contenus par un répertoire avant de supprimer le répertoire lui-même.
  • Si le parcours est utilisé pour copier une sous-arborescence, il faut créer le sous-répertoire avant de copier les fichiers qu'il doit contenir

 

14.6.3. Les opérations récursives

Les fonctionnalités offertes par la classe Files ne s'appliquent pas de manière récursive : il est nécessaire de parcourir l'arborescence en utilisant une des deux techniques ci-dessus pour réaliser des opérations sur un répertoire.

Par exemple, la méthode size() de la classe Files ne s'applique que sur un fichier. Pour déterminer la taille d'un répertoire (en fait la taille des fichiers qu'il contient), il faut écrire du code qui va parcourir son contenu et cumuler les tailles des fichiers qu'il contient.

Exemple ( code Java 7 ) :
  public static long getDirectorySize(final Path repertoire) throws IOException {
    final AtomicLong size = new AtomicLong();
    if (!Files.isDirectory(repertoire)) {
      throw new IllegalArgumentException(
          "Le chemin n'est pas celui d'un répertoire");
    }
    Files.walkFileTree(repertoire, new SimpleFileVisitor<Path>() {
    
      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
          throws IOException {
        if (Files.isRegularFile(file)) {
          size.addAndGet(attrs.size());
        }
        return FileVisitResult.CONTINUE;
      }
      
      @Override
      public FileVisitResult preVisitDirectory(Path dir,
          BasicFileAttributes attrs) throws IOException {
        FileVisitResult resultat = FileVisitResult.CONTINUE;
        if (!dir.equals(repertoire)) {
          size.addAndGet(getDirectorySize(dir));
          resultat = FileVisitResult.SKIP_SUBTREE;
        }
        return resultat;
      }
    });
    return size.get();
  }

Il est possible de télécharger séparément les exemples du JDK : plusieurs de ces exemples situés dans le sous-répertoire sample/nio/file concernent des fonctionnalités utilisant des opérations récursives avec l'API NIO2.

 

14.7. L'utilisation de systèmes de gestion de fichiers

Un système de gestion de fichiers est encapsulé par un objet de type FileSystem qui permet de créer des objets qui pourront interagir avec lui.

Il faut utiliser la fabrique FileSystems pour obtenir une instance de type FileSystem.

 

14.7.1. La classe FileSystems

La classe FileSystems est une fabrique pour obtenir des instances de type FileSystem.

La méthode getDefault() renvoie une instance de type FileSystem qui encapsule le système de fichiers de la JVM.

La méthode getFileSystem() renvoie une instance de type FileSystem qui encapsule le système de fichiers dont l'URI est fourni en paramètre.

Plusieurs surcharges de la méthode newFileSystem() permettent de créer une instance spécifique de type FileSystem.

 

14.7.2. La classe FileSystem

La classe FileSystem encapsule un système de fichiers. C'est essentiellement une fabrique d'instances d'objets dépendants du système encapsulé notamment : Path, PathMatcher, FileStores, WatchService, ...

Pour obtenir une instance de la classe FileSystem qui encapsule le système de fichiers par défaut, il faut utiliser la méthode getDefault() de la classe FileSystems.

Les systèmes de fichiers n'utilisent pas tous le même séparateur dans les chemins de leurs éléments : par exemple, Windows utilise le caractère antislash, les systèmes de type Unix utilisent le caractère slash, ...

Pour connaître le séparateur utilisé par le système, il est possible d'invoquer la méthode getSeparator() de la classe FileSystem.

Exemple ( code Java 7 ) :
private static void testGetSeparator() {
  String separator = FileSystems.getDefault().getSeparator();
  System.out.println(separator);
}

Résultat :
\

La méthode getRootDirectories() permet d'obtenir un objet de type Iterable<Path> qui permet d'obtenir les éléments racine du système de fichiers par défaut.

Exemple ( code Java 7 ) :
public static void getRootDirectories() throws IOException {
  Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
  for (Path name: dirs) {
    System.err.println(name);
  }
}

Résultat :
C:\

 

14.7.3. La création d'une implémentation de FileSystem

La classe FileSystem est extensible.

Il est par exemple possible de développer ses propres implémentations permettant d'offrir différentes vues d'un système de fichiers (cacher des fichiers sensibles, accès en lecture seule à tous les éléments du système, ...).

Il faut créer une classe qui hérite de la classe FileSystemProvider et une classe qui hérite de la classe FileSystem.

Exemple ( code Java 7 ) :
public class MonFileSystem extends FileSystem {
  // ...
}

La prise en compte du FileSystem se fait en utilisant le service Provider de la JVM. Il faut donc packager le Filesystem dans une archive de type jar contenant un sous-répertoire META-INF/services avec un fichier java.nio.file.spi.FileSystemProvider listant les noms pleinement qualifiés des sous-classes de type FileSystemProvider.

L'implémentation d'un FileSystem n'a pas besoin d'être liée à un «vrai» système de fichiers.

 

14.7.4. Une implémentation de FileSystem pour les fichiers Zip

L'implémentation du JDK propose en standard une implémentation spéciale de la classe FileSystem pour faciliter la manipulation de fichiers compressés au format ZIP. Son utilisation rend la manipulation d'archives de type zip beaucoup plus aisée que l'utilisation des classes du package java.util.zip.

Il faut utiliser la fabrique FileSystems pour créer une instance de type FileSystem en invoquant la méthode newFileSystem() et en lui passant en paramètre une instance de type Path qui encapsule le chemin de l'archive à manipuler.

Il est alors possible d'utiliser cette instance de FileSystem pour obtenir des chemins contenus dans l'archive puisque l'archive est vue elle-même comme un système de fichiers particulier. L'utilisation de ces chemins se fait de la même manière que pour les chemins obtenus d'une instance de type FileSystem encapsulant un système de fichiers du système d'exploitation.

L'exemple ci-dessous affiche le contenu d'un fichier contenu dans une archive de type jar.

Exemple ( code Java 7 ) :
  public static void testZip() throws IOException {
    // Path de l'archive
    final Path jarfile = Paths.get("c:/java/test/archive.jar");
          
    // création d'une instance de FileSystem pour gérer les zip
    final FileSystem fs = FileSystems.newFileSystem(jarfile, null);
    // Path du fichier à accéder dans l'archive
    final Path mf = fs.getPath("META-INF", "MANIFEST.MF");
          
    // lecture et affichage du fichier contenu dans l'archive
    try (BufferedReader readBuffer = Files.newBufferedReader(mf, 
      Charset.defaultCharset())) {
      String ligne = "";
      while ((ligne = readBuffer.readLine()) != null) {    
        System.out.println(ligne);
      }
    }
  }

L'extraction d'un fichier d'une archive de type zip se fait simplement en invoquant la méthode copy() de la classe Files en lui passant en paramètres une instance de type Path du chemin dans l'archive et une instance de type Path du chemin cible.

L'exemple ci-dessous extrait un fichier contenu dans une archive de type jar.

Exemple ( code Java 7 ) :
  public static void testZip() throws IOException {
    // Path de l'archive
    final Path jarfile = Paths.get("c:/java/test/archive.jar");
          
    // creation d'une instance de FileSystem pour gérer les zip
    final FileSystem fs = FileSystems.newFileSystem(jarfile, null);
          
    // Path du fichier cible
    final Path cible = Paths.get("c:/java/test/MANIFEST.MF");
    Files.deleteIfExists(cible);
          
    // extraire l'élément de l'archive
    Files.copy(fs.getPath("/META-INF/MANIFEST.MF"), cible);
    if (Files.exists(cible)) {
      System.out.println("fichier " + cible.getFileName() + 
        " extrait de l'archive " + jarfile);
    }
  }

Pour créer une archive de type zip vide, il faut créer une instance de type FileSystem en utilisant la méthode newFileSystem() et en lui passant en paramètre :

  • une URI du chemin de l'archive dont le protocole est jar:file:
  • une collection de type Map qui contienne une occurrence ayant pour clé "create" et pour valeur "true"

L'exemple ci-dessous crée une archive de type zip vide.

Exemple ( code Java 7 ) :
  private static FileSystem creerZipFileSystem(Path zipFile) throws IOException {
    final URI uri = URI.create("jar:file:" + zipFile.toUri().getPath());
      
    final Map<String, String> env = new HashMap<>();
    env.put("create", "true");
    return FileSystems.newFileSystem(uri, env);
  }

L'ajout d'un fichier dans une archive se fait en utilisant la méthode copy() de la classe Files avec comme paramètres le chemin de la source et le chemin dans l'archive.

L'exemple ci-dessous ajoute un nouveau fichier dans une nouvelle archive de type zip.

Exemple ( code Java 7 ) :
  public static void testAjouterZip() throws IOException {
    final Path pathZip = Paths.get("c:/java/test/monarchive.zip");
      
    Files.deleteIfExists(pathZip);
      
    // important : invoquer la méthode close() du FS
    try (FileSystem fs = creerZipFileSystem(pathZip)) {
      Path source = Paths.get("c:/java/test/monfichier.txt");
      Path dest = fs.getPath("/", "monfichier.txt");
      Files.copy(source, dest, StandardCopyOption.COPY_ATTRIBUTES);
    }
  }

Pour que le fichier soit correctement ajouté, il est important d'invoquer la méthode close() sur l'instance de type FileSystem qui encapsule l'archive. Dans l'exemple ci-dessus, cette invocation est assurée par l'utilisation d'un try-with-resource.

 

14.8. La lecture et l'écriture dans un fichier

La lecture et l'écriture dans un fichier se font toujours de la même façon avec NIO2 mais l'API propose des méthodes utilitaires pour faciliter le travail.

La gestion des opérations de types entrées/sorties a évolué au fur et à mesure des versions de Java.

IO

NIO

NIO2

Java 1.0 et 1.1

Java 1.4 (JSR 151)

Java 7 (JSR 203)

Synchrone bloquant

Synchrone non bloquant

Asynchrone non bloquant

File

InputStream

OutputStream

Reader (Java 1.1)

Writer (Java 1.1)

Socket

RandomAccessFile

FileChannel

SocketChannel

ServerSocketChannel

(Charset, Selector,

ByteBuffer)

Path

AsynchronousFileChannel

AsynchronousByteChannel

AsynchronousSocketChannel

AsynchronousServerSocketChannel

SeekableByteChannel


La classe Files propose plusieurs méthodes pour faciliter la lecture ou l'écriture de fichiers et de flux selon les besoins allant des plus simples aux plus complexes.

Les méthodes readAllBytes() et readAllLines() permettent de lire l'intégralité du contenu d'un fichier respectivement d'octets et texte. Deux surcharges de la méthode write() permettent d'écrire l'intégralité d'un fichier. Ces méthodes sont à réserver pour de petits fichiers.

Les méthodes newBufferedReader() et newBufferedWriter() sont des helpers pour faciliter la création d'objets de types BufferedReader et BufferedWriter permettant la lecture et l'écriture de fichiers de type texte en utilisant un tampon.

Les méthodes newInputStream() et newOutputStream() sont des helpers pour faciliter la création d'objets permettant la lecture et l'écriture de fichiers d'octets.

Ces quatre méthodes sont des helpers pour créer des objets du package java.io.

La méthode newByteChannel() est un helper pour créer un objet de type SeekableByteChannel.

La classe FileChannel propose des fonctionnalités avancées sur l'utilisation d'un fichier (verrous, mapping direct à une zone de la mémoire, ...) : cette classe a été enrichie pour fonctionner avec NIO2.

 

14.8.1. Les options d'ouverture d'un fichier

L'énumération StandardOpenOption implémente l'interface OpenOption et définit les options d'ouverture standard d'un fichier :

Valeur

Rôle

APPEND

Si le fichier est ouvert en écriture alors les données sont ajoutées au fichier. Cette option doit être utilisée avec les options CREATE ou WRITE

CREATE

Créer un nouveau fichier s'il n'existe pas sinon le fichier est ouvert

CREATE_NEW

Créer un nouveau fichier : si le fichier existe déjà alors une exception est levée

DELETE_ON_CLOSE

Supprimer le fichier lorsque son flux associé est fermé : cette option est utile pour des fichiers temporaires

DSYNC

Demander l'écriture synchronisée des données dans le système de stockage sous-jacent (pas d'utilisation des tampons du système)

READ

Ouvrir le fichier en lecture

SPARSE

Indiquer au système que le fichier est clairsemé ce qui peut lui permettre de réaliser certaines optimisations si l'option est supportée par le système de fichiers (c'est notamment le cas avec NTFS)

SYNC

Demander l'écriture synchronisée des données et des métadonnées dans le système de stockage sous-jacent

TRUNCATE_EXISTING

Si le fichier existe et qu'il est ouvert en écriture alors il est vidé. Cette option doit être utilisée avec l'option WRITE

WRITE

Ouvrir le fichier en écriture


Ces options sont utilisables avec toutes les méthodes qui ouvrent des fichiers. Elles ne sont pas toutes mutuellement exclusives.

 

14.8.2. La lecture et l'écriture de l'intégralité d'un fichier

La classe Files propose les méthodes readAllLines() et readAllBytes() qui renvoient respectivement une collection de type List<String> et un tableau d'octets contenant l'intégralité d'un fichier texte ou binaire. Bien sûr l'utilisation de ces méthodes est à réserver pour des fichiers de petites tailles.

La méthode readAllLines() de la classe Files permet de lire l'intégralité d'un fichier et de renvoyer son contenu sous la forme d'une collection de chaînes de caractères.

Exemple ( code Java 7 ) :
List<String> lignes =  Files.readAllLines(  
  FileSystems.getDefault().getPath("monfichier.txt"), StandardCharsets.UTF_8);  
for (String ligne : lignes)
  System.out.println(ligne);

La méthode readAllLines() attend en paramètre un objet de type Path qui encapsule le chemin du fichier à lire et un objet de type Charset qui précise le jeu d'encodage de caractères du fichier. Elle s'occupe d'ouvrir le fichier, lire le contenu et fermer le flux.

La méthode readAllBytes() de la classe Files permet de lire l'intégralité d'un fichier et renvoyer son contenu sous la forme d'un tableau d'octets.

Exemple ( code Java 7 ) :
Path file = FileSystems.getDefault().getPath("monfichier.bin");
byte[] contenu = Files.readAllBytes(file);

La méthode write() permet d'écrire le contenu d'un fichier. Elle possède deux surcharges :

  • Path write(Path path, byte[] bytes, OpenOption... options)
  • Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options)
Exemple ( code Java 7 ) :
    final Path pathSource = Paths.get("c:/java/source.txt");
    final Path pathCible = Paths.get("c:/java/cible.txt");
    final List<String> lignes = Files.readAllLines(pathSource, Charset.defaultCharset());
    Files.write(pathCible, lignes, Charset.defaultCharset());

Exemple ( code Java 7 ) :
    final Path pathSource = Paths.get("c:/java/source.bin");
    final Path pathCible = Paths.get("c:/java/cible.bin");
    // lire et écrire tout le fichier
    final byte[] bytes = Files.readAllBytes(pathSource);
    Files.write(pathCible, bytes);

 

14.8.3. La lecture et l'écriture bufférisées d'un fichier

Avant Java 7, pour lire un fichier avec un tampon, il fallait invoquer le constructeur de la classe BufferedReader en lui passant en paramètre un objet de type Reader.

Exemple :
BufferedReader in = new BufferedReader(new FileReader("monfichier.txt"));

A partir de Java 7, il est possible d'utiliser la méthode newBufferedReader() de la classe Files.

Exemple ( code Java 7 ) :
BufferedReader in = Files.newBufferedReader(Paths.get("monfichier.txt"), 
  Charset.forName("UTF-8"));

Le résultat est quasiment le même mais il est nécessaire de préciser le jeu d'encodage des caractères. La classe FileReader utilise toujours le jeu par défaut du système. Même si ce n'est pas une bonne pratique, il est possible d'obtenir ce jeu d'encodage de caractères en invoquant la méthode java.nio.charset.Charset.defaultCharset().

La méthode newBufferedReader() de la classe Files renvoie un objet de type BufferedReader qui permet de lire le fichier dont le chemin et le jeu de caractères d'encodage sont fournis en paramètres.

Exemple ( code Java 7 ) :
  public static void testNewBufferedReader() throws IOException {
    Path sourcePath = Paths.get("C:/java/temp/monfichier.txt");
    try (BufferedReader reader = Files.newBufferedReader(sourcePath,
        StandardCharsets.UTF_8)) {
      String line = null;
      while ((line = reader.readLine()) != null) {
        System.out.println(line);
      }
    }
  }

La méthode newBufferedReader() ouvre un fichier de type texte pour des lectures avec un tampon. Elle retourne un objet de type BufferedReader.

Exemple ( code Java 7 ) :
    Path fichier = Paths.get("monfichier.txt");
    Charset charset = Charset.forName("US-ASCII");
    try (BufferedReader reader = Files.newBufferedReader(fichier, charset)) {
      String line = null;
      while ((line = reader.readLine()) != null) {
        System.out.println(line);
      }
    } catch (IOException ioe) {
      ioe.printStacktrace();
    }

La méthode newBufferedWriter() ouvre un fichier de type texte pour des écritures avec un tampon. Elle retourne un objet de type BufferedWriter.

Exemple ( code Java 7 ) :
    Path fichier = Paths.get("monfichier.txt");
    Charset charset = Charset.forName("US-ASCII");
    String contenu = "Contenu du fichier";
    try (BufferedWriter writer = Files.newBufferedWriter(fichier, charset)) {
      writer.write(contenu, 0, contenu.length());
    } catch (IOException ioe) {
      ioe.printStacktrace();
    }

 

14.8.4. La lecture et l'écriture d'un flux d'octets

Les méthodes newInputStream() et newOutputStream() permettent d'obtenir une instance de type InputStream et une instance de type OutputStream sur le fichier dont le chemin est fourni en paramètre :

Méthode

Rôle

InputStream newInputStream(Path path, OpenOption... options)

Créer un objet de type InputStream

OutputStream newOutputStream(Path path, OpenOption... options)

Créer un objet de type OutputStream


Les méthodes newInputStream() et newOutputStream() attendent en paramètres un objet de type Path et un varargs de type OpenOption.

La méthode newInputStream() ouvre un fichier pour des lectures sans tampon. Elle retourne un objet de type InputStream.

Exemple ( code Java 7 ) :
  public static void testNewInputStream() throws IOException {
    Path path = Paths.get("c:/java/test/monfichier.txt");
    try (InputStream in = Files.newInputStream(path);
        BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
      String line = null;
      while ((line = reader.readLine()) != null) {
        System.out.println(line);
      }
    } catch (IOException x) {
      System.err.println(x);
    }
  }

La méthode newOutputStream() ouvre un fichier pour des écritures sans tampon. Elle retourne un objet de type OutputStream. Si aucun paramètre de type OpenOption n'est précisé, la méthode va utiliser les paramètres CREATE et TRUNCATE_EXISTING par défaut (créer le fichier s'il n'existe pas et le vider s'il existe).

Exemple ( code Java 7 ) :
  public static void testNewOutputStream() throws IOException {
    Path path = Paths.get("c:/java/test/monfichier.txt");
    try (OutputStream out = Files.newOutputStream(path,
        StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
      out.write('X');
    }
  }

 

14.8.5. La lecture et l'écriture d'un fichier avec un channel

L'API Java NIO propose de réaliser des opérations d'entrées/sorties utilisant des channels et des tampons (ByteBuffer) ce qui améliore les performances par rapport à l'API Java IO.

Par défaut les flux de java.io lisent ou écrivent uniquement un octet ou un caractère à la fois.

Les opérations de lectures/écritures de java.nio utilisent un tampon (ByteBuffer). L'interface ByteChannel propose des fonctionnalités de base pour de telles lectures ou écritures.

La méthode newByteChannel() de la classe Files renvoie une instance d'un channel NIO de type SeekableByteChannel. Elle possède deux surcharges :

  • SeekableByteChannel newByteChannel(Path path, OpenOption... options)
  • SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)

Ces deux surcharges permettent d'ouvrir ou de créer un fichier et de lui associer un channel en fonction des paramètres d'ouverture de type OpenOption fournis. Par défaut le channel est ouvert en lecture (option READ).

Exemple ( code Java 7 ) :
    final Path path = Paths.get("C:/java/test/fichier.bin");
      
    Files.deleteIfExists(path);
      
    try (SeekableByteChannel sbc = Files.newByteChannel(path, 
         StandardOpenOption.WRITE, StandardOpenOption.SYNC)) {
      
       // ...
    }

L'interface java.nio.channels.SeekableByteChannel ajoute à l'interface ByteChannel la possibilité de gérer une position dans le channel, de vider un channel et d'obtenir la taille du fichier associé au channel. Cela permet de se déplacer dans le channel pour réaliser une opération de lecture ou d'écriture sans avoir à parcourir les données jusqu'à la position désirée. Un SeekableByteChannel est donc un channel qui possède des fonctionnalités similaires à celles proposées par la classe java.io.RandomAccessFile.

L'interface SeekableByteChannel hérite des interfaces : AutoCloseable, ByteChannel, Channel, Closeable, ReadableByteChannel et WritableByteChannel.

Elle propose plusieurs méthodes pour permettre de se déplacer dans le fichier avant de réaliser une opération de lecture ou d'écriture.

Méthode 

Rôle

long position()

Retourner la position courante dans le channel

SeekableByteChannel position(long newPosition)

Changer la position dans le channel

int read(ByteBuffer dst)

Lire un ensemble d'octets du channel dans le tampon fourni en paramètre. Retourne le nombre d'octets lus ou -1 si la fin du channel est atteinte

long size()

Retourner la taille en octets du flux auquel le channel est connecté

SeekableByteChannel truncate(long size)

Tronquer le contenu de l'élément sur lequel le channel est connecté à la taille fournie en paramètre. Cela permet de redimensionner la taille du flux associé au channel avec la valeur fournie en paramètre

int write(ByteBuffer src)

Ecrire les octets fournis en paramètre à la position courante dans le channel


La méthode read() tente une lecture pour remplir le nombre d'octets du tampon passé en paramètre. Elle renvoie -1 si la fin du flux est atteinte. La position courante dans le channel est augmentée de la taille des données lues.

La méthode write() écrit les octets du tampon passé en paramètre à partir de la position courante dans le channel. Si le fichier est ouvert avec l'option APPEND, alors la position courante est située à la fin du fichier. Elle renvoie le nombre d'octets écrits. La position courante dans le channel est augmentée de la taille des données écrites.

La surcharge de la méthode position() qui attend un paramètre de type long permet de déplacer la position courante dans le channel. Elle renvoie le channel lui-même pour permettre un chaînage des appels de cette méthode. La taille du flux connecté au channel n'est pas modifiée si la valeur fournie en paramètre est supérieure à sa taille totale. L'utilisation de cette méthode n'est pas recommandée avec un channel ouvert avec l'option APPEND.

La méthode truncate() permet de réduire la taille totale du flux connecté au channel. Si la taille fournie en paramètre est inférieure à la taille totale courante, alors les octets entre la taille fournie et la taille totale sont perdus. Si la taille fournie est supérieure ou égale à la taille du flux connecté au channel alors l'invocation de la méthode n'a aucun effet. Une implémentation de cette interface peut interdire l'utilisation de cette méthode si le channel est ouvert avec l'option APPEND.

Exemple ( code Java 7 ) :
    final ByteBuffer donneesBonjour = ByteBuffer.wrap("Bonjour".getBytes());
    final ByteBuffer donneesBonsoir = ByteBuffer.wrap("Bonsoir".getBytes());
      
    final Path path = Paths.get("C:/java/test/fichier.bin");
     
    Files.deleteIfExists(path);
    try (FileChannel fileChannel = FileChannel.open(path, 
         StandardOpenOption.CREATE, StandardOpenOption.WRITE,
         StandardOpenOption.SYNC)) {
      fileChannel.position(100);
      fileChannel.write(donneesBonjour);
    }
      
    try (SeekableByteChannel sbc = Files.newByteChannel(path, 
         StandardOpenOption.WRITE, StandardOpenOption.SYNC)) {
      sbc.position(200);
      sbc.write(donneesBonsoir);
    }

La méthode Files.newByteChannel() permet de créer une instance de type SeekableByteChannel. Si le fichier connecté au channel est sur le système de fichiers par défaut, il est possible de caster l'objet retourné en un objet de type FileChannel.

La classe abstraite FileChannel propose des fonctionnalités avancées à utiliser sur un channel connecté à un fichier :

  • des octets peuvent être lus ou écrits sans modifier la position courante dans le channel
  • une région du fichier peut être mappée directement en mémoire (cette fonctionnalité est intéressante pour manipuler de gros fichiers)
  • l'écriture de données peut être forcée pour être faite directement sur le système de stockage afin d'éviter une perte de données en cas de crash du système
  • une région du fichier peut être verrouillée pour empêcher l'accès par d'autres applications

 

14.9. Les liens et les liens symboliques

Il existe deux types de liens :

  • liens physiques (hard links) : ils permettent de faire référence à un élément physique du système de fichiers qui doit exister. Si le fichier cible est modifié alors le lien est aussi modifié.
  • liens symboliques (symbolic links) : ils permettent de faire référence à un autre élément du système de fichiers. Si l'élément cible est supprimé alors le lien existe toujours mais il est invalide.

La classe Files propose deux méthodes pour créer des liens physiques et des liens symboliques.

Méthode

Rôle

Path createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs)

Créer un lien symbolique vers un élément

Path createLink(Path link, Path existing)

Créer un lien physique

 

14.9.1. La création d'un lien physique

Les liens physiques (hard links) possèdent quelques restrictions :

  • le fichier cible doit exister
  • le fichier cible doit être sur la même partition
  • il possède les mêmes attributs que le fichier cible

Pour créer un lien, il faut invoquer la méthode createLink() de la classe Files qui attend en paramètres deux objets de type Path : le premier est le chemin du lien, le second est le chemin du fichier cible qui s'il n'existe pas lèvera une exception de type NoSuchFileException.

Exemple ( code Java 7 ) :
  public static void testCreateLink() throws IOException {
    Path lien = Paths.get("c:/java/test/monlien.lnk");
    Path cible = Paths.get("c:/java/test/monfichier.txt");
    Files.createLink(lien, cible);
  }

 

14.9.2. La création d'un lien symbolique

La méthode createSymbolicLink() de la classe Files permet de créer un lien symbolique. Le premier paramètre de type Path est le chemin du lien symbolique. Le second paramètre de type Path est le chemin vers le fichier ou le répertoire cible. Le paramètre de type varargs FileAttributes permet de préciser les options du lien qui seront utilisées lors de sa création.

Exemple ( code Java 7 ) :
Path lien = Paths.get("/home/jm/monlien");
Path cible = Paths.get("/home/jm/monfichier.txt"); 
Files.createSymbolicLink(lien, cible);
if (Files.isSameFile(lien, cible)) {
  System.out.println("Identique");
} else {
  System.out.println("Non identique");
}

L'utilisation des liens symboliques est conditionnée par le fait que le système d'exploitation sous-jacent propose un support de ces liens. Si le système sous-jacent ne supporte pas les liens symboliques, une exception de type UnsupportedOperationException est levée lors de l'invocation de la méthode createSymbolicLink().

Exemple sous Windows XP

Exemple ( code Java 7 ) :
   public static void testSymbolicLink() {
    Path newLink = Paths.get("C:/test_link");
    Path target = Paths.get("C:/Users/test");
    try { 
      Files.createSymbolicLink(newLink, target);
    } catch (IOException ioe) {
      ioe.printStackTrace();
    } catch (UnsupportedOperationException uoe) {
      // Le systeme de fichiers ne supporte pas les liens symboliques. 
      uoe.printStackTrace(); 
    }
  }

Le support des liens symboliques est aussi contrôlé par un SecurityManager en utilisant l'option LinkPermission("symbolic") : leur support est désactivé par défaut. Une exception de type SecurityException peut donc être levée si un SecurityManager est utilisé et que les droits adéquats ne sont pas activés.

 

14.9.3. L'utilisation des liens et des liens symboliques

La méthode toRealPath() de l'interface Path permet de retourner un chemin dont les liens symboliques contenus dans le chemin sont résolus.

La méthode isSymbolicLink() de l'interface Path permet de déterminer si l'élément précisé par le chemin est un lien symbolique ou non.

La méthode readSymbolicLink() de la classe Files renvoie le chemin de la cible du lien symbolique ou lève une exception de type NotLinkException si l'élément dont le chemin fourni en paramètre n'est pas un lien symbolique.

La suppression d'un lien se fait en utilisant la méthode delete() de la classe Files : dans ce cas, c'est le lien qui est supprimé et non le fichier cible.

Certaines méthodes de la classe Files attendent en paramètre un varargs de type LinkOption. L'option LinkOption.NOFOLLOW_OPTIONS permet de demander de ne pas suivre les liens pour réaliser l'action demandée.

 

14.10. La gestion des attributs

Les éléments d'un système de fichiers possèdent des métadonnées généralement nommées attributs : le type d'éléments (fichier, répertoire, lien), la taille, la date de création et de modification, les permissions d'utilisation, ... Le nombre de ces métadonnées et la façon dont elles sont gérées sont dépendants du système d'exploitation.

NIO 2 permet de gérer les permissions sur les fichiers. Malheureusement, ces permissions sont dépendantes du système de fichiers sous-jacent. NIO 2 propose des classes dédiées pour chaque système de fichiers supporté qui sont regroupées dans le package java.nio.file.attribute.

L'accès aux métadonnées a été enrichi avec NIO 2 : certains attributs de base sont accessibles par la classe Files d'autres sont accessibles au travers de vues.

L'implémentation par défaut propose plusieurs vues pour les principaux types de système d'exploitation :

  • Basic : cette vue est commune à tous les systèmes d'exploitation
  • Dos : cette vue est dédiée aux systèmes d'exploitation Windows
  • Posix : cette vue est dédiée aux systèmes d'exploitation de type Unix like avec notamment une gestion sur des permissions adaptées à ce type de système

Il est aussi possible qu'une implémentation spécifique soit fournie par un tiers ou encore, de développer sa propre implémentation.

 

14.10.1. La gestion individuelle des attributs

La classe Files propose plusieurs méthodes pour obtenir individuellement certains de ces attributs pour un élément dont le chemin est fourni en paramètre.

Méthode

Rôle

boolean isDirectory(Path, LinkOption)

Renvoyer un booléen qui précise si l'élément est un répertoire

boolean isRegularFile(Path, LinkOption...)

Renvoyer un booléen qui précise si l'élément est un fichier

boolean isSymbolicLink(Path)

Renvoyer un booléen qui précise si l'élément est un lien symbolique

boolean isHidden(Path)

Renvoyer un booléen qui précise si l'élément est caché

FileTime getLastModifiedTime(Path, LinkOption...)

Renvoyer la date/heure de dernière modification de l'élément

Path setLastModifiedTime(Path, FileTime)

Modifier la date de dernière modification de l'élément

UserPrincipal getOwner(Path, LinkOption...)

Renvoyer le propriétaire du fichier

Path setOwner(Path, UserPrincipal)

Modifier le propriétaire du fichier

Set<PosixFilePermission> getPosixFilePermissions(Path, LinkOption...)

Renvoyer les droits d'un élément d'un système de type Unix

Path setPosixFilePermissions(Path, Set<PosixFilePermission>)

Modifier les droits d'un élément d'un système de type Unix

Object getAttribute(Path, String, LinkOption...)

Obtenir la valeur d'un attribut de l'élément

Path setAttribute(Path, String, Object, LinkOption...)

Modifier la valeur d'un attribut de l'élément

boolean isExecutable()

Renvoyer un booléen qui précise si l'élément peut être exécuté

boolean isReadable()

Renvoyer un booléen qui précise si l'élément peut être lu

boolean isWritable()

Renvoyer un booléen qui précise si l'élément peut être modifié

long size(Path)

Renvoyer la taille en octets d'un fichier


Il est possible d'utiliser la méthode getOwner(Path) de la classe Files pour obtenir un objet de type UserPrincipal qui encapsule le propriétaire du fichier.

Exemple ( code Java 7 ) :
  public static void testGetOwner() throws IOException {
    Path fichier = Paths.get("C:/java/temp/monfichier.txt");
    UserPrincipal owner = Files.getOwner(fichier);
    System.out.println(owner);
  }

Résultat :
THINKPAD_X60S\jm (User)

 

14.10.2. La gestion de plusieurs attributs

Si l'application a besoin de plusieurs attributs d'un même élément, il est plus efficace d'utiliser une des surcharges de la méthode readAttributes() qui renvoie un objet encapsulant des attributs d'une même famille. Les performances peuvent être dégradées si le système de fichiers est consulté plusieurs fois pour obtenir des attributs.

Méthode

Rôle

Map<String, Object> readAttributes(Path, String, LinkOption...)

Renvoyer une collection d'attributs lus en une seule opération

<A extends BasicFileAttributes> A readAttributes(Path, Class<A>, LinkOption...)

Renvoyer un objet qui encapsule les attributs lus en une seule opération. Le type de cet objet est précisé en paramètre

 

Exemple ( code Java 7 ) :
  public static void lectureBasicAttributs() {
    Path monFichier = Paths.get("C:/Users/jm/AppData/Local/Temp/monfichier.txt");
    BasicFileAttributes basicAttrs;
    try {
      basicAttrs = Files.readAttributes(monFichier, BasicFileAttributes.class);
      
      System.out.println("creationTime     = " + basicAttrs.creationTime());
      System.out.println("lastAccessTime   = " + basicAttrs.lastAccessTime());
      System.out.println("lastModifiedTime = " + basicAttrs.lastModifiedTime());
      System.out.println("isDirectory      = " + basicAttrs.isDirectory());
      System.out.println("isOther          = " + basicAttrs.isOther());
      System.out.println("isRegularFile    = " + basicAttrs.isRegularFile());
      System.out.println("isSymbolicLink   = " + basicAttrs.isSymbolicLink());
      System.out.println("size             = " + basicAttrs.size());
      System.out.println("fileKey          = " + basicAttrs.fileKey());
    } catch (IOException ex) {
      ex.printStackTrace();
    }  
  }

Résultat :
creationTime    = 2011-07-19T14:12:07.916077Z
lastAccessTime  = 2011-07-19T14:12:07.916077Z
lastModifiedTime = 2011-07-23T16:39:05.957393Z
isDirectory     = false
isOther         = false
isRegularFile   = true
isSymbolicLink  = false
size            = 16
fileKey         = null

Pour obtenir une instance de type BasicFileAttributes, il faut invoquer la méthode readAttributes() de la classe Files en lui passant en paramètre le chemin du fichier et une instance de type Class pour la classe BasicFileAttributes. Il est aussi possible de préciser des options sous la forme d'un varargs de l'énumération de type LinkOption.

La valeur LinkOption.NOFOLLOW_LINKS indique de ne pas suivre les liens symboliques.

La méthode readAttributes() permet de lire en une seule opération plusieurs attributs encapsulés dans l'objet retourné lors de son invocation, ce qui est plus efficace que de lire ces attributs un par un.

Les attributs creationTime, lastModifiedTime et lastAccessTime encapsulés dans la classe BasicFilesAttributes sont de type java.nio.file.attribute.FileTime qui encapsule un horodatage.

Il est possible de créer une instance de la classe FileTime en utilisant les méthodes :

  • from(long, TimeUnit) : créer une instance à partir de la valeur et de l'unité fournies en paramètre
  • fromMillis(long) : créer une instance à partir du nombre de millisecondes fourni en paramètre
Exemple ( code Java 7 ) :
  public static void testSetLastModifiedTime() throws IOException {
    Path fichier = Paths.get("c:/java/test/monfichier.txt");
    long currentTime = System.currentTimeMillis();
    FileTime ft = FileTime.fromMillis(currentTime);
    Files.setLastModifiedTime(fichier, ft);
  }

La méthode fileKey() renvoie un objet qui encapsule une clé unique du fichier dans le système de fichiers si celui-ci supporte cette fonctionnalité sinon elle renvoie null.

 

14.10.3. L'utilisation des vues

Les différents types de systèmes de fichiers possèdent des attributs communs mais possèdent aussi des attributs spécifiques. La notion de vue regroupe plusieurs attributs ce qui permet d'obtenir ces attributs en une fois. L'API propose en standard plusieurs vues qui sont spécialisées :

  • BasicFileAttributeView : propose une vue qui contient des attributs communs à tous les systèmes de fichiers
  • DosFileAttributeView : propose une vue qui permet un support des quatre attributs spécifiques à un système de fichiers de type DOS (readonly, hidden, system et archive)
  • PosixFileAttributeView : propose une vue qui permet un support des attributs spécifiques à un système de fichiers de type Posix notamment la gestion des droits pour le propriétaire, le groupe et les autres utilisateurs.
  • FileOwnerAttributeView : propose une vue qui permet une gestion du propriétaire de l'élément qui correspond par défaut à celui qui a créé l'élément
  • AclFileAtributeView : propose une vue qui permet le support de la gestion des droits de type ACL
  • UserDefinedFileAttributeView : propose une vue qui permet le support de métadonnées spécifiques à un système de fichiers

Une vue peut permettre un accès en lecture seule aux données ou permettre leur mise à jour.

Un système de fichiers ne peut être supporté que par la BasicFileAttributeView ou être supporté par plusieurs vues. Un système de fichiers peut même proposer une ou plusieurs vues spécifiques qui ne sont pas fournies en standard par l'API.

Pour obtenir une vue spécifique, il faut utiliser la méthode getFileAttributeView() de la classe Files en précisant le type de la vue souhaitée.

Exemple ( code Java 7 ) :
  public static void testBasicFileAttributeView() throws IOException {
    Path path = Paths.get("c:/java/test/monfichier.txt");
    BasicFileAttributeView basicView = Files.getFileAttributeView(path,
        BasicFileAttributeView.class);
    if (basicView != null) {
      BasicFileAttributes basic = basicView.readAttributes();
         
      System.out.println("isRegularfile    " + basic.isRegularFile());
      System.out.println("isDirectory      " + basic.isDirectory());
      System.out.println("isSymbolicLink   " + basic.isSymbolicLink());
      System.out.println("isOther          " + basic.isOther());
      System.out.println("size             " + basic.size());
      System.out.println("creationTime     " + basic.creationTime());
      System.out.println("lastAccesstime   " + basic.lastAccessTime());
      System.out.println("lastModifiedTime " + basic.lastModifiedTime());
    }
  }

Les informations de la vue basic peuvent aussi être obtenues en utilisant la classe Files : cependant l'utilisation de la vue permet d'obtenir toutes les informations avec un seul accès à l'élément du système d'exploitation.

 

14.10.4. La gestion des permissions DOS

La classe DosFileAttributes encapsule les attributs d'un élément d'un système de fichiers de type DOS : read only, hidden, archive et system.

Exemple ( code Java 7 ) :
  public static void testDosFileAttributes() throws IOException {
    Path fichier = Paths.get("C:/java/temp/monfichier.txt");
    try {
      DosFileAttributes attr = Files.readAttributes(fichier,
          DosFileAttributes.class);
      System.out.println("isReadOnly = " + attr.isReadOnly());
      System.out.println("isHidden   = " + attr.isHidden());
      System.out.println("isArchive  = " + attr.isArchive());
      System.out.println("isSystem   = " + attr.isSystem());
    } catch (UnsupportedOperationException ueo) {
      ueo.printStackTrace();
    }
  }

Résultat :
isReadOnly = false
isHidden   = false
isArchive  = true
isSystem   = false

Il est aussi possible d'utiliser les méthodes getAttribute() et setAttribute() de la classe Files. L'inconvénient de ces méthodes est que l'attribut concerné est fourni sous la forme d'une chaîne de caractères. Celle-ci doit être composée du nom de la vue suivi du caractère deux points suivi du nom de l'attribut.

Exemple ( code Java 7 ) :
  public static void testGetFileAttribute() throws IOException {
    Path fichier = Paths.get("C:/java/temp/monfichier.txt");
    try {
      System.out.println("isReadOnly = " + 
        Files.getAttribute(fichier,"dos:readonly", LinkOption.NOFOLLOW_LINKS));
      System.out.println("isHidden   = " + 
        Files.getAttribute(fichier,"dos:hidden", LinkOption.NOFOLLOW_LINKS));
      System.out.println("isArchive  = " + 
        Files.getAttribute(fichier,"dos:archive",LinkOption.NOFOLLOW_LINKS));
      System.out.println("isSystem   = " +  
        Files.getAttribute(fichier,"dos:system", LinkOption.NOFOLLOW_LINKS));
    } catch (UnsupportedOperationException ueo) {
      ueo.printStackTrace();
    }
  }

Si le nom de l'attribut fourni en paramètre n'est pas supporté alors une exception de type IllegalArgumentException est levée.

Exemple ( code Java 7 ) :
  public static void testGetFileAttribute() throws IOException {
    Path fichier = Paths.get("C:/java/temp/monfichier.txt");
    try {
      System.out.pr intln("isReadOnly = " + 
        Files.getAttribute(fichier,"dos:readolny", LinkOption.NOFOLLOW_LINKS));
    } catch (UnsupportedOperationException ueo) {
      ueo.printStackTrace();
    }
  }

Résultat :
Exception
in thread "main" java.lang.IllegalArgumentException: 'readolny' not
recognized
  at sun.nio.fs.AbstractBasicFileAttributeView$AttributesBuilder.<init>(Unknown Source)
  at sun.nio.fs.AbstractBasicFileAttributeView$AttributesBuilder.create(Unknown Source)
  at sun.nio.fs.WindowsFileAttributeViews$Dos.readAttributes(Unknown Source)
  at sun.nio.fs.AbstractFileSystemProvider.readAttributes(Unknown Source)
  at java.nio.file.Files.readAttributes(Unknown Source)
  at java.nio.file.Files.getAttribute(Unknown Source)
  at com.jmdoudoux.test.nio2.TestNIO2.testGetFileAttribute(TestNIO2.java:385)
  at com.jmdoudoux.test.nio2.TestNIO2.main(TestNIO2.java:57)

La méthode setAttribute() de la classe Files permet de modifier un attribut d'un élément du système de fichiers.

Exemple ( code Java 7 ) :
  public static void testSetFileAttribute() throws IOException {
    Path fichier = Paths.get("C:/java/temp/monfichier.txt");
    try {
      Files.setAttribute(fichier,"dos:hidden", false);
    } catch (UnsupportedOperationException ueo) {
      ueo.printStackTrace();
    }
  }

 

14.10.5. La gestion des permissions Posix

La gestion des permissions de type Posix se fait sur trois niveaux : propriétaire, groupe et autres utilisateurs.

Avant Java 7, la modification des attributs d'un fichier sur système POSIX devait se faire en utilisant la méthode System.exec() ou en invoquant une méthode native.

Avec NIO 2, il faut utiliser les classes PosixFilePermission et PosixFilePermissions pour gérer les permissions des systèmes de fichiers respectant la norme POSIX.

Exemple ( code Java 7 ) :
Path monFichier = Paths.get("/tmp/monfichier.txt");
Set<PosixFilePermission>filePermissions    =
  PosixFilePermissions.fromString("rw-rw-r--");
FileAttribute<Set<PosixFilePermission>> fileAttribute     =
  PosixFilePermissions.asFileAttribute(filePermissions);
Files.createFile(monFichier, fileAttribute);

Attention : les attributs réellement positionnés sur le fichier peuvent être différents en fonction de règles définies sur le système de fichiers comme par exemple l'utilisation d'un umask sous un système de type Unix.

L'interface PosixFileAttributes qui hérite de l'interface BasicFileAttributes propose des méthodes pour obtenir le propriétaire, le groupe de l'élément du système de fichiers et les permissions.

Méthode

Rôle

UserPrincipal owner()

Renvoyer le propriétaire

GroupPrincipal()

Renvoyer le groupe

Set<PosixFilePermission> permissions()

Renvoyer les permissions de lecture/écriture/exécution du propriétaire, du groupe et des autres


Exemple ( code Java 7 ) :
    Path fichier = Paths.get("/home/jm/test.txt");
    PosixFileAttributes attrs = Files.readAttributes(fichier, PosixFileAttributes.class);
    UserPrincipal owner = attrs.owner();
    GroupPrincipal group = attrs.group();
    System.out.println("Le fichier appartient à " + owner + ":" + group);

L'énumération PosixFilePermission contient des valeurs pour gérer les droits de lecture, écriture et exécution pour le propriétaire, le groupe et les autres : OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE.

Les permissions sont encapsulées dans une collection de type Set d'éléments de type PosixFilePermission.

Exemple ( code Java 7 ) :
    PosixFilePermission[] permissionsArray = { 
    PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
    PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE };
    Set<PosixFilePermission> newPermissions = new HashSet<>(
      Arrays.asList(permissionsArray));

Les gestions des permissions peut se faire en manipulant directement la collection.

Exemple ( code Java 7 ) :
    Set<PosixFilePermission> permissions = attributes.permissions();
    permissions.add(PosixFilePermission.OTHERS_READ);
    permissions.remove(PosixFilePermission.GROUP_WRITE);
    Files.setPosixFilePermissions(path, permissions);

La classe Files propose la méthode getPosixFilePermissions(Path, LinkOption ...) qui renvoie une collection de type Set<PosixFilePermission> encapsulant les permissions de lecture/écriture/exécution du propriétaire, du groupe et des autres pour l'élément dont le chemin est fourni en paramètre.

La classe PosixFilePermissions propose des méthodes pour faciliter la manipulation d'un ensemble de permissions.

Méthode

Rôle

static FileAttribute<Set<PosixFilePermission>> asFileAttribute(Set<PosixFilePermission> perms)

Créer une instance de type FileAttribute qui encapsule l'ensemble des permissions fournies en paramètre

static Set<PosixFilePermission> fromString(String perms)

Renvoyer un ensemble de permissions à partir d'une chaîne de caractères au format rwxrwxrwx

static String toString(Set<PosixFilePermission> perms)

Renvoyer une représentation de l'ensemble des permissions sous la forme d'une chaîne de caractères au format rwxrwxrwx


La méthode toString() de la classe PosixFilePermissions renvoie une chaîne de caractères qui représente les permissions.

Exemple ( code Java 7 ) :
      Path fichier = Paths.get("/home/jm/test.txt");
      PosixFileAttributes attrs = Files.readAttributes(fichier, PosixFileAttributes.class);
      Set<PosixFilePermission> permissions = attrs.permissions();
      System.out.println(PosixFilePermissions.toString(permissions));

Inversement, la méthode fromString() permet de renvoyer une collection de permissions à partir de leur représentation sous la forme d'une chaîne de caractères.

Exemple ( code Java 7 ) :
      Path fichier = Paths.get("/home/jm/test.txt");
      Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-rw-");
      FileAttribute<Set<PosixFilePermission>> attr = 
        PosixFilePermissions.asFileAttribute(perms);
      Files.createFile(fichier, attr);

La méthode setPosixFilePermission(Path, Set<PosixFilePermission>) de la classe Files permet de modifier les permissions sur un élément du système de fichiers dont le chemin est fourni en paramètre sous réserve que les droits actuels sur le fichier le permettent.

Exemple ( code Java 7 ) :
      Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-rw-r--");
      Files.setPosixFilePermissions(fichier, permissions);

 

14.11. La gestion des unités de stockages

Les fichiers et les répertoires contenus dans un système de fichiers sont stockés dans un périphérique de stockage. Ces systèmes de stockages peuvent être des unités physiques sous la forme de disques (disque dur, SSD, ...) ou des unités logiques (partitions sur un disque, ...).

La classe java.nio.file.FileStore encapsule un système de stockage.

Le point d'entrée d'un système de stockage est dépendant du système d'exploitation :

  • Sous Windows : c'est un volume désigné par une lettre suivie du caractère « : », les lettres A et B sont réservées aux lecteurs de disquettes, la lettre C est la partition de boot, les autres lettres sont attribuées aux autres partitions, disques ou systèmes de stockage externes
  • Sous Unix : c'est un point de montage qui correspond à un répertoire dans le système de fichiers

Pour obtenir une instance de la classe FileStore qui encapsule le système de stockage, il faut utiliser la méthode getFileStore() de la classe Files en lui passant en paramètres une instance de type Path qui encapsule un élément du système de fichiers correspondant au système de stockage.

La méthode getFileStores() de la classe FileSystem permet d'obtenir une instance de type Iterable<FileStore> qui contient tous les systèmes de stockage accessibles.

Exemple ( code Java 7 ) :
    Iterable<FileStore> fileStores = FileSystems.getDefault().getFileStores();
    for (FileStore fileStore : fileStores) {
      System.out.println(fileStore);
      System.out.println("name : "+ fileStore.name() + ", type : "
          + fileStore.type());
    }

La méthode supportsFileAttributView() permet de vérifier si une vue relative aux méta-données est supportée ou non par le FileStore.

Exemple ( code Java 7 ) :
    for (FileStore store : FileSystems.getDefault().getFileStores()) {
      System.out.println(store);
      System.out.println("Support BasicFileAttribute : "
          + store.supportsFileAttributeView(BasicFileAttributeView.class));
      System.out.println("Support DosFileAttribute : "
          + store.supportsFileAttributeView(DosFileAttributeView.class));
      System.out.println("Support PosixFileAttribute : "
          + store.supportsFileAttributeView(PosixFileAttributeView.class));
    }

La classe FileStore possède aussi plusieurs méthodes pour obtenir des informations concernant la taille du système de stockage :

  • sur l'espace totale avec la méthode getTotalSpace()
  • sur l'espace disponible avec la méthode getUsableSpace()
  • sur l'espace non alloué avec la méthode getUnallocatedSpace().
Exemple ( code Java 7 ) :
    final int UN_GIGA = 1024 * 1024 * 1024;
    for (FileStore store : FileSystems.getDefault().getFileStores()) {
      try {
        long total = store.getTotalSpace() / UN_GIGA;
        long used = (store.getTotalSpace() - store.getUnallocatedSpace()) / UN_GIGA;
        long avail = store.getUsableSpace() / UN_GIGA;
        System.out.format("%-20s total=%5dGo used=%5dGo avail=%5dGo%n", store,
            total, used, avail);
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

 

14.12. Les notifications de changements dans un répertoire

Avant Java 7, pour obtenir des notifications lorsque les éléments d'un répertoire étaient modifiés, il fallait développer son propre mécanisme de polling ou utiliser une bibliothèque comme JPathWatch ou JNotify.

Un polling sur le contenu du répertoire permet de savoir si une modification est intervenue dans les fichiers d'un répertoire : ceci consiste à rechercher des modifications de façon périodique en vérifiant le statut de tous les fichiers du répertoire par rapport à leur précédent état.

Java 7 propose l'API WatchService qui offre cette fonctionnalité en standard : NIO2 propose la classe WatchService qui permet d'obtenir des événements sur des actions réalisées sur un répertoire surveillé du système de fichiers. L'API WatchService est performante mais elle n'est pas récursive.

L'utilisation de l'API WatchService pour obtenir des notifications requiert la mise en oeuvre de plusieurs étapes :

  • créer une instance de type WatchService
  • enregistrer cette instance auprès du répertoire concerné en précisant le type de notifications auquel on souhaite s'abonner (création, modification, suppression). Un objet de type WatchKey est obtenu suite à cet enregistrement
  • utiliser une boucle pour obtenir les événements encapsulés dans un objet de type WatchKey
  • utiliser l'objet de type WatchKey : il faut parcourir et traiter les événements qu'il contient
  • chaque objet de type WatchKey doit être réinitialisé
  • une fois que l'objet WatchService n'est plus utile, il est préférable d'invoquer sa méthode close() pour libérer les ressources natives utilisées

 

14.12.1. La surveillance d'un répertoire

L'implémentation de la classe WatchService s'appuie généralement sur le mécanisme d'événements sous-jacent du système d'exploitation (ChangeNotification sous Windows, inotify sous Linux, FSEvents sous Mac OS X). Si un tel mécanisme n'existe pas alors l'implémentation va utiliser un mécanisme de polling. Dans tous les cas, cette implémentation est spécifique à chaque JVM et système d'exploitation.

Pour obtenir une instance de type WatchService, il faut invoquer la méthode newWatchService() de la classe FileSystem.

Exemple ( code Java 7 ) :
  WatchService watchService = FileSystems.getDefault().newWatchService();

Un objet de type WatchService peut s'utiliser sur un objet qui implémente l'interface Watchable. L'interface Path hérite de l'interface Watchable. L'interface Watchable définit deux surcharges de la méthode register() qui attendent en paramètre une instance de type WatchService et les types d'événements qui doivent être capturés.

Il faut donc créer une instance de type Path qui encapsule le chemin du répertoire que l'on souhaite surveiller. La surveillance d'un répertoire se fait en enregistrant l'objet de type WatchService auprès de l'objet de type Path qui encapsule le chemin du répertoire.

Exemple ( code Java 7 ) :
      final Path dir = Paths.get("c:/java/test");
      
      WatchKey key = dir.register(watcher, 
         StandardWatchEventKinds.ENTRY_CREATE, 
         StandardWatchEventKinds.ENTRY_DELETE, 
         StandardWatchEventKinds.ENTRY_MODIFY);

La méthode register() attend en paramètre un objet de type WatchService et un ensemble de varargs de type WatchEvent.Kind qui permet de préciser les types d'événements à revecoir. La méthode register() attend donc en paramètre l'instance de type WatchService et accepte plusieurs types événements définis dans la classe java.nio.file.StandardWatchEventKinds.

Les types d'événements concernant les modifications dans un répertoire sont définis dans la classe StandardWatchEventKinds sous la forme de champs statiques de type WatchEvent.Kind<Path>.

WatchEvent.Kind<Path> ENTRY_CREATE

un nouvel élément est créé ou renommé dans le répertoire

WatchEvent.Kind<Path> ENTRY_MODIFY

un élément du répertoire est modifié

WatchEvent.Kind<Path> ENTRY_DELETE

un élément du répertoire est supprimé ou renommé. Les modifications/suppressions du répertoire lui-même ne sont pas concernées

WatchEvent.Kind<Object> OVERFLOW

indique qu'un ou plusieurs événements peuvent avoir été perdus ou manqués


Lors de l'enregistrement d'un répertoire, il faut préciser les types d'événements auxquels on souhaite s'abonner. Les événements de type OVERFLOW sont reçus automatiquement : il n'est pas nécessaire de préciser le type OVERFLOW lors de l'enregistrement.

La méthode register() renvoie un objet de type WatchKey qui encapsule l'enregistrement du chemin avec l'objet de type WatchService.

L'interface WatchKey définit les méthodes d'un jeton qui représente l'enregistrement d'un objet WatchService sur un objet de type Watchable.

Un objet de type WatchKey reste valide jusqu'à ce que :

  • Il soit annulé en invoquant sa méthode cancel()
  • L'objet de type Watchable n'existe plus
  • La méthode close() de l'objet WatchService() est invoquée

Un objet de type WatchKey possède un état qui peut prendre plusieurs valeurs :

  • ready : l'objet peut recevoir de nouveaux événements. C'est l'état de l'objet lors de sa création
  • signaled : l'objet possède un ou plusieurs événements à traiter. Pour revenir à l'état ready, il faut invoquer la méthode reset()
  • invalid : l'objet n'est plus actif. Cet état est obtenu en invoquant sa méthode cancel(), en invoquant la méthode close() de l'objet de type WatchService ou si le répertoire n'est plus accessible

Un objet de type WatchKey encapsule le résultat de l'enregistrement du WatchService sur l'objet de type Path.

Après l'enregistrement, l'objet de type WatchKey est dans l'état ready et y reste jusqu'à ce que :

  • la méthode cancel() de l'objet WatchKey soit invoquée
  • la méthode close() de l'objet WatchService soit invoquée
  • le répertoire n'est plus accessible

Les objets de type WatchKey sont thread-safe.

Pour arrêter l'émission des événements, il faut invoquer la méthode cancel() de la classe WatchKey ou la méthode close() de la classe WatchService.

La méthode watchable() de la classe WatchKey renvoie un objet de type Path qui encapsule le chemin du répertoire sur lequel l'abonnement aux notifications a été réalisé.

 

14.12.2. L'obtention des événements

La réception des événements ne se fait pas par un mécanisme asynchrone comme enregistrer un callback de type listener : il est nécessaire de créer son propre polling pour obtenir les événements.

Le traitement des événements doit ainsi se faire dans un thread dédié pour ne pas bloquer le thread courant.

Lorsqu'un changement est détecté, l'état de l'objet WatchKey passe à signaled. Pour obtenir le ou les événements non traités liés à ces changements, il faut invoquer la méthode poll() ou take() de l'objet WatchService :

Méthode

Rôle

poll()

Retourne le prochain WatchKey ou null si aucun n'est présent

poll(long timeout, TimeUnit unit)

Retourne le prochain WatchKey en attendant le temps fourni en paramètre sous la forme d'une durée et d'une unité sinon retourne null

take()

Retourne le prochain WatchKey en attendant indéfiniment jusqu'à ce qu'un ou plusieurs événements soient disponibles


Il faut utiliser une boucle qui invoque l'une de ces méthodes pour obtenir les événements à traiter.

Exemple ( code Java 7 ) :
      while(true) { 
         WatchKey key = watchService.take();
         // ...
      }

Si un changement est détecté dans un ou plusieurs éléments du répertoire, alors l'état de l'instance du WatchKey passe à « signaled » et l'événement est mis dans une queue pour traitement.

Exemple ( code Java 7 ) :
      boolean running = true;
      // ...
      while (running) {
          try {
              // key = watcher.take();
              key = watcher.poll(1000, TimeUnit.MILLISECONDS);
              
              if (key != null) {
                  for (final WatchEvent<?> event : key.pollEvents()) {
                      final Path name = (Path) event.context();
                      System.out.format(event.kind() + " " + "%s\n", name);
                  }
                  key.reset();
              }
          } catch (final InterruptedException e) {
              e.printStackTrace();
          }
      }

La méthode pollEvents() de l'interface WatchKey permet d'obtenir tous les événements qui sont stockés dans l'objet.

Il est important d'invoquer la méthode reset() de l'interface WatchKey pour permettre de remettre son état à ready : elle renvoie un booléen qui précise si l'objet de type WatchKey est toujours valide et actif. L'invocation de la méthode reset() sur un objet de type WatchKey annulé ou déjà dans l'état ready n'a aucun effet.

Attention : lorsqu'un événement est reçu, il n'y a aucune garantie que l'opération qui est à l'orgine de l'événement soit terminée.

 

14.12.3. Le traitement des événements

Le ou les événements sont encapsulés dans un objet qui implémente l'interface WatchKey. Pour obtenir les événements, il faut invoquer la méthode pollEvents() de la classe WatchKey qui renvoie une collection de type List<WatchEvent< ?>>. Cette méthode supprime de l'objet WatchKey les événements qu'elle renvoie.

Il faut itérer sur les éléments de la collection pour traiter chacun des événements encapsulés.

Un objet de type WatchEvent<?> est typé avec un type qui sera utilisé comme contexte de l'événement.

Exemple ( code Java 7 ) :
WatchEvent<Path> evenement = (WatchEvent<Path>) event;
Path chemin = evenement.context();

Un événement obtenu par un objet de type WatchService est encapsulé dans un objet qui implémente l'interface WatchEvent<T> qui possède trois méthodes :

Méthode

Rôle

T context()

Renvoyer le contexte de l'événement

int count()

Retourner le nombre d'occurences de l'événement

WatchEvent.Kind<T> kind

Renvoyer le type de l'événement


Pour chaque événement à traiter, il est possible de connaître :

  • le type de l'événement en invoquant la méthode kind() de l'objet de type WatchEvent
  • en invoquant la méthode context() de l'objet de type WatchEvent, le chemin relatif au répertoire enregistré qui est encapsulé dans le Path sur lequel l'événement (création, suppression ou mise à jour) a eu lieu
  • le chemin du répertoire concerné (c'est notamment pratique si plusieurs répertoires ont été enregistrés) en invoquant la méthode watchable() de l'objet de type WatchKey

La méthode kind() permet d'obtenir le type de l'événement sous la forme d'une interface de type WatchEvent.Kind<T>.

La méthode count() permet de savoir combien de fois l'événement a été émis.

La méthode context() permet de renvoyer un objet qui encapsule le contexte associé à l'événement.

Exemple ( code Java 7 ) :
    for (final WatchEvent<?> event : key.pollEvents()) {
      final Path name = (Path) event.context();
      System.out.format(event.kind() + " " + "%s\n", name);
    }
    key.reset();

Attention : une fois que les événements ont été traités, il est important de remettre l'objet de type WatchKey dans l'état ready en invoquant sa méthode reset(). Si la méthode reset() renvoie false, alors l'objet de type WatchKey n'est plus valide et il faut donc interrompre les traitements d'écoute des événements.

 

14.12.4. Un exemple complet

Cette section propose un exemple complet de mise en oeuvre de l'API WatchService.

Exemple ( code Java 7 ) :
package com.jmdoudoux.test.nio2;

import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit;

public class TestWatcherService {

  public static void main(final String[] args) {
    final Path source = Paths.get("c:/java/test/fichier.txt");
    final Path copie = Paths.get("c:/java/test/fichier_copie.txt");
    final Path renomme = Paths.get("c:/java/test/fichier_nouveau.txt");

    final MonWatcher monWatcher = new MonWatcher();
    monWatcher.start();

    try {
      Thread.sleep(1000);
      System.out.println("Copie " + source + " -> " + copie);
      Files.copy(source, copie, REPLACE_EXISTING, COPY_ATTRIBUTES);

      Thread.sleep(2000);
      System.out.println("Deplacement " + copie + " -> " + renomme);
      Files.move(copie, renomme, REPLACE_EXISTING, ATOMIC_MOVE);

      Thread.sleep(2000);
      System.out.println("Supression fichier " + renomme);
      Files.deleteIfExists(renomme);

      Thread.sleep(5000);
    } catch (final IOException ioe) {
      ioe.printStackTrace();
    } catch (final InterruptedException e) {
      e.printStackTrace();
    }

    monWatcher.setRunning(false);
  }
}

class MonWatcher extends Thread {

  private boolean running = true;

  public boolean isRunning() {
    return running;
  }

  public void setRunning(final boolean running) {
    this.running = running;
  }

  @Override
  public void run() {
    WatchService watcher;
    try {
      watcher = FileSystems.getDefault().newWatchService();
      final Path dir = Paths.get("c:/java/test");

      WatchKey key = dir.register(watcher,
          StandardWatchEventKinds.ENTRY_CREATE,
          StandardWatchEventKinds.ENTRY_DELETE,
          StandardWatchEventKinds.ENTRY_MODIFY);

      while (running) {
        try {
          // key = watcher.take();
          key = watcher.poll(1000, TimeUnit.MILLISECONDS);
        } catch (final InterruptedException e) {
          e.printStackTrace();
        }

        if (key != null) {
          for (final WatchEvent<?> event : key.pollEvents()) {
            final Path name = (Path) event.context();
            System.out.format(event.kind() + " " + "%s\n", name);
          }
          boolean reset = key.reset();
          if (!reset) {
            running = false;
          }
        }
      }
    } catch (final IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Résultat :
Copie c:\java\test\fichier.txt -> c:\java\test\fichier_copie.txt
ENTRY_CREATE fichier_copie.txt
ENTRY_MODIFY fichier_copie.txt
Deplacement c:\java\test\fichier_copie.txt -> c:\java\test\fichier_nouveau.txt
ENTRY_MODIFY fichier_copie.txt
ENTRY_DELETE fichier_copie.txt
ENTRY_CREATE fichier_nouveau.txt
ENTRY_MODIFY fichier_nouveau.txt
Supression fichier c:\java\test\fichier_nouveau.txt
ENTRY_DELETE fichier_nouveau.txt

 

14.12.5. L'utilisation et les limites de l'API WatchService

L'API WatchService permet d'être notifié des changements qui surviennent sur les éléments d'une entité, par exemple sur un répertoire d'un système de fichiers.

Cette fonctionnalité est intéressante mais elle présente quelques limites qu'il est important de connaitre :

  • Aucun événements n'est émis concernant les sous-répertoires du répertoire observé : dans ce cas, il faut parcourir les sous-répertoires et enregistrer un objet de type WatchService sur chacun d'entre-eux.
  • Les performances et l'ordre des événements sont dépendants de l'implémentation.
  • Lorsqu'un evénement est reçu, il n'y a pas de garantie que les traitements à l'origine de l'événement soient terminés.

 

14.13. La gestion des erreurs et la libération des ressources

Lors d'opérations d'entrées-sorties de nombreuses erreurs inattendues peuvent survenir, par exemple un fichier qui n'existe pas, un manque de droit d'accès, une erreur de lecture, ...

Toutes ces erreurs sont encapsulées dans une exception de type IOException ou d'un de ses sous-types. Toutes les méthodes qui réalisent des opérations d'entrées-sorties peuvent lever ces exceptions.

Avant Java 7, les opérations de type I/O devaient être utilisées dans un bloc try et les exceptions pouvant être levées, traitées dans des blocs catch. La fermeture des flux devait être assurée dans un bloc finally pour garantir son exécution dans tous les cas.

Exemple :
Charset charset = Charset.forName("UTF-8");
String contenu = "Bonjour";
BufferedWriter writer = null;
try {
    writer = Files.newBufferedWriter(file, charset);
    writer.write(contenu, 0, contenu.length());
} catch (IOException ioe) {
    ioe.printStackTrace();
} finally {
    if (writer != null) {
        writer.close();
    }
}

A partir de Java SE 7, il est préférable d'utiliser l'opérateur try-with-ressources pour assurer la libération automatique des ressources et la gestion des exceptions.

Exemple ( code Java 7 ) :
      Charset charset = Charset.forName("UTF-8");
      String contenu = "Bonjour";
      try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
          writer.write(contenu, 0, contenu.length());
      } catch (IOException ioe) {
          ioe.printStackTrace();
      }

Plusieurs exceptions héritent de l'exception FileSystemException qui hérite elle-même de l'exception IOException.

La classe FileSystemException encapsule plusieurs attributs qui sont des chaînes de caractères:

  • file : le nom du fichier impliqué
  • message : un message détaillé sur l'exception
  • reason : la raison pour laquelle l'opération a échoué
  • otherFile : renvoie le nom d'un second fichier impliqué
Exemple ( code Java 7 ) :
  public static void copierFichier() {
    Path source = Paths.get("c:/java/test/monfichier.txt");
    Path cible = Paths.get("c:/java/test/monfichier_copie.txt");
    try {
      Files.copy(source, cible);
    } catch (FileAlreadyExistsException e) {
      System.err.format("Copie impossible : le fichier %s existe déjà", e.getFile());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

Résultat :
Copie impossible : le fichier
c:\java\test\monfichier_copie.txt existe déjà

De nombreuses ressources utilisées par l'API NIO 2 telles que les channels ou les flux implémentent l'interface java.io.Closeable. Ceci permet leur prise en compte par l'opérateur try-with-ressource qui invoque leur méthode close() et libère ainsi les ressources devenues inutiles.

Java 7 propose une fonctionnalité nommée Automatic Resource Management ou ARM. L'ARM propose de réduire la quantité de code à produire par le développeur pour gérer une ressource et surtout pour libérer les ressources qui lui sont associées.

Des langages comme C, C++ ou Delphi, offrent aux développeurs un contrôle total sur l'allocation et la désallocation mémoire des objets créés en utilisant des opérateurs comme malloc, free, new, delete, ...

Contrairement à eux, Java ne propose pas de contrôle sur le processus de désallocation des ressources d'un objet. La JVM propose un mécanisme nommé garbage collection ou ramasse-miettes qui assure la libération des ressources mémoires des objets qui ne sont plus utilisés.

Il est possible de demander à la JVM de forcer l'exécution du ramasse-miettes en utilisant les méthodes System.gc() ou Runtime.getRuntime.gc() : ce ne sont que des suggestions de demandes que la JVM n'est pas obligée de suivre.

Il n'est pas recommandé d'utiliser ces méthodes dans son code et dans tous les cas la logique des traitements ne doit pas reposer sur ces méthodes.

La gestion de la mémoire par la JVM, notamment grâce au garbage collector, a grandement amélioré la productivité des développeurs et la fiabilité des applications. Cependant le ramasse-miettes n'est pas capable de faire seul la libération des ressources notamment dans le cas de ressources natives fournies par le système d'exploitation sous-jacent de la JVM. Ce type de ressources doit être libéré explicitement par le développeur qui doit invoquer la méthode adéquate généralement dans un bloc try/finally.

Exemple :
package com.jmdoudoux.test.java7;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class MaClasse {
  public MaClasse() {
  }
  
  public static void main(String[] args) {
    InputStream file = null;
    try {
      file = new FileInputStream(new File("test.bin"));
      byte fileContent[] = new byte[(int) file.available()];
      file.read(fileContent);
    } catch (IOException ioe) {
      ioe.printStackTrace(); 
    } finally {
      try {
        file.close();
      } catch (IOException ioe) {
        // traitement de l'exception au besoin
      }
    }
  }
}

L'invocation de la méthode close() dans la clause finally doit être faite dans un bloc try/catch car elle peut lever une exception de type IOException.

Il est possible de déclarer plusieurs ressources dans un bloc try : leurs méthodes close() seront toutes invoquées même si une de ces invocations lève une exception durant sont exécution.

Si une exception de type IOException est levée dans les traitements du bloc try et une autre dans le bloc finally générée par le compilateur pour fermer les ressources, c'est toujours l'exception du bloc try qui sera propagée.

Il est possible d'avoir des informations sur l'exception masquée en utilisant la méthode getSuppressed() de la classe Throwable.

Il n'est généralement pas pratique d'utiliser en même temps les instructions catch et finally avec l'instruction try. Il est préférable d'utiliser simplement un bloc finally avec le try et de laisser la gestion des exceptions à un niveau supérieur.

Exemple :
  public void maMethode() throws IOException {
    try {
      // ...
    } finally {
      // ...
    }
  }

 

14.14. L'interopérabilité avec le code existant

Les objets de type Path obtenus sur le système de fichiers par défaut sont interopérables avec des objets de type java.io.File. Les objets de type Path obtenus sur d'autres systèmes de fichiers peuvent ne pas être interopérables.

Pour faciliter le portage de code utilisant l'API java.io vers NIO2, la classe java.io.File propose la méthode toPath() qui crée une instance de type Path à partir des informations encapsulées dans l'instance de type File.

Exemple ( code Java 7 ) :
Path input = file.toPath();

Il est ainsi facile de bénéficier des fonctionnalités offertes par NIO2 sans avoir à tout réécrire.

Exemple :
file.delete();

Il est possible de réécrire cette portion de code en utilisant NIO2.

Exemple ( code Java 7 ) :
Path fp = file.toPath();
Files.delete(fp);

Inversement, la classe Path propose la méthode toFile() permettant de créer une instance de la classe java.io.File qui correspond aux informations encapsulées dans l'instance de type Path.

 

14.14.1. L'équivalence des fonctionnalités entre java.io et NIO2

Comme l'API NIO2 est une nouvelle API, il n'y a pas de correspondance directe entre les deux API mais le tableau ci-dessous fournit un résumé de l'équivalence des principales fonctionnalités.

Fonctionnalité

java.io

NIO 2

Encapsuler un chemin

java.io.File

java.nio.file.Path

Vérifier les permissions

File.canRead(), File.canCrite() et File.canExecute()

Files.isReadable(), Files.isWritable() et Files.isExecutable(). 

Vérifier le type d'élément

File.isDirectory(), File.isFile()

Files.isDirectory(Path, LinkOption...),

Files.isRegularFile(Path, LinkOption...),

Taille d'un fichier

File.length()

Files.size(Path)

Obtenir ou modifier la date de dernière mise à jour

File.lastModified() ,

File.setLastModified(long)

Files.getLastModifiedTime(Path, LinkOption...), 

Files.setLastModifiedTime(Path, FileTime)

Modifier les attributs

File.setExecutable(), File.setReadable(), File.setReadOnly(), File.setWritable()

Files.setAttribute(Path, String, Object, LinkOption...)

Déplacer un fichier

File.renameTo()

Files.move()

Supprimer un fichier

File.delete()

Files.delete()

Créer un fichier

File.createNewFile()

Files.createFile()

 

File.deleteOnExit()

Option DELETE_ON_CLOSE à utiliser sur la méthode createFile()

Créer un fichier temporaire

File.createTempFile()

Files.createTempFile(Path, String, FileAttributes<?>), Files.createTempFile(Path, String, String, FileAttributes<?>)

Tester l'existence d'un fichier

File.exists

Files.exists() ou Files.notExists()

Obtenir le chemin absolu

File.getAbsolutePath() ou File.getAbsoluteFile()

Path.toAbsolutePath()

 

File.getCanonicalPath() ou File.getCanonicalFile()

Path.toRealPath() ou Path.normalize()

Convertir en URI

File.toURI()

Path.toURI()

L'élément est-il caché ?

File.isHidden()

Files.isHidden()

Obtenir le contenu d'un répertoire

File.list() ou File.listFiles()

Path.newDirectoryStream()

Créer un répertoire

File.mkdir() ou File.mkdirs()

Path.createDirectory()

Obtenir le contenu du répertoire racine

File.listRoots()

FileSystem.getRootDirectories()

 

File.getTotalSpace()

FileStore.getTotalSpace()

 

File.getFreeSpace()

FileStore.getUnallocatedSpace()

 

File.getUsableSpace()

FileStore.getUsableSpace()


Il est possible d'obtenir plus de détails à l'url :

http://docs.oracle.com/javase/tutorial/essential/io/legacy.phpl


Développons en Java v 2.20   Copyright (C) 1999-2021 Jean-Michel DOUDOUX.   
[ Précédent ] [ Sommaire ] [ Suivant ] [ Télécharger ]      [ Accueil ] [ Commentez ]