В одном из своих проектов мне понадобилось автоматизированно формировать документы договоров для клиентов. Договор представляет собой юридический документ длиной около 10 страниц и является шаблоном: в нужных местах подставляются данные конкретного клиента.
Задача
Первичные требования были такими:
В сложном стилизованном документе doc или docx вывести нужную информацию в помеченных местах.
В дальнейшем они были уточнены и расширены:
- В сложном стилизованном документе docx вывести данные в помеченных местах.
- Разметка вывода данных должна быть похожа на скриптлеты:
${}, <%%>, <%=%>
. - Данными для вывода могут быть объект. Нужна возможность обращения к полям.
- Для вывода использовать один из скриптовых языков: Groovy, JavaScript.
- Нужно иметь возможность выводить списки объектов в таблицы, в каждой ячейке отображая поля.
Имеющиеся решения
Оказалось, что имеющиеся в области продукты (я говорю про платформу Java), не решают поставленную задачу. Ниже краткий обзор продуктов:
Jasper reports
В качестве шаблона использует файл xml-разметки *.jrxml. Файл разметки документа + данные (как из БД, так и Map параметров) отдаются процессору, который формирует любой из следующих форматов: PDF, XML, HTML, CSV, XLS, RTF, TXT.
Не устроило:
- Это не WYSIWYG, даже при наличии iReport — визуального средства формирования jrxml-файлов.
- Надо хорошо изучить JasperReports API, чтобы создать и стилизовать сложный шаблон.
- Не выводит в нужном формате. Можно и PDF, но хотелось бы иметь возможность потом поредактировать выходной документ.
Docx4java
Инструмент манипулирования составными частями docx-, pptx-, xlsx- документов с помощью Java-API.
Не устроило:
- В документации к Docx4Java нет совпадающего с моими требованиями кейса. Есть краткое упоминание функции XMLUtils.unmarshallFromTemplate, который делает простейшие подстановки.
- Обработка повторов вывода реализована через XML-исходники с использование XPath, ссылка.
Apache POI
Инструмент манипулирования составными частями doc-, ppt-, xls- документов с помощью Java-API. Изначально создан для получения данных из документов этих форматов.
Не устроило:
- Нет решешения поставленной задачи.
Решение задачи
Это было интересно :)
1. Контент документа хранится в виде xml, сжатом в zip-архиве. Распаковка-запаковка трудна тем, что традиционный зиппер JDK 6 не поддерживает явное указание кодировки (видимо, имен файлов). Получался битый docx при архивировании. Пришлось при запаковке контента использовать Groovy-класс-обертку AntBuilder, имеющий соответствующий параметр.
2. Любой введенный в MS Word текст может быть разбит программой на кусочки и помещен в разные группы xml-тэгов.Таким образом, мне пришлось вначале решить задачу очистки шаблона от сгенерированных прокладок xml. Для этой задачи я использовал regex-выражения, мне они показались быстрее SAX-парсера (хотя производительность не мерил).
3. Я решил использовать Groovy как скриптовый язык из-за простоты, Java-природы и входящего в ядро процессора шаблонов. С ним также возникли интересные трудности. Оказалось, что даже в небольшом документе на 10 листов, можно легко нарваться на ограничение по длине строки между двумя скриптлетами. Пришлось заменять весь текст, идущий между скриптлетами на UUID-строки, прогонять процессор шаблонов Groovy, и только на выходе заменять UUID-строки на исходные кусочки XML.
Преодолев эти трудности, я опробовал проект в реальной жизни. Получилось хорошо!
Я создал англоязычный сайт проекта и опубликовал его.
Адрес проекта: snowindy.github.com/scriptlet4docx/
Пример использования API
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("name", "John");
params.put("sirname", "Smith");
DocxTemplater docxTemplater = new DocxTemplater(new File("path_to_docx_template/template.docx"));
docxTemplater.process(new File("path_to_result_docx/result.docx"), params);
На этом можно закончить, адресуя вас на сайт проекта…
Но для улучшения понимания, переведу наиболее интересную секцию “Scriptlet types explanation”.
Типы скриптлетов, подробнее
Disclaimer: При работе с темплейтами, процессор шаблонов Groovy переводит все скриптлеты в Groovy-код, а текст между скриптлетами выводит таким образом: out.print('template_text')
${ data }
Эквивалент выводу data в out: out.print(data)
<%= data %>
Эквивалент выводу data в out: out.print(data)
<% any_code %>
Выполнение кода внутри скриптлета, ничего не выводит. Может быть использован для условного вывода:
<% if (cond) { %>
Этот текст будет вставлен, если "cond == true"
<% } else { %>
Этот текст будет вставлен, если "cond != true"
<% } %>
$[ @listVar.field ]
Это самый интересный вид скриптлетов! Он используется для вывода списков объектов в таблицу внутри docx-документа. Должен использоваться внутри ячейки таблицы.
К примеру, у нас есть список объектов Person. Объект имеет два поля: 'name' и 'address'. Мы хотим вывести список в таблице из двух колонок.
- Создание параметра 'personList' в карте входных значений, ссылается на список объектов.
- Создание таблицы с двумя колоноками и одной строкой в docx-документе.
-
$[@person.name]
надо ввести в первую ячейку;$[@person.address]
— во вторую. - Все готово, список personList будет распечатан в таблице.
Живой пример шаблона
Лежит здесь: ссылка.
Развитие проекта
Если я действительно изобрел новый подход к процессингу docx-шаблонов, хотелось бы популяризовать его.
Проекту есть куда стремиться:
- Полноценное кеширование,
- Поддержка скриптлетов в списках
- Внедрение Streaming API
Буду рад советам в распространении проекта в англоязычной аудитории!
Автор: Snowindy