вторник, 18 декабря 2012 г.

hibernate-configuration-3.0.dtd

Обнаружил вчера WTF в старом проекте:

Заголовок файла hibernate.cfg.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "/usr/local/etc/hibernate-configuration-3.0.dtd">

При попытке выполнить локально естественно все падает с ошибкой с очень длинным стеком вызовов, но упирается все в:

Caused by: org.hibernate.HibernateException: Could not parse configuration: /hibernate.cfg.xml
    at org.hibernate.cfg.Configuration.doConfigure(Configuration.java:1586)
~[hibernate-core-3.5.6-Final.jar:3.5.6-Final]
...
Caused by: org.dom4j.DocumentException: /usr/local/etc/hibernate-configuration-3.0.dtd (No such file or directory) Nested exception: /usr/local/etc/hibernate-configuration-3.0.dtd (No such file or directory)
    at org.dom4j.io.SAXReader.read(SAXReader.java:484) ~[dom4j-1.6.1.jar:1.6.1]
    at org.hibernate.cfg.Configuration.doConfigure(Configuration.java:1576) ~[hibernate-core-3.5.6-Final.jar:3.5.6-Final]

Собственно все правильно, с чего бы там лежать этому файлу? Меняю на

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

Компилирую, запускаю, работает. На всякий случай отключаю сеть, проверяю, работает.
Лезу в историю svn, вижу мучительные попытки заставить код работать. Первая версия содержала правильный заголовок. Далее идут попытки что-то исправить: переключиться на http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd, убрать ссылку на dtd вовсе, подставить относительный путь к файлу, и в завершении - захардкодить абсолютный путь к файлу. Что пытались исправить, непонятно, но проблема явно была где-то в другом месте.

Ссылка на исходники DTDEntityResolver версии 3.5
и javadoc

В hibernate 3.6 поменяли url с http://hibernate.sourceforge.net на http://www.hibernate.org, но старый url все еще можно использовать, правда в лог выведется предупреждение о том, что вы используете старую версию.

среда, 5 декабря 2012 г.

Maven и определяемые на этапе сборки параметры проекта

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

После непродолжительного гугления родилась такая схема:

1. Создаем файл, в котором хранятся все возможные настроечные параметры, назовем его dev.properties, например
jdbc.url=jdbc:postgresql://localhost/test
jdbc.username=test_user
jdbc.password=test_password

2. Определяем файлы, в которые должны быть на этапе сборки проставлены данные параметры. В терминах maven этот процесс носит название фильтрация ресурсов. В нужных файлах проставляем плэйсхолдеры, к примеру в файле config.properties:
jdbc.url=${jdbc.url}
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
3. Теперь самая хитрая часть. Поскольку свойства maven проекта по сути синглетоны (как и у ant), то они сохраняют переданное при инициализации значение. Т.е если вызывать maven с параметрами командной строки -Djdbc.username=test, а в pom.xml определить <jdbc.username>xxx</jdbc.username> то параметр jdbc.username будет иметь значение test.
4. Создадим фильтр, чтобы не хранить свойства в pom.xml. Для этого используем файл, созданный на шаге 1.
<filters>
   <filter>src/main/resources/dev.properties</filter>
</filters>
Теперь по умолчанию наши свойства проекта будут грузиться из dev.config, но их можно будет переопределить через параметры командной строки.

5. Последний штрих - фильтрация ресурсов. Для этого версия maven-resources-plugin должна быть не меньше 2.3
<resources>
   <resource>
       <includes>
          <include>config.properties</include>
          <include>logback.xml</include>
       </includes>
       <directory>src/main/resources</directory>
       <filtering>true</filtering>
   </resource>
   <resource>
      <directory>src/main/resources</directory>
         <excludes>
           <exclude>dev.properties</exclude>
         </excludes>
       <filtering>false</filtering>
   </resource>
</resources>
6. Настраиваем Jenkins, подставляя нужные значения в поле MAVEN_OPTS (build->advanced->MAVEN_OPTS)
полезные ссылки: Пример фильтрации ресурсов в maven

четверг, 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 позволяет настраивать количество конкурентных потребителей, а также динамически увеличивать количество потребителей при увеличении нагрузки.





пятница, 17 августа 2012 г.

Отложенная доставка JMS сообщений в HornetQ

Иногда при обработке JMS сообщения возникла ситуация, когда нужно повторить операцию через какое-то время. Можно вместо того, чтобы висеть в Thread.sleep(), занимая ценные системные ресурсы, просто повторно послать сообщение обратно в очередь, указав время, когда его следует начать обрабатывать.
Стандарт JMS никак не регламентирует возможность доставки сообщений с определенной задержкой, тем не менее, поскольку это весьма полезная возможность, многие производители включают ее в реализацию.
Для HornetQ произвести задержку в доставке сообщения можно просто установив нужное значение в свойство _HQ_SCHED_DELIVERY (или Message.HDR_SCHEDULED_DELIVERY_TIME)
// задержка в 5 секунд относительно текущего момента.
message.setLongProperty("_HQ_SCHED_DELIVERY", System.currentTimeMillis() + 5000);
Источники:
HornetQ User Manual
Sending delayed JMS Messages

понедельник, 6 августа 2012 г.

Внедрение HornetQ JMS 2.2.5 сервера и клиента в приложение

В этой статье рассказывается, как внедрить в свое приложение замечательный HornetQ сервер. И это оказалось проще, чем я предполагал вначале.
Мне требовалась асинхронность обработки сообщений, потокобезопастность и возможность мониторинга, и желательно не писать много своего кода. Так что я остановился на HornetQ как уже знакомой мне реализации, которая удовлетворяем моим запросам.
Возьмем версию HornetQ 2.2.5 как последнюю стабильную на данный момент.
Зависимоcти для maven:
<dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-core</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
   <dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-jms</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.hornetq</groupId>
   <artifactId>hornetq-logging</artifactId>
   <version>2.2.5.Final</version>
   <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.jboss.netty</groupId>
   <artifactId>netty</artifactId>
   <version>3.2.3.Final</version>
</dependency>
<dependency>
   <groupId>org.jboss.spec.javax.jms</groupId>
   <artifactId>jboss-jms-api_1.1_spec</artifactId>
   <version>1.0.0.Final</version>
   <scope>compile</scope>
</dependency>
Конфигурация сервера и очередей.

В classpath должны находиться файлы hornetq-configuration.xml, содержащий настройки сервера, hornetq-jms.xml с настройками очередей, и hornetq-users.xml, содержащий настройки пользователей.
hornetq-configuration.xml

<configuration xmlns="urn:hornetq"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-configuration.xsd">

    <persistence-enabled>false</persistence-enabled>
    <!-- Connectors -->

    <connectors>
        <connector name="in-vm">
            <factory-class>org.hornetq.core.remoting.impl.invm.InVMConnectorFactory</factory-class>
        </connector>
    </connectors>

    <acceptors>
        <acceptor name="in-vm">
            <factory-class>org.hornetq.core.remoting.impl.invm.InVMAcceptorFactory</factory-class>
        </acceptor>
    </acceptors>

    <!-- Other config -->

    <security-settings>
        <!--security for example queue-->
        <security-setting match="#">
            <permission type="createDurableQueue" roles="guest"/>
            <permission type="deleteDurableQueue" roles="guest"/>
            <permission type="createNonDurableQueue" roles="guest"/>
            <permission type="deleteNonDurableQueue" roles="guest"/>
            <permission type="consume" roles="guest"/>
            <permission type="send" roles="guest"/>
        </security-setting>
    </security-settings>
</configuration>

hornetq-jms.xml 
<configuration xmlns="urn:hornetq"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-jms.xsd">

    <connection-factory name="ConnectionFactory">
        <connectors>
            <connector-ref connector-name="in-vm"/>
        </connectors>
        <entries>
            <entry name="ConnectionFactory"/>
        </entries>
        <consumer-window-size>0</consumer-window-size>
        <retry-interval>1000</retry-interval>
        <retry-interval-multiplier>1.5</retry-interval-multiplier>
        <max-retry-interval>60000</max-retry-interval>
        <reconnect-attempts>1000</reconnect-attempts>
    </connection-factory>

    <!--the queue used by the example-->
    <queue name="paymentQueue">
        <entry name="queue/paymentQueue"/>
    </queue>
    <queue name="statusQueue">
        <entry name="queue/statusQueue"/>
    </queue>

</configuration>

hornetq-jms.xml
<configuration xmlns="urn:hornetq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="urn:hornetq /schema/hornetq-users.xsd">
    <!-- the default user.  this is used where username is null-->
    <defaultuser name="guest" password="guest">
        <role name="guest"/>
    </defaultuser>
</configuration>


Старт встроенного сервера очень прост:
  
import org.hornetq.jms.server.embedded.EmbeddedJMS;

  EmbeddedJMS server = new EmbeddedJMS();
  server.start();
Далее создаем нужное число потребителей сообщений:
 
            QueueConnectionFactory cf = (QueueConnectionFactory) server.lookup("ConnectionFactory");
            Queue queue = (Queue) server.lookup("queue/paymentQueue");
            QueueConnection conn = cf.createQueueConnection("guest", "guest");
            for(int i = 0; i< Config.getConsumerCount(); i++){
                Session consumerSession = conn.createSession(false, Session.CLIENT_ACKNOWLEDGE);
                MessageConsumer consumer = consumerSession.createConsumer(queue);
                consumer.setMessageListener(new PaymentSender());
            }
Класс PaymentSender должен реализовывать интерфейс javax.jms.MessageListener
Запускаем обработчики:
 
conn.start();
Теперь нужно создать класс для отсылки сообщений.
 
    private static QueueConnectionFactory qconFactory = null;
    private static QueueConnection qcon = null;


    public static void init() throws JMSException {
        qconFactory = (QueueConnectionFactory) HornetqListener.server.lookup("ConnectionFactory");
        qcon = qconFactory.createQueueConnection("guest", "guest");

    }

    public static void destroy() throws JMSException {
        qcon.close();
    }

    public static void addPaymentToQueue(Transaction t) throws JMSException {

        QueueSession qsession = qcon.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = (Queue) HornetqListener.server.lookup("queue/paymentQueue");
        QueueSender qsender = qsession.createSender(queue);
        qcon.start();
        TextMessage msg = qsession.createTextMessage();
        msg.setText(String.valueOf(t.getId()));
        logger.debug("message {} putted in queue 'queue/paymentQueue'", msg.getText());
        qsender.send(msg);
        qsession.close();
    }
HornetqListener.server - это статическая переменная, в которую мы сохранили созданный экземпляр объекта EmbeddedJMS
Мониторинг очередей реализуем через JMX
private static int max_queue = 200;

QueueView paymentqueue = jmxQuery("paymentQueue");
if(paymentqueue.getMessagesInQueue() > max_queue)
  max_queue = (int) paymentqueue.getMessagesInQueue();
String percentPayment = (new Double(paymentqueue.getMessagesInQueue()))/max_queue * 100d + "%";

private QueueView jmxQuery(String queueName) throws Exception {
        MBeanServer mbeanServer = java.lang.management.ManagementFactory.getPlatformMBeanServer();
        QueueView queueView = new QueueView();

        ObjectName name = new ObjectName("org.hornetq:module=JMS,type=Queue,name=\""+ queueName +"\"");

        queueView.setConsumersCount((Integer) mbeanServer.getAttribute(name, "ConsumerCount"));
        queueView.setMessagesDelivering((Integer) mbeanServer.getAttribute(name, "DeliveringCount"));
        queueView.setMessagesInQueue((Long) mbeanServer.getAttribute(name, "MessageCount"));
        queueView.setMessagesAdded((Long) mbeanServer.getAttribute(name, "MessagesAdded"));
        queueView.setName((String) mbeanServer.getAttribute(name, "Name"));

        name = new ObjectName("org.hornetq:module=Core,type=Server");

        queueView.setConnectionCount((Integer) mbeanServer.getAttribute(name, "ConnectionCount"));
        queueView.setThreadPoolMaxSize((Integer) mbeanServer.getAttribute(name, "ThreadPoolMaxSize"));
        queueView.setServerVersion((String) mbeanServer.getAttribute(name, "Version"));
        return queueView;
    }
Конфигурационные файлы взяты отсюда

четверг, 2 августа 2012 г.

Загрузка диапазонов номеров мобильных операторов с сайта Россвязи

На сайте Россвязи есть замечательный, периодически обновляющийся список, содержащий номерные ёмкости и телефонные компании, к которым эти диапазоны относятся. В данной статье описывается процедура получения и разбора этого файла с помощью апачевского HttpClient и обычного SAX парсера.

  • Скачиваем html со списком кодов

Для этого вполне подойдет org.apache.http.impl.client.DefaultHttpClient

DefaultHttpClient client = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://rossvyaz.ru/docs/articles/DEF-9x.html");

HttpResponse resp = client.execute(httpget);
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
    InputStream is = resp.getEntity().getContent();
    List registry = new ArrayList();
    parseHTML(is, registry);
}

Но вот незадача, контент сжат gzip'ом. В принципе это даже неплохо, уменьшается время загрузки и сетевой трафик, так что просто добавим поддержку gzip в HttpClient. Делается это путем добавления перехватчиков на запрос и ответ:

private DefaultHttpClient prepareHttpClient() {
        DefaultHttpClient client = new DefaultHttpClient();
        client.addRequestInterceptor(new HttpRequestInterceptor() {

            public void process(
                    final HttpRequest request,
                    final HttpContext context) throws HttpException, IOException {
                if (!request.containsHeader("Accept-Encoding")) {
                    request.addHeader("Accept-Encoding", "gzip");
                }
            }

        });

        client.addResponseInterceptor(new HttpResponseInterceptor() {
            public void process(
                    final HttpResponse response,
                    final HttpContext context) throws HttpException, IOException {
                HttpEntity entity = response.getEntity();
                if (entity != null) {
                    Header ceheader = entity.getContentEncoding();
                    if (ceheader != null) {
                        HeaderElement[] codecs = ceheader.getElements();
                        for (int i = 0; i < codecs.length; i++) {
                            if (codecs[i].getName().equalsIgnoreCase("gzip")) {
                                response.setEntity(
                                        new GzipDecompressingEntity(response.getEntity()));
                                return;
                            }
                        }
                    }
                }
            }

        });
        return client;
    }

Теперь осталось разобрать полученный html и вытащить из него DEF коды.

  • Парсим полученный html

Воспользуется стандартным потоковым java SAX парсером (javax.xml.stream.XMLEventReader), поскольку полученный файл большой, и к тому же является невалидным XML файлом, так что придется произвести небольшие ухищрения, чтобы его разобрать. Разбор производится "на лету", то есть начинается уже в процессе скачивания html, за счет чего достигается приличная скорость при небольшом потреблении памяти. Весь процесс занимает порядка трех секунд.


static final String TR = "tr";
static final String TD = "td";

...

private void parseHTML(InputStream is, List<DEFCode> result) {
        try {
            XMLInputFactory inputFactory = XMLInputFactory.newInstance();
            XMLEventReader eventReader = inputFactory.createXMLEventReader(is, "windows-1251");
            String[] buff = new String[6];
            int count = 0;
            int ind = 0;
            while (eventReader.hasNext()) {
                try {
                    XMLEvent event = eventReader.nextEvent();

                    if (event.isStartElement()) {
                        StartElement startElement = event.asStartElement();
                        // start new row
                        if (TR.equals(startElement.getName().getLocalPart())) {
                            buff = new String[6];
                            count++;
                            ind = 0;
                        }

                        if (TD.equals(event.asStartElement().getName().getLocalPart())) {
                            event = eventReader.nextEvent();
                            buff[ind++] = event.asCharacters().getData();
                            continue;
                        }
                    }

                    if (event.isEndElement()) {
                        EndElement endElement = event.asEndElement();
                        if (TR.equals(endElement.getName().getLocalPart())) {
                            if(count != 1){ // пропускаем первую строку с заголовком
                                result.add(validateRow(count, buff));
                            }
                        }
                    }
                } catch (XMLStreamException e) { // вероятнее всего это незакрытый тег, игнорируем ошибку
                    logger.error("skip error");
                } catch(Exception ex){ // вероятнее всего файл просто закончился, завершаем обработку
                    logger.error("skip error, break");
                    break;
                }
            }
            eventReader.close();

        } catch (XMLStreamException e) { // что-то не так с кодировкой или структурой файла
            e.printStackTrace();
        }
    }

private DEFCode validateRow(int counter, String[] nextLine) throws ValidationException {
        // здесь проводится проверка строки на валидность и если 
        // строка не валидна, выбрасывается эксепшн с номером строки 
    }

вторник, 24 июля 2012 г.

Настройка очередей в JBoss 7

В этой статье описывается настройка очередей в JBoss AS 7.1.1

Последний раз мне приходилось работать с JBoss 5 и с тех пор есть определенный прогресс. Сервер запускается и останавливается быстро, появились средства конфигурации и мониторинга. В 7й ветке есть тенденция отказа от отдельных xml файлов для настроек очередей и источников данных, и рекомендованный способ - добавлять их непосредственно в конфигурационный файл сервера. Хотя старый способ все еще работает, добавленные таким образом очереди не будут видны в админской консоли как очереди и по ним нельзя будет посмотреть статистику.

Итак, для начала нам нужно просто подключить поддержку очередей. По умолчанию в standalone.xml она не подключена. Для включения поддержки копируем из standalone-full.xml все, связанное с messaging, а именно:

...

<extension module="org.jboss.as.messaging"/>

...

<subsystem xmlns="urn:jboss:domain:messaging:1.1">
            <hornetq-server>
                <persistence-enabled>true</persistence-enabled>
                <journal-file-size>102400</journal-file-size>
                <journal-min-files>2</journal-min-files>

                <connectors>
                    <netty-connector name="netty" socket-binding="messaging"/>
                    <netty-connector name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                    </netty-connector>
                    <in-vm-connector name="in-vm" server-id="0"/>
                </connectors>

                <acceptors>
                    <netty-acceptor name="netty" socket-binding="messaging"/>
                    <netty-acceptor name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                        <param key="direct-deliver" value="false"/>
                    </netty-acceptor>
                    <in-vm-acceptor name="in-vm" server-id="0"/>
                </acceptors>
                <security-settings>
                    <security-setting match="#">
                        <permission type="send" roles="guest"/>
                        <permission type="consume" roles="guest"/>
                        <permission type="createNonDurableQueue" roles="guest"/>
                        <permission type="deleteNonDurableQueue" roles="guest"/>
                    </security-setting>
                </security-settings>

                <address-settings>
                    <address-setting match="#">
                        <dead-letter-address>jms.queue.DLQ</dead-letter-address>
                        <expiry-address>jms.queue.ExpiryQueue</expiry-address>
                        <redelivery-delay>1000</redelivery-delay>
                        <max-size-bytes>10485760</max-size-bytes>
                        <address-full-policy>BLOCK</address-full-policy>
                        <message-counter-history-day-limit>10</message-counter-history-day-limit>
                    </address-setting>
                </address-settings>
               <jms-connection-factories>
                    <connection-factory name="InVmConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/ConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <connection-factory name="RemoteConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="netty"/>
                        </connectors>
                        <entries>
                            <entry name="RemoteConnectionFactory"/>
                            <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <pooled-connection-factory name="hornetq-ra">
                        <transaction mode="xa"/>
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/JmsXA"/>
                        </entries>
                    </pooled-connection-factory>
                </jms-connection-factories>
          <jms-destinations>
                    <jms-queue name="testQueue">
                        <entry name="queue/test"/>
                        <entry name="java:jboss/exported/jms/queue/test"/>
                    </jms-queue>
                    <jms-topic name="testTopic">
                        <entry name="topic/test"/>
                        <entry name="java:jboss/exported/jms/topic/test"/>
                    </jms-topic>
          </jms-destinations>
</hornetq-server>
...
        <socket-binding name="messaging" port="5445"/>
        <socket-binding name="messaging-throughput" port="5455"/>

После этого можно добавить непосредственно сами очереди:

<jms-destinations>
                    <jms-queue name="defaultQueue">
                        <entry name="queue/default">
                        <entry name="java:jboss/defaultQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="beelineQueue">
                        <entry name="queue/beeline">
                        <entry name="java:jboss/beelineQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="mtsQueue">
                        <entry name="queue/mts">
                        <entry name="java:jboss/mtsQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="mfQueue">
                        <entry name="queue/mf">
                        <entry name="java:jboss/mfQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="ExpiryQueue">
                        <entry name="queue/expiry">
                        <entry name="java:jboss/ExpiryQueue">
                    </entry></entry></jms-queue>
                    <jms-queue name="DLQ">
                        <entry name="queue/dead">
                        <entry name="java:jboss/deadQueue">
                    </entry></entry></jms-queue>
                </jms-destinations>

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

Добавляем в конфигурацию описание этих очередей. Тут есть магия с префиксами jms.queue. где-то их нужно указывать, где-то нет.

После этого можно приступать к настройке обработчиков сообщений. Это обычные Message Driving Bean (MDB). Можно описать их с помощью аннотаций:

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
        @ActivationConfigProperty(propertyName="destination", propertyValue="java:jboss/defaultQueue"),
        @ActivationConfigProperty(propertyName = "maxSession", propertyValue = "10")
        })
public class DefaultQueueHandler implements MessageListener {

При деплое они подцепятся автоматически. В принципе это уже рабочая конфигурация, но если используется spring или hibernate может потребоваться создание контекста, еще до того, как очереди начнут обрабатывать сообщения. В этом случае можно поступить следующим образом: создать EJB, ответственный за инициализацию контекста, перед инициализацией остановить обработку сообщений во всех очередях, а после инициализации возобновить ее. Это позволить избежать включения отдельного war модуля только для того, чтобы поднять контекст.

@Startup
@Singleton(name = "InitContextEJB")
public class InitContextEJB{

    private String jconsolePrefix = "jboss.as:subsystem=messaging,hornetq-server=default,jms-queue=";

    @PostConstruct
    public void atStartup() {
        stopQueues();
        System.out.println("+++++++++++++++++++ CONTEXT +++++++++++++++++++");
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                "applicationContext.xml",
                "data-source.xml"
                );
        startQueues();
    }

    @PreDestroy
    public void atDestroy(){
        stopQueues();
    }

    private void startQueues() {
        try {
            setPaused(new ObjectName(jconsolePrefix + "defaultQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "beelineQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "mtsQueue"), false);
            setPaused(new ObjectName(jconsolePrefix + "mfQueue"), false);
        } catch (MalformedObjectNameException e) {
            e.printStackTrace();
        }

    }

    private void stopQueues() {
        try {
            setPaused(new ObjectName(jconsolePrefix + "defaultQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "beelineQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "mtsQueue"), true);
            setPaused(new ObjectName(jconsolePrefix + "mfQueue"), true);
        } catch (MalformedObjectNameException e) {
            e.printStackTrace();
        }

    }

Для остановки и запуска очередей можно вспомнить, что они по-сути MDB, а значит доступны через JMX консоль и javax.management.MBeanServer. Приведу вариант с MBeanServer, как более независимый от реализации и стандартный.

protected void setPaused(final ObjectName objectName, final Boolean method) {
        try {
            MBeanServer mbeanServer = java.lang.management.ManagementFactory.getPlatformMBeanServer();
            System.out.println(objectName + " paused before= " + mbeanServer.getAttribute(objectName, "paused"));

            mbeanServer.invoke(objectName, method ? "pause" : "resume",new Object[]{}, new String[]{});

            System.out.println(objectName + " paused after= " + mbeanServer.getAttribute(objectName, "paused"));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Перехват и глотание исключений при остановке желательно, поскольку сам HornetQ сервер может остановиться раньше (например при остановке JBoss)

Полезные ссылки:

четверг, 21 июня 2012 г.

Генерация pdf из java

Появилась недавно задача - добавить в pdf шаблон несколько строк, причем pdf был довольно сложный по составу. Немного поискав варианты решения, понял, есть два пути - использовать генератор с шаблонами, либо загружать готовый pdf документ и непосредственно выводить в него текст. Поняв, что первый вариант меня не устраивает по той причине, что шаблон уже в pdf формате, и рисовать его заново мне не хочется, я остановился на варианте с непосредственным выводом текста в pdf.

Первой мне попалась библиотека pdfbox. К сожалению, не удалось заставить ее корректно отображать русские буквы. Возможно, мне просто не повезло со шрифтом, который я пытался использовать, или не понравилась кодировка, не знаю. Возможно когда-нибудь я предоставлю ей второй шанс.

Следующая на очереди была iText. С помощью неё удалось решить поставленную задачу. Замеченные особенности:

  • Практически все тулзы для генерации pdf файлов внедряют в итоговый файл только subset'ы используемых шрифтов. Вывод - не нужно использовать внедренные в pdf шрифты, если не хотите получить слова с отсутствующими буквами.
  • Стандартные шрифты, например BaseFont.HELVETICA, содержат в себе только латинские символы и не могут быть использованы для вывода текста в кириллице.
  • Используйте Unicode шрифты, загружайте их из файла и внедряйте в итоговый pdf документ. Сделать это просто, вызвав BaseFont.createFont("Liberation-Sans.ttf", BaseFont.IDENTITY_H, true)
И пример использования:

    File tmp = File.createTempFile("pdf", null);
    FileOutputStream outStream = new FileOutputStream(tmp);
    Document pdDoc = new Document(PageSize.A4);
    PdfWriter writer = PdfWriter.getInstance(pdDoc, outStream);
    pdDoc.open();
    try{
        PdfContentByte cb = writer.getDirectContent();

        InputStream is = 
                    getClass().getClassLoader().getResourceAsStream("/templates/template.pdf");
        if (is == null) {
            throw new DocumentException("cannot load template");
        }

        String fontPath = "/templates/" + 
                          Config.getInstance().getString("templates.font");
        URL u = getClass().getClassLoader().getResource(fontPath);
        if(u == null){
            throw new  DocumentException("cannot load font");
        }
        // load font from resource
        BaseFont bf = BaseFont.createFont(u.getFile(), BaseFont.IDENTITY_H, true);
        PdfReader reader = new PdfReader(is);
        PdfImportedPage page = writer.getImportedPage(reader, 1);

        // Copy first page of existing PDF into output PDF
        pdDoc.newPage();
        cb.addTemplate(page, 0, 0);

        cb.saveState();

        // write  text
        cb.beginText();
        cb.setFontAndSize(bf, 10);
        cb.setTextMatrix(197, 676);
        cb.showText("some text");

        cb.setFontAndSize(bf, 7);

        cb.setTextMatrix(70, 645);
        cb.showText("smaller text");

        cb.endText();
        cb.restoreState();


        page = writer.getImportedPage(reader, 2);
        pdDoc.newPage();
        //add second page as is
        cb.addTemplate(page, 0, 0);


        page = writer.getImportedPage(reader, 3);
        pdDoc.newPage();
        //add third page as is
        cb.addTemplate(page, 0, 0);
    }finally{
        pdDoc.close();
    }

среда, 20 июня 2012 г.

Внедрение Groovy для вычисления простых выражений

Небольшой пример внедрения groovy для вычисления простых выражений. Не требует создания сложных тяжеловесных конструкций с Binding и создания отдельных файлов скриптов.

import groovy.util.Eval;

HashMap params = new HashMap();
params.put("field1", "10");
params.put("field2", "11");

Boolean res = (Boolean) Eval.me("param", params, "param.field1 == param.field2");
Документация на класс Eval

пятница, 15 июня 2012 г.

Использование OpenSource крипто библиотек для работы с ГОСТовыми сертификатами CryptoPro

В данной статье делается попытка ответить на вопрос: возможно ли использовать OpenSource библиотеки для работы с ГОСТовыми сертификатами?
Если коротко, то да.
Для проверки использовалась последняя на данный момент версия OpenSSL (1.0.1с), собранная из исходников с поддержкой gost engine
Для начала нужно установить КриптоПро CSP 3.6 R3, поскольку в нем реализована поддержка экспорта сертификатов в формате pfx. Эту часть работ нужно делать в операционной системе Windows. Не буду описывать подробно этот процесс, скажу лишь, что нужно сохранить приватный ключ в формате CryptoPro на флэшку и импортировать его в хранилище "Реестр". Выгрузив сертификат в файл, например store.pfx (включая приватный ключ), можно вывести приватный ключ в stdout командой
/usr/local/ssl/bin/openssl pkcs12 -info -engine gost -nodes -in ~/store.pfx 

  • -info вывести информацию о содержимом файла в формате pkcs12 (.pfx)
  • -engine использовать указанный криптографический модуль
  • -nodes не нужно шифровать приватный ключ
  • -in входной файл
Если вы получаете ошибку вида "unknown PBE algorithm"
Enter Import Password:
MAC Iteration 2000
MAC verified OK
PKCS7 Data
Shrouded Keybag: undefined, Iteration 2000
Bag Attributes
    Microsoft Local Key set: <No Values>
    localKeyID: 01 00 00 00
    friendlyName: REGISTRY\\1ba6dcf8-b953-4774-8a9e-de98de071f24
    Microsoft CSP Name: Crypto-Pro GOST R 34.10-2001 Cryptographic Service Provider
Error outputting keys and certificates
140669637789344:error:06074079:digital envelope routines:EVP_PBE_CipherInit:unknown pbe algorithm:evp_pbe.c:167:TYPE=1.2.840.113549.1.12.1.80
140669637789344:error:23077073:PKCS12 routines:PKCS12_pbe_crypt:pkcs12 algor cipherinit error:p12_decr.c:83:
140669637789344:error:2306A075:PKCS12 routines:PKCS12_item_decrypt_d2i:pkcs12 pbe crypt error:p12_decr.c:130:
То это означает, что вам потребуется другое средство для получения pfx файла, утилитка P12FromGostCSP.exe
Если вы получаете ошибку вида "unknown digest algorithm"

Enter Import Password:
MAC Iteration 2048
Mac verify error: invalid password?
140572666472096:error:2306B076:PKCS12 routines:PKCS12_gen_mac:unknown digest algorithm:p12_mutl.c:88:
140572666472096:error:2307E06D:PKCS12 routines:PKCS12_verify_mac:mac generation error:p12_mutl.c:122:

то скорее всего в вашем openssl нет поддержки ГОСТовых алгоритмов либо вы используете не ту копию openssl.

Если все прошло без ошибок, то в консоли вы получите приватный ключ и сертификат. Скопировав выведенный в консоль приватный ключ (вместе со словами -----BEGIN PRIVATE KEY----- и -----END PRIVATE KEY-----) в отдельный файл private.key мы сможем с его помощью создавать ЭЦП.

Команда для генерации примерно следующая:

/usr/local/ssl/bin/openssl cms -sign -inkey ~/private.key -in ~/file.txt -CAfile ~/CA.cer 
-signer ~/client.cer -engine gost -out ~/test.sign -outform DER -noattr -binary

  • -sign генерация подписи
  • -inkey путь к приватному ключу
  • -in подписываемый файл
  • -CAfile файл УЦ
  • -signer файл с сертификатом, которым осуществляется подпись
  • -engine использовать указанный криптографический модуль
  • -out файл, в который будет записана ЭЦП
  • -outform формат подписи
  • -noattr не добавлять дополнительных аттрибутов в подпись
  • -binary считать подписываемый файл массивом байт, а не текстом

К сожалению, Bouncy Castle (версии 1.46), хоть в нем и декларируется поддержка ГОСТовых алгоритмов, не может разобрать формат приватного ключа, так что использовать его для генерации ЭЦП на данный момент не представляется возможным.

UPD:
Bouncy Castle 1.55 уже умеет работать с такими приватными ключами

суббота, 9 июня 2012 г.

Создание ЭЦП с помощью CryptoPro

В данной статье описывается процесс создания ЭЦП c помощью библиотеки CryptoPro.
Для создания подписи необходим приватный ключ и сертификат. Рассмотрим гипотетическую ситуацию, когда приватный ключ прислан по почте, а не сгенерирован самостоятельно - порочная, но широко распространенная практика.
Предположим, что файл клиентского сертификата называется client.cer, сертификата УЦ - CA.cer, а приватный ключ находится в директории 999996.000

1. Установка CryptoPro CSP и CryptoPro JCP

Добываем каким-либо образом дистрибутивы, самый простой способ - скачать их с сайта CryptoPro, предварительно там зарегистрировавшись. Триальная версия полнофункциональна и работает 30 дней.
При установке CSP пакет cprocsp-rdr-gui скорее всего не установится, поскольку использует древние версии Motif, но он не нужен для работы, так что установка данного пакета не является обязательной.
Также нужно убедиться, что в системе есть libcurl.so (эта библиотека используется для получения CRL)
После установки необходимо проверить, что в файле /etc/opt/cprocsp/config.ini прописан правильный путь к libcurl.so, по умолчанию он ведет на /usr/local/lib/64/libcurl.so что является ошибкой.
После этого устанавливаем CryptoPro JCP (jcp_plus_jtls_1.0.53.jar)
После успешной установки нужно запустить ControlPane.sh из-под рута и поменять путь к хранилищу ключей на /var/opt/cprocsp/keys/{$user.name}

2. Установка приватного ключа и сертификатов

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

  • Скопировать в корень дискеты или флэшки сертификат и приватный ключ. Под приватным ключом понимается директория 999996.000 и ее содержимое, файлы header.key, masks2.key, masks.key, name.key, primary2.key, primary.key
    $ cp -R /path/to/key/999996.000 /media/flashdrive/
    $ cp /path/to/cert/client.cer /media/flashdrive/
    
  • Выполнить команду по копированию ключа с флэшки на диск. Ключ попадет в пользовательское хранилище 'My'. Выполнять команду нужно под пользователем, который будет использовать данный контейнер для подписи. 999996 - название (alias) контейнера. gate@example.com - то, что прописано в поле E сертификата ( можно посмотреть командой keytool --printcert -file /path/to/cert/client.cer )
    $ csptest -keycopy -src '\\.\FLASH\gate@example.com' -dest '\\.\HDIMAGE\999996'
    Проверить, что все скопировалось, можно командой
    $ ls -al /var/opt/cprocsp/keys/<username>
  • Альтернативный путь, если нет дискеты или csptest выдает ошибку
    Error number 0x8009000f (2148073487). Object already exists.
    Руками скопировать приватный ключ в хранилище командой
    $ cp -R /path/to/key/999996.000 /var/opt/cprocsp/keys/<username>/
  • Ассоциировать сертификат с контейнером. Сертификат попадет в пользовательское хранилище 'My'
    $ certmgr -inst -file /path/to/file/client.cer -cont '\\.\HDIMAGE\999996'
  • Установить сертификат УЦ из-под пользователя root командой
    # certmgr -inst -store root -file /path/to/file/CA.cer
Контейнер должен быть готов к использованию, проверить это можно командами:
$ certmgr --list
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : C=RU, S=RUSSIA, L=MOSCOW, O=ORGANIZATION, OU=IT, CN=GATE_DEMO, E=gate@example.com
Serial         : 0x2225000000007DF78065
PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB
=============================================================================

[ErrorCode: 0x00000000]
Обратите внимание на строку "PrivateKey Link: Yes. Container: HDIMAGE\\999996.000\D7BB". Она показывает наличие связи сертификата и приватного ключа, если выводится "PrivateKey Link: No" это означает, что связь не установлена и использовать такой контейнер для подписи не удастся.
$ certmgr --list -store root
Certmgr 0.9 prerelease (c) "CryptoPro",  2007-2010.
program for managing certificate(CRL) and stores

=============================================================================
1-------
Issuer         : DC=ru, DC=issuer, CN=EXAMPLE
Subject        : DC=ru, DC=issuer, CN=EXAMPLE
Serial         : 0xE44263EF7B42044F9E20FFF14C6F1327
PrivateKey Link: No
=============================================================================

[ErrorCode: 0x00000000]

3. Генерация ЭЦП

Под ЭЦП обычно понимается отсоединенная (detached) подпись в формате pkcs#7. Т.е помимо самой подписи, в сообщение внедряется вся цепочка сертификатов. CryptoPro не предоставляет отдельного пакета CMS для легкой генерации криптографических сообщений, но в принципе в пакете JCP есть все необходимое, чтобы сформировать корректное сообщение самостоятельно. Код по большей части взят из примеров, которые идут в JCP.
    private static byte[] signWithCryptoProJcp(byte[] data) throws Exception{

        String alias = "999996";
        String caFile = "/path/to/CA.cer";
        String certFile = "/path/to/client.cer";

        //load keys for sign
        final PrivateKey[] keys = new PrivateKey[1];
        keys[0] = CMStools.loadKey(alias, null);

        //load certificates chain
        final Certificate[] certs = new Certificate[2];
// функция CMStools.loadCertificate() почему-то не работает, хотя сертификат есть в хранилище
// пришлось читать сертификат из файла
        certs[0] = CMStools.readCertificate(certFile);
        certs[1] = CMStools.readCertificate(caFile);
        return createCMS(data, keys, certs, true);
    }


    private static byte[] createCMS(byte[] data, PrivateKey[] keys,
                                   Certificate[] certs,
                                   boolean detached)
            throws Exception {
        //create CMS
//        Array.writeFile("/home/grigory/test.msg", data);
        final ContentInfo all = new ContentInfo();
        all.contentType = new Asn1ObjectIdentifier(new OID(CMStools.STR_CMS_OID_SIGNED).value);
        final SignedData cms = new SignedData();
        all.content = cms;
        cms.version = new CMSVersion(1);
        // digest
        cms.digestAlgorithms = new DigestAlgorithmIdentifiers(1);
        final DigestAlgorithmIdentifier a = new DigestAlgorithmIdentifier(
                new OID(CMStools.DIGEST_OID).value);
        a.parameters = new Asn1Null();
        cms.digestAlgorithms.elements[0] = a;
        if (detached)
            cms.encapContentInfo = new EncapsulatedContentInfo(
                    new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                    null);
        else
            cms.encapContentInfo =
                    new EncapsulatedContentInfo(new Asn1ObjectIdentifier(
                            new OID(CMStools.STR_CMS_OID_DATA).value),
                            new Asn1OctetString(data));
        // certificates
        final int ncerts = certs.length;
        cms.certificates = new CertificateSet(ncerts);
        cms.certificates.elements = new CertificateChoices[ncerts];
        for (int i = 0; i < cms.certificates.elements.length; i++) {
            final ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate certificate =
                    new ru.CryptoPro.JCP.ASN.PKIX1Explicit88.Certificate();
            final Asn1BerDecodeBuffer decodeBuffer =
                    new Asn1BerDecodeBuffer(certs[i].getEncoded());
            certificate.decode(decodeBuffer);
            cms.certificates.elements[i] = new CertificateChoices();
            cms.certificates.elements[i].set_certificate(certificate);
        }
        // Signature.getInstance
        final Signature signature =
                Signature.getInstance(JCP.GOST_EL_SIGN_NAME);
        byte[] sign;
        // signer infos
        final int nsign = keys.length;
        cms.signerInfos = new SignerInfos(nsign);
        for (int i = 0; i < cms.signerInfos.elements.length; i++) {
            signature.initSign(keys[i]);
            signature.update(data);
            sign = signature.sign();
            cms.signerInfos.elements[i] = 
                     new ru.CryptoPro.JCP.ASN.CryptographicMessageSyntax.SignerInfo();
            cms.signerInfos.elements[i].version = new CMSVersion(1);
            cms.signerInfos.elements[i].sid = new SignerIdentifier();

            final byte[] encodedName =
                    ((X509Certificate) certs[i]).getIssuerX500Principal().getEncoded();
            final Asn1BerDecodeBuffer nameBuf = new Asn1BerDecodeBuffer(encodedName);
            final Name name = new Name();
            name.decode(nameBuf);

            final CertificateSerialNumber num = new CertificateSerialNumber(
                    ((X509Certificate) certs[i]).getSerialNumber());
            cms.signerInfos.elements[i].sid.set_issuerAndSerialNumber(
                    new IssuerAndSerialNumber(name, num));
            cms.signerInfos.elements[i].digestAlgorithm =
                    new DigestAlgorithmIdentifier(new OID(CMStools.DIGEST_OID).value);
            cms.signerInfos.elements[i].digestAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signatureAlgorithm =
                    new SignatureAlgorithmIdentifier(new OID(CMStools.SIGN_OID).value);
            cms.signerInfos.elements[i].signatureAlgorithm.parameters = new Asn1Null();
            cms.signerInfos.elements[i].signature = new SignatureValue(sign);
        }
        // encode
        final Asn1BerEncodeBuffer asnBuf = new Asn1BerEncodeBuffer();
        all.encode(asnBuf, true);
//        Array.writeFile("/home/grigory/test.signature", asnBuf.getMsgCopy());
        return asnBuf.getMsgCopy();
    }
 
У CryptoPro есть замечательный сервис проверки ЭЦП, где можно проверить, что сгенерированная подпись верна.

вторник, 22 мая 2012 г.

Maven. Добавление локального jar файла в зависимости

Собственно все просто, пример стянут со stackoverflow


   <dependency>
       <groupId>ru.signalcom</groupId>
       <artifactId>signalcom.cms</artifactId>
       <version>1.2.4</version>
       <scope>system</scope>
       <systemPath>${basedir}/lib/sccms.jar</systemPath>
   </dependency>

пятница, 30 марта 2012 г.

SimpleDateFormat is not thread-safe

Забавно, но класс SimpleDateFormat оказывается не tread-safe, так что хранение инстанса в статической переменной и использование ее в многопоточном приложение иногда приводит к тому, что дата парсится неправильно.

Из вариантов решения: либо синхронизировать вызовы методов parse и format, либо использовать копирующий конструктор при каждом обращении.

четверг, 15 марта 2012 г.

Создание ЭЦП с помощью Signal COM

Цель этой статьи - разобраться, как использовать библиотеку Signal COM для создания ЭЦП. Под ЭЦП понимается подпись в формате PKCS#7, которая содержит помимо собственно подписи(ей) также информацию о сертификатах и не обязательные дополнительные атрибуты (время подписи и т.п)

  • Получение и установка дистрибутива

Вначале нужно получить дистрибутив библиотеки Signal COM, купив ее у производителя или скачав триальную версию с сайта. Нужны дистрибутивы Java CMS и JCP. Приобретать SDK настоятельно не рекомендую, поскольку содержание полностью соответствует таковому из триальной версии.

Установив дистрибутив, лучше в отдельную копию JDK, можно посмотреть, что же поменялось. Добавился новые jar файлы с реализацией криптопровайдера sccsp.jar и CMS sccms.jar, запись о регистрации криптопровайдера ru.signalcom.crypto.provider.SignalCOMProvider в файле java.security, подменились файлы local_policy.jar и US_export_policy.jar. По идее можно сразу приступать к работе.

  • Формирование хранилища

SignalCOM использует для хранения приватного ключа формат pkcs#8. Приватный ключ связан каким-то образом с генератором случайных чисел (при инициализации генератора случайных чисел требуется пароль от приватного ключа). Для работы в java удобнее всего сформировать хранилище в формате JKS или PKCS#12, для этого нужен приватный ключ и цепочка сертификатов.

Ниже приведен пример сборки хранилища для формата PKCS#12; формирование хранилища в формате JKS ничего принципиально от него ничем не отличается. Позднее, используя keytool, всегда можно сконвертировать хранилище в нужный формат, главное не забыть указать опцию -srcprovidername SC, поскольку не все провайдеры могут работать с гостовыми сертификатами. Также нужно учесть, что пароль на хранилище и приватный ключ будет одинаковым, даже если первоначально приватный ключ не имел пароля.

String root = "/home/gr/contact/TestKey";
        String certRoot = "/home/gr/contact/TestKey/CA";
        String clientCertRoot = "/home/gr/contact/TestKey/OpenKey";
        String passwd = "changeit";

        // Инициализация хранилища
        KeyStore store = KeyStore.getInstance("PKCS#12", "SC");
        store.load(null, null);

        // Чтение секретного ключа PKCS#8
        KeyFactory keyFac = KeyFactory.getInstance("PKCS#8", "SC");
        byte[] encoded = fread(root + FILE_SEPARATOR + "Keys" + FILE_SEPARATOR + "00000001.key");
        KeySpec privkeySpec = new PKCS8EncodedKeySpec(encoded);
        PrivateKey priv = keyFac.generatePrivate(privkeySpec);

        // Чтение сертификата УЦ
        CertificateFactory cf = CertificateFactory.getInstance("X.509", "SC");
        FileInputStream in = new FileInputStream(certRoot + FILE_SEPARATOR + "certca_1902.pem"); // CA 7
        X509Certificate cacert = (X509Certificate) cf.generateCertificate(in);
        in.close();

        // Чтение собственного сертификата
        in = new FileInputStream(clientCertRoot + FILE_SEPARATOR + "cert_9342.pem"); // CA 7
        X509Certificate cert = (X509Certificate) cf.generateCertificate(in);
        in.close();

        // Формирование цепочки сертификатов
        Certificate[] chain = new Certificate[2];
        chain[0] = cert;
        chain[1] = cacert;

        // Помещение в хранилище секретного ключа с цепочкой сертификатов
        store.setKeyEntry("Test key", priv, passwd.toCharArray(), chain);
        // Помещение в хранилище сертификата УЦ
        store.setCertificateEntry("Test CA", cacert);

        // Запись хранилища
        FileOutputStream out = new FileOutputStream(new File(root + FILE_SEPARATOR + "store.pfx"));
        store.store(out, passwd.toCharArray());
        out.close();

Пример конвертирования хранилища из формата PKCS#12 в JKS с использованием keytool

$keytool -importkeystore -srcstoretype pkcs12 -deststoretype JKS -srckeystore store.pfx -destkeystore test.jks -srcprovidername SC
  • Формирование подписи

После создания хранилища можно приступать к формированию подписи, это можно сделать с помощью следующего кода:

public static byte[] signPkcs7SignalCom(String pkcs12Store, String pass, String alias, 
                                        String keyPassword, byte[] data, SecureRandom random) throws SignatureException {
        try {
            FileInputStream in = null;
            ByteArrayInputStream inp = null;
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            OutputStream sigOut = null;
            try{
                in = new FileInputStream(KEYSTORE_PATH + pkcs12Store);
                KeyStore keyStore = getKeystore(pkcs12Store, keyPassword, "PKCS12", "SC", random);
                keyStore.load(in, pass.toCharArray());
                List certs = new ArrayList();
                X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);
                //указываем, каким сертификатом будем подписывать
                certs.add(cert);
                PrivateKey priv = (PrivateKey) keyStore.getKey(alias, pass.toCharArray());

                SignedDataGenerator generator = new SignedDataGenerator(out);
                // экземпляр объекта SecureRandom должен быть предварительно проинициализирован!
                generator.addSigner(new Signer(priv, cert, random));
                generator.addCertificatesAndCRLs(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs)));
                //для создания отсоединенной подписи, т.е такой, которая не включает в себя данные, ставим true
                generator.setDetached(true);
                sigOut = generator.open();
                inp = new ByteArrayInputStream(data);
                byte[] buf = new byte[1024];
                int len;
                while ((len = inp.read(buf)) > 0) {
                    sigOut.write(buf, 0, len);
                }
                generator.close();
                byte[] signedData = out.toByteArray();
                // для тестовых целей можно провалидировать подпись, но в общем случае это не нужно
                //boolean res = verifyPkcs7Sign(signedData, data);
                return signedData;

            }finally{
                if(in != null)
                    in.close();
                if(inp != null)
                    inp.close();
                if(out!= null)
                    out.close();
                if(sigOut != null)
                    sigOut.close();
            }

        } catch (IOException e) {
            throw new SignatureException("Signing error", e);
        } catch (GeneralSecurityException e) {
            throw new SignatureException("Signing error", e);
        }catch (ru.signalcom.crypto.cms.CMSException e) {
            throw new SignatureException("Signing error", e);
        }
    }

Необычным тут является использование объекта SecureRandom, все остальное в принципе похоже на подписи с помощью остальных криптопровайдеров. SecureRandom нужно предварительно проинициализовать, используя следующий код:

        SecureRandom random = null;
        try {
            random = SecureRandom.getInstance("GOST28147PRNG", "SC");
            random.setSeed(new String(privateKeyDirectory + ";NonInteractive;Password=" + pass).getBytes());
        } catch (Exception e) {
            throw new SignatureException("Cannot init class secure random", e);
        }

privateKeyDirectory - путь до директории с файлами kek.opq masks.db3 mk.db3 rand.opq. причем для файла rand.opq должны быть выдано право на запись.

  • Валидация подписи

Для валидации подписи необходим сертификат, которым была осуществлена подпись и, если подпись была не присоединенной, исходные данные, которые были подписаны. В простейшем случае проверка подписи осуществляется так (этот код только проверяет, что подпись была осуществлена данным сертификатом, цепочка сертификатов не валидируется):

public static boolean verifyPkcs7SignatureSignalCom(String certKeystore, String keystorePass, 
                                                    String alias, byte[] signed, byte[] data) throws Exception {
        //List certStores = new ArrayList();

        FileInputStream inp = new FileInputStream(KEYSTORE_PATH + certKeystore);
        KeyStore store = KeyStore.getInstance("PKCS12", "SC");
        store.load(inp, keystorePass.toCharArray());
        inp.close();
        // получаем сертификат, которым производилась подпись
        X509Certificate cert = (X509Certificate) store.getCertificate(alias);
        if(cert ==null){
            throw new SignatureException("Certificate not found");
        }
        InputStream in = new ByteArrayInputStream(signed);
        ContentInfoParser cinfoParser = ContentInfoParser.getInstance(in);
        if (!(cinfoParser instanceof SignedDataParser)) {
            throw new RuntimeException("SignedData expected here");
        }
        SignedDataParser parser = (SignedDataParser) cinfoParser;
        InputStream content = parser.getContent();
        if (content == null) {
            // отсоединённая подпись
            if (data == null) {
                throw new RuntimeException("signature detached, please provide data to compare with");
            }
            parser.setContent(new ByteArrayInputStream(data));
        }
        parser.process();
        // почему-то не работает эта часть кода, хотя в документации сертификат получаелся именно таким способом
        //certStores.add(parser.getCertificatesAndCRLs());


        try {
            Collection signerInfos = parser.getSignerInfos();
            Iterator it = signerInfos.iterator();
            while (it.hasNext()) {
                SignerInfo signerInfo = it.next();
                // собственно валидация
                boolean res = signerInfo.verify(cert);
                return res;
            }
        } finally {
            parser.close();
            in.close();
        }

        return false;
    }

пятница, 2 марта 2012 г.

Настройка axis клиента на работу с несколькими хостами по протоколу https c клиентскими сертификатами

Итак, пока есть время, быстренько опишу этот опыт.

Axis, использует системные настройки для осуществления соединения по https, т.е. теоретически можно указать системные свойства

System.setProperty("javax.net.ssl.keyStore", "securirty/cacerts");
System.setProperty("javax.net.ssl.keyStorePassword", "");
System.setProperty("javax.net.ssl.trustStore", "security/cacerts");

и клиент будет их использовать, что вполне допустимо, если нужно соединение только с одним хостом. Но что делать, если нужно коннектиться к нескольким хостам, к каждому с разными сертификатами, причем эти сертификаты находятся в разных хранилищах и имеют разные пароли? Вариантов решения проблемы два: первый - использовать jax-ws для генерации клиента, и дальше идет черная магия с конфигурацией, либо использовать свою SSLConnectionFactory, но завязываться на имя хоста. Какой их этих путей лучше, не знаю, я выбрал решение с созданием SSLConnectionFactory, как более простое. идея и часть кода взята отсюда

Итак, создаем стандартных axis клиентов по wsdl файлам, для каждого хоста получатся свои классы клиента. Далее, перед вызовом методов веб сервиса необходимо установить свойство

AxisProperties.setProperty("axis.socketSecureFactory", "org.yourcompany.packagename.CustomSSLSocketFactory");

Содержимое файла CustomSSLSocketFactory.java примерно следующее:

package org.yourcompany.packagename;

import org.apache.axis.components.net.BooleanHolder;
import org.apache.axis.components.net.JSSESocketFactory;
import org.apache.axis.components.net.SecureSocketFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Hashtable;

/**
 * Custom SSL socket factory to use our integrated keystore.
 * 

* Based loosely on org.apache.axis.components.net.SunJSSESocketFactory */ public class CustomSSLSocketFactory extends JSSESocketFactory implements SecureSocketFactory { private static final String FILE_SEPARATOR = System.getProperty("file.separator"); private static final String KEYSTORE_PATH = System.getProperty("java.home") + FILE_SEPARATOR + "lib" + FILE_SEPARATOR + "security" + FILE_SEPARATOR; public CustomSSLSocketFactory(Hashtable attributes) { super(attributes); } @Override public Socket create(String host, int port, StringBuffer otherHeaders, BooleanHolder useFullURL) throws Exception { sslFactory = createSSlSocketFactory(host); return super.create(host, port, otherHeaders, useFullURL); } private SSLSocketFactory createSSlSocketFactory(String host) throws Exception { String keystorePass = null; String keystorePath = null; if (host.equals("google.com")){ keystorePass = "passforgoogle"; keystorePath = "googlestore.jks"; } else if (host.equals("yandex.com")) { keystorePass = "passforyandex"; keystorePath = "yandexstore.jks"; } else if (host.equals("yahoo.com")) { keystorePass = "passforyahoo"; keystorePath = "yahoostore.jks"; } if (keystorePass == null || keystorePath == null) { System.out.println("unknown host "+ host +", cannot create socket factory"); return null; } char[] keystorepass = keystorePass.toCharArray(); FileInputStream is = new FileInputStream(KEYSTORE_PATH + keystorePath); // create required keystores and their corresponding manager objects KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(is, keystorepass); is.close(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keystorepass); SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(kmf.getKeyManagers(), null, null); return sslContext.getSocketFactory(); } }

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

Следующей на очереди будет описание работы с библиотекой шифрования Signal COM.

Генерация запроса на сертификат и загрузка сертификата в JKS

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

Сразу оговорюсь, что речь идет о выпуске полноценного сертификата, а не самоподписанного, по самоподписанным много информации есть в сети. Генерация запроса на выпуск сертификата невозможна без приватного ключа, обычно запрос на сертификат и приватный ключ генерируются совместно, одной командой.

 
$openssl req -out request.csr -new -newkey rsa:2048 -utf8 -keyout private.key

Доментация на команду openssl req
подробнее относительно опций:
  • -utf8 означает, что аттрибуты будут храниться как utf8 строки, что необходимо, если используются русские буквы, иначе в запросе получите кракозябры.
  • -new генерирует новый запрос на сертификат, с запросом всех полей.
  • -newkey rsa:2048 генерируется новый RSA ключ длиной 2048 бит.
  • запрос записывается в файл request.csr, приватный ключ сохраняется в файл private.key

Полученный запрос (request.csr) отравляется в центр сертификации (Certificate Authority, CA), который через некоторое время присылает сертификат. Обычно сертификат присылается на email, указанным при генерации запроса на сертификат. Ни в коем случае не нужно отправлять приватный ключ в центр сертификации, если вы это сделаете, ключ будет считаться скомпрометированным и придется повторять процесс заново.
  • Импорт сертификата в JKS хранилище.

Для успешного импорта нужно следующее: приватный ключ, личный (клиентский) сертификат, и сертификат центра сертификации. Обычно приватный ключ имеет расширение pem, key или der и слово private в названии файла. Если ключ в формате PEM, то проверить, что это приватный ключ, можно в любом текстовом редакторе. Приватный ключ должен начинаться со строк -----BEGIN ENCRYPTED PRIVATE KEY----- или -----BEGIN PRIVATE KEY-----
Клиентский сертификат обычно имеет расширение pem, der или crt. Сертификат центра сертификации имеет расширение pem, der или crt и обычно содержит в имени файла слово CA.
Просмотреть содержимое сертификата можно командой
$keytool -printcert -v -file cert.pem
Существуют определенные сложности с импортом только приватного ключа в jks хранилище хранилище посредством keytool, поэтому импорт производится в два этапа, на первом создается хранилище в формате PKCS#12, в которое помещаются приватный ключ и цепочка сертификатов, и потом это хранилище конвертируется в формат jks. При задании пароля на хранилище рекомендуется указать тот же пароль, что и и на приватный ключ.

$openssl pkcs12 -export -out store.pfx -inkey private.key -in clientcert.pem -certfile CAcert.pem
$keytool -importkeystore -destkeystore store.jks -srckeystore store.pfx -srcstoretype PKCS12

Доментация на команду openssl pkcs12
После конвертации можно поменять пользовательское имя (alias) у клиентского сертификата, по умолчанию он будет иметь имя "1".
Посмотреть, что же получилось, можно приведенной ниже командой.
 
$keytool -v -list -keystore store.jks
Альтернативным способом проверки будет загрузка полученного на предыдущем шаге файла store.pfx в IE и открытие в нем сайта, с которым необходимо осуществлять защищенное соединение.
Обычно требуется также добавить сертификат центра сертификации в cacerts, если сертификат сервера, на который производится соединение, выдан тем же CA (а обычно это так), сделать это можно командой

 
$keytool -import -trustcacerts -alias write_alias_here -file CAcert.pem -keystore cacerts

Полученное хранилище можно использовать для организации защищенного соединения с удаленным хостом.