Przydatne skrypty czyli groch z kapustą

Skrypt który umożliwia łatwiejsze przeglądanie logów serwera, zamienia ich kolejność w ten sposób, że ostatnie wpisy (w liczbie 100) pojawiają się na górze pliku.

writer = new File("serverREVERT.log").newWriter("UTF-8", false)
lines=new File("server.log").readLines().asList()

max=lines.size()-1
min=lines.size()-200

for(index in max..min){
writer.writeLine(lines.get(index--))
}
writer.close()


WinTail

Kod wyświetlający na konsoli ostatnie 200 linii z pliku logów serwera mógłby wyglądać tak:

//pathToLogFile="C:\\JAVA\\glassfish\\domains\\domain1\\logs\\server.log"
pathToLogFile="C:\\JAVA\\jboss-4.2.1.GA\\server\\default\\log\\server.log"
linesNumber=100

"cmd /c cls".execute()
lines=new File(pathToLogFile).readLines().asList()
max=lines.size()-1
min=lines.size()-linesNumber
if(min<0)min=0>Można go rozbudować o kilka dodatkowych linii, ktorych zadaniem jest wyswietlenie blokow oznaczonych przez debuggera, oczywiscie sami musimy zadbać o to, by bloki które chcemy wyświetlić były oznaczone w odpowiedni sposób. W moim kodzie blok SQL, który chce monitorować otaczam tagami [SQLDEBUG] i [END-SQLDEBUG]. Skrypt jest wykonywany jesli zmienna DEBUG jest ustawiona na 1.


DEBUG=1
if(DEBUG==1){
pattern=/\[SQLDEBUG\].*\[END-SQLDEBUG\]/

print "\n\n ************************************************************"
print "\n ************** OZNACZONE KOMUNIKATY DEBUGGERA **************"
print "\n ************************************************************\n"
content=new File(pathToLogFile).getText();
content.eachMatch(pattern){ match->
print "\n"+match[0]
}
print "\n ************************************************************\n"
}


Skrypt zmieniający duże litery na małe w danym katalogu


groovy -e "new File(\".\").eachFile{it.renameTo(new File(it.getName().toLowerCase()))}"



Skrypt listujący pliki z drzewa podkatalogów



outF=new File("output.log").newWriter("UTF-8", false)
new File(".").eachFile{
if(it.getName().matches(".*svn.*")||it.getName().matches(".*prop.*")||it.getName().matches(".*base.*"))
return
if (it.isDirectory())
listujKatalog it
}
listujKatalog new File(".")



void listujKatalog(File aKat){
outF.writeLine("--"+aKat.getName())
aKat.eachFile{
if(it.getName().matches(".*doc.*")){
outF.writeLine(it.getName())
}
}
}
outF.close()

Rekurencyjne przetwarzanie drzewa katalogów

Często okazuje się że istnieje potrzeba wykonania jakiejś operacji na wszystkich zbiorach które znajdują się na dysku i umieszczone są w różnych katalogach, jako elementy potomne mające wspólny korzeń. Wtedy wykorzystać można bardzo wygodny mechanizm przetwarzania rekurencyjnego. Jako przykład niech posłuży skrypt, który z drzewa katalogów usuwa wszystkie katalogi (wraz z zawartością) .svn zawierające pliki związane z obsługą svn.


import org.apache.commons.io.FileUtils

def basedir="kat1\\kat2" //SCIEZKA_DO_ROOT

outputLog=new File("output.log").newWriter("UTF-8", false)

dir = new File(basedir)
fileList=[]
dir.eachFileRecurse{ f ->
if((f.name=='.svn')&&(f.isDirectory())&&(f.exists())){
fileList.add(f)
}
}


for(mf in fileList){
outputLog.writeLine( "Usunieto: "+mf.canonicalPath)
FileUtils.deleteDirectory(mf)
}

outputLog.writeLine( "Calkowita liczba usunietych:"+fileList.size())
outputLog.close()

Do poprawnego działania konieczne jest sciągnięcie biblioteki commons.io.*, a następnie umieszczenie pliku commons-io-xxxxx.jar w katalogu GROOVY_HOME\lib

Groovy a wyrażenia regularne

Wprowadzenie


Teraz gdy już przebrnąłem przez podstawowe metody WE/WY zacząłem kombinować jak wykorzystać Grooviego do przetwarzania plików tekstowych. A wiadomo, że gdy mamy na myśli język skryptowy i ocenę jego przydatności we wspomnianym zakresie to koniecznie należy przyjrzeć się w jaki sposób obsługuje on wyrażenia regularne.

Wydaje mi się że o mocy która kryje się w wyrażeniach regularnych nikogo przekonywać nie trzeba. Naprawdę nie mogę zrozumieć, dlaczego tak mało edytorów tekstu tworzonych pod okna wspiera i wykorzystuje ten potężny mechanizm wyszukiwania i przetwarzania wzorców. Ale do rzeczy.

Do przetwarzania wyrażeń regularnych Groovy wykorzystuje biblioteki dostarczane z JDK. Oczywiście wprowadza szereg udogodnień, które sprawiają, że przeszukiwanie tekstów pod kątem występowania w nich jakiegoś wzorca jest dużo prostsze i przyjemniejsze niż w samej Javie.
Dzięki mechanizmom wyrażeń regularnych Groovy pozwala na:
  • określenie czy (a jeśli tak to ile razy) w tekście wystąpił ciąg znaków zgodny ze wzorcem
  • wykonanie określonych operacji na dopasowanym łańcuchu znaków
  • dokonanie zastąpień dopasowanych łańcuchów znakowych
  • podział dopasowanego ciągu na podciągi

Definiowanie wzorców



Przetwarzanie tekstów z wykorzystaniem wyrażeń regularnych wiąże się z koniecznością definiowania wzorców dopasowań. Są to instrukcje dla mechanizmu przetwarzającego zawierające informacje czego ma szukać i w jaki sposób to przetwarzać. Mają one postać ciągów znakowych, zawierających pewne symbole o specjalnym znaczeniu. Dzięki nim możemy określić pewne grupy znaków, liczb, które mają zostać wyszukane w tekście. Najważniejsze z nich to:

  • . (kropka) - dowolny znak
  • ^ - początek linii
  • $- koniec linii
  • \d- dowolna liczba
  • \D- dowolny znak nie będący liczbą
  • \s- biały znak
  • \S- dowolny znak nie będący znakiem białym
  • \w- słowo
  • \W-znaki nie układające się w słowa
  • \b-granica słowa
  • ()-operator grupowania
  • (a|b)-warunek logiczny "lub" (a lub b)
  • * - zero lub więcej wystąpień poprzedzającego znaku(grupy)
  • + - jeden lub więcej wystąpień poprzedzającego znaku(grupy)
  • ? - dokładnie jedno wystąpienie poprzedzającego znaku(grupy)
  • x{m,n} - co najmniej m i co najwyżej n znaku(grupy) poprzedzajacej
  • x{m} - dokladnie m wystapien znaku (grupy) poprzedzajacej
Wzorce dopasowań, rozpoznawane przez Grooviego ograniczane są znakami "/"(slash). Przykładowe definicje masek wyglądają następująco:
  • /\d\d-\d\d-\d\d\s?\d\d:\d\d/ lub w uproszczeniu \d{2}-\d{2}-\d{2}\s?\d{2}:\d{2} - dopasuje się do wszystkich dat zapisanych w formacie "liczba liczba-liczba liczba- liczba liczba SPACJA liczba liczba:liczba liczba", czyli np. 01-02-07 10:22
  • /href=".*?"/ - maska pozwalająca na wyszukanie w tekście wystąpień adresów URL
Pod adresem http://www.nvcc.edu/home/drodgers/ceu/resources/test_regexp.asp znaleźć można proste narzędzie, które służy do testowania wrażeń regularnych.

Dopasowanie zachłanne

Operatory rozszerzające (takie jak * i +) mają pewną cechę, o której zawsze należy pamiętać. Chodzi o to, że dążą one do dopasowania jak najszerszego możliwego ciągu znakowego pasującego do zadanego wzorca.

Spójrzmy na następującą maskę: /href=".*"/.
Należy rozumieć ją następująco:
dopasuj się do ciągu znaków, rozpoczynającego się słówkiem "href", po którym występuje znak "=" i cudzysłów ("), w jego wnętrzu ma znaleźć się zero lub więcej dowolnych znaków kończących się cudzysłowem.

Wydawać by się mogło, że taki wzorzec prawidłowo wyciągnie nam wszystkie adresy stron w postaci href="adres". Tak rzeczywiście by było gdyby nie zachłanność dopasowania operatora *.
Problem występuje w sytuacji gdy wyrażenie regularne zostanie zastosowane np. do następującego tekstu:

To jest (href="www.pierwszy.adres.url") pierwszy rozdzial (href="www.drugi.adres.url")

Dopasowanie nastąpi w następujący sposób: znaleziony zostanie następujący ciąg znaków: href=", i jak dotąd wszystko jest zgodne z oczekiwaniami, jednak potem zaczynają się problemy, bo każdy znak, który pojawi się potem, aż do napotkania OSTATNIEGO cudzysłowu zostanie uznane jako pasujące do ".*", a więc wynikiem zastosowania reguły wzorca będzie ciąg znaków:

href="www.pw.edu.pl") pierwszy rozdzial (href="www.bla.bla.pl"

Czy o to mi chodziło ? Niekoniecznie....

SPOSÓB OGRANICZENIA ZACHŁANNOŚCI:

Aby ograniczyć zachłanność operatora, należy we wnętrzu reguły, bezpośrednio po znaku "*", a przed znakiem ograniczającym zakres dopasowania (u nas cudzysłów) znaku "?". Spowoduje to dopasowanie się nie do maksymalnie najdłuższego łańcucha, tylko do najkrótszego.

A więc nasza reguła wybierająca powinna wyglądać następująco: /href=".*?"/. Wynik jej działania jest już zgodny z naszymi oczekiwaniami :

href="www.pierwszy.adres.url"
href="www.drugi.adres.url"

Wykorzystywanie wyrażeń regularnych


Metody find() i matches()
Wyrażenia regularne w Groovym obsługiwane są przez klasę java.util.regex.Matcher, która udostępnia dwie bardzo użyteczne metody- find() i matches().
  • matcher.find()- pozwala na określenie czy w określonym ciągu znakowym, da się wyszukać podciąg zgodny ze wzorcem dopasowania
  • matcher.matches()- przeszukuje łańcuch wejściowy i próbuje dopasować cały wzorzec
Dla uproszczenia Groovy definiuje dwa pomocne operatory:
  • =~ jest tożsamy z uruchomieniem metody matcher.find()
  • ==~ - jest tożsamy z uruchomieniem metody matcher.matches()
Metoda find() zwraca obiekt matcher. Aby dostać się do wszystkich zwróconych przez nią obiektów można wywołać jej metodę each() - wraz z zastosowaniem domknięcia. Przykładem działania niech będzie skrypt wyszukujący w łańcuchu znaków wystąpień cyfry 4.

text='1 2 4 8 64'
(text=~/4/).each{match->print match}

(text=~/4/) - zwraca obiekt matcher, który zawiera w sobie dopasowane ciągi znaków ( w tym przypadku wystąpienia znaku 4)

Metoda String.eachMatch(regex)Alternatywnym i w moim przekonaniu bardziej intuicyjnym sposobem dotarcia do obiektów otrzymanych w wyniku zastosowania wzorca, jest metoda String.eachMatch(wyrazenie_regularne).
Dzięki niej i wykorzystaniu mechanizmu domknięć możliwe jest przetworzenie każdego podzbioru znaków, który na podstawie wzorca wyrazenie_regularne zostani wyodrębniony z obiektu String, na którym wywoływana jest metoda. Np.

pattURL=/href=".*?"/
content=new File("regularExp_source.txt").getText();
content.eachMatch(pattURL){ match->
print "\n"+match[0]
}

Powyższy skrypt powoduje że w pliku regularExp_source.txt wyszukane zostaną wszystkie adresy url. Należy zwrócić uwagę na ciekawą rzecz. Mianowicie obiekt matcher wyniki dopasowania zwraca nie w postaci pojedynczego łańcucha znaków, lecz tablicy, pod której zerowym indeksem znajduje się wynik dopasowania. Jeśli we wzorcu wystąpiło n operatorów grupowania () to każda z dopasowanych grup znajdzie swe miejsce pod kolejnym indeksem.

Metoda String.replaceAll(regex)
Inną ciekawą metodą, jest String.replaceAll(regex). Pozwala ona na zamianę jednych ciągów znakowych na inne.

Schemat jej działania jest następujący:
content.replaceAll(/wzorzec/){it - 'ciągZastępowany'+'ciągZastępujący'}. Aby zamiana nastąpiła ciągZastępowany musi zostać dopasowany przez wzorzec.

Przykładem zastosowania jest skrypt zamieniający polskie znaki na ich liczbowe reprezentacje w utf.

myFile=new File("znakiUTF.txt") myFile.write("\n")
content=new File("znakiAscii.txt").text
content=content.replaceAll(/Ą/){it - 'Ą'+'\\\\u0104'}
content=content.replaceAll(/ą/){it - 'ą'+'\\\\u0105'}
content=content.replaceAll(/Ć/){it - 'Ć'+'\\\\u0106'}
content=content.replaceAll(/ć/){it - 'ć'+'\\\\u0107'}
content=content.replaceAll(/Ę/){it - 'Ę'+'\\\\u0118'}
content=content.replaceAll(/ę/){it - 'ę'+'\\\\u0119'}
content=content.replaceAll(/Ł/){it - 'Ł'+'\\\\u0141'}
content=content.replaceAll(/ł/){it - 'ł'+'\\\\u0142'}
content=content.replaceAll(/Ó/){it - 'Ó'+'\\\\u00D3'}
content=content.replaceAll(/ó/){it - 'ó'+'\\\\u00F3'}
content=content.replaceAll(/Ś/){it - 'Ś'+'\\\\u015A'}
content=content.replaceAll(/ś/){it - 'ś'+'\\\\u015B'}
content=content.replaceAll(/Ź/){it - 'Ź'+'\\\\u0179'}
content=content.replaceAll(/ź/){it - 'ź'+'\\\\u017A'}
content=content.replaceAll(/Ż/){it - 'Ż'+'\\\\u017B'}
content=content.replaceAll(/ż/){it - 'ż'+'\\\\u017C'}
myFile.write(content,"UTF-8")

Operacje odczytu i zapisu

Obsługa plików tekstowych


Przetwarzanie plików tekstowych i obsługa wyrażeń regularnych jest jednym z najczęstszych zastosowań języków skryptowych, dlatego zgłębianie możliwości grooviego rozpocząłem od obsługi operecji wejścia/wyjścia.

Odczyt pliku

W najprostszym przypadku odczyt pliku można wykonać w jednej linii kodu. Ma on następującą postać:

new File("source.txt").eachLine{linia -> print linia}

W powyższym przykładzie wykorzystałem powszechnie stosowany mechanizm domknięć (clousures). Polega on na tym że metoda przekazuje swój wynik do operacji które znajdują się w zasięgu nawiasów klamrowych. Dostęp do wartości zwróconej przez metodę uzyskuje się przy pomocy odczytu wartości parametru zdefiniowanego na samym początku domknięcia, przed znakiem "->" - w tym przypadku "linia". Powyższy kod po przeczytaniu każdej z linii pliku wyświetli ją na konsoli. Dokumentacja języka mówi, że nie musimy się martwić o to, że podczas odczytu takiego pliku może wystąpić wyjątek. Mechanizmy obsługi plików zadbają o to by został on właściwie obsłużony, a źródło prawidłowo zamknięte.

Często zdarza się, że nie istnieje konieczność przetwarzania kolejnych linii pliku. Wtedy można użyć metod wczytujących do zmiennych tekstowych całą tekstową zawartość pliku. Skrypt dokonujący tej czynności wygląda następująco:

content=new File("source.txt").text
print content

Wczytuje on do zmiennej "content" całą zawartość pliku source.txt, a potem wyświetla ją na ekranie.

W sytuacji, gdy zaistnieje potrzeba odczytu pliku o niestandardowym kodowaniu, wtedy można posłużyć się metodą getText(encoding).

content=new File("source.txt").getText("UTF-8")
print content

Zapis do pliku

W przypadku gdy chcemy jednokrokowo zapisać całą zawartość pliku tekstowego, mamy do dyspozycji metodę:

new File("output.txt").write("hello :)")

W przypadku, gdy zachodzi konieczność obsługi kodowania, można podać je jako dodatkowy parametr metody:

new File("output.txt").write("hello :)", "UTF-8")

Jeżeli chcielibyśmy zapisywać tekst całymi liniami można skorzystać z następującego sposobu:

writer = new File("myText.txt").newWriter("UTF-8", true)
writer.writeLine("pozdrowienia")
writer.close()

Dodatkowy parametr w metodzie pobierającej obiekt Writer decyduje o tym czy plik ma zostać założony lub wyczyszczony(false), czy też treść ma być do niego dopisana(true).


Należy pamiętać że otwarcie pliku do zapisu zakłada nowy plik, lub kasuje jego starą zawartość. Jeżeli zaistnieje konieczność dopisywnia tekstu do istniejącego pliku można skorzystać z metody append():

new File("output.txt").append("hello :) - po raz drugi")

tak samo jak w poprzednich sytuacjach, opcjonalnie można określić kodowanie pliku:

new File("output.txt").append("hello :) - po raz drugi","utf-8")

Obsługa plików binarnych

Sposoby obsługi plików binarnych, oraz inne metody stosowane przy zapisie i odczycie plików szczegółowo omówione zostały pod adresem http://groovy.codehaus.org/Streams,+Readers,+and+Writers

Domknięcia (closures)

Domknięcie jest to mechanizm, który w Groovym jest wyjątkowo często stosowany. Ma postać bloku kodu objętego nawiasami klamrowymi. Posiada on cechy nie tylko obiektu (można go stworzyć, przesyłać do niego referencje), ale też metody(może przyjmować parametry i zwracać wartości).

Najprostszy przykład domknięcia mógłby wyglądać następująco (być może nie jest zbyt finezyjny, ale póki co sam się tego uczę)

myclosure={textToPrint->print textToPrint}
myclosure("Witaj !")


Powyższy skrypt w pierwszej linii definiuje domknięcie o nazwie myclosure, które pobiera jeden parametr o nazwie "textToPrint". Parametr ten umieszcza się po nawiasie klamrowym, a przed znakiem "->". Jego wartość jest dostępna we wnętrzu domknięcia. Powyższy przykład odwołuje się do otrzymanego parametru i wyświetla jego wartość na konsoli.

W przypadku gdy nasze domknięcie pobiera tylko jeden parametr można skorzystać z oferowanej przez Grooviego skróconej formy definicji i odwołania się do domknięcia. Wartość parametru dostępna jest we wnętrzu domknięcia po odwołaniu się do słowa kluczowego "it".

shortClosure={print it}
shortClosure("Hello Mariusz !!!")

Należy pamiętać że kod domknięcia jest wykonywany dopiero przy odwołaniu się do niego.

Możliwe jest też definiowanie domknięć przyjmujących więcej niż jeden parametr. W takim przypadku przed znakiem -> należy umieścić ich listę:

sumArgClosure={arg1,arg2-> print "SUM="+(arg1+arg2)}
sumArgClosure(5,3)

Powyższy skrypt wyświetli na ekranie napis SUM=8

Bardzo ciekawą własnością domknięć jest to, że istnieje możliwość przesyłania do nich referencji. Jako przykład niech posłuży kod klasy Calculator.

public class Calculator{
def argList=[2,3,4]

public void applyOperation(String operation,c){
print "\nWykonywana operacja to:\n***"+operation+"***"

for (elem in argList){
print "\n"+c(elem)
}
print "\n******"
}


}

def calc=new Calculator()
calc.applyOperation("Mnozeniex3"){(it*3)}
calc.applyOperation("Potegowanie"){(it*it)}

W klasie zdefiniowana jest lista argumentów (
argList=[2,3,4]) na której będą wykonywane operacje, oraz jedna metoda, która jako swój parametr przyjmuje domknięcie i tekstową nazwę operacji którą ma wykonać(chociaż znaczenie tego parametru jest mało istotne - wykorzystywane jest tylko do wyświetlenia komentarza). We wnętrzu metody zdefiniowana została pętla która przechodzi po wszystkich elementach listy argumentów, a następnie dla każdego z nich wykonuje domknięcie, przysłane do metody w parametrze :

for (elem in argList){
print "\n"+ c(elem)
}
print "\n******"
}

Wywołanie w.w. metody ma postać:

calc.applyOperation("Mnozeniex3"){(it*3)}
calc.applyOperation("Potegowanie"){(it*it)}

Za każdym razem na argumentach kalkulatora zostanie wywołana operacja zdefiniowana w domknięciu. Należy zwrócić uwagę na pewne udogodnienie, mianowicie definiowanie domknięcia przesyłanego w parametrze może wystąpić poza argumentami funkcji (chociaż notacja tradycyjna, po przecinku też jest dopuszczalna).

Groovy - pierwsze kroki

Jestem przekonany że każdy(w sumie nie wiem czy każdy ale ja na pewno tak :) ), kto choć przez chwilę obcował z systemami linuxowo unixowymi i
doświadczył potężnych możliwości, które drzemią w mechanizmach skryptów powłoki ma trudności z dostosowaniem się do dramatycznej wręcz liczby ograniczeń jakie
w tym zakresie narzuca system Windows. Możliwości jego powłoki są żenująco mizerne. Na szczęście istnieje prosty sposób na uzyskanie potężnej funkjcjonalności.
Lekiem na całe zło może okazać się zastosowanie języków skryptowych, takich jak Ruby,Python czy Groovy.

Ze względu na wspólne korzenie Javy i Grooviego do mój wybór był prosty. Po krótkim już rozeznaniu wiem że okazał się słuszny. Prosta intuicyjna, javopodobna składnia i potężne możliwości - to w mojej subiektywnej ocenie najważniejsze cechy Grooviego, które sprawiają, że czas poświęcony na jego naukę naprawdę nie będzie stracony.

Nie chcę rozwodzić się nad sposobem instalacji, ani cechami podstawowej składni języka - te zagadnienia zostały szczegółowo opisane na stronie Groovy-ego. Chciałbym skupić
się na konkretnych problemach, do których rozwiązania przydały mi się potężne możliwości tego języka.

JSF - czyli jak ja to rozumiem...

JavaServer Faces - wstęp
Od jakiegoś czasu interesowałem się technologią JSF, nigdy jednak nie wystarczyło mi czasu i motywacji by zająć się ją na poważnie. Teraz się to zmieniło - mam już motywację (nowa praca :) ) - a czas chcę i muszę zorganizować, nic więc nie stoi na przeszkodzie by zacząć przekopywać internet, książki i dokumentacje w poszukiwaniu wartościowych informacji.

Postanowiłem, że będę zamieszczał tu rzeczy, które wydają mi się szczególnie ciekawe i godne zapamiętania. Wiem, że wielokrotnie mogę się mylić w rozumieniu nowych dla mnie zagadnień i mechanizmów.

Wobec tego zwracam się z prośbą do wszystkich odwiedzających tę stronę o uwagi krytyczne i wytykanie mi wszystkich zauważonych nieścisłości i pomyłek.

Java Server Faces - wariacje na temat... czyli pierwsze refleksje
Podczas kilkuletniego obcowania z J2EE wyrobiłem sobie zdanie, że tworzenie warstwy prezentacji jest słabym punktem całego procesu projektowania i implementacji tego typu aplikacji. Tworzenie interfejsów użytkownika zawsze kojarzyło mi się z czymś żmudnym i nie do końca zrozumiałym. Chociaż pojawiły się technologie wspomagające takie jak tagi JSTL, czy biblioteka Tiles to ciągle nie rozwiązywały one wszystkich problemów. Mam wrażenie że chociaż ewoluowały w kierunku ciągłych ulepszeń to jednak nie były rozwiązaniami kompleksowymi. Budowanie interfejsów ciągle wiązało się z uciążliwym iterowaniem list, wymagało mieszania logiki aplikacji z elementami prezentacyjnymi. Mam wrażenie że sytuację radykalnie zmieniło pojawienie się technologii JavaServer Faces.

Cechy JSF które wydają mi się godne zauważenia i wyróżniają ten framework spośród innych to:

  • Przede wszystkim JSF jest standardem, który zapewne niedługo włączony zostanie do specyfikacji JEE.
  • JSF jest frameworkiem komponentowym. Istnieje utrzymywany ścisły związek pomiędzy komponentami interfejsu użytkownika a komponentami zarządzanymi przez framework, których zadaniem jest dostarczanie danych i reagowaniem na zdarzenia generowane przez użytkownika.
    Wszystkie komponenty implementują ten sam interfejs, więc w wielu przypadkach obsługa ich zachowania odbywa się w sposób bardzo do siebie zbliżony. Skraca to czas potrzebny na opanowanie zasad posługiwania się technologią.
  • Wszystkie elementy ułożone na stronie układają się w określoną hierarchię. Istnieje możliwość programowego wpływania na tą hierarchię.
  • Tworzenie intefejsu użytkownika nie wymaga zrywania z obiektowym paradygmatem tworzenia aplikacji. Przy obsłudze zdarzeń ciągle operujemy na obiektach, framework zdejmuje z nas obowiązek przesyłania parametrów w postaci tekstowej.
  • JSF wprowadza znany z aplikacji desktopowych model obsługi zdarzeń.
  • Mechanizmy frameworka dbają o to, by zapamiętywać stan komponentu pomiędzy różnymi żądaniami nadsyłanymi przez klienta.
  • Architektura JSF jest oparta o dobrze znane i wielokrotnie sprawdzone w praktyce wzorce projektowe takie jak: MVC, Observer, Composite.
  • Konfiguracja jest jasna, zgromadzona w plikach xml.
  • Osadzanie komponentów na stronie odbywa się w sposób deklaratywny, nie wymaga mieszania logiki z elementami odpowiedzialnymi za prezentację.
  • Framework dostarcza pokaźną bibliotekę komponentów interfejsu użytkownika renderujące określone struktury danych.
  • Wraz z frameworkiem dostarczane są obiekty pomocnicze takie jak renderery, walidatory poprawności danych.
  • Framework dostarcza mechanizmów przetwarzania wielokrokowych formularzy, walidację danych, konwersję typów, obsługę zdarzeń,.
Różnica pomiędzy manage bean i backing bean
Technologia JSF wprowadza kilka nowych pojęć - te, których zrozumienie sprawiło mi trochę kłopotu to "managed bean" i "backing bean".

Na początek "backing bean". Jest to komponent, który w architekturze MVC, o którą oparty jest JSF ma pełnić rolę warstwy modelu i stanowić punkt styku pomiędzy warstwą prezentacji i biznesową. Ma on delegować żądania klienta do niższych warstw aplikacji. Jest on skojarzony z odpowiadającym mu komponentem UI (User Interface) używanym na stronie. Definiuje skojarzone z nim właściwości i metody stanowiące o logice jego zachowania. Dzięki nim występuje całkowita separacja logiki od definicji komponentu interfejsu użytkownika.

Czym wobec tego jest "managed bean" ?

Różnica pomiędzy nimi jest bardzo subtelna, podjąłem nawet próbę jej odkrycia. Niestety nie znalazłem nic co by całkowicie mnie przekonało, ale na jednym z forów trafiłem na ciekawą dyskusję, a niewątpliwie jej najciekawsze zdanie brzmi:

"managed beans are instances of backing beans which are made available to the JSF pages by defining them in the faces-config.xml"

Myślę że na chwilę obecną, należy przyjąć ją za całkiem rozsądną...

Etapy przetwarzania żądania przez framework JSF

W przetwarzaniu żądania można wyróżnić kilka faz:

  • Restore View - etap tworzenia drzewa komponentów skojarzonych z żądanym zasobem (stroną). W przypadku gdy w obrębie danej sesji użytkownika drzewo było już tworzone, framework w tej fazie odtwarza pierwotny stan komponentów.
  • Apply Request Value - w przypadku gdy drzewo zostało odtworzone, w tym etapie następuje uaktualnienie wartości komponentów. Następuje sprawdzenie czy w obiekcie żądania (request) nie znajdują się nowsze wartości elementów komponentu UI, jeśli istnieją, zostają one uaktualnione.
  • Process Validations - w tej fazie przetwarzania uruchomione zostają validatory poprawności skojarzone z elementami komponentu UI. Sprawdzana jest kompletność i poprawność wartości pól.
  • Update Model Values - w tym etapie przetwarzania komponent interfejsu użytkownika sprawdza i uwspólnia model danych ze skojarzonym z nim beanem.
  • Invoke Application - w tej fazie obsługiwane są wszystkie skolejkowane zdarzenia.
  • Render Response - po uwzględnieniu wszystkich zmian jakie zaszły przy przetwarzaniu żądania tworzone jest nowe drzewo komponentów i trwale jest ono kojarzone z określoną stroną jsp.

ToyStore - sklep z zabawkami ( Sitemesh, JSF, Spring, Hibernate, Oracle Express )- CZĘŚĆ I I- WARSTWA WEB

Zapraszam do zapoznania się z częścią I artykułu :
ToyStore - sklep z zabawkami ( Sitemesh, JSF, Spring, Hibernate, Oracle Express )- CZĘŚĆ I - warstwa danych i biznesowa


A więc do dwóch warstw naszej aplikacji teraz podejmę próbę dopracowania trzeciej - umożliwiającej dostęp do aplikacji przy pomocy przeglądarki WWW. Jako że ostatnio przechodzę okres zauroczenia frameworkiem JSF, wybór technologii realizacji zadania nie był trudny... a wiec do rzeczy...

Konfiguracja serwletu FacesServlet i webowego kontekstu aplikacji

Framework JSF jest oparty na ogólnie znanym wzorcu projektowym MVC (Model View Controller ) nic więc dziwnego, że najważniejszym elementem jego konfiguracji jest właściwe zdefiniowanie parametrów Serwletu Rozdzielającego (Servlet Dispatcher) . W przypadku JSF nosi on nazwę FacesServlet. Jego konfiguracja odbywa się w deskryptorze rozmieszczenia - pliku /WEB-INF/web.xml.

Sama modyfikacja pliku jest bardzo prosta sprowadza się ona do kilku rzeczy:

Po pierwsze należy zdefiniować sam serwlet. Robi się to przy pomocy następującego kodu:

<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

Ponieważ nasza aplikacja ma mieć możliwość korzystania z obiektów stworzonych przez Springa należy zdefiniować jej kontekst springa, przez definicję odpowiedniego listenera:

<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

Ostatnim krokiem będzie zdefiniowanie filtra, który wszystkie żądania zakończone przyrostkiem *.faces będzie przekierowywał do serwletu Faces Servlet. Wygląda on następująco:

<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>

Nasz serwlet jest już gotowy do pracy... teraz czas na stworzenie pierwszego komponentu JSF.

Pierwszy scenariusz - wyświetlenie listy kategorii
Na początek chciałbym wyświetlić listę kategorii produktów, które są w bazie. Chciałbym, aby była ona prezentowana w postaci tabelki, z której to możnaby przejść do edycji kategorii, usunąć ją, dodać nową, a także obejrzeć jakie produkty do niej należą.

Wykorzystanie technologii JSF implikuje, że do realizacji tego scenariusza będziemy potrzebowali definicji dwóch rzeczy, a mianowicie:
  • strony na której zdefiniowany będzie UI kompotent odpowiedzialny za renderowanie tabelki
  • beana ( backing beana), który w architekturze MVC umiejscowiony będzie w warstwie modelu i stanowić będzie zaplecze dla komponentu UI. Jego zadaniem będzie dostarczenie danych potrzebnych do wypełnienia tabelki, jak również metod stanowiących o logice działania (więcej o beanach napisałem w artykule JSF- czyli jak ja to rozumiem...).
Nasz pierwszy backing bean - CategoryListUI
Pierwszym krokiem, który należy wykonać jest zdefiniowanie obiektu - beana, który zasili naszą tabelkę w dane i dostarczy obsługującej ją logiki. W terminologii JSF bean taki nazywa się "backing bean".

A więc umiejscawiam go w pakiecie mw.toystore.web.ui i nadaję nazw CategoryListUI.

Nasz bean będzie korzystał z kontekstu springowego i za jego pośrednictwem uzyska referencje do interfejsu biznesowego IProductManager. Aby obiekt ten został prawidłowo zaincjalizowany przy pomocy mechanizmu IoC w klasie backing beana należy zdefiniować własność tego typu, oraz metody dostępowe (setter i getter). Stosowny fragment kodu wygląda następująco:

.......
private IProductManager productManager;

public IProductManager getProductManager() {
return productManager;
}

public void setProductManager(IProductManager productManager) {
this.productManager = productManager;
}
.......

Teraz, gdy nasz komponent potrafi już sięgnąć do warstwy biznesowej można stworzyć metodę, za której pośrednictwem komponent interfejsu użytkownika (UI) leżący na stronie i skojarzony z naszym beanem uzyska dostęp do listy obiektów kategorii. Niech nazywa się ona getCategoryList(). Jej konstrukcja będzie bardzo prosta, bo ograniczy się tylko do wywołania metody z obiektu interfejsu biznesowego i zwrócenia na zewnątrz pobranej z niższych warstw aplikacji kolekcji obiektów kategorii. Nasza metoda wygląda następująco:

public List getCategories(){
return this.productManager.getCategoryList();
}

W tym momencie nasz bean potrafi zaserwować listę kategorii, posiada więc minimalną funkcjonalność. Dzięki niemu komponent interfejsu użytkownika będzie w stanie uzyskać kolekcję obiektów reprezentujących różne kategorie.

Zanim jakikolwiek komponent interfejsu użytkownika skorzysta z naszego beana musi zostać wykonana stosowna konfiguracja, która dokonuje się przez dokonanie modyfikacji w pliku /WEB-INF/faces-config.xml. Przyjrzyjmy się im trochę dokładniej.

Propagowanie beana jako Managed Bean

Przyszedł czas na integrację naszego beana z frameworkiem JSF. Odbywa się ona przez dokonanie odpowiednich wpisów w pliku konfiguracyjnym /WEB-INF/faces-config.xml.

Myślę, że można powiedzieć, że każdy backing bean, którego opiszemy w tym pliku nabywa kilka nowych cech. Przede wszystkim, nadana mu tutaj logiczna nazwa jest widoczna dla wszystkich komponentów interfejsu użytkownika. Mogą się do niego odwoływać i na podstawie uzyskanych od niego informacji renderować odpowiednie elementy interfejsu użytkownika.
Cyklem życia takiego beana zarządza już sam framework. W żadnym miejscu nie ma konieczności martwienia się o jego stworzenie i poprawną inicjalizację. Zadba o to framework. Być może dlatego po propagacji, beany uzyskują nową nazwę - managed beans.

Ale po kolei. Przyjrzyjmy się kolejnym elementom naszego pliku /WEB-INF/faces-config.xml.

Cała konfiguracja objęta jest tagami <faces-config> </faces-config>.

Przed definicją beana znajduje się wpis konfiguracyjny o specjalnym znaczeniu. Mianowicie powoduje on, że framework JSF uzyskuje dostęp także do kontekstu springowego (temat integracji frameworków Spring i JSF opisałem w artykule "Próba połączenia JSF i Spring..." ). Dzięki temu do naszego managed beana możliwe jest wstrzyknięcie obiektów zarządzanych przez kontener IoC (a więc np. naszego interfejsu biznesowego IProductManager). Stosowny fragment pliku wygląda następująco:

<application>
<variable-resolver>org.springframework.web.jsf.DelegatingVariableResolver</variable-resolver>
</application>

Definicja samego managed beana wygląda następująco:

<managed-bean>
<managed-bean-name>categoryListUI</managed-bean-name>
<managed-bean-class>mw.toystore.web.ui.CategoryListUI</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>productManager</property-name>
<value>#{productManager}</value>
</managed-property>
</managed-bean>

Powyższe wpisy powodują że stworzony zostanie managed bean
o identyfikatorze categoryListUI (pod tą nazwą będzie on widzialny dla komponentów UI). Jako klasę beana wskazałem omawianą wcześniej klasę mw.toystore.web.ui.CategoryListUI . Kolejna właściwość określa, że bean będzie przesyłany w obiekcie żądania.
Na uwagę zasługuje sekcja:

<managed-property>
<property-name>productManager</property-name>
<value>#{productManager}</value>
</managed-property>

Jak pamiętamy, klasa naszego beana ma zdefiniowaną właściwość o nazwie propertyManager, oraz dwie metody dostępowe do niej. W tym momencie zostają one wykorzystane przez framework do ich inicjalizacji. W kontekście Springa odszukany zostaje obiekt o logicznej nazwie
productManager ( id="productManager"), a następnie jego wartość zostaje wstrzyknięta do naszego beana za pomocą wywołania metody setProductManager().

Nasz bean został spropagowany, jest dostępny dla stron JSP, teraz możemy przejść do tworzenia strony JSP, na której umieszczony zostanie komponent interfejsu użytkownika, który skorzysta z danych zwróconych z naszego beana i zrenderuje dla nas stosowną dla nich tabelę.

Strona z komponentem UI - categoryList.jsp

Na początek w katalogu głównym naszej aplikacji zakładam plik cateogoryList.jsp. Chciałbym aby komponent JSF umieszczony na niej wykorzystywał zdefiniowany wcześniej managed bean i wyświetlał nazwy wszystkich kategorii. A więc potrzebna będzie tabela.

Z początku nie wiedziałem jak się zabrać za jej tworzenie ale z pomocą przyszło google i artykuł Jacka Laskowskiego poświęcony tabelom w JSF. Znalazłem tam sczególowy opis znacznika <h:dataTable>

Aby uniknąć problemów z wyświetlaniem polskich znaczków rozpoczynam od ustawienia kodowania na utf-8:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>

Na początek definicja bibliotek znaczników związanych z wykorzystaniem elementów JSF:

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

Ponieważ będzie ona potrzebna na wszystkich stronach wykorzystujących komponenty JSF, wydzieliłem ją do pliku /WEB-INF/jsp/include.jsp i włączam do moich stron za pomocą dyrektywy include:

<%@ include file="/WEB-INF/jsp/include.jsp"%>

Od tej chwili do elementów JSF będę mógł odwołać się przy pomocy schematu "przedrostek_tagliba:nazwa_elementu", gdzie jako przedrostek wystąpi jeden z dwóch zdefiniowanych powyżej (f dla tagów z jsf-core i h dla tagów z jsf-html).

A więc od początku. Na szczycie hierarchii elementów JSF umieszczonych na stronie zawsze musi znaleźć się element f:view>/f:view>. Dopiero w jego wnętrzu mogą zostać umieszczone inne tagi. Definicja naszej najprostszej tabelki będzie wyglądała następująco:

<h:dataTable value="#{categoryListUI.categories}" var="category" border="1" styleClass="listtab"> <h:column> <f:facet name="header"> <f:verbatim>Name</f:verbatim> </f:facet> <h:outputText value="#{category.name}" /> </h:column> </h:dataTable>

Kluczowe znaczenie mają dwa atrybuty ustawiane na samym początku a mianowicie:
  • value - powoduje że lista obiektów, które mają zostać zaprezentowane w tabelce pobrana zostanie z obiektu o nazwie catgoryListUI (nazwa logiczna z pliku faces-config.xml), na podstawie jego własności categories . Tu należałoby wyjaśnić jedną rzecz. Mianowicie bean ten nie musi mieć własności categories, wystarczy, że tak jak w naszym przypadku będzie miał zdefiniowaną metodę getCategories().
  • var - jest to nazwa logiczna obiektu pobranego z kolekcji przy pojedynczej iteracji
Atrybut "styleClass" powoduje że zrenderowana tabelka w kodzie wynikowym przypisana będzie do klasy CSS o nazwie "listtab" (klasę tą zdefiniujemy później, przy itegracji z SiteMeshem).

Definicja pojedynczej kolumny tabeli następuje we wnętrzu tagu <h:column> </h:column>. Nie musimy oprogramowywać samej iteracji - wystarczy definicja nagłówka kolumny (<f:facet name="header"></f:facet name="header">) i wskazania pola z którego przy kolejnej iteracji powinna zostać wyciągnięta wartość wierszowa (<h:outputText value="#{category.name}" />).

A więc definicja naszej najprostszej tabeli wygląda na kompletną. Czas na uruchomienie serwera , a potem umieszczeniem na nim naszej aplikacji.

Po wpisaniu adresu: http://localhost:3333/ToyStoreJSFApp/categoryList.faces

Na ekranie pojawiła się tabelka:

A więc wygląda na to że pierwszy i kluczowy przypadek użycia naszej aplikacji został zrealizowany. Wyświetla ona tabelkę z kategoriami produktów.




ToyStore - sklep z zabawkami ( Sitemesh, JSF, Spring, Hibernate, Oracle Express )- CZĘŚĆ I - warstwa danych i biznesowa

Wstęp
Już pierwszy kontakt z frameworkiem JSF, jaki zyskałem po zrobieniu pierwszej aplikacji (spring+jsf) przekonał mnie, że obrany przeze mnie kierunek jest jak najbardziej właściwy. JSF wydaje się być tym czego od dawna już szukałem - sam framework i biblioteka komponentów, wprowadzają rewolucję do projektowania interfejsów webowych. Koniec z JSTL i iterowaniem po kolekcjach :))).

Zachęcony pierwszym sukcesem postanowiłem przystąpić do realizacji trochę bardziej złożonego projektu.

Według moich założeń za warstwę prezentacji bezpośrednio odpowiadałby JSF udekorowany przy pomocy SiteMesha, do tworzenia obiektów warstwy biznesowej i dao chciałbym użyć Springa, jako ORMa użyłbym Hibernate. Jako bazę relacyjną wybrałem Oracle Express. Dodatkowo Spring wystąpiłby w roli frameworka integracyjnego i głównego "wstrzykiwacza" zależności ;)). Wszystko składaneby było do kupy Antem i deployowane na JBoss-a 4.0.5.

Założenia projektu:
Cała aplikacja realizowałaby kila przypadków użycia z zakresu prezentacji i zarządzania produktów przypisanych do określonych kategorii.


No cóż... moja aplikacja swą złożonością na pewno nikogo nie powali na kolana, ale do prezentacji możliwości frameworków wydaje się całkiem ok.

Środowisko projektowe:

Jak większość moich projektów, tak i ten powstanie w Eclipse dozbrojonym przez Hibernate Tools, WTP i JBoss IDE. Projekt będzie się nazywał MWToyStoreJSFApp i będzie miał następującą strukturę:


Kolejny krok to import i kompletowanie wszystkich potrzebnych bibliotek. Ich lista w końcu ustaliła się na:
********************
antlr-2.7.6.jar
asm-attrs.jar,asm.jar
glib-2.1.3.jar, cglib-nodep-2.1_3.jar
commons-beanutils.jar, commons-collections-2.1.1.jar, commons-collections.jar, commons-digester.jar, commons-logging-1.0.4.jar, commons-logging.jar
dom4j-1.6.1.jar
ehcache-1.2.jar
hibernate3.jar
html_basic.tld
jdbc2_0-stdext.jar
jsf-api.jar, jsf-impl.jar
jsf_core.tld, jstl.jar
jta.jar
log4j-1.2.11.jar
ojdbc14.jar
servlet-api.jar
sitemesh-2.3.jar, sitemesh-decorator.tld, sitemesh-page.tld
spring-aspects.jar, spring-mock.jar, spring.jar, standard.jar

********************

Pierwsze planowanie

I w ten oto sposób zrobiłem pierwszy krok - powstał zaczątek mojego projektu. Nadszedł czas tworzenia koncepcji. Na chwilę obecną skupię się na zaplanowanie warstwy danych i biznesowej, JSF ciągle stanowi dla mnie zagadkę, więc tworzeniem modelu i implementacją warstwy web zajmę się w późniejszym etapie realizacji.

Na początek stworzyłem iście imponujący model dziedziny:


Zarządzanie obiektami encji odbywać się będzie za pośrednictwem warstwy danych, na którą w mojej aplikacji złoży się niewielki interfejs IProductManagerDao i implementująca go klasa dostępowa ProductManagerDaoHibernate.


Pomiędzy warstwą danych a web wprowadzę biznesową warstwę pośredniczącą. Tak samo jak w warstwie niższej złoży się na nią jeden interfejs metod biznesowych IProductManager i implementująca go klasa ProductManager.


Tak jak wspomniałem już wcześniej warstwę web muszę potraktować w sposób indywidualny, dlatego na tę chwilę wstrzymam się z projektowaniem i wreszcie zacznę implementację :))).

Przygotowanie bazy danych:
Na samym początku na podstawie modelu dziedziny piszę skrypt inicjalizacji bazy danych:

CREATE TABLE CATEGORY(ID NUMBER NOT NULL PRIMARY KEY,
DESCRIPTION VARCHAR(500),
NAME VARCHAR(50),
LOGO VARCHAR(10));

CREATE TABLE PRODUCT(ID INT NOT NULL PRIMARY KEY,
CATID INT NOT NULL,
DESCRIPTION VARCHAR(500),
PRICE NUMBER(10),
NAME VARCHAR2(50),
LOGO VARCHAR2(50),
CONSTRAINT fk_products_in_cat FOREIGN KEY (CATID)REFERENCES CATEGORY (ID)
);

INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(1,'Ksiazki','Wszystkie ksiazki');
INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(2,'Czasopisma','Dzienniki, miesięczniki');
INSERT INTO CATEGORY(ID,NAME,DESCRIPTION) VALUES(3,'Broszury','Przewodniki turystyczne');

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(1,'Winnetou',1,'Wszystkie trzy tomy',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(2,'Tomek Wilmowski',1,'Wszystkie części powieści przygodowej',50);

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(3,'Motor',2,'Miesięcznik',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(4,'Piłka Nożna',2,'Miesięcznik',10);
INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(5,'Mój Pies',2,'Miesięcznik',10);

INSERT INTO PRODUCT(ID,NAME,CATID,DESCRIPTION,PRICE) VALUES(6,'Przewodnik turystyczny',3,'Broszura',10);

Jeszcze tylko kilka ustawień w moim build. xml, uruchomienie, radość z poprawnego wykonania się skryptu i oto moje tabele znalazły się w bazie danych. Uff.. a więc coś się ruszyło. :).

Tworzenie mapowań ORM (Category.hbm.xml i Product.hbm.xml) obiektów domeny
Mapowania te stworzyłem przy pomocy Hibernate Tools. Na początek, posługując się wizzardem(File->New->Project->Hibernate->Other->Hibernate Configuration File) zdefiniowałem dane potrzebne do ustanowienia połączenie do bazy danych. W wyniku jego działania otrzymałem plik konfiguracyjny hibernate.cfg.xml . Ma on następującą postać:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">oracle.jdbc.driver.OracleDriver
</property>

<property name="hibernate.connection.password">abc</property>
<property name="hibernate.connection.url">jdbc:oracle:thin:@localhost:1521:XE</property>

<property name="hibernate.connection.username">wojcikm</property>
<property name="hibernate.default_schema">WOJCIKM</property>

<property name="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</property>

</session-factory>

</hibernate-configuration>

Po stworzniu pliku konfiguracyjnego przełączam perspektywe na "Hibernate Console"
(Window->Open Perspective->Hibernate Console) , i w jej obrębie otwaram sobie widok Hibernate Configurations (Window->Show View->Hibernate Configurations) .
Teraz w jego obrębie, klikam na prawy przycisk myszy i po otwarciu menu kontekstowego wybieram opcję Add Configuration. Otwiera się wizzard, w którym podaję dane konfiguracji:

Kluczowym krokiem jest podanie "Configuration file" - tu należy wskazać wygenerowany wcześnej plik "hibernate.cfg.xml" .

Po poprawnym zdefiniowaniu połączenia plugin pozwala na przeglądanie obiektów bazy danych.

Po rozwinięciu węzła "Database" powinny pojawić się obiekty DB.

UWAGA!!!
Wiele czasu i nerwów straciłem na dociekanie przyczyn wystąpienia pewnego błędu. Otóż przy próbie rozwinięcia gałązki nie pojawiało się nic.

Okazało się że przyczyna leży w pliku hibernate.cfg.xml, a dokładniej w jego opcji:

<property name="hibernate.default_schema">WOJCIKM</property>


Z niewiadomych mi przyczyn jej wartość musi zostać napisana WIELKIMI LITERAMI.

Po zdefiniowaniu "Console Configuration" czas przystąpić do wygenerowania plików mapowań Hibernate. W tym celu przez wybranie opcji z belki narzędziowej uruchamiam kolejny wizzard:

Po otwarciu się okna neleży wypełnić opcje:
(zakładka Main)

  • Console Confiugration - z listy należy wybrać nazwę ostatnio zdefiniowanej konfiguracji
  • Output Directory - katalog do którego wygenerowane zostaną pliki
  • Package - pakiet do którego należeć będą pliki (mw.toystore.domain)
(zakładka Exporters)
  • Hibernate XML Mappings (.hbm.xml)

Po wciśnięciu przycisku Run do wskazanego katalogu zostaną wygenerowane pliki mapowań.

Plugin tworzy oddzielny plik dla każdej encji db, ja łączę je w jeden duży plik HibernateMapping.hbm.xml i umieszczam w katalogu WEB-INF/context . Plik ten ma następującą postać. Należy pamiętać, że mapowania w tym pliku muszą być całkowicie zgodne z właściwościami obiektów domeny (mw.toystore.domain.*). Obiekty domenowe mogą również zostać wygenerowane przez plugin - jednak ja wolałem zrobić to ręcznie.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="mw.toystore.domain.Category" table="CATEGORY">
<id name="id" column="ID">
<generator class="sequence">
<param name="sequence">WOJCIKM.CATEGORY_ID_SEQ</param>
</generator>
</id>
<property name="description" type="string">
<column name="DESCRIPTION" length="500" />
</property>
<property name="name" type="string">
<column name="NAME" length="50" />
</property>
<property name="logo" type="string">
<column name="LOGO" length="10" />
</property>
<set name="products" inverse="true">
<key>
<column name="CATID" precision="22" scale="0" not-null="true" />
</key>
<one-to-many class="mw.toystore.domain.Product" />
</set>
</class>


<class name="mw.toystore.domain.Product" table="PRODUCT">
<id name="id" column="ID">
<generator class="sequence">
<param name="sequence">WOJCIKM.PRODUCT_ID_SEQ</param>
</generator>
</id>
<many-to-one name="category" class="mw.toystore.domain.Category" fetch="select">
<column name="CATID" precision="22" scale="0" not-null="true" />
</many-to-one>
<property name="description" type="string">
<column name="DESCRIPTION" length="500" />
</property>
<property name="price" type="java.lang.Long">
<column name="PRICE" precision="10" scale="0" />
</property>
<property name="name" type="string">
<column name="NAME" length="50" />
</property>
<property name="logo" type="string">
<column name="LOGO" length="50" />
</property>
</class>
</hibernate-mapping>


Kod obiektów domeny zgodny z powyższym mapowaniem wygląda następująco:




Realizacja warstwy biznesowej - dostępu do danych (dao)
Kod metod klasy realizującej operacje na obiektach domeny - mw.toystore.db.dao.ProductManagerDaoHibernate (klasa ta implementuje interfejs mw.toystore.db.dao.IProductManagerDao) ma ma następującą postać (kod niekompletny):

package mw.toystore.db.dao;

import mw.toystore.domain.Product;
import mw.toystore.domain.Category;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.hibernate.*;

/**
* @created 18-sie-2006 11:49:42
* @version 1.0
*/
public class ProductManagerDaoHibernate extends HibernateDaoSupport implements
IProductManagerDao {

private Log logger = LogFactory.getLog(getClass());
public List getCategoryList() {
return getHibernateTemplate().find("from Category");
}

public Category getCategoryById(int id) {
return (Category) getHibernateTemplate().get(Category.class,
new Integer(id));
}

public void saveCategory(Category cat) {
getHibernateTemplate().saveOrUpdate(cat);
}

public void removeCategory(Category cat) {
Category c=this.getCategoryById(cat.getId().intValue());
this.getHibernateTemplate().delete(c);
}
....
}



Zadaniem metod których kod zamieściłem powyżej jest manipulacja obiektami domenowymi. Klasa ta jest wydziedziczona z klasy HibernateDaoSupport pochodzącej ze Springa. Cecha ta powoduje że w kodzie metod można wykorzystywać springowe wsparcie dla frameworka Hibernate. Powoduje to daleko idące uproszczenie kodu metod. Nice !!!! :))


Realizacja warstwy biznesowej
Tak jak wspominałem wcześniej logika biznesowa jest realizowana przez klasę mw.toystore.ProductManager która implementuje interfejs mw.toystore.IProductManager.
Fragment jej kodu wygląda następująco:

public class ProductManager implements IProductManager {

private Log logger = LogFactory.getLog(getClass());
private IProductManagerDao productManager;

public Product getProductById(int id){
return productManager.getProductById(id);
}

public void saveProduct(Product p){
this.productManager.saveProduct(p);
}
...

}


W moim przypadku znaczenie tej warstwy jest niewielkie. Kod metod odwołuje się do adekwatnych metod z warstwy Dao za pośrednictwem interfejsu IProductManagerDao.

Nie muszę martwić się o prawidłową jego inicjalizacje. O to zadba framework IoC - czyli Spring(ale o tym za chwilę ) :).

Konfiguracja kontekstu springowego
Kontrolę nad poprawną inicjalizacją obiektów sprawuje kontroler IoC (Inversion of Control), czyli w przypadku mojej aplikacji framework Spring. Aby mógł od dobrze wykonywać swoją pracę musi on zostać gruntownie poinformowany o wszystkich obiektach aplikacji i wzajemnych relacjach, które pomiędzy nimi zachodzą.

Pliki konfiguracyjne kontenera IoC umieściłem w podkatalogu /WEB-INF/contex. Przy inicjalizacji całej aplikacji automatycznie wczytywany jest plik /WEB-INF/applicationContext.xml (nazwa musi być ustandaryzowana !!!!) w nim to zawarte są odwołania do kilku plików z podkatalogu /WEB-INF/context.

Wymagane jest istnienie tylko pliku /WEB-INF/applicationContext.xml, mogłyby znaleźć się tu wszystkie mapowania jednak w celu łatwiejszego zapanowania nad konfiguracją podzieliłem ją na pliki:

  • /WEB-INF/applicationContext.xml
  • /WEB-INF/context/springapp-servlet-datasources.xml
  • /WEB-INF/context/springapp-servlet-db.xml
  • /WEB-INF/context/HibernateMapping.hbm.xml
  • /WEB-INF/context/springapp-servlet-bus.xml

Plik applicationContext.xml

Plik springapp-servlet-datasources.xml

<!--
*********************************************************
************* Warstwa konfiguracji źródeł danych
*********************************************************
-->

<beans>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>oracle.jdbc.driver.OracleDriver</value>
</property>
<property name="url">
<value>jdbc:oracle:thin:@localhost:1521:XE</value>
</property>
<property name="username">
<value>wojcikm</value>
</property>
<property name="password">
<value>abc</value>
</property>
</bean>
</beans>

W pliku tym jest zdefiniowany tylko jeden bean "dataSource" - w jego właściwościach podane są podstawowe informacje, które pozwalają na nawiązanie połączenia z bazą danych.

Plik springapp-servlet-db.xml
Spring oferuje szereg obiektów wspomagających realizację odwzorowań obiektowo relacyjnych. Aby mogły być one poprawnie zainicjalizowane musi zostać zdefinowanych szereg parametrów.

<!--
*********************************************************
************* Warstwa odwzorowań obiektowo-relacyjnych
*********************************************************
-->

<beans>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.Oracle9Dialect
</prop>
</props>
</property>
</bean>

<bean id="productManagerDaoHibernate" class="mw.toystore.db.dao.ProductManagerDaoHibernate">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
</beans>

Następujący fragment pliku:

<property name="dataSource">

<ref bean="dataSource" />
</property>


stanowi sztandarowy przykład kofiguracji wstrzyknięcia IoC. Bean "sessionFactory" (którego definicji akurat nie musimy tworzyć bo pochodzi ze standardowych bibliotek springowych) posiada właściwość (property) o nazwie
"dataSource" (w jego kodzie MUSZĄ zostać zdefiniowane metody setDataSource() i getDataSource() - zgodnie ze specyfikacją JavaBean).
Jako wartość
podawana jest referencja do innego beana- a mianowicie omawianego wcześniej, zdefiniowanego w pliku springapp-servlet-datasources.xml beana "dataSource" .
Na podstawie omawianych wpisów konfiguracyjnych kontener IoC, stworzy najpierw beana "dataSource", a następnie beana "sessionFactory". Następnie na obiekcie "sessionFactory" wywołana będzie metoda "setDataSource()" a jako jej parametr przekazany zostanie obiekt "dataSource". Od tego momentu programista może w dowolny sposób używać obiektu "sessionFactory" - wartość jego właściwości dataSource nie jest już równa null - została ona zainicjalizowana.

Należy zwrócić szczególną uwagę na ustawienie właściwej wartości dla property "mappingLocations"

<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>


Za jej pośrednictwem informujemy Springa gdzie znajdują się wygenerowane przez nas wcześniej mapowania obiektowo relacyjne dla istniejących obiektów dziedzinowych.

Plik springapp-servlet-bus.xml

definicja zależności pomiędzy obiektami warstwy biznesowej.
<!--
*************************************************
************* Warstwa usług biznesowych ********
*************************************************
-->
<beans>
<bean id="productManager" class="mw.toystore.bus.ProductManager">
<property name="productManager">
<ref bean="productManagerDaoHibernate" />
</property>
</bean>
</beans>

Beany zdefiniowane w tym pliku odpowiadają za realizację logiki biznesowej. Tworzony jest bean "productManager" będący obiektem klasy
"mw.toystore.bus.ProductManager". Bean ten ma zdefiniowaną właściwość "productManager" (i odpowiednie metody dostępowe). Właściwość ta zostanie zainicjalizowana obiektem "productManagerDaoHibernate" , który zdefiniowany został już wcześcniej. Od tej pory programiści którzy będą wykorzystywać obiekt "productManager" będą mogli odnoscić się niżej, do metod interfejsu DAO implementowanego przez beana "productManagerDaoHibernate".
W kodzie klasy deklaracja właściwości i jej metod dostępowych wygląda następująco (należy zwrócić uwagę na fakt, że do poprawnego wstrzyknięcia przez Springa własności "
productManager" konieczne są wszystkie elementy przedstawione poniżej ):

public class ProductManager implements IProductManager {
private IProductManagerDao productManager;

public IProductManagerDao getProductManager(){
return productManager;
}

public void setProductManager(IProductManagerDao newVal){
productManager = newVal;
}
......
}


Plik HibernateMapping.hbm.xml
Nie jest to plik bezpośrednio związany ze Springiem (choć pośrednio na pewno tak) - znajduje się w nim mapowania obiektów bazy danych na obiekty dziedziny. Struktura, rola i zawartość tego pliku opisałem wcześniej, więc nie ma potrzeby zatrzymywania się przy nim w tej chwili.

Test poprawności
A więc podsumowując w chwili obecnej mamy stworzone dwie warstwy aplikacji - biznesową i danych. Stworzony zostały mapowania Hibernate i konfiguracja kontenera IoC. Kompilacja przebiega bez błędów - czas więc przekonać się czy wszystkie mapowania zostały stworzone prawidłowo, czy aplikacja prawidłowo odwołuje się do bazy i pobiera z niej dane.
W tym celu napisałem niewielką aplikację , która za pośrednictwem fabryki Springa odnosi się do interfejsu warstwy biznesowej "IProductManager" , wywołuje na nim metodę, która sięga do warstwy DAO i pobiera obiekty reprezentujące dane przechowywane w DB. A więc do roboty.

Na początku należy przeprowadzić niewielkie czynności przygotowawcze. Najważniejsza z nich wynika z faktu, że nasza aplikacja nie będzie uruchamiana na serwerze aplikacji. Nie ma więc szans wiedzieć czegokolwiek na temat istnienia katalogów "/WEB-INF" i " /WEB-INF/context" ,
a więc miejsc w których przechowywane są pliki konfiguracyjne kluczowych bibliotek: kontenera IoC (Spring) i bibliotek Hibernate. Aby zmienić ten jakże niepożądany stan wszystkie pliki konfiguracyjne muszą znaleźć się w katalogu dopisanego do classpath. W pewnym uproszczeniu (przy założeniu że nasza aplikacja będzie uruchamiana tylko za pośrednictwem IDE) można przyjąć że pliki te powinny znaleźć się w katalogu "src".

Druga rzecz, którą należy wykonać to w skopiowanym (a więc leżącym w katalogu "src"!!!) pliku należy zmienić ścieżkę do pliku z mapowaniami Hibernate z:

<property name="mappingLocations">
<value>/WEB-INF/context/HibernateMapping.hbm.xml</value>
</property>

na

<property name="mappingLocations">
<value>HibernateMapping.hbm.xml</value>
</property>


I to w zasadzie wszystko. Teraz już można zabrać się za pisanie aplikacji.

Niech aplikacja nazywa się "Main" i leży w pakiecie "mw.toystore".

Na początek importy, które zapewnią nam dostęp do obiektów domeny, obiektów fabryki Springa i obiektów warstwy biznesowej:

import java.util.*;
import mw.toystore.domain.*;

import org.springframework.beans.factory.*;
import org.springframework.context.support.*;
import org.springframework.context.*;
import mw.toystore.bus.*;

W pierwszym kroku, w kodzie aplikacji tworzę obiekt kontekstu aplikacji. Przechowuje on informacje o powiązaniach i zależnościach między mapowanymi obiektami. Jako parametr pobiera on nazwy plików, które zawierają mapowania.

ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"springapp-servlet-datasources.xml", "springapp-servlet-db.xml", "springapp-servlet-bus.xml"});

Następnie obiekt ten rzutuję na interfejs umożliwiający bezpośredni dostęp do fabryki beanów:

BeanFactory factory = (BeanFactory) context;

Teraz nic nie stoi na przeszkodzie by poprosić o beana z warstwy biznesowej:

IProductManager p=(IProductManager)factory.getBean("productManager");

Następnie wywołuję na nim metodę pobierającą kolekcję kategorii:

List catList=p.getCategoryList();

Następnie iteruję i próbuję wyświetlić nazwę każdej kategorii w kolekcji:

Iterator itcat=catList.iterator();
while(itcat.hasNext()){
Category c=(Category)itcat.next();
System.out.println(c.getName());

}

I to w zasadzie wszystko. Po uruchomieniu aplikacji na ekran wyrzucony został wynik:

"
....
Czasopisma
Broszury
....
"

Brak pojawienia się wyjątku świadczy o tym że mapowania wykonane zostały prawidłowo i o tym, że kontener springa również został skonfigurowany poprawnie.

Zdaję sobie sprawę, że powyższy test był bardzo pobieżny i niezgodny ze sztuką, ale myślę że przy założeniu że piszemy aplikację - tutorial można uznać go za całkowicie wystarczający.

Uff a więc przebiliśmy się przez dużą część materiału. Można śmiało powiedzieć, że nasza aplikacja DZIAŁA ALE NIE WYGLĄDA. Aby wyglądała należy wzbogacić ją o warstwę web i uruchomić na serwerze aplikacji - ale o tym w następnej części.....