W jednym z wpisów dotyczących aplikacji AnimalShelter pisałem w jaki sposób przy pomocy Thymeleaf można wygenerować plik PDF. Do ich tworzenia wykorzystywałem szablony HTML, która następnie uzupełniałem treścią. Natomiast niedawno natrafiłem na inny, równie ciekawy sposób kreowania PDFów. Zamiast samemu pisać kod szablonu możemy wykorzystać do tego dedykowany program. Właśnie takim rozwiązaniem jest Jaspersoft Studio. Wykorzystując interfejs graficzny jesteśmy w stanie zaprojektować wygląd naszego pliku PDFa poprzez wypełnienie go tabelkami, wykresami czy napisami. Na jego postawie Jasper stworzy nam szablon w specjalnym formacie JRXML, który następnie wykorzystamy w kodzie naszej aplikacji napisanej w Javie. Na koniec pozostanie nam uzupełnienie go interesującą nas treścią i przy pomocy silnika biblioteki wygenerowanie pliku wynikowego.

Zadanie do wykonania

Sprawdźmy zatem jak to wygląda w praktyce. Naszym zadaniem będzie wygenerowanie pliku PDF, na którym umieścimy tytuł, dwie tabelki oraz informację o aktualnej dacie. Wygląda to na standardowe wymaganie, jednak w tym przypadku wykorzystamy wyżej przedstawiony sposób. Zaczniemy jak zawsze od utworzenia nowego projektu. Będzie on oczywiście oparty o Mavena. Dodajmy do niego niezbędną zależność do biblioteki Jasper przy pomocy, której będziemy w stanie wygenerować docelowy plik PDF. Utworzymy również od razu dwie klasy DTO przechowujące dane do zapisania.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
      http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>pl.devcezz</groupId>
  <artifactId>jasper</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>

    <jasper.reports.version>6.19.1</jasper.reports.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>net.sf.jasperreports</groupId>
      <artifactId>jasperreports</artifactId>
      <version>${jasper.reports.version}</version>
    </dependency>
  </dependencies>
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package pl.devcezz.jasper;

public class FootballPlayer {

  private static Long NEXT_ID_PLAYER = 1L;

  private final Long id;
  private final String firstname;
  private final String lastname;
  private final Position position;

  private FootballPlayer(
      Long id, 
      String firstname, 
      String lastname, 
      Position position) {
    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.position = position;
  }

  public static FootballPlayer of(
      String firstname, 
      String lastname, 
      Position position) {
    return new FootballPlayer(
        NEXT_ID_PLAYER++, 
        firstname, 
        lastname, 
        position);
  }

  public Long getId() {
    return id;
  }

  public String getFirstname() {
    return firstname;
  }

  public String getLastname() {
    return lastname;
  }

  public Position getPosition() {
    return position;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package pl.devcezz.jasper;

public class FootballClub {

  private static Long NEXT_ID_CLUB = 1L;

  private final Long id;
  private final String name;
  private final String country;

  private FootballClub(Long id, String name, String country) {
    this.id = id;
    this.name = name;
    this.country = country;
  }

  public static FootballClub of(String name, String country) {
    return new FootballClub(NEXT_ID_CLUB++, name, country);
  }

  public Long getId() {
    return id;
  }

  public String getName() {
    return name;
  }

  public String getCountry() {
    return country;
  }
}

Wykorzystanie biblioteki

Skoro mamy już przygotowaną podstawę zadania przyszła pora na napisanie głównej części programu. W jej ramach utworzymy dwie listy zawierające piłkarzy oraz ich kluby, które następnie przekażemy do specjalnych kolekcji Jaspera JRBeanCollectionDataSource. Dzięki niej biblioteka będzie widziała w jaki sposób obsłużyć ten typ obiektu. Następną instrukcją jest utworzenie mapy posiadającej klucze w postaci literału, które określają nazwę zmiennej wykorzystywanej przez Jaspera, oraz wartości będące danymi do wyświetlenia.

Kolejnym krokiem jest skompilowanie szablonu, który jest w postaci specjalnego rozszerzenia JRXML. To właśnie go będziemy za chwilę tworzyć w Jaspersoft Studio. W następnej linijce wypełniamy raport wcześniej zdefiniowanymi danymi przy pomocy statycznej metody fillRaport z klasy JasperFillManager. Na sam koniec pozostaje nam już tylko eksport raportu do pliku PDF. Stworzony kod jest krótki i na naprawdę przejrzysty.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package pl.devcezz.jasper;

import net.sf.jasperreports.engine.JREmptyDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

import static pl.devcezz.jasper.Position.ATTACKER;
import static pl.devcezz.jasper.Position.DEFENDER;
import static pl.devcezz.jasper.Position.MIDFIELD;

public class JasperMain {

  private static final String FILE_NAME = "src/main/resources/jasper-design.jrxml";
  private static final String OUT_FILE = "src/main/resources/football-report.pdf";

  public static void main(String[] args) throws JRException, FileNotFoundException {
    List<FootballPlayer> footballPlayers = List.of(
        FootballPlayer.of("Leo", "Messi", ATTACKER),
        FootballPlayer.of("Mason", "Mount", MIDFIELD),
        FootballPlayer.of("Mats", "Hummels", DEFENDER)
    );
    List<FootballClub> footballClubs = List.of(
        FootballClub.of("Paris Saint-Germain", "France"),
        FootballClub.of("Chelsea London", "England"),
        FootballClub.of("Borussia Dortmund", "Germany")
    );

    JRBeanCollectionDataSource footballPlayersCollectionDataSource =
        new JRBeanCollectionDataSource(footballPlayers);
    JRBeanCollectionDataSource footballClubsCollectionDataSource =
        new JRBeanCollectionDataSource(footballClubs);

    Map<String, Object> parameters = Map.of(
        "title", "Players and their current clubs",
        "footballPlayersDataSource", footballPlayersCollectionDataSource,
        "footballClubsDataSource", footballClubsCollectionDataSource
    );

    JasperReport jasperDesign = JasperCompileManager.compileReport(FILE_NAME);
    JasperPrint jasperPrint = JasperFillManager.fillReport(
        jasperDesign,
        parameters,
        new JREmptyDataSource());

    File file = new File(OUT_FILE);
    OutputStream outputSteam = new FileOutputStream(file);
    JasperExportManager.exportReportToPdfStream(jasperPrint, outputSteam);

    System.out.println("Football report successfully generated!");
  }
}

Trzeba zwrócić uwagę na fakt, że do wykreowania mapy wykorzystaliśmy statyczną metodę of tejże kolekcji. W ten sposób do zmiennej parameters został przypisany niemutowalny obiekty, z którymi Jasper nie do końca sobie radzi. Gdybyśmy na tym poprzestali to nasz program zakończyłby się niepowodzeniem. Z tego powodu skorzystaliśmy z konstruktora HashMap, aby utworzyć jego odpowiednik w postaci mutowalnej. Nie do końca wiem skąd wynika to ograniczenie, być może biblioteka po drodze dorzuca swoje dane do naszej mapy.

Niemutowalna mapa blokuje nam wygenerowanie raportu PDF
Niemutowalna mapa blokuje nam wygenerowanie raportu PDF

Tworzenie szablonu

W 25 linii powyższego kodu odwołujemy się do pliku jasper-design.jrxml, jednak na ten moment on nie istnieje. Trzeba go zatem stworzyć we wcześniej wspomnianym programie Jaspersoft Studio. Wchodzimy, więc na stronę twórców i ściągamy aktualnie dostępną wersję. Instalujemy i uruchamiamy go. Klikamy File -> New -> Jasper Report i z listy dostępnych opcji wybieramy Blank A4. Nadajemy nazwę dla pliku i przechodzimy dalej aż do końca. Naszym oczom powinien ukazać się następujący ekran.

Przygotowany Jaspersoft Studio do pracy na szablonem PDF
Przygotowany Jaspersoft Studio do pracy na szablonem PDF

Kolejnym krokiem jest zdefiniowanie dla naszego szablonu parametrów, z których ma on skorzystać. Muszą one mieć taką samą nazwę jak klucze mapy parameters z kodu - title, footballPlayersDataSource, footballClubsDataSource. Aby to uczynić wybieramy prawym przyciskiem myszy pozycję Parameters z sekcji Outline, znajdującej się po lewej na dole, i klikamy Create Parameter. Dla title przypisujemy typ String, natomiast dla dwóch list klasę JRBeanCollectionDataSource. Skoro mamy do czynienia z kolekcjami to musimy nauczyć Jasper jakie pola mają ich elementy. Robimy to poprzez kliknięcie na głównym elemencie w tej samej sekcji Outline. Odpowiednio je nazywamy i wybieramy opcję, aby stworzyć pusty zbiór danych. Teraz tworzymy odpowiednie pola dla nowopowstałych zbiorów FootballPlayer oraz FootballClub. Wynik naszego działania powinien wyglądać następująco.

Niezbędne parametry dla szablonu danych Jaspera
Niezbędne parametry dla szablonu danych Jaspera

Warto dodać, że pole position to typ wyliczeniowy, o którym nasze studio nie wie. Jeśli zostawilibyśmy je jako String to nasz program Javowy rzuciłby wyjątkiem. Aby uniknąć takiej sytuacji nadaliśmy jej klasę java.lang.Object. Jest to pewnego rodzaju obejście, które zadziała prawidłowo. Natomiast pewnie przy bardziej skomplikowanych enumach należałoby lepiej przestudiować problem.

Wypełnianie szablonu sparametryzowanymi elementami

Na starcie zajmijmy się tytułem naszego raportu. Przeciągamy, więc stworzony wcześniej parametr title na centralną cześć ekranu do sekcji Title. Przy pomocy opcji znajdujących się w prawym dolnym rogu oraz interfejsu graficznego możemy dostosować wygląd i umiejscowienie zmiennej tytułu. Przy okazji usuńmy niepotrzebne sekcje. Ja zdecydowałem się na wyrzucenie Page Header, Column Header, Column Footer i Page Footer.

Przyszła pora na dodanie pierwszej z tabelek. Z palety w prawym górnym rogu wybieramy element Table, który przeciągamy do centralnej części na Detail 1. W kreatorze na początku wybieramy jeden z dostępnych datasetów, zaznaczamy ‘Use a Datasource expression’ i z parametrów bierzemy ten odpowiadający naszemu zbiorowi.

Okienko do wyboru parametru odpowiadającemu datasetowi
Okienko do wyboru parametru odpowiadającemu datasetowi

Kolejny ekran wyświetli nam listę pól jaką chcemy uwzględnić w tabelce. Oczywiście zaznaczamy wszystkie i klikamy ‘Next’. Naszym oczom ukaże się kreator layoutu. Po kilku zmianach wizualnych kończymy pracę nad tabelką. Ten schemat powtarzamy dla drugiego zbioru danych z klubami piłkarskimi. Nie będę już tłumaczył jak można dojść do efektu poniżej, dlatego zachęcam po prostu do popróbowania możliwości jakie daje nam Jaspersoft Studio.

Finalny wygląd szablonu raportu PDF
Finalny wygląd szablonu raportu PDF

Przy okazji nadmieńmy, że pojawił się jeszcze element odpowiedzialny za wyświetlanie daty. Nadaliśmy mu odpowiedni format ‘yyyy-MM-dd’ i umieściliśmy w sekcji Summary.

Efekt prac

Skoro nasz szablon jasper-design.jrxml został skończony to możemy zapisać efekt prac i skopiować go do programu. Plik oczywiście wklejamy do katalogu resources. Nadeszła chwila, w której to uruchamiamy program czego wynikiem będzie plik o nazwie football-report.pdf.

Plik PDF będący wynikiem działania naszej aplikacji
Plik PDF będący wynikiem działania naszej aplikacji

Podsumowanie

Jasper daje nam ciekawą alternatywę do tworzenia plików PDF. Możemy tworzyć szablony przy pomocy programu graficznego nie zagłębiając się w tajniki HTML i CSS. Dla mnie jest to duży plus, ale jednak Jaspersoft Studio jest dosyć toporny i mało intuicyjny. Co prawda łatwo możemy stworzyć proste struktury, ale żeby je lepiej umiejscowić w raporcie musimy się nieźle napracować. Dodatkowo stylizowanie elementów porozrzucane jest po wielu zakładkach przez co ciężko mi było zmienić chociażby kolor czcionki. Pozostawiam, więc Tobie wybór z czego chciałbyś lub chciałabyś skorzystać. Mam nadzieję, że artykuł dał Ci szersze spojrzenie na możliwości rozwiązania tego samego problemu jakim jest generowanie pliku PDF.

Cały kod znajdziesz tutaj: https://github.com/cezarysanecki/code-from-blog