Практически каждый разработчик, создающий информационные системы, сталкивается с необходимостью формирования различных отчетов и печатных форм. Это характерно и для большинства приложений разработанных на нашей платформе. Например, в системе, над которой я работаю в настоящее время, их 264. Для того чтобы не писать каждый раз логику формирования отчетов с нуля, мы разработали специальную библиотеку (под катом будет объяснено, почему нам не подошли существующие). Называется она YARG — Yet Another Report Generator.
YARG позволяет:
- Генерировать отчет в формате шаблона или конвертировать результат в PDF;
- Создавать шаблоны отчетов в привычных и распространенных форматах: DOC, ODT, XLS, DOCX,XLSX, HTML;
- Создавать сложные XLS и XLSX шаблоны: с вложенными областями данных, графиками, формулами и т.д.;
- Использовать в отчетах изображения и HTML-разметку;
- Хранить структуру отчетов в формате XML;
- Запускать standalone приложение для генерации отчетов, что делает возможным использование библиотеки вне Java-экосистемы (например для генерации отчетов в PHP);
- Интегрироваться с IoC-фреймворками (Spring, Guice).
Эта библиотека используется в платформе CUBA в качестве основы для движка отчетов. Мы развиваем ее с 2010 года, но совсем недавно решили сделать ее открытой, и выложили ее код на GitHub с лицензией Apache 2.0.
Данная статья призвана привлечь к ней внимание сообщества.
В основе библиотеки лежит простая идея разделения выборки данных и отображения данных в готовый отчет (data layer & presentation layer). Выборка данных описывается разнообразными скриптами, а отображение данных настраивается прямо в документах-шаблонах. При этом, для того, чтобы создать шаблон не требуются специальные средства, достаточно иметь под рукой Open Office или Microsoft Office.
Отчет состоит из так называемых полос. Полоса одновременно является и набором данных и областью в шаблоне, куда эти данные отображаются (связывает data layer и presentation layer).
Рассмотрим для начала пример в стиле Hello World.
Очень простой пример
Представим, что у нас есть фирма и нам нужно вывести список всех сотрудников фирмы, с указанием должности сотрудника.
Мы создаем полосу отчета c именем Staff, в которой указываем, что данные загружаются SQL запросом:
select name, surname, position from staff
Java Code
ReportBuilder reportBuilder = new ReportBuilder();
ReportTemplateBuilder reportTemplateBuilder = new ReportTemplateBuilder()
.documentPath("/home/haulmont/templates/staff.xls")
.documentName("staff.xls")
.outputType(ReportOutputType.xls)
.readFileFromPath();
reportBuilder.template(reportTemplateBuilder.build());
BandBuilder bandBuilder = new BandBuilder();
ReportBand staff= bandBuilder.name("Staff")
.query("Staff", "select name, surname, position from staff", "sql")
.build();
reportBuilder.band(staff);
Report report = reportBuilder.build();
Reporting reporting = new Reporting();
reporting.setFormatterFactory(new DefaultFormatterFactory());
reporting.setLoaderFactory(
new DefaultLoaderFactory().setSqlDataLoader(new SqlDataLoader(datasource)));
ReportOutputDocument reportOutputDocument = reporting.runReport(
new RunParams(report), new FileOutputStream("/home/haulmont/reports/staff.xls"));
Далее мы создаем xls-шаблон, в котором отмечаем именованный регион Staff и расставляем алиасы в ячейках.
Примеры посложнее рассматриваются ниже.
Немного истории
Несколько лет назад у нас возникла потребность в массовом создании отчетов в одном из наших проектов. Нужно было создавать отчеты в формате XLS и DOC, а также конвертировать результат из DOC и XLS в PDF. Нам требовалось, чтобы библиотека:
- позволяла создавать отчеты (по крайней мере, шаблоны отчетов) обычным пользователям;
- поддерживала загрузку данных из различных источников;
- поддерживала различные форматы шаблонов (XLS, DOC, HTML);
- поддерживала конвертацию отчетов в PDF;
- была расширяемой (позволяла быстрое добавление новых способов загрузки данных и новых форматов шаблонов);
- легко встраивалась в различные IoC контейнеры.
Сначала мы пытались использовать JasperReports, но он, во-первых, не умеет создавать DOC отчеты (есть платная библиотека для этого), во-вторых, его возможности по генерации XLS отчетов сильно ограничены (не получится использовать графики, формулы, форматы ячеек), и, в-третьих, создание шаблонов требует определенного навыка и специальных инструментов, а для описания загрузки данных нужно писать Java-код. Существовало также много библиотек, концентрирующихся на каком-то конкретном формате, но единой библиотеки мы не нашли.
Поэтому мы решили создать механизм, позволяющий единообразно описывать отчеты, независимо от типа шаблона и способа загрузки данных.
Первые шаги
Для работы с XLS уже тогда существовало много разных библиотек (POI-HSSF, JXLS и т.д.) и было решено использовать Apache POI, как самую на тот момент популярную. А вот для работы с DOC файлами такого разнообразия не наблюдалось. Вариантов было совсем немного: использовать UNO Runtime — API для интеграции с сервером Open Office или работать с DOC файлами через COM- объекты. Проект POI-HWPF тогда находился в зачаточном состоянии (недалеко ушел он и сейчас). Мы решили использовать интеграцию с Open Office, потому что увидели много положительных отзывов от людей, которые успешно интегрировались с Open Office на совершенно разных языках (Python, Ruby, C#).
Если с POI-HSSF все было более или менее просто (за исключением полного отсутствия возможности работы с графиками), то с UNO Runtime нам пришлось испытать множество проблем.
- Отсутствует внятный API для работы с таблицами. Например, чтобы скопировать строку таблицы, нужно использовать системный буфер обмена (выделяя строку, копируя и вставляя ее в нужное место).
- Для каждой генерации отчета порождается процесс Open Office (и уничтожается после печати). Изначально мы использовали библиотеку bootstrapconnector для порождения процессов, но вскоре убедились, что во многих случаях она оставляет процесс в живых (в зависшем состоянии) или вообще не пытается процесс завершить, что приводило к коллапсу системы через какое-то время. Нам пришлось переписать логику запуска и уничтожения процессов Open Office, воспользовавшись наработками ребят, написавших jodconverter.
- UNO Runtime (и сам Open Office сервер) имеет проблемы с потокобезопасностью, из-за чего под нагрузкой сервер может зависнуть или внезапно остановиться из-за внутренней ошибки. Это привело к тому, что пришлось делать механизм повторного запуска отчетов (если отчет не напечатался — попробовать напечатать его еще раз). Это, естественно, сказывается на скорости работы данного вида отчетов.
Docx4j
Долгое время мы использовали только XLS и DOC шаблоны, но затем было решено поддержать также XLSX и DOCX. Выбор пал на библиотеку DOCX4J, которая к тому времени набрала популярность.
Важным достоинством этой библиотеки для нас стало то, что она предоставляет низкоуровневый доступ к структуре документа (фактически оперируя с XML). С одной стороны это несколько усложнило код и логику, а с другой открыло почти безграничные возможности по управлению документом, так как любые операции над ним были теперь возможны.
Еще более серьезным достоинством стала возможность отказаться от запуска Open Office для генерации DOCX-отчетов.
Пример посложнее
Представим, что у нас есть книжный магазин. Давайте попробуем с помощью нашей библиотеки сделать отчет, выводящий в XLS список магазинов и список книг проданных в каждом из магазинов.
Представим также, что мы (владельцы магазина) совсем не знаем язык программирования Java, но на наше счастье наш системный администратор знаком с SQL, и у нас даже есть база данных, содержащая информацию обо всех продажах.
В первую очередь давайте создадим шаблон отчета в формате xls. Сразу отметим полосы отчета с помощью именованных регионов.
Затем опишем загрузку данных с помощью SQL.
select shop.id as "id", shop.name as "name", shop.address as "address"
from store shop
select book.author as "author", book.name as "name", book.price as "price", count(*) as "count"
from book book where book.store_id = ${Shop.id}
group by book.author, book.name, book.price
Теперь мы должны описать отчет с помощью XML.
<?xml version="1.0" encoding="UTF-8"?>
<report name="report">
<templates>
<template code="DEFAULT"
documentName="bookstore.xls"
documentPath="./test/sample/bookstore/bookstore.xls"
outputType="xls"
outputNamePattern="bookstore.xls"/>
</templates>
<rootBand name="Root" orientation="H">
<bands>
<band name="Header" orientation="H"/>
<band name="Shop" orientation="H">
<bands>
<band name="Book" orientation="H">
<queries>
<query name="Book" type="sql">
<script>
select
book.author as "author",
book.name as "name",
book.price as "price",
count(*) as "count"
from book
where book.store_id = ${Shop.id}
group by book.author, book.name, book.price
</script>
</query>
</queries>
</band>
</bands>
<queries>
<query name="Shop" type="sql">
<script>
select
shop.id as "id",
shop.name as "name",
shop.address as "address"
from store shop
</script>
</query>
</queries>
</band>
</bands>
<queries/>
</rootBand>
</report>
Запустив отчет из командной строки, мы получим следующий документ
В данном отчете мы видим, что одна полоса может ссылаться на другую. Полоса Book ссылается на полосу Shop, таким образом для каждого магазина мы выбираем список проданных в нем книг. Полоса Book является вложенной в Shop.
Еще пример
Теперь представим, что наш магазин получил крупный заказ и нам нужно выставить счет заказчику. Попробуем создать отчет, в котором в качестве шаблона используется документ DOCX, а результат конвертируется в PDF. Загрузку данных для разнообразия опишем Groovy-скриптом.
<?xml version="1.0" encoding="UTF-8"?>
<report name="report">
<templates>
<template code="DEFAULT"
documentName="invoice.docx"
documentPath="./test/sample/invoice/invoice.docx"
outputType="pdf"
outputNamePattern="invoice.pdf"/>
</templates>
<formats>
<format name="Main.date" format="dd/MM/yyyy"/>
<format name="Main.signature" format="${html}"/>
</formats>
<rootBand name="Root" orientation="H">
<bands>
<band name="Main" orientation="H">
<queries>
<query name="Main" type="groovy">
<script>
return [
[
'invoiceNumber':99987,
'client' : 'Google Inc.',
'date' : new Date(),
'addLine1': '1600 Amphitheatre Pkwy',
'addLine2': 'Mountain View, USA',
'addLine3':'CA 94043',
'signature':<![CDATA['<html><body><b>Mr. Yarg</b></body></html>']]>
]]
</script>
</query>
</queries>
</band>
<band name="Items" orientation="H">
<queries>
<query name="Main" type="groovy">
<script>
return [
['name':'Java Concurrency in practice', 'price' : 15000],
['name':'Clear code', 'price' : 13000],
['name':'Scala in action', 'price' : 12000]
]
</script>
</query>
</queries>
</band>
</bands>
<queries/>
</rootBand>
</report>
Можно заметить, что Groovy-скрипт возвращает список ассоциативных массивов в качестве результата (если точнее — List<Map<String, Object>). Таким образом, каждый элемент списка представляет собой строку с именованными данными (ключ — имя параметра, значение — параметр).
Теперь создадим шаблон счета. В таблицу сверху поместим имя и адрес клиента, а также дату выставления счета.
Далее создадим таблицу со списком товаров, за которые выставляется счет. Для того чтобы таблица 2 была привязана к списку товаров, вставим в первую ячейку специальный маркер (##band=Items).
Запустив отчет, мы увидим следующее.
Интеграция и расширение функциональности
Библиотека изначально проектировалась для расширения и интеграции в различные приложения. Примером такой интеграции может служить использование YARG в платформе CUBA. В качестве IoC-фреймворка мы используем Spring. Давайте посмотрим как YARG может встраиваться в Spring.
<bean id="reporting_lib_Scripting"
class="com.haulmont.reports.libintegration.ReportingScriptingImpl"/>
<bean id="reporting_lib_GroovyDataLoader"
class="com.haulmont.yarg.loaders.impl.GroovyDataLoader">
<constructor-arg ref="reporting_lib_Scripting"/>
</bean>
<bean id="reporting_lib_SqlDataLoader"
class="com.haulmont.yarg.loaders.impl.SqlDataLoader">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="reporting_lib_JpqlDataLoader"
class="com.haulmont.reports.libintegration.JpqlDataDataLoader"/>
<bean id="reporting_lib_OfficeIntegration"
class="com.haulmont.reports.libintegration.CubaOfficeIntegration">
<constructor-arg value="${cuba.reporting.openoffice.path?:/}"/>
<constructor-arg>
<list>
<value>8100</value>
<value>8101</value>
<value>8102</value>
<value>8103</value>
</list>
</constructor-arg>
<property name="displayDeviceAvailable">
<value>${cuba.reporting.displayDeviceAvailable?:false}</value>
</property>
<property name="timeoutInSeconds">
<value>${cuba.reporting.openoffice.docFormatterTimeout?:20}</value>
</property>
</bean>
<bean id="reporting_lib_FormatterFactory"
class="com.haulmont.yarg.formatters.factory.DefaultFormatterFactory">
<property name="officeIntegration" ref="reporting_lib_OfficeIntegration"/>
</bean>
<bean id="reporting_lib_LoaderFactory" class="com.haulmont.yarg.loaders.factory.DefaultLoaderFactory">
<property name="dataLoaders">
<map>
<entry key="sql" value-ref="reporting_lib_SqlDataLoader"/>
<entry key="groovy" value-ref="reporting_lib_GroovyDataLoader"/>
<entry key="jpql" value-ref="reporting_lib_JpqlDataLoader"/>
</map>
</property>
</bean>
<bean id="reporting_lib_Reporting" class="com.haulmont.yarg.reporting.Reporting">
<property name="formatterFactory" ref="reporting_lib_FormatterFactory"/>
<property name="loaderFactory" ref="reporting_lib_LoaderFactory"/>
</bean>
Основной bean в данном описании — reporting_lib_Reporting. Он предоставляет доступ к основной функциональности библиотеки — созданию отчетов. Для нормального функционирования необходимо определить фабрику форматтеров (работающих с различными типами документов — DOCX, XLSX, DOC и т.д.) и фабрику загрузчиков (загружающих данные). Также, если вы собираетесь использовать DOC отчеты, необходимо задать бин reporting_lib_OfficeIntegration, который отвечает за интеграцию с Open Office (с помощью которого обрабатываются DOC и ODT отчеты).
Следует заметить, что для добавления, например, нового загрузчика не нужно переопределять никаких классов библиотеки, достаточно добавить его в описание свойства dataLoaders в бине reporting_lib_LoaderFactory. Что мы в принципе и сделали, добавив jpql загрузчик данных (java<entry key=«jpql» value-ref=«reporting_lib_JpqlDataLoader»/>
).
Для более серьезных изменений можно наследовать библиотечные классы или создавать свои с нуля, реализуя предоставленные интерфейсы. Практически вся функциональность библиотеки связана через интерфейсы и легко расширяется.
Standalone режим
Еще одной особенностью библиотеки YARG является то, что ее можно использовать как standalone приложение для генерации отчетов. Таким образом, имея на компьютере установленную JRE, вы можете генерировать отчеты из командной строки. Например, у вас есть серверное приложение на PHP и вы хотите генерировать XLS-отчеты. Вам достаточно создать XLS-шаблон, XML-описание отчета и после этого вы с помощью простой консольной команды сможете генерировать отчет.
Пример команды:
yarg -rp ~/report.xml -op ~/result.xls “-Pparam1=20/04/2014”
Заключение
В качестве заключения приведу несколько скриншотов UI, который предоставляет платформа CUBA для создания отчетов на движке YARG:
Фрагменты редактора отчета
Мастер создания отчетов
И пример отчета с графиками:
Шаблон отчета с графиком и диаграммой
Готовый отчет с графиком и диаграммой