четверг, 1 ноября 2012 г.

Использование Apache POI для разбора документов Word

     В данной статье описывается как скопировать часть текстового docx файла в другой docx файл, с сохранением используемых стилей. Задача кажется тривиальной, но на практике возникли некоторые трудности.
     Итак, предположим, есть большой docx файл, состоящий из логически разделенных частей, разделенных, скажем, разрывом страницы (Ctrl+Enter). Требуется создать на каждый такой кусок отдельный файл, с сохранением форматирования текста.
    Воспользуемся библиотекой Apache POI  для разбора файла. Для этого создадим проект и добавим в зависимости POI 3.8. Версия 3.7 не подходит по причине того, что в ней нет возможности перенести стили из одного документа в другой.
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>3.8</version>
        </dependency>

И собственно код для разбора и создания документов:

        XWPFDocument doc = new XWPFDocument(new FileInputStream("/home/username/test.docx"));
        List<XWPFParagraph> list = doc.getParagraphs();
        XWPFDocument tmp = new XWPFDocument();
        tmp.createStyles();
        String fileName = "test";
        boolean isNew = true;

        for (XWPFParagraph p : list) {
            XWPFStyles style = tmp.getStyles();
            if (p.getStyleID() != null && !style.styleExist(p.getStyleID())) {
                style.addStyle(doc.getStyles().getStyle(p.getStyle()));
            }

            // формируем имя файла из первых символов
            if (isNew || StringUtils.isBlank(fileName)) {
                fileName = p.getParagraphText().trim();
                if (fileName.length() > 100) {
                    fileName = StringUtils.left(fileName, 50);
                }
            }
            XWPFParagraph tmpParagraph = tmp.createParagraph();
            tmpParagraph.setStyle(p.getStyle());
            isNew = false;
            for (XWPFRun r : p.getRuns()) {
                XWPFRun tmpRun = tmpParagraph.createRun();
                tmpRun.setTextPosition(r.getTextPosition());

                // важно использовать именно метод toString() поскольку 
                // этот метод сохраняет возможные символы "\n", которые getText обрезает
                tmpRun.setText(r.toString());
                tmpRun.setBold(r.isBold());
                tmpRun.setFontFamily(r.getFontFamily());
                tmpRun.setFontSize(r.getFontSize());
                tmpRun.setItalic(r.isItalic());
                tmpRun.setStrike(r.isStrike());
                tmpRun.setSubscript(r.getSubscript());
                tmpRun.setUnderline(r.getUnderline());
                // метод
isPageBreak всегда возвращает false, 
                // независимо от того, содержится ли разрыв страницы в параграфе или нет
                // так что используем грязный хак
                p.setPageBreak(r.getCTR().toString().contains("<w:br w:type=\"page\"/>"));
            }
            if (p.isPageBreak()) {
                try {
                    isNew = true;
                    tmp.write(new FileOutputStream("/home/username/result/" + fileName + ".docx"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
                tmp = new XWPFDocument();

                // требуется версия POI  >= 3.8 чтобы сделать это
                tmp.createStyles();
            }
        }
        try {

             // сохраним последний кусок в файл
            tmp.write(new FileOutputStream("/home/username/result/" + fileName + ".docx"));
        } catch (IOException e) {
            e.printStackTrace();
        }

Сравнение работы геосервисов Google и Yandex

    Не так давно мне пришлось заниматься проблемой определения географических координат (широта, долгота) множества точек, зная только их адрес, что предоставило хорошую возможность для изучения возможностей сервисов геолокации, предоставляемые Google и Yandex.
    Использовать сервисы геолокации оказалось не просто, а очень просто. Для доступа к функциям не нужно генерировать никаких ключей, просто формируем запрос, дергаем URL сервиса по GET, конвертируем вернувшийся JSON в java объекты и сохраняем результат в базе.
    Исходные данные примерно таковы: несколько тысяч адресов примерно следующего формата: Область, Улица, Строение или Улица, Строение и  отдельно от них Город. Все адреса находятся в России, что несколько сужает область поиска. Адреса набирались вручную и давно, и могут содержать опечатки, описки и устаревшие сведения.
    Итак, первым пробуем Google geocoding API.
Плюсы, которые я вначале не посчитал за плюсы, а понял, что это плюсы, только после того, как воспользовался аналогичным сервисом от Яндекса:
  • Все просто и удобно. Можно ограничить область поиска, можно выбрать язык результатов поиска. Не избыточный формат возвращаемых данных.
  • Правильный поиск с приемлемой точностью. Можно рассчитывать на то, что если нашелся только один адрес, это будет корректный адрес. Поиск происходит последовательно, т.е вначале ищется населенный пункт, потом улица, потом дом. Следовательно, если населенный пункт не найден, то дальше поиск не осуществляется. Если не найдена улица или дом, поиск вернет координаты населенного пункта или центра улицы соответственно, указав найденную точность в результатах.
  • Не пытается исправить ошибки в адресе, если они есть. Другими словами, не пытается подсунуть "вероятно правильный" результат.
  • Возвращает почтовый индекс, если адрес найден с точностью до дома.
Минусы:
  • Не всегда достаточно полная база, особенно по маленьким городам, что приводит к низкой точности координат (часто до города).
  •  Формат возвращаемого результата не соответствует общепринятому в России, т.е. результат возвращается в формате "Советская ул., 10, Тюмень, Тюменская область, Россия, 625003". Кончено результат также возвращается разбитый на части по административным единицам, да и этот в принципе не сложно развернуть, но все же неудобно.
  • Иногда возвращается адрес конкретного объекта, например: "Сальское Медицинское Училище, ГОУ, Кирова ул., 17, Сальск, Ростовская область, Россия, 347630" Мне в принципе не нужно знать адрес учреждения, которое находится по данному адресу.
  • Ограничение на частоту выполнения запросов. Есть ограничение на количество запросов за день (2,5 тыс) и некое ограничение на частоту запросов, из-за чего пришлось получать адреса очень медленно и с задержками.
     В результате порядка 10% адресов оказалось невозможно  распознать, в основном из-за того, что нашлось более одного адреса. Некоторые адреса были неправильно написаны, например Кольчугин вместо Кольчугино. Некоторые адреса реально не существовали, например "Улица Тестовая д 123" в городе Бобруйск. Но больше всего проблем возникало при написании адреса в виде "Большая Сухаревская пл., 16/18 стр.2". В этом случае Гугл часто возвращал более одного результата, находя к примеру в данном случае дом 16 и дом 2.
    Подумав немного, я решил уточнить результаты с помощью Яндекса. Ведь Яндекс находится в России, значит, теоретически, результаты должны быть лучше.
   Итак, Yandex geocode API.
Плюсы:
  • Находит адрес с точностью до дома даже в маленьких городах.
  • Иногда правильно исправляет опечатки.
  • Общепринятый формат вывода результата.
Минусы:
  • Неправильный, избыточный поиск. Назовем его "боязнь ничего не найти". Вернуть любой более-менее похожий результат. Иногда это срабатывает, например, уже упоминавшийся населенный пункт Кольчугин во Владимирской области был правильно исправлен на Кольчугино, но иногда приводит к тому, что возвращается просто неверный результат, совершенно в другом городе. Например, "Смоленская обл., Рудня, ул. Киреева, 66" превращается в "Россия, Смоленская область, Смоленск, улица Кирова".
  • При указании точного адреса может вернуться более одного результата. Чтобы не ходить далеко, возьмем пример, приведенный на сайте самого Яндекса. "ул. Тверская, дом 7". Данный запрос вернет 5 результатов. Помимо ожидаемой Тверской улицы, сервис вернет также 4 Тверские-Ямские улицы. При этом у всех (кроме одной) будет точность до дома. Какой логикой руководствовались создатели сервиса, мне непонятно. Убедиться своими глазами.
  • Избыточность в описании результатов. Может быть создатели сервиса от Яндекса считают, что это придает солидности, мне же кажется, что это просто добавляет ненужной работы и программисту, и сетевому оборудованию. Количество различных java объектов, необходимых для разбора ответа от Яндекса в 2 раза больше, чем для Гугла.
Как результат: сервис от Гугла предоставляет недостаточную точность результатов в небольших городах, а сервисом от Яндекса пользоваться можно только в том случае, если проводится ручная проверка результатов.

Код для работы с сервисом Яндекса:

Использование Spring для работы со встроенным HornetQ сервером

       В предыдущем посте рассматривался пример внедрения HornetQ сервера в приложение. Попробуем немного усовершенствовать его, переложив часть работы на Spring.
       Необходимо подключить к проекту spring-jms.jar, после чего добавить немного кода в конфиг Spring
Это наш встроенный HornetQ сервер, он будет стартовать и останавливаться автоматически при поднятии контекста.

<bean class="org.hornetq.jms.server.embedded.EmbeddedJMS"
    destroy-method="stop"
     id="jmsServer"
     init-method="start"/>

Локатор ConnecitonFactory. Этот класс возвращает экземпляр ConnecitonFactory. Обычно ConnecitonFactory биндится на JNDI, но не в нашем случае, так что требуется специальный класс для того, чтобы получить ее.

 <bean class="example.jms.JmsConnecitonFactoryLocator"
                         depends-on="jmsServer"
                         factory-method="lookupConnectionFactory"
                         id="jmsConnectionFactory">
      <constructor-arg= name="server" ref="jmsServer" />
</bean>

Это стандартный JMSTemplate, с его помошью мы будет отсылать сообщения в очередь

<bean class="org.springframework.jms.core.JmsTemplate"
                         depends-on="jmsServer"
                         id="jmsQueueTemplate">
        <property name="connectionFactory">
            <ref bean="jmsConnectionFactory"/>
        </property>    
</bean>
JmsQueueLocator - класс-фабрика для получения экземпляра очереди. Необходим для того, чтобы организовать приемник сообщений.


<bean class="example.jms.JmsQueueLocator"
               depends-on="jmsServer"
               factory-method="lookupQueue"
               id="paymentQueue">
        <constructor-arg name="server" ref="jmsServer" />
        <constructor-arg name="queueName" value="queue/paymentQueue" />
</bean>
Собственно класс, отвественный за получение сообщений. Поддерживает несколько конкурентных получателей.Обратите внимание на ref="transactionPusher". Данный бин определяется в коде посредством аннотации @Component(value = "transactionPusher")


<bean id="jmsContainerPayment"  
            class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="jmsConnectionFactory"/>
        <property name="destination" ref="paymentQueue"/>
        <property name="messageListener" ref="transactionPusher" />
        <property name="concurrentConsumers" value="5"/>
</bean>


Теперь код:
Класс-фабрика для получения соединения с нашим встроенным JMS сервером

public class JmsConnecitonFactoryLocator {
    private static final Logger logger = LoggerFactory.getLogger(JmsConnecitonFactoryLocator.class);

    public static HornetQJMSConnectionFactory lookupConnectionFactory(EmbeddedJMS server){
        HornetQJMSConnectionFactory cf = (HornetQJMSConnectionFactory) server.lookup("ConnectionFactory");
        if(cf == null){
            logger.error("connection factory is null");
        }else{
            logger.info("connection factory is not null");
        }
        return cf;
    }
}


Класс-фабрика для получения экземпляра очереди

public class JmsQueueLocator {
    public static Queue lookupQueue(EmbeddedJMS server, String queueName){
        return (Queue) server.lookup(queueName);
    }
}


Класс - приемник сообщений


@Component(value = "transactionPusher")
public class TransactionPusher implements MessageListener {
    private static final Logger logger = LoggerFactory.getLogger(TransactionPusher.class);

    @Override
    public void onMessage(Message message) {

       try{
            TextMessage m = (TextMessage)message;
            m.acknowledge(); // говорим, что успешно приняли сообщение.
            // сделать что-то полезное
        } catch (JMSException e) {  

            logger.error("JMS exceptoin: ", e);
        }
    }
}


Пример использования JMSTemplate для отсылки сообщений:

public void addPaymentToQueue(final Transaction t) throws JMSException {
        jmsTemplate.send(paymentQueue, new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                TextMessage msg = session.createTextMessage();
                msg.setText(String.valueOf(t.getId()));
                return msg;
            }
        });
}


     Как можно заметить, Spring берет на себя значительную часть работы по организации рутинных операций, позволяя сосредоточиться на программировании бизнес логики приема и отправки сообщений. Класс DefaultMessageListenerContainer позволяет настраивать количество конкурентных потребителей, а также динамически увеличивать количество потребителей при увеличении нагрузки.