Le but de ces execercices est de manipuler les ByteBuffer
. Comme nous n'avons pas encore vu de primitives réseau, nous utiliserons les fichiers pour lire et écrire des octets.
Dans ces exercices, vous écrirez tout votre code dans le main
.
Nous ferons mieux plus tard mais pour l'instant ce n'est pas le but des exercices.
StoreWithByteOrder
qui lit des
long
au clavier et qui les écrits dans un fichier. Le programme prend en argument sur la ligne de commande :
LE
pour little-endian et BE
pour big-endian;Par exemple, l'appel java fr.uge.net.buffer.StoreWithByteOrder LE foo.bin
écrira les longs entrés au clavier dans foo.bin
en little-endian.
En commençant avec le fichier StoreWithByteOrder.java, écrire le program StoreWithByteOrder
.
Si vous exécutez :
% java StoreWithByteOrder BE long-be.bin 1 ^Dvous devriez obtenir un fichier long-be.bin contenant 7 octets valant 0 suivis d'un octet valant 1. Pour voir la valeur des octets dans un fichier, nous vous fournissons l'outil File2Hex.jar qui affiche la valeur en hexadecimal de chaque octet du fichier. En utilisant cet outil, vous devriez obtenir :
% java -jar File2Hex.jar long-be.bin 00 00 00 00 00 00 00 01
Si vous exécutez :
% java StoreWithByteOrder LE long-le.bin 1 ^Dvous devriez obtenir un fichier long-le.bin contenant un octet valant 1 suivi de 7 octets valant 0. En utilisant
File2Hex.jar
, vous devriez obtenir :
% java -jar File2Hex.jar long-le.bin 01 00 00 00 00 00 00 00
Nous voulons écrire un program FileWithEncoding
qui prend deux paramètres :
L'API Java offre la méthode String Files.readString(Path path, Charset cs)
qui fait précisément cela.
Cependant dans cet exercice, nous vous demandons d'accéder au fichier par un FileChannel
qui permet simplement de lire (et d'écrire) les octets bruts.
Vous devrez utiliser la méthode charset.decode
pour décoder ces octets. Ce n'est pas la manière optimale de procéder mais on s'en satisfera pour ce cours (cf. Exercice 4).
Dans le fichier ReadFileWithEncoding.java, complétez la méthode stringFromFile
en utilisant un FileChannel
.
Vous pouvez obtenir la taille en octets d'un fichier par la méthode fileChannel.size()
.
Attention : Même si buffer
a la même taille que votre fichier, la méthode
fileChannel.read(buffer)
ne garantit pas de remplir buffer
en un seul appel. Vous devrez l'appeler jusqu'à ce que buffer
soit plein (i.e., !buffer.hasRemaining()
).
Vous pouvez tester votre programme avec le fichier test.txt. Attention, il faut faire "enregistrez-sous" pour télécharger le fichier et ne surtout pas faire de copier-coller. Si votre code est correct, vous devriez voir :
% java ReadFileWithEncoding utf8 test.txt a€Alors que,
% java ReadFileWithEncoding iso-8859-1 test.txt aâ ¬
Il n'y a pas de magie ! Le fichier test.txt contient les 5 octets 61 E2 82 AC 0A. Le fichier n'a pas d'encodage préféré.
Si on le decode en utilisant le charset UTF8, les 5 octets seront interprétés comme suit :
61 -> a E2 82 AC -> € 0A -> line returnEn UTF8, les caractères sont représentés par un nombre variable d'octets. Vous pouvez apprendre ici comment cela marche.
Dans le charset iso-8859-1, chaque caractère est codé par un octet. Les octets du fichier sont interprétés comme suit :
61 -> a E2 -> â 82 -> control caracter that cannot be printed AC -> ¬ 0A -> line return
On veut écire un programme ReadStandardInputWithEncoding
qui lit tous les octets de l'entrée standard et les décode dans un charset donné. Le programme prendra le nom du charset sur la ligne de commande.
Ce programme a vocation à être utilisé comme suit :
$ cat test.text | java ReadStandardInputWithEncoding utf8
D'habitude, vous accédez à l'entrée standard par un Scanner
mais pour cet exercice, nous vous demandons d'y accéder comme un flux d'octets.
On obtient un ReadableByteChannel
correspondant à l'entrée standard avec :
ReadableByteChannel in = Channels.newChannel(System.in);
Un ReadableByteChannel
se comporte comme un FileChannel
en lecture sauf que nous ne savons pas à l'avance combien d'octets
vont arriver. Le seul moyen de savoir que tous les octets ont été lus est d'attendre que la méthode readableByteChannel.read
renvoie -1
. Vous devrez donc lire dans un buffer de taille fixe et l'agrandir quand il est plein.
Dans le fichier ReadStandardInputWithEncoding.java, écrire la méthode stringFromStandardInput
qui lit tous les octets de l'entrée standard et renvoie la chaîne correspondante.
Pour tester votre code, vous pouvez utiliser :
% cat test.txt | java fr.uge.net.buffers.ReadStandardInputWithEncoding utf8
Pour tester que vous agrandissez bien votre buffer de lecture, vous pouvez utiliser le fichier test2.txt.
% cat test2.txt | java ReadStandardInputWithEncoding utf8
% cat test2.txt | wc 10000 40000 2208890 % cat test2.txt | java ReadStandardInputWithEncoding utf8 | wc 10000 40000 2208890
Dans les exercices précédents, nous avons codé des chaînes de caractères en utilisant différents jeux de caractères.
Pour ce faire, nous avons utilisé les méthodes encode
et decode
du Charset
correspondant.
Cette approche, qui est la seule que nous utilisons dans le cours (pour simplifier), a plusieurs inconvénients majeurs :
encode
va créer un nouveau ByteBuffer
à chaque appel, il n'est pas possible de remplir un ByteBuffer
déjà existant,decode
doit avoir tous les octets à décoder dans un même ByteBuffer
, il n'est donc pas possible de décoder au fur et à mesure. Si l'on veut décoder un fichier de 2 Go, il faut donc un ByteBuffer
de 2 Go contenant tous les octets du fichier.Il existe une méthode plus efficace : utiliser les classes CharsetEncoder et CharsetDecoder.
Par exemple, pour décoder unByteBuffer buffer
avec un Charset charset
, l'appel charset.decode(buffer)
est équivalent à
charset.newDecoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE) .decode(buffer);Dans l'exercice précédent, il fallait attendre la fin de l'entrée avant de commencer le décodage, afin d'être sûr de pouvoir décoder la fin. En utilisant le
CharsetDecoder
produit lors de l'appel charset.newDecoder()
, on va pouvoir décoder les octets au fur et à mesure.
Les Decoder
(ainsi que les Encoder
), consomment des octets (ou caractères) d'un buffer d'entrée, les traduisent et écrivent les caractères (ou octets) résultants dans un CharBuffer
de sortie.
La classe CharsetDecoder
a une méthode final CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)
, où le dernier paramètre : endOfInput
indique si nous sommes arrivés à la fin de l'entrée. Chaque invocation de la méthode decode
(encode
) décode (encode) autant d'octets que possible du buffer d'entrée,
en écrivant les caractères résultants dans le buffer de sortie.
Attention la méthode decode
ne va pas nécessairement consommer toute la zone de travail du buffer d'entrée. Il faudra bien prendre soin de ne pas perdre les octets restants qui seront utilisés lors du prochain appel à encode
CoderResult
. L'appel à decode
(encode
) se termine pour l'une des quatre raisons suivantes :
Underflow
: il n'y a plus d'entrée à traiter, ou l'entrée est insuffisante.Overflow
: un débordement est signalé lorsque la place restante dans le buffer de sortie est insuffisante.Malformed-input error
: une erreur d'entrée mal formée est signalée lorsque la séquence d'entrée n'est pas bien formée. Unmappable-character error
: une erreur de caractère non applicable est signalée si une séquence d'entrée désigne un caractère qui ne peut être représenté dans le jeu de caractères de sortie.charset.decode(buffer)
plus en haut qu'il est possible de fixer des actions
à faire en cas d'erreur (malformed-input ou unmappable-character). Ici, nous voulons ignorer les symboles produisant l'erreur en les remplaçant par la chaîne vide ""
. L'action par défaut pour les erreurs de type "malformed-input" et "unmappable-character" est de les signaler (soit avec un objet de classe CodeResult
ou une exception).
Lorsque la méthode decode
(encode
) est appelée pour la dernière fois, en passant true
pour l'argument endOfInput
,
il faut faire suivre cet appel par un appel à la méthode flush
pour que l'encodeur puisse vider tout état interne dans le buffer de sortie. Si la méthode flush
se termine avec succès, elle renvoie CoderResult.UNDERFLOW
.
Nous allons reprendre l'exercice 2 en utilisant un CharsetDecoder
. Votre programme doit lire un fichier (avec FileChannel
) avec un buffer d'entrée de taille fixe. On vous demande en plus d'afficher les caractères décodés au fur et à mesure du décodage.
Complétez la méthode stringFromFile
du code DecoderOnTheFly.java.
% java fr.uge.net.buffers.ReadFileWithDecoder utf8 test-utf8.txt 4 a€ #¶a sfs a€#¶asfs % java fr.uge.net.buffers.ReadFileWithDecoder utf8 utf8-malformed.txt 4 Exception in thread "main" java.lang.IllegalArgumentException: MALFORMED INPUT ...