Xis’ blog - Mój wirtualny schowek
Polska Społeczność Symfony

NetBeans i ujednolicanie JDialog

Miałem niedawno okazję dłużej popracować nad projektami Javy w NetBeans 6.5 (wcześniej poważniej NetBeans używałem tylko do Grails). Środowisko to nie było dotychczas moim ulubionym, głównie ze względu na wydajność, co do której można mieć spore zastrzerzenia, jednak zauważyłem, że ma ono również mnóstwo zalet. Pierwszą jaką dostrzegłem jest to, że projekty generowane przez to IDE są naprawdę ładnie utworzone, wykorzystują standardowe narzędzia Javy, takie jak np. Ant i nie uzależniają użytkownika od używania „jedynego i właściwego” IDE. NetBeans może zauroczyć również stopniem dopracowania swoich elementów – jak już coś wspiera, to wspiera naprawdę dobrze. Przykładem tu może być sposób, w jaki można za pomocą tego środowiska utworzyć i zarządzać aplkacjami wykorzystującymi Web Services w aplikacji – zarówno klientem, jak i usługą wykorzystującącymi API JAX-WS. Kolejny (lecz na pewno nie ostatni) plus należy się NetBeans za właściwie bezkonkurencyjny edytor wizualny aplikacji wykorzystujących Swing.

I o Swingu właśnie będzie ten wpis (nieco przydługi, ostrzegam).

Nie do końca podoba mi się domyślna filozofia okien dialogowych w Javie. Np. rozmiar – niby można zmieniać rozmiar „świeżo” utworzonego okienka dialogowego rozciągając je, ale menadżery okien nie pozwalają na jego maksymalizację (brakuje bowiem przycisku maksymalizacji). Wiem, że takie jest założenie okienek dialogowych, ale wyczuwam tu pewną niekonsekwencję. Ponadto „goły” JDialog jest praktycznie pozbawiony funkcjonalności, np. brakuje mu domyślnych zachowań takich jak potwierdzanie i anulowanie, reakcję na przycisk Escape i Enter – te braki sprawiają, że każda implementacja okienka dialogowego musi być poprzedzona żmudnym wykonywaniem tych banalnych czynności.

Postanowiłem więc wykonać coś na wzór bazy dla okna dialogowego, która to baza będzie wyposażona we wszystkie cechy, które wg mnie posiadać powinna każda implementacja JDialog. W takiej bazie będę umieszczał różne komponenty – w zależności od tego, jakiego dialogu potrzebuję.

szkic

Chcę uzyskać bazę dla dialogu, w której będę umieszczał przeróżne panele – począwszy od prostych, informacyjnych (wtedy taki dialog przypominałby funkconalnością JOptionPane), skończywszy na całkiem zaawansowanych funkcjonalnie formularzach edycji wykorzystujących panele, które przygotuję sobie osobno (dzięki temu będę mógł wykorzystać panele także w innych sytuacjach). Wszystkie jednak dialogi cechować się mają wspólnymi właściwościami: dialog można zatwierdzić (submit), albo odrzucić (cancel). Wciśnięcie Escape na aktywnym oknie powoduje jego zamknięcie (funkcjonalność identyczna do wciśnięcia przycisku Anuluj), a Enter – zatwierdzenie (kliknięcie OK).

W tym celu zbudowałem JDialog w NetBeans wybierając z menu projektu New->JDialog Form i nazwałem go DialogBase. W edytorze wizualnym umieściłem dwa przyciski (nazwałem je jBtnOK i jBtnCancel), oraz jeden JPanel (jPanelMain), z ustawionym layoutem BorderLayout – w tym panelu będę umieszczał przeróżne panele „implementacyjne” – w zależności od przeznaczenia mojego dialogu.

Na początek, domyślny konstruktor i obsługa własności panel (czyli obiekt panelu, który umieszczamy w oknie):

    private JPanel panel;
 
    public DialogBase(JPanel panel) {
        this( null, true );
        setLocationRelativeTo(null);
        setPanel(panel);
    }
 
    protected void setPanel(JPanel panel) {
        this.panel = panel;
        jPanelMain.removeAll();
        jPanelMain.add( panel, BorderLayout.CENTER );
        panel.setVisible( true );
        Dimension d = new Dimension(panel.getPreferredSize());
        this.setSize( d.width, d.height+70 );
    }
 
    protected JPanel getPanel() {
        return panel;
    }

Metoda setPanel() ustawia własność panel oraz zajmuje się osadzeniem go w oknie dialogu i ustawieniem odpowiedniego rozmiaru okna (wysokość zwiększam o wielkość przycisków na dole okna).

Aby wiedzieć w jaki sposób moje okno dialogowe zostało zamknięte, wyposażam je we własność exitStatus:

    public static final int DLG_EXIT_CANCEL = 0;
    public static final int DLG_EXIT_OK = 1;
 
    private int exitStatus = DLG_EXIT_CANCEL;
 
    // getter i setter

Domyślne zamknięcie okna poskutkuje zapamiętaniem statusu „Cancel”.

Czas na obsługę klawisza Escape i Enter. Posiłkując się ciekawym na ten temat artykułem, wzbogaciłem mój DialogBase o następujący kod:

    @Override
    protected JRootPane createRootPane() {
        JRootPane theRootPane = new JRootPane();
 
        KeyStroke escStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 );
        theRootPane.registerKeyboardAction( new ActionListener() {
            public void actionPerformed( ActionEvent actionEvent ) {
                doCancel();
            }
        }, escStroke, JComponent.WHEN_IN_FOCUSED_WINDOW );
 
        KeyStroke entStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 );
        theRootPane.registerKeyboardAction( new ActionListener() {
            public void actionPerformed( ActionEvent actionEvent ) {
                doSubmit();
            }
        }, entStroke, JComponent.WHEN_IN_FOCUSED_WINDOW );
 
        return theRootPane;
    }

Jak widzisz, wykorzystuję tu tajemnicze metody doSubmit() i doCancel(). Co to za metody?

    private void doSubmit() {
        if( onSubmit() ) {
            setExitStatus( DLG_EXIT_OK );
            dispose();
        }
    }
 
    private void doCancel() {
        if( onCancel() ) {
            setExitStatus( DLG_EXIT_CANCEL );
            dispose();
        }
    }
 
    protected boolean onSubmit() {
        return true;
    }
 
    protected boolean onCancel() {
        return true;
    }

Obie metody działają dość podobnie: jeśli metoda onSubmit() (lub analogicznie onCancel()) zwróci true, ustawiamy odpowiedni exitStatus i zamykamy okno. Zarówno doSubmit() jak i doCancel() wiążę też za pomocą zdarzenia ActionEvent z przyciskami (wystarczy dwukrotnie kliknąć na przycisku w edytorze wizualnym i wyedytować wygenerowaną przez NetBeans pustą metodę):

    private void jBtnCancelActionPerformed(java.awt.event.ActionEvent evt) {
        doCancel();
    }
 
    private void jBtnOKActionPerformed(java.awt.event.ActionEvent evt) {
        doSubmit();
    }

OK, ale po co nam te trywialne metody onSubmit() i onCancel()? Wykorzystamy je np. do walidacji w przyszłych implementacjach naszych okien dialogowych (jeśli zdecydujemy się na ich przesłonięcie, jeśli nie – niczego złego nie zrobią ;) ).

Wygląda na to, że nasza baza jest gotowa. Użyjmy jej więc na jakimś przykładzie – np. formularzu logowania.

W NetBeans, tworzymy nowy JPanel Form, nazywamy go PanelLogin i dodajemy odpowiednie elementy interfejsu użytkownika (jak na załączonym wyżej szkicu), oczywiście to ten panel będziemy osadzać w bazie dialogu, więc nie dodajemy już przycisków OK i Anuluj, bo nie musimy. Element JTextField, zawierający nazwę użytkownika nazywamy jTxtUsername, a JPasswordField (z hasłem) – jTxtPassword.
Dodajemy dwie publiczne metody do pobrania wprowadzonych nazwy użytkownika i hasła:

    public String getUsername() {
        return jTxtUsername.getText();
    }
 
    public String getPassword() {
        return new String(jTxtPassword.getPassword());
    }

A teraz nasz dialog logowania.
W NetBeans tworzę nową klasę Javy (New->Java Class), a nie dialog(!) i nazywam ją DialogLogin. Nasza nowa klasa musi dziedziczyć po DialogBase. Ponadto, nasz dialog powinien zwracać obiekt użytkownika (jakiejś klasy np. User, której szczegóły nie są tu istotne) jeśli logowanie się powiedzie. W tym celu powołujemy własność returnValue (wraz z getterem i setterem). Przy okazji dodaję też licznik prób logowania – proste umożliwienie trzykrotnej pomyłki przy logowaniu.

public class DialogLogin extends DialogBase {
    private User returnValue;
    private int tries = 3;
 
    public DialogLogin() {
        super(new PanelLogin());
    }
}

Czas na obsługę logiki biznesowej naszego dialogu. Anulowanie dialogu zostawiamy bez zmian – po prostu zamykamy okno. Jednak musimy zaimplementować obsługę zatwierdzania formularza. W tym celu przesłaniamy naszą metodę onSubmit():

    @Override
    protected boolean onSubmit() {
        PanelLogin thePanel = ( PanelLogin ) this.getPanel();
 
        User u = App.login( thePanel.getUsername(), thePanel.getPassword() );
        if( u == null ) {
            JOptionPane.showMessageDialog( this, "Nie udało się zalogować");
            return (--tries<=0);
        }
 
        returnValue = u;
        return true;
    }

Jeśli App.login() (przyjmijmy, że ta metoda odpowiada za autoryzację aplikacji) zwróci null, to znaczy, ze nie zalogowaliśmy się do aplikacji – dialog nie zamknie się, ale pokaże nam info o nieprawidłowych parametrach logowania (i tak do momentu aż wartość tries osiągnie wartość mniejszą lub równą zeru). Jeśli użytkownik jest prawidłowy, dialog zamknie się ustawiwszy uprzednio własność returnValue.

Powinno działać. Sprawdźmy:

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
        }
        catch( Exception ex ) {}
 
        DlgLogin dlg = new DlgLogin();
        dlg.setVisible( true );
        System.err.println("Logowanie zakończone: "+dlg.getExitStatus());
        if( dlg.getExitStatus() == DialogBase.DLG_EXIT_OK ) {
            if( dlg.getReturnValue() != null ) {
                System.err.println("W porządku, użytkownik prawidłowy");
            }
            else {
                System.err.println("Brak dostępu!");
            }
        }
        else {
            System.err.println("Anulowano!");
        }
    }

Voila!

We wpisie użyłem ikon Free Business Icons opublikowanych na licencji Creative Commons oraz Silk na licencji LGPL.

Tagi: , , ,

Komentarze (4)

Zrób sobie przerwę

Dość intensywnie ostatnio pracuję (dużo pracy, a do tego taka ilość ciekawych rzeczy dookoła, że nie sposób nie poświęcić na ich badanie wieczora lub dwóch), do pracy tej używam rzecz jasna komputera. Odbiło się to niestety negatywnie na moim samopoczuciu, możnaby rzec, ze dopadło mnie coś na wzór RSI, tyle że dotyczyło wzroku i bólów głowy. Już tak mam, że kiedy „wkręcę” się w jakąś czynność, coś mnie zainteresuje, potrafię pracować bez przerw i kompletnie nie kontroluję tego ile czasu zajmuje mi praca. Skupiam się wtedy na wykonywanej czynności maksymalnie, zapominając o świecie poza nią. Postanowiłem więc coś w tej sprawie zrobić i – oprócz zdroworozsądkowego odstawienia na jakiś czas komputera – znaleźć narzędzie, które pozwoli mi nieco zadbać o swoje zdrowie w pracy z nim. Znalazłem dwa programy, ktore umożliwiają kontrolę nad przerwami.

Pierwszy z nich to EyesRelax. Programik ten umożliwia ustawienie odstępu czasowego między przerwami i – uruchomiony w tle – odlicza nam czas na do odpoczynku, gdy ten nadejdzie, EyesRelax zasłania ekran komputera białym tłem. Ciekawą jego funkcją jest Parent Mode, która pozwala na całkowite zablokowanie komputera na czas przerwy (odblokowanie może nastąpić dopiero po podaniu hasła). Programik ekstremalnie prosty, ale spełniający dokładnie swoje zadanie. Niestety jednak, nie do końca, bowiem jedna jego wada spowodowała, że zacząłem rozglądać się za innym rozwiązaniem. EyesRelax ma jakiś błąd, która sprawia, że białe plansze nie zawsze zasłaniają pulpit, często samoczynnie wędrują sobie w tło i – jeśli używa się zmaksymalizowanego okna jakiegoś programu – użytkownik nie ma pojęcia, że właśnie w tej chwili powinien zrobić przerwę. Ja niestety potrzebuję narzędzia, które – nawet brutalnie – wyrwie mnie z amoku programowania i każe odpocząć :) Dlatego zacząłem szukać dalej.

Znalazłem WorkRave. To narzędzie bardzo pozytywnie zaskoczyło mnie ilością możliwości konfiguracji, oraz samą koncepcją walki z RSI. Program ten oferuje trzy sposoby zabezpieczenia przed przepracowaniem – i wszystkie można stosować naraz:

  • Mikroprzerwy – krótkie odpoczynki pozwalające „odetchnąć”, przede wszytskim oczom i szyi,
  • Odpoczynki – dłuższe przerwy na np. spacer, śniadanie, ćwiczenia, albo po prostu zrobienie sobie herbaty,
  • Kontrola całkowitego czasu pracy – skutecznie powstrzymuje przed niechcianymi nadgodzinami.

Poza samym odliczaniem czasu, WorkRave mile zaskakuje możliwością dodania wstępnych „ostrzeżeń” o tym, że zbliża się przerwa, a także biblioteką porad dotyczących ćwiczeń (akomodacji oczu, mięśni szyi, rąk itd.) które możemy robić w trakcie odpoczynku – ilość proponowanych ćwiczeń jest całkiem spora, a same ich opisy dość szczegółowe (z obrazkami ;) ). Ponadto warto zauważyć, że program można przełączać w różne tryby pracy (Normalny, Cichy i Zawieszony), dzięki czemu nie jest on nachalny np. gdy oglądamy na komputerze film. Narzędzie to posiada też opcję pracy w sieci, ale nie udało mi się jej jeszcze przetestować.

Gorąco zachęcam do używania takich kontrolerów czasu pracy, niech najlepszym dowodem ich skuteczności będzie fakt, że skończyły się moje problemy z oczami i bólami głowy, ba – kiedy dokładnie stosuję się do zaleceń WorkRave, dzień w pracy mija mi tak, że wracam do domu w ogóle nie będąc zmęczonym!

Tagi: , ,

Komentarze (2)

Grails – DataTable z filtrem i pagerem

Mojej przygody z Grails ciąg dalszy. Zauważyłem, że framework ten posiada bardzo wiele fajnych wtyczek. Jedną z nich jest grails-ui, który korzystając z dobrodziejstw Yahoo! User Interface Library pozwala na wstawianie ciekawych komponentów na stronę za pomocą zaledwie jednego znacznika GSP. Jednym z takich komponentów jest DataTable, bazujący na yui:dataTable, pozwalający np. na pobieranie danych za pomocą żądań asynchronicznych AJAX w formacie JSON (lub XML, wedle życzenia), dzielenie wyników na podstrony (pager), mechanizm skórek itd. Znacznik GSP gui:dataTable posiada wiele opcjonalnych atrybutów, którymi można skonfigurować naszą tabelę z danymi (ilość maksymalna wierszy na jednej podstronie, format danych, kolumny itd.). Mam jednak wrażenie, że autor wtyczki nie pomyślał o możliwości dołączenia do naszej tabeli filtrowania wyników. Owszem, w Sieci można znaleźć przykłady filtrowania dla ‚gołego’ yui:dataTable, ale większość z nich, albo nie wspiera cięcia na podstrony, albo filtruje wyniki dopiero po stronie klienta (a z serwera pobiera pełną listę danych). Dla mnie to niestety za mało.

Oto przykład na to, jak korzystając ze znacznika GSP gui:dataTable zaimplementować dataTable, który daje się połączyć z prostym filtrem wyników.

Do przedstawienia przykładu wykorzystam dane dostępne na stronie z przykładami użycia dataTable w YUI.  Załóżmy więc, że mam bazę danych restauracji (pizzerii).

d12

Użyję więc klasy dziedzinowej (ang. domain class) Restaurant o postaci:

class Restaurant {
    String title
    String address
    String city
    String state
}

Listę tę chcę mieć dostępną pod adresem http://localhost:8080/PagedDatatable/restaurant/list, czyli potrzebny mi będzie kontroler RestaurantController, a w nim metoda list. W metodzie tej, kontroler nie ma zbyt wiele roboty – po prostu serwuje statyczną stronę, toteż jej postać jest taka:

    def list = {
    }

Niezbyt skomplikowane, prawda? :) Pusta metoda sprawi, że kontroler od razu zabierze się za renderowanie wyniku, strony GSP, która wygląda tak:

<g:javascript library="yui" />
<gui:resources components="['dataTable']"/>
 
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta name="layout" content="main" />
    <title>Restaurant List</title>
  </head>
  <body>
    <div class="yui-skin-sam" style="width: 600px;">
      <label for="fltr[title]">Title:</label><input type="text" id="fltr[title]" value="">
      <label for="fltr[city]">City:</label><input type="text" id="fltr[city]" value="">
      <button id="filterButton">Filter</button><br/>
 
      <gui:dataTable
        id="myDataTable"
        controller="restaurant"
        action="listAsJSON"
        rowsPerPage="15"
        sortedBy="title"
        columnDefs="[
        [key:'title', label:'Title'],
        [key:'address', label: 'Address'],
        [key:'city', label: 'City'],
        [key:'state', label: 'State']
        ]"
        />
 
    </div>
 
    <script>
      YAHOO.util.Event.onDOMReady(function () {
 
        updateFilter  = function () {
          GRAILSUI.myDataTable.customQueryString =
            "title="+YAHOO.util.Dom.get("fltr[title]").value+"&"+
            "city="+YAHOO.util.Dom.get("fltr[city]").value;
 
          var state = GRAILSUI.myDataTable.getState();
          // gui:dataTable odwołuje się do własności sorting obiektu state,
          // podczas gdy yui oferuje jedynie własność sortedBy (czyżby bug?)
          // kopiujemy własność, by uniknąć błędu
          state.sorting = state.sortedBy;
          // reset pagera, zawsze po filtrowaniu wracamy do pierwszej strony
          state.pagination.recordOffset = 0;
          query = GRAILSUI.myDataTable.buildQueryString(state);
 
          GRAILSUI.myDataTable.getDataSource().sendRequest(query,{
            success : GRAILSUI.myDataTable.onDataReturnReplaceRows,
            failure : GRAILSUI.myDataTable.onDataReturnReplaceRows,
            scope   : GRAILSUI.myDataTable,
            argument: state
          });
        };
 
        YAHOO.util.Event.on('filterButton','click',function (e) {
          updateFilter();
        });
      });
    </script>
 
  </body>
</html>

Na początku strony, deklarujemy użycie wtyczki YUI! oraz komponentu dataTable – w ten sposób Grails zadba o to, by nasza strona została wyposażona w linki do odpowiednich skryptów i arkuszy stylów. W znaczniku używam opcjonalnego atrybutu id, jest to spowodowane faktem, że będę się odwoływał do naszej tabeli i muszę wiedzieć jak. Gdybym nie użył id, dostałbym tabelę z unikalnym, losowym identyfikatorem. Nasza tabela wywołuje asynchronicznie metodę listAsJSON kontrolera RestaurantController (o niej za chwilę). Ważne jest, żeby w zdarzeniu onDOMReady powiązać kliknięcie w przycisk Filter z funkcją updateFilter(), w której konstruujemy parametry żądania i wstawiamy je do własności customQueryString naszej tabeli. Następnie obchodzimy jeden bug(?) gui:dataTable, resetujemy pager (na wypadek, gdybyśmy załadowali wynik z mniejszą ilością podstron niż obecna) i przeładowujemy naszą tabelę.

Gotowe :)

Na koniec tylko metoda listAsJSON naszego kontrolera:

def listAsJSON = {
 
        def pagingConfig = [
            max: params.max ?: 15,
            offset: params.offset ?: 0,
            sort: params.sort ?: 'title',
            order: params.order ?: 'asc'
        ]
 
        def resultFilter = {
            or {
                if(params.title && params.title != ''){
                    like("title", "${params.title}%")
                }
                if(params.city && params.city != ''){
                    like("city", "${params.city}%")
                }
            }
        }
 
        def c = Restaurant.createCriteria()
        def list = c.list(pagingConfig, resultFilter)
        def totalRec = list.totalCount
 
        response.setHeader("Cache-Control", "no-store")
        def data = [
            totalRecords: totalRec,
            results: list
        ]
        render data as JSON
    }

Korzystamy tu z dobrodziejstw dwóch ciekawych możliwości oferowanych przez Grails. Pierwszą z nich są Criteria, pozwalające na budowanie filtrów naszego zapytania (za pomocą domknięć mechanizm ten staje się jeszcze atrakcyjniejszy niż jego pierwowzór pochodzący z Hibernate). Każda klasa dziedzinowa posiada możliwość stworzenia obiektu klasy Criteria za pomocą metody createCriteria(). Drugą ciekawostką jest konwerter JSON, ładowany za pomocą importu:

import grails.converters.JSON

Konwerter ten, w locie, zamieni naszą mapę wyników na format JSON, zrozumiały dla naszej tabeli.

d22

Gotowe. Miłego filtrowania :)

Tagi: , , ,

Komentarze (4)

Grails i kompozytowe klucze w Hibernate

Grails to bardzo ciekawy framework przygotowany dla języka Groovy. Od razu zdobył moją sympatię za to, że – nie tylko nazwą – przypomina architekturę Ruby on Rails, czy też Symfony dla PHP, realizując najważniejsze polityki nowoczesnych frameworków: Convention Over Configuration i Do Not Repeat Yourself. Zawsze ubolewałem (i w sumie nadal ubolewam) nad brakiem takiego rozwiązania dla ‚czystej’ Javy (może Ty znasz jakiś przykład?), dlatego tym chętniej zainteresowałem się tajnikami Grails.

Przyznam, że raczej sceptycznie reaguję na różne ‚liberalne’ języki programowania, np. fakt, że jakąś operację można wykonać używając różnych konstrukcji języka (bo wg mnie zaciemnia to nieco obraz samego kodu i utrudnia jego pielęgnację), albo dynamiczcne typowanie (zawsze przeklinałem PHP za tę ‚przypadłość’), jednak dałem Groovy’emu szansę, za jego główną zaletę: pełną kompatybilność z Javą – Groovy jest de facto wykonywany przez maszynę wirtualną Javy, więc kompatybilność ta jest oczywista.

Umówmy się – należę do tych bardziej konserwatywnych programistów wierzących w ‚twardą’ składnię, a w Groovy nie umiem (jeszcze) nic poza „Hello World”, toteż bardzo zależało mi na frameworku, w którym będę mógł wykorzystać już istniejące ziarenka Javy.  Samego Groovy’ego też chętnie zgłębię, ale najpierw chcę zobaczyć co mogę zrobić z Grailsami wykorzystując istniejący już kod Javy.

Grails – jak RoR, czy Symfony – posiada własny mechanizm mapowania obiektowo-relacyjnego (gdzieś tam głęboko napędzany przez Hibernate), zwany GORM, pozwalający na mapowanie klas Groovy’ego z bazą danych. Ja jednak, przez założenie, chcę mapować ziarna encji Hibernate napisane w Javie. Grails pozwala na to bez problemów, kilka prostych czynności i już możemy wykorzystać przygotowane wcześniej encje w Grails.

No, może nie całkiem bez problemów.

Aby zmapować klasy encji wystarczy, zgodnie z opisem na stronie Grails, wyedytować plik DataSource.groovy, dodać mapowanie w hibernate.cfg.xml i wreszcie skopiować swoje klasy javy do katalogu src/java. Po wykonaniu tych czynności możemy już wygenerować na podstawie encji nowy kontroler i widok, wklepując w konsoli:

grails generate-all pelna.nazwa.naszej.Klasy

Grails wygeneruje dla nas wszystkie niezbędne klasy kontrolerów (w języku Groovy, rzecz jasna), elementy widoku (strony GSP), słowem mamy już cały CRUD. Problem jednak pojawia się w przypadku, gdy nasza encja wyposażona jest w klucz złożony, a schemat naszej bazy danych – jak w moim przypadku –  nie może zostać zmodyfikowany, bo jest wykorzystywany przez inne programy.

Grails teoretycznie pozwoli na mapowanie takich encji, wygeneruje też dla nich kontrolery i pliki widoków, jednak nie bedą one działały. Rozważmy poniższy przypadek.

Mamy encję Produkt (mapowaną  na tabelę „PRODUKTY”), encję Cennik (mapowaną na tabelę „CENNIKI”) i encję Cena (tabela „CENY”) . NetBeans, na podstawie istniejącego schematu bazy danych, wygenerował mi takie klasy (po drobnej korekcie nazw własności):

package net.schowek.grailsapp;
 
// importy
 
@Entity
@Table(name = "PRODUKTY")
@NamedQueries({@NamedQuery(name = "Produkt.findAll", query = "SELECT p FROM Produkt p")})
public class Produkt implements Serializable {
    private static final long serialVersionUID = 1L;
 
    @Id
    @Basic(optional = false)
    @Column(name = "pr_id")
    private Integer id;
 
    @Basic(optional = false)
    @Column(name = "pr_nazwa")
    private String nazwa;
 
    @Column(name = "pr_opis")
    private String opis;
 
    public Produkt() {
    }
 
    public Produkt(Integer prId) {
        this.id = prId;
    }
 
    // gettery i settery
 
    @Override
    public String toString() {
        return getNazwa();
    }
}
package net.schowek.grailsapp;
 
// importy
 
@Entity
@Table(name = "CENNIKI")
@NamedQueries({@NamedQuery(name = "Cennik.findAll", query = "SELECT c FROM Cennik c")})
public class Cennik implements Serializable {
    private static final long serialVersionUID = 1L;
 
    @Id
    @Basic(optional = false)
    @Column(name = "cn_id")
    private Integer id;
 
    @Column(name = "cn_nazwa")
    private String nazwa;
 
    public Cennik() {
    }
 
    public Cennik(Integer cnId) {
        this.id = cnId;
    }
 
    // gettery i settery
 
    @Override
    public String toString() {
        return getNazwa();
    }
}
package net.schowek.grailsapp;
 
// importy
 
@Entity
@Table(name = "CENY")
@NamedQueries({@NamedQuery(name = "Cena.findAll", query = "SELECT c FROM Cena c")})
public class Cena implements Serializable {
    private static final long serialVersionUID = 1L;
 
    @EmbeddedId
    private CenaPK id;
 
    @JoinColumn( name = "ce_pr_id", referencedColumnName = "pr_id", insertable = false, updatable = false  )
    @ManyToOne
    private Produkt produkt;
 
    @JoinColumn( name = "ce_cn_id", referencedColumnName = "cn_id", insertable = false, updatable = false  )
    @ManyToOne
    private Cennik cennik;
 
    @Column(name = "ce_cena")
    private BigDecimal cena;
 
    public Cena() {
    }
 
    public Cena(CenaPK cenaPK) {
        this.id = cenaPK;
    }
 
    // gettery i settery
 
    @Override
    public String toString() {
        return getId().toString();
    }
}

I wreszcie klasa określająca nasz klucz złożony:

package net.schowek.grailsapp;
 
// importy
 
@Embeddable
public class CenaPK implements Serializable {
 
    @Basic(optional = false)
    @Column(name = "ce_pr_id")
    private int cePrId;
 
    @Basic(optional = false)
    @Column(name = "ce_cn_id")
    private int ceCnId;
 
    public CenaPK() {
    }
 
    public CenaPK(int cePrId, int ceCnId) {
        this.cePrId = cePrId;
        this.ceCnId = ceCnId;
    }
 
    public int getCePrId() {
        return cePrId;
    }
 
    public void setCePrId(int cePrId) {
        this.cePrId = cePrId;
    }
 
    public int getCeCnId() {
        return ceCnId;
    }
 
    public void setCeCnId(int ceCnId) {
        this.ceCnId = ceCnId;
    }
 
    @Override
    public String toString() {
        return "net.schowek.grailsapp.CenaPK[cePrId=" + cePrId + ", ceCnId=" + ceCnId + "]";
    }
 
}

Tak przygotowanesą podstawą wygenerowanej aplikacji.  Po uruchomieniu aplikacji za pomocą komendy grails run-app mogę już eksplorować zasoby cen i produktów. Problem jednak zaczyna się, gdy chcę przejrzeć listę cen. Już na liście (http://localhost:8080/GrailsApp/cena/list) widać, że odnośniki do rekordów cen nie przypominają standardowych odnośników do rekordów tabel o prostych kluczach. Przykładowy link do ceny wyglądał tak:

http://localhost:8080/GrailsAppl/cena/show/net.schowek.grailsapp.CenaPK%5BcePrId%3D63%2C+ceCnId%3D2%5D

a strona, którą można pod nim zobaczyć wygląda tak:

err
Jak widać, Grails w miejsce id, podstawia wartość obiektu klasy CenaPK, a ten reprezentowany jest przez swoją metodę toString().  Po odwołaniu się do serwera i zapytaniu o cenę o podanym kluczu, serwer – co oczywiste – częstuje nas wyjątkiem.

Oto prosty sposób, w jaki można temu zaradzić (zakładając, że klucz złożony składa się z pól typu numerycznego).

Przede wszystkim, zmieniłem kod klasy CenaPK na taki (zmieniłem toString() i dodałem jeden konstruktor):

    public static final String KEY_SEPARATOR="_";
 
    @Override
    public String toString() {
        return cePrId+KEY_SEPARATOR+ceCnId;
    }
 
    public CenaPK(String k) {
        String[] keys = k.split( KEY_SEPARATOR );
        this.cePrId = Integer.parseInt(keys[0]);
        this.ceCnId = Integer.parseInt(keys[1]);
    }

Jak widzisz, połączyłem w jeden string dwa klucze oddzielone od siebie separatorem. Oznacza to, że ponowne wygenerowanie widoków komendą:

grails generate-views net.schowek.grailsapp.Cena

poskutkuje nową listą cen, zawierającą odnośniki do rekordów podobne do tych:

http://localhost:8080/GrailsAppl/cena/show/63_2

Aby wszystko grało, musiałem jeszcze przerobić kontroler cen (plik CenaController.groovy w katalogu controllers/) i zmienić wszystkie linie ładujące pojedynczy rekord ceny z

def cenaInstance = Cena.get( params.id )

na

def cenaInstance = Cena.get( new CenaPK(params.id) )

Od tej pory mogłem się już cieszyć działającą aplikacją obsługującą klucze złożone.

Aha, separatorem klucza może być w zasadzie dowolny znak, jednak są tu wyjątki – np. nie zadziała na pewno slash, ponieważ jest on interpretowany przez UrlMappera grails. Warto zatem sprawdzić co zadziała, a co nie.

Tagi: , , ,

Skomentuj

Zen Cart w zgodzie z WAMP

Ostatnio miałem okazję wykonać kilka modyfikacji dla platformy sklepowej Zen Cart zainstalowanej na serwerze mojego klienta. Zen Cart to platforma sklepowa oparta o darmowy osCommerce. Istnieje dla niego dość sporo rozszerzeń i wtyczek, ale moje zlecenie było tak specyficzne, że niestety nie obyło się bez potrzeby modyfikacji kodu sklepu (czego, mówiąc szczerze, bardzo starałem się uniknąć).

Wersja produkcyjna sklepu działa pod kontrolą systemu UNIX‚owego, a specyfika zlecenia polegała na tym, że musiałem dokonać kilku poważnych poprawek – co najważniejsze – działając na różnych platformach (czasem musiałem pracować na domowym Linuksie, czasem na Windows).

Przygotowanie środowiska roboczego pod Linuksem zajęło mi naprawdę niewiele czasu; szybko napisałem skrypty automatycznie pobierające bazę danych i synchronizujące pliki kodu z wersją produkcyjną. Gorzej, gdy zacząłem potrzebować możliwości modyfikacji kodu sklepu spod Windows. Muszę przyznać, że był to mój debiut w pisaniu czegokolwiek w php na tym systemie, ale w końcu postanowiłem dać mu szansę. Jako środowiska programistycznego użyłem Aptana Studio (choć mam wrażenie, że wystarczyłby sam Eclipse z PDT), a całość zamierzałem postawić na – znanym mi ze słyszenia – WAMPie. Instalacja WAMP odbyła się bardzo sprawnie, pierwsze Hello World – również bez niespodzianek. Problem jednak zaczął się po instalacji – a jakże – Zen Carta.

Pobrałem zrzut bazy danych i pliki z kodem sklepu i zainstalowałem je w WAMP wykorzystując do tego możliwość tworzenia aliasów.  Okazało się jednak, że instalacja ta nie działa, WAMP serwuje mi tylko komunikaty błędów, albo jakieś krzaki zamiast polskich znaków. Oto lista czynności, które musiałem wykonać, aby zacząć normalnie pracować nad zmianami dla klienta:

Problem 1Apache częstuje mnie błędem 500 (Internal Server Error) przy jakimkolwiek odwołaniu.

Problem ten spowodowany był wyłączoną domyślnie obsługą modułu rewrite przez WAMPowego Apacza. Pomogła zwykła edycja httpd.conf i odkomentowanie linijki:

LoadModule rewrite_module modules/mod_rewrite.so

(oczywiście po tym procesie należy wyedytować odpowiednio plik .htacces – zgodnie z potrzebami i ścieżkami)

Problem 2 – Kodowanie polskich znaków bazy danych

Zen Cart niespecjalnie zwraca uwagę na różne możliwości kodowania znaków diakrytycznych bazy danych – na serwerze produkcyjnym była jakaś domyślna konfiguracja i taką też wgrałem u siebie. Niestety, okazało się, że WAMPowe domyślne ustawienie kodowania UTF8 pozwala na wczytanie bazy bez problemów (wpisy są dobrze widoczne z poziomu PhpMyAdmina), jednak sam sklep wyświetla znaki zapytania zamiast polskich literek.  Tu nie obyło się bez modyfikacji kodu Zen Carta i tak  w pliku /includes/init_includes/init_database.php dodałem na końcu pliku linijki:

if( HTTP_SERVER == 'http://localhost' )
    $db-&gt;Execute("SET NAMES 'latin2'  COLLATE 'latin2_general_ci'");

i wreszcie zobaczyłem polskie znaki na moim localhost. Powyższy ifcheck wstawiłem po to, by system działał z innymi znakami tylko na konfiguracji roboczej – w wersji produkcyjnej niech zostanie bez zmian. Później i tak usunę te linijki, toteż opatrzyłem je łatwymi do odnalezienia komentarzami. Dodam, że Zen Cart jest tak napisany, że połączenie z bazą danych odbywa się w jeszcze kilku innych miejscach,  należy więc mieć na względzie potencjalną potrzebę dodania powyższych linijek do innych plików, zawierających linijkę:

$db-&gt;connect( DB_SERVER, DB_SERVER_USERNAME, DB_SERVER_PASSWORD, DB_DATABASE, USE_PCONNECT, false );

Problem 3 – Puste listy produktów

Kiedy już zażegnałem problemy ‚techniczne’, okazało się, że sklep działa, ale listy produktów są w nim puste. Nie dostawałem komunikatów błędów, ale mimo to strona główna zawierała jedynie boczne sideboksy, a w środku nic. Okazało się, że problemem tutaj był fakt, że domyślna konfiguracja php w WAMP ma wyłączoną opcję Short Tags, co powoduje, że wszelkie template’y zawierające znacznik otwarcia kodu php <? zamiast <?php nie były interpretowane przez parser php. Włączenie powyższej opcji w pliku php.ini spowodowało prawidłowe wyświetlanie produktów.

Po tych poprawkach i restarcie wszystkich usług WAMPa mogłem wreszcie zacząć prace nad sklepem. Wydaje mi się jednak, że – gdybym chciał oprzeć na takiej konfiguracji wersję produkcyją sklepu – musiałbym się jeszcze sporo natrudzić, by uznać to rozwiązanie za stabilne.

Tagi: , , , ,

Komentarze (1)