Генерируем отчеты с помощью FreeMarker

Инструментарий для генерации отчетов вариативен: JasperReports, BIRT, Pentaho. Наш backend-разработчик — Мари — решила попробовать альтернативный способ — FreeMarker. В этой статье она рассмотрела основные плюсы и минусы этого инструмента. Максимально необычно, но интересно :)

<i>Знакомьтесь, Мари :)</i>
Знакомьтесь, Мари :)

В одном из проектов, на исследование был получен FreeMarker. И предложен он был для генерации отчетов. Казалось бы, зачем?

Но интерес был сильнее, а времени на исследование и пробы было достаточно.

Генерируем отчеты с помощью FreeMarker

Итак, приступим.

Как говорит официальная документация:

Apache FreeMarker — это механизм шаблонов: библиотека Java для генерации текстового вывода (HTML-страницы, xml, файлы конфигурации, исходный код и. т.д).

<i>Представим наглядно</i>
Представим наглядно

Схема простая: у нас есть шаблон, в него мы можем сложить любые данные и получить любой доступный выходной формат (их вполне достаточно, чтобы выбрать подходящий).

<i>Табличка поможет разобраться что к чему</i>
Табличка поможет разобраться что к чему

В данной статье я расскажу обо всем, что нашла в рамках исследования.

Сегодня перед нами стояла задача разработки web-приложения, способного генерировать отчеты в двух форматах: .csv, .pdf.

Как мы уже знаем, сам FreeMarker так не умеет 🙁 Но для чего же нам тогда руки?

Начнем с более простой задачи. Судя по Википедии:

CSV — текстовый формат, предназначенный для представления табличных данных.

А значит, если соблюсти синтаксис формата, нам достаточно выходного формата plaintext.

Для примера будем генерировать отчеты по коммунальным счетчикам и их показателям.

Нам потребуются следующие зависимости (сам FreeMarker и инструмент для генерации документов). Если ваше приложение не использует spring boot, зависимость, конечно, будет другая (находится по первой ссылке в гугле).

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.4</version> </dependency>

Начнем с шаблона:

<#ftl output_format="plainText"> <#import "report_var.ftl" as var> <#assign delimiter = ";"> <#---------------------------------------------------------------------------> <#compress> <@writeHeaders var.headers/> <@writeColums columns=csvRecords/> </#compress> <#---------------------------------------------------------------------------> <#macro writeHeaders headers> <#list headers as header>${header} <#if header_has_next> ${delimiter} </#if></#list> </#macro> <#---------------------------------------------------------------------------> <#macro writeColums columns> <#list columns as periodicity> ${periodicity.deviceId} ${delimiter} ${periodicity.date} ${delimiter} ${periodicity.lastTime} ${delimiter} ${periodicity.lastIndicator} ${delimiter} ${periodicity.average} </#list> </#macro>

Как разделитель (#assign delimiter) используем “;” но его можно задавать вне шаблона как и остальные данные, например csvRecords. Заголовки для первой строки лежат в другом шаблоне, чтобы использовать их при генерации таблицы в других документах, шаблон выглядит так:

<#ftl output_format="plainText"> <#assign headers = ["Идентификатор устройства", "Дата" , "Последнее время события", "Последнее показание" , "Среднее время между показаниями"]>

В блоке <#compress > вызываются макросы writeHeaders, writeColumns для записи заголовка и всех пришедших данных соответственно. FreeMarker дает много возможностей для работы с коллекциями, это хорошо описано в его официальной документации.

Для того, чтобы заполнить шаблон данными, собираем коллекцию из пар “имя в шаблоне” - “значение”. В данном примере происходит это так:

Map<String, Object> data = new HashMap<>(); var records = getRecords(12); data.put("csvRecords", records);

Теперь дело за малым, создаем файл и заполняем необходимыми данными. При этом, не забываем указывать кодировку (!!!), иначе никакой кириллицы мы не увидим.

File file = new File(reportName); file.deleteOnExit(); try (Writer writer = new FileWriter(file, StandardCharsets.UTF_8){ Template template = freeMarkerConfigurer.getConfiguration().getTemplate("csv.ftl"); template.process(data, writer); } catch (IOException | TemplateException e) { log.warn(e.getMessage()); }

На выходе получаем файл формата .csv, который можем открыть в табличном редакторе. Кажется, это было достаточно просто.

С pdf-файлами дела сложнее. Мной был выбран такой путь: сначала генерируем html, затем из него pdf (можно было попробовать воспользоваться xml, но как-нибудь в другой раз). Теперь к FreeMarker добавляется библиотека xhtmlrenderer.

Шаблон для такого отчета выглядит уже как html-документ. Как оказалось, для использования в шаблоне нужно закрывать все теги, например:

<#import "report_var.ftl" as var> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"></meta> <style> body { font-family: 'Roboto', sans-serif; } table, th, td { border: 1px solid black; } td { white-space: nowrap; } .columnId { white-space: normal; } </style> <title>Отчет по счетчику ${meterId}</title> </head> <body> <#if isExist!false> <h2>Отчет по счетчику ${meterId} <#if dateFrom??> за период ${dateFrom} - ${dateTo}</#if></h2> <table> <tr> <#list var.headers as header> <th> ${header} </th> </#list> </tr> <#list records as periodicity> <tr> <td class="columnId">${periodicity.deviceId}</td> <td>${periodicity.date}</td> <td>${periodicity.lastTime}</td> <td>${periodicity.lastIndicator}</td> <td>${periodicity.average}</td> </tr> </#list> </table> <#else> <h2>Не найдена информация по счетчику</h2> </#if> </body> </html>

Но логика заполнения данными все та же, складываем в коллекцию все необходимое:

data.put("isExist", true); data.put("meterId", meterId); data.put("records", records);

Для записи в файл можно генерировать как html-файл, так и строку (в данном примере строка, но разницы никакой), отличие лишь в том, какой *Writer вы будете использовать.

StringWriter stringWriter = new StringWriter(); Template template = freeMarkerConfigurer.getConfiguration().getTemplate("pdf.ftl"); template.process(data, stringWriter); String html = stringWriter.toString(); OutputStream os = new FileOutputStream(file); fileRenderer.htmlToPDFRender(html, os);

Запись в файл выглядит достаточно просто, но тут уже стоит обратить внимание на используемый шрифт. Выбрать нужно тот, который поддерживает используемый алфавит (указываем его и тут, и в шаблоне).

try (os) { final ITextRenderer iTextRenderer = new ITextRenderer(); ITextFontResolver fontResolver = iTextRenderer.getFontResolver(); final ClassPathResource regular = new ClassPathResource("templates/Roboto-Regular.ttf"); fontResolver.addFont(regular.getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); iTextRenderer.setDocumentFromString(html); iTextRenderer.layout(); iTextRenderer.createPDF(os, true); } catch (DocumentException | IOException e) { log.warn(e.getMessage()); }

На выходе снова получаем файл.

Казалось бы, можно остановиться. Но как же картинки? Их же можно вставить через шаблон. Вставлять будем как base64. Добавляем в шаблон эту строку, не забывая закрыть тег 🙂

<img class="img" src="data:image/png;base64, ${imagePath}" alt="smile"></img>

Загружаем картинку из ресурсов (или любого другого места), преобразуем и вставляем в ту же коллекцию с данными для шаблона.

ClassPathResource image = new ClassPathResource("templates/smile.png"); byte[] fileContent = image.getInputStream().readAllBytes(); String encodedString = Base64.getEncoder().encodeToString(fileContent); data.put("imagePath", encodedString);

Следующие действия такие же как и раньше. Но на выходе уже документ с душой.

:)
:)

Интерес повел меня еще дальше, и я нашла, что картинки тоже можно генерировать, с помощью той же библиотеки. Для примера создадим печать, у нас же отчет как-никак.

Шаблон формата xhtml, но с html тоже будет работать (все так же не забываем про шрифты и кодировку).

<#ftl output_format="XHTML"> <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"></meta> <style> body { font-family: 'Roboto', sans-serif; font-size: 100px; } .wrapper { height: 800px; width: 800px; border-radius: 300px; border: 50px double green; margin: 20px auto; } .text { text-align: center; color: green; margin-top: 40%; font-size: 100%; } </style> <title></title> </head> <body> <div class="wrapper"> <div class="text">Одобрено</div> </div> </body> </html>

Работаем с шаблоном так же как и выше

File image = new File("image.png"); image.deleteOnExit(); File file = new File("image.html"); file.deleteOnExit(); try (Writer writer = new FileWriter(file, StandardCharsets.UTF_8)) { Template template = freeMarkerConfigurer.getConfiguration().getTemplate("image.ftl"); template.process(null, writer); OutputStream os = new FileOutputStream(image); fileRenderer.htmlToPNGRender(file, os); } catch (IOException | TemplateException e) { log.warn(e.getMessage()); }

А вот генерация картинки выглядит уже вот так. Здесь так же обращаю внимание на шрифт! Также можно задать размер изображения.

try (os) { int width = 1024; int height = 1024; Java2DRenderer renderer = new Java2DRenderer(html, width, height); final ClassPathResource regular = new ClassPathResource("templates/Roboto-Regular.ttf"); AWTFontResolver awtFontResolver = new AWTFontResolver(); awtFontResolver.setFontMapping("roboto", Font.createFont(TRUETYPE_FONT, regular.getInputStream())); renderer.getSharedContext().setFontResolver(awtFontResolver); BufferedImage image = renderer.getImage(); FSImageWriter writer = new FSImageWriter(); writer.write(image, os); } catch (IOException | FontFormatException e) { log.warn(e.getMessage()); }

Полученное изображение так же преобразуем в base64 и вставляем или используем с другими целями. У меня получилось так:

Генерируем отчеты с помощью FreeMarker

Какие итоги могу подвести:

  • работать с шаблонами мне показалось очень удобным, при должной фантазии можно создавать достаточно сложные шаблоны для разных целей
  • доступные выходные форматы дают много возможностей для творчества, можно преобразовать практически во все что угодно, при правильном подходе.
  • использовать их в проекте, если отчеты небольшие и простые, вполне себе разумно и не сложно.
  • нужно быть внимательным с кодировкой, шрифтами и путям к файлам 🙁 Я не уделила этому внимания первоначально и потратила достаточно времени на то, чтобы разобраться в чем же дело.
  • самое интересное в самом простом.

Каждый проект уникален, и для каждого решения есть свой подходящий инструмент. Здесь FreeMarker оправдал наш выбор полностью.

Работали с FreeMarker? Как вам?

Если вы узнали что-то новое и вам было полезно, будем рады положительное оценке под записью 🙂

5
Начать дискуссию