Wydział Matematyki i Informatyki Uniwersytetu Łódzkiego www.math.uni.lodz.pl/ radmat radmat@math.uni.lodz.pl
Konwersja i walidacja W wyniku działania konwersji i walidacji surowe dane, przesyłane w postaci żądania HTTP, przekształcane są na logiczne struktury danych występujące w Javie. Następnie sprawdzane są pod kątem poprawności. Dzięki temu możemy zapomnieć o wielu pojawiających się problemach, które ujawniają się w przypadku innych technologii webowych. JSF zadba za nas niemal o wszystko. Procesy konwertowania i walidacji stanowią istotną część drugiej i trzeciej fazy cyklu życia aplikacji JSF. Po wykonaniu pierwszego kroku JSF wie, czy żądanie przesłane przez użytkownika zostało zrealizowane po raz pierwszy w obrębie danej sesji (tzw. initial view), czy też użytkownik korzysta z danego widoku po raz kolejny (tzw. postback). Oczywiście w pierwszym przypadku konwertowanie i walidacja nie występują, ponieważ użytkownik nie miał możliwości przesłania żadnych danych, które podlegałyby konwersji i walidacji.
Konwertowanie standardowych wartości jest procesem w dużym stopniu zautomatyzowanym. Wszystkie prymitywne typy danych, takie jak int, double, itp. mogą być bez problemu konwertowane do typu String i odwrotnie. Dzięki temu wartość typu double zapisana np. w polu tekstowym zostanie wyświetlona poprawnie, natomiast po zmianie tej wartości przez użytkownika, Java spróbuje dokonać konwersji łańcucha znaków na liczbę. Jeśli operacja ta przebiegnie poprawnie, to wartość pola zostanie przekazana dalej do walidacji. Problemy zaczynają się, gdy istnieje potrzeba skorzystania z klasy, która nie może być automatycznie konwertowana na typ String. W tej sytuacji konieczne staje się napisanie własnego konwertera.
Konwertery standardowe W zdecydowanej większości typowych przypadków konwersja realizowana jest za pomocą standardowych konwerterów. Programiści firmy Sun udostępnili standardowe konwertety dla wartości typów prymitywnych: 1 Liczb całkowitych IntegerConverter LongConverter ShortConverter ByteConverter 2 Liczb zmiennoprzecinkowych DoubleConverter FloatConverter 3 Znaków CharacterConverter 4 Wartości logicznych BooleanConverter
Oprócz wymienionych powyżej konwerterów typów prymitywnych otrzymujemy dwa dodatkowe: BigIntegerConverter - dzięki któremu można wykonywać operacje na liczbach całkowitych o dowolnej długości BigDecimalConverter - dzięki któremu można wykonywać operacje na liczbach zmiennoprzecinkowych o dowolnej długości
NumberConverter - rozbudowany konwerter liczb Nie w każdym przypadku konwersji liczb wystarczą nam powyższe konwertery. Trochę więcej możliowści konwersji daje klasa NumberConverter. <h:inputtext id="info" value="#{ziarno.kwota}"> <f:convertnumber type="currency" /> </h:inputtext>
Najważniejsze atrybuty: minfractiondigits - minimalna liczba cyfr umieszczonych po przecinku maxfractiondigits - maksymalna liczba cyfr umieszczonych po przecinku minintegerdigits - minimalna liczba cyfr części ułamkowej maxintegerdigits - maksymalna liczba cyfr części ułamkowej integeronly - określa, czy powinna być zwracana jedynie część całkowita liczby type - określa typ konwertowanej liczby. Dozwolonymi wartościami są: percent (procent), currency (kwota pieniężna), number (liczba) pattern - określa wzorzec, jaki musi spełniać konwertowany łańcuch znaków. Wzorzec ten musi spełniać reguły określone w java.text.decimalformat.
Zauważmy, że w zależności od przekazanej wartości i ustawień niektórych atrybutów, konwerter może zwrócić obiekt typu Long lub Double. Oba typy dziedziczą po klasie Number, dlatego to właśnie atrybuty tej klasy powinny być deklarowane w klasie ziaren zarządzanych.
DataTimeConverter - konwerter daty i czasu Data i czas - analogicznie jak w przypadku wcześniejszych technologii - reprezentowane są w JSF przez klasę java.util.date. W przeciwieństwie do pozostałych klas konwerterów standardowych, konwerter DataTimeConverter nie zawsze zadziała zgodnie z naszymi oczekiwaniami! Związane jest to z możliwością różnego zapisu daty i czasu. Niezbędne okazuje się wówczas jawne określenie konwertera, opcjonalnie z dodatkowymi argumentami. Można to uczynić w następujący sposób: <h:inputtext id="info" value="#{ziarno.czas}"> <f:convertdatatime type="time" /> </h:inputtext>
Ten prosty zapis pozwala na uzyskanie oczekiwanego przez nas efektu. Oczywiście kontrola nad konwertowaniem daty i czasu może być bardziej wyrafinowana, dzięki zastosowaniu atrybutów: type - określa składowe, jakie mogą wystąpić w obrębie zmiennej typu Date. Dozwolonymi wartościami są date, time i both. datastyle - określa jeden z predefiniowanych formatów daty, jaki może przyjąć zmienna. Dozwolonymi wartościami są default, short, medium, long, full. timestyle - określa jeden z predefiniowanych formatów czasu, jaki może przyjąć zmienna. Dozwolone wartości są takie same, jak w przypadku datastyle. pattern - określa dowolny schemat daty i czasu, zgodny ze specyfikacją akceptowaną przez klasę java.text.simpledataformat. Np. wzorcowi h:mm odpowiadają m.in. 02:18, 09:30, czy 18:00.
EnumConverter - konwerter typu wyliczeniowego Typy wyliczeniowe rzadko występują w aplikacjach webowych. Twórcy takich aplikacji często bowiem zamiast tradycyjnego typu wyliczeniowego stosują zwykłe łańcuchy znaków. Zauważmy bowiem, że typy wyliczeniowe są de facto łańcuchami znaków. W związku z tym konwerter typu wyliczeniowego EnumCoverter nie sprawia tylu kłopotów, ile konwerter daty i czasu. Koniecznie trzeba zwrócić uwagę w jaki sposób należy wprowadzać wartości tekstowe, aby te zostały dopasowane do typu wyliczeniowego. Oczywiście należy wprowadzać je dokładnie, trzeba zachować wielkości znaków i nie stosować białych znaków. Za bezpośrednią operację konwersji tekstu do typu wyliczeniowego odpowiedzielna jest metoda Enum.ValueOf().
Przykład konwertera Wbrew pozorom napisanie własnego konwertera nie jest skomplikowane. Musimy jedynie obsłużyć dwie kluczowe operacje, czyli konwersję wprowadzonego do aplikacji typu do łańcucha znaków oraz operację odwrotną. Po utworzeniu klasy musimy jedynie dodać odpowiednie wpisy w pliku faces-config.xml. Klasa konwertera musi implementować interfejs javax.faces.convert.converter. Dzięki temu interfejsowi można zapomnieć o interakcji z JSF. Można zaimplementować następujące metody: Object getasobject(facescontext kontekst, UIComponent komponent, String wartość) - metoda ta zwraca obiekt przekonwertowany z podanej wartości tekstowej. Komponent określa skąd została pobrana wartość. String getasstring(facescontext kontekst, UIComponent komponent, Object wartosc) - metoda ta zwraca łańcuch znaków, utworzony na podstawie przekazanego obiektu.
Walidator Wszystkie walidatory muszą implementować interfejs javax.faces.validator.validator. W tej klasie implementuje się jedną metodę public void validate (FacesContext kontekst, UIComponent komponent, Object wartość), która poddaje walidacji przekazaną wartość uzyskaną z danego komponentu, w aplikacji o podanym kontekście. W przypadku błędu, metoda powinna wyrzucić wyjątek ValidatorException.
Walidacja liczb Walidatory DoubleRangeValidator i LongRangeValidator odpowiadają za walidację liczb, które mieszczą się w zakresie podanym w atrybutach minimum i maximum. <h:inputtext id="wiek" value="#{osoba.wiek}"> <f:validatelongrange minimum="1" maximum="110" /> </h:inputtext>
Walidacja łańcuchów znaków Za walidację łańcuchów znaków odpowiada klasa LengthValidator. Udostępnia - podobnie jak walidatory liczb - atrybuty minimum i maximum, które określają minimalną i maksymalną długość łańcucha znaków, który podlega walidacji: <h:inputtext id="imie" value="#{osoba.imie}"> <f:validatelength minimum="3" maximum="15" /> </h:inputtext>
Jeden z podstawowych mechanizmów walidacji może polegać na sprawdzaniu, czy wartość danego pola została wypełniona. JSF daje taką możliwość, ale nie za pomocą osobnego walidatora. Wystarczy bowiem skorzystać z atrybutów required i requiredmessage komponentów wejścia. Pierwszy z nich określa, czy wprowadzenie wartości w danym polu jest obowiązkowe. Drugi natomiast zawiera informację, która zostanie wyświetlona w przypadku, gdy pole nie zostanie wypełnione: <h:inputtext id="imie" value="#{osoba.imie}" required="true" requiredmessage="nie podano imienia." />
Przykład konwertera i walidatora Napiszemy teraz klasę, której zadanie będzie polegało na konwertowaniu oddzielonych spacjami liczb (wprowadzanymi z pól tekstowych) do listy liczb całkowitych i odwrotnie. Taki konwerter wykorzystamy do obliczania sumy dowolnej ilość liczb.
Klasa Kalkulator.java public class Kalkulator { private int liczba1; private int liczba2; private int suma; private ListaLiczb liczby; public int getliczba1() { return liczba1; } public void setliczba1(int liczba1) { this.liczba1=liczba1; }
public int getliczba2() { return liczba2; } public void setliczba2(int liczba2) { this.liczba2=liczba2; } public int getsuma() { return suma; } public String oblicz() { this.suma=this.getliczba1()+this.getliczba2(); return null; }
public ListaLiczb getliczby() { return liczby; } public void setliczby(listaliczb liczby) { this.liczby=liczby; } } public String obliczmulti() { this.suma=this.getliczby().getsuma(); return null; }
Klasa ListaLiczb.java import java.util.list; public class ListaLiczb { private List<Integer> lista; public List<Integer> getlista() { return lista; } public void setlista(list<integer> lista) { this.lista=lista; }
} public int getsuma() { if(lista==null) return 0; int suma=0; for(integer i:this.lista) suma+=i; return suma; }
Klasa KonwerterLiczbJava import java.util.arraylist; import java.util.list; import javax.faces.application.facesmessage; import javax.faces.component.uicomponent; import javax.faces.context.facescontext; import javax.faces.convert.converterexception;
public class KonwerterLiczb implements javax.faces.convert.converter { public String getasstring(facescontext kontekst, UIComponent komponent, Object wartosc) { if(!(wartosc instanceof ListaLiczb)) throw new ConverterException (new FacesMessage("Blad konwersji.")); } List<Integer> lista =((ListaLiczb)wartosc).getLista(); String wynik=""; for(integer i:lista) wynik+=(i+" "); return wynik;
public Object getasobject(facescontext kontekst, UIComponent komponent, String wartosc) { String[] liczby=wartosc.split(" "); List<Integer> lista=new ArrayList<Integer>(); for(string s:liczby) { try { Integer i=integer.valueof(s); lista.add(i); } catch(numberformatexception nfe) { throw new ConverterException (new FacesMessage("Blad konwersji.")); } }
} } ListaLiczb ll=new ListaLiczb(); ll.setlista(lista); return ll;
Plik index.xhtml <?xml version= 1.0 encoding= UTF-8?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/tr/xhtml1/dtd/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <title>kalkulator</title> </h:head>
<h:body> <h:form id="formularz"> <h:outputlabel for="formularz:liczba1"> Liczba 1:</h:outputLabel> <h:inputtext id="liczba1" value="#{kalkulator.liczba1}" required="true" requiredmessage="podaj 1 liczbe" /> <br />
<h:outputlabel for="formularz:liczba2"> Liczba 2:</h:outputLabel> <h:inputtext id="liczba2" value="#{kalkulator.liczba2}" required="true" requiredmessage="podaj 2 liczbe" /> <br /> <h:outputlabel for="formularz:liczby"> Liczby:</h:outputLabel> <h:inputtext id="liczby" value="#{kalkulator.liczby}" required="true" requiredmessage="podaj liczby" /> <br />
<h:commandbutton value="oblicz" action="#{kalkulator.oblicz}" /> <h:commandbutton value="oblicz sumę" action="#{kalkulator.obliczmulti}" /> <br /> <h:outputtext value="suma: #{kalkulator.suma}" /> </h:form> </h:body> </html>
Plik index.xhtml z własnym walidatorem <h:outputlabel for="formularz:liczba1"> Liczba 1:</h:outputLabel> <h:inputtext id="liczba1" value="#{kalkulator.liczba1}" /> <br /> <h:outputlabel for="formularz:liczba2"> Liczba 2:</h:outputLabel> <h:inputtext id="liczba2" value="#{kalkulator.liczba2}" /> <br /> <h:outputlabel for="formularz:liczby"> Liczby:</h:outputLabel>
<h:inputtext id="liczby" value="#{kalkulator.liczby}" > <f:validator id="walidator" validatorid="walidatorliczb"> /> </h:inputtext> <br /> <h:commandbutton value="oblicz" action="#{kalkulator.oblicz}" /> <h:commandbutton value="oblicz sumę" action="#{kalkulator.obliczmulti}" /> <br /> <h:outputtext value="suma: #{kalkulator.suma}" /> </h:form> </h:body> </html>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_1.x version="2.1"> <managed-bean> <managed-bean-name>kalkulator</managed-bean-name> <managed-bean-class> pl.helion.jeeweb.fazyaplikacjijsf.kalkulator </managed-bean-class> <managed-bean-scope>request</managed-bean-scope> </managed-bean>
<converter> <converter-for-class> pl.helion.jeeweb.fazyaplikacjijsf.listaliczb </converter-for-class> <converter-class> pl.helion.jeeweb.fazyaplikacjijsf.konwerterlicz </converter-class> </converter> </faces-config>
JPA W przypadku wielu aplikacji internetowych źródłem danych jest baza danych, działająca w obrębie wybranego serwera bazodanowego, który najczęściej funkcjonuje w architekturze klient-serwer. Z reguły dostęp do bazy danych odbywa się za pomocą języka zapytań SQL, dzięki któremu możliwe jest tworzenie baz danych i wszystkich elementów wchodzących w jej skład, czyli tabel, widoków, itp., a także oczywiście wypełnianie bazy danymi. Język SQL (w przeciwieństwie do języków programowania takich jak np. C++, czy Java) występuje w różnych odmianach, w zależności od serwera bazodanowego, w którym został zaimplementowany. Można m.in. wyróżnić następujące języki, wywodzące się od standardu SQL: T-SQL (Microsoft SQL Server) PL/SQL (Oracle) PL/pgSQL (PostreSQL) MySQL
Różnice te sprawiają, że tworzenie aplikacji z wykorzystaniem jednego standardu języka SQL trzeba pisać z myślą o konkretnej implementacji języka SQL. Problem ten został dostrzeżony przez programistów Javy. Jego rozwiązanie polega na wykorzystaniu technologii JDBC - Java DataBase Connectivity. Technologia ta pełni funkcję interfejsu między aplikacjami tworzonymi w języku Java, a konkretnymi implementacjami baz danych SQL. Dzięki zastosowaniu JDBC można tworzyć aplikacje używając jednolitego zestawu klas, niezależnie od zastosowanej implementacji. Szczegóły połączenia z bazą danych mogą być przechowywane w jednym miejscu aplikacji, dzięki czemu ewentualne zmiany ustawień nie stanowią problemu.
Mimo ustandaryzowania sposobu połączenia i komunikacji z bazą danych, zapytania SQL nadal pozostawały zależne od konkretnej bazy danych. W przypadku rozbudowanych aplikacji webowych bardzo kłopotliwe było wprowadzanie zmian do klas korzystających z baz danych. Natomiast sam kod był bardzo nieprzejrzysty.
ORM ORM (Object-Relational Mapping) - mapowanie obiektowo-relacyjne. Zadaniem tego mechanizmu jest odwzorowywanie obiektów istniejących w obrębie języków programowania i powiązań między tymi obiektami na relacje istniejące w relacyjnych bazach danych. Zasadę działania mechanizmu ORM można podzielić na etapy: 1 Przygotowania - polegają na zestawieniu połączenia z bazą danych oraz utworzeniu odwzorowań pomiędzy obiektami, a relacjami występującymi w bazie danych. 2 Właściwe operacje - na tym etapie wykonujemy żądane operacje w języku SQL na danych.
Hibernate Hibernate jest najpopularniejszą implementacją odwzorowania ORM dla języka Java. Założeniem jego twórców było zaoferowanie rozwiązania określanego po angielsku jako Relational Persistence For Idiomatic Java, co oznacza zapewnienie trwałości obiektów ze wsparciem dla wszystkich mechanizmów obiektowych języka Java, takich jak obsługa asocjacji, kompozycji, dziedziczenia, polimorfizmu i kolekcji. Hibernate cechuje wysoka wydajność i skalowalność oraz wiele możliwości wydawania zapytań do bazy danych. Jest rozwiązaniem kategorii professional open source. Z jednej strony jego źródła są dostępne, a z drugiej jest on rozwijany przez firmę JBoss Inc., będącą znaczącym graczem na rynku serwerów aplikacj.
Architektura Hibernate: Rysunek: Architektura Hibernate
Stany obiektu w Hibernate 1 Ulotny (ang. transient) utworzony operatorem new, ale niezwiązany z sesją 2 Trwały (ang. persistent) posiada identyfikator i reprezentację w bazie danych związany z sesją 3 Odłączony (ang. detached) obiekt, który był trwały, ale jego sesja się zakończyła można go modyfikować, a następnie związać z nową sesją
Obsługa trwałości obiektów w aplikacjach opartych o Hibernate polega głównie na wywoływaniu odpowiednich metod na rzecz obiektu Session. Do uczynienia obiektu trwałym służy metoda save() obiektu Session. Odczyt obiektu o znanym identyfikatorze umożliwiają bardzo podobne do siebie metody load() i get() obiektu Session, natomiast do usuwania reprezentacji obiektu z bazy danych służy metoda delete() obiektu Session.
Klasy trwałe public class Osoba { private Long ID; private String nazwisko; public void setid(long ID) { this.id=id; } public Long getid() { return ID; } public void setnazwisko(string nazwisko) { this.nazwisko=nazwisko; } public String getnazwisko() { return nazwisko; } }
Trwałe klasy to klasy implementujące encje występujące w modelu danych aplikacji. Nie wszystkie instancje trwałej klasy muszą być trwałe. Hibernate najlepiej działa z klasami spełniającymi reguły POJO - Plain Old Java Object. Klasy POJO dla Hibernate muszą posiadać metody set / get dla trwałych pól i bezargumentowy konstruktor. Zalecane jest, aby klasa posiadała wyróżniony identyfikator (najlepiej sztuczny) w formie dodatkowego pola w klasie, typu nieprostego.
Odwzorowanie obiektowo-relacyjne w Hibernate jest definiowane w pliku lub plikach XML. Typowo odwzorowanie dla każdej trwałej klasy opisane jest w odrębnym pliku z rozszerzeniem.hbm.xml. Język do opisu odwzorowania jest zorientowany na opis odwzorowania z punktu widzenia klasy Java, a nie tabeli. Dokumenty opisujące odwzorowanie można tworzyć ręcznie lub korzystając z narzędzi. Istnieją generatory tworzące plik odwzorowania na podstawie klasy POJO, a także generujące klasy POJO i pliki odwzorowania dla istniejących tabel w bazie danych. Dokumenty opisujące odwzorowanie wskazywane są w pliku konfiguracyjnym hibernate.cfg.xml, ale mogą też być wskazane programowo w aplikacji.
Praca z obiektami Utworzenie i zachowanie obiektu Osoba o=new Osoba(); o.setnazwisko("kowalski"); Long ID=(Long)session.save(o); Odczyt i modyfikacja obiektu Osoba o=osoba()session.load(osoba.class, new Long(20)) o.setnazwisko("kowalski"); Usuwanie obiektu Osoba o=osoba()session.load(osoba.class, new Long(20)) session.delete(o);
Przykład asocjacji Załóżmy, że mamy dwie klasy trwałe: Wydział i Osoba, będące w relacji 1:N. Plik Osoba.java public class Osoba { private Long id; private String nazwisko; private Wydział w; } Plik Osoba.hbm.xml <class name="osoba" table="osoba">... <many-to-one name="w" column="osoba_id" not-null="true" /> </class>
Plik Wydział.java public class Wydział { private Long id; private String nazwa; private Set os; } Plik Wydział.hbm.xml <class name="wydział" table="wydział">... <set name="os"> <key column="osoba_id" /> <one-to-many class="wydział" /> </set> </class>
Język zapytań HQL Zapytania w HQL (Hibernate Query Language) mają następujące cechy: składnia podobna do języka SQL zorientowany obiektowo zapytania odwołują się do klas, a nie do tabel FROM osoba AS o WHERE o.nazwisko LIKE Kowalski ; Pełna składnia: List osoba=(list)session.createquery( "FROM osoba AS o WHERE o.nazwisko LIKE Kowalski ").list();
Java Persistance Java Persistence to nowy, opracowany razem z EJB 3.0 standard zapewniania trwałości obiektów w aplikacjach Java EE i Java SE, stanowiący część specyfikacji Java EE od wersji 5.0. Został on opracowany razem z EJB 3.0 w odpowiedzi na niepowodzenie lansowanej do tej pory koncepcji encyjnych EJB i niewątpliwy sukces technologii odwzorowania obiektowo-relacyjnego takich jak Hibernate czy Oracle Toplink. Technologie te, mimo że oparte o te same idee, różnią się jeśli chodzi o API. Standard Java Persistence jest oparty o odwzorowanie obiektowo-relacyjne i definiuje standardowe API do obsługi trwałości obiektów.
Elementy standardu Java Persistence to: Interfejs programistyczny Java Persistence API, obejmujący interfejs do zarządcy trwałości EntityManager; Język zapytań Java Persistence Query Language (JPQL), o składni przypominającej SQL, umożliwiający tworzenie przenaszalnych zapytań; Metadane o odwzorowaniu obiektowo-relacyjnym, najczęściej umieszczone w kodzie w formie adnotacji, z możliwością dodatkowej konfiguracji w środowisku produkcyjnym poprzez XML-owe pliki konfiguracyjne.
Encje @Entity public class Blad implements Serializable { @id private Long id; private String opis; public Blad() {}; public Long getid() {return id;} public void setid (Long id) {this.id=id;} public String getopis() {return opis;} public void setopis (String opis) { this.opis=opis; } }
Zapytania do bazy danych 1 Rodzaje zapytań (metody obiektu EntityManager): dynamiczne w JPQL - createquery() dynamiczne natywne - createnativequery() zapewniają lepszą optymalizację nazwane (JPQL lub natywne) - createnamedquery() 2 Wykonanie zapytania (metody obiektu Query): getresultlist() - zwraca wyniki zapytania jako kolekcję typu List getsingleresult() - służy do odczytu pojedynczego wyniku zapytania w postaci obiektu Object executeupdate() - służy do wykonania polecenia UPDATE lub DELETE
Zapytanie dynamiczne EntityManager em;... List<Blad> wynik=null; Query q=em.createquery("select b FROM Blad WHERE b.opis LIKE %problem % "); wynik=q.getresultlist();
Zapytanie nazwane Plik Blad.java: @Entity @NamedQuery(nazwa="szukaj", query= "SELECT b FROM Blad b WHERE b.opis LIKE:klucz")... EntityManager em;... <List> Blad wynik=null; wynik=em.createnamequery("szukaj").setparameter( "klucz","%krytyczny%").getresultlist();