Главная / Статьи / Конкурс 2007 / Конкурсные работы (53) / Java + OpenOffice.org = генератор отчетов

Java + OpenOffice.org = генератор отчетов

Многие разработчики бизнес-приложений часто сталкиваются с необходимостью формировать в своей программе документы и печатать их. Эти документы условно можно разделить на две категории: «договора» и «отчеты». Обычно они создаются на основе некоторого шаблона.

Для «договоров» этот шаблон представляет собой текст, общий для всех видов таких «договоров» (обычно его называют «рыба») с пустыми местами для данных, уникальных для каждого экземпляра «договора». Для «отчетов» шаблон обычно имеет табличный вид и в строках таблицы находятся однородные данные. Для формирования таких шаблонов и документов на основе этих шаблонов существует достаточное количество специальных программ, так называемых «генераторов отчетов». Они предоставляют удобный интерфейс для создания шаблонов и API для формирования и печати готовых документов на основе шаблонов.

В одном из проектов, над которым я работаю, возникла необходимость печатать как «отчеты», так и «договора». Сам проект написан на Java, поэтому и инструмент, с помощью которого можно было бы печатать документы должен как минимум иметь API для Java. Второе требование было, что инструмент должен быть как можно более гибким и открытым и желательно бесплатным. Мне пришлось столкнуться с работой с JasperReports 1.2.7 и в нем есть два огромных недостатка: если уже есть «рыба» документа в каком-то текстовом формате, то его нельзя непосредственно использовать для формирования шаблона и то, что формирование шаблонов «договоров» в нем сильно затруднено. Здесь у меня возникла идея: а почему бы не использовать OpenOffice.org в качестве такого инструмента? Он обладает достаточной гибкостью, открытой архитектурой, очень развитым SDK для Java, может работать с широким спектром текстовых документов и он бесплатен! Можно написать небольшой код, который бы использовал существующий документ в качестве шаблона, вставлял туда нужные данные и печатал или сохранял его.

Для реализации этой идеи существует как минимум два подхода. В обоих подходах нужно сначала сформировать шаблон из существующей «рыбы» или создать с нуля. В этом шаблоне нужно неким образом пометить специальные места, куда наша программа будет вставлять данные. Второй этап – формирование конечного документа на основе шаблона и информации, специфичной именно для данного экземпляра документа. Так как формат документа Open Document открыт, хорошо документирован и основан на XML – можно написать свой обработчик данного формата, который бы в шаблон вставлял нужную информацию и формировал результирующий документ.

Я подробнее расскажу о втором подходе – загрузке шаблона и его обработке с помощью API OpenOffice.org.

Для этого понадобится:

По большому счету для рассматриваемой задачи можно и не использовать OpenOffice.org SDK, если в вашей сборке OpenOffice.org имеются нужные java-классы. Их можно найти в каталоге $OOO_HOME/program/classes, где $OOO_HOME – путь к установке OpenOffice.org. Если объем SDK для вас велик, а нужных файлов в сборе нет, можно скачать на порядок меньший OpenOffice.org URE по адресу http://download.openoffice.org/2.2.0/ure.html, но в нем отсутствует документация разработчика. Нам понадобятся такие файлы: unoil.jar и juh.jar. Их нужно будет подключить к проекту в той среде, которую вы будете использовать для запуска этого примера. Или указать в classpath, если будете запускать из командной строки. Однако, для лучшего понимания приведенного кода советую как минимум прочитать введение в UNO в документации разработчика к OpenOffice.org SDK.

Возьмем какой-нибудь простую «рыбу» документа для примера. Теперь создадим простой, но наглядный шаблон с несколькими переменными. На рис.1 приведен исходный файл, на основе которого будем создавать шаблон.

Рисунок 1: Исходная "рыба" документа


Итак, первым этапом нужно в нашем шаблоне отметить места, в которые потом наша программа будет вставлять нужную информацию. Сделать это можно несколькими способами, я остановлюсь на наиболее простом. Будем использовать так называемые «поля», в частности одну из их разновидностей – переменные. Идея заключается в том, чтобы вставлять в нужные места шаблона переменные с разными именами, а наша программа потом будет в документе заменять значения этих переменных на нужные.

Для того, чтобы вставить переменную, установите курсор на нужное место в тексте, выберите в меню Вставка->Поля->Дополнительно или нажмите Ctrl+F2. В появившемся диалоге выберите вкладку «Переменные», в списке «Тип поля» выберите «Установить переменную», а в списке «Формат» выберите «Текст».
Далее осталось самое главное – в поле ввода «Имя» ввести имя переменной, а в поле «Значение» - ее первоначальное значение.

Рисунок 2: Диалог ввода переменных


В нашей задаче значение может быть любым и для наглядности условимся, что значение у всех переменных будет «НЕ ЗАДАНО». Имя переменной должно быть уникальным в рамках данного шаблона, но если нужно одну и ту же переменную использовать несколько раз, то можно сначала установить значение переменной как описано выше, а потом использовать поле другого типа: «Показать переменную» и указать только имя переменной. На рис. 2 приведен диалог ввода переменных. Имена переменных соответствуют следующим полям: CONTRACT_DATE – дата заключения договора, CONTRACT_NUMBER – номер договора, CUSTOMER – Заказчик, EXECUTOR – Исполнитель, EXECUTOR_PERSON – представитель Исполнителя. Сохраним полученный файл под именем template.odt. Вот, что у нас в результате получилось:

Файл под именем template.odt

 

Кстати, этот документ тоже можно использовать в качестве шаблона. Можете убрать текст статьи и оставить только текст договора, а можете ничего не менять.

Наконец, переходим ко второму этапу – обработке шаблона с помощью нашей программы. Текст программы приведен в Листинге 1. Я постарался прокомментировать места, которые могут показаться непонятными, а сам принцип работы вкратце следующий. Как параметр программе передается имя файла с шаблоном, а значения переменных для простоты прописаны в самом коде, хотя их, например, тоже можно загружать из какого-нибудь текстового файла. В HashMap variableMap хранятся соответствия имен переменных их значениям и в начале программы он для наглядности вручную заполняется. Далее вызывается функция connect(), которая устанавливает соединение с OpenOffice.org и устанавливает внутренние переменные, которые в дальнейшем используются в разных местах кода. Потом с помощью функции openDocument открывается документ и получается его интерфейс XtextFieldsSupplier. Он в свою очередь имеет функцию getTextFields, которая возвращает перечисление xTextFieldsEnumeration всех «полей» в документе. Теперь осталось пройтись по всем текстовым полям, отобрать из них нужные и установить их значения в соответствии variableMap.

В цикле для каждого поля проверяется, поддерживает ли он сервис com.sun.star.text.TextField.SetExpression – «установить переменную». Если это так – значит это наше специальное поле и для него запрашивается набор свойств xPropertySet. Значение свойства VariableName как раз и является именем нашей переменной, которое мы установили в шаблоне. Затем по имени переменной в variableMap ищется ее значение и устанавливается как свойство поля Content. Чтобы не было проблем с отображением значений устанавливается еще два свойства поля – IsVisible для маркировки поля как видимого и SubType для установки типа поля как строкового. Хотя выше при установке поля я и писал, что нужно устанавливать тип поля как «Текст» и не говорил, чтобы устанавливали свойство «Невидимое», но для перестраховки все-таки в коде программы явно устанавливаю эти свойства. Далее нужно обновить отображаемые значения переменных на экране, это делается с помощью функции refresh() интерфейса XRefreshable.

Все что нам осталось сделать – сохранить или распечатать полученный документ. Сохранить можно с помощью функции saveDocument или saveAsDocument. Первая сохраняет документ под текущим именем, что в нашем примере не понадобится, так как будет испорчен шаблон. Вторая сохраняет документ под новым именем – чтобы, например, передать его по электронной почте. Распечатать документ можно тоже двумя способами: вызвать функцию printDocument и это будет равносильно нажатию кнопки печати на принтере по умолчанию или вызвать функцию executeCommands. Эта функция использует специальный механизм обработчиков команд OpenOffice.org. Как параметр ей передается массив строк – имен команд, которые нужно выполнить. Имена команд являются обозначениями тех действий, которые может выполнить OpenOffice.org. Например, команда Print равносильна выбору в меню Файл->Печать, Save – Файл->Сохранить и т.д. В данном примере используется команда Print, которая выведет диалог печати и даст пользователю возможность произвести различные настройки печати.

Эта команда выполняется асинхронно, поэтому есть возможность, что документ еще не будет подготовлен для печати, а мы уже будем пытаться его закрыть – в таком случае просто ничего не распечатается. Здесь приходится применить хитрость – пытаться закрыть документ до тех пор, пока будет происходить исключение com.sun.star.util.CloseVetoException – знак того, что документ еще не подготовлен для печати.
При запуске программы не забудьте указать в качестве параметра имя файла-шаблона и указать в classpath пути к библиотекам URE. В результате запуска получаем готовый документ, приведенный на рис. 3.

Рисунок 3: Шаблон после обработки программой


В результате на напечатанном документе серого фона не будет видно, это просто специально сделано на экранной форме для маркировки мест, в которых находятся «поля».

Заключение

И пусть не ругают меня гуру Java за статические переменные, отсутствие обработки некоторых исключений, отсутствие проверки возвращаемых значений на null и т.п. Данный пример был сделан на основе небольшой части разрабатываемого мной большого проекта, и код пришлось сильно сокращать, чтобы поместить в рамки статьи. Тем более, что задача данной статьи – знакомство с возможностями API OpenOffice.org на реальном примере и указаний возможных вариантов решения тех или иных проблем, а не выдача готового «красивого» решения.

В любом случае это решение успешно работает в реальном проекте. Конечно кода в нем на порядок больше но и возможностей соответственно тоже больше.
Есть еще огромное количество возможностей, которые можно реализовать. Например, можно выгружать нужные данные в Calc, на основании которых делать различные диаграммы. Или сделать отчеты табличного вида с помощью Calc. Можно расширить возможности данного примера: сделать загрузку файлов из памяти (например, хранимых в БД в BLOB) или сделать специальный редактор шаблонов, который бы позволял удобно расставлять переменные и как-то автоматически связывать их с данными из вашей программы. Специалисты скажут: но ведь есть большое количество различных способов передать в документы данные из внешних источников, например, с помощью «источников данных». Но каждый способ оптимален для каждого конкретного случая, и в моем случае описанный в данной статье вариант был единственно возможным. Может для кого-то он тоже окажется полезным.

package org;
import java.util.HashMap;
import javax.swing.*;
import java.io.*;
import com.sun.star.beans.*;
import com.sun.star.util.*;
import com.sun.star.io.*;
import com.sun.star.connection.*;
import com.sun.star.lang.*;
import com.sun.star.text.*;
import com.sun.star.frame.*;
import com.sun.star.container.*;
import com.sun.star.view.XPrintable;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XInterface;
import com.sun.star.uno.XNamingService;
import com.sun.star.uno.XComponentContext;
public class OOOReportGenerator {

  private static XComponentContext xRemoteContext = null;
  private static XMultiComponentFactory xRemoteServiceManager = null;
  private static XURLTransformer xTransformer = null;
  private static XComponentLoader xComponentLoader = null;
  private static XDesktop xDesktop = null;

  public OOOReportGenerator() {
  }

  /* соедиенение с экземпляром OpenOffice.org
   * и инициализация нужных переменных
   */
  public static void connect() throws Exception {
    // получим контекст удаленного компонента офиса
    xRemoteContext = com.sun.star.comp.helper.Bootstrap.bootstrap();
    xRemoteServiceManager = xRemoteContext.getServiceManager();

    // создадим сервис, который понадобится при печати
    Object transformer = xRemoteServiceManager.createInstanceWithContext(
        "com.sun.star.util.URLTransformer", xRemoteContext );
    xTransformer = (XURLTransformer)UnoRuntime.queryInterface(
        XURLTransformer.class, transformer);

    // получим сервис Desktop
    Object desktop = (XInterface) xRemoteServiceManager.createInstanceWithContext(
        "com.sun.star.frame.Desktop", xRemoteContext);
    xDesktop = (XDesktop)UnoRuntime.queryInterface(
        XDesktop.class, desktop);

    // данный интерфейс позволяет загружать и сохранять документы
    xComponentLoader = (XComponentLoader)UnoRuntime.queryInterface(
        XComponentLoader.class, desktop);
  }

  /* Открыть документ, на который указывает sURL
   * для простоты предполагаем, что sURL - путь к файлу
   */
  public static XComponent openDocument(String sURL) throws Exception {
    // преобразуем путь к файлу в URL
    java.io.File sourceFile = new java.io.File(sURL);
    StringBuffer sTmp = new StringBuffer("file:///");
    sTmp.append(sourceFile.getCanonicalPath().replace('\\', '/'));
    sURL = sTmp.toString();

    PropertyValue[] loadProps = new PropertyValue[0];
    return xComponentLoader.loadComponentFromURL(sURL, "_blank", 0, loadProps);
  }

  /* Закрыть документ, если askIfVetoed=true, то в случае попытки закрытия
   * документа с несохраненными изменениями будет выдан соответствующий запрос
   */
  public static boolean closeDocument(XComponent comp, boolean askIfVetoed) {
    XCloseable c = (XCloseable)UnoRuntime.queryInterface(
        XCloseable.class, comp);
    boolean dispose = true;
    try {
      c.close(false);
    } catch (com.sun.star.util.CloseVetoException e) {
      if ( askIfVetoed ) {
        int action = JOptionPane.showConfirmDialog(null, "Есть несохраненные изменения!" +
            "Действительно хотите закрыть?",
            "Внимание", JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_OPTION);
        if ( JOptionPane.NO_OPTION == action ) {
          dispose = false;
        }
      }
      if ( dispose ) {
        comp.dispose();
      }
    }
    return dispose;
  }

  /* Распечатать документ на принтере по умолчанию
   */
  public static void printDocument(XComponent comp)
  throws com.sun.star.lang.IllegalArgumentException {
    XPrintable xPrintable = (XPrintable)UnoRuntime.queryInterface(
        XPrintable.class, comp);
    PropertyValue[] printOpts = new PropertyValue[0];
    xPrintable.print(printOpts);
  }

  /* Сохранить документ под текущим именем
   */
  public static void saveDocument(XComponent comp, PropertyValue[] props) {
    XStorable store = (XStorable)UnoRuntime.queryInterface(
        XStorable.class, comp);
    saveAsDocument(comp, store.getLocation(), props);
  }

  /* Сохранить документ под именем, на которое указывает aURL
   */
  public static void saveAsDocument(XComponent comp, String aURL, PropertyValue[] props) {
    XStorable store = (XStorable)UnoRuntime.queryInterface(
        XStorable.class, comp);
    try {
      store.storeToURL(aURL, props);
    } catch (Exception e) {
      System.out.println( "Не могу сохранить файл!" + e );
    }
  }

  /*
   * Выполнить команды с помощью механизма обработчика
   * команд
   */
  public static void executeCommands( String[] commands )
  throws com.sun.star.uno.Exception {
    // Получаем текущий фрейм
    XFrame xFrame = xDesktop.getCurrentFrame();
    // Находим соответствующий DispatchProvider.
    XDispatchProvider xDispatchProvider = (XDispatchProvider)UnoRuntime.queryInterface(
        XDispatchProvider.class, xFrame );
    for ( int n = 0; n < commands.length; n++ ) {
      // Подготовим URL
      com.sun.star.util.URL[] aURL  = new com.sun.star.util.URL[1];
      aURL[0] = new com.sun.star.util.URL();
      com.sun.star.frame.XDispatch xDispatch = null;

      aURL[0].Complete = ".uno:" + commands[n];
      xTransformer.parseSmart( aURL, ".uno:" );

      // Попытаемся получить обработчик для полученного URL
      xDispatch = xDispatchProvider.queryDispatch( aURL[0], "", 0 );
      if ( xDispatch != null ) {
        com.sun.star.beans.PropertyValue[] lParams = new com.sun.star.beans.PropertyValue[0];
        Object obj = xRemoteServiceManager.createInstanceWithContext(
            "com.sun.star.frame.DispatchHelper", xRemoteContext );
        XDispatchHelper dh = (XDispatchHelper)UnoRuntime.queryInterface(
            XDispatchHelper.class, obj);
        dh.executeDispatch(xDispatchProvider, aURL[0].Complete, "", 0, lParams);
      } else {
        System.out.println( "Не найден обработчик для " + aURL[0].Complete );
      }
    }
  }

  public static void main(String[] args) {
    HashMap<String,String> variableMap = new HashMap<String,String>();

    variableMap.put("CONTRACT_NUMBER", "1234567");
    variableMap.put("CONTRACT_DATE", "31 декабря 2007 г.");
    variableMap.put("EXECUTOR", "ООО \"Рога и Копыта\"");
    variableMap.put("EXECUTOR_PERSON", "Потыкайло Петра Сергеевича");
    variableMap.put("CUSTOMER", "Пупкин Василий Иванович");

    try {
      connect();

      XComponent currentDocument = openDocument(args[0]);

      XTextFieldsSupplier xTextFieldsSupplier = (XTextFieldsSupplier)UnoRuntime.queryInterface(
          XTextFieldsSupplier.class, currentDocument);

      // Создадим перечисление всех полей документа
      XEnumerationAccess xEnumerationAccess = xTextFieldsSupplier.getTextFields();
      XEnumeration xTextFieldsEnumeration = xEnumerationAccess.createEnumeration();
      XRefreshable xRefreshable = (XRefreshable)UnoRuntime.queryInterface(
          XRefreshable.class, xEnumerationAccess);

      while ( xTextFieldsEnumeration.hasMoreElements() ) {
        Object service = xTextFieldsEnumeration.nextElement();
        XServiceInfo xServiceInfo = (XServiceInfo)UnoRuntime.queryInterface(
            XServiceInfo.class, service);

        if (xServiceInfo.supportsService("com.sun.star.text.TextField.SetExpression")) {
          XPropertySet xPropertySet = (XPropertySet)UnoRuntime.queryInterface(
              XPropertySet.class, service);
          String name = (String)xPropertySet.getPropertyValue("VariableName");
          Object content = variableMap.get(name);
          xPropertySet.setPropertyValue("SubType",
              new Short(com.sun.star.text.SetVariableType.STRING));
          xPropertySet.setPropertyValue("Content",
              content == null ? " " : content.toString());
          xPropertySet.setPropertyValue("IsVisible", true);
        }
      }
      xRefreshable.refresh();

      String[] cmds = {"Print"};
      executeCommands(cmds);
      /* можно еще
       * printDocument(currentDocument) - распечатать на принтере по умолчанию
       * saveDocument (currentDocument, props) - сохранить текущий документ
       * saveAsDocument (currentDocument, sURL, props) - сохранить под новым именем
       */

      closeDocument(currentDocument, false);
      XCloseable c = (XCloseable)UnoRuntime.queryInterface(
          XCloseable.class, currentDocument);
      while(true) {
        try {
          c.close(false);
          break;
        } catch (com.sun.star.util.CloseVetoException e) {
        }
        Thread.sleep(200);
      }
    } catch(Exception e) {
      System.out.println("Ошибка печати документа");
    }
  }
}

Об авторе
Подробнее: Астафьев Артем Сергеевич
Дата создания: 01.05.2007

Обсудить
на форуме: community.i-rs.ru

Rambler's Top100