Kurs Java od podstaw Tydzień 4

Omówienie aplikacji domowej – część II

Omówienie aplikacji domowej – część II

W pierwszej części omówiliśmy tworzenie ProductService, w drugiej części zajmiemy się ogarnięciem relacji z plikiem – czyli naszą bazą danych.

W przypadku klas Dao, nie będziemy ich testować – za to później połączymy je do serwisów i to właśnie je będziemy testować. Jednak póki co omówmy klasy Dao – ja zrobię to tylko na ProductDao, ponieważ UserDao jest zrobione analogicznie. 😉

Pliki

Na początku stworzyłem sobie metodę odpowiedzialną za stworzenie pliku o ile nie istnieje. Dzięki temu unikniemy błędów spowodowanymi np. próbą odczytu z nieistniejącego pliku – co najwyżej będzie on pusty.

W tym celu stworzyłem osobny pakiet Utils – w którym będę przechowywał przeróżne „narzędzia” czyli np. jakieś conwertery itd.

Klasa FileUtils wygląda tak i ma tylko jedną metodę statyczną odpowiedzialną za stworzenie pliku o konkretnej nazwie.

package utils;

import java.io.File;
import java.io.IOException;

public class FileUtils {

    public static void createNewFile(String fileName) throws IOException {
        File file = new File(fileName);
        file.createNewFile();
    }
}

Do tego celu posłużyłem się klasą File. 

Skoro już to mamy to przejdźmy dalej.

ProductDao

Na początku stworzymy sobie interfejs mówiący nam o tym jakie operację możemy wykonać na pliku. Póki co nie przejmuj się „throws …” musi to po prostu się tu znaleźć, a mówi to tylko tyle, że metoda może rzucić właśnie taki wyjątek. O wyjątkach powiemy sobie wkrótce.

Zapisywanie produktu:

void saveProduct(Product product) throws IOException;

Zapisywanie listy produktów:

void saveProducts(List<Product> products) throws FileNotFoundException;

Usuwanie produktu o danym ID

void removeProductById(Long productId) throws IOException;

Usuwanie produktu o podanym productName

void removeProductByName(String productName) throws IOException;

Zwrócenie wszystkich produktów:

List<Product> getAllProducts() throws IOException;

Zwrócenie produktu przez jego ID

Product getProductById(Long productId) throws IOException;

Zwrócenie produktu po jego nazwie.

Product getProductByProductName(String productName) throws IOException;

Implementacja

Czas na napisanie implementacji interfejsu ProductDao, w tym celu stworzyłem klasę ProductDaoImpl, która implementuje właśnie ten interfejs. Dodatkowo umieściłem ją w pakiecie dao symbolizujący klasy odpowiedzialne za relację z bazą lub plikiem.

Na początku potrzebujemy konstruktora – ja stworzyłem tylko parametrowy. Jednym z parametrów jest nazwa pliku, do którego mają być zapisywane produkty oraz productType, który symbolizuję jaki produkt zapisujemy – Cloth czy Boots. Posłuży nam później do odczytu z pliku.

public ProductDaoImpl(String fileName, String productType) throws IOException {
    this.fileName=fileName;
    this.productType=productType;
    FileUtils.createNewFile(fileName);
}

W konstruktorze wykorzystuję również metodę createNewFile z wcześniej omawianej klasy FileUtils.

getAllProducts

Zacznijmy od metody wykorzystywanej prawie w każdej innej metodzie – getAllProducts odpowiedzialnej za przeparsowanie całego pliku.

public List<Product> getAllProducts() throws IOException {
}

Na początku tworzymy pustą listę, do której będziemy zapisywać wczytane produkty – oraz BufferedReader potrzebny do odczytu z pliku.

List<Product> products = new ArrayList<Product>();
BufferedReader bufferedReader = new BufferedReader(new FileReader(fileName));

Następnie potrzebujemy pętli do wczytywania linijca po linijce, aż do końca pliku:

String readLine = bufferedReader.readLine();
while(readLine != null) {

}

A w pętli potrzebujemy mechanizmu do parsowania – czyli analizowania wczytanego Stringa – do klasy Product. Czyli mechanizmu, który wyciągnie nam wszystkie potrzebne informację do utworzenia obiektu Product.

Product product = ProductParser.stringToProduct(readLine, productType);

W tym celu stworzyłem sobię osobną klasę ProductParser oraz metodę stringToProduct – gdzie daję wczytaną linię (readLine) oraz jaki typ produktu – Product, Boots czy Cloth. Aby wiedział jak parsować. Przejdźmy do implementacji tej metody.

Czyli tworzymy sobię klasę ProductParser – ja zrobiłem to w pakiecie: src/main/java/entity/parser – w końcu parsery są śliśle związane z naszymi modelami.

Implementacja statycznej metody stringToProduct wygląda tak:

public static Product stringToProduct(String productStr, String productType) {
    if (productType.equals("PRODUCT")) {
        return convertToProduct(productStr);
    } else if (productType.equals("CLOTH")) {
        return convertToCloth(productStr);
    } else if (productType.equals("BOOTS")) {
        return convertToBoots(productStr);
    }

    return null;
}

Czyli na podstawie konkretnego productType wywołuje odpowiednie konwertery, które zwracają obiekt typu Product.

ConvertToProduct:

private static Product convertToProduct(String productStr) {
    String [] productInformations = productStr.split(Product.PRODUCT_SEPARATOR);

    Long id = Long.parseLong(productInformations[0]);
    String productName = productInformations[1];
    Float price = Float.parseFloat(productInformations[2]);
    Float weight = Float.parseFloat(productInformations[3]);
    String color = productInformations[4];
    Integer productCount = Integer.parseInt(productInformations[5]);

    return new Product(id, productName, price, weight, color, productCount);
}

Przy użyciu metody split – rozbijam całego Stringa na tablicę Stringów. Podając jak ma dzielić Stringa – czyli przy użyciu separatora zdefiniowanego w klasie Product – a wygląda dokładnie to tak:

public final static String PRODUCT_SEPARATOR = "#";

Po przeprasowaniu całego Stringa – wyciągnięciu z tablicy wartości i niektórych przerzutowaniu na liczbę tworzy się obiekt Product.

Analogicznie jest zrobiony convertToCloth:

private static Cloth convertToCloth(String productStr) {
    String [] productInformations = productStr.split(Product.PRODUCT_SEPARATOR);

    Long id = Long.parseLong(productInformations[0]);
    String productName = productInformations[1];
    Float price = Float.parseFloat(productInformations[2]);
    Float weight = Float.parseFloat(productInformations[3]);
    String color = productInformations[4];
    Integer productCount = Integer.parseInt(productInformations[5]);
    String size = productInformations[6];
    String material = productInformations[7];

    return new Cloth(id, productName, price, weight, color, productCount, size, material);
}

Oraz convertToBoots

private static Boots convertToBoots(String productStr) {
    String [] productInformations = productStr.split(Product.PRODUCT_SEPARATOR);

    Long id = Long.parseLong(productInformations[0]);
    String productName = productInformations[1];
    Float price = Float.parseFloat(productInformations[2]);
    Float weight = Float.parseFloat(productInformations[3]);
    String color = productInformations[4];
    Integer productCount = Integer.parseInt(productInformations[5]);
    Integer size = Integer.parseInt(productInformations[6]);
    Boolean isNaturalSkin = Boolean.parseBoolean(productInformations[7]);

    return new Boots(id, productName, price, weight, color, productCount, size, isNaturalSkin);
}

Różnią się one tylko większą ilością parametrów.

Zauważ, że metody convertTo- są private czyli dostępne tylko z poziomu klasy. Dzięki temu metoda stringToProduct sama decyduje jaką funkcję wywołać, a nie osoba używająca parsera z klasie DAO.

Mając już nasz productParser możemy wrócić do wczytywania produktów:

Product product = ProductParser.stringToProduct(readLine, productType);

Na wszelki wypadek możemy sprawdzić czy, aby na pewno nie został zwrócony null – czyli sytuacja, gdy productType jest nierozpoznawalny przez stringToProduct.

Product product = ProductParser.stringToProduct(readLine, productType);
if (product != null) {
    products.add(product);
}

Na koniec zamykamy plik, zwracamy listę i wtedy nasza metoda wygląda tak:

public List<Product> getAllProducts() throws IOException {
    List<Product> products = new ArrayList<Product>();
    BufferedReader bufferedReader = new BufferedReader(new FileReader(fileName));

    String readLine = bufferedReader.readLine();
    while(readLine != null) {
        Product product = ProductParser.stringToProduct(readLine, productType);
        if (product != null) {
            products.add(product);
        }
        readLine = bufferedReader.readLine();
    }
    bufferedReader.close();

    return products;
}

saveProducts

Przejdźmy do implementacji metody odpowiedzialnej za zapisanie całej listy produktów.

Na początku potrzebujemy stworzyć obiekt odpowiedzialny za zapisywanie danych do pliku – oczywiście w trybie dopisywanie, a nie nadpisywania!

PrintWriter printWriter = new PrintWriter(new FileOutputStream(fileName, true));

Następnie w pętli przejść po wszystkich elementach listy i każdy z nich zapisać do pliku:

for(Product product : products) {
    printWriter.write(product.toString() + "\n");
}

Na koniec zamknąć plik.

printWriter.close();

I cała metoda wygląda tak:

public void saveProducts(List<Product> products) throws FileNotFoundException {
    PrintWriter printWriter = new PrintWriter(new FileOutputStream(fileName, true));
    for(Product product : products) {
        printWriter.write(product.toString() + "\n");
    }
    printWriter.close();
}

Ale teraz stop!

Skoro nasz parser ma ustaloną politykę wczytywania – czyli dzielic Stringa na podstawie Separatora # to metoda toString powinna z paisywać produkt w odpowiedniej formie. Inaczej parser nie zadziała.

@Override
public String toString() {
    return id + PRODUCT_SEPARATOR + productName + PRODUCT_SEPARATOR + price + PRODUCT_SEPARATOR + weight + PRODUCT_SEPARATOR + color + PRODUCT_SEPARATOR + productCount;
}

I po tym zabiegu możemy już zapisywać wszystkie produkty.

Jednak nie do końca – spójrz, że metoda do zapisywania produktów będzie zawsze nadpisywać plik – czyli będzie powielać produkty po każdym zapisie. W tym celu musimy najpierw wyczyścić plik np. tworząc metodę clearFile w klasie FileUtils, która wygląda tak:

public static void clearFile(String fileName) throws FileNotFoundException {
    PrintWriter pw = new PrintWriter(fileName);
    pw.close();
}

Otwieramy plik i zamykamy – plik zostanie wyczyszczony.

Teraz nasza metoda wygląda tak:

public void saveProducts(List<Product> products) throws FileNotFoundException {
    FileUtils.clearFile(fileName);
    PrintWriter printWriter = new PrintWriter(new FileOutputStream(fileName, true));
    for(Product product : products) {
        printWriter.write(product.toString() + "\n");
    }
    printWriter.close();
}

W innych metodach będziemy własnie korzystać z metody wczytania produktów do listy – dodamy/usuniemy produkt z listy i ponownie będziemy zapisywać całą listę do pliku.

saveProduct

Wczytujemy listę, zapisujemy do niej product i zapisujemy całą liste – wszystko przy użyciu gotowych metod.

public void saveProduct(Product product) throws IOException {
    List<Product> products = getAllProducts();
    products.add(product);
    saveProducts(products);
}

getProductById

Wyszukiwanie produktu po id polega na wczytaniu całej listy oraz przeszukaniu jej w celu znalezienie id.

public Product getProductById(Long productId) throws IOException {
    List<Product> products = getAllProducts();

    for (Product product : products
         ) {
        boolean isFoundProduct = product.getId().equals(productId);
        if (isFoundProduct) {
            return product;
        }

    }

    return null;
}

getProductByProductName

To samo w przypadku wyszukiwania po productName

public Product getProductByProductName(String productName) throws IOException {
    List<Product> products = getAllProducts();

    for (Product product : products
            ) {
        boolean isFoundProduct = product.getProductName().equals(productName);
        if (isFoundProduct) {
            return product;
        }

    }

    return null;
}

removeProductById

Usuwanie po ID polega na przeszukaniu wczytanej listy – jeżeli ID zostanie znaleziony to produkt spod konkretnego indeksu – który symbolizuję zmienna i. Do usunięcia użyjemy metody remove, której podajemy index elementu w liście.

public void removeProductById(Long productId) throws IOException {
    List<Product> products = getAllProducts();

    for(int i=0;i<products.size(); i++) {
        boolean isFoundProduct = products.get(i).getId().equals(productId);
        if (isFoundProduct) {
            products.remove(i);
        }
    }

    saveProducts(products);
}

removeProductByProductName

I analogicznie usuwanie po productName:

public void removeProductByName(String productName) throws IOException {
    List<Product> products = getAllProducts();

    for(int i=0;i<products.size(); i++) {
        boolean isFoundProduct = products.get(i).getProductName().equals(productName);
        if (isFoundProduct) {
            products.remove(i);
        }
    }

    saveProducts(products);
}

Jeszcze coś!

Skoro napisaliśmy już tyle kodu to trzeba go „zapamiętać” w gicie:

git add *
git commit -m "implemented product dao"

I ewentualnie wypchnąć zmiany na zdalne repozytorium:

git push origin master

I ewentualnie pochwalić się linkiem do repozytorium w komentarzu. 😉

Podsumowanie

Moje całe rozwiązanie możesz podejrzeć tutaj.

To na tyle omawiania zadania, mam nadzieję, że wszystko czego nie mogłeś zrozumieć zostało wyjaśnione – jeśli tak się nie stało to pytaj w komentarzu. Chcę, abyś wiedział czemu tak, a nie inaczej – a czemu tak nie działa. Pytając nauczysz się jeszcze więcej. 😉

Mając zrobione podsumowanie materiału z poprzednich tygodnii możemy przejść do dalszej nauki i dalszego tworzenia aplikacji. 😉

Subscribe
Powiadom o
guest
6 komentarzy
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Poul
Poul
2 lat temu

Mam pytanie dotyczące atrybutu append w konstruktorze FileOutpustream. Jeżeli true oznacza dodawanie danych do już istniejących, a do tego przez każdym zapisem danych do pliku czyścimy plik, to czy danie false nie robi nam już tego za nas? Nie wiem, może czegoś nie rozumiem, może coś ominąłem. ale sprawdzałem obie opcje i program działa tak samo, przynajmniej tak mi się wydaje.

Łukasz
Łukasz
2 lat temu

Cześć Kamil! : )

Pobrałem twój kod z git-a i w momencie gdy dodaję kolejnego użytkownika bądź ubranie czy buty pojawia się błąd:

Exception in thread „main” java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.String.substring(String.java:1969)
at java.lang.String.split(String.java:2353)
at java.lang.String.split(String.java:2422)
at entity.parser.ProductParser.convertToCloth(ProductParser.java:37)
at entity.parser.ProductParser.stringToProduct(ProductParser.java:13)
at dao.ProductDaoImpl.getAllProducts(ProductDaoImpl.java:72)
at dao.ProductDaoImpl.saveProduct(ProductDaoImpl.java:26)
at Main.main(Main.java:24)

Process finished with exit code 1

i generalnie nie dopisuje kolejnej rzeczy do pliku.

Dodaję kolejnego usera jako nowy obiekt i zapisuję do userDao metodą saveUser.
Czy gdzieś popełniam błąd?

Łukasz
Łukasz
2 lat temu
Reply to  Łukasz

Ok, trzeba to zrobić przez utworzenie listy produktów lub userów i wtedy użyć metody saveProduct/User.

Czy w klasie UserDaoImpl nie brakuje w metodzie czyszczenia pliku? Bo kolejni userzy robiąc w ten sposób są dodawani do istniejących i się powtarzają.

Kamil Klimek
Kamil Klimek
2 lat temu
Reply to  Łukasz

Cześć,
może czasami wystąpić małe zamieszanie w kodzie, więc super, że piszesz o tym, a jeszcze lepiej, że znalazłeś rozwiązanie.

Co do czyszczenia pliku, to prawda, że zapomniałem o metodzie czyszczenia pliku w UserDaoImpl, dzięki za informację. 😉

Łukasz
Łukasz
2 lat temu
Reply to  Kamil Klimek

Jeszcze w metodzie getAllProducts() i getAllUsers() brakuje readLine = bufferedReader.readLine();, ponieważ program gdy wchodzi w pętle while nie ma jak z niej wyjść i się wiesza, tak mi się wydaje 🙂

Kamil Klimek
Kamil Klimek
2 lat temu
Reply to  Łukasz

Kurcze, masz rację – faktycznie zapomniałem o tak ważnym elemencie. Niestety pośpiech robi swoje, jednak dzięki za reakcję. 😉