Dzisiejszy post będzie nietypowy: dużo kodu i kilka speszyl-haków:)
Pokażę jak szybko i raczej bezboleśnie generować PDFy na podstawie HTMLa + CSS3 z wykorzystaniem szablonów w wydaniu Facelets.
Założenia
Oczywiście do generowania PDF możemy podejść na wiele sposobów. Możemy np. użyć natywnego api lub nawet zastosować Open Office postawionego w trybie servera (bez GUI) i korzystać ze wszelkich narzędzi do tworzenia dokumentu, aby na końcu "zapisać jako" pdf.
Natomiast w tym poście zakładam, że chcemy rozwiązać sprawę najprościej jak się da, bez potrzeby studiowania autystycznej dokumentacji.
Generujemy PDF
Na początek potrzebujemy biblioteki: http://code.google.com/p/flying-saucer (w paczce mamy ITexta i xercesa). Flying saucer to wspaniałe narzędzie, które przykrywa iTexta wygodnym api: na wejściu podajemy urla do strony, a na strumieniu wyjściowym dostajemy PDFa. Strona może odwoływać się do CSS3, w którym definiujemy nagłówki, stopki, numerowanie stron, podział stron. Wszystko zostanie kolejno opisane w dalszej części.
Pdfy będziemy zwracać z następującego servletu:
@SuppressWarnings("serial")
@WebServlet("/programy_szkolen_pdf/*")
public class PdfServlet extends HttpServlet {
private static final String FONTS_PACKAGE = "fonts";
private static final String FONTS_DIR = "/" + FONTS_PACKAGE + "/";
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/pdf");
OutputStream os = response.getOutputStream();
String url = getUrl(request);
try {
ITextRenderer renderer = initRenderer();
renderer.setDocument(url);
renderer.layout();
renderer.createPDF(os);
} catch (Exception ex){
throw new ServletException(ex);
}
response.flushBuffer();
}
private String getUrl(HttpServletRequest request) {
String pdfServletPath = request.getServletPath();
String cp = request.getContextPath();
String program = request.getRequestURI();
program = program.replace(cp + pdfServletPath, "");
program = program.substring(0, program.length() - 4); // cut .pdf
String url = "http://localhost:8080" + cp + program + ".xhtm?pdf=true";
return url;
}
}
Jak widać API latającego spodka jest proste:
- posługujemy się obiektem rendererm który posiada stan
- ustawiamy mu URL do strony, którą chcemy skonwertować na PDF
- wołamy metodę układającą pdf
- wynik kierujemy do strumienia odpowiedzi servletu.
I to generalnie byłoby wszystko:)
Zostało kilka szczegółów...
Metoda getUrl w moim przypadku robi pewne oszustwo operując na ścieżkach
Użytkownik woła servlet:
/programy_szkolen_pdf/szkolenie-java-start.pdf
btw: zauważcie jak zamapowany jest servlet: @WebServlet("/programy_szkolen_pdf/*")
A servlet wskazuje latającemu spodkowi adres:
http://localhost:8080/strona/szkolenie-java-start.xhtm?pdf=true
Zakładam, że zasoby do renderowania znajdują się na tej samej maszynie, stąd localhost.
Do czego służy parametr pdf dowiemy się za chwilę:)
Mamy jeszcze nieco smutnych szczegółów technicznych w naszym latającym spodku.
Renderera trzeba zainicjować - metoda initRenderer.
Jeżeli w PDF chcemy używać polskich (lub jakichkolwiek innych barbarzyńskich znaków), to musimy zarejestrować w rendereze fonty, których używamy w CSS (Arial, Verdana, cokolwiek). Służy do tego metoda renderer.getFontResolver().addFont
Ja napisałem prosty automat, metodę findFonts(), który automatycznie wyszukuje plików z fontami w *pakiecie* zdefiniowanym w stałej FONTS_PACKAGE. Chodzi o to aby uniknąć konfigurowania ścieżek. Po prostu skanujemy pakiet z class path.
Zatem do odpowiedniego pakietu należy sobie wkopiować (np z systemu) fonty. Oczywiście katalog powinien być niedostępny z sieci aby nie być posądzonym o dystrybucję komercyjnych fontów - jeżeli takowych używacie.
private ITextRenderer initRenderer() throws IOException, DocumentException{
List fonts = findFonts();
ITextRenderer renderer = new ITextRenderer();
for (String font : fonts) {
renderer.getFontResolver().addFont(FONTS_DIR + font,
BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}
return renderer;
}
private List findFonts() throws IOException {
Enumeration resources = getClass().getClassLoader().getResources(FONTS_PACKAGE);
URL resource = resources.nextElement();
File directory = new File(resource.getFile());
List fonts = new ArrayList();
if (!directory.exists()) {
return fonts;
}
File[] files = directory.listFiles();
for (File file : files) {
fonts.add(file.getName());
}
return fonts;
}
Szablony
Dlaczego Facelets i
JSF 2?
Do tej pory różnego rodzaju strony tworzyłem w PHP z wykorzystaniem
Smarty. Po kilku doświadczeniach uznałem to za... powiedzmy niewygodne.
Chcę być dobrze zrozumiany - Smarty są ok dla pewnej klasy problemów: np wówczas gdy mamy kontent w bazie danych (jakiś CMS), pobieramy kontent jako wiersze i rozsmarowujemy po po "dziurkach" w szablonie. Wówczas imperatywne API Smarty (w postaci obiektu, na którym wołamy metody definiujące zmienne szablonu) jest jak najbardziej ok.
Natomiast ja w klasach problemów typu strona firmowa, preferuję przechowywać kontent (strzępy konwentu) w plikach XHTML. W takim wypadku praca z imperatywnym API to po prostu pomyłka. Jako "weteran" aplikacji webowych czuję się jak ryba w wodzie poruszając się w kodzie. Baza i CMS jedynie mnie ogranicza.
Dlatego tworząc nową wersję strony formowej zdecydowałem się na coś... delikatnie mówiąc... zastawiającego. Użycie JSF - standardu prezentacji enterprise (która ma swoich fanów jak i anty-fantów) do stworzenia strony www :O
Okazało się, że jeżeli nie używać formularzy (pomijamy większość faz cyklu obsługi żądania), nie używać ManagedBeanów, a jedynie składania kawałków XHTMla to okazuje się, bo jest to bardzo produktywne narzędzie.
Przykład:
strona
http://bottega.com.pl/szkolenie-jsf-2
wygląda jak każdy widzi.
Jest wyraźnie podzielona na kontent merytoryczny i ozdobniki w postaci nagłówka, stopki i paneli bocznych.
A tak wygląda jej kod:
Java Enterprise Edition
JSF 2
Java Server Faces 2
Projektanci, programiści
2 dni
50% wykłady / 50% warsztaty
Program został skontrowany tak aby przedstawić zagadnienia w kontekście konkretnych problemów, które ilustrują praktyczne wykorzystanie każdej z funkcjonalności JSF.
Program szkolenia zawiera rozszerzenie o najlepsze praktyki projektowe i architektoniczne.
Tematyka szkolenia obejmuje wszystkie cechy frameworka JSF 2.0 istotne z punktu widzenia developera aplikacji.
Znaczniki ui:definie definiują wartości zmiennych, które będą używane w szablonie.
Wartości to czysty kod XHTML, który jest na bieżąco validowany przez edytor.
Stronę możemy zawołać na 3 sposoby:
- podając jej url:
http://bottega.com.pl/szkolenie-jsf-2
- renderując ją do pdf:
http://bottega.com.pl/programy_szkolen_pdf/szkolenie-jsf-2.pdf (wówczas nasz servlet przekaże adres http://bottega.com.pl/szkolenie-jsf-2.xhtm?pdf=true) do Flying Saucera
- zagnieżdżając w pływającej ramce na facebooku podając adres
http://bottega.com.pl/szkolenie-jsf-2.xhtm?fb=true
W każdym z tych przypadków oczekujemy innego wyglądu.
Zatem znacznik ui:composition wskazuje szablon: template="/layout/training_template.xhtml"
Plik ten układa w odpowiednich miejscach parametry szkolenia. Szablon ten odpowiada jedynie za fragment strony (układ info o szkoleniu), ale nie zawiera np. nagłówka, stopki, ani bocznych boxów.
Szkolenie:
Informacje ogólne
Jak widać, szablon ten definiuje 2 zmienne: title i body. Wartości tych zmiennych zawierają w sobie XHTMLa oraz wartości zmiennych z dekorowane pliku - znacznik ui:insert.
Warunkowe renderowanie
Pomarańczowy przycisk "Zapytaj o szkolenie" jest niepożądany w przypadku wersji PDF i facebook, dlatego został warunkowo renderowany, przy pomocy panelGroup i atrybutu rendered:
Bean UrlChecker będzie opisany już za chwilę.
Wybór szablonu docelowego
Szablon szkolenia, nie jest szablonem "ostatecznym". Jest on dekorowane znowuż przez szablon wybierający szablon główny: template="/layout/dispatching_template.xhtml"
Jest to warstwa pośrednia pozwalająca w jednym miejscu przechowywać kod, który potencjalnie ulegnie zmianie. Tak oto wygląda dispatching_template.xhtml:
Jak widać nie dodaje żądnej wizualizacji a jedynie deleguje dekorowanie do kolejnego szablonu. Z tym, że nie mamy tutaj konkretnej ścieżki a jedynie wyrażenie wyliczane w Javie (tak w normalnym języku programowania a nie w jakimś pokracznym języku opartym o znaczniki):
@ManagedBean
public class UrlChecker {
public String getTemplateName(){
if (isPdf())
return "template_pdf";
if (isFacebook())
return "template_facebook";
return "template";
}
public boolean isStandard(){
return ! (isPdf() || isFacebook());
}
public boolean isPdf(){
return "true".equals(getParameter("pdf"));
}
public boolean isFacebook(){
return "true".equals(getParameter("fb"));
}
private String getParameter(String name){
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
return externalContext.getRequestParameterMap().get(name);
}
}
Zatem jeżeli zawołamy naszą stronę w ten sposób:
http://bottega.com.pl/szkolenie-jsf-2.xhtm
to w wyniku otrzymamy zawartość dekorowaną szablonem template_pl, który zawiera banner, stopę i boczne boxy
jeżeli natomiast w ten sposób:
http://bottega.com.pl/szkolenie-jsf-2.xhtm?pdf=true
to szablon dekorujący template_pdf_pl doda nagłówek i stopkę dla dokumnetu
a takie wywołanie
http://bottega.com.pl/szkolenie-jsf-2.xhtm?fb=true skutkuje zastosowaniem niemal niewidocznego szablonu dla facebooka.
Podsumowanie dekorowania
Mamy zatem następującą hierarchię treści:
szablon: szablon www | szablon pdf | szablon facebook
szablon: dispatcher
szablon: training
strona: konkretne szkolenie
Czyli na najwyższym poziomie mamy trzy perspektywy (szablony) spojrzenia na dane merytoryczne zawarte w plikach opisujących szkolenie.
Inne strony nie będące programem szkoleń po prostu pomijają szablon training_template i są dekorowane wprost szablonem dispatcher template
Układ dokumentu PDF w HTML i CSS3
Składanie stron w HTML jest dosyć łatwe. Wręcz banalne w porównaniu z natywnym API lub autyzmem Open Office.
Szablon pdf to zwykły XHTML:
<head>
</head>
<body>
!!! TUTAJ trzy warstwy opisane poniżej !!!
<</body>
Style po komentarzy "Specific Elements" są *kluczowe*.
Wewnątrz pierwszej warstwy mamy kolejno kolejno:
nagłówek:
stopkę:
zawartość:
Na zakończenie bonus dla wytrwałych
Jak wołać w JSF 2 metody z parametrem? Zapewniam, że może się przydać w bardziej wyrafinowanych szablonach:)
Tak robimy to w
Seam od 5 lat:
Natomiast w czystym JSF trzeba wykonać kilka prostych kroków aby uzyskać możliwość stosowania powyższej składni:
http://ocpsoft.com/java/jsf2-java/jsf2-how-to-add-the-magic-of-el-el2-to-jsf/
//====================================
Mam nadzieję, że udało mi się pokazać, że w Javie można efektywnie a przy tym czysto i elegancko zbudować stronę.
Przy innej okazji pokażę jak Filtry Servletów w dosłownie kilku linijkach biją na głowę autystyczne htaccess:)
A Smarki?... Już nigdy więcej:)