31 stycznia 2009

Raporty w Grails - rozdział 10 z "Beginning Groovy and Grails"

Pewnie już widać koniec książki, bo końcowe zagadnienia sprawiają wrażenie bardzo wyrafinowanych. Właśnie! Czy ja napisałem "wyrafinowanych"?! I tu jest cały pies pogrzebany! Właśnie takie przeświadczenie towarzyszy nam, kiedy rozważamy funkcjonalność raportowania czy przetwarzania wsadowego. Są wartościowe, ale tak rzadko rozważane, że zazwyczaj podchodzimy do nich na samym końcu uważając, że są nieciekawe technologicznie. Sam tego doświadczałem, więc i Wam przypiąłem taką łatkę ;-) Po lekturze rozdziału 10. "Reporting" z książki Beginning Groovy and Grails: From Novice to Professional widzę jak bardzo się myliłem. Czy ja nie pisałem już, że "czytanie książek poszerza horyzonty" (mimo uwag, w felietonie w ostatnim wydaniu ComputerWorld Polska "Lektura nieobowiązkowa" - "Czytasz książeczki, masz los kiepski" albo "Wynika z tego, że prawdziwa wiedza przychodzi z obserwacji życia, a nie z książek." - są to jedynie wycinki i proszę potraktować je z przymróżeniem oka)?!

Wracając do tematu...

W rozdziale 10. "Reporting" możemy przeczytać jak przyjemnym tematem jest wdrożenie funkcjonalności raportowania w Grails z użyciem otwartego JasperReports. Integracja między Grails a JasperReports będzie niezwykle dynamiczna - korzystamy z już i tak dynamicznego Grails (dzięki Groovy) z wykorzystaniem mechanizmu dynamicznego wywoływania metod (ala mechanizm prześwietlania w Javie - ang. Java Reflection).

Wymagania stawiane rozwiązaniu raportującemu to możliwość tworzenia wielu raportów, w różnych formatach - PDF, HTML, TXT, RTF, XLS, CSV oraz XML - w podejściu DRY (ang. Don't Repeat Yourself - oddzielenie odpowiedzialności i enkapsulacja) na bazie modelu i dynamicznego wywoływania metod w Grails. Pojawia się bardzo skomplikowany diagram powiązań (związków) między poszczególnymi elementami aplikacji z kontrolerami, usługą i widokami. Stworzony zostaje znacznik GSP g:report, który wskazuje na kontroler i jego akcję jako właśnie kontrolera całej interakcji w systemie, co ostatecznie kończy się przesłaniem raportu do użytkownika. Autorzy podkreślają rolę poszczególnych elementów systemu, aby kontroler ReportController jedynie kontrolował przepływ danych między składowymi systemu, w którym pobranie danych o zadaniach użytkownika leży w gestii kolejnego kontrolera TodoController z dynamicznie dołączanymi do klas domenowych metodami bazodanowymi. Usługa ReportService odpowiada za interakcję z JasperReports. Każdy ma swoją rolę w systemie. Jeśli ktokolwiek (przy tak skromnym opisie systemu) zadaje sobie pytanie, dlaczego ReportController przekazuje dane raportowe do raportu zamiast oczekiwać, że to raport będzie odszukiwał konieczne dane za pomocą SQL, spieszę z wyjaśnieniem, że jest to poruszone w książce i sprowadza się do łatwości (a właściwie jej braku) zarządzania SQLem. SQL jest ukryty pod klasami domenowymi w Grails, więc warto skorzystać z gotowego rozwiązania.

JasperReports jest rozwiązaniem raportującym, które składa się ze środowiska uruchmomieniowego JasperReports oraz edytora raportów iReport. Rozpoczynamy od definicji raportu w iReport, w którym możemy umieścić elementy graficzne oraz wykresy, z użyciem XML. Do szablonu raportu dołączane są dane i powstaje raport w formacie PDF, XML, HTML, CSV, XLS, RTF i TXT. Do wyrysowania raportów JasperReports korzysta z dodatkowych bibliotek. Sam motor JasperReports jedynie wiąże szablon ze źródłem danych, parametrami i konfiguracją do eksportera raportowego. Eksporter zwraca ByteArrayOutputSource do aplikacji. W przypadku Grails, wystarczy ustawić odpowiedni rodzaj treści (ang. content type) w odpowiedzi HTML i wysłać odpowiedź do przeglądarki.

JasperReports korzysta z Abstract Window Toolkit (AWT), więc uruchomienie go w niegraficznych środowiskach uniksowych wymaga parametru -Djava.awt.headless=true, np. w zmiennej JAVA_OPTS w Grails.

Integracja JasperReports z aplikacją grailsową rozpoczyna się od przekopiowania koniecznych bibliotek z iReport/lib do katalogu lib aplikacji grailsowej. One z kolei trafią do WEB-INF/lib podczas budowania aplikacji webowej.

Przykładowa aplikacja korzysta ze źródła danych JasperReports opartego na JavaBeans (ang. JavaBeans data source), tj. dane pobierane są z listy klas dziedzinowych przekazywanych do usługi ReportService. Utworzenie definicji raportu w iReport wymaga wskazania na klasy dziedzinowe przez zmienną CLASSPATH, które domyślnie (w Grails 1.0.x) znajdują się w USER_HOME/.grails/1.0.x/projects/<projekt_grailsowy>/classes. Na zakończenie pracy z iReport otrzymujemy szablon raportu jako plik jrxml. Kompilujemy szablon do pliku jasper, który kopiujemy do web-app/reports aplikacji.

Zgodnie z konwencją Grails utworzenie własnego znacznika GSP polega na utworzeniu klasy, której nazwa kończy się TagLib w grails-app/taglib. Znacznik tworzymy poleceniem grails create-tag-lib Report, które tworzy samą klasę znacznika oraz test integracyjny w test/integration. Sam znacznik jest domknięciem (metodą w klasie) z dwoma parametrami - mapą atrybutów znacznika attrs oraz jego zawartością (treścią) body. Zdumiewające jest, że wiele z tych informacji zabrakło w rozdziale poświęconym GSP - rozdział 5. "Building the User Interface" (relacja z lektury w Tworzenie interfejsu użytkownika w Grails - rozdział 5 z "Beginning Groovy and Grails"). W klasie znacznika skorzystano z dostępnego domyślnie (dzięki integracji ze Spring Framework) obiektu grailsAttributes - za jego pomocą możemy odczytać kontekst aplikacji - def appPath = grailsAttributes.getApplicationUri(request) czy out, który jest strumieniem wyjściowym.

Następnie tworzy się kontroler ReportController poleceniem grails create-controller Report. W kontrolerze możemy wykorzystać DI ze Spring Framework i wystarczy zadeklarować klasę usługi ReportService reportService jako pole instancji klasy, aby Grails (poprzez Springa) przekazał (wstrzelił) zależność automatycznie. W klasie kontrolera korzysta się z (Listing 10-4)
 ApplicationContext ctx = (ApplicationContext) session.getServletContext()
.getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT)
def controller = ctx.getBean("${params._controller}")
do pobrania (z kontekstu springowego) instancji kontrolera i dynamicznego wykonania metody na niej (Listing 10-4):
 def inputCollection = controller."${params._action}"(params)
Przywyknięcie do tego typu składania i wykonywania metod dynamicznie będzie mnie jeszcze kosztowało nielada wysiłku umysłowego.

Autorzy zwracają uwagę na (potencjalny) pomysł połączenia kontrolera ReportController z usługą ReportService i odradzają go, gdyż "the controller's purpose is to control, not do the actual work". Pamiętajmy o tym - kłania się wzorzec MVC. Tym samym cała wiedza o komunikacji system-JasperReports jest w jednym miejscu - usłudze. Nie jest ona trywialna, gdyż poszczególne formaty raportów wymagają specjalnej obsługi. Stworzenie usługi to wydanie polecenia grails create-service Report.

Na koniec ukłon w stronę alternatywnego rozwiązania - wdrożenia wtyczki grailsowej Jasper, która stała się inspiracją dla całego rozwiązania raportowego w tym rozdziale. Wtyczka opiera się na założeniu, że to raport pobiera dane za pomocą SQL. Instalacja wtyczki to grails install-plugin Jasper.