Tworzenie aplikacji desktopowej z Java Desktop Application w NetBeans IDE 6.1 - część 2

Z Jacek Laskowski - Wiki Projektanta Java EE

W poprzednim artykule Tworzenie aplikacji desktopowej z Java Desktop Application w NetBeans IDE 6.1 przedstawiłem procedurę tworzenia aplikacji desktopowej "podpierając się" asystentem Java Desktop Application dostępnym w NetBeans IDE 6.1 oraz Java Persistence API (JPA) jako mechanizmem tworzenia odpowiednich struktur bazodanowych automatycznie. Głównym mankamentem aplikacji był brak korzystania z modelu z projektu przychodnia-model, gdzie pewne wartości były generowane automatycznie, a samo mapowanie encji odbywało się poprzez wydzielony plik mapowania META-INF/orm.xml.

W tym artykule przedstawię kolejne etapy udoskonalania aplikacji o wykorzystanie modelu z projektu przychodnia-model.

Dla poprawnej współpracy między interfejsem użytkownika (GUI) aplikacji desktopowej przychodnia a modelem opartym o JPA należy skorzystać ze wspacia technologii, która ostatnimi czasy ponownie wraca do łask - Java Beans 1.0.1. W skrócie sprowadza się do wysłania zdarzenia przy każdorazowej zmianie wartości atrybutu encji (tutaj też może kryć się jedna z zalet stosowania adnotacji JPA na poziomie metody zamiast polach instancji, gdzie w pierwszym przypadku metoda zapisu (ang. setter) faktycznie jest wykonywana przez JPA, a więc i nastąpi wysłanie zdarzenia o zmianie).

Spis treści

Wprowadzenie java.beans.PropertyChangeSupport do modelu aplikacji

Napisałem w Tworzenie aplikacji desktopowej z Java Desktop Application w NetBeans IDE 6.1, że można skorzystać z pomocy Add Property... jednakże sam skorzystałem z ręcznego dodania odpowiednich pól instancji i asystenta Getter and Setter.... Zaletą Add Property jest możliwość utworzenia metod zapisu i odczytu automatycznie wraz z generowaniem zdarzeń o ich wywołaniach, co właśnie przyda mi się do rozsyłania zdarzeń.

Porównując klasę pl.jaceklaskowski.przychodnia.Pacjent utworzoną przez asystenta Java Desktop Application a klasę pl.jaceklaskowski.przychodnia.model.Pacjent główną różnicą jest skorzystanie z klasy java.beans.PropertyChangeSupport. Właśnie za pomocą tej klasy następuje poinformowanie kontrolek GUI o zmianie wartości pola encji i konieczności odświeżenia kontrolek GUI. Zmieniam klasy w projekcie przychodnia-model, tak aby korzystały z niej z pomocą asystenta Add Property - kasuję wszystkie pola instancji (poza private List<Wizyta> wizyty) i tworzę je ponownie z Add Property zaznaczając opcję Generate Property Change Support.

Grafika:add-property.gif

pl.jaceklaskowski.przychodnia.model.Pacjent

Problem w tym, że wygenerowane metody zapisu i odczytu w ogóle nie korzystają z propertyChangeSupport (!) Nie pozostaje mi nic innego jak skorzystać z niezawodnej metody Copy-And-Paste, czyli wrócić do pierwotnej wersji klasy Pacjent i po prostu dodać do jej ciała odpowiednie wywołania. Ostatecznie klasa pl.jaceklaskowski.przychodnia.Pacjent prezentuje się następująco:

package pl.jaceklaskowski.przychodnia.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.util.List;

public class Pacjent implements Serializable {

    private static final long serialVersionUID = 1L;
    private String pesel;
    private String imie;
    private String nazwisko;
    private String telefon;
    private int wiek;
    // TODO: Zmienic na Enum
    private boolean plec;
    private List<Wizyta> wizyty;
    private PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);

    public Pacjent() {
    }

    public Pacjent(String pesel, String imie, String nazwisko, String telefon) {
        this.pesel = pesel;
        this.imie = imie;
        this.nazwisko = nazwisko;
        this.telefon = telefon;
    }

    public String getPesel() {
        return pesel;
    }

    public void setPesel(String pesel) {
        String oldPesel = this.pesel;
        this.pesel = pesel;
        changeSupport.firePropertyChange("pesel", oldPesel, pesel);
    }

    public String getImie() {
        return imie;
    }

    public void setImie(String imie) {
        String oldImie = this.imie;
        this.imie = imie;
        changeSupport.firePropertyChange("imie", oldImie, imie);
    }

    public String getNazwisko() {
        return nazwisko;
    }

    public void setNazwisko(String nazwisko) {
        String oldNazwisko = this.nazwisko;
        this.nazwisko = nazwisko;
        changeSupport.firePropertyChange("nazwisko", oldNazwisko, nazwisko);
    }

    public boolean isPlec() {
        return plec;
    }

    public void setPlec(boolean plec) {
        boolean oldPlec = this.plec;
        this.plec = plec;
        changeSupport.firePropertyChange("plec", oldPlec, plec);
    }

    public String getTelefon() {
        return telefon;
    }

    public void setTelefon(String telefon) {
        String oldTelefon = this.telefon;
        this.telefon = telefon;
        changeSupport.firePropertyChange("telefon", oldTelefon, telefon);
    }

    public int getWiek() {
        return wiek;
    }

    public void setWiek(int wiek) {
        int oldWiek = this.wiek;
        this.wiek = wiek;
        changeSupport.firePropertyChange("wiek", oldWiek, wiek);
    }

    public List<Wizyta> getWizyty() {
        return wizyty;
    }

    public void setWizyty(List<Wizyta> wizyty) {
        this.wizyty = wizyty;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (pesel != null ? pesel.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Pacjent)) {
            return false;
        }
        Pacjent other = (Pacjent) object;
        if ((this.pesel == null && other.pesel != null) || (this.pesel != null && !this.pesel.equals(other.pesel))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Pacjent[id=" + pesel + "]";
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        changeSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        changeSupport.removePropertyChangeListener(listener);
    }
}

pl.jaceklaskowski.przychodnia.model.Wizyta

Podobnie postępuję z pl.jaceklaskowski.przychodnia.model.Wizyta:

package pl.jaceklaskowski.przychodnia.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.util.Date;

public class Wizyta implements Serializable {

    private static final long serialVersionUID = 1L;
    private long id;
    private Pacjent pacjent;
    private Date dzien;
    private String objawy;
    private String rozpoznanie;
    private String leczenie;
    private PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);

    public Long getId() {
        return id;
    }

    public void setId(long id) {
        long oldId = this.id;
        this.id = id;
        changeSupport.firePropertyChange("id", oldId, id);
    }

    public Pacjent getPacjent() {
        return pacjent;
    }

    public void setPacjent(Pacjent pacjent) {
        Pacjent oldPacjent = this.pacjent;
        this.pacjent = pacjent;
        changeSupport.firePropertyChange("pacjent", oldPacjent, pacjent);
    }

    public Date getDzien() {
        return dzien;
    }

    public void setDzien(Date dzien) {
        Date oldDzien = this.dzien;
        this.dzien = dzien;
        changeSupport.firePropertyChange("dzien", oldDzien, dzien);
    }

    public String getLeczenie() {
        return leczenie;
    }

    public void setLeczenie(String leczenie) {
        String oldLeczenie = this.leczenie;
        this.leczenie = leczenie;
        changeSupport.firePropertyChange("leczenie", oldLeczenie, leczenie);
    }

    public String getObjawy() {
        return objawy;
    }

    public void setObjawy(String objawy) {
        String oldObjawy = this.objawy;
        this.objawy = objawy;
        changeSupport.firePropertyChange("objawy", oldObjawy, objawy);
    }

    public String getRozpoznanie() {
        return rozpoznanie;
    }

    public void setRozpoznanie(String rozpoznanie) {
        String oldRozpoznanie = this.rozpoznanie;
        this.rozpoznanie = rozpoznanie;
        changeSupport.firePropertyChange("rozpoznanie", oldRozpoznanie, rozpoznanie);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 61 * hash + (int) (this.id ^ (this.id >>> 32));
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Wizyta other = (Wizyta) obj;
        if (this.id != other.id) {
            return false;
        }
        if (this.pacjent.equals(other.pacjent) 
           && (this.pacjent == null || !this.pacjent.equals(other.pacjent))) {
            return false;
        }
        if (this.dzien.equals(other.dzien) 
           && (this.dzien == null || !this.dzien.equals(other.dzien))) {
            return false;
        }
        if (this.objawy.equals(other.objawy)
           && (this.objawy == null || !this.objawy.equals(other.objawy))) {
            return false;
        }
        if (this.rozpoznanie.equals(other.rozpoznanie)
           && (this.rozpoznanie == null || !this.rozpoznanie.equals(other.rozpoznanie))) {
            return false;
        }
        if (this.leczenie.equals(other.leczenie)
           && (this.leczenie == null || !this.leczenie.equals(other.leczenie))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Wizyta[id=" + id + "]";
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        changeSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        changeSupport.removePropertyChangeListener(listener);
    }
}

META-INF/orm.xml

Oznaczam pole changeSupport jako transient, ustawiam atrybuty metadata-complete oraz access w orm.xml w projekcie przychodnia-generatortabel:

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
  version="1.0">
    <description>Kartoteka</description>
    <package>pl.jaceklaskowski.przychodnia.model</package>
    <entity class="Pacjent" name="Pacjent" metadata-complete="true" access="PROPERTY">
        <description>Encja Pacjent</description>
        <named-query name="znajdzPacjentaPoPeselu">
            <query>SELECT p FROM Pacjent p WHERE p.pesel LIKE :pesel</query>
        </named-query>
        <attributes>
            <id name="pesel" />
            <one-to-many name="wizyty" mapped-by="pacjent">
                <cascade>
                    <cascade-all />
                </cascade>
            </one-to-many>
            <transient name="changeSupport" />
        </attributes>
    </entity>
    <entity class="Wizyta" name="Wizyta" metadata-complete="true" access="PROPERTY">
        <description>Encja Wizyta</description>
        <attributes>
            <id name="id">
                <generated-value generator="AUTO"/>
            </id>
            <basic name="dzien">
                <temporal>DATE</temporal>
            </basic>
            <many-to-one name="pacjent" />
            <transient name="changeSupport" />
        </attributes>
    </entity>
</entity-mappings>

i sprawdzam działanie aplikacji przychodnia-generatortabel.

Pierwsze uruchomienie - wyjątek Cannot determine the type (class) of the property attribute [changeSupport]

Niestety uruchomienie kończyło się błędem:

Exception in thread "main" javax.persistence.PersistenceException: No Persistence provider for EntityManager named przychodnia-derbyPU:
Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory:
oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
Local Exception Stack: 
Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): 
oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader@17182c1
Internal Exception: javax.persistence.PersistenceException: 
Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
Exception Description: predeploy for PersistenceUnit [przychodnia-derbyPU] failed.
Internal Exception: Exception [TOPLINK-7216] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): 
oracle.toplink.essentials.exceptions.ValidationException
Exception Description: Cannot determine the type (class) of the property attribute [changeSupport] in entity class [class pl.jaceklaskowski.przychodnia.model.Pacjent]. 
Ensure there is corresponding get method for that property name on the entity class.
 at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
 at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
 at pl.jaceklaskowski.przychodnia.GeneratorTabel.persist(GeneratorTabel.java:15)
 at pl.jaceklaskowski.przychodnia.GeneratorTabel.main(GeneratorTabel.java:11)
Caused by: javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): 
oracle.toplink.essentials.exceptions.EntityManagerSetupException
Exception Description: predeploy for PersistenceUnit [przychodnia-derbyPU] failed.
Internal Exception: Exception [TOPLINK-7216] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
Exception Description: Cannot determine the type (class) of the property attribute [changeSupport] in entity class [class pl.jaceklaskowski.przychodnia.model.Pacjent]. 
Ensure there is corresponding get method for that property name on the entity class.
 at oracle.toplink.essentials.internal.ejb.cmp3.EntityManagerSetupImpl.predeploy(EntityManagerSetupImpl.java:643)
 at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.callPredeploy(JavaSECMPInitializer.java:171)
 at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.initPersistenceUnits(JavaSECMPInitializer.java:239)
 at oracle.toplink.essentials.internal.ejb.cmp3.JavaSECMPInitializer.initialize(JavaSECMPInitializer.java:255)
 at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:155)
 ... 4 more
Caused by: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
Exception Description: predeploy for PersistenceUnit [przychodnia-derbyPU] failed.
Internal Exception: Exception [TOPLINK-7216] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
Exception Description: Cannot determine the type (class) of the property attribute [changeSupport] in entity class [class pl.jaceklaskowski.przychodnia.model.Pacjent].
Ensure there is corresponding get method for that property name on the entity class.
 at oracle.toplink.essentials.exceptions.EntityManagerSetupException.predeployFailed(EntityManagerSetupException.java:228)
 ... 9 more
Caused by: Exception [TOPLINK-7216] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
Exception Description: Cannot determine the type (class) of the property attribute [changeSupport] in entity class [class pl.jaceklaskowski.przychodnia.model.Pacjent]. 
Ensure there is corresponding get method for that property name on the entity class.
 at oracle.toplink.essentials.exceptions.ValidationException.unableToDetermineClassForProperty(ValidationException.java:1745)
 at oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataValidator.throwUnableToDetermineClassForProperty(MetadataValidator.java:401)
 at oracle.toplink.essentials.internal.ejb.cmp3.xml.accessors.XMLClassAccessor.buildAccessor(XMLClassAccessor.java:135)
 at oracle.toplink.essentials.internal.ejb.cmp3.xml.accessors.XMLClassAccessor.processAccessors(XMLClassAccessor.java:371)
 at oracle.toplink.essentials.internal.ejb.cmp3.metadata.accessors.ClassAccessor.process(ClassAccessor.java:498)
 at oracle.toplink.essentials.internal.ejb.cmp3.xml.accessors.XMLClassAccessor.process(XMLClassAccessor.java:322)
 at oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataProcessor.processMappingFile(MetadataProcessor.java:568)
 at oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataProcessor.processMappingFiles(MetadataProcessor.java:533)
 at oracle.toplink.essentials.ejb.cmp3.persistence.PersistenceUnitProcessor.processORMetadata(PersistenceUnitProcessor.java:368)
 at oracle.toplink.essentials.internal.ejb.cmp3.EntityManagerSetupImpl.predeploy(EntityManagerSetupImpl.java:607)
 ... 8 more

 The following providers:
oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider
Returned null to createEntityManagerFactory.

 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:154)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
 at pl.jaceklaskowski.przychodnia.GeneratorTabel.persist(GeneratorTabel.java:15)
 at pl.jaceklaskowski.przychodnia.GeneratorTabel.main(GeneratorTabel.java:11)

Nie mogąc namierzyć błędu konfiguracji po mojej stronie, a mając na uwadze, że używana wersja Oracle TopLink Essentials to 2.0.1 (Build b09d-fcs (12/06/2007)) postanowiłem uaktualnić TopLinka (gdybym użył projektu opartego o Apache Maven 2 aktualizacja sprowadziłaby się do zmiany w pom.xml na wymaganą wersję).

Konfiguracja biblioteki TopLinkEssenaitls-v2.1b33

Domyślna konfiguracja biblioteki TopLink Essentials składa się z dwóch jarów:

Grafika:library-manager-toplink.gif

Pobieram ostatnią dostępną wersję TopLink Essentials V2.1 b33 (sekcja Promoted binary builds) ze strony Implementation of Java Persistence API Downloads, instaluję poleceniem java -jar glassfish-persistence-installer-v2.1-b33.jar.

jlaskowski@work /cygdrive/c/apps
$ java -jar glassfish-persistence-installer-v2.1-b33.jar
glassfish-persistence
glassfish-persistence\README
glassfish-persistence\3RD-PARTY-LICENSE.txt
glassfish-persistence\LICENSE.txt
glassfish-persistence\toplink-essentials-agent.jar
glassfish-persistence\toplink-essentials.jar
installation complete

, a następnie definiuję bibliotekę TopLinkEssenaitls-v2.1b33.

Grafika:library-manager-toplink-v21b33.gif

Ustawiam bibliotekę TopLinkEssenaitls-v2.1b33 w projekcie przychodnia-generatortabel (Properties > Libraries).

Grafika:przychodnia-generatortabel-libraries.gif

i ponownie uruchamiam projekt przychodnia-generatortabel.

Nadal pojawia się wspomniany wyjątek - Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.1 (Build b33-fcs (05/14/2008)). Wydaje się, że użycie pól transient może być głównym winowajcą - Toplink Exception (TOPLINK-28018) - deploy time - B36 oraz NPE from MetadataAccessor.getReferenceClassName when deploying an ear containing persistence units.

Powrót do TopLink Essentials z NetBeans IDE

Wracam do wersji TopLink Essentials, która dystrybuowana jest z NetBeans IDE 6.1 i do pola changeSupport klas pl.jaceklaskowski.przychodnia.model.Pacjent oraz pl.jaceklaskowski.przychodnia.model.Wizyta dodaję adnotację @Transient. Oczywiście dodanie adnotacji @Transient wymagało dodania biblioteki TopLink Essentials jako zależności projektu przychodnia-model. Tym samym model nie jest już "technologicznie czysty". Niedobrze.

Ciekawostką klasy pl.jaceklaskowski.przychodnia.Wizyta utworzonej przez asystenta Java Desktop Application jest wykorzystanie adnotacji, której wcześniej nie miałem okazji użyć:

@JoinColumn(name = "PACJENT_PESEL", referencedColumnName = "PESEL")
@ManyToOne
private Pacjent pacjentPesel;

podczas gdy po stronie wiodącej relacji jeden-do-wielu - w klasie pl.jaceklaskowski.przychodnia.Pacjent mamy taką oto konstrukcję:

@OneToMany(mappedBy = "pacjentPesel")
private Collection<Wizyta> wizytaCollection;

Moja konfiguracja mapowania JPA - META-INF/orm.xml - bazuje na domyślnych wartościach pól w tabeli i owe @JoinColumn nie jest potrzebne, gdyż tak na prawdę nic nie wnosi w tym konkretnym przypadku.

Migracja do pakietu pl.jaceklaskowski.przychodnia.model

Przechodzę do projektu przychodnia. Jako, że większość kodu jest generowana i zabiezpieczona przed edycją niemożliwe jest ręczna zmiana pakietów klas Pacjent oraz Wizyta, które w projekcie przychodnia należą do pakietu pl.jaceklaskowski.przychodnia, podczas gdy w przychodnia-model do pl.jaceklaskowski.przychodnia.model. Ich miejsce zajmą klasy z projektu przychodnia-model, który staje się biblioteką zależną projektu przychodnia. Zmieniły się pakiety klas Pacjent i Wizyta, więc trochę ręcznej modyfikacji jest wymagane. Wystarczy usunąć pakiety z pełnych nazw klas Pacjent oraz Wizyta i skorzystać z Ctrl+Shift+I (Fix Imports).

Okazuje się, że i takie podejście nie jest możliwe, gdyż podczas przeniesienia do innego pakietu pojawia się błąd:

Grafika:isnotafileondisk.gif

Zgłosiłem jako Issue 135277: New - {0} is not a file on disk during Refactoring of Java Desktop Application.

Nie widzę innego wyjścia jak edycja klasy pl.jaceklaskowski.przychodnia.PrzychodniaView poza środowiskiem NetBeans IDE 6.1. Owa klasa składa się z dwóch plików.

Grafika:przychodniaview-allfiles.gif

Ale moment! Analizując pliki, jakimś cudem natrafiłem na widok Inspector, który pojawia się podczas edycji klasy PrzychodniaView. Domyślnie uruchamia się w lewym dolnym rogu środowiska NetBeans.

Grafika:inspector.gif

W nim zaznaczam list i po prawej stronie NetBeans wyświetlają się właściwości zmiennej, gdzie w zakładce Code istnieje możliwość zmiany typu - Type Parameters.

Grafika:list-properties.gif

Oczywiście zmiana wymaga, aby wcześniej podpiąć projekt przychodnia-model jako zależność projektu przychodnia, aby klasy pl.jaceklaskowski.przychodnia.model.Pacjent i pl.jaceklaskowski.przychodnia.model.Wizyta były w ogóle dostępne. Kasuję klasę pl.jaceklaskowski.przychodnia.Pacjent. Konieczna jest edycja klasy PrzychodniaView w trybie Source, tak aby "zdjąć" pełne typowanie klas Pacjent i Wizyta (usuwamy pakiet pl.jaceklaskowski.przychodnia) i Ctrl+Shift+I (Fix Imports). Kilka dodatkowych zmian związanych z typami, co pozostawiam jako zadanie domowe i koniec zmian.

Migracja do konfiguracji JPA z projektu przychodnia-generatortabel

F6 (zakładając, że projektem głównym jest przychodnia) i...

2008-05-18 15:54:46 org.jdesktop.application.Application$1 run
SEVERE: Application class pl.jaceklaskowski.przychodnia.Przychodnia failed to launch
javax.persistence.PersistenceException: No Persistence provider for EntityManager named przychodnia-derby;create=truePU: 
Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory: 
oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
Local Exception Stack: 
Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader@17182c1
Internal Exception: javax.persistence.PersistenceException: 
Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
Exception Description: predeploy for PersistenceUnit [przychodnia-derby;create=truePU] failed.
Internal Exception: Exception [TOPLINK-30007] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))):
oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
Exception Description: An exception was thrown while loading class: 
pl.jaceklaskowski.przychodnia.Pacjent to check whether it implements @Entity, @Embeddable, or @MappedSuperclass.
Internal Exception: java.lang.ClassNotFoundException: pl.jaceklaskowski.przychodnia.Pacjent
 at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
 at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
 at pl.jaceklaskowski.przychodnia.PrzychodniaView.initComponents(PrzychodniaView.java:338)
 at pl.jaceklaskowski.przychodnia.PrzychodniaView.<init>(PrzychodniaView.java:41)
 at pl.jaceklaskowski.przychodnia.Przychodnia.startup(Przychodnia.java:19)
 at org.jdesktop.application.Application$1.run(Application.java:171)
 at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
 at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
 at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273)
 at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183)
 at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160)
 at java.awt.EventDispatchThread.run(EventDispatchThread.java:121)

No tak! Kompletnie o tym zapomniałem. Przecież mapowanie odbywa się poprzez plik META-INF/orm.xml (zamiast adnotacji), więc kasuję persistence.xml w projekcie przychodnia i dodaję przychodnia-generatortabel jako zależność projektu przychodnia (skąd pobrane zostaną pliki META-INF/persistence.xml oraz META-INF/orm.xml).

Grafika:przychodnia-properties-compile.gif

Ponownie F6 i...ten sam wyjątek! Powiedziałbym, że niemożliwe, ale się dzieje! Okazuje się, że samo F6 nie przebudowuje projektu, więc konieczne jest Clean and Build, a po nim dopiero F6 (jakieś pomysły jak to rozwiązać elegancko?).

Kolejne uruchomienie: No Persistence provider for EntityManager named przychodnia-derby;create=truePU

Tym razem uruchomienie aplikacji kończy się błędem

2008-05-18 16:02:38 org.jdesktop.application.Application$1 run
SEVERE: Application class pl.jaceklaskowski.przychodnia.Przychodnia failed to launch
javax.persistence.PersistenceException: No Persistence provider for EntityManager named przychodnia-derby;create=truePU:  No META-INF/persistence.xml was found in classpath.
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:154)
 at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
 at pl.jaceklaskowski.przychodnia.PrzychodniaView.initComponents(PrzychodniaView.java:338)
 at pl.jaceklaskowski.przychodnia.PrzychodniaView.<init>(PrzychodniaView.java:41)
 at pl.jaceklaskowski.przychodnia.Przychodnia.startup(Przychodnia.java:19)
 at org.jdesktop.application.Application$1.run(Application.java:171)
 at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
 at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
 at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273)
 at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183)
 at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160)
 at java.awt.EventDispatchThread.run(EventDispatchThread.java:121)

a to dlatego, że aplikacja korzysta z jednostki trwałej (ang. persistence unit) o nazwie przychodnia-derby;create=truePU, a nasza jednostka to przychodnia-derbyPU (konfiguracja znajduje się w pliku META-INF/persistence.xml). To jest akurat proste do poprawki, bo w trakcie edycji PrzychodniaView już natrafiłem na tą nazwę i spodziewałem się tego błędu.

Otwieram PrzychodniaView w trybie Design, gdzie w widoku Inspector (lewy dolny róg) zaznaczam entityManager [EntityManager], a następnie w widoku Properties (po prawej stronie) w zakładce Properties ustawiam persistenceUnit na przychodnia-derbyPU. Zmiana jest możliwa przez wciśnięcie przycisku ... (po prawej pola persistenceUnit) i w oknie dialogowym entityManager [EntityManager] - persistenceUnit wybieram Plain text i wpisuję potrzebną wartość - przychodnia-derbyPU.

Grafika:entityManager-persistenceUnit.gif

Uruchomienie - "AWT-EventQueue-0" java.lang.ClassCastException

Ponownie F6 i działa, ale przy wciśnięciu New pojawia się wyjątek:

Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException
 at java.lang.Class.cast(Class.java:2990)
 at org.jdesktop.beansbinding.Binding.convertForward(Binding.java:1312)
 at org.jdesktop.beansbinding.Binding.getSourceValueForTarget(Binding.java:844)
 at org.jdesktop.swingbinding.impl.ListBindingManager.valueAt(ListBindingManager.java:104)
 at org.jdesktop.swingbinding.JTableBinding$BindingTableModel.getValueAt(JTableBinding.java:713)
 at javax.swing.JTable.getValueAt(JTable.java:2638)
 at javax.swing.JTable.prepareRenderer(JTable.java:5652)
 at javax.swing.plaf.basic.BasicTableUI.paintCell(BasicTableUI.java:2072)
 at javax.swing.plaf.basic.BasicTableUI.paintCells(BasicTableUI.java:1974)
 at javax.swing.plaf.basic.BasicTableUI.paint(BasicTableUI.java:1770)
 at javax.swing.plaf.ComponentUI.update(ComponentUI.java:143)
 at javax.swing.JComponent.paintComponent(JComponent.java:763)
 at javax.swing.JComponent.paint(JComponent.java:1027)
 at javax.swing.JComponent.paintToOffscreen(JComponent.java:5122)
 at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:285)
 at javax.swing.RepaintManager.paint(RepaintManager.java:1128)
 at javax.swing.JComponent._paintImmediately(JComponent.java:5070)
 at javax.swing.JComponent.paintImmediately(JComponent.java:4880)
 at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:723)
 at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:679)
 at javax.swing.RepaintManager.seqPaintDirtyRegions(RepaintManager.java:659)
 at javax.swing.SystemEventQueueUtilities$ComponentWorkRequest.run(SystemEventQueueUtilities.java:128)
 at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
 at java.awt.EventQueue.dispatchEvent(EventQueue.java:597)
 at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273)
 at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183)
 at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160)
 at java.awt.EventDispatchThread.run(EventDispatchThread.java:121)

To jest z powodu zmian w typach między wygenerowaną encją Pacjent (którą właśnie skasowałem), a moją wersją Pacjenta. W utworzonym typem pola plec jest typu Short, podczas gdy w mojej wersji pole ma typ boolean (kobieta/mężczyzna, który docelowo powinien być bardziej naturalnym typem wyliczeniowym). Kiedy przyjrzałem się wygenerowanemu kodowi PrzychodniaView znalazłem wskazanie na typy kolumn i dla Plec mamy:

columnBinding.setColumnClass(Short.class);

Odszukuję miejsca, gdzie wskażę na typ Boolean (ostatecznie mógłbym zmienić typ atrybutu encji, ale uparłem się, aby zmienić utworzony kod a nie swój). Trochę mi zajęło zanim dotarłem do tego właściwego miejsca. Jak w podchodach, gdzie po śladach trzeba było dotrzeć do wyznaczonego miejsca.

Kiedy edytowałem PrzychodniaView w górnej części edytora (tryb Design) znajduje się miejsce, z pomocniczymi komunikatami i jednym z nich był (zmiana komunikatów następuje przy każdorazowym wciśnięciu lewego klawisza myszki):

Grafika:przychodniaview-design.gif

Teraz wystarczy zaznaczyć masterTable w widoku Inspector (dostępny podczas edycji PrzychodniaView w trybie Design) i w oknie Properties w zakładce Properties dostępny jest wiersz model. Na pierwszy rzut oka nie widać, aby cokolwiek było w tym polu ustawione, więc zmylić może owa pusta w polu jako potencjalne oznaczenie braku jakiejkolwiek wartości.

Grafika:masterTable-properties.gif

Nic bardziej mylnego! Wystarczy, więc wcisnąć magiczny przycisk ..., aby dostać się do edytora typów poszczególnych kolumn. To jest dokładnie to miejsce, którego potrzebowałem! Przy okazji można zmienić kolejność wyświetlania kolumn.

Grafika:masterTable-model-boolean.gif

I ponownie F6. Znowu wyjątek CCE (!)

Wracam do edycji PrzychodniaView i sprawdzam inne pola w tabeli (odpowiadającej encji Pacjent) i tabeli szczegółów (Wizyta). Jakkolwiek nie istnieje atrybut model dla detailTable, to istnieje elements. Okazuje się, że w nim jest ustawiony zły Binding Expression. Zmieniam na poprawny - ${selectedElement.wizyty}.

Grafika:detailTable-elements-wizyty.gif

Koniecznie należy usunąć changeSupport, id oraz pacjent z tabeli Selected. Pierwszy changeSupport nie zawiera danych do edycji, drugi id jest generowany automatycznie przez JPA podczas zapisu do bazy danych, a pacjent jest polem wiązania między encjami i również nie jest przeznaczony do edycji przez użytkownika aplikacji.

Grafika:detailTable-elements-wizyty-selected.gif

Ponownie F6 i ponownie CCE!

Śledzenie wykonania aplikacji - "namierzanie" CCE

Zdesperowany ustawiam punkt zatrzymania (ang. break point) na java.lang.Class.cast(Class.java:2990).

W trakcie uruchomienie Debuggera pojawił się komunikat, który uprościł mi odszukiwanie miejsca, gdzie możnaby włączyć punkt zatrzymania, który właśnie zdefiniowałem.

Not able to submit breakpoint LineBreakpoint Class.java : 2990, reason: Breakpoint belongs to disabled source root 'C:\apps\java6\src.zip'. See Window/Debugging/Sources.

Wiele z nich jest wyłączonych, gdyż skomplikowałyby śledzenie wykonywania programu. Trzeba je włączyć explicite.

W Window > Debugging > Sources włączam śledzenie klas z Java SE 6.

Grafika:sources.gif

Wciskam przycisk New w aplikacji i tym sposobem przechwytuję miejsce zgłoszenia wyjątku. Instancja, która powoduje wyjątek jest typu Short, a parametr do rzutowania jest typu boolean. Wniosek jest prosty: gdzieś jeszcze zaszyto typ atrybutu plec jako Short.

Finalne uruchomienie

Przez dłuższą chwilę nie mogłem znaleźć miejsca, gdzie wciąż tkwił Short zamiast boolean, aż przypomniałem sobie o konieczności przebudowywania projektu przed uruchomieniem. Tak było i tym razem. Okazuje się, że niektóre ze zmian nie są zauważane przez NetBeans jako wymuszające przebudowywanie projektu i Run bazuje na starych plikach, więc owa zmiana Short na Boolean nie została uruchomiona. Clean and Build, Run i aplikacja działa z kolumną Plec wyrysowywaną jako pole typu checkbox!

Grafika:aplikacja.gif

Jest jeszcze kilka zmian, jakie należy wdrożyć w aplikacji, jak np. oparcie się o typ wyliczeniowy w encji i wyrysowywanie pola Plec jako listy mężczyzna/kobieta, usunięcie zera przy braku wartości w kolumnie Wiek, wyrównanie wartości w kolumnach i uzupełnianie pola Plec i Wiek na podstawie peselu, polskie tłumaczenie aplikacji oraz kolumna Dzien mogłaby być wypełniana z kontrolki ala Kalendarz. Może znajdą się chętni, którzy chcieliby popróbować się z NetBeans IDE 6.1, jego projektem typu Java Desktop Application oraz JPA? Zapraszam do kontaktu na jacek@laskowski.net.pl.

Kompletny projekt aplikacji dostępny jest do pobrania jako netbeans-przychodnia-czesc2.zip. Po imporcie może być konieczne zbudowanie projektów (Clean and Build menu), gdyż NetBeans czasami oznacza projekty jako błędne ze względu na brak wymaganych zależności projektowych.

Osobiste