вторник, 18 декабря 2012 г.
hibernate-configuration-3.0.dtd
Заголовок файла 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 и определяемые на этапе сборки параметры проекта
После непродолжительного гугления родилась такая схема:
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}3. Теперь самая хитрая часть. Поскольку свойства maven проекта по сути синглетоны (как и у ant), то они сохраняют переданное при инициализации значение. Т.е если вызывать maven с параметрами командной строки -Djdbc.username=test, а в pom.xml определить <jdbc.username>xxx</jdbc.username> то параметр jdbc.username будет иметь значение test.
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
4. Создадим фильтр, чтобы не хранить свойства в pom.xml. Для этого используем файл, созданный на шаге 1.
<filters>Теперь по умолчанию наши свойства проекта будут грузиться из dev.config, но их можно будет переопределить через параметры командной строки.
<filter>src/main/resources/dev.properties</filter>
</filters>
5. Последний штрих - фильтрация ресурсов. Для этого версия maven-resources-plugin должна быть не меньше 2.3
<resources>6. Настраиваем Jenkins, подставляя нужные значения в поле MAVEN_OPTS (build->advanced->MAVEN_OPTS)
<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>
полезные ссылки: Пример фильтрации ресурсов в maven
четверг, 1 ноября 2012 г.
Использование Apache POI для разбора документов Word
Итак, предположим, есть большой 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
Использовать сервисы геолокации оказалось не просто, а очень просто. Для доступа к функциям не нужно генерировать никаких ключей, просто формируем запрос, дергаем URL сервиса по GET, конвертируем вернувшийся JSON в java объекты и сохраняем результат в базе.
Исходные данные примерно таковы: несколько тысяч адресов примерно следующего формата: Область, Улица, Строение или Улица, Строение и отдельно от них Город. Все адреса находятся в России, что несколько сужает область поиска. Адреса набирались вручную и давно, и могут содержать опечатки, описки и устаревшие сведения.
Итак, первым пробуем Google geocoding API.
Плюсы, которые я вначале не посчитал за плюсы, а понял, что это плюсы, только после того, как воспользовался аналогичным сервисом от Яндекса:
- Все просто и удобно. Можно ограничить область поиска, можно выбрать язык результатов поиска. Не избыточный формат возвращаемых данных.
- Правильный поиск с приемлемой точностью. Можно рассчитывать на то, что если нашелся только один адрес, это будет корректный адрес. Поиск происходит последовательно, т.е вначале ищется населенный пункт, потом улица, потом дом. Следовательно, если населенный пункт не найден, то дальше поиск не осуществляется. Если не найдена улица или дом, поиск вернет координаты населенного пункта или центра улицы соответственно, указав найденную точность в результатах.
- Не пытается исправить ошибки в адресе, если они есть. Другими словами, не пытается подсунуть "вероятно правильный" результат.
- Возвращает почтовый индекс, если адрес найден с точностью до дома.
- Не всегда достаточно полная база, особенно по маленьким городам, что приводит к низкой точности координат (часто до города).
- Формат возвращаемого результата не соответствует общепринятому в России, т.е. результат возвращается в формате "Советская ул., 10, Тюмень, Тюменская область, Россия, 625003". Кончено результат также возвращается разбитый на части по административным единицам, да и этот в принципе не сложно развернуть, но все же неудобно.
- Иногда возвращается адрес конкретного объекта, например: "Сальское Медицинское Училище, ГОУ, Кирова ул., 17, Сальск, Ростовская область, Россия, 347630" Мне в принципе не нужно знать адрес учреждения, которое находится по данному адресу.
- Ограничение на частоту выполнения запросов. Есть ограничение на количество запросов за день (2,5 тыс) и некое ограничение на частоту запросов, из-за чего пришлось получать адреса очень медленно и с задержками.
Подумав немного, я решил уточнить результаты с помощью Яндекса. Ведь Яндекс находится в России, значит, теоретически, результаты должны быть лучше.
Итак, Yandex geocode API.
Плюсы:
- Находит адрес с точностью до дома даже в маленьких городах.
- Иногда правильно исправляет опечатки.
- Общепринятый формат вывода результата.
- Неправильный, избыточный поиск. Назовем его "боязнь ничего не найти". Вернуть любой более-менее похожий результат. Иногда это срабатывает, например, уже упоминавшийся населенный пункт Кольчугин во Владимирской области был правильно исправлен на Кольчугино, но иногда приводит к тому, что возвращается просто неверный результат, совершенно в другом городе. Например, "Смоленская обл., Рудня, ул. Киреева, 66" превращается в "Россия, Смоленская область, Смоленск, улица Кирова".
- При указании точного адреса может вернуться более одного результата. Чтобы не ходить далеко, возьмем пример, приведенный на сайте самого Яндекса. "ул. Тверская, дом 7". Данный запрос вернет 5 результатов. Помимо ожидаемой Тверской улицы, сервис вернет также 4 Тверские-Ямские улицы. При этом у всех (кроме одной) будет точность до дома. Какой логикой руководствовались создатели сервиса, мне непонятно. Убедиться своими глазами.
- Избыточность в описании результатов. Может быть создатели сервиса от Яндекса считают, что это придает солидности, мне же кажется, что это просто добавляет ненужной работы и программисту, и сетевому оборудованию. Количество различных java объектов, необходимых для разбора ответа от Яндекса в 2 раза больше, чем для Гугла.
Код для работы с сервисом Яндекса:
Использование Spring для работы со встроенным HornetQ сервером
Необходимо подключить к проекту 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 никак не регламентирует возможность доставки сообщений с определенной задержкой, тем не менее, поскольку это весьма полезная возможность, многие производители включают ее в реализацию.
Для 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 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(); Listregistry = 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Документация на класс Evalparams = new HashMap (); params.put("field1", "10"); params.put("field2", "11"); Boolean res = (Boolean) Eval.me("param", params, "param.field1 == param.field2");
пятница, 15 июня 2012 г.
Использование OpenSource крипто библиотек для работы с ГОСТовыми сертификатами CryptoPro
Если коротко, то да.
Для проверки использовалась последняя на данный момент версия 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 входной файл
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:
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
Для создания подписи необходим приватный ключ и сертификат. Рассмотрим гипотетическую ситуацию, когда приватный ключ прислан по почте, а не сгенерирован самостоятельно - порочная, но широко распространенная практика.
Предположим, что файл клиентского сертификата называется 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()); Listcerts = 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 { //ListcertStores = 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, 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
Полученное хранилище можно использовать для организации защищенного соединения с удаленным хостом.