====== TP Java: Extensions de l'analyseur ====== ===== Lecture dans une base relationnelle ===== L'objectif de cette partie est de lire les lignes à analyser dans une base relationnelle. Dans votre compte oracle, accessible via [[http://b710ntb.univ-lyon1.fr:5560/isqlplus]] (ou depuis l'extérieur de l'université via la méthode décrite [[http://liris.cnrs.fr/~ecoquery/oraclessh/|ici]]), créer une table ''donnees'' à l'aide du script suivant: DROP TABLE donnees; CREATE TABLE donnees ( source varchar(255) NOT NULL, num integer NOT NULL, ligne varchar(255), PRIMARY KEY (source,num) ); -- -- Contenu de la table donnees -- INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 0, '/*'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 1, ' * To change this template, choose Tools | Templates'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 2, ' * and open the template in the editor.'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 3, ' */'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 4, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 5, 'package utilfichiers;'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 6, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 7, '/**'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 8, ' *'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 9, ' * @author emmanuelcoquery'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 10, ' */'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 11, 'public class Arguments {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 12, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 13, ' public static void main(String [] args) {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 14, ' for(int i = 0; i< args.length; i++) {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 15, ' System.out.println(args[i]);'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 16, ' }'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 17, ' }'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 18, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/Arguments.java', 19, '}'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 0, '/*'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 1, ' * To change this template, choose Tools | Templates'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 2, ' * and open the template in the editor.'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 3, ' */'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 4, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 5, 'package utilfichiers;'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 6, 'import java.io.*;'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 7, '/**'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 8, ' *'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 9, ' * @author emmanuelcoquery'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 10, ' */'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 11, 'public class LecteurDeFichier {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 12, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 13, ' private BufferedReader reader;'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 14, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 15, ' public LecteurDeFichier(String filename) throws FileNotFoundException {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 16, ' reader = new BufferedReader(new FileReader(filename));'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 17, ' }'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 18, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 19, ' public String lireLigne() throws IOException {'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 20, ' return reader.readLine();'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 21, ' }'); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 22, ''); INSERT INTO donnees (source, num, ligne) VALUES('src/utilfichiers/LecteurdeFichier.java', 23, '}'); COMMIT; ==== Interface et classe pour se connecter à la BD ==== On souhaite dans un premier temps créer une classe permettant d'ouvrir une connexion Oracle. Afin d'utiliser l'aspect général de JDBC, on va tout d'abord créer une interface ''ConnectionProvider'' comme suit: package utilfichiers; import java.sql.Connection; import java.sql.SQLException; public interface ConnectionProvider { public Connection getConnection() throws SQLException; } Télécharger le fichier [[http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html|ojdbc14.jar]] depuis le site [[http://www.oracle.com]]. Une copie de la documentation JavaDoc se trouve [[http://download.oracle.com/otn_hosted_doc/jdeveloper/1012/jdbc-javadoc/index.html|ici]]. Ajouter ce fichier à votre projet de la manière suivante: Clic droit sur le "répertoire" //Libraries// de votre projet -> //Add JAR/Folder//. Indiquer alors l'emplacement où vous avez sauvé le fichier [[http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html|ojdbc14.jar]]. Le pilote JDBC pour Oracle est maintenant accessible au projet. Créer une classe ''OracleProvider''((toujours dans le package ''utilfichiers'')) qui implémente ''ConnectionProvider''. Cette classe comportera un champ ''ods'' de la classe ''oracle.jdbc.pool.OracleDataSource'' qui sera initialisé dans le constructeur avec un code similaire au code suivant: ods = new OracleDataSource(); ods.setDriverType("thin"); ods.setServerName("pedagowin710"); ods.setPortNumber(1521); ods.setDatabaseName("ora10g"); ods.setUser("votreLoginOracle"); ods.setPassword("votreMotDePasseOracle"); Remarque: changer ''pedagowin710'' en ''localhost'' depuis [[enseignement:aide:oracle|l'extérieur de l'université en ssh]] Enfin ajouter la méthode ''getConnection()'' qui renvoie le résultat de l'appel à ''getConnection()'' sur le champ ''ods''. ==== Test de la connection ==== Ajouter à la classe ''OracleProvider'' une méthode ''main'' pour la tester. Dans cette méthode, créer un objet ''OracleProvider'', et l'utiliser pour récupérer un objet du type ''java.sql.Connection''. Utiliser cet objet pour créer un objet de type ''java.sql.Statement'' via la méthode ''createStatement()''. Cet objet va être utilisé pour interroger la base de donnée. Utiliser la méthode ''executeQuery(String sql)'' du ''Statement'' pour exécuter la requête suivante: SELECT ligne FROM donnees On stockera le résultat dans un objet de type ''java.sql.ResultSet''. On utilisera ensuite la méthode ''next()'' et la méthode ''getString(String column)'' de cet objet pour parcourir le résultat dans une boucle ''while'' et l'afficher (via ''System.out.println(String s)''). Lancez ensuite le code via NetBeans. On pourra s'inspirer des extraits de code suivants: // Création du statement Statement stat = .... // Exécution de la requête ResultSet rs = stat.executeQuery("SELECT ......"); // Parcours du résultat while (rs.next()) { ...... rs.getString("ligne") ...... } ==== Lecteur de base de données ==== Créer une classe ''LecteurBD'' qui implémente l'interface ''Lecteur''. Elle possédera des champs ''connection'' de la classe ''java.sql.Connection'' et ''result'' de la classe ''java.sql.ResultSet''. Le constructeur prendra en argument un ''ConnectionProvider'', ainsi qu'une ''String'' qui contiendra une requête SQL. On supposera que la première colonne du résultat de cette requête se nomme ''ligne'' et contient des chaînes de caractères. Le constructeur utilisera le ''ConnectionProvider'' pour récupérer une ''Connection'' et créera un ''Statement'' via cette dernière. On utilisera ce ''Statement'' pour exécuter la requête passée en argument, dont le résultat sera placé dans le champ ''result''. Il reste enfin à ajouter la méthode ''lireLigne()''. Elle fonctionnera de la manière suivante: * passer au n-uplet suivant avec la méthode ''next()'' du champ ''result''. * s'il n'y a pas de n-uplet suivant, renvoyer ''null'', après avoir fermé la ''Connection'' * sinon, renvoyer la valeur de la colonne ''ligne'' Il reste un problème: la méthode ''lireLigne'' peut renvoyer une ''IOException'' et pas une ''SQLException''. Pour contourner le problème, on ajoutera un ''try''/''catch'' autour de coeur de la méthode, avec le code suivant pour la partie ''catch'': } catch (SQLException e) { throw new IOException(e); } Enfin modifier la méthode main de l'analyseur pour que si le nom du fichier commence par ''oracle:'', l'analyseur utilise un LecteurBD initialisé avec une OracleProvider et avec la requête suivante: SELECT ligne FROM donnees **Exercice supplémentaire:** modifier à nouveau cette méthode pour que la requête corresponde à la fin de l'argument. Par exemple l'argument ''oracle:SELECT\ ligne\ FROM\ donnees'' correspondrait à l'utilisation de la requête précédente. ===== Collection de Lecteurs et analyse récursive de répertoires ===== L'objectif ici est de permettre d'analyser un ensemble de fichiers en une fois en spécifiant plusieurs fichiers sur la ligne de commande. Lorsqu'un fichier est un répertoire, on analysera alors les fichiers qui sont dans le répertoire, ainsi que tous les sous-répertoires. Pour cela on procédera en deux étapes: la première étape consistera à construire un ensemble (//i.e.// une ''Collection'') de ''Lecteur''s, un pour chaque fichier à analyser. La deuxième étape consistera à parcourir cette collection pour lancer l'analyse sur chacun des ''Lecteur''s. ==== Une classe GestionArguments ==== Le rôle de cette classe sera de construire la ''Collection'' mentionnée ci-dessus. Elle devra comporter un champ ''lecteurs'' de type ''Collection''. Ce champ sera initialisé dans le constructeur de la classe en utilisant une implémentation de ''Collection'' que vous choisirez. Elle comportera également une méthode ''public Collection getLecteurs()'' qui renverra le champ ''lecteurs''. Enfin on ajoutera une méthode ''public void ajoute(String fichier)'' qui se comportera de la manière suivante: * Si ''fichier'' est égal à ''"-"'', on ajoute un ''LecteurClavier'' à ''lecteurs''. * Si ''fichier'' est une URL (si vous avez déjà fait la première amélioration), on ajoute un ''LecteurReseau'' pour cette URL dans ''lecteurs''. * Sinon ''fichier'' désigne soit un fichier, soit un répertoire. Dans ce cas on créera une instance de la classe ''java.io.File'' à partir de ''fichier''. On utilisera cet objet pour distinguer les cas suivants: * Si le fichier est un fichier normal (voir la méthode ''isFile()''), ajouter un nouveau ''LecteurDeFichier'' pour ce fichier. * Si le fichier est un répertoire (voir la méthode ''isDirectory()''), récupérer l'ensemble des fichiers et des répertoires contenus dans celui-ci en utilisant la méthode ''listFiles()'', qui renvoie un tableau de ''File''. Pour chaque ''File'' dans ce tableau, on appellera récursivement la méthode ''ajoute'' et utilisant au passage la méthode ''getCanonicalPath()'' pour transformer ce ''File'' en ''String''. * Si le fichier n'existe pas (voir la méthode ''exists()'') lever une nouvelle exception ''java.io.FileNotFoundException''. On passera le nom du fichier en argument au constructeur de l'exception. * Sinon, on lancera une nouvelle exception ''IOException'' On passera le nom du fichier en argument au constructeur de l'exception. ==== Utilisation dans la méthode main ==== Au début de la méthode ''main'' de la classe ''Analyseur'', créer une instance de ''GestionArguments''. Créer ensuite l'analyseur en utilisant le premier argument de la ligne de commande. * S'il n'y a pas d'autres arguments en ligne de commande, appeler une fois la méthode ''ajoute'' de votre ''GestionArguments'' avec comme paramètre la chaîne ''"-"'' pour ajouter un ''LecteurClavier''. * Sinon, pour chaque argument de la ligne de commande, on appellera cette méthode ''ajoute''. Remarque: on peut gérer les exceptions ''FileNotFoundException'' et ''IOException'' lancées dans ''ajoute'' à cet endroit en affichant un message d'erreur, ce qui permet de traiter les autres fichiers. Une fois tous les arguments traités, on récupère les lecteurs via la méthode ''getLecteurs'', puis on parcours la collection ainsi obtenue, par exemple avec la nouvelle forme de la boucle ''for''. Pour chacun des ''Lecteur''s de la collection, on appelle la méthode ''analyse'' de l'''Analyseur'' et on affiche le résultat comme au [[cci-java-tp1|TP 1]] ou au [[cci-java-tp2|TP 2]]. ===== Des informations plus riches sur les lignes ===== Le but est d'améliorer les informations affichées lorsqu'une ligne est trouvée par l'analyseur. Pour cela, on ajoute dans la classe Ligne des informations sur l'endroit d'où provient la ligne. Cela aura un impact sur l'interface ''Lecteur''. On en profitera pour améliorer les implémentations de cette interface à l'aide d'une classe abstraite. Remarque: cette amélioration se combine très bien avec la précédente pour obtenir des résultats plus clairs. ==== Des changements dans la classe Ligne ==== Afin de savoir d'où provient la ligne représentée par l'objet, on ajoute un champ ''String provenance'' à la classe ''Ligne''. Modifier le constructeur pour prendre un argument supplémentaire indiquant cette provenance. Ajouter une méthode ''String getProvenance()'' qui renvoie le champ ''provenance''. Enfin ajouter une méthode ''public String toString()'' qui renverra une chaîne de caractères //ppp//://nnn//://ttt// où //ppp// est la provenance, //nnn// est le numéro de ligne et enfin ''ttt'' est le texte de la ligne. ==== Des changements dans l'interface Lecteur ==== On change l'interface ''Lecteur'' de la manière suivante: la méthode public String lireLigne() throws IOException; devient public Ligne lireLigne() throws IOexception; En effet, c'est le ''Lecteur'' qui disposera des informations sur la provenance des lignes. Une conséquence de cette modification est que ce sont à présent les ''Lecteur''s qui vont se charger de la numérotation des lignes. ==== Une classe abstraite pour implémenter des Lecteurs ==== On peut remarquer que les implémentations de l'interface ''Lecteur'' se ressemblent: mise à part le LecteurBD, elles utilisent toutes un ''BufferedReader'' pour lire les lignes. Comme on doit ajouter à présent un compteur de lignes, on peut ce dire que cela va encore faire du code à dupliquer dans les différentes implémentations. Créer une classe abstraite ''LecteurAbstrait'' et déclarer qu'elle implémente l'interface ''Lecteur''. Ajouter un champ ''BufferedReader source''. On ne s'occupera pas de l'initialisation de ce champ dans le constructeur de la classe. À la place, on va créer une méthode qui va s'occuper de cela. L'utilité de cette manière de procéder apparaîtra au moment où on étendra cette classe. Ajouter une méthode ''protected void setReader(Reader reader)'' qui initialise le champ ''source'' en passant ''reader'' en argument du constructeur de la classe ''BufferedReader''. Ajouter un champ ''int numLigne''. Dans le constructeur, initialiser ce champ à ''0''. Ajouter un champ ''String origine'' et modifier le constructeur pour prendre un paramètre qui va initialiser ce champ. On a à présent toutes les briques pour écrire la méthode ''lireLigne()'' de l'interface ''Lecteur''. Plus précisement, cette méthode va exécuter les actions suivantes: * récupérer le texte de la ligne grâce à la méthode ''readLine()'' du champ ''source''; * incrémenter le numéro de ligne courant; * créer une instance de ''Ligne'' en utilisant le texte, le numéro de ligne et le champ ''origine'' pour la provenance; * renvoyer cet objet ''Ligne''. ==== Nouvelles versions des différents Lecteurs ==== On peut à présent recoder de manière simple les différentes implémentation de ''Lecteur'' (sauf le ''LecteurBD''): pour chacune d'elles, il suffit de procéder comme suit: * déclarer que la classe étend ''LecteurAbstrait''; * la première ligne du constructeur doit à présent être un appel à ''super'' avec comme argument la chaîne de caractères qui sert à spécifier l'origine des lignes; * on supprime le champ ''BufferedReader'': en effet il y a déjà un ''BufferedReader'' dans ''LecteurAbstrait''. * dans le constructeur, on transforme l'initialisation du ''BufferedReader'' en un appel à la méthode ''setReader'' (qui est héritée de ''LecteurAbstrait''). * Supprimer la méthode ''lireLigne()'': elle est déjà implémentée dans ''LecteurAbstrait''. On peut voir qu'au final, chacune des implémentations de ''Lecteur'' est plus courte qu'avant. ===== Quelques autres idées ===== * On peut gérer plus finement l'ouverture et la fermeture des fichier. Pour cela il faut fermer le ''BufferedReader'' une fois que toutes les lignes ont été lues. On peut également créer les ''Reader''s au moment où la première ligne est demandée. Pour cela on peut utiliser un champ booléen ainsi qu'une méthode abstraite ''protected void initReader()'' qui remplace la méthode ''setReader(Reader)''. Cette méthode sera appelée dans ''lireLigne()'' la première fois. * Créer une implémentation de ''Lecteur'' à partir d'une collection de lignes. Utiliser ensuite cette implémentation pour enchaîner les analyses et permettre de chercher les lignes contenant un ensemble de mots.