Latest Entries »

Saturday, November 7, 2009

Niestandardowa konwersja i weryfikacja JavaServer Faces

Po poznaniu podstawowych konwerterów i weryfikatorów JSF-ów możemy przystąpić do trudniejszego zadania.
Jak wiadomo wszystko w Java jest obiektem. Właśnie w ten sposób wszystkie informacje są zapisywane na serwerze. Natomiast informacje, które są wyświetlane w oknie przeglądarki muszą być prezentowane jako napisy, listy i obrazy. Powoduje to potrzebę konwersji tych dwóch rodzajów informacji. Na początku zajmiemy się tworzeniem własnych klas implementujących interfejs javax.faces.convert.Converter, który posiada dwie metody:
public Object getAsObject(FacesContext context, UIComponent component, String value) {...}

public String getAsString(FacesContext context, UIComponent component, Object value) {...}

Pierwsza metoda naszej klasy konwertuje wpisaną wartość tekstową na obiekt dowolnej klasy gdy wysyłamy formularz na serwer, a druga metoda konwertuje wszystkie obiekty na postać łańcuchów i wysyła je do przeglądarki.
Utwórzmy następującą hierarchię klas:
public class ZipCode {
//Klasa reprezentuje kod pocztowy.
private String zip;
public ZipCode(String zip) {
this.zip = zip;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}

@Override
public String toString() {
return zip;
}
}
//Klasa imię użytkownika.
public class Name {
private String name;

Name(String value) {
this.name = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
//Klasa reprezentuje obiektowe nazwisko.
public class Surname {
private String surname;
public Surname(String surname) {
this.surname = surname;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
@Override
public String toString() {
return surname;
}
}
//Nasz Bean, który trzyma dane formularza:
public class RegistrationForm2 {
private Name name = new Name("");
private Surname surname = new Surname("");
private ZipCode zipCode = new ZipCode("");
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;

}
public Surname getSurname() {
return surname;
}
public void setSurname(Surname surname) {
this.surname = surname;
}
public ZipCode getZipCode() {
return zipCode;
}
public void setZipCode(ZipCode zipCode) {
this.zipCode = zipCode;
}
}
Teraz aby implementacja JSF wiedziała co zrobić z naszymi obiektami ZipCode,Name,Surname musimy pomóc i stworzyć odpowiednie klasy konwerterów np: ZipCodeConverter, NameConverter i SurnameConverter. Gdy konwersja się nie powiedzie powinniśmy wystosować odpowiedni wyjątek ConverterException().


//---------------------------------
import java.util.Locale;
import java.util.ResourceBundle;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;


/**
*
* @author riaa
*/
public class NameConverter implements Converter{

//Metoda konwertuje łańcuch z przeglądarki na obiekt typu Name.
public Object getAsObject(FacesContext context, UIComponent component, String value) {
//Jeżeli Name jest puste.
if (value.isEmpty())
{
//Pobieramy ustawienia lokalizacji użytkownika przeglądarki.
Locale L = context.getViewRoot().getLocale();
//Tworzymy obiekt, który ładuje klasy przy pomocy metod klasy Class
//a nie standardowo przez konstruktory.
ClassLoader loader = Thread.currentThread().getContextClassLoader();
//Pobieramy obiekt mymessages.properties przy pomocy
//statycznej metody: ResourceBundle.getBundle
ResourceBundle bundle = ResourceBundle.getBundle(context.getApplication().getMessageBundle(), L, loader);
//Wyszukujemy w pliku napisu o id = javax.faces.component.UIInput.REQUIRED.
String resource = bundle.getString("javax.faces.component.UIInput.REQUIRED");
//Tworzymy obiekt wyświetlany w ice:message zastępując znak {0}
// wartością id danej ice:inputText.
FacesMessage f = new FacesMessage(FacesMessage.SEVERITY_ERROR, resource.replace("{0}", component.getId()),
resource.replace("{0}", component.getId()));
throw new ConverterException(f);
}
//Jeżeli Name zawiera tylko litery.
if (value.matches("[A-Za-z]+"))
return new Name(value);
else
{
//Przypadek gdy ktoś w polu imię podał cyfrę.
//Ten sam sposób na na początku.
Locale L = context.getViewRoot().getLocale();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ResourceBundle bundle = ResourceBundle.getBundle(context.getApplication().getMessageBundle(), L, loader);
String resource = bundle.getString("robpie.Name");
FacesMessage f = new FacesMessage(FacesMessage.SEVERITY_ERROR, resource.replace("{0}", component.getId()),
resource.replace("{0}", component.getId()));
throw new ConverterException(f);
}
}

public String getAsString(FacesContext context, UIComponent component, Object value) {
return value.toString();
}

}
//-----------------------------------------
import java.util.Locale;
import java.util.ResourceBundle;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

/**
*
* @author riaa
*/
public class SurnameConverter implements Converter{

public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value.isEmpty())
{
Locale L = context.getViewRoot().getLocale();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ResourceBundle bundle = ResourceBundle.getBundle(context.getApplication().getMessageBundle(), L, loader);
String resource = bundle.getString("javax.faces.component.UIInput.REQUIRED");
FacesMessage f = new FacesMessage(FacesMessage.SEVERITY_ERROR, resource.replace("{0}", component.getId()),
resource.replace("{0}", component.getId()));
throw new ConverterException(f);
}
if (value.matches("[A-Za-z]"))
{
return new Surname(value);
}
else
{
//Przypadek gdy ktoś w polu imię podał cyfrę.
Locale L = context.getViewRoot().getLocale();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ResourceBundle bundle = ResourceBundle.getBundle(context.getApplication().getMessageBundle(), L, loader);
String resource = bundle.getString("robpie.Surname");
FacesMessage f = new FacesMessage(FacesMessage.SEVERITY_ERROR, resource.replace("{0}", component.getId()),
resource.replace("{0}", component.getId()));
throw new ConverterException(f);
}
}

public String getAsString(FacesContext context, UIComponent component, Object value) {
return value.toString();
}

}
//---------------------------------
import java.util.Locale;
import java.util.ResourceBundle;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

/**
*
* @author riaa
*/
public class ZipCodeConverter implements Converter{

public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value.matches("[0-9][0-9][-][0-9][0-9][0-9]"))
return  new ZipCode(value);
else
{
Locale L = context.getViewRoot().getLocale();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ResourceBundle bundle = ResourceBundle.getBundle(context.getApplication().getMessageBundle(), L, loader);
String resource = bundle.getString("robpie.ZipCode");
FacesMessage f = new FacesMessage(FacesMessage.SEVERITY_ERROR, resource, resource);
throw new ConverterException(f);
}
}

public String getAsString(FacesContext context, UIComponent component, Object value) {
return value.toString();
}
}
Teraz do każdej kontrolki dodajmy znacznik f:converter.:
<h:form prependId="false">
<table>
<tr>
<td>Imię:</td>
<td><ice:inputText id="name" value="#{registrationForm2.name}">
<f:converter converterId="NameConverter" />
<f:validateLength minimum="5" maximum="20" />
</ice:inputText></td>
<td><ice:message for="name" /></td>
</tr>
<tr>
<td>Nazwisko:</td>
<td><ice:inputText id="surname" value="#{registrationForm2.surname}" >
<f:validateLength minimum="5" maximum="20" />
</ice:inputText></td>
<td><ice:message for="surname" /></td>
</tr>
<tr>
<td>Kod pocztowy:</td>
<td><ice:inputText id="zipCode" value="#{registrationForm2.zipCode}" required="true">
<f:converter converterId="ZipConverter" />
</ice:inputText></td>
<td><ice:message for="zipCode" /></td>
</tr>
</table>
<ice:commandButton value="Dalej" action="registration2"/>
</h:form>

A na sam koniec musimy w pliku faces-config.xml dodać znacznik converter z odpowiednimi wpisami:

<converter>
<converter-id>ZipConverter</converter-id>
<converter-class>robpie.ZipCodeConverter</converter-class>
</converter>
<converter>
<converter-id>NameConverter</converter-id>
<converter-class>robpie.NameConverter</converter-class>
</converter>
<converter>
<converter-for-class>robpie.Surname</converter-for-class>
<converter-class>robpie.SurnameConverter</converter-class>
</converter>


Teraz spróbujmy przetestować formularz:

Thursday, November 5, 2009

Konwersja < i > na encje htm'a lt i gt

Dla osób, które często korzystają z SyntaxHightlightera na pewno się przyda prosty program, który zamieni wszystkie znaczniki < > na odpowiednie &lt i &gt.
Pozdrawiam bloggerów.

Konwersja i weryfikacja przy pomocy standardowych kontrolek JSF

Nadszedł czas omówić konwersję i weryfikację danych w aplikacji JavaServer Faces przy użyciu standardowych mechanizmów przygotowanych przez twórców JSF. Przykład ten będzie na tyle prosty aby każdy mógł zrozumieć naturę standardowych mechanizmów kontroli wpisywanych danych przez użytkownika, a następnie pozwoli nam utworzyć własne klasy implementujące odpowiednie interfejsy. No to zaczynamy:

1) Utwórzmy formularz, który będzie zawierać następujące pola tekstowe:

Za informację przechowywane w każdym polu będzie odpowiadać klasa RegistrationForm:

public class RegistrationForm {
private String name;
private String surname;
private Date dateOfBirth;
private int age;
private double doubleValue;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSurname() {
return surname;
}

public void setSurname(String surname) {
this.surname = surname;
}


public Date getDateOfBirth() {
return dateOfBirth;
}

public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public double getDoubleValue() {
return doubleValue;
}

public void setDoubleValue(double doubleValue) {
this.doubleValue = doubleValue;
}

}
Natomiast nasz kod strony powinien na początku wyglądać następująco:

<h:form prependId="false">
<table>
<tr>
<td>Imię:</td>
<td><ice:inputText id="name" value="#{registrationForm.name}" >
</ice:inputText></td>
<td></td>
</tr>
<tr>
<td>Nazwisko:</td>
<td><ice:inputText id="surname" value="#{registrationForm.surname}" >
</ice:inputText></td>
<td></td>
</tr>
<tr>
<td>Data urodzenia:</td>
<td>
<ice:selectInputDate id="dateOfBirth"
value="#{registrationForm.dateOfBirth}">
</ice:selectInputDate> </td>
<td></td>
</tr>
<tr>
<td>Wiek:</td>
<td><ice:inputText id="age" value="#{registrationForm.age}" >
</ice:inputText></td>
<td></td>
</tr>
<tr>
<td>Wartość double:</td>
<td><ice:inputText id="doubleValue" value="#{registrationForm.doubleValue}">
</ice:inputText></td>
<td></td>
</tr>
</table>
</h:form>

<ice:commandButton value="Dalej" action="registration"/>
Teraz utwórzmy drugą stronę RegistrationComplete.jspx i utwórzmy w pliku faces-config.xml regułę nawigacji pomiędzy obiema stronami.
Strona RegistrationComplete.jspx:

<jsp:root version="2.1"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:ice="http://www.icesoft.com/icefaces/component">
<jsp:directive.page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"/>
<f:view>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>RegistrationComplete.jspx</title>
</head>
<body>
<h1>Dziękujemy za rejestrację!</h1>
<ice:form>

</ice:form>
</body>
</html>
</f:view>
</jsp:root>
Reguła nawigacji:

<navigation-rule>
<from-view-id>/robpie/RegistrationFormIFaces.jspx</from-view-id>
<navigation-case>
<from-outcome>registration</from-outcome>
<to-view-id>/robpie/RegistrationComplete.jspx</to-view-id>
</navigation-case>
</navigation-rule>
Czas na przetestowanie czy reguła nawigacji działa. Uruchamiamy projekt i przechodzimy na stronę: RegistrationFormIFaces.jspx i klikamy przycisk "Dalej". Powinniśmy ujrzeć drugą stronę.

Weryfikacja i konwersja.

Każda kontrolka posiada atrybut required, który oznacza czy pole jest wymagane. np:

<ice:inputText id="name" value="#{registrationForm.name}" required="true">
</ice:inputText>
<ice:inputText id="age" value="#{registrationForm.age}" > </ice:inputText>
Oznacza to, że jeśli nie uzupełnimy danego pola to nie będziemy mogli przejść na drugą stronę. Teraz dodajmy jeszcze jeden znacznik, który poinformuje nas o napotkanych błędach ponieważ standardowo błędy tego typu nie są wyświetlane na stronie (Wyświetlają się w logach serwera Glassfish). W każdy wolny znacznik wstawmy następujący kod:


<ice:message for="name" />
<ice:message for="surname" />
<ice:message for="dateOfBirth" />
<ice:message for="age" />
<ice:message for="doubleValue" />


Atrybut for lokalizuje nam dla jakiego identyfikatora id kontrolki wejściowej ma wyświetlać błędy.
Teraz nasza strona powinna wyglądać następująco:


<h:form prependId="false">
<table>
<tr>
<td>Imię:</td>
<td><ice:inputText id="name" value="#{registrationForm.name}" required="true">
</ice:inputText></td>
<td><ice:message for="name" /></td>
</tr>
<tr>
<td>Nazwisko:</td>
<td><ice:inputText id="surname" value="#{registrationForm.surname}" required="true">
</ice:inputText></td>
<td><ice:message for="surname" /></td>
</tr>
<tr>
<td>Data urodzenia:</td>
<td>
<ice:selectInputDate id="dateOfBirth"
value="#{registrationForm.dateOfBirth}"
required="true">
</ice:selectInputDate> </td>
<td><ice:message for="dateOfBirth" /></td>
</tr>
<tr>
<td>Wiek:</td>
<td><ice:inputText id="age" value="#{registrationForm.age}" >
</ice:inputText></td>
<td><ice:message for="age" /></td>
</tr>
<tr>
<td>Wartość double:</td>
<td><ice:inputText id="doubleValue" value="#{registrationForm.doubleValue}">
</ice:inputText></td>
<td><ice:message for="doubleValue" /></td>
</tr>
</table>
<ice:commandButton value="Dalej" action="registration"/>
</h:form>
W tym momencie czas przetestować naszą aplikację. Uruchamiamy ją i przechodzimy na stronę z rejestracją. Klikamy "Dalej". Widać, że w tym momencie wyświetlają się pewne komunikaty, które informują, że pole jest wymagane.
Validation Error: Value is required.

Teraz dodamy kolejne validatory, a na końcu pokażę jak zmienić standardowe komunikaty frameworku JavaServer Faces.
Dodamy:


<f:validateLength minimum="5" maximum="20" />
<f:convertDateTime pattern="dd/MM/yyyy" />
<f:validateLongRange minimum="18" maximum="98" />
<f:validateDoubleRange minimum="3.14" maximum="18.5" />

1)Weryfikacja długości łańcucha znakowego.
2)Konwersja łańcucha znakowego na typ Date po stronie serwera.
3)Weryfikacja przedziału liczb całkowitoliczbowych.
4)Weryfikacja przedziałów liczb zmiennoprzecinkowych.
Odpowiednie kontrolki dodajmy między znaczniki inputText i selectInputDate:



<ice:inputText id="name" value="#{registrationForm.name}" required="true">
<f:validateLength minimum="5" maximum="20" />
</ice:inputText>
<ice:inputText id="surname" value="#{registrationForm.surname}" required="true">
<f:validateLength minimum="5" maximum="20" />
</ice:inputText>
<ice:selectInputDate id="dateOfBirth"
value="#{registrationForm.dateOfBirth}"
renderAsPopup="true"
required="true">
<f:convertDateTime pattern="dd/MM/yyyy" />
</ice:selectInputDate>
<ice:inputText id="age" value="#{registrationForm.age}" >
<f:validateLongRange minimum="18" maximum="98" />
</ice:inputText>
<ice:inputText id="doubleValue" value="#{registrationForm.doubleValue}">
<f:validateDoubleRange minimum="3.14" maximum="18.5" />
</ice:inputText>
Zwiększyliśmy restrykcje weryfikacji i nieporawne dane zostają wykryte a odpowiednie błędy są wyświetlane.
Powinniśmy teraz zmienić treść każdego z błędów na taki, który nam odpowiada.
Zaletą JSF jest to, że wszystkie komunikaty przechowywujemy w jednym miejscu. Każdy komunikat jest zmienną odpowiedniej klasy. Raz zdefiniowane błędy będą dostępne w każdym formularzu i na każdej stronie. W przeciwieństwie do ASP.NET nie musimy klepać wszędzie errorMessage w każdym miejscu.
Musimy utworzyć odpowiedni plik np:
myMessage.properties i tam umieścić nasze komunikaty:
Klikamy na swoją paczkę PPM wybieramy New -> Other -> Other -> Properties File.
Klikamy PPM na plik myMessages.properties i klikamy Edit.
Wklejamy następujący kod:



javax.faces.component.UIInput.REQUIRED=<b><span >*Pole {0} obowiązkowe.</span></b>
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=<b><span style="color:red">*Wartość {2} musi być w przedziale od {0} do {1}.</span></b>
javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=<b><span >*Wartość {2} musi być w przedziale od {0} do {1}.</span></b>
javax.faces.converter.DateTimeConverter.CONVERTER=Please enter a valid date.
javax.faces.converter.DateTimeConverter.DATE=<b><span >Podaj poprawny format daty: {1}</span></b>
javax.faces.component.UIInput.CONVERSION=<b><span style="color:red">*Błąd konwersji.</span></b>
javax.faces.validator.LengthValidator.MINIMUM=<b><span >*Pole {1} musi być większe od {0}</span></b>
javax.faces.validator.LengthValidator.MAXIMUM=<b><span style="color:red">*Pole {1} musi być mniejsze od {0}</span></b>

Ostatnim krokiem będzie dodanie odpowiedniego wpisu do pliku faces-config.xml informującego gdzie framework ma szukać pliku z komunikatami.

Uruchamiamy aplikację i testujemy działanie.



<application>
<message-bundle>robpie.myproperties</message-bundle>
</application>

Utworzenie pustej strony frameworku ICEFaces.

Zaczynając pracę w frameworku JavaServer Faces szybko dostrzegamy jak ubogi jest zestaw kontrolek, którymi możemy się posłużyć. Niemniej jednak świat aplikacji web w Java wygląda zachęcająco. Istnieje mnóstwo szkieletów aplikacji ulepszających JSF np ciekawy projekt http://www.icefaces.org/main/home/ . Tak naprawdę ICEFaces jest zestawem ciekawych kontrolek i rozszerza FacesServlet przez co jest łatwy do implementacji i do zrozumienia. Tyle słowem wstępu. Zacznijmy pracę.

Tworzenie strony ICEFaces:

W tym miejscu zakładam, że poprawnie zainstalowałeś moduły NB dające mozliwość współpracy z tym ciekawym projektem.
Pierwszym krokiem jest wybranie PPM new -> JSF JSP Page.
Naszym oczom pojawi się następujące okno:

Musimy wybrać opcję JSP Document(XML Syntax) i kliknąć finish. Naszym oczom pojawi sie nowa strona o rozszerzeniu *.jspx. Strony o tym rozszerzeniu są de facto plikami xml-owymi co pozwala nam na łatwiejsze diagnozowanie błędów jak w przypadku Facelets. Teraz abyśmy mogli używać w NB komponentów przekazać informację o xml namespace odnośnie biblioteki ICEFaces. Wygląda to następująco:


<jsp:root version="2.1"
f="http://java.sun.com/jsf/core"
h="http://java.sun.com/jsf/html"
jsp="http://java.sun.com/JSP/Page"
ice="http://www.icesoft.com/icefaces/component">
</jsp:root>
<jsp:directive.page contentType="text/html;
charset=UTF-8" pageEncoding="UTF-8"/>
<f:view>
<ice:form>

<ice:form>
</f:view>


Teraz pomiędzy znacznikami wrzucamy co nam tylko przyjdzie do głowy.
Pozdrawiam.