Introduction
Sérialisation ?
- Données en mémoire (tas) sous la forme d'un graphe d'objets
- Sérialisation : transformation d'un graphe d'objets en séquence d'octets ou de caractères
-
Pourquoi ?
- Pour sauvegarder les objets sur une mémoire de masse (et pouvoir les restaurer plus tard)
- Pour échanger des objets avec d'autres processus (localement ou à distance via le réseau)
- Pour l'édition humaine de données à utiliser : fichier de configuration
Familles de formats de sérialisation
-
Formats binaires
- Représentation par une séquence de bits
- Avantages : formats compacts, compression possible des redondances, possibilités d'indexation pour accès aléatoire
- Inconvénient : non lisibilité par un humain (donc débuggage plus difficile)
-
Formats textuels
- Représentation par une séquence de caractères
- Avantage : human-friendly, meilleure compatibilité avec des vieux protocoles de communication textuels (SMTP...)
- Inconvénient : représentations lourdes, peu adaptées à des données binaires par nature (sons, images...), surcoût de calcul
Quelques formats usuels
- Format de sérialisation binaire Java : utilisé par l'API standard pour sauvegarder des objets
- Format XML (eXtended Markup Language) : <description>format textuel à balises</description> ; format dérivé binaire : Binary XML
- Format JSON (JavaScript Object Notation) : dérivé de la façon de représenter des objets en JavaScript (comme son nom l'indique), très populaire pour faire des requêtes asynchrones sur le web ; supporte les dictionnaires, tableaux, chaînes et nombres ; format dérivé binaire : BSON
- Format YAML (YAML ain't Markup Language) : format lisible utilisant des dictionnaires, listes avec la possibilité d'inclure des références et des indications de type (donc nativement plus adapté pour représenter un graphe d'objets)
- Format CSV (Comma Separated Values) : format textuel adapté pour la sérialisation de matrices (pas d'objets), chaque colonne est séparée par un séparateur (typiquement virgule)
- Format INI : format textuel utilisé pour les fichiers de configuration (dictionnaire clé=valeur) ; utilisé par les fichiers properties en Java
- ...
Formats dédiés
- Spécialisation possible de formats généraux (comme XML) en utilisant un schéma
- Le schéma ajoute des contraintes pour les balises utilisables ; différents types de schémas en XML : DTD, XSD, RelaxNG, Schematron...
- Utile pour l'échange de données entre applications hétérogènes pour vérifier la cohérence des données
- Quelques languages XML issus de schémas : XHTML, MathML, OpenDocument, SVG...
Approches d'API de lecture/écriture
-
Manipulation d'arbres/graphes d'objets
- Approche aisée : accès direct et aléatoire aux structures en mémoire
- Approche coûteuse : les structures sont intégralement en mémoire
-
Manipulation d'événements
- On ne considère pas la structure mais des événements élémentaires : ouverture d'un objet, d'un tableau, écriture d'une valeur, fermeture d'un objet...
- Les structures ne résident pas en mémoire, les accès sont séquentiels
-
Styles d'API en lecture :
- Pushing par rappel de fonctions callback
- Pulling par itérateur d'événements
Sérialisation Java
Sérialisation automatique
Caractéristiques de la sérialisation Java
- Simple d'usage : tout est réalisé automagiquement par introspection
- Pour sérialiser un graphe d'objets : il suffit que tous les objets de ce graphe implantent l'interface marqueur Serializable
- La plupart des objets (tableaux, listes, dictionnaires...) de l'API standard sont déjà Serializable
- Tous les champs d'un objet sont sérialisés (même private) saufs ceux qualifiés de transient
-
La version de la classe utilisée lors de la sérialisation doit être la même que celle utilisée lors de la désérialisation :
- private static final long serialVersionUID = 4242L; // A définir explicitement... sinon le compilateur le génère automatiquement
-
Utilisation de la sérialisation standard Java à éviter pour désérialiser du contenu fourni par un utilisateur externe : possibilité pour un attaquant d'insérer des objets pouvant potentiellement exécuter du code ou créer des denis de service
- Danger limitable en utilisant des filtres de désérialisation (introduits par la PEP 415 et implantés à partir de Java 17)
Sérialisons un graphe d'objets
package fr.upem.jacosa.serialization; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; public class BookSerialization { public static class Book implements Serializable { private final String title; private final int price; public Book(String title, int price) { this.title = title; this.price = price; } } public static void main(String[] args) throws IOException { String file = args[0]; List<Book> l = new ArrayList<Book>(); l.add(new Book("Hamlet", 10)); Book b = new Book("Othello", 5); l.add(b); l.add(b); try (ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(file)))) { oos.writeObject(l); } } }
Graphe d'objet sérialisé
Résultat de la sérialisation
Rechargeons les livres sérialisés
List<Book> l = null; try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializedBooks"))) { l = (List<Book>)ois.readObject(); }
Exceptions potentielles
- FileNotFoundException si le fichier n'est plus là
- ClassNotFoundException si la classe n'est pas présente
- InvalidClassException* s'il y a discordance entre les versions de classe
- ObjectStreamException* si le format du fichier est altéré
-
Possibilité de vérifier un objet désérialisé en implantant ObjectInputValidation :
- la méthode validateObject() vérifie l'objet et lève InvalidObjectException* en cas de problème
Les exceptions * héritent de IOException
Sérialisation avancée
Plus de contrôle avec Externalizable
-
Plutôt que d'utiliser la sérialisation automatique par introspection, on fait tout manuellement en écrivant/lisant les champs :
- Méthode writeExternal(ObjectOutput out) pour écrire des données : out.writeInt(i), out.writeUTF(s), out.writeObject(o)
- Méthode readExternal(ObjectInput in) pour lire les données sérialisées : in.readInt(), in.readBoolean(), in.readUTF(), in.readObject()...
- Désérialisation: la classe doit posséder un constructeur sans argument pour créer l'instance ; ensuite writeExternal sera appelé.
Sérialisons un livre en compressant son titre
package fr.upem.jacosa.serialization; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class Book implements Externalizable { private String title; private int price; public Book() { } // A no-arg constructor is required for Externalizable public Book(String title, int price) { /* ... */ } public void writeExternal(ObjectOutput out) throws IOException { out.writeShort(price); // we assume that the price does not reach 2^15 euros // we compress the title by storing first its length // and each sequence of identical characters prefixed by their repeat number out.writeShort(title.length()); char currentChar = '\0'; int number = 0; for (int i = 0; i <= title.length(); i++) { if (i < title.length() && title.charAt(i) == currentChar) number++; else { if (number > 0) { out.writeChar(currentChar); out.writeByte(number); } if (i < title.length()) { currentChar = title.charAt(i); number = 1; } } } } public void readExternal(ObjectInput in) throws IOException { price = in.readShort(); int titleLength = in.readShort(); StringBuilder sb = new StringBuilder(); int i = 0; while (sb.length() < titleLength) { char c = in.readChar(); byte repeats = in.readByte(); for (byte j = 0; j < repeats; j++) sb.append(c); } title = sb.toString(); } }
Quelques remarques pour approfondir
-
Sérialisation de tous les champs non-transient du graphe
- attention aux données confidentielles en cas de communication externe
-
Modification possible de fichiers sérialisés
- altération des données, déni de service...
- possibilité d'utiliser un SignedObject
- Sérialisation des beans par XML : XMLEncoder, XMLDecoder
Manipulation de données en XML
Le format XML
Un peu de XML
- Évolution de Standard Generalized Markup Language (SGML)
-
Principe : emboîtement de sections délimitées par des balises (<balise> ... </balise>)
- représentation possible par un arbre
- Plusieurs espaces de nom peuvent cohabiter : <namespace1:balise>, <namespace2:balise>
- Les balises peuvent accueillir des attributs : <balise attribut1="valeur1" attribut2="valeur2">...</balise>
- <balise></balise> (balise sans contenu) peut être remplacé par <balise />
- Une balise peut avoir pour enfant un noeud CDATA (chaîne de caractère)
-
Certains caractères sont spéciaux : ", &, ', < et >
- déspécialisables en ", &, ', <, >
- On peut forcer un passage en CDATA en l'entourant ainsi : <![CDATA[ ... ]]> (plus besoin de déspécialiser)
- <!-- Voici un exemple de commentaire -->
Exemple de document XML
<?xml version="1.0"?> <shelf> <book license="PUBLIC_DOMAIN"> <title>Quatrevingt-treize</title> <author>Victor Hugo</author> </book> <!-- Here is a little more complex book --> <book license="COPYRIGHT"> <title><![CDATA[ <title>HTML for Babies</title> ]]></title> <author>Vanden-Heuvel</author> </book> </shelf>
Document Object Model (DOM)
API Document Object Model
- API définie par la W3C avec de nombreuses implantations : actuellement version 3 (v4 en développement)
- Manipule le document XML sous la forme d'un arbre
- Permet d'associer aussi des événements aux noeuds
- Incontournable pour la manipulation de pages HTML avec JavaScript (mais ce n'est pas l'objet de ce cours)
- API présente dans le JDK Java (org.w3c.dom)
- Présentation complète de l'API sur le site de JM Doudoux
Initialisation d'analyseur DOM
InputStream is = ...; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(false); DocumentBuilder builder = null; try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { } Document doc = builder.parse(is);
Les principaux noeuds
Ils implantent l'interface org.w3c.dom.Node
Noeuds | Enfants possibles |
Document | Element, ProcessingInstruction, Comment, DocumentType |
DocumentFragment | |
Element | Element, Text, Comment, ProcessingInstruction, CDataSection, EntityReference |
Attr | Text, EntityReference |
Text | Feuille |
CDataSection | Feuille |
Comment | Feuille |
Les autres noeuds
D'autres noeuds potentiellement utiles
ProcessingInstruction | Feuille |
Entity | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
EntityReference | |
Notation | Feuille |
Informations sur un noeud
- String getNodeName() : nom de l'élément, de l'attribut
- String getNodeValue() : valeur textuelle contenue pour un attribut, du texte, un commentaire (null pour un élément)
- String getTextContent() : contenu textuel du noeud (en explorant le sous-arbre)
- short getNodeType() : constante pour le type de noeud (évite d'utiliser instanceof)
- NodeList getChildNodes() : tous les noeuds enfants
- NodeList getElementsByTagName(String tagName) : noeuds enfants portant un certain nom
- Node getParentNode() : noeud parent
- String getAttribute(String name) : attribut d'un élément
Comment utiliser NodeList et NamedNodeMap ?
En utilisant (entre-autres) les méthodes int getLength() et Node item(int i)
Ne pas hésiter à consulter la Javadoc
Récupérons tous les titres de livres
// parse and fetch the XML document Document doc = builder.parse(is); // get the shelf that is the root element Element shelf = doc.getDocumentElement(); // iterate over each book of the shelf for (int i = 0; i < shelf.getChildNodes().getLength(); i++) { Element book = shelf.getChildNodes().item(i); String title = book.getElementsByTagName("title") .item(0).getTextContent(); System.out.println(title); }
Modification d'un arbre
DOM peut être aussi utilisé pour modifier un arbre :
- Node cloneNode(boolean deepCopy) : pour copier un noeud (ainsi que ses descendants si copie profonde)
- Node removeChild(Node oldChild) : pour supprimer un enfant
- Node replaceChild(Node newChild, Node oldChild) : pour le remplacement de noeud
- void setNodeValue(String nodeValue) : pour indiquer la valeur d'un noeud (CDATA, valeur d'attribut)
- void setAttribute(String key, String value) : pour changer un attribut d'un élément
Là encore, la Javadoc est utile !
Cas particulier des documents HTML
-
HTML ≠ XML
- HTML repose sur le format SGML (normalisé en 1986)
- XML (normalisé en 1999) est une version plus simple de SGML facilitant l'interopérabilité
- Il existe une variante de HTML basée sur le XML : XHTML (la syntaxe est moins permissive que le HTML standard)
-
HTML dans la vraie vie
- Peu de pages sont rédigées en XHTML
- Aussi bien pour le HTML que le XHTML, on rencontre souvent des erreurs de syntaxe
- Les analyseurs syntaxiques utilisés par les navigateurs sont très permissifs
- Alors que le parser DOM intégré dans le JDK sera perturbé par du XHTML avec erreurs (le HTML classique ne pouvant être analysé)
- Solution : utiliser un analyseur plus tolérant pour du web scraping
Exemple : récupération des conditions de circulation sur le site sytadin.fr
package fr.upem.jacosa.serialization; import org.jsoup.Jsoup; import java.io.IOException; import java.util.regex.Pattern; public class JSoupDemo { /** * With this demonstration of JSoup, we webscrap the trafic information in Paris area * using the website sytadin.fr * This code may be obsolete if the Sytadin website changes. */ public static void main(String[] args) throws IOException { var jamRegex = Pattern.compile("([0-9]+) km"); var doc = Jsoup.connect("http://www.sytadin.fr/").get(); System.out.println("Title: " + doc.title()); var jam = doc.getElementById("cumul_bouchon"); int jamLength = -1; for (var child: jam.children()) { if (child.tagName().equals("img")) { String alt = child.attr("alt"); var matcher = jamRegex.matcher(alt); if (matcher.matches()) jamLength = Integer.parseInt(matcher.group(1)); } if (jamLength >= 0) break; } if (jamLength < 0) System.out.println("Cannot find jam length"); else System.out.println(String.format("Jam length: %d", jamLength)); } }
SAX
Simple API for XML (SAX) : org.xml.sax
= Approche évenementielle de type push (événements récupérés par callbacks)
- API présente dans le JDK Java
- Initialisation d'un XMLReader
-
Création d'un ContentHandler définissant les méthodes de callback à utiliser lors de l'analyse et rattachement avec xmlReader.setContentHandler(ch) :
- {start, stop}Document()
- {start, stop}Element(uri, localName, qname, [attributes])
- setDocumentLocator(locator)
- characters(ch, start, length)
- ...
- Arghh ! Il y a trop de méthodes à implanter ; utilisons plutôt DefaultHandler avec une implantation vide de toutes les méthodes
- Lancement de l'analyse avec xmlReader.parse(inputSource)
Référencement des balises d'un fichier XML avec SAX
package fr.upem.jacosa.serialization; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.util.Set; import java.util.TreeSet; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; public class SAXReferencer { public static void main(String[] args) throws IOException, SAXException, ParserConfigurationException { Reader r = new InputStreamReader(System.in); // Where to store each tag name final Set<String> tagSet = new TreeSet<String>(); XMLReader saxReader = SAXParserFactory.newDefaultInstance().newSAXParser().getXMLReader(); saxReader.setContentHandler(new DefaultHandler() { public void startElement( String uri, String localName, String qName, Attributes attributes) { tagSet.add(qName); // We use the qualified name } }); saxReader.parse(new InputSource(r)); System.out.println(tagSet); } }
StAX
API StAX
- Approche évenementielle de type pull avec deux styles d'accès : par curseur ou itérateur
- API présente dans le JDK Java
- Avantage de l'approche curseur sur l'approche itérateur : moins d'objets alloués en mémoire
-
Choisir l'approche itérateur ou curseur ?
- En cas de besoin de stocker les événements ⟶ approche itérateur
- Sinon (dans la majorité des cas) ⟶ approche curseur
- Création du parser à partir d'une fabrique : XMLInputFactory factory = XMLInputFactory.newInstance()
- Description complète de l'API sur le site de JM Doudoux
Approche par curseur
- Création du XMLStreamReader à partir de la factory : XMLStreamReader reader = xmlif.createXMLStreamReader(input)
-
Récupération du type d'événement avec reader.next() (constante entière : XMLStreamConstants.START_DOCUMENT, XMLStreamConstants.END_DOCUMENT, XMLStreamConstants.COMMENT, XMLStreamConstants.START_ELEMENT, XMLStreamConstants.END_ELEMENT, XMLStreamConstants.CHARACTERS...)
- ⚠ Toujours tester avant s'il existe un élément suivant avec reader.hasNext() (sinon on arrive à la fin du fichier)
-
Possibilité d'obtenir des informations supplémentaires avec des getters :
- Nom de la balise : String reader.getName()
- Texte rencontré : String reader.getText()
- Informations sur les attributs d'une balise : int reader.getAttributeCount(int i), String reader.getAttributeName(int i), String reader.getAttributeValue(int i)
- ...
- Génération de deux événements START_ELEMENT et END_ELEMENT par les balises auto-fermantes du types <foo /> comme si elles étaient de la forme <foo></foo>
- Filtration possible d'un reader pour obtenir uniquement les éléments d'intérêt avec XMLEventReader filteredReader = xmlif.createFilteredReader(reader, r -> r.isStartElement()); (pour obtenir uniquement les balises ouvrantes)
☞ Une approche similaire par curseur est également utilisée pour parcourir des enregistrements de BDD avec l'API JDBC
Affichage d'événements avec l'approche par curseur
package fr.upem.jacosa.serialization; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.util.Arrays; import java.util.HashMap; /** * Very simple application to print the encountered tags in a XML document * using the cursor approach of the StAX API. */ public class SimpleStAXCursorApp { public static void main(String[] args) throws XMLStreamException { XMLInputFactory xmlif = XMLInputFactory.newInstance(); XMLStreamReader reader = xmlif.createXMLStreamReader(System.in); try { while (reader.hasNext()) { int eventType = reader.next(); switch (eventType) { case XMLStreamConstants.START_DOCUMENT -> System.out.println("Start of the document"); case XMLStreamConstants.END_DOCUMENT -> System.out.println("End of the document"); case XMLStreamConstants.COMMENT -> System.out.println("Encountered comment: " + reader.getText()); case XMLStreamConstants.START_ELEMENT -> { var attributes = new HashMap<String, String>(); for (int i = 0; i < reader.getAttributeCount(); i++) attributes.put(reader.getAttributeName(i).getLocalPart(), reader.getAttributeValue(i)); System.out.println("Start element " + reader.getName() + " with attributes " + attributes); } case XMLStreamConstants.END_ELEMENT -> System.out.println("End element " + reader.getName()); case XMLStreamConstants.CHARACTERS -> System.out.println("Encountered characters " + reader.getText()); } } } finally { reader.close(); } } }
Recherche de livres d'un auteur donné
- Fichier XML de livres
- Comment obtenir la liste des livres de tous les livres d'un auteur donné ?
- Problème : il s'agit de la collection de la BNF avec 40 millions de livres ; heureusement elle est triée par auteur
- Chargement du fichier XML avec DOM impossible
- On utilise un XMLStreamReader
package fr.upem.jacosa.serialization; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; /** Extract all the bookks from a given author using a cursor with StAX */ public class BookPuller2 { public static String readText(XMLStreamReader reader) throws XMLStreamException { int type = reader.next(); assert(type == XMLStreamConstants.CHARACTERS); return reader.getText().trim(); } public static void main(String[] args) throws IOException, XMLStreamException { String expectedAuthor = args[0]; XMLInputFactory xmlif = XMLInputFactory.newInstance(); XMLStreamReader reader = xmlif.createXMLStreamReader(BookPuller2.class.getResourceAsStream("/books/books.xml")); try { // Iterate over the events String currentTitle = null; String currentAuthor = null; int eventType = reader.next(); while (eventType != XMLStreamConstants.END_DOCUMENT && (currentAuthor == null || expectedAuthor.compareTo(currentAuthor) <= 0)) { if (eventType == XMLStreamConstants.START_ELEMENT) { if (reader.getName().getLocalPart().equals("book")) { currentTitle = currentAuthor = null; } if (reader.getName().getLocalPart().equals("title")) currentTitle = readText(reader); else if (reader.getName().getLocalPart().equals("author")) currentAuthor = readText(reader); } else if (eventType == XMLStreamConstants.END_ELEMENT && reader.getName().getLocalPart().equals("book")) { if (expectedAuthor.equals(currentAuthor)) System.out.println(currentTitle); } eventType = reader.next(); } } finally { reader.close(); } } }
Manipulation de données en JSON
Présentation
Bibliothèque JSON
JSON est un format simple à analyser, il existe des bibliothèques pour quasiment tous les langages :
- En Java : paquetage org.json ; pas présent dans le JDK mais inclus dans le SDK Android
- En JavaScript : intégré dans le langage avec l'objet JSON : JSON.parse() (ne pas utiliser eval)
- En Python : module json en standard
Toutes les bibliothèques permettent de convertir dictionnaires et tableaux en JSON (et vice-versa) ; le support de la lecture/écriture en streaming est plus rare. \\
Types supportés :
- Dictionnaire : {"clé1": "valeur1", "clé2": "valeur2", ...}
- Tableau : ["a", "b", "c", ...]
- Primitifs : ["chaîne UTF-8", 4241, 3.14, true, false, null]
Site de référence : http://json.org/
Une bibliothèque en JSON
{ "shelf": [ {"title": "Quatrevingt-treize", "author": "Victor Hugo", "price": 5, "license": "PUBLIC_DOMAIN"}, {"title": "<title>HTML for babies</title>", "author": "Vanden-Heuvel", "price": 10, "license": "COPYRIGHT"} ] }
Quelques exemples avec org.json
Arborescence de fichiers en JSON
Ce dont nous avons besoin :
- Classe File pour représenter un chemin de fichier : File f = new File(...);
- f.isFile() et f.isDirectory() pour savoir ce que représente un chemin
- File[] files = f.listFiles() pour lister tous les enfants d'un répertoire
- ArrayList<File> pour stocker les listes de fichier
- JSONArray pour la conversion en tableau JSON
- TreeMap<File> pour stocker les fichiers d'un répertoire en ordre lexicographique
- JSONObject pour la conversion en dictionnaire JSON
Niveau 1 : liste des fichiers enfant d'un répertoire
package fr.upem.jacosa.serialization; import java.io.File; import java.util.ArrayList; import java.util.List; import org.json.JSONArray; public class JSONDirectoryListing { public static void main(String[] args) { File d = new File(args[0]); List<File> l = new ArrayList<File>(); for (File f: d.listFiles()) l.add(f); JSONArray array = new JSONArray(l); System.out.print(array.toString()); } }
Niveau 2 : arborescence complète d'un répertoire
package fr.upem.jacosa.serialization; import java.io.File; import java.util.Map; import java.util.TreeMap; import org.json.JSONObject; public class JSONDirectoryTree { /** Recursively build the tree */ public static Object getTree(File root) { if (root.isDirectory()) { Map<String, Object> tree = new TreeMap<String, Object>(); for (File f: root.listFiles()) tree.put(f.getName(), getTree(f)); return tree; } else return root.getName(); } public static void main(String[] args) { File root = new File(args[0]); Object tree = getTree(root); if (tree instanceof String) System.err.println(root + " is a simple file"); else // root is a directory System.out.print(new JSONObject(tree).toString()); } }
Résultats
Niveau 1 :["music", "pictures", "videos"]Niveau 2 :
{ "music": { "MysteriousMusic.flac": true, "TheSongOfJava.mp3": true, "TicTac.wav": true }, "pictures": { "duke.png": true, "gosling.jpg": true }, "videos": { "motion.flv": true, "video2.avi": true, "video.mkv": true } }
Recherche d'une clé dans un fichier JSON
import java.io.InputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.json.JSONObject; /** Program searching a value linked to a key into a JSON dictionary */ public class JSONKeyFinder { public static void find(JSONObject obj, String searchedKey, List<Object> l) { for (Iterator<String> keyIt = obj.keys(); keyIt.hasNext(); ) { String key = keyIt.next(); if (key.equals(searchedKey)) l.add(obj.get(key)); // key found if (obj.get(key) instanceof JSONObject) find(obj, searchedKey, l); // recursive search } } public static String read(InputStream is) { return null; // TODO: to be written } public static void main(String[] args) { String searchedKey = args[0]; String content = read(System.in); // Read all the content given to System.in, must be implemented JSONObject obj = new JSONObject(content); List<Object> l = new ArrayList<Object>(); find(obj, searchedKey, l); for (Object a: l) System.out.println(a); // Call the toString() method of a } }
Bibliothèque Jackson
- Jackson : bibliothèque Java spécialisée pour la sérialisation JSON
- Support possible d'autres formats que le JSON : BSON, YAML, XML, CSV...
-
Plus de fonctionnalités que org.json :
- API pour lecture événementielle (approche par curseur)
- Mapping automatique d'objets JSON en objets Java
Mapping automatique avec ObjectMapper de Jackson
package fr.upem.jacosa.serialization; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import java.util.Map; public class JacksonBook { record Book(String title, int price) {} public static void main(String[] args) throws JsonProcessingException { var jsonMapper = new ObjectMapper(); var yamlMapper = new ObjectMapper(new YAMLFactory()); // try to jsonify a book and reload it Book b1 = new Book("Jackson's test", 1000); String b1json = jsonMapper.writeValueAsString(b1); System.out.println(b1json); Book b1reloaded = jsonMapper.readValue(b1json, Book.class); // specifying the class is compulsory System.out.println("Are b1 and b1reloaded the same? " + b1.equals(b1reloaded)); System.out.println(yamlMapper.writeValueAsString(b1)); // work now with a map of books var bookMap = Map.of("Useful book", b1, "Interesting book", new Book("The Art of Computer Programming", 2000), "Useless book", new Book("The book of vacuum", 0)); String bookMapJson = jsonMapper.writeValueAsString(bookMap); String bookMapYaml = yamlMapper.writeValueAsString(bookMap); System.out.println("JSON:\n" + bookMapJson); System.out.println("YAML:\n" + bookMapYaml); var bookMapJsonReloaded = jsonMapper.readValue(bookMapJson, new TypeReference<Map<String, Book>>() {}); var bookMapYamlReloaded = yamlMapper.readValue(bookMapYaml, new TypeReference<Map<String, Book>>() {}); System.out.println("Are bookMap and bookMapJsonReloaded the same? " + bookMap.equals(bookMapJsonReloaded)); System.out.println("Are bookMap and bookMapYamlReloaded the same? " + bookMap.equals(bookMapYamlReloaded)); } }
Fichiers properties
Configuration d'une application
Comment indiquer des informations de configuration pour une application ?
- Directement en dur dans le code : à ne jamais faire
- Regroupement de constantes (public static final ...) : mieux mais nécessite quand même une recompilation
- Passage d'arguments au programme (tableau args de main) : pertinent pour les paramètres les plus importants
- Utilisation de variables d'environnement : \\
- Map<String, String> envMap = System.getenv()
- Fichier de configuration : à privilégier si beaucoup de paramètres existent
Format de fichier de configuration
Expressif (types supportés), éditable et lisible facilement par un humain
- Création d'un format ad-hoc (avec un DSL) : utile si le paramétrage est complexe
- Utilisation d'un format clé-valeur : pour des besoins plus légers
Fichier clé/valeur .properties
# Un petit commentaire clé1 = valeur1 clé2 = valeur2 ... cléI = valeur I \ sur deux lignes ... cléN = une valeur avec un caractère unicode \u2190
-
Limitations
- Les clés et valeurs ne sont pas typables (String)
- Le fichier est représenté en ISO-8859-1 : les caractères Unicode doivent être déspécialisés
Remarque : il existe une version XML des fichiers properties
Transférons un fichier .properties dans une Map
package fr.upem.jacosa.serialization; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Properties; public class PropertiesReader { // Tutorial : // https://docs.oracle.com/javase/tutorial/essential/environment/properties.html public static Map<String, String> readProperties(String filepath) throws IOException { Properties p = new Properties(); FileInputStream in = new FileInputStream(filepath); p.load(in); in.close(); Map<String, String> map = new HashMap<String, String>(); for (Object key: p.keySet()) map.put(key.toString(), p.getProperty(key.toString())); return map; } }