28 grudnia 2007

Prezentacja dużych tabel na stronach HTML

W czym tkwi problem? Otóż chciałbym mieć komponent, który umożliwi prezentację dużej tabeli na stronie HTML, przy czym tabela ta, jest zarówno bardzo długa, jak i bardzo szeroka. Jedną z możliwości, właśnie tą, o której dziś będę pisał jest wyświetlanie tylko pewnego "okna" tej tabeli wraz z nagłówkami widocznych wierszy i kolumn. Naturalnie trzeba też umożliwić przewijanie, tj. zmianę pozycji "okna". Jeden rysunek wart jest tysiąca słów, a więc ideę tę prezentuję poniżej w postaci graficznej:


Zatem zamiast wyświetlać całą tabelę wyświetlamy tylko pewien jej wycinek i dodajemy guziki do przewijania. Powinniśmy także prezentować informację o aktualnie wyświetlanym fragmencie, czyli np. jak to pokazano na powyższej ilustracji napis "2-4 / 5" co oznacza, że aktualnie wyświetlone są kolumny od 2. do 4. a cała tabela ma 5 kolumn.

Moim ostatecznym celem jest implementacja komponentu JSF, ewentualnie w postaci kompozycji Facelets, ale w dzisiejszym artykule poprzestanę na warstwie prezentacji, czyli JavaScript + HTML = DHTML. W jaki sposób zrealizujemy mechanizm ukrywania kolumn i wierszy, które nie należą do wyświetlanego "okna"? Naturalnie poprzez dodanie stylu style="display: none;" do odpowiednich elementów <tr> i <td>. Przewijanie "okna" zaimplementujemy przy pomocy jednej klasy JavaScript. Przy okazji polecam świetny artykuł o klasach w języku JavaScript – "3 ways to define a JavaScript class". JavaScript będzie użyty tylko do zmiany pozycji "okna" poprzez odpowiednie żonglowanie wartościami display, stylów elementów <tr> i <td> natomiast inicjalizacja wstępnej pozycji "okna" musi być zakodowana w tabeli HTML. Tabela zaprezentowana uprzednio na ilustracji (z dodanymi elementami <input type="checkbox" /> w komórkach) powinna więc wyglądać tak:

<table id="myTable">
<tr>
<td>&nbsp;</td>
<td style="display:none;">Arek</td>
<td>Borys</td>
<td>Czarek</td>
<td>Darek</td>
<td style="display:none;">Ewa</td>
</tr>
<tr style="display:none;">
<td>1</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr style="display:none;">
<td>2</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>3</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>4</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>5</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr style="display:none;">
<td>6</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
</table>

Aby kod JavaScript, który za chwilkę zaprezentuję działał poprawnie ważne jest żeby określić styl style="display:none;" dla wszystkich elementów <td> z ukrytej kolumny tabeli, nawet wtedy gdy dana komórka znajduje się w ukrytym wierszu. Wiersz może bowiem stać się widoczny w skutek przewijania "okna" w górę lub w dół, natomiast wszystkie komórki danej kolumny powinny pozostać ukryte. A oto klasa JavaScript implementująca przewijanie tabeli:

function TableRewinder
(tableName, vCols, vRows, leftOffset, topOffset, leftHeader, topHeader) {

// wartość atrybutu id tabeli, na której operujemy
this.tableName = tableName;

// ile kolumn, wliczając okno i nagłówek ma tabela
this.visibleCols = vCols;

// ile wierszy, wliczając okno i nagłówek ma tabela
this.visibleRows = vRows;

// przesunięcie okna względem pierwszej kolumny treści tabeli
this.leftOffset = leftOffset;

// przesunięcie okna względem pierwszego wiersza treści tabeli
this.topOffset = topOffset;

// ile kolumn tabeli stanowi lewy nagłówek
this.leftHeader = leftHeader;

// ile wierszy tabeli stanowi górny nagłówek
this.topHeader = topHeader;

this.getTable = function() {
return document.getElementById(this.tableName);
};

this.visibleLeftmost = function() {
return this.leftHeader + this.leftOffset;
};

this.visibleRightmost = function() {
return this.leftOffset + this.visibleCols - 1;
};

this.visibleTopmost = function() {
return this.topHeader + this.topOffset;
};

this.visibleBottommost = function() {
return this.topOffset + this.visibleRows - 1;
};

this.canGoLeft = function() {
return this.visibleLeftmost() > this.leftHeader;
};

this.canGoRight = function() {
return this.visibleRightmost() < this.getTable().rows[0].cells.length - 1;
};

this.canGoUp = function() {
return this.visibleTopmost() > this.topHeader;
};

this.canGoDown = function() {
return this.visibleBottommost() < this.getTable().rows.length - 1;
};

this.toggleRows = function(hideRowNum, showRowNum) {
this.getTable().rows[hideRowNum].style.display = 'none';
this.getTable().rows[showRowNum].style.display = '';
};

this.toggleColumns = function(hideColNum, showColNum) {
for(i = 0 ; i < this.getTable().rows.length; i++) {
rowColumns = this.getTable().rows[i].cells;

rowColumns[hideColNum].style.display = 'none';
rowColumns[showColNum].style.display = '';
}
};

this.rewindLeft = function() {
if(this.canGoLeft()) {
this.toggleColumns(this.visibleRightmost(), this.visibleLeftmost() - 1);

this.leftOffset -= 1;
}
};

this.rewindRight = function() {
if(this.canGoRight()) {
this.toggleColumns(this.visibleLeftmost(), this.visibleRightmost() + 1);

this.leftOffset += 1;
}
};

this.rewindUp = function() {
if(this.canGoUp()) {
this.toggleRows(this.visibleBottommost(), this.visibleTopmost() - 1);

this.topOffset -= 1;
}
};

this.rewindDown = function() {
if(this.canGoDown()) {
this.toggleRows(this.visibleTopmost(), this.visibleBottommost() + 1);

this.topOffset += 1;
}
};
}

Kod jest nieco rozwlekły, ale to dobrze robi jego czytelności. Znaczenie poszczególnych parametrów konstruktora klasy (funkcji TableRewinder) zostało opisane nad odpowiadającymi im zmiennymi klasowymi. Jak widać, klasa działa również z tabelami, które mają liczbę wierszy czy kolumn nagłówka inną niż 1. Poniżej kod, który utworzy instancję odpowiednią dla przedstawionej powyżej tabeli HTML oraz przykład użycia tej instancji:

var myTableRewinder = new TableRewinder('myTable', 4, 4, 1, 2, 1, 1);

<span onclick="myTableRewinder.rewindLeft()">lewo</span>
<span onclick="myTableRewinder.rewindRight()">prawo</span>
<span onclick="myTableRewinder.rewindUp()">góra</span>
<span onclick="myTableRewinder.rewindDown()">dół</span>

Po złożeniu zaprezentowanych elementów w jedną stronę DHTML otrzymujemy poniższy efekt. Nie ładny, bo bez odpowiednich CSS’ów, ale działa! Jak dobrze pójdzie w jednym z kolejnych artykułów opakuje to w komponent JSF, który wygeneruje coś ładniejszego.

17 grudnia 2007

Jestem magistrem

Dziś jest dla mnie dzień wyjątkowy. O godzinie 15 z minutami ukończyłem studia broniąc – na ocenę bardzo dobrą – pracę magisterską i uzyskując tytuł Magistra Informatyki na wydziale Matematyki, Informatyki i Mechaniki Uniwersytetu Warszawskiego. Co prawda z opóźnieniem półtora rocznym, ale zważywszy na fakt, że od początku czwartego roku studiów pracowałem zawodowo nie jest to wynik słaby, tym bardziej gdy weźmie się pod uwagę, jak wielu moich kolegów z wydziału nie zdobyło się na ten ostatni wysiłek. No ale nie trudno to zrozumieć, dobra praca wyjątkowo skutecznie studzi zapał do aktywności uniwersyteckiej. W tym miejscu chciałem serdecznie podziękować mojemu promotorowi prof. Janowi Madeyowi. Dziękuję Panie Profesorze za poświęcony czas i energię!

9 grudnia 2007

Uwierzytelnianie i autoryzacja w Acegi Security

Zajmuję się ostatnio nieco zabezpieczaniem różnego rodzaju zasobów IT, w szczególności aplikacji WWW i im więcej to robię tym więcej zbiera mi się na krytykę standardów przemysłowych, które do tego służą. Postanowiłem podzielić się moim postrzeganiem tematu, ale zanim to zrobię ugruntuję nieco swą wiedzę i przygotuje podstawę do dyskusji. Mówiąc standardy przemysłowe mam na myśli de-facto-standardy, czyli np. Acegi Security i mechanizmy popularnych serwerów aplikacyjnych, np. BEA WebLogic Server. W dzisiejszym artykule opiszę, jak wygląda architektura uwierzytelniania i autoryzacji w Acegi. Będzie to jednak opis nie konstruktywny w tym sensie, że nie będę opisywał konfiguracji i nie pokażę przykładu – to już zostało zrobione przez Michała Gołackiego w artykule "Acegi - Uwierzytelnianie i Autoryzacja w Springu".

Całość składa się z trzech zasadniczych części: mechanizmu odpowiedzialnego za uwierzytelnienie, który tworzy kontekst bezpieczeństwa, mechanizmu, który przechowuje ten kontekst oraz mechanizmu autoryzacji, który w trakcie działania aplikacji z tego kontekstu korzysta. Zacznijmy od mechanizmu przechowującego kontekst i tego, co to jest ten kontekst.

Klasą, która przechowuje kontekst bezpieczeństwa jest org.acegisecurity.context. SecurityContextHolder. Najciekawszą z punktu widzenia tego artykułu metodą wspomnianej klasy jest statyczna metoda getContext(), która udostępnia obiekt implementujący interfejs org.acegisecurity.context.SecurityContext, czyli właśnie rzeczony kontekst. Interfejs ten definiuje metodę getAuthentication(), która zwraca z kolei obiekt implementujący interfejs org.acegisecurity.Authentication. I o to właśnie chodzi, jest to bodaj najważniejszy obiekt w całej tej zabawie. Obiekt ten tworzony jest w pewnych nieinteresujących nas w tej chwili okolicznościach i początkowo zawiera dane uwierzytelniające, np. login i hasło. Następnie, ów obiekt przekazywany jest do metody authenticate(Authentication authentication) klasy implementującej interfejs org.acegisecurity.AuthenticationManager. Jak zwykle, to jaka to jest klasa zależy od konfiguracji i jest to punkt w którym możemy podłączyć nasz mechanizm uwierzytelniania. AuthenticationManager weryfikuje dane uwierzytelniające, np. login i hasło i jeśli są one poprawne wypełnia obiekt Authentication danymi określającymi uprawnienia uwierzytelnionej tożsamości, w przeciwnym wypadku rzuca wyjątek. Uprawnienia te to tablica obiektów implementujących interfejs org.acegisecurity.GrantedAuthority i w typowej sytuacji odpowiada to tablicy nazw ról. Zauważmy, że oznacza to pobranie zestawu uprawnień danej tożsamości w momencie uwierzytelnienia. A co, gdy zestaw uprawnień zmienia się w czasie, ale nie w prostej zależności od czasu? Nie twierdzę, że nie da się zaimplementować takiego scenariusza dla Acegi, ale jest to ewidentnie droga wbrew jego architekturze, która jest zwyczajnie nie dość elastyczna. Przyjrzyjmy się teraz autoryzacji.

Autoryzacja przeprowadzana jest przez klasę implementującą interfejs org.acegisecurity. AccessDecisionManager, a dokładnie, przez jej metodę decide(Authentication authentication, Object secureObject, ConfigAttributeDefinition config). Naturalnie to, jaka klasa jest użyta zależy od konfiguracji i możemy w to miejsce podstawić własną implementację mechanizmu autoryzacji. Metoda ta nie zwraca jednak typu logicznego, który określa decyzję o dostępie. Kluczowy jest tutaj parametr secureObject, który de facto jest akcją, dla której wykonujemy autoryzację. Niestety typ parametru nie mówi nam nic o tym obiekcie, ale może to być np. implementacja interfejsu org.aopalliance.intercept.MethodInvocation. Interfejs ten w szczególności definiuje metodę proceed(), którą należy wywołać, jeśli autoryzacja się powiedzie. Jak dla mnie przedziwne rozwiązanie, ale zapewne były jakieś powody aby zrobić to w ten właśnie sposób. Stosując jedną z typowych konfiguracji wykorzystującą klasy dostarczane przez Acegi, wywołanie wspomnianej metody decide(…) interfejsu AccessDecisionManager możemy sprowadzić do wywołania metody vote(…) klasy org.acegisecurity.vote.RoleVoter z tymi samymi parametrami. Klasa ta po prostu sprawdza czy tablica obiektów GrantedAuthority zapisanych w obiekcie Authentication zawiera uprawnienia wymagane do wykonania danej akcji.

Ciekaw jestem, jaka jest wasza opinia o Acegi Security. Ja osobiście uważam, że nie jest to rozwiązanie dostosowane do środowisk korporacyjnych. Prosty model autoryzacji dla pojedynczej aplikacji, oparty na dobrze zdefiniowanych rolach owszem, ale nie wiele więcej.

3 grudnia 2007

Dynamiczne proxy i adnotacje, czyli dobrana para

Dzisiaj dodam nieco smaczku (dla zaostrzenia apetytu) do rozwiązania, które opisałem ostatnio w artykule "Mechanizm dynamicznego proxy z Reflection API". Zmodyfikuję pokazany tam kod w ten sposób, by metody proxy odczytywały adnotację umieszczoną na interfejsie klasy biznesowej i wykonywały akcję na podstawie wartości parametrów tejże adnotacji. Zacznę od pokazania jeszcze raz interfejsu i klasy, dla których budujemy proxy, ale tym razem ze wspomnianą adnotacją. Oto one:

public interface MyService {

@MyAnnotation(“my annotation value”)
public void myOperation();
}

public class MyServiceBean implements MyService {

public void myOperation () {
System.out.println("Message from myOperation()");
}
}

Naturalnie użyta tu adnotacja MyAnnotation nie jest jakąś magiczną adnotacją, musimy ją sami zdefiniować tak jak definiujemy klasy i interfejsy. Oto odpowiedni kod:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
public @interface MyAnnotation {
String value();
}

Podkreślę tutaj, iż to dzięki temu, że jedyny parametr naszej adnotacji nazywa się 'value' możemy używać notacji skróconej i zamiast @MyAnnotation(value = "my annotation value") pisać @MyAnnotation("my annotation value") tak jak to pokazałem w przykładzie na początku artykułu. Naturalnie, jakby komuś było za mało można dodać kolejne parametry.

Zmodyfikujmy teraz interfejs ProxyAction i jego testową implementację tak, aby metody doBefore() i doAfter() akceptowały parametr typu String będący wartością naszej adnotacji. Oto kod:

public interface ProxyAction {

public boolean doBefore(String annotationValue);

public void doAfter(String annotationValue);
}

public class InvokeAction implements ProxyAction {

public boolean doBefore(String annotationValue) {
System.out.println("Message from doBefore(" + annotationValue + ")");

return true;
}

public void doAfter(String annotationValue) {
System.out.println("Message from doAfter("+ annotationValue + ")");
}
}

Pozostaje jeszcze tylko zmodyfikować kod klasy ProxyHandler implementującej interfejs InvocationHandler tak aby odczytywał i przekazywał w wywołaniach wartość adnotacji. W tym celu dodajmy metodę getValue() i zmodyfikujmy metodę invoke() tak jak pokazano poniżej:

public Object invoke(Object proxy, Method proxiedMethod, Object[] args)
throws Throwable {

Object result = null;

try {
String annotationValue = getValue(proxiedMethod);

if (proxyAction.doBefore(annotationValue))
result = proxiedMethod.invoke(proxiedInstance, args);

proxyAction.doAfter(annotationValue);
} catch (InvocationTargetException invocationExc) {
throw invocationExc.getTargetException();
}

return result;
}

private String getValue(Method proxiedMethod) {

for(Annotation annotation : proxiedMethod.getAnnotations()) {
if(annotation instanceof MyAnnotation) {
return ((MyAnnotation)annotation).value();
}
}

throw new RuntimeException("Annotation not found");
}

Prawdopodobnie dobrze by jeszcze było zastąpić wyjątek RuntimeException rzucany z metody getValue() jakimś własnym, ale to już szczegół. Nie wiem jak wy, ale ja jestem zachwycony elegancją, prostotą i siłą wyrazu tego rozwiązania i niewykluczone, że zaprezentuje jeszcze coś ciekawego na bazie tych mechanizmów.

2 grudnia 2007

Mechanizm dynamicznego proxy z Reflection API

Dzisiaj zaprezentuję mechanizm dynamicznego proxy (ang. dynamic proxy) udostępniany przez Reflection API, mechanizm wykorzystywany (między innymi) przez Spring’a do implementacji AOP (akr. Aspect-Oriented Programming). Generalnie, dynamiczne proxy umożliwia wywoływanie metod naszej klasy za pośrednictwem metod innej klasy. Pozwala to wykonać pewne akcje przed jak i po wywołaniu właściwej metody albo też ewentualnie zadecydować o nie wykonywaniu tejże. Ale gdyby to było tylko tyle to byłoby to zwykłe proxy. Dlaczego jest ono dynamiczne? Otóż dlatego, że klasa będąca klasą proxy jest generowana dynamicznie w czasie wykonania. Nie mam pomysłu jak zwięźle i trafnie opisać, o co dokładnie chodzi, więc od razu przechodzę do meritum. Załóżmy, że mamy następujący interfejs i implementującą go klasę:

public interface MyService {

public void myOperation();
}

public class MyServiceBean implements MyService {

public void myOperation () {
System.out.println("Message from myOperation()");
}
}

Aby to, co chcę finalnie zbudować wyglądało ładnie użyjemy Spring’a (choć można i bez niego). Poniżej konfiguracja ziarna dla klasy pokazanej powyżej, póki co nic ciekawego, ale będziemy modyfikować:

<bean id="myService" class="mypackage.MyServiceBean">
</bean>

A teraz zdefiniujmy interfejs z dwiema metodami – doBefore() i doAfter() – które to będą wywołane przed i po właściwym wywołaniu metody biznesowej. Dodatkowo, metoda doBefore() będzie zwracała rezultat typu logicznego (typu boolean) o tym znaczeniu, że metoda biznesowa będzie wywoływana tylko wtedy, gdy rezultat ten będzie miał wartość ‘true’. Oto on wraz z implementacją testową:

public interface ProxyAction {

public boolean doBefore();

public void doAfter();
}

public class InvokeAction implements ProxyAction {

public boolean doBefore() {
System.out.println("Message from doBefore()");

return true;
}

public void doAfter() {
System.out.println("Message from doAfter()");
}
}

Naszym celem jest oczywiście takie skonstruowanie kodu, aby to, jaka implementacja interfejsu ProxyAction będzie użyta zależało od konfiguracji Spring’a. To co było banalne już mamy, teraz przechodzimy do sedna.

W dzisiejszej zabawie chodzi o to, aby zastąpić definicję elementu zarządzanego Spring pokazaną powyżej taką definicją, aby zamiast instancji klasy implementującej metody biznesowe dostać instancje innej klasy, utworzonej dynamicznie dzięki mechanizmowi dynamicznych proxy z Reflection API. Taka instancja naturalnie będzie implementowała te same interfejsy co nasza klasa biznesowa. To jest właśnie gwóźdź programu. W tym celu musimy napisać klasę implementującą interfejs java.lang.reflect.InvocationHandler. Oto kod:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ProxyHandler implements java.lang.reflect.InvocationHandler {

private Object proxiedInstance;

private ProxyAction proxyAction;

protected ProxyHandler(Object obj, ProxyAction proxyAction) {
this.proxiedInstance = obj;
this.proxyAction = proxyAction;
}

public Object invoke(Object proxy, Method proxiedMethod, Object[] args)
throws Throwable {

Object result = null;

try {
if (proxyAction.doBefore())
result = proxiedMethod.invoke(proxiedInstance, args);

proxyAction.doAfter();
} catch (InvocationTargetException invocationExc) {
throw invocationExc.getTargetException();
}

return result;
}
}

Kolejnym elementem układanki jest klasa implementująca wzorzec fabryki i tworząca instancje odpowiednio skonfigurowanych proxy. Oto kod:

public class ProxiedServiceFactory {

private ProxyAction proxyAction;

public Object newInstance(Object proxiedInstance) {
return java.lang.reflect.Proxy.newProxyInstance(
proxiedInstance.getClass().getClassLoader(),
proxiedInstance.getClass().getInterfaces(),
new ProxyHandler(proxiedInstance, proxyAction));
}

public void setProxyAction(ProxyAction proxyAction) {
this.proxyAction = proxyAction;
}
}

Działa to tak, że fabryka buduje obiekt dynamicznego proxy na bazie instancji klasy implementującej logikę biznesową przekazanej do wywołania metody newInstance(). Budując wspomniany obiekt proxy przekazujemy jednocześnie instancję klasy implementującą interfejs java.lang.reflect.InvocationHandler. Metoda invoke() tej klasy będzie wywoływana zamiast metod biznesowych i to właśnie tam decydujemy, co tak naprawdę się stanie. Ja akurat postanowiłem, że wywołamy tam metody doBefore() i doAfter() na instancji implementującej interfejs MyService zdefiniowanej w momencie tworzenia fabryki. Aby spiąć wszystko elegancko do kupy należy użyć konfiguracji Spring pokazanej poniżej (w miejsce tej pokazanej na początku artykułu):

<bean id="proxiedServiceFactory" class="mypackage.ProxiedServiceFactory">
<property name="proxyAction">
<bean class="mypackage.InvokeAction" />
</property>
</bean>

<bean id="myService" factory-bean="proxiedServiceFactory" factory-method="newInstance">
<constructor-arg>
<bean class="mypackage.MyServiceBean" />
</constructor-arg>
</bean>

To tyle. Naturalnie, z dokładnością do możliwości użycia operatora ‘instanceof’, ziarna zarządzanego ‘myService’ używamy tak, jakby to była normalna instancja naszej klasy biznesowej. Podkreślę jeszcze tylko, bo być może nie jest to jasne, że to co pokazałem to jest rodzaj szkieletu aplikacji (ang. framework). Mam tu na myśli to, że całą tę skomplikowaną infrastrukturę implementujemy tylko raz, niezależnie od tego ile mamy klas biznesowych. W następnym artykule mam nadzieję pokazać przykład ciekawego wykorzystania pokazanego tu mechanizmu.

22 listopada 2007

Bieżący kontekst Spring'a w aplikacji JSF

Wyobraźmy sobie, że mamy aplikację Spring + JSF (choć łatwo uogólnić na inne technologie WWW). W pewnej klasie potrzebujemy ziarna zarządzanego Spring, ale nie możemy go wstrzyknąć (ang. injection) poprzez konfigurację, gdyż cyklem życia tej klasy nie zarządza Spring. Przykładem niech będzie komponent JSF. To nie Spring tworzy instancje tych komponentów, tylko JSF, więc nie możemy wstrzyknąć naszej zależności. Poniżej fragment kodu pokazujący jak pobrać bieżący kontekst Spring’a (utworzony przez ContextLoaderListener zdefiniowany w web.xml) i z niego ziarno zarządzane. Oto on:


import javax.faces.context.FacesContext;
import javax.servlet.ServletContext;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

ServletContext servletContext = (ServletContext)FacesContext.
getCurrentInstance().getExternalContext().getContext();

WebApplicationContext applicationContext = WebApplicationContextUtils.
getWebApplicationContext(servletContext);

Object springBean = applicationContext.getBean("beanName");

9 listopada 2007

Hibernate Search, czyli wyszukiwanie pełnotekstowe encji Hibernate

Zacznijmy od początku, czyli co to jest wyszukiwanie pełnotekstowe? Wyobraźmy sobie, że implementujemy funkcjonalność wyszukiwania produktów w sklepie internetowym. Nie chodzi jednak o jakieś formularze gdzie podajemy wartości dla ściśle określonych atrybutów tylko o wyszukanie na podstawie opisu tekstowego produktu z użyciem słów kluczowych. Chcemy więc mieć taki mały Google który wyszukuje wśród naszych obiektów, a mówiąc ściślej encji (ang. entity) Hibernate. Istnieją co najmniej dwie rozsądne metody implementacji takiej funkcjonalności, tzn. ja znam dwie: Hibernate Search oraz użycie odpowiedniego indeksu bazy danych (FULLTEXT INDEX). W niniejszym artykule pokażę, jak zbudować prostą aplikację na bazie Hibernate Search. Za punkt wyjścia przyjmę aplikację, którą przedstawiłem w artykule "Aplikacja Hibernate z adnotacjami i Spring'iem", ale de facto nada się każda aplikacja Hibernate z odwzorowaniem obiektowo-relacyjnym zdefiniowanym w formie adnotacji. Zanim jednak przejdę do czynu dwa słowa o tej technologii. Otóż Hibernate Search to nic innego jak integracja Apache Lucene z Hibernate Core w ten sposób, aby operacje na encjach były odwzorowane na odpowiednie operacje na indeksie Lucene. Dodatkowo, efektem wyszukiwania w indeksie Lucene nie jest – dajmy na to – lista identyfikatorów, lecz lista encji zarządzanych Hibernate. Indeks właśnie jest najważniejszym elementem technologii Lucene. Zawiera on informacje o dokumentach Lucene i umożliwia sprawne wyszukiwanie tych pasujących do naszego zapytania. O dokumentach tych możemy myśleć jak o obiektach. Podstawowe operacje API Lucene to dodanie dokumentu do indeksu, usunięcie go i modyfikacja. To są właśnie operacje wywoływane automatycznie przez Hibernate Search ilekroć encje – które zdefiniowaliśmy jako indeksowane – są zapisywane, usuwane bądź modyfikowane. To rzekłszy przechodzę do implementacji.

Zacznijmy od pobrania biblioteki Hibernate Search (ja użyłem wersji 3.0.0 GA) i dodania jej .jar’ów (hibernate-search.jar i lucene-core-2.2.0.jar) do projektu. Kolejnym krokiem jest konfiguracja indeksu Lucene. Rzecz jest trywialna i sprowadza się do określenia – w formie właściwości (ang. property) Hibernate – klasy implementującej dostawcę indeksu oraz kolejnego parametru wymaganego dla dostawcy użytego w naszej aplikacji. Ja używam Hibernate w połączeniu ze Spring’iem a więc modyfikuję ziarno zarządzane Spring’a definiujące fabrykę sesji Hibernate. Powinno ono wyglądać następująco (patrz artykuł "Aplikacja Hibernate z adnotacjami i Spring'iem"):

<bean id="hibernateSessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="hibernateDataSource" />
<property name="schemaUpdate" value="true" />
<property name="hibernateProperties">
<value>
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.search.default.directory_provider =
org.hibernate.search.store.FSDirectoryProvider

hibernate.search.default.indexBase = c:/index
</value>
</property>
<property name="annotatedClasses">
<list>
<value>entity.Product</value>
</list>
</property>
</bean>

Jak można by się domyślać wartość ‘c:/index’ to katalog w którym będą przechowywane pliki stanowiące indeks. Weźmy teraz jedną z naszych encji Hibernate i za pomocą kilku adnotacji sprawmy by była indeksowana. Poniżej przykład dla klasy produktu:


@Entity @Indexed
public class Product {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@DocumentId
private Long productId;

@Field(index = Index.TOKENIZED, store = Store.NO)
private String name;

public String getName() {
return name;
}

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

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}
}

Wszystkie adnotacje Hibernate Search pochodzą z pakietu org.hibernate.search.annotations. Pozostałe to adnotacje JPA (wykorzystywane także przez Hibernate). Adnotacja @Indexed sprawia, że encja będzie indeksowana. Tak jak @Id określa identyfikator encji tak @DocumentId określa identyfikator dokumentu w indeksie. Bodaj zawsze powinna to być ta sama właściwość klasy, ale nie musi być. Adnotacja @Field określa właściwości klasy, które będą indeksowane. Nie jest tak jak w przypadku JPA, że domyślnie indeksowane a w przypadku JPA utrwalane (ang. persisted) są wszystkie właściwości. Atrybut index = Index.TOKENIZED adnotacji określa, że indeksowane będą poszczególne słowa a nie cała wartość. Właśnie tego chcemy dla właściwości tekstowych, ale do indeksu możemy włączyć także właściwości będące np. datą aby umożliwić filtrowanie a wtedy nie chcemy aby wartość była w jakikolwiek sposób rozbijana. Atrybut store = Store.NO powoduje, że wartość sama w sobie nie będzie przechowywana w indeksie. W pewnych przypadkach przechowywanie wartości ma sens, ale jest to dyskusja poza zakresem tego artykułu. Stwórzmy teraz klasę DAO z metodą searchProducts(String searchText), która zwróci listę encji zarządzanych Hibernate spełniających zadane kryterium wyszukiwania. Oto przykładowy kod źródłowy:


public class ProductDAOBean extends HibernateDaoSupport implements ProductDAO {

public void saveProduct(Product product) {
getHibernateTemplate().saveOrUpdate(product);
}

@SuppressWarnings("unchecked")
public List<Product> searchProducts(String searchText) {
List<Product> products = null;

Session session = getSession();
try {
FullTextSession fullTextSession = Search.createFullTextSession(session);

MultiFieldQueryParser queryParser =
new MultiFieldQueryParser(new String[] { "name" }, new StandardAnalyzer() );

FullTextQuery fullTextQuery fullTextSession =
.createFullTextQuery(queryParser.parse(searchText), Product.class);

products = fullTextQuery.list();

} catch (HibernateException hibernateExc) {
throw convertHibernateAccessException(hibernateExc);
} catch (ParseException parseExc) {
throw new DataRetrievalFailureException("Lucene exception", parseExc);
} finally {
releaseSession(session);
}

return products;
}
}

Metoda saveProduct(Product product) będzie potrzebna, aby zasiedlić bazę danych oraz indeks Lucene danymi testowymi. Nie możemy zrobić tego inaczej (np. poprzez SQL INSERT) gdyż wtedy nasze obiekty nie zostałyby zindeksowane. I jeszcze klasa testu JUnit 4 wraz z metodami do instalacji danych testowych:

public class ProductDAOBeanTest {

private ProductDAOBean productDAOBean;

@Before
public void initialize() {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "config/dao.xml" });

productDAOBean = (ProductDAOBean) context.getBean("productDAO");
}

@Test
public void testSearchProducts() {
List<Product> products = productDAOBean.searchProducts("wiertarka bosch");

for(Product product : products)
System.out.println(product.getName());
}

@Test
public void installData() {
productDAOBean.saveProduct(newProduct("Wiertarka Bosch GSB 13RE"));
productDAOBean.saveProduct(newProduct("Wiertarka Bosch PSB 500RE"));
productDAOBean.saveProduct(newProduct("Szlifierka Bosch GWS 1000"));
productDAOBean.saveProduct(newProduct("Strug Bosch GHO 26-82"));
productDAOBean.saveProduct(newProduct("Wiertarka Makita HP 1620"));
productDAOBean.saveProduct(newProduct("Szlifierka Makita 9565H"));
productDAOBean.saveProduct(newProduct("Szlifierka Skill 9470"));
}

private Product newProduct(String prodName) {
Product product = new Product();

product.setName(prodName);

return product;
}
}

Metoda searchProducts(String searchText) akceptuje zapytania zgodne ze składnią Lucene Query Language i jest to sporo więcej niż tylko lista słów. Zachęcam do lektury dokumentacji tego języka (jest krótka i przyjemna) i eksperymentów.

1 listopada 2007

Aplikacja Hibernate z adnotacjami i Spring'iem

W wyniku długotrwałej ewolucji, z chęci dotrzymania kroku, ale i potrzeby zachowania zgodności wstecznej dzisiejszy Hibernate stał się technologią o wielu obliczach. Z jednej strony możemy używać Hibernate jako implementacji JPA, z drugiej strony ciągle żywe jest jego tradycyjne API. Używając Hibernate w sposób, nazwijmy to tradycyjny (tj. nie jako JPA) możemy powierzyć konfigurację szkieletowi (ang. framework) Spring lub wykonać ją samodzielnie, z wykorzystaniem plików konfiguracyjnych hibernate.cfg.xml lub hibernate.properties. Generalnie, wszystko sprowadza się do zbudowania obiektu implementującego interfejs org.hibernate.SessionFactory, który jest sercem technologii Hibernate. Jeśli nie używamy do tego celu Spring’a to pierwszym krokiem jest zbudowanie obiektu klasy org.hibernate.cfg.Configuration (albo AnnotationConfiguration). Możemy to zrobić wczytując właśnie plik konfiguracyjny, ale możemy też zbudować go w sposób programowy. Na podstawie tego obiektu tworzymy obiekt fabryki sesji (implementujący org.hibernate.SessionFactory). Jeśli budowę obiektu fabryki sesji powierzymy Spring’owi, to konfigurację wykonujemy definiując odpowiednio ziarno zarządzane (ang. managed bean) kontekstu Spring’a. Dodatkowo, oprócz konfiguracji obiektu fabryki sesji definiujemy odwzorowanie obiektowo-relacyjne dla naszych encji (ang. entity). Odwzorowanie to możemy wykonać zasadniczo na jeden z dwu sposobów; z użyciem plików XML lub z użyciem adnotacji. W dzisiejszym artykule opiszę, jak używać Hibernate konfigurowany przez Spring z odwzorowaniem zdefiniowanym w postaci adnotacji.

Zacznijmy od utworzenia zwykłego (tj. nie www) projektu Java (w Eclipse jest to New > Project > Java Project). Projekt nazwijmy 'Hibernate-Spring'. Teraz pobierzmy biblioteki Hibernate z działu 'Download' portalu http://www.hibernate.org/. Potrzebne będą 'Hibernate Core' (ja użyłem wersji 3.2.4.sp1) oraz 'Hibernate Annotations' (użyłem wersji 3.3.0.GA). Pobrane archiwa trzeba rozpakować a biblioteki dodać do aplikacji. Jeśli używamy któregoś z klonów Eclipse to możemy to zrobić definiując zestaw .jar'ów jako bibliotekę użytkownika. W tym celu wybierzmy opcję Window > Preferences, gałąź Java > Build Path > User Libraries. Następnie kliknijmy guzik New i podajmy nazwę biblioteki, np. 'Hibernate'. Teraz używając guzika 'Add JARs' dodajmy do nowo zdefiniowanej biblioteki 'Hibernate' wymagane pliki .jar (to jakie są wymagane pokazuje niżej). Analogicznie jak z bibliotekami dla Hibernate musimy zrobić z bibliotekami dla Spring'a. Ze strony http://www.springframework.org/download pobieramy więc 'Spring Framework' i dodajemy do projektu (ja mam wersję 2.1 M3). Jeśli używamy Eclipse'a możemy zdefiniować bibliotekę użytkownika o nazwie 'Spring'. Potrzebujemy jeszcze sterownika JDBC odpowiedniego dla naszej bazy danych, ja używam MySQL i zdefiniowałem sobie bibliotekę użytkownika 'MySQL-JDBC'. To, jakie biblioteki są wystarczające dla naszego przykładu widać na poniższej ilustracji. Jest to widok definiowania bibliotek użytkownika:


Aby tak zdefiniowane biblioteki użytkownika dodać do projektu, klikamy na nim prawym guzikiem myszy i wybieramy Build Path > Add Libraries > User Library, klikamy Next, zaznaczamy nasze biblioteki i klikamy Finish.

Jesteśmy gotowi, by zacząć w końcu tworzyć naszą aplikację. Zaimplementujemy klasę DAO (dao.ProductDAOBean i interfejs dao.ProductDAO) dla encji produktu (entity.Product), która będzie miała proste metody do zapisu i wyszukania z użyciem identyfikatora. Oto ich kod źródłowy:

public interface ProductDAO {
public void saveProduct(Product product);

public Product findProduct(Long productId);
}

public class ProductDAOBean extends HibernateDaoSupport implements ProductDAO {

public void saveProduct(Product product) {
getHibernateTemplate().saveOrUpdate(product);
}

public Product findProduct(Long productId) {
return (Product) getHibernateTemplate().get(Product.class, productId);
}
}

Klasa DAO dziedziczy z klasy pomocniczej HibernateDaoSupport dostarczanej przez Spring. Ułatwia ona znacznie programowanie z użyciem Hibernate, zwalnia między innymi z koniczności samodzielnego zarządzania zasobami, tj. obiektami sesji oraz zapewnia spójną strukturę wyjątków. Poniżej kod naszej encji:

@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;

private String name;

public String getName() {
return name;
}

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

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}
}

Proszę zauważyć, że mimo iż używamy tradycyjnego API Hibernate’a, odwzorowanie klasy na relację definiujemy z użyciem adnotacji JPA. Naturalnie w bardziej skomplikowanych przypadkach będziemy chcieli sięgać po rozszerzenia specyficzne dla Hibernate. A teraz najważniejsze, czyli plik kontekstu Spring, który stanowi de facto konfigurację Hibernate, oto on:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="hibernateDataSource"
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/hibernate" />
<property name="username" value="root" />
<property name="password" value="passwd" />
</bean>

<bean id="hibernateSessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="hibernateDataSource" />
<property name="schemaUpdate" value="true" />
<property name="hibernateProperties">
<value>
hibernate.dialect = org.hibernate.dialect.MySQLDialect
</value>
</property>
<property name="annotatedClasses">
<list>
<value>entity.Product</value>
</list>
</property>
</bean>

<bean id="productDAO" class="dao.ProductDAOBean">
<property name="sessionFactory" ref="hibernateSessionFactory" />
</bean>

</beans>

Charakterystyczne jest tutaj użycie klasy AnnotationSessionFactoryBean jako obiektu implementującego interfejs org.hibernate.SessionFactory. Właśnie tej klasy należy użyć, aby móc definiować odwzorowanie obiektowo-relacyjne w formie adnotacji. Na koniec przygotujemy jeszcze klasę testu JUnit 4. Naturalnie do projektu należy dodać bibliotekę JUnit w wersji co najmniej 4. Oto owa klasa, przy założeniu, że plik definiujący kontekst Spring’a nazwaliśmy 'dao.xml' i umieściliśmy go w pakiecie 'config':

public class ProductDAOBeanTest {
private ProductDAOBean productDAOBean;

@Before
public void initialize() {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"config/dao.xml"});

productDAOBean = (ProductDAOBean)context.getBean("productDAO");
}

@Test
public void testSaveProduct() {
Product product = new Product();

product.setName("Super produkt");

productDAOBean.saveProduct(product);

product = productDAOBean.findProduct(product.getProductId());

Assert.assertEquals("Super produkt", product.getName());
}
}

To by było na tyle. Aby przekonać się, że rzeczywiście działa możemy jeszcze zerknąć do bazy danych. Dla jasności, poniżej widok na strukturę projektu:

25 października 2007

Porównanie mechanizmów stronicowania wyników zapytania

Istotnym elementem typowej aplikacji jest przetwarzanie, np. wyświetlanie, pewnej listy obiektów będącej wynikiem wyszukiwania w bazie danych. Bardzo często zdarza się, że jednorazowo potrzebny jest tylko pewien fragment tej listy, np. 10 najnowszych zamówień. Technologia trwałości danych powinna umożliwiać i ułatwiać pobieranie tylko pewnego podzbioru (strony) wyników zapytania i pobieranie kolejnych podzbiorów (stron) w miarę potrzeby. Poniżej pokażę, w jaki sposób implementację stronicowania wyników zapytania wspierają technologie (i specyfikacje) Hibernate, JPA, JDO, JDBC oraz Apache iBATIS Data Mapper. Jest to drugie z kryteriów porównania technologii trwałości danych, jakie tutaj prezentuję. Pierwszym było porównanie ze względu na wsparcie dla budowania dynamicznych zapytań. Wpierw, w artykule "Dynamiczne zapytania i selekcja poprzez przykład czyli Hibernate nokautuje JPA" porównałem Hibernate i JPA. Potem dodałem jeszcze wpis o iBATIS’ie w artykule "Dynamiczne zapytania z Apache iBATIS Data Mapper". Zapraszam do lektury, zwłaszcza, jeśli swoje wybory technologii lubisz opierać na konkretnych przesłankach.

Hibernate

Technologia Hibernate oferuje bardzo wygodny mechanizm stronicowania wyników zapytań. Mechanizm ten jest zaimplementowany na poziomie funkcji interfejsu programisty, nie na poziomie języka zapytań, stronicowanie rezultatu zapytania jest więc niezależne od treści samego zapytania. Fragment kodu pobierający drugą dziesiątkę zamówień (klasa Order) posortowanych po dacie złożenia zamówienia (atrybut placedDate) wygląda następująco:

Query query =
session.createQuery("FROM Order order ORDER BY order.placedDate");

query.setFirstResult(10);
query.setMaxResults(10);

List orders = query.list();

Uruchomienie powyższego kodu spowoduje wygenerowanie odpowiedniego zapytania SQL, uwzględniającego specyfikę danej bazy danych. Hibernate wspiera bardzo wiele relacyjnych baz danych i ich dialekty języka SQL.

JPA - Java Persistence API

Wsparcie dla stronicowania oferowane przez specyfikacje Java Persistence API jest niemal identyczne jak w przypadku technologii Hibernate. Fragment kodu pobierający drugą dziesiątkę zamówień (klasa Order) posortowanych po dacie złożenia zamówienia (atrybut placedDate) wygląda następująco:

Query query = eManager.createQuery
("SELECT order FROM Order order ORDER BY order.placedDate");

query.setFirstResult(10);
query.setMaxResults(10);

List orders = query.getResultList();

JDO - Java Data Objects

Technologia Java Data Objects wspiera stronicowanie wyników zapytania poprzez odpowiednie konstrukcje samego języka zapytań. Samo zapytanie może być wyrażone na dwa sposoby; w postaci pojedynczego obiektu klasy String definiującego jego treść lub poprzez ciąg wywołań odpowiednich funkcji interfejsu programisty. Stosując drugą z możliwości fragment kodu pobierający zamówienia (klasa Order) od 10. do 20. sortując po dacie złożenia zamówienia (atrybut placedDate) wygląda następująco:

Query query = pManager.newQuery(Order.class);

query.setOrdering("placedDate ASC");
query.setRange(10, 20);

Collection orders = (Collection) query.execute();

Możliwe jest także zdefiniowanie zakresu stronicowania jako zmiennych, a następnie wywoływanie wielokrotnie tego samego zapytania z podaniem różnych wartości. Analogiczna do powyższej funkcjonalność byłaby wtedy zaimplementowana następująco:

Query query = pManager.newQuery(Order.class);

query.setOrdering("placedDate ASC");
query.setRange(":1, :2");

Collection orders = (Collection) query.execute(10, 20);

Stosując podejście z zapytaniem wyrażonym jako pojedynczy napis analogiczny kod wygląda następująco:

Query query = pManager.newQuery
("SELECT FROM Order ORDER BY placedDate ASC RANGE :1, :2");

Collection orders = (Collection) query.execute(10, 20);

JDBC

Technologia JDBC służy do uruchamiania zapytań SQL i pobierania ewentualnych wyników tych zapytań. Zapytania są przekazywane do uruchomienia w niezmienionej formie, zatem technologia ta nie oferuje żadnych ułatwień dla implementacji stronicowania wyników zapytań. Elementy składni języka SQL służące do implementacji stronicowania nie są objęte odpowiednim standardem, tak więc zapytanie pobierające określony fragment normalnego wyniku może mieć różną postać w zależności od użytego systemu zarządzania bazą danych. Zapytanie SQL pobierające drugą dziesiątkę zamówień (z tabeli ORDER) posortowanych po dacie złożenia zamówienia (kolumna PLACED_DATE), zgodne ze składnią akceptowaną przez bazy danych MySQL i PostgreSQL wyglądałoby następująco:

SELECT * FROM ORDER
ORDER BY PLACED_DATE
LIMIT 10 OFFSET 10

Apache iBATIS Data Mapper

Technologia iBATIS Data Mapper nie generuje zapytań SQL a jedynie uruchamia te zdefiniowane przez programistę. Implementując stronicowanie trzeba więc posługiwać się takimi samymi metodami jak w przypadku użycia JDBC. Ograniczanie zakresu wyniku zapytania musi być zaimplementowane w zapytaniu SQL, w sposób właściwy dla używanej bazy danych.

21 października 2007

Narzędzie 'Red Hat Palette' z Red Hat Developer Studio

W dzisiejszym artykule pokażę jedno z narzędzi Red Hat Developer Studio, panel listujący biblioteki znaczników JSP (także dla JSF) o nazwie Red Hat Palette. Dobry obrazek wart jest tysiąca słów, zacznę więc od pokazania jak ten panel wygląda. Oto on:



I po co nam taki panel? Jest to podręczna lista tego, co mamy do dyspozycji – bardzo przydatna dla tych, którzy nie koniecznie pracują z JSP/JSF na co dzień i nie pamiętają nazw odpowiednich znaczników. Szybkie spojrzenie na taką listę dobrze odświeża pamięć. Panel ten obsługuje także funkcję drag-and-drop. Po przeciągnięciu elementu z panelu na kod źródłowy strony automatycznie dodawana jest deklaracja odpowiedniej przestrzeni nazw oraz pojawia się okno do wprowadzenia wartości atrybutów. Aby panel był wyświetlany należy, jak to w Eclipse, wybrać opcje Window > Show View > Other > Red Hat Palette i nacisnąć OK. Widać to na poniższej ilustracji:



Do panelu można dodawać również swoje biblioteki. W tym celu należy użyć funkcji importu klikając ikonę wskazaną na poniższej ilustracji:



Red Hat Developer Studio standardowo ma już zdefiniowane wiele bibliotek. Dla przykładu MyFaces Tomahawk. Aby skonfigurować, które biblioteki będą pokazane w palecie należy kliknąć na ikonę konfiguracji wskazaną na poniższej ilustracji:



Biblioteki podzielone są na grupy. Biblioteka Tomahawk znajduje się w grupie MyFaces. Aby biblioteka ta była widoczna w palecie należy dla parametru 'hidden' grupy MyFaces wybrać wartość 'no'. Pokazano to na poniższej ilustracji:



Grupa MyFaces oprócz Tomahawk zawiera także biblioteki Extensions i Sandbox. Domyślnie mają one parametry 'hidden' ustawione na 'no' a więc wszystkie będą widoczne w palecie. Paleta wygląda teraz tak:


16 października 2007

Integracja technologii Facelets i Ajax4JSF

Za punkt wyjścia do dzisiejszego artykułu przyjmę, że czytelnik ma do dyspozycji jakąś działającą aplikację JSF 1.2 + Facelets, do której będzie można dołożyć Ajax4JSF i przetestować poprawność integracji. Jeśli takiej aplikacji akurat nie ma pod ręką, to można ją sobie bardzo łatwo wygenerować używając Red Hat Developer Studio, o którym pisałem w artykule "Już jest Red Hat Developer Studio". Tą właśnie aplikacją, wygenerowaną przy użyciu IDE od Red Hat’a będę się posługiwał w treści artykułu. Jak wygenerować taką przykładową aplikację JSF 1.2 + Facelets pokazałem przy okazji artykułu "Integracja JSF 1.2 i Spring z pomocą Red Hat Developer Studio".

Zaczynamy od pobrania biblioteki Ajax4JSF. Ja pobrałem najnowszą w chwili pisania tego artykułu wersję 1.1.1 (plik jboss-ajax4jsf-1.1.1-bin.zip). Archiwum rozpakowujemy i kopiujemy plik \ajax4jsf\lib\ajax4jsf-1.1.1.jar do folderu \WEB-INF\lib\ naszej aplikacji. Kolejny krok to dodanie poniższej konfiguracji do pliku web.xml:

<context-param>
<param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>
<param-value>com.sun.facelets.FaceletViewHandler</param-value>
</context-param>
<filter>
<display-name>Ajax4jsf Filter</display-name>
<filter-name>ajax4jsf</filter-name>
<filter-class>org.ajax4jsf.Filter</filter-class>
</filter>
<filter-mapping>
<filter-name>ajax4jsf</filter-name>
<servlet-name>Faces Servlet</servlet-name>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>

Następnie, z pliku faces-config.xml usuwamy deklarację mechanizmu obsługi widoku technologii Facelets, jako że teraz odpowiednia konfiguracja będzie wykonana przez Ajax4JSF, usuwamy więc element:

<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>

To by było na tyle, jeszcze tylko przekonamy się, że działa. Edytujmy dowolną stronę naszej aplikacji zawierającą element wprowadzania tekstu, tj. h:inputText (albo input z atrybutem jsfc="h:inputText"). Jeśli używamy aplikacji pochodzącej z Red Hat Developer Studio to będzie to plik \pages\inputname.xhtml. Do wybranego elementu h:inputText (lub input) dodajmy podelement a4j:suport, tak jak pokazano poniżej, dodajemy także deklarację przestrzeni nazw a4j:

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:a4j="https://ajax4jsf.dev.java.net/ajax">

<input jsfc="h:inputText" id="name" value="#{person.name}">
<a4j:support event="onkeyup" reRender="personName" />
</input>

Atrybut event elementu a4j:support określa zdarzenie, które spowoduje wysłanie żądania AJAX do serwera, uaktualniając tym samym wartość właściwości name elementu zarządzanego JSF o nazwie person. Atrybut reRender określa, że każdorazowo po otrzymaniu odpowiedzi na wysłane żądanie AJAX zostanie odświeżony element strony z atrybutem id="personName". Więcej o samym Ajax4JSF można przeczytać w dokumencie "Ajax4jsf Developer Guide". Dodajmy więc na naszej stronie (zaraz pod elementem form) element do wyświetlania wartości #{person.name} z atrybutem id="personName", tj:

<h:outputText value="Wpisano: #{person.name}" id="personName"/>

Uruchommy teraz aplikację i przekonajmy się, że działa. Każde naciśnięcie (a właściwie zwolnienie) klawisza na klawiaturze powinno być odzwierciedlone w tekście wyświetlanym przez element h:outputText. U mnie wygląda to tak:

15 października 2007

Terminologia ze świata urządzeń mobilnych

Od kilku dni jestem właścicielem palmtopa E-TEN Glofiish X500+ z systemem Windows Mobile 6 Professional. Ten oto fakt sprawił, że zacząłem się żywo interesować światem tego typu urządzeń a w szczególności ich programowaniem, najlepiej w Javie. Moja wiedza na temat tego segmentu jest znikoma, postanowiłem więc zacząć od opanowania zalewu terminologii, od ustalenia co jest co. Poniżej zademonstruje moje zrozumienie, wynikające w dużej mierze z lektury Wikipedii. Komentarze mile widziane.

Pierwsza kategoria nazw dotyczy klasyfikacji samych urządzeń. Do tej kategorii zaliczam terminy: PDA (akr. Personal Digital Assistant), palmtop, smartphone oraz MDA (akr. Mobile Digital Assistant). PDA i palmtop oznaczają dokładnie to samo, czyli mały komputer mieszczący się w dłoni. Słowo "palm" z angielskiego oznacza "dłoń". Dla odróżnienia, laptop to komputer trzymany na kolanach a desktop to ten, który stoi na biurku. MDA miało być nazwą dla PDA rozszerzonego o moduł GSM, tak więc MDA to PDA z funkcją telefonu komórkowego. Nie słyszałem jednak żeby ktoś używał tego terminu, chyba nie jest on zbyt popularny, więc dla uniknięcia zamieszania swojego Glofiish’a nazywał będę palmtopem. Smartphone to coś, co jest głównie telefonem, a więc w szczególności ma ograniczony rozmiar wyświetlacza i dodatkowo wyświetlacz ten nie jest dotykowy. Oczywiście granica między palmtopem a smartphonem oraz zwyczajnym telefonem jest płynna i chyba też nie koniecznie jest to ważne żeby ją jakoś ściśle zdefiniować.

Druga kategoria nazw wiąże się z systemem operacyjnym zainstalowanym na danym urządzeniu, ale nie jest to nazwa samego systemu. Do tej kategorii zaliczam terminy: Pocket PC i Palm. Pocket PC to palmtop (lub MDA) z systemem Windows w dowolnej wersji. Termin ten zapewne pochodzi od nazw pierwszych wersji systemu Windows na palmtopy. Były to Pocket PC 2000 i Pocket PC 2002. Palm to palmtop z systemem operacyjnym Palm OS.

Trzecia kategoria to różne nazwy opisujące system operacyjny. Do tej kategorii zaliczam terminy: Windows CE, Windows Mobile, Pocket PC, Symbian OS (oraz po prostu Symbian) i Palm OS. Z Palm OS i Symbian OS sprawa jest prosta. Są to nazwy systemów operacyjnych. Ten drugi stosowany jest w ogromnej ilości smartphone’ów, w szczególności, w bodaj wszystkich produkowanych przez Nokie. Windows CE to taki skrót dla określenia wszystkich systemów Windows opartych na jądrze CE. CE to właśnie jądro dla systemów Microsoft’u na palmtopy, tak jak NT jest jądrem dla Windows XP i innych przeznaczonych dla komputerów klasy PC. Sortując chronologicznie, kolejne wersje systemów z jądrem CE to: Pocket PC 2000, Pocket PC 2002, Windows Mobile 2003, Windows Mobile 2003 SE (akr. Second Edition), Windows Mobile 5 i najnowszy Windows Mobile 6. Do tego dochodzą jeszcze różne edycje. Dla najnowszej wersji są to Classic (dla palmtopów bez telefonu), Professional (dla palmtopów z telefonem) oraz Standard (dla smartphone’ów).

Ostatnia kategoria to terminy związane z platformą Java dla urządzeń mobilnych, są to: J2ME, Java ME, CDC, CLDC i MIDP. Jest to sedno moich zainteresowań i z pewnością coś na ten temat będę jeszcze pisał. Póki co sam wiem nie wiele więc się powstrzymam.

3 października 2007

Aliasy dla nazw importowanych klas

Jakiś czas temu przyszedł mi do głowy bardzo prosty i zdawałoby się genialny w swej prostocie pomysł, aby umożliwić nadawanie aliasów importowanym klasom. Wyobraźmy sobie kod, który jednocześnie używa wielu klas o tej samej nazwie a pochodzących z różnych pakietów. Przykładowo java.sql.Date i java.util.Date. Póki co nie ma wyjścia – musimy używać w pełni kwalifikowanych nazw klas. Rozwiązanie problemu wydaje się oczywiste – wystarczyło by udostępnić składnie zademonstrowaną poniżej lub jakąś analogiczną:

import java.util.Date;
import java.sql.Date as SqlDate;

Znaczenie takiej konstrukcji jest chyba oczywiste. Co więcej, takie rozszerzenie języka jest bardzo łatwe do zaimplementowania, jako że jest to tylko lukier syntaktyczny (składniowy). Pomysł ten powrócił do mnie dzisiaj i tym razem zadałem sobie trud sprawdzenia, co Internet ma do powiedzenia w tej sprawie. Okazuje się, że pomysł mój jest całkiem rozsądny, jako że ten sam pomysł mieli też inni ludzie i co więcej zarejestrowali go jako propozycję rozszerzenia (ang. request for improvement) języka Java. Problem był zgłaszany kilkukrotnie, ale najbliżej tego, co powyżej przedstawiłem jest zgłoszenie w BugDatabase numer 4194542. Skoro pomysł jest rozsądny i łatwy w implementacji to czemu jeszcze tego nie mamy? Po lekturze komentarzy pod propozycjami wprowadzenia tego lub podobnego rozszerzenia mogę wskazać dwa kontrargumenty. Pierwszy natury praktycznej – te same klasy mogły by mieć różne nazwy w różnych fragmentach kodu, jako że nic nie mogłoby powstrzymać nas od nadania tej samej klasie różnych aliasów w zależności od sytuacji. Jest to niewątpliwie kontrargument wart rozważenia. Drugi to już bardziej filozofia – należy rozważnie wprowadzać nowe elementy składni, aby język nie rozrósł się zanadto. Język Java ma już jednak w tej chwili kilka konstrukcji składniowych, które można by, kierując się tym tokiem rozumowania, uznać za zbędne. W wersji 5 (tj. 1.5) dodano kilka kolejnych. Przykładem niech będzie instrukcja warunkowa (dostępna bodaj od początku), np.:

return yourAge() > 50 ? "An old man" : "A young person";

Z konstrukcji tej można by przecież było zrezygnować, a powyższy kod zastąpić kodem używającym konstrukcji warunku:

if( yourAge() > 50 )
return "An old man";
else
return "A young person";

Jestem ciekaw, jakie są opinie polskich programistów na ten temat. Jesteście "za a nawet przeciw"? Widzicie jeszcze jakieś kontrargumenty?

4 września 2007

Integracja JSF 1.2 i Spring z pomocą Red Hat Developer Studio

Artykuł ten jest właściwie nieco rozwlekłym komentarzem do artykułu "Próba połączenia JSF i Spring" napisanego przez Mariusza Wójcika. Mariusz pokazał w nim jak zintegrować ze Springiem JSF 1.1, jednak trochę się pozmieniało i integracja z JSF 1.2 wymaga pewnego komentarza. Przy okazji pokaże pewne dobroci nowego IDE ze stajni Red Hat. W zasadzie wystarczyło by powiedzieć, że tak jak w przypadku JSF 1.1 należało w pliku faces-config.xml umieścić konfigurację:

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

tak w przypadku JSF 1.2 trzeba użyć Spring’a w wersji co najmniej 2.1 i zamiast powyższego umieścić tam:

<application>
<el-resolver>
org.springframework.web.jsf.el.DelegatingFacesELResolver
</el-resolver>
</application>

ale przydałoby się jeszcze przetestować że to działa. Przetestujemy to poprzez modyfikację przykładowego projektu dostępnego w Ret Hat Developer Studio, o którym napisałem w artykule "Już jest Red Hat Developer Studio". Zaczynamy więc od utworzenia projektu JSF 1.2. Uruchamiamy kreatora New > Project > JSF Project. Wybieramy opcje jak na poniższej ilustracji i klikamy Next:



Upewnijmy się teraz, że jako Runtime wybraliśmy serwer Tomcat (który wcześniej należało zdefiniować) a nie JBoss. Klikamy Finish i mamy działający projekt JSF 1.2 + Facelets. Zanim przejdziemy do integracji ze Spring’iem upewnijmy się, że projekt działa. W tym celu najwygodniej jest użyć specjalnego guziczka dostępnego w naszym IDE. Kliknięcie go powoduje odpalenie wbudowanej w Eclipse przeglądarki z URL’em aktywnego (otwartego w drzewie zasobów) projektu. Widać to na poniższej ilustracji:



Ponieważ chcemy, aby nasz przykład był możliwie prosty ograniczymy się do przeniesienia elementu zarządzanego (ang. managed bean) z zakresu JSF do zakresu Spring. Z pliku faces-config.xml usuwamy więc fragment:

<managed-bean>
<managed-bean-name>person</managed-bean-name>
<managed-bean-class>demo.Person</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>name</property-name>
<value />
</managed-property>
</managed-bean>

Odpowiadającą definicję będziemy musieli umieścić w kontekście Spring’a, tworzymy więc odpowiedni plik definujący ten kontekst. Klikamy prawym guzikiem na katalogu WEB-INF i wybieramy New > Other > Spring Bean Definition > Next. Teraz podajemy nazwę pliku. Standardowo powinno to być applicationContext.xml, ale może być też cokolwiek innego. Ja wybieram spring.xml. Klikamy Finish. Uwaga! Plik się utworzył poprawnie, ale okienko kreatora się nie zamknęło (taki mały błąd), aby je zamknąć możemy kliknać Cancel. Do nowo utworzonego pliku dodajemy definicję elementu zarządzanego Spring, odpowiadającą temu, co usunęliśmy z JSF:

<bean id="person" class="demo.Person">
<property name="name" value="Name" />
</bean>

Aby plik kontekstu Spring został wczytany dodajemy do pliku web.xml odpowiednią konfigurację pokazaną poniżej. Jeśli nasz plik znajduje się w katalogu WEB-INF i nazywa się applicationContext.xml to pokazany element context-param nie jest konieczny:

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

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring.xml</param-value>
</context-param>

Musimy jeszcze do projektu dodać bibliotekę Spring (spring.jar) w wersji co najmniej 2.1 oraz do pliku faces-config.xml dodać, o ile jeszcze tego nie zrobiliśmy, element el-resolver pokazany na początku artykułu i gotowe. O tym, że działa przekonujemy się widząc jako proponowane imię wartość "Name" zdefiniowaną w konfiguracji Spring’a:


30 sierpnia 2007

Już jest Red Hat Developer Studio

Wreszcie pojawiło się niecierpliwie oczekiwane Red Hat Developer Studio, wersja beta 1. Co to jest? IDE zbudowane na bazie Eclipse IDE, a konkretnie na bazie Exadel Studio Pro, znakomitego produktu należącego do kupionej niedawno przez Red Hat firmy Exadel. Co takiego wspaniałego ma to IDE? Wygląda na to, że nie tak wiele ponad to, co miał produkt Exadel’a, ale to już wystarczy by być numerem 1. Pakiet instalacyjny Red Hat Developer Studio jest dość pokaźnych rozmiarów, wersja pod Windows to aż 524MB, ale jest tam nie tylko IDE. Pakiet zawiera także serwer aplikacyjny JBoss Application Server 4.2. Instalator pyta nas czy chcemy zainstalować ten serwer czy tylko IDE, więc przeciwnicy paczek w stylu wszystko w jednym nie powinni bardzo narzekać, a ci którzy chcą używać JBoss AS mają ułatwione zadanie. Dobrą wiadomością dla wielbicieli nowości jest, że IDE zostało zbudowane na Eclipse w wersji 3.3. Dla nieświadomych użytkowników Exadel Studio mam jeszcze jedną wiadomość. Dziś, tj. 30 sierpnia wygasła licencja. Co prawda można rozwijać istniejące projekty, ale tworzenie nowych nie jest już możliwe, co widać na poniższej ilustracji.

8 sierpnia 2007

Dynamiczne zapytania z Apache iBATIS Data Mapper

Napisałem jakiś czas temu w artykule "Dynamiczne zapytania i selekcja poprzez przykład czyli Hibernate nokautuje JPA" o wsparciu dla dynamicznych zapytań oferowanym przez Hibernate i o braku takowego w specyfikacji JPA. Zajmuje się ostatnio porównywaniem różnych technologii trwałości danych dla Javy, więc w ramach porównania opisze dziś jak dynamiczne zapytania wspiera Apache iBATIS Data Mapper (iBATIS SQL Maps). Dla przypomnienia czym jest iBATIS polecam lekturę moich poprzednich artykułów na ten temat: "Wstęp do technologii Apache iBATIS Data Mapper", "Uruchamiamy aplikację JPetStore dla Apache iBATIS Data Mapper", "Niebanalny przykład mapowania dla Apache iBATIS Data Mapper".

Wyszukiwanie z użyciem technologii iBATIS Data Mapper realizujemy poprzez uruchomienie zapytania SQL zdefiniowanego w pliku XML. iBATIS wspiera definiowanie dynamicznych zapytań udostępniając odpowiednie znaczniki XML. Dla przykładu weźmy klasę:

public class Person {
private String firstName;

private String lastName;

// nie istotne szczegóły pomijam
}

Załóżmy, że klasa ta odwzorowana jest w bazie danych do tabeli i kolumn o nazwach analogicznych do odpowiednio nazwy klasy i nazw atrybutów z tą różnicą, że pisanych dla odróżnienia wielkimi literami. Dynamiczne zapytanie SQL wyszukujące obiekty klasy Person mogłoby wyglądać następująco:

SELECT * FROM PERSON

<dynamic prepend=”WHERE”>
<isNotNull prepend=”AND” property=”firstName”>
FIRSTNAME = #firstName#
</isNotNull>

<isNotNull prepend=”AND” property=”lastName”>
LASTNAME = #lastName#
</isNotNull>
</dynamic>

Naturalnie iBATIS oferuje również warunki inne niż isNotNull, ale to już szczegół, bowiem całość nie jest niestety zbyt ciekawa. Wsparcie dla dynamicznych zapytań jest mocno ograniczone. iBATIS nie wspiera w sposób jawny dynamicznej listy tabel i związanej z nimi listy warunków złączeniowych. Nie jest więc możliwe zdefiniowanie w ten sposób zapytania, którego kryteria odwzorowane są w drzewie powiązanych klas a nie w pojedynczej klasie. Nie da się więc zdefiniować dynamicznego zapytania dla formatki wyszukiwania faktur w której oprócz kryteriów dotyczących samej faktury takich jak wartość czy data wystawienia będziemy mieli kryteria dotyczące wystawcy czy odbiorcy.

2 sierpnia 2007

Wyniki ankiety 'Czy znasz i używasz JDO'

Jakiś czas temu zainteresowałem się specyfikacją JDO. Poszukałem, poczytałem, napisałem artykuł „Co to jest JDO i co nowego w JDO 2.0” i wywiesiłem ankietę. Wyniki poniżej. Głosów nie ma wiele, ale wystarczy by potwierdzić, że specyfikacja jest plus minus martwa. JDO bez wątpienia zasługuje na chwalebne miejsce w dziejach IT i Javy w szczególności, ale w chwili obecnej ja nie znajduję powodów, dla których warto by było inwestować w to swój czas i siły. Pewną szansą dla JDO może być jeszcze JPOX 1.2, a mówiąc precyzyjniej jego wsparcie dla obiektowej bazy danych DB4O które ma nadejść wraz z tą wersją. Obserwując jednak stopień frustracji zespołu JPOX w liczbie dwu osób można mieć wątpliwości czy wersja ta wyjdzie z fazy beta, a jeśli tak to czy jej użytkowanie nie będzie obarczone zbyt dużym ryzykiem wiążącym się z niepewnym jutrem. Ja z pewnością będę monitorował sytuację i być może przyjże się JDO jeszcze raz, w innym, czysto obiektowym świetle.

nie znam i nie interesuje mnie28 %
8
nie znam ale chętnie poznam48 %
14
znam troche3 %
1
znam dobrze ale nie lubię3 %
1
znam dobrze i lubię17 %
5

23 lipca 2007

Monitorowanie i modyfikacja ruchu HTTP z narzędziem WebScarab

Bywa tak, że to co w tutorialach i przykładach typu „witaj świecie” wygląda bardzo miło i działa bez zarzutu potrafi szydzić z naiwnych gdy przyjdzie do implementacji rzeczywistych systemów. Takim cichym szydercą stała się dla mnie ostatnio technologia związana z serwisami sieciowymi (ang. web services). Pierwszym krokiem w zwalczaniu przeciwności losu jest wyizolowanie i identyfikacja problemu. W przypadku serwisów sieciowych w stylu SOAP po HTTP niewątpliwym sprzymierzeńcem jest monitor HTTP, ale możemy chcieć czegoś więcej niż tylko możliwość monitorowania tego, co się dzieje. Dobrze jest móc także modyfikować w locie żądania i odpowiedzi HTTP a jeszcze lepiej, gdy możemy także zaprogramować modyfikacje automatyczną. Wszystko to i jeszcze więcej daje nam WebScarab, jest nawet wersja Java WebStart.

Zaczynamy od konfiguracji proxy. Przechodzimy do zakładki Proxy > Listeners. W polach Address i Port na dole okna wpisujemy adres i port, na którym Proxy będzie nasłuchiwał. W polu Base URL podajemy kompletny URL (łącznie z nazwą protokołu i portem) na który żądania HTTP będą przesyłane. Naciskamy guzik Start to prawej stronie okna.

Przekierowywanie i monitorowanie ruchu HTTP mamy załatwione. Przechodzimy do modyfikacji, na początek manualnej. W tym celu otwieramy zakładkę Proxy > Manual Edit, zaznaczamy interesujące nas metody HTTP. Zaznaczenie opcji Intercept requests spowoduje, że ilekroć WebScarab otrzyma żądanie będziemy poproszeni o podjęcie akcji. Mamy różne możliwości podglądu treści żądania, zaczynając od podglądu zupełnie nie formatowanego na parsowaniu zawartości XML skończywszy. Po zmodyfikowaniu lub tylko przejrzeniu treści żądanie przesyłamy dalej klikając Akcept changes. Zaznaczenie opcji Intercept responses wywoła analogiczny efekt dla odpowiedzi. Ilustracja poniżej.


No i wreszcie gwóźdź programu, modyfikacja programowa. Przechodzimy do zakładki Proxy > Bean Shell, zaznaczamy kontrolkę Enabled i voila! Twórcy WebScarab byli mili i napisali nawet kilka linijek kodu. Bazując na przykładach ze strony projektu rozszerzyłem go troche, tak że powinno już być w miarę jasne z czym to się je. Zastanawia mnie tylko, co to tak naprawdę jest. Wygląda zupełnie jak Java. Czyżby pokusili się o implementację interpretera Javy? Szczęśliwie nie musiałem drążyć zbyt głęboko żeby zaimplementować to co akurat było mi potrzebne. Ilustracja poniżej, mam nadzieje, że kod który na niej widać jest co nieco czytelny.

15 czerwca 2007

Dynamiczne zapytania i selekcja poprzez przykład czyli Hibernate nokautuje JPA

Najprościej i najwydajniej byłoby gdyby wszystkie zapytania, czy to SQL, HQL czy JPQL mogły być statyczne. Niestety, kiedy aplikacja umożliwia wyszukiwanie obiektów poprzez określenie tylko niektórych z możliwych do określenia wartości musi ona używać zapytań dynamicznych. Banalnym przykładem może być formatka wyszukiwania klienta, która umożliwia wyszukiwanie po numerze identyfikacyjnym, nazwisku, adresie, numerze telefonu itd., przy czym oczywiście możemy podać dowolną z tych wartości lub też dowolną ich kombinację. Uruchomienie takiego wyszukiwania koniec końców zakończyłoby się wygenerowaniem zapytania SQL w stylu:

SELECT * FROM Customer
WHERE firstName = 'Jan' AND phone = '504 101 102' AND street LIKE '%Słowackiego%'

a problem w tym, że nie wiemy a priori które właściwości klienta umieścić w klauzuli WHERE. Można by umieścić wszystkie właściwości, zamiast równości używać operatora LIKE a w miejsce nie podanych wartości wstawiać maskę wyszukiwania %, ale co z wydajnością takiego zapytania, zwłaszcza gdy pełne wyszukiwanie obejmowałoby kilka złączonych tabel? Zdaje się, że większość aplikacji nie obejdzie się bez zapytań dynamicznych. Jak więc je tworzyć? Najprostsze, najżmudniejsze, po prostu najgorsze, ale niestety czasami jedyne dostępne rozwiązanie polega na mozolnym zlepianiu treści zapytania w gąszczu instrukcji warunkowych. Dopóki jesteśmy w świecie JDBC, gdzie wszystko jest właśnie takie bez trudu akceptujemy ten balast, ale my nie chcemy by to był nasz świat i dlatego idziemy dalej.

Hibernate oferuje zgrabne rozwiązanie problemu dynamicznych zapytań udostępniając Hibernate Criteria API. Weźmy dla przykładu poniższe klasy:

public class Company {
private String name;

private Person representative;

// metody pomijam
}

public class Person {
private String firstName;

private String lastName;

// metody pomijam
}

Nie będę tu pisał podręcznika do Hibernate bo tych napisano już sporo. Poprzestanę na pokazaniu przykładów niezbędnych do kontynuowania artykułu. Załóżmy, że mamy obiekt klasy Company o nazwie templateCompany utworzony przez formatkę wyszukiwania firm. Kod realizujący dynamiczne wyszukiwanie firm spełniających zadane kryteria mógłby wyglądać tak:

Criteria criteria = session.createCriteria(Company.class)
.createAlias("representative", "rep");

if(templateCompany.getName() != null)
criteria.add(Restrictions.eq("name", templateCompany.getName()));

if(templateCompany.getRepresentative().getFirstName() != null)
criteria.add(Restrictions.eq("rep.firstName",
templateCompany.getRepresentative().getFirstName()));

if(templateCompany.getRepresentative().getLastName() != null)
criteria.add(Restrictions.eq("rep.lastName",
templateCompany.getRepresentative().getLastName()));

List<Company> companies = criteria.list();

Już jest nieźle, ale można całkiem fajnie, z użyciem mechanizmu selekcji poprzez przykład (ang. query by example). Ideą mechanizmu selekcji poprzez przykład jest, aby wyszukiwać obiekty nie poprzez uruchomienie zapytania, ale poprzez podanie obiektu przykładowego, stanowiącego wzorzec. Po zastosowaniu tej techniki powyższy przykład upraszcza się do:

Criteria criteria = session.createCriteria(Company.class)
.add(Example.create(templateCompany))
.createCriteria("representative")
.add(Example.create(templateCompany.getRepresentative()));

List<Company> companies = criteria.list();

Domyślnie działa to w ten sposób, że pod uwagę brane są tylko te właściwości obiektów używanych jako wzorzec, które są różne niż null. Trzeba jeszcze dodać, że powyższe mechanizmy można ze sobą dowolnie mieszać i łączyć. Muszę przyznać, że Hibernate mile mnie zaskoczył. Mechanizm jest naprawdę fajny, aczkolwiek niestety nie idealny. Rozszerzmy nieco nasz przykład, dodajmy kolejną klasę oraz jedną właściwość do klasy Company:

public class Company {
private String name;

private Person representative;

private Address address;

// metody pomijam
}


public class Address {
private String street;

private String city;

// metody pomijam
}

Jak tu teraz napisać kod umożliwiający wyszukiwanie firm także po adresie? Stosując podejście pierwsze, nieco rozwlekłe, wygląda to tak:

Criteria criteria = session.createCriteria(Company.class)
.createAlias("representative", "rep")

.createAlias("address", "address");

if(templateCompany.getName() != null)
criteria.add(Restrictions.eq("name", templateCompany.getName()));

if(templateCompany.getRepresentative().getFirstName() != null)
criteria.add(Restrictions.eq("rep.firstName",
templateCompany.getRepresentative().getFirstName()));

if(templateCompany.getRepresentative().getLastName() != null)
criteria.add(Restrictions.eq("rep.lastName",
templateCompany.getRepresentative().getLastName()));


if(templateCompany.getAddress().getStreet() != null)
criteria.add(Restrictions.eq("address.street",
templateCompany.getAddress().getStreet()));

if(templateCompany.getAddress().getCity() != null)
criteria.add(Restrictions.eq("address.city",
templateCompany.getAddress().getCity()));

List<Company> companies = criteria.list();

Kodu robi się sporo, ale przynajmniej mamy to, o co chodziło. Zawsze też można napisać jakąłś sprytną klasę pomocniczą. Niestety, stosując podejście drugie, selekcję poprzez przykład, taką jaką zaimplementowano ją w Hibernate Criteria API zrobić się tego po prostu nie da! I jest to tym bardziej drażniące, że wydaje się, iż wystarczyłaby drobna zmiana API. Mam cichą nadzieję, że jest to jednak moje a nie Hibernate’a przeoczenie i ktoś pokaże mi, że jednak to się zrobić da. No i wreszcie nokaut zapowiedziany w tytule artykułu. JPA nie oferuje żadnego takiego ani jakiegokolwiek innego mechanizmu wpierającego tworzenie dynamicznych zapytań. Problem ten stara się zaadresować projekt JPACriteria, http://jpacriteria.sourceforge.net/, ale jest on jeszcze we wczesnej fazie rozwoju.

9 czerwca 2007

Zbędne złączenia w zapytaniach SQL generowanych przez TopLink JPA

To, co dzisiaj opiszę to efekt moich doświadczeń z pracy z TopLink Essentials 2.0-b47-beta3 jako dostawcą Java Persistence API 1.0. Nie weryfikowałem, czy inne implementacje zachowują się analogicznie ani czy na ten temat mówi cokolwiek specyfikacja, ale zważywszy, że jest to szczegół implementacyjny wydaje mi się to wysoce nieprawdopodobne. No więc do rzeczy. Zerknijmy na poniższe klasy, encje JPA:

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long productId;

private String productDescr;

private ProductCategory productCategory;
}

@Entity
public class ProductCategory {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long categoryId;

private String categoryName;

@OneToMany(mappedBy = "productCategory", fetch = FetchType.LAZY)
private List products;
}

Nie istotne dla przykładu właściwości i metody pominąłem dla przejrzystości. Dla encji tych TopLink wygenerował następujące tabele i więzy w bazie danych:

CREATE TABLE PRODUCT (
PRODUCTID BIGINT NOT NULL,
PRODUCTDESCR VARCHAR(255),
PRODUCTCATEGORY_CATEGORYID BIGINT,
PRIMARY KEY (PRODUCTID)
)

CREATE TABLE PRODUCTCATEGORY (
CATEGORYID BIGINT NOT NULL,
CATEGORYNAME VARCHAR(255),
PRIMARY KEY (CATEGORYID)
)

ALTER TABLE PRODUCT
ADD CONSTRAINT FK_PRODUCT_PRODUCTCATEGORY_CATEGORYID
FOREIGN KEY (PRODUCTCATEGORY_CATEGORYID)
REFERENCES PRODUCTCATEGORY (CATEGORYID)

Napiszmy teraz zapytanie JPQL pobierające wszystkie produkty należące do danej kategorii. Oto pierwsza próba:

SELECT p FROM Product p WHERE p.productCategory.categoryId = :categoryId

No i w tym miejscu nabrałem wątpliwości. Czy aby na pewno JPA zadba o optymalizację? Czy zauważy, że złączenie z tabelą PRODUCTCATEGORY nie jest potrzebne do wyliczenia wyniku tego zapytania? TopLink we wspomnianej wersji nie zauważy i wygeneruje następujące zapytanie SQL:

SELECT t0.PRODUCTID, t0.PRODUCTDESCR, t0.PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT t0, PRODUCTCATEGORY t1
WHERE (t1.CATEGORYID = ?) AND (t1.CATEGORYID = t0.PRODUCTCATEGORY_CATEGORYID)

Hmmm… czyżby implementacja takiej z pozoru prostej optymalizacji była trudna? Korci mnie żeby zajrzeć jak to implementują inni, ale póki co wygrywa brak czasu, tym bardziej że wystarczy zmienić nieco nasze zapytanie i będzie jak trzeba. Poprawiona, równoznaczna semantycznie wersja zapytania JPQL poniżej:

SELECT p FROM Product p WHERE p.productCategory = :prodCat

Zmiana wydaje się oczywista, aczkolwiek do uruchomienia takiego zapytania nie wystarczy już identyfikator kategorii, będziemy potrzebowali obiektu kategorii. Jest to trochę irytujące zwłaszcza, że obiekt ten posłuży li tylko jako opakowanie do identyfikatora, ale z drugiej strony ratuje sytuację, bowiem jako parametr zapytania wystarczy podać obiekt new ProductCategory(categoryId). Upewnijmy się jeszcze, że teraz jest dobrze. Rzeczywiście, wygenerowane zapytanie SQL to:

SELECT PRODUCTID, PRODUCTDESCR, PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT
WHERE PRODUCTCATEGORY_CATEGORYID = ?