Aller au contenu

TP — Mini gestionnaire de documents

Objectif

Vous allez développer la partie métier d’une petite application Java de gestion de documents.

L’interface graphique vous est fournie. Votre travail consiste à comprendre et utiliser :

L’objectif n’est pas de faire un joli écran. L’objectif est de comprendre comment on structure proprement une application.

Dans votre projet, il faut modifier votre fichier module (pour la partie graphique):

module GestionDocument {
    requires java.desktop;
}

Contexte métier

Une application permet à un utilisateur de :

L’entreprise souhaite que l’application puisse évoluer facilement :

Vous devez donc concevoir une architecture simple mais évolutive.


Ce que l’on attend de vous

Vous devez compléter la partie métier d’une application de gestion de documents.

Le programme devra manipuler au minimum deux types de documents :

L’interface graphique est déjà fournie. Vous ne devez pas vous concentrer sur le code Swing.


Compétences visées

À la fin du TP, vous devez être capable d’expliquer :


Structure du projet

src/
 ├── exception/
 │    └── DocumentException.java
 │
 ├── model/
 │    ├── AbstractDocument.java
 │    ├── TextDocument.java
 │    └── LogDocument.java
 │
 ├── repository/
 │    ├── DocumentRepository.java
 │    ├── InMemoryDocumentRepository.java
 │    └── FileDocumentRepository.java -> fourni
 │
 ├── service/
 │    └── DocumentService.java
 │
 └── ui/
      └── DocumentApp.java -> fourni aussi c'est l'ihm

Travail à faire

Partie 1 — Créer une classe abstraite AbstractDocument

Créer une classe abstraite AbstractDocument contenant les attributs communs à tous les documents :

Cette classe devra :

Contraintes


Partie 2 — Créer deux classes filles

Créer 2 classes qui héritent de AbstractDocument :

TextDocument

LogDocument


Partie 3 — Définir une interface de stockage

Créer une interface DocumentRepository qui définit le contrat suivant :

Méthodes attendues

Pour le moment on va utiliser la classe mère Exception. qui est la classe mère.

void save(AbstractDocument document) throws Exception;

AbstractDocument findById(String id) throws Exception;

List<AbstractDocument> findAll() throws Exception;

Partie 4 — Implémenter un stockage en mémoire (dans une Collection)

Créer une classe InMemoryDocumentRepository qui implémente DocumentRepository.

Cette classe stocke les documents dans une collection.

Notre Objectif

Utiliser une interface pour permettre l’utilisation de plusieurs implémentations différentes sans modifier le reste de votre programme.


Partie 5 — Implémenter un stockage en mémoire

La classe FileDocumentRepository est fourni ci-dessous :

package fr.formation.semaine2.repository;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import fr.formation.semaine2.exception.DocumentException;
import fr.formation.semaine2.model.AbstractDocument;
import fr.formation.semaine2.model.LogDocument;
import fr.formation.semaine2.model.TextDocument;

/**
 * Ici, je garde les conventions de nommage.
 * Cette classe permet de stocker nos documents dans un fichier
 */
public class FileDocumentRepository implements DocumentRepository {

    private final Path dossier;

    public FileDocumentRepository(String nomDossier) throws IOException {
        this.dossier = Paths.get(nomDossier);
        Files.createDirectories(dossier);
    }

    @Override
    public void save(AbstractDocument document) throws DocumentException {
        try {
            Path fichier = dossier.resolve(document.getId() + ".txt");
            String data = document.getType()
                    + "\n" + document.getTitre()
                    + "\n" + document.getContenu();
            Files.writeString(fichier, data);
        } catch (IOException e) {
            throw new DocumentException("Erreur lors de la sauvegarde du document", e);
        }
    }

    @Override
    public AbstractDocument findById(String id) throws DocumentException {
        try {
            Path fichier = dossier.resolve(id + ".txt");
            if (!Files.exists(fichier)) {
                return null;
            }

            List<String> lignes = Files.readAllLines(fichier);
            if (lignes.size() < 3) {
                throw new DocumentException("Le fichier du document est invalide");
            }

            String type = lignes.get(0);
            String titre = lignes.get(1);
            String contenu = String.join("\n", lignes.subList(2, lignes.size()));

            if ("LOG".equals(type)) {
                return new LogDocument(titre, contenu);
            }
            return new TextDocument(titre, contenu);

        } catch (IOException e) {
            throw new DocumentException("Erreur lors de la lecture du document", e);
        }
    }

    @Override
    public List<AbstractDocument> findAll() throws DocumentException {
        List<AbstractDocument> documents = new ArrayList<>();

        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dossier, "*.txt")) {
            for (Path path : stream) {
                String filename = path.getFileName().toString();
                String id = filename.substring(0, filename.length() - 4);
                AbstractDocument doc = findById(id);
                if (doc != null) {
                    documents.add(doc);
                }
            }
        } catch (IOException e) {
            throw new DocumentException("Erreur lors de la lecture du dossier", e);
        }

        return documents;
    }
}

Cette classe permet de :

Vous devez créer une nouvelle classe qui implémente aussi DocumentRepository.

Remarque

Ici, les exceptions sont importantes : les accès aux fichiers peuvent échouer !


Partie 6 — Créer une exception métier (optionnelle si pas encore vu)

Créer une classe DocumentException. Elle servira à encapsuler les erreurs de lecture/écriture de fichiers avec un message plus compréhensible.

Exemples :


Partie 7 — Créer un service métier

Créer une classe DocumentService chargée de :

Le service devra manipuler AbstractDocument et DocumentRepository.

C’est ici que le polymorphisme doit apparaître.


Code de l’interface graphique fourni

Le code ci-dessous vous est donné pour tester votre travail. Vous n’avez pas à le réécrire.

package fr.formation.semaine2.gestion.ui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;

import fr.formation.semaine2.model.AbstractDocument;
import fr.formation.gestion.repository.DocumentRepository;
import fr.formation.gestion.repository.FileDocumentRepository;
import fr.formation.gestion.repository.InMemoryDocumentRepository;
import fr.formation.gestion.repository.JsonDocumentRepository;
import fr.formation.gestion.service.DocumentService;

public class DocumentApp extends JFrame {

    private final DocumentRepository memoryRepository = new InMemoryDocumentRepository();
    private DocumentRepository fileRepository;
   // private DocumentRepository jsonRepository;

    // si on voulait figer le mode de sauvegarde
 	//private final DocumentService service = new DocumentService(new InMemoryDocumentRepository());
	// ici on pourra choisir dynamiquement
    private DocumentService service;

    private final JComboBox<String> repositoryBox =
            new JComboBox<>(new String[]{"Mémoire", "Fichier TXT"});

    private final JComboBox<String> typeBox =
            new JComboBox<>(new String[]{"TEXT", "LOG"});

    private final JTextField titleField = new JTextField(25);
    private final JTextArea contentArea = new JTextArea(8, 30);
    private final JTextArea displayArea = new JTextArea();

    private final JButton applyStorageButton = new JButton("Appliquer le stockage");
    private final JButton saveButton = new JButton("Enregistrer");
    private final JButton refreshButton = new JButton("Rafraîchir");
    private final JButton clearButton = new JButton("Vider la saisie");

    private final DefaultListModel<AbstractDocument> listModel = new DefaultListModel<>();
    private final JList<AbstractDocument> documentList = new JList<>(listModel);

    public DocumentApp() {
        super("Mini gestionnaire de documents");

        initRepositories();
        initService();
        initComponents();
        buildLayout();
        bindEvents();
        refreshDocuments();

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(1100, 650);
        setLocationRelativeTo(null);
    }

    private void initRepositories() {
        try {
            fileRepository = new FileDocumentRepository("data/txt");
          //  jsonRepository = new JsonDocumentRepository("data/json");
        } catch (Exception e) {
            JOptionPane.showMessageDialog(
                    this,
                    "Erreur lors de l'initialisation des stockages : " + e.getMessage(),
                    "Erreur",
                    JOptionPane.ERROR_MESSAGE
            );
        }
    }

    private void initService() {
        service = new DocumentService(memoryRepository);
    }

    private void initComponents() {
        repositoryBox.setPreferredSize(new Dimension(150, 28));
        typeBox.setPreferredSize(new Dimension(120, 28));
        applyStorageButton.setPreferredSize(new Dimension(180, 28));
        saveButton.setPreferredSize(new Dimension(140, 30));
        refreshButton.setPreferredSize(new Dimension(120, 30));
        clearButton.setPreferredSize(new Dimension(140, 30));

        contentArea.setLineWrap(true);
        contentArea.setWrapStyleWord(true);

        displayArea.setEditable(false);
        displayArea.setLineWrap(true);
        displayArea.setWrapStyleWord(true);
        displayArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));

        documentList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    }

    private void buildLayout() {
        setLayout(new BorderLayout(10, 10));

        JPanel northPanel = buildNorthPanel();
        JSplitPane centerSplitPane = buildCenterPanel();

        add(northPanel, BorderLayout.NORTH);
        add(centerSplitPane, BorderLayout.CENTER);
    }

    private JPanel buildNorthPanel() {
        JPanel globalPanel = new JPanel(new BorderLayout(0, 8));
        globalPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10));

        JPanel storagePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 5));
        storagePanel.setBorder(BorderFactory.createTitledBorder("Mode de stockage"));

        storagePanel.add(new JLabel("Stockage :"));
        storagePanel.add(repositoryBox);
        storagePanel.add(applyStorageButton);

        JPanel formPanel = new JPanel(new BorderLayout(8, 8));
        formPanel.setBorder(BorderFactory.createTitledBorder("Saisie d'un document"));

        JPanel firstLine = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 5));
        firstLine.add(new JLabel("Titre :"));
        firstLine.add(titleField);
        firstLine.add(new JLabel("Type :"));
        firstLine.add(typeBox);

        JScrollPane contentScrollPane = new JScrollPane(contentArea);
        contentScrollPane.setPreferredSize(new Dimension(200, 140));

        JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 5));
        buttonsPanel.add(saveButton);
        buttonsPanel.add(refreshButton);
        buttonsPanel.add(clearButton);

        formPanel.add(firstLine, BorderLayout.NORTH);
        formPanel.add(contentScrollPane, BorderLayout.CENTER);
        formPanel.add(buttonsPanel, BorderLayout.SOUTH);

        globalPanel.add(storagePanel, BorderLayout.NORTH);
        globalPanel.add(formPanel, BorderLayout.CENTER);

        return globalPanel;
    }

    private JSplitPane buildCenterPanel() {
        JScrollPane listScrollPane = new JScrollPane(documentList);
        listScrollPane.setBorder(BorderFactory.createTitledBorder("Liste des documents"));

        JScrollPane displayScrollPane = new JScrollPane(displayArea);
        displayScrollPane.setBorder(BorderFactory.createTitledBorder("Contenu du document"));

        JSplitPane splitPane = new JSplitPane(
                JSplitPane.HORIZONTAL_SPLIT,
                listScrollPane,
                displayScrollPane
        );
        splitPane.setDividerLocation(350);
        splitPane.setResizeWeight(0.35);

        JPanel wrapper = new JPanel(new BorderLayout());
        wrapper.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        wrapper.add(splitPane, BorderLayout.CENTER);

        JSplitPane outer = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        outer.setLeftComponent(listScrollPane);
        outer.setRightComponent(displayScrollPane);
        outer.setDividerLocation(350);
        outer.setResizeWeight(0.35);

        return outer;
    }

    private void bindEvents() {
        applyStorageButton.addActionListener(e -> {
            changeRepository();
            refreshDocuments();
        });

        saveButton.addActionListener(e -> saveDocument());

        refreshButton.addActionListener(e -> refreshDocuments());

        clearButton.addActionListener(e -> clearForm());

        documentList.addListSelectionListener(e -> {
            if (!e.getValueIsAdjusting()) {
                AbstractDocument selected = documentList.getSelectedValue();
                if (selected != null) {
                    displayArea.setText(selected.format());
                }
            }
        });
    }

    private void changeRepository() {
        String choice = repositoryBox.getSelectedItem().toString();

        if ("Fichier TXT".equals(choice)) {
            service = new DocumentService(fileRepository);
        }
//        else if ("Fichier JSON".equals(choice)) {
//            service = new DocumentService(jsonRepository);
//        }
        else {
            service = new DocumentService(memoryRepository);
        }

        JOptionPane.showMessageDialog(
                this,
                "Stockage actif : " + choice,
                "Information",
                JOptionPane.INFORMATION_MESSAGE
        );
    }

    private void saveDocument() {
        try {
            String title = titleField.getText().trim();
            String content = contentArea.getText().trim();
            String type = typeBox.getSelectedItem().toString();

            if (title.isEmpty()) {
                JOptionPane.showMessageDialog(this, "Le titre est obligatoire.");
                return;
            }

            if (content.isEmpty()) {
                JOptionPane.showMessageDialog(this, "Le contenu est obligatoire.");
                return;
            }

            service.createAndSave(type, title, content);

            clearForm();
            refreshDocuments();

            JOptionPane.showMessageDialog(this, "Document enregistré avec succès.");

        } catch (Exception e) {
            JOptionPane.showMessageDialog(
                    this,
                    "Erreur lors de l'enregistrement : " + e.getMessage(),
                    "Erreur",
                    JOptionPane.ERROR_MESSAGE
            );
        }
    }

    private void refreshDocuments() {
        try {
            listModel.clear();
            List<AbstractDocument> documents = service.getAll();

            for (AbstractDocument doc : documents) {
                listModel.addElement(doc);
            }

            displayArea.setText("");

        } catch (Exception e) {
            JOptionPane.showMessageDialog(
                    this,
                    "Erreur lors du chargement des documents : " + e.getMessage(),
                    "Erreur",
                    JOptionPane.ERROR_MESSAGE
            );
        }
    }

    private void clearForm() {
        titleField.setText("");
        contentArea.setText("");
        typeBox.setSelectedIndex(0);
        titleField.requestFocus();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            DocumentApp app = new DocumentApp();
            app.setVisible(true);
        });
    }
}

Questions à se poser

  1. Pourquoi DocumentRepository est-elle une interface et non une classe classique ?
  2. Pourquoi AbstractDocument est-elle abstraite ?
  3. Quel est l’intérêt d’avoir TextDocument et LogDocument plutôt qu’une seule classe Document ?
  4. Où se situe le polymorphisme dans ce projet ?
  5. Si l’on voulait ajouter un type JsonDocument, quelles classes faudrait-il modifier ?
  6. Quel est l’intérêt d’utiliser une exception personnalisée DocumentException ?
  7. Que faudrait-il changer pour remplacer le stockage mémoire par un stockage fichier ?

Bonus

Si vous terminez en avance, vous pouvez :


Conseil

Ne cherchez pas à faire compliqué.

Un bon code pour ce TP est un code :