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 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 ojdbc14.jar depuis le site http://www.oracle.com. Une copie de la documentation JavaDoc se trouve 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 ojdbc14.jar. Le pilote JDBC pour Oracle est maintenant accessible au projet.

Créer une classe OracleProvider1) 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 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 Lecteurs, un pour chaque fichier à analyser.

La deuxième étape consistera à parcourir cette collection pour lancer l'analyse sur chacun des Lecteurs.

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<Lecteur>. 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<Lecteur> 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 Lecteurs de la collection, on appelle la méthode analyse de l'Analyseur et on affiche le résultat comme au TP 1 ou au 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:tttppp 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 Lecteurs 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 Readers 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.
1)
toujours dans le package utilfichiers