Java saját szoftver készítése 11. rész – Hibák kezelése

A Megrendelés nyilvántart szoftverem utolsó finomításait végzem, most foglalkoztam azzal, hogy bolondbiztossá tegyem. Ennek az lenne a célja, hogy akármilyen adatokat is adjon be a felhasználó, a szoftver ne álljon le hibajelzéssel, hanem kapjam el a kivételeket vagy előzzem meg a hibajelzések adását.

Ebben a bejegyzésben ezeken a hibakezelési dolgokon megyek végig, és ahol szükséges, ott forráskóddal mutatom meg, miről van szó.

Megrendelések felvitele

Egy korábbi bejegyzésemnél már említettem a megrendelések felvitele és módosítása ablakot, ahol az író munkabérét és a nyereséget valós időben változtattam, a mennyiség, az egységár és az író egységára mezők alapján. Erről volt szó:

Ez a metódust viselt gondot az író fizetésének frissítéséről:

private void fillSallaryAmount() {
   double amount = Double.valueOf(quantityField.getText()) * Double.valueOf(rateField.getText());
   sallaryAmountLabel.setText(String.valueOf(amount));
}

Ez pedig a nyereségről:

private void fillProfitAmount() {
   double amount = Double.valueOf(quantityField.getText()) * Double.valueOf(unitPriceField.getText()) - Double.valueOf(quantityField.getText()) * 
            Double.valueOf(rateField.getText()) ;
   profitAmountLabel.setText(String.valueOf(amount));
}

A múltkori bejegyzésben már volt róla szó, hogy milyen Listenerrel oldottam meg a mezők figyelését. Akkor azt is elmondtam, hogy ha rossz számot kezdtem el beírni, és teljesen visszatöröltem az értéket, akkor a figyelő elkapta azt a pillanatot is, amikor épp üres volt a mező, és erre ezt a hibát dobta:

Exception in thread "AWT-EventQueue-0" java.lang.NumberFormatException: empty String

A nem számok beírásakor pedig bár én a dialógusablakban nem láttam, a háttérben a Java már hibát dobott, mivel a beírt nem szám karaktereket a Listener továbbította a két fenti metódusnak. A Double.valueOf nem szám értékre pedig ezt eredményezi:

Exception in thread "AWT-EventQueue-0" java.lang.NumberFormatException: For input string: "s"

A valós időben figyelő Listener miatt nem elég csak a Küldés gomb megnyomásakor ellenőrizni a beviteli mezőket, így a fenti két metódust átírtam:

private void fillSallaryAmount() {
   try {
      if (quantityField.getText() != null && !quantityField.getText().isEmpty() && rateField.getText() != null && !rateField.getText().isEmpty()) {
         double amount = Double.valueOf(quantityField.getText()) * Double.valueOf(rateField.getText());
         sallaryAmountLabel.setText(String.valueOf(amount));
      } else {
         sallaryAmountLabel.setText("");
      }
   } catch (NumberFormatException ex) {
      // nothing happens, just handling if a user is pressing a non-number key    
   } 
}
    
private void fillProfitAmount() {
   try {
      if (quantityField.getText() != null && !quantityField.getText().isEmpty() && unitPriceField.getText() != null && !unitPriceField.getText().isEmpty() && rateField.getText() != null && !rateField.getText().isEmpty()) {
         double amount = Double.valueOf(quantityField.getText()) * Double.valueOf(unitPriceField.getText()) - Double.valueOf(quantityField.getText()) * Double.valueOf(rateField.getText()) ;
         profitAmountLabel.setText(String.valueOf(amount));
      } else {
         profitAmountLabel.setText("");
      }
   } catch (NumberFormatException ex) {
      // nothing happens, just handling if a user is pressing a non-number key    
   } 
}

A nem szám értékeknél dobott NumberFormatException-t csak elkaptam, és jeleztem az esetleges későbbi programozónak, hogy okkal nem írtam a catch ágba semmit, mert egyelőre nem kell oda most más.

Dátumok kezelése

Bár a megrendelések dátumánál már az összes JXDatePicker panel kezdőértékét az aktuális mai dátumra állítottam, a fizetési dátumnál például egyáltalán nem furcsa, ha a dátum nincs megadva. Ez elég gyakran megtörténik egy vadiúj megrendelésnél, mivel azoknál csak a megrendelés dátuma adott, a kifizetésé még egy jövőbeni esemény.

Ezt eddig nagyvonalúan nem néztem meg, most azonban már kezelnem kellett a nem megadott dátum értékeket, mivel az adatbázisba mentés, az onnan kiolvasás miatt egy csomó dátumkonverzió kellett a különböző típusok között.

Ez például:

this.insert.setDate(1, Date.valueOf(company.getLaunch()));

hibát dobott, mert a null-t próbáltam Date típusra konvertálni.

Az összes dátummal foglalkozó kódrészletet átírtam, hogy a null értékeket is kezelni tudjuk:

if (company.getLaunch() != null) {
   this.insert.setDate(1, Date.valueOf(company.getLaunch()));
} else {
   this.insert.setDate(1, null);
}

vagy itt:

if (rs.getDate("launch") != null) {
   company.setLaunch(rs.getDate("launch").toLocalDate());
} else {
   company.setLaunch(null);
}

Ez ugye különösebb gondot nem okozott, csak át kellett bogarászni a forráskódokat, és az összes dátummal kapcsolatos metódust átírni.

Ablak bezárása

A tesztelés során problémát okozott az is, ha valamelyik dialógus ablakot nem a Mégsem gombbal zártam le, hanem a jobb felső sarokban lévő X-szel. Ha emlékeztek, akkor a Controller osztályokban egy WindowsListenerrel figyeltem, hogy a feldobott dialógusablakot bezártam-e. Erről itt írtam.

modifyDelete.addWindowListener(new WindowAdapter() {
   public void windowClosed(WindowEvent e) {
      if (modifyDelete.getSelectedEvent().equals("delete")) {
         try {
            deleteCompany(Integer.valueOf(modifyDelete.getSelectedId()));
            modifyDeleteCompany();
         } catch (SQLException ex) {
            System.out.println("Hiba: " + ex.getMessage());
            JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
         }
      } else if (modifyDelete.getSelectedEvent().equals("modify")) {
         try {
            modifyCompany(Integer.valueOf(modifyDelete.getSelectedId()));
            modifyDeleteCompany();
         } catch (SQLException ex) {
            System.out.println("Hiba: " + ex.getMessage());
            JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
         }
      }
   }
});

A dialógusablak egy String változóba beírta, hogy melyik gombbal zártam be őt, ha a Küldés gombbal, akkor a String a “modify”, ha a törlés gombbal, akkor a “delete” értéket kapta. Az értékadások nélkül a String null állapotban maradt.

Az ablak X-szel történő bezárásakor aktiválódott a windowClosed metódus, és a történet ott szállt el, amikor a getSelectedEvent() által visszaadott null értéket összehasonlítottam a “delete” stringgel.

Ezt sokféle képpen meg lehetett volna oldani, én az egész blokk elé betettem egy feltételvizsgálatot, mely megnézte, hogy nem-e null állapotban maradt a String változóm, azaz nem-e X-szel zártam be az ablakot.

modifyDelete.addWindowListener(new WindowAdapter() {
   public void windowClosed(WindowEvent e) {
      if (modifyDelete.getSelectedEvent() != null) {
         if (modifyDelete.getSelectedEvent().equals("delete")) {
            try {
               deleteCompany(Integer.valueOf(modifyDelete.getSelectedId()));
               modifyDeleteCompany();
            } catch (SQLException ex) {
               System.out.println("Hiba: " + ex.getMessage());
               JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
            }
         } else if (modifyDelete.getSelectedEvent().equals("modify")) {
            try {
               modifyCompany(Integer.valueOf(modifyDelete.getSelectedId()));
               modifyDeleteCompany();
            } catch (SQLException ex) {
               System.out.println("Hiba: " + ex.getMessage());
               JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
            }
         }
      }
   }
});

A catch ágakban mindenhol szerepel egy feldobott hibaablak az adatbázishiba esetére, és egy konzolra történő kiíratás. Utóbbit a lefordított szoftver felhasználója nem fogja látni, ezeket magamnak írogattam be most a tesztelésre, de nem baj, ha benne marad. A későbbiekben is módosítom majd a kódot, az user úgysem látja, nekem segítség lehet.

Nem érdemes eltrehánykodni a kivételkezelést sem

Csak érdekességként írom le, mert ide illik, és én is megtanultam a leckét: a kivételkezelést sem érdemes eltrehánykodni. A végleges szoftver 26 együttműködő fájlból áll, és nagyon sok helyen van adatbázisművelet, ami ugye SQLException kivételt dobhat.

Mivel a NetBeans addig nem futtatja a kódot, míg a kivételek nincsenek kezelve, ezért a kismillió helyen én így oldottam meg a kivételkezelést:

} catch (SQLException ex) {
   System.out.println("Hiba: " + ex.getMessage());
}

Ami szerintem tök rendben volt, mivel kiírtam a hibaüzenetet is. Aztán kódolás közben valamit elrontottam, és a rendszer kiírta a fenti hibaüzenetet. Ezzel csak az volt a baj, hogy a 26 osztály mindegyik metódusa ugyanezt az üzenetet dobta volna, így nem lettem tőle okosabb. Át kellett írnom az összes osztályt, és pontosabb kivételkezelést beállítani, ahol megmondtam, éppen melyik osztályban vagyunk. Így a szoftvert lefuttatva már nem csak a fenti általános hibaüzenetet kaptam, hanem a Java megmondta, melyik osztályban volt a hiba. A javítása 3 másodperc volt, a kivételkezelések hibaüzenetének pótlása kb. 40 perc 🙂

Új ügyél/áfa/megrendelés/vállalat/dolgozó hozzáadása

A Controller osztályok kezelik az új egyed hozzáadását is. A CustomerController például így adta hozzá az új ügyfelet:

public void addNewCustomer() throws SQLException {
        CustomerDetailsDialog customerDetails = new CustomerDetailsDialog(frame, true);
        customerDetails.setVisible(true);
        customerDetails.addWindowListener(new WindowAdapter() {
            public void windowClosed(WindowEvent e) {
                if (customerDetails.getSelectedButton() != null) {
                    if (customerDetails.getSelectedButton().equals("send") && customerDetails.getErrorMessage() == null) {
                        try {
                            customerRepo.save(new Customer(customerDetails.getName(), customerDetails.getZip(), customerDetails.getCity(), customerDetails.getStreet(),
                        customerDetails.getHouse(), customerDetails.getTaxnumber(), customerDetails.getRegnumber(), customerDetails.getContact(), customerDetails.getEmail(), 
                        customerDetails.getWebsite()));
                        } catch (SQLException ex) {
                            System.out.println("Hiba: " + ex.getMessage());
                            JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
                        }
                    }
                }
            }
        });
    }

Feldobott egy CustomerDetailsDialog űrlapot, amiben szépen fel lehetett vinni az ügyfél adatokat a megfelelő mezőkbe:

Itt is egy figyelőt állítottam szolgálatba, a Küldés gomb megnyomása egy “send” értéket írt bele a selectedButton String változóba, a Mégsem gomb pedig egy “cancel” értéket.

Ugye itt is le kellett kezelnem azt az esetet, ha a felhasználó a jobb felső sarokban lévő X-szel zárta be az ablakot, így az egész blokkot egy feltételbe foglaltam, azaz megnéztem, hogy egyáltalán megnyomták-e valamelyik gombot:

if (customerDetails.getSelectedButton() != null) {

A CustomerDetailsDialog-ban viszont az űrlap becsukása előtt lefuttattam egy ellenőrzést, ami a Küldés gomb megnyomása után futott le, és ha hibát talált, akkor feldobott egy figyelmeztető ablakot, és nem engedte elküldeni az űrlapot. Ezzel két dolgot is megoldottam, egyrészt a kötelezőnek választott mezőket mindenképpen ki kellett tölteni, másrészt a hibás adatokat nem adtam át az adatbázisba mentéshez, hanem javításra kértem fel az usert:

Az erre szolgáló metódus pedig a CustomerDetailsDialog esetén így néz ki:

private boolean fieldCheck() {
   boolean back = true;
   String errorMessage = "";
        
   if (nameField.getText().equals("")) {
      back = false;
      errorMessage += "- Az ügyfél neve mező nem lehet üres.\n";
   }
   if (!emailField.getText().equals("")) {
      if (!emailField.getText().contains("@")) {
         back = false;
         errorMessage += "- Az email cím hibásnak tűnik, mert nem tartalmaz @ jelet.\n";
      }
   }

   if (!back) {
      this.errorMessage = "A következő hibákat találtam a kitöltés közben: \n";
      this.errorMessage += errorMessage;
   }
        
    return back;
}

Lehetne mindenféle dolgot ellenőrizni, de mivel ez a szoftver saját célra lesz, így csak a minimális feltételeket vizsgáltam. Például, legalább egy fő adat mindenhol kell, ez ügyfélnél a név, dolgozónál a név, de például az ÁFA típusnál kell az érték is. Ha a felhasználó extra adatokat is megad, azokat leellenőrzöm, például, ha beír email címet, akkor annak már tartalmaznia kell egy kukac jelet is. De például az irányítószám és a házszám sem numerikus érték, utóbbinál a lépcsőház és ajtó részlet miatt, az előbbinél pedig a külföldi ügyfeleim miatt, akiknél az irányítószám betűt is tartalmaz. Ezeket az elveket mindegyik űrlapnál betartottam, és főleg a numerikus mezőket ellenőriztem, nehogy elszálljon a program nem numerikus értékek bevitele miatt.

Itt pedig a CustomerDetailsDialog űrlap Küldés gombjának figyelője, amin látszik, hogy az űrlap nem csukódhat be, amíg a fieldCheck még hibát talál:

private void sendButtonActionPerformed(java.awt.event.ActionEvent evt) {                                           
   selectedButton = "send";
   if (!fieldCheck()) {
      JOptionPane.showMessageDialog(new javax.swing.JDialog(), errorMessage, "Hiba a kitöltés során!", JOptionPane.ERROR_MESSAGE);
   } else {      
      JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Az adatok sikeresen elmentésre kerültek.");
      this.dispose();
   }
}  

Ha visszatérünk a CustomerController osztályba, a kiegészített addNewCustomer() metódushoz, akkor láthatjuk, mi a hiba:

if (customerDetails.getSelectedButton() != null) {
   if (customerDetails.getSelectedButton().equals("send")) {
      try {
         customerRepo.save(new Customer(customerDetails.getName(), customerDetails.getZip(), customerDetails.getCity(), customerDetails.getStreet(), customerDetails.getHouse(), customerDetails.getTaxnumber(), customerDetails.getRegnumber(), customerDetails.getContact(), customerDetails.getEmail(), customerDetails.getWebsite()));

Miután ellenőriztem, hogy történt gombnyomás, és nem az X-szel zártam be a CustomerDetailsDialog űrlapot, megnéztem, hogy az Elküldés (“send”) lett-e megnyomva. Ha igen, akkor lekértem az adatokat, és megpróbáltam adatbázisba menteni az adatokat. Amire az Java hibát dobott.

Mi volt a baj? A megoldásra viszonylag gyorsan rájöttem, hiszen az űrlapot elküldve én megnyomtam a Küldés gombot, így a getSelectedButton már a “send” stringet adta vissza lekérdezéskor. Közben azonban az adatok nem voltak megfelelőek, és a fieldCheck metódus feldobott egy hibaablakot, és nem engedte bezárni az űrlapot. Ettől függetlenül a Küldés gomb megnyomása megtörtént, és a háttérben a CustomerController meghívta a DAO osztály save metódusát, elküldve neki a rossz adatokat, a Java pedig hibaüzenettel leállt.

Ezen a problémát kétféle dologgal segítettem:

A CustomerController már azt is vizsgálta, hogy üres-e a CustomerDetailsDialog űrlap errorMessage változója, mert ha igen, akkor a fieldCheck mindent rendben talált, és biztosan nem dob hibát az adatbáziskezelő rendszer az adatok elmentésekor.

public void addNewCustomer() throws SQLException {
   CustomerDetailsDialog customerDetails = new CustomerDetailsDialog(frame, true);
   customerDetails.setVisible(true);
   customerDetails.addWindowListener(new WindowAdapter() {
      public void windowClosed(WindowEvent e) {
         if (customerDetails.getSelectedButton() != null) {
            if (customerDetails.getSelectedButton().equals("send") && customerDetails.getErrorMessage() == null) {
               try {
                  customerRepo.save(new Customer(customerDetails.getName(), customerDetails.getZip(), customerDetails.getCity(), customerDetails.getStreet(), customerDetails.getHouse(), customerDetails.getTaxnumber(), customerDetails.getRegnumber(), customerDetails.getContact(), customerDetails.getEmail(), customerDetails.getWebsite()));
               } catch (SQLException ex) {
                  System.out.println("Hiba: " + ex.getMessage());
                  JOptionPane.showMessageDialog(new javax.swing.JDialog(), "Adatbázishiba: " + ex.getMessage(), "Adatbázishiba", JOptionPane.ERROR_MESSAGE);
               }
            }
         }
      }
   });
}

A CustomerDetailsDialog űrlap fieldCheck metódusa is egy kis kiegészítésre szolgált, hiszen, ha az űrlap kitöltésekor nem talált hibát, akkor tényleg null errorMessage értéket tudott lekérdezni a Controller, de ha a felhasználó hibázott, akkor az errorMessage értéket kapott. Ezután amennyiben a felhasználó javította a hibákat, és a fieldCheck nem talált már problémát, akkor az errorMessage értékét vissza kellett állítania nullára, különben a Controller sosem kapná meg a jelzést az adatbázisba mentésre. És így lett teljes a történet:

private boolean fieldCheck() {
   boolean back = true;
   String errorMessage = "";
        
   if (nameField.getText().equals("")) {
      back = false;
      errorMessage += "- Az ügyfél neve mező nem lehet üres.\n";
   }

   if (!back) {
      this.errorMessage = "A következő hibákat találtam a kitöltés közben: \n";
      this.errorMessage += errorMessage;
   } else {
      this.errorMessage = null;
   }
        
   return back;
}