среда, 8 апреля 2015 г.

Впечатления о часах Garmin Fenix 2


Скоро будет 8 месяцев, как я пользуюсь часам Garmin Fenix 2. Стоит сразу сказать, что на текущий момент активно продается следующее поколение Garmin Fenix 3. Данные часы расчитаны на любителя активного отдыха и на спортсмена. Мне причислить себя к обоим категориям очень сложно. Да, когда тепло, я люблю пробежаться 5 км. Очень люблю ездить на работу на велосипеде.

Велосипед

Расскажу о работе с устройством на велосипеде. Велосипед относится к разряду складных велосипедов и не претендует на спортивные показатели. По конструктивным особенностям, на этот велосипед сложно поставить спаренный датчик кадиенса и скорости. В прошлом году фирма Garmin выпустила комплект инерционных датчиков для велосипеда. Датчик кадиенса крепится к шатуну. Датчик скорости крепится к втулке заднего колеса или толстой пластиковой спице колеса. Часы крепятся на руль, на держателе для Garmin часов Forerunner и Fenix. Это мягкая накладка из полиуритана.

Перед началом старта надо немного разогнаться. Таким нехитрым способом датчики просыпаются и быстрее распознаются часами. При медленной езде можно проехать не одну сотню метров, перед тем, как часы распознают датчик скорости. Датчик кадиенса расположен чуточку ближе и распознается быстрее. Наверное, алгоритм просыпания этого датчика расчитан на гораздо меньшую угловую скорость. Если спешить совсем не хочется, можно пропустить распознавание датчика скорости и приступить к определению спутников. Часы всё равно не забудут про датчик скорости и подключат его во время движения. С датчиками кадиенса и скорости всё понятно - они любят движение. Распознавание спутников, наоборот, происходит быстрее, если велосипед не движется. В принципе, можно поступить двумя способами. Перед стартом включить режим спорта "Велосипед" чуть чуть загодя. Скажем, так, включить и постоять возле велосипеда с пол минуты. Потом сесть и поехать. Тогда, после распознавания датчиков кадиенса и скорости, часы быстро находят спутники. Мое мнение, что включение спутников идет сразу, а сигнализация на экране идет после подключения велосипедных датчиков. Есть второй способ. Не дожидаясь синхронизации со спутниками двигаться на велосипеде. Это отнимет какое-то время и можно успеть проехать еще несколько сот метров. Если нажать на кнопку "стрелка вниз", часы перейдут в режим движения. Тут то и видно самое интересное и ужасное. Время движения идет. Скорость вращения педалей идет. А вот скорость движения равна нулю. Пока спутники не подключаться, скорость рана нулю. И не важно, что датчик скорости уже подключен. Примерно через 600 - 700 метров, после синхронизации со спутниками, часы покажут надпись, что они определили размер колеса велосипеда. Но этот показатель ни куда не сохраняется. Если нажать красную кнопку start/stop и потом нажать resume (продолжить), то часы радостно выдадут ту же надпись, что они определили размер колеса.

В часах есть датчик температуры. Только вот показания датчика стабилизируются с температурой окружающей среды, примерно, через 10 минут пути. Если велосипед стоял дома, и часы показывали 24-26 градусов, а на улице 0 градусов, то график температуры будет показывать плавное снижение в течении 8-10 минут. Внешний датчик температуры остывает чуточку быстрее. Порядка 5 минут. Единственное неудобство в часах, это глубоко в меню спрятано управление внешним датчиком температуры. Я не так часто беру его с сабой на велосипеде. Чаще использую его во время бега.

Бег

Если уж на велосипеде я не спринтер, то и в беге я не КМС. Бег для меня, это для тонуса и для дыхательной системы. Обычно, к пульсометрам в комплекте идет датчик для бега FootPod. С этими часами данный датчик сопрягается, но не обязателен. Его вполне заменяет внутренний инерционный датчик. Такой датчик стоит в часах Garmin Swim, распознает стиль плавания и количество гребков. Этот же датчик распознает движение рук во время бега. Тем самым ведется учет количества шагов. Если во время бега или ходьбы рукой вытирать пот со лба или разглядывать показания циферблата часов, показания количества шагов сбивается. Пройденное расстояние контролируется с помощью спутников.

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

Погода

Вот тут часы Garmin Fenix 2 показывают себя очень хорошо. Тут и температура внутреннего датчика и температура внешнего датчика. И график атмосферного давления за последние 48 часов. Причем, за давлением часы следят постоянно. Тут и альтиметр (высота над уровнем моря). Правда, во время перепада давления, альтиметр может показать средне потолочные значения от -47 метров, до +68 метров. Но это всё равно гораздо точнее, чем пляшущие цифры высоты на смартфоне с включенной навигацией. При включенной синхронизации со спутниками, высота над уровнем моря стабилизируется. Да, бывают перепады высоты +/- 3 метра. Но это уже не 40-60 метров.
Есть очень полезная функция - оповещение о смене погоды при резкой смене давления.

Синхронизация

До появления прошивки 4.40, я постоянно испытывал дискомфорт и ощущение, что меня производитель нагнул на деньги. Синхронизация по Bluetooth работала отвратительно. Постоянно рвалось соединение. На новой прошивке связь со сматрфоном Android KitKat работает.

Часы обладают интересной функциональностью - оповещение о входящих вызовах и SMS сообщениях. В реальной ситуации, эта функция для меня бесполезна. Надо выбирать одно из двух - либо связь по Bluetooth, либо внешние датчика ANT+. Датчики мне интересней, нежели ромбики вместо русских букв SMS сообщения.

Уведомления

В часах есть своя пищалка для будильника, но в условиях города, мощности писка не достаточно. Зато есть другая система - вибрация. Вот тут пожаловаться я не могу. Часы, закрепленные на полиуритановом держателе, очень сильно передают жужжание по корпусу всего велосипеда. В этом я убедился, когда включил режим автопаузы. Автопауза - это остановка счетчика времени, когда стоишь на светофоре и ждешь зеленого светофора. Иногда не держусь руками за руль. Всё равно, ощущается вибрация велосипеда. Да, если не быть готовым к такому поведению велосипеда, можно и не заметить. Если же знаешь о сигналах, замечаешь. Уведомления идут каждый пройденный километр во время бега и каждые пять километров во время езды на велосипеде.

Полезные настройки

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

Сравнение с другими системами

До появления Garmin Fenix 2, я пользовался программой Endomondo на смартфоне. Вот, что мне нравилось, так это подсказки по отставанию или опережению предыдущих показателей на дистанции. Вот этой функции мне действительно не хватает в Fenix. Пока было тепло, ездил сразу с Fenix и Endomondo. Потом всё сливал на сайты и смотрю. По холоду, что то желание лазить в карман, за телефоном, не возникало. Начало теплеть, задумался опять об использовании Endomondo.

Что удобно в Endomondo, это показание собственных рекордов и общей статистики за неделю и месяц. В Garmin Connect такого нет.

Непонятности

Вот, чего я действительно не понимаю, так это количество неких калорий. Еду каждый день по одному маршруту на работу и домой. Если еду медленно, расход калорий больше. Еду так, что весь в мыле, а калорий меньше. Бег показывает расход калорий на километр пути равный где-то 5 километрам пути на велосипеде. Но после велосипеда, на мне вся одежда мокрая, а после бега, как в той юморестке


Жена: - Ты где бы?
Муж: - Бегал.
Жена: - Но твоя футболка сухая и не пахнет.
Муж: - Ты носки нюхай.
и так далее.

воскресенье, 11 мая 2014 г.

Первые впечатления о Garmin Swim


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

Прожив кучу лет, решил себе сделать подарок. Подарил себе часы для плавания в бассейне - Garmin Swim.

Когда работаешь, совсем не задумываешься о количестве кругов. Тем более, о количестве гребков и самом быстром круге. Максимум, на что хватает времени - взглянуть на большие часы бассейна на очередном круге.

Да, теперь, через много лет, скорость не та и сил нет уже после 3-го круга (300 метров). Помню, что в детстве, вышел из бассейна, сел в трамвай и проспал свою остановку.

Где же ты, мой тренер? Да вот же он, на левой руке. Единственное, чего не хватает этому тренеру - сказать мне, что я лентяй и он меня не пустит в воду на следующей тренировке.

 

Во время заплыва ничего не видно, кроме черного ремешка на запястье и блестящего зеркального кружка на ремешке. Руки летают в воде, голова идет то вверх, то вниз. Всё встает на свои места после финиша и нажатия паузы или стопа. В бассейне нет времени на любимый смартфон для анализа каждого заплыва. Возвращаюсь домой, подключаю ANT+ датчик к смартфону и отправляю данные в Garmin Connect. В тихой, домашней обстановке изучаю свои результаты на экране ноутбука.

Загрузка данных из часов на компьютер

Да, производитель оборудования позаботился о передаче данных на собственный сайт. Это решение работает только под Microsoft IE (Windows). Должно работать под MacOS. Я же любитель Linux. Приходится искать альтернативы.

На текущий момент мне удалось опробовать один удачный проект под названием Garmin-Forerunner-610-Extractor. Программа загружает результаты тренировки, fit файлы, на ноутбук. Надо будет заняться поиском локальной системы анализа полученных данных.

вторник, 1 октября 2013 г.

Подписывание XML документов

Подписывание XML документа

В предыдущей статье, Apache CXF и ЭЦП для SOAP сообщений СМЭВ, я рассказал о том, как можно средствами Apache CXF подписывать SOAP сообщение. Подпись появляется в заголовке SOAP сообщения.

Интересный вопрос появляется, когда передаваемый документ должен содержать подпись. Если рассматривать в рамках системы СМЭВ, передаваемый SOAP запрос будет содержать две подписи. Первая подпись внутри тела сообщения (envelope->body). Вторая подпись в заголовке сообщения (envelope->header).

Реализация

По своей сути, этот пример является переработанной частью системы подписывания SOAP сообщения, реализованная в WSS4JOutInterceptor. Добавлен самый интересный фрагмент - поиск и удаление старых подписей.

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.xml.sign;

public interface SoapClient {
   public static final String SIGN_PROVIDER = "..."; // название провайдера JCA ГОСТ 
   public static final String XML_PROVIDER = "..."; // название провайдера JSR-105 XMLDSIGN
   public static final String CRYPTO_GOST_DIGEST = "http://www.w3.org/2001/04/xmldsig-more#gostr3411";
   public static final String CRYPTO_GOST_SIGN = "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411";
}
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.xml.sign;

import java.security.*;
import java.util.*;
import javax.xml.crypto.dsig.*;
import org.apache.ws.security.components.crypto.CryptoType;
import org.w3c.dom.*;

/**
 * Система подписывания XML документа
 * 
 * @author Aleksey Sushko
 */
public class Signer {
    private Properties cryptoSign;
    private String signKeyAlias;
    private XMLSignatureFactory signatureFactory;

    public void createSigner(String signKeyAlias, Properties properties) throws NoSuchProviderException {
        this.signKeyAlias = signKeyAlias;
        cryptoSign = new Properties();
  
        for(Object propKey : properties.keySet()) {
            String key = propKey.toString();
            String val = properties.getProperty(key);
   
            if(val != null) {
                if(key.startsWith("cryptoSign.") && key.length() > 11) {
                    cryptoSign.setProperty(key.substring(11), val);
                }
            }
        }
        signatureFactory = XMLSignatureFactory.getInstance("DOM", XML_PROVIDER);
    }

    public void sign(Document document) throws Exception {
        LocalMerlin crypto = new LocalMerlin(cryptoSign); 
        PrivateKey privateKey = crypto.getPrivateKey(signKeyAlias, null);
        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
        cryptoType.setAlias(signKeyAlias);
        X509Certificate[] certs = crypto.getX509Certificates(cryptoType);
  
        // ищем старые разделы ЭЦП
        NodeList nl = document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if(nl.getLength() != 0) {
            // Убираем все старые подписи
            for(int i = nl.getLength() - 1; i >= 0; i--) {
                Node item = nl.item(i);
                Node parent = item.getParentNode();
                parent.removeChild(item);
            }
        }
  
        // приведение документа к каноническому виду
        CanonicalizationMethod c14nMethod = signatureFactory.newCanonicalizationMethod(
            CanonicalizationMethod.EXCLUSIVE, 
            (C14NMethodParameterSpec) null);
        // не подписывать другие подписи 
        Transform transform_1 = signatureFactory.newTransform(
            Transform.ENVELOPED, 
            (TransformParameterSpec) null);
        Transform transform_2 = signatureFactory.newTransform(
            CanonicalizationMethod.EXCLUSIVE, 
            (TransformParameterSpec) null);
        // аглоритм расчета хеш функции (digest GOST3411)
        DigestMethod digestMethod = signatureFactory.newDigestMethod(
            SoapClient.CRYPTO_GOST_DIGEST, 
            null);
        // алгоритм подписи (GOST 3410-2001)
        SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(
            SoapClient.CRYPTO_GOST_SIGN, 
            null);
        // указываем подписываемые данные
        Reference ref = signatureFactory.newReference("", 
            digestMethod,
            Arrays.asList(transform_1, transform_2),
            null, null);
        // информационный блок подписи
        SignedInfo signedInfo = signatureFactory.newSignedInfo(
            c14nMethod,
            signatureMethod,
            Collections.singletonList(ref));

        // сведения о ключе, которым подписываются данные
        KeyInfoFactory keyInfoFactory = signatureFactory.getKeyInfoFactory();
        X509Data x509Data = keyInfoFactory.newX509Data(Arrays.asList(certs));
        KeyInfo keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(x509Data));

        // создаем блок подписи
        XMLSignature signature = signatureFactory.newXMLSignature(signedInfo, keyInfo);
        DOMSignContext domSignContext = new DOMSignContext(privateKey, document.getDocumentElement());
  
        // устанавливаем нужного поставщика подписей, который добыл закрытый ключ для подписи
        domSignContext.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", 
            Security.getProvider(SoapClient.SIGN_PROVIDER));
  
        // подписываем документ
        signature.sign(domSignContext);
    }
}

Применение

Надо подумать о том, как этим функционалом правильно пользоваться. По сути, подписать XML документ может одна система, а передать SOAP сообщение другая система. Это означает, что подписи могут быть получены на разных ключах.

Рассматривая структуру XML документов системы СМЭВ, мы видим вставку блока Signature во все документы. Это значит, что взяв за основу JAXB, и преобразовав подписанный XML документ, мы получим набор Java объектов с информацией о подписи. Выполнив обратное JAXB преобразование, мы получим прежний подписанный XML документ. Указанные методы преобразования CanonicalizationMethod и Transform дадут те же самые данные, что были у исходного XML документа.

Получается, что мы можем заполнить POJO объект данными, преобразовать в XML представление, прогнать через подписание, свернуть в новый POJO объект. Полученный объект передать в CXF SopaClient. Apache CXF вернет POJO объект в XML представление, вставит его в тело SOAP запроса, подпишет сообщение и отправит на сервер.

Остается дело за малым - нужно правильно приготовить WSS4JInInterceptor для проверки подписей принимаемых ответов. Реализовав обе части, WSS4JOutInterceptor и WSS4JInInterceptor, можно смело браться за реализацию собственной СМЭВ службы.

На затравку

Я начинал рассказывал о том, как использовать JCA провайдера КриптоПро JCP для работы с Apache CXF. Скажу так, что взяв исходный материала OpenJava JDK, SunMSCAPI провайдера, можно сделать свой мост через MS Crypto API к КриптоПро CSP. Надо знать самое главное - в Oracle JCA провайдере можно легко реализовать расчет хеш функций, подпись, проверку подписи, контейнер сертификатов и ключей. Нельзя сделать только шифрование и расшифровку данных. Для этого надо получить сертификат Oracle для подписывания библиотек криптографического провайдера.

воскресенье, 3 марта 2013 г.

Apache CXF и ЭЦП для SOAP сообщений СМЭВ

Apache CXF и ЭЦП для SOAP сообщений СМЭВ

Если мы любители Java и столкнулись с СМЭВ, то первым делом задумываемся о WS-Security. Это электронная цифровая подпись в передаваемых SOAP сообщениях.

И вот, мы открываем поисковый сервер, набираем запрос и попадаем в блог компании КриптоПро. Там описывается методика формирования подписи в SOAP сообщении. Я, как только увидел этот опус, сразу понял, что тут народ что-то замутил.

Первое, что бросается в глаза, это использование WSS4J, Axis2. Предположим, что в IBM WebSphere, может и сейчас используется стек Axis2. В JBoss AS 7.1.1, Apache Camel, да и в Apache Karaf (OSGi), мы видим другой стек - Apache CXF.

Открыв документацию на IBM сайте, http://www.ibm.com/developerworks/ru/library/j-jws13/index.html , мы видим довольно добротную документацию по подписыванию и шифрованию SOAP сообщений. Взглянув на блог http://www.cryptopro.ru/blog/2012/07/02/podpis-soobshchenii-soap-dlya-smev-s-ispolzovaniem-kriptopro-jcp , я ужаснулся тому, как всё замешано. Тут нам и WSS4J API, и SOAP Message API. А где же прелести CXF с минимальными настройками?
И так приступим к разбору.

Доступ к хранилищу ключей и сертификатов

Настройка WSSj4 заключается в правильном описании файла свойств crypto.properties
org.apache.ws.security.crypto.provider=org.company.soap.impl.ws.security.components.crypto.LocalMerlin
org.apache.ws.security.crypto.merlin.keystore.provider=JCP
org.apache.ws.security.crypto.merlin.keystore.type=HDImageStore

В WSSj4 нам предлагается криптографический компонент Merlin. Я его заменяю на собственную локализацию LocalMerlin. Причина заключается в работе JCP с хранилищем закрытых ключей HDImageStore. По умолчанию, базовый Merlin должен получить доступ к файлу хранилища. Но в JCP, базовое хранилище, это некий каталог файловой системы. Соответственно, Merlin, не откроет хранилище.

Вторая особенность JCP хранилища HDImageStore. Если мы сделаем хранение контейнера ключа без пароля, мы должны передать null в качестве значения пароля. Merlin вместо этого передает массив нулевой длины.

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.ws.security.components.crypto;

import java.io.IOException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.util.Enumeration;
import java.util.Properties;

import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.components.crypto.CredentialException;
import org.apache.ws.security.components.crypto.Merlin;

/**
 * Замена базового провайдера работы с хранилищами сертификатов и ключей.
 * Цель - использование особенностей работы c хранилищем закрытых ключей JCP (HDImageStore).
 *  
 * @author Aleksey Sushko
 */
public class LocalMerlin extends Merlin {

 private static final String HD_IMAGE_STORE = "HDImageStore";
 private static final org.apache.commons.logging.Log LOG = 
   org.apache.commons.logging.LogFactory.getLog(Merlin.class);

 /**
  * Дубликат закрытой функции из базового класса.
  */
 private static String createKeyStoreErrorMessage(KeyStore keystore) throws KeyStoreException {
  Enumeration<String> aliases = keystore.aliases();
  StringBuilder sb = new StringBuilder(keystore.size() * 7);
  boolean firstAlias = true;
  while (aliases.hasMoreElements()) {
   if (!firstAlias) {
    sb.append(", ");
   }
   sb.append(aliases.nextElement());
   firstAlias = false;
  }
  String msg = " in keystore of type [" + keystore.getType()
    + "] from provider [" + keystore.getProvider()
    + "] with size [" + keystore.size() + "] and aliases: {"
    + sb.toString() + "}";
  return msg;
 }
 
 
 public LocalMerlin() {
  super();
 }

 public LocalMerlin(Properties properties, ClassLoader loader)
   throws CredentialException, IOException {
  super(properties, loader);
 }

 public LocalMerlin(Properties properties) throws CredentialException,
   IOException {
  super(properties);
 }

 /**
  * Базовый Merlin не загружает хранилище личных ключей и доверенных сертификатов,
  * если файл не указан.
  * Цель - при отсутствии имени файла, система поднимает хранилище личных ключей.
  */
 @Override
 public void loadProperties(Properties properties, ClassLoader loader)
   throws CredentialException, IOException {
  super.loadProperties(properties, loader);
  
  String provider = properties.getProperty(CRYPTO_KEYSTORE_PROVIDER);
  if (provider != null)
   provider = provider.trim();
  
  if(keystore == null) {
   String passwd = properties.getProperty(KEYSTORE_PASSWORD, "security");
   String type = properties.getProperty(KEYSTORE_TYPE, KeyStore.getDefaultType());
   
   if(passwd != null)
    passwd = passwd.trim();
   if(type != null)
    type = type.trim();
   
   if(HD_IMAGE_STORE.equals(type))
    keystore = load(null, passwd, provider, type);
  }
  
  if(truststore == null) {
   String passwd = properties.getProperty(TRUSTSTORE_PASSWORD, "changeit");
   String type = properties.getProperty(TRUSTSTORE_TYPE, KeyStore.getDefaultType());
   
   if(passwd != null)
    passwd = passwd.trim();
   if(type != null)
    type = type.trim();
   
   if(HD_IMAGE_STORE.equals(type))
    truststore = load(null, passwd, provider, type);
  }
 }

 /**
  * Gets the private key corresponding to the identifier.
  *
  * @param identifier The implementation-specific identifier corresponding to the key
  * @param password The password needed to get the key
  * @return The private key
  */
 @Override
 public PrivateKey getPrivateKey(
   String identifier,
   String password
   ) throws WSSecurityException {
  if (keystore == null) {
   throw new WSSecurityException("The keystore is null");
  }
  try {
   if (identifier == null || !keystore.isKeyEntry(identifier)) {
    String msg = "Cannot find key for alias: [" + identifier + "]";
    String logMsg = createKeyStoreErrorMessage(keystore);
    LOG.error(msg + logMsg);
    throw new WSSecurityException(msg);
   }
   if (password == null && privatePasswordSet) {
    password = properties.getProperty(KEYSTORE_PRIVATE_PASSWORD);
    if (password != null) {
     password = password.trim();
    }
   }
   
   Key keyTmp = loadPrivateKey(identifier, password);
   if (!(keyTmp instanceof PrivateKey)) {
    String msg = "Key is not a private key, alias: [" + identifier + "]";
    String logMsg = createKeyStoreErrorMessage(keystore);
    LOG.error(msg + logMsg);
    throw new WSSecurityException(msg);
   }
   return (PrivateKey) keyTmp;
  } catch (KeyStoreException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  } catch (UnrecoverableKeyException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  } catch (NoSuchAlgorithmException ex) {
   throw new WSSecurityException(
     WSSecurityException.FAILURE, "noPrivateKey", new Object[]{ex.getMessage()}, ex
     );
  }
 }

 /**
  * Получение закрытого ключа из хранилища.
  * Для хранилища HDImageStore пустой пароль передается как null.
  * Базовый вариант передает пустой массив char[]{}. 
  */
 protected Key loadPrivateKey(String identifier, String password)
   throws KeyStoreException, NoSuchAlgorithmException,
   UnrecoverableKeyException {
  
  char[] passwordArray = null;
  
  if(password == null) { 
   String type = keystore.getType();
   if(HD_IMAGE_STORE.equals(type))
    passwordArray = null;
   else
    passwordArray = new char[]{};
  }
  else {
   passwordArray = password.toCharArray();
  }
  
  Key keyTmp = keystore.getKey(identifier, passwordArray);
  
  return keyTmp;
 }
}

Теперь у нашей реализации WSS4J есть доступ к базовому хранилищу личных ключей JCP HDImageStore. Идем дальше.

Инициализация XMLDSIGN

На форуме КриптоПро, и в документации, нам объясняют, что в каталог $JRE_HOME/lib/ext мы установили много библиотек для работы JCP провайдера. В том числе и библиотеку JCPxml.jar.

Теперь, мы самостоятельно должны положить туда же xmldsign.jar, wss4j.jar, xalan.jar, common-loggin.jar и еще кучу библиотек. Но позвольте, в CXF стеке того же JBoss AS 7.1.1 уже есть весь выше перечисленный набор. Да и набор всех библиотек JCP провайдера, нам приходится вкладывать в каталог lib нашего EAR приложения.

Что-то тут не так. Проведем эксперимент, убираем из каталога $JRE_HOME/lib/ext библиотеку JCPxml.jar. Пропишим ее в maven зависимости локального проекта. Туда же пропишем и библиотеку JCP XMLDSigRI.jar

        <dependency>
            <groupId>crypto-pro</groupId>
            <artifactId>JCPxml</artifactId>
            <version>${cryptopro-jcp.version}</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/JCPxml.jar</systemPath>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>crypto-pro</groupId>
            <artifactId>XMLDSigRI</artifactId>
            <version>${cryptopro-jcp.version}</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/lib/XMLDSigRI.jar</systemPath>
            <type>jar</type>
        </dependency>
Запускаем тест и получаем ошибку. Метод JCPXMLDSigInit.isInitialized() не может найти класс исключения org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException. Очень странно видеть это на stanalone application (JUnit test).
if(!JCPXMLDSigInit.isInitialized()){
    JCPXMLDSigInit.init();
}

Делать не чего, надо писать собственную реализацию поднятия XMLDSIGN. Если выполнить рекомендации КриптоПро, то мы можем получить кучу аналогичных ошибок в других местах WSS4J, DOM, SAX, XSLT стека по всем проектам, работающим на боевом сервере приложений.

Поднимаем свою реализацию

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.xml.security;

import java.security.Provider;
import java.security.Security;

import org.apache.ws.security.WSSConfig;
import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.algorithms.SignatureAlgorithm;
import org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException;
import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Constants;

import org.company.soap.impl.xml.security.algorithms.SignatureGostR34102001Gostr3411;
import org.company.soap.impl.xml.security.algorithms.SignatureGostR34102001URN;

/**
 * Инициализация XMLDSIGN.
 * 
 * @author Aleksey Sushko
 *
 */
public class XmlDSignTools {
 public static final String URL_V1_ALGORITHM_DIGEST = Constants.MoreAlgorithmsSpecNS + "gostr3411";
 public static final String URL_V2_ALGORITHM_DIGEST = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr3411";
 public static final String URL_V1_ALGORITHM_SIGNATURE = Constants.MoreAlgorithmsSpecNS + "gostr34102001-gostr3411";
 public static final String URL_V2_ALGORITHM_SIGNATURE = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411";
 public static final String URL_V1_ALGORITHM_ENCRIPTION = Constants.MoreAlgorithmsSpecNS + "gost28147";
 public static final String URL_V2_ALGORITHM_ENCRIPTION = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gost28147";
 
 public static final String JCENAME_ALGORITHM_DIGEST = "GOST3411";
 public static final String JCENAME_ALGORITHM_SIGNATURE = "GOST3411withGOST3410EL";
 public static final String JCENAME_ALGORITHM_ENCRIPTION = "GostJCE/CBC/ISO10126Padding";
 
 public static void init() throws AlgorithmAlreadyRegisteredException, XMLSignatureException, ClassNotFoundException {
  org.apache.xml.security.Init.init();
  
  // CryptoPro MessageDigest
  JCEMapper.Algorithm digest = new JCEMapper.Algorithm("", JCENAME_ALGORITHM_DIGEST, "MessageDigest");
  JCEMapper.register(URL_V1_ALGORITHM_DIGEST, digest);
  JCEMapper.register(URL_V2_ALGORITHM_DIGEST, digest);
  
  // CryptoPro Signature
  JCEMapper.Algorithm signature = new JCEMapper.Algorithm("", JCENAME_ALGORITHM_SIGNATURE, "Signature");
  JCEMapper.register(URL_V1_ALGORITHM_SIGNATURE, signature);
  JCEMapper.register(URL_V2_ALGORITHM_SIGNATURE, signature);
  
  // CryptoPro BlockEncryption
  JCEMapper.Algorithm encryption = new JCEMapper.Algorithm("GOST28147", JCENAME_ALGORITHM_ENCRIPTION, "BlockEncryption", 256);
  JCEMapper.register(URL_V1_ALGORITHM_ENCRIPTION, encryption);
  JCEMapper.register(URL_V2_ALGORITHM_ENCRIPTION, encryption);
  
  SignatureAlgorithm.register(URL_V1_ALGORITHM_SIGNATURE, SignatureGostR34102001Gostr3411.class);
  SignatureAlgorithm.register(URL_V2_ALGORITHM_SIGNATURE, SignatureGostR34102001URN.class);
  
  Provider jceProvider = loadJceProvider();
  WSSConfig.appendJceProvider(jceProvider.getName(), jceProvider);
 }
 
 public static Provider loadJceProvider() {
  Provider jceProvider = Security.getProvider("CryptoProXMLDSig");
  if(jceProvider == null) {
   jceProvider = new ru.CryptoPro.JCPxml.dsig.internal.dom.XMLDSigRI();
   Security.addProvider(jceProvider);
  }
  return jceProvider;
 }
 
 public static void initProvider(String providerName, String className) throws Exception {
  Provider provider = Security.getProvider(providerName);
  if(provider == null) {
   loadProvider(className);
  }
 }
 
 private static void loadProvider(String className) throws Exception {
  ClassLoader loader = Thread.currentThread().getContextClassLoader();
  Class<?> c = loader.loadClass(className);

  java.security.Provider newProvider = (java.security.Provider) c.newInstance();
  java.security.Security.addProvider(newProvider);
 }
}
За основу был взят класс описания DSA подписи - SignatureDSA.
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.xml.security.algorithms;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.AlgorithmParameterSpec;

import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.algorithms.SignatureAlgorithmSpi;
import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Base64;

/**
 * Базовый класс реализации алгоритма ГОСТ ЭЦП.
 * 
 * @author Aleksey Sushko
 */
abstract class SignatureGostR34102001 extends SignatureAlgorithmSpi {

 /** {@link org.apache.commons.logging} logging facility */
 static org.apache.commons.logging.Log log = 
  org.apache.commons.logging.LogFactory.getLog(SignatureGostR34102001.class.getName());

 /** Field algorithm */
 private java.security.Signature _signatureAlgorithm = null;

 /**
  * Constructor SignatureGostR34102001
  *
  * @throws XMLSignatureException
  */
 SignatureGostR34102001() throws XMLSignatureException {
  String algorithmID = JCEMapper.translateURItoJCEID(engineGetURI());
  if (log.isDebugEnabled())
   log.debug("Created SignatureGostr34102001Gostr3411 using " + algorithmID);

  String provider = JCEMapper.getProviderId();
  try {
   if (provider == null) {
    this._signatureAlgorithm = Signature.getInstance(algorithmID);
   } else {
    this._signatureAlgorithm = 
     Signature.getInstance(algorithmID, provider);
   }
  } catch (java.security.NoSuchAlgorithmException ex) {
   Object[] exArgs = { algorithmID, ex.getLocalizedMessage() };
   throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs);
  } catch (java.security.NoSuchProviderException ex) {
   Object[] exArgs = { algorithmID, ex.getLocalizedMessage() };
   throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineSetParameter(AlgorithmParameterSpec params) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.setParameter(params);
  } catch (InvalidAlgorithmParameterException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected boolean engineVerify(byte[] signature) throws XMLSignatureException {
  try {
   if (log.isDebugEnabled())
    log.debug("Called gostr34102001-gostr3411.verify() on " + Base64.encode(signature));

   return this._signatureAlgorithm.verify(signature);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  } 
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitVerify(Key publicKey) throws XMLSignatureException {
  if (!(publicKey instanceof PublicKey)) {
   String supplied = publicKey.getClass().getName();
   String needed = PublicKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException
   ("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   System.out.println("engineInitVerify publicKey is " + publicKey.getClass());
   
   this._signatureAlgorithm.initVerify((PublicKey) publicKey);
  } catch (InvalidKeyException ex) {
   // reinstantiate Signature object to work around bug in JDK
   // see: http://bugs.sun.com/view_bug.do?bug_id=4953555
   Signature sig = this._signatureAlgorithm;
   try {
    this._signatureAlgorithm = Signature.getInstance
    (_signatureAlgorithm.getAlgorithm(), _signatureAlgorithm.getProvider().getName());
   } catch (Exception e) {
    // this shouldn't occur, but if it does, restore previous
    // Signature
    if (log.isDebugEnabled()) {
     log.debug("Exception when reinstantiating Signature:" + e);
    }
    this._signatureAlgorithm = sig;
   }
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected byte[] engineSign() throws XMLSignatureException {
  try {
   byte jcebytes[] = this._signatureAlgorithm.sign();
   
   return jcebytes;
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitSign(Key privateKey, SecureRandom secureRandom)
 throws XMLSignatureException {
  if (!(privateKey instanceof PrivateKey)) {
   String supplied = privateKey.getClass().getName();
   String needed = PrivateKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   this._signatureAlgorithm.initSign((PrivateKey) privateKey,
     secureRandom);
  } catch (InvalidKeyException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineInitSign(Key privateKey) throws XMLSignatureException {
  if (!(privateKey instanceof PrivateKey)) {
   String supplied = privateKey.getClass().getName();
   String needed = PrivateKey.class.getName();
   Object exArgs[] = { supplied, needed };

   throw new XMLSignatureException("algorithms.WrongKeyForThisOperation", exArgs);
  }

  try {
   this._signatureAlgorithm.initSign((PrivateKey) privateKey);
  } catch (InvalidKeyException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte[] input) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(input);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte input) throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(input);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * @inheritDoc
  */
 @Override
 protected void engineUpdate(byte buf[], int offset, int len)
 throws XMLSignatureException {
  try {
   this._signatureAlgorithm.update(buf, offset, len);
  } catch (SignatureException ex) {
   throw new XMLSignatureException("empty", ex);
  }
 }

 /**
  * Method engineGetJCEAlgorithmString
  *
  * @inheritDoc
  */
 @Override
 protected String engineGetJCEAlgorithmString() {
  return this._signatureAlgorithm.getAlgorithm();
 }

 /**
  * Method engineGetJCEProviderName
  *
  * @inheritDoc
  */
 @Override
 protected String engineGetJCEProviderName() {
  return this._signatureAlgorithm.getProvider().getName();
 }

    /**
     * Method engineSetHMACOutputLength
     *
     * @param HMACOutputLength
     * @throws XMLSignatureException
     */
 @Override
    protected void engineSetHMACOutputLength(int HMACOutputLength)
            throws XMLSignatureException {
        throw new XMLSignatureException(
     "algorithms.HMACOutputLengthOnlyForHMAC");
    }

    /**
     * Method engineInitSign
     *
     * @param signingKey
     * @param algorithmParameterSpec
     * @throws XMLSignatureException
     */
 @Override
    protected void engineInitSign(
        Key signingKey, AlgorithmParameterSpec algorithmParameterSpec)
            throws XMLSignatureException {
        throw new XMLSignatureException(
            "algorithms.CannotUseAlgorithmParameterSpecOnDSA");
    }
 
}
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.xml.security.algorithms;

import org.apache.xml.security.signature.XMLSignatureException;
import org.apache.xml.security.utils.Constants;

public class SignatureGostR34102001Gostr3411 extends SignatureGostR34102001 {

 /** Field _URI */
 public static final String _URI = Constants.MoreAlgorithmsSpecNS + "gostr34102001-gostr3411";

 @Override
 protected String engineGetURI() {
  return SignatureGostR34102001Gostr3411._URI;
 }

 /**
  * Constructor SignatureGostr34102001Gostr3411
  *
  * @throws XMLSignatureException
  */
 public SignatureGostR34102001Gostr3411() throws XMLSignatureException {
 }
}
/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.xml.security.algorithms;

import org.apache.xml.security.signature.XMLSignatureException;

public class SignatureGostR34102001URN extends SignatureGostR34102001 {
 /** Field _URI */
 public static final String _URI = "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102001-gostr3411";

 @Override
 protected String engineGetURI() {
  return SignatureGostR34102001URN._URI;
 }

 /**
  * Constructor SignatureGost34102001URN
  *
  * @throws XMLSignatureException
  */
 public SignatureGostR34102001URN() throws XMLSignatureException {
 }
}

Настройка WSS4J

Для старого WSS4J этого кода, должно было хватить. Но на свет вышли версии 1.6.x и в них появился очередной поставщик XML трансформаций. Делать нечего, берем библиотеку JCP XMLDSigRI.jar. Правда, сама КриптоПро, толком не объясняет что это такое. Хотите использовать WSS4J 1.6.x, берите, а не хотите - не будет работать и всё тут.

Постараюсь ввести в курс дела. В старой версии Apache Santuario XMLDSIGN существовал XML файл с описанием разных видов трансформации XML документов, алгоритмов расчета хеш функций (MessageDigest), алгоритмов подписи и шифрования. Была одна неприятная особенность всего этого дела. Выражалась она в том, что Apache Santuario один раз инициализировал себя и не было возможности добавить собственные правила. Например, для того же ГОСТ ЭЦП.

Был только один механизм - установить системную переменную, в которой указать модернизированный xml документ. Если у нас не сервер приложений, а обычное приложение, есть шанс самостоятельно указать xml файл, для инициализации Apache Santuario.

Позвольте, но при чем тут WSS4J, если описание идет о реализации XMLDSIGN? Ответ жутко не интересный. В коде проекта WSS4J можно указать поставщика базовой криптографии, реализующего работу с сертификатами, ключами и алгоритмами. Поставщик XML преобразований зашит намертво - ApacheXMLDSig.

Нет, конечно же, можно как-то обойти инициализацию этого провайдера. Только, внутри статического метода init() класса WSSConfig идет прямой вызов статического метода addXMLDSigRIInternal(), который самым безобразным образом поднимает поставщика ApacheXMLDSig.

Короче, не нужен нам ApacheXMLDSig, будьте любезны, перепишите свою реализацию WSS4J под себя. Моя цель не ломать имеющуюся реализацию сторонних библиотек, а настроить систему под Российские реалии.

И так, мне понадобится найти классы, где идет обращение к поставщику XML трансформаций и заменить на использование того, что дает КриптоПро в библиотеке XMLDSigRI.jar.

Приступим. Находим название класса поставщика от КриптоПро. Тут изобретать ничего не пришлось. Имена классов, до буквы, совпадают с классами из XMLDSIGN. В JUnit тесте поднимаем объект поставщика и запрашиваем у него название.

За подписывание SOAP сообщения, в WSS4J отвечает класс WSSecSignature. Конструкторы данного класса вызывают защищенный метод init(), который, подобно собаке, мертвой хваткой цепляется за поставщика ApacheXMLDSig. Делаем собственную реализацию данного класса, для использования нужного нам поставщика от JCP

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.ws.security.message;

import java.security.Provider;
import java.security.Security;

import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;

import org.apache.ws.security.WSSConfig;
import org.apache.ws.security.message.WSSecSignature;

/**
 * Переопределяем базовый объект WSSecSignature.
 * Цель - подставить DOM Provider "CryptoProXMLDSig" вместо базового "ApacheXMLDSig".
 * @author Aleksey Sushko
 *
 */
public class LocalWSSecSignature extends WSSecSignature {

 public LocalWSSecSignature() {
  super();
  init();
 }

 public LocalWSSecSignature(WSSConfig config) {
  super(config);
  init();
 }

 private void init() {
  Provider jceProvider = loadJceProvider();
  signatureFactory = XMLSignatureFactory.getInstance("DOM", jceProvider);
  keyInfoFactory = KeyInfoFactory.getInstance("DOM", jceProvider);
 }

 private Provider loadJceProvider() {
  Provider jceProvider = Security.getProvider("CryptoProXMLDSig");
  if(jceProvider == null) {
   jceProvider = new ru.CryptoPro.JCPxml.dsig.internal.dom.XMLDSigRI();
   Security.addProvider(jceProvider);
  }
  return jceProvider;
 }
}

Осталось найти, где идет создание базового WSSecSignature и сделать вызов нового LocalWSSecSignature. Этим узким местом является обработчик события подписания данных org.apache.ws.security.action.SignatureAction. Создаем ему собственную альтернативу

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.impl.xml.security.action;


import java.util.List;

import javax.security.auth.callback.CallbackHandler;

import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSEncryptionPart;
import org.apache.ws.security.WSPasswordCallback;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.action.Action;
import org.apache.ws.security.handler.RequestData;
import org.apache.ws.security.handler.WSHandler;
import org.apache.ws.security.message.WSSecSignature;
import org.apache.ws.security.util.WSSecurityUtil;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import org.company.soap.impl.ws.security.message.LocalWSSecSignature;

/**
 * Замена базового механизма подписания SOAP запроса.
 * Вместо создания WSSecSignature создается LocalWSSecSignature.
 * 
 * @see org.apache.ws.security.action.SignatureAction
 * @see org.apache.ws.security.message.WSSecSignature
 * @see org.company.soap.impl.ws.security.message.LocalWSSecSignature
 * @author Aleksey Sushko
 *
 */
public class LocalSignatureAction implements Action {
    public void execute(WSHandler handler, int actionToDo, Document doc, RequestData reqData)
            throws WSSecurityException {
        CallbackHandler callbackHandler = 
            handler.getPasswordCallbackHandler(reqData);
        WSPasswordCallback passwordCallback = 
            handler.getPasswordCB(reqData.getSignatureUser(), actionToDo, callbackHandler, reqData);
        WSSecSignature wsSign = loadWSSecSignature(reqData);

        if (reqData.getSigKeyId() != 0) {
            wsSign.setKeyIdentifierType(reqData.getSigKeyId());
        }
        if (reqData.getSigAlgorithm() != null) {
            wsSign.setSignatureAlgorithm(reqData.getSigAlgorithm());
        }
        if (reqData.getSigDigestAlgorithm() != null) {
            wsSign.setDigestAlgo(reqData.getSigDigestAlgorithm());
        }

        wsSign.setUserInfo(reqData.getSignatureUser(), passwordCallback.getPassword());
        wsSign.setUseSingleCertificate(reqData.isUseSingleCert());
        if (reqData.getSignatureParts().size() > 0) {
            wsSign.setParts(reqData.getSignatureParts());
        }
        
        if (passwordCallback.getKey() != null) {
            wsSign.setSecretKey(passwordCallback.getKey());
        }

        try {
            wsSign.prepare(doc, reqData.getSigCrypto(), reqData.getSecHeader());

            Element siblingElementToPrepend = null;
            for (WSEncryptionPart part : reqData.getSignatureParts()) {
                if ("STRTransform".equals(part.getName()) && part.getId() == null) {
                    part.setId(wsSign.getSecurityTokenReferenceURI());
                } else if (reqData.isAppendSignatureAfterTimestamp()
                        && WSConstants.WSU_NS.equals(part.getNamespace()) 
                        && "Timestamp".equals(part.getName())) {
                    List<Element> elements = 
                        WSSecurityUtil.findElements(
                            doc.getDocumentElement(), part.getName(), part.getNamespace()
                        );
                    if (elements != null && !elements.isEmpty()) {
                        Element timestampElement = elements.get(0);
                        Node child = timestampElement.getNextSibling();
                        while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
                            child = child.getNextSibling();
                        }
                        siblingElementToPrepend = (Element)child;
                    }
                }
            }

            List<javax.xml.crypto.dsig.Reference> referenceList = 
                wsSign.addReferencesToSign(reqData.getSignatureParts(), reqData.getSecHeader());

            if (reqData.isAppendSignatureAfterTimestamp() && siblingElementToPrepend == null) {
                wsSign.computeSignature(referenceList, false, null);
            } else {
                wsSign.computeSignature(referenceList, true, siblingElementToPrepend);
            }

            wsSign.prependBSTElementToHeader(reqData.getSecHeader());
            reqData.getSignatureValues().add(wsSign.getSignatureValue());
        } catch (WSSecurityException e) {
            throw new WSSecurityException("Error during Signature: ", e);
        }
    }

    protected WSSecSignature loadWSSecSignature(RequestData reqData) {
        WSSecSignature wsSign = new LocalWSSecSignature(reqData.getWssConfig());
        return wsSign;
    }
}

Настройка CXF

Ну вот, навалил кучу куда, а когда же дойдет дело до CXF? Как раз, настало время всё свести воедино. Конечно, можно не парится и показать кусок xml текста, в котором Spring поможет нам настроить CXF для работы с ЭЦП. Мне, по идейным соображениям, такой подход не интересен. Ни в JavaEE проектах, ни в OSGi проектах, я не использую Spring. Буду продолжать по старинки, Java like, так сказать.

В Apache CXF надо настроить систему перехватчиков событий работы с SOAP сообщениями.

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.smev.soap;

import java.util.HashMap;
import java.util.Map;

import org.apache.cxf.binding.soap.interceptor.SoapInterceptor;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.WSConstants;

import org.company.soap.impl.xml.security.action.LocalSignatureAction;

/**
 * Настройка системы перехвата и обработки сообщений
 * в реализации Apache CXF.
 * @author Aleksey Sushko
 */
public class SoapFeatures {

 public void initializeCleint(Object port) {
  Client client = ClientProxy.getClient(port);
  
  LoggingInInterceptor loggingInInterceptor = new LoggingInInterceptor();
  LoggingOutInterceptor loggingOutInterceptor = new LoggingOutInterceptor();
  
  client.getInInterceptors().add(loggingInInterceptor);
  client.getOutInterceptors().add(createSignatureOutInterceptor());
  client.getOutInterceptors().add(loggingOutInterceptor);
  client.getOutFaultInterceptors().add(loggingOutInterceptor);
 }

 /**
  * Параметр signatureUser определяет псевдоним ключа, используемого для подписи.
  * Параметр user нужен для KeystorePasswordCallback,
  * чтобы пользователь вернул пароль к закрытому ключу, указанному в signatureUser.
  * @return
  */
 private SoapInterceptor createSignatureOutInterceptor() {
  // определяем собственный класс системы подписывания
  Map<Integer, Class<?>> wssConnfigMap = new HashMap<Integer, Class<?>>();
  wssConnfigMap.put(Integer.valueOf(WSConstants.SIGN), LocalSignatureAction.class);

  Map<String, Object> params = new HashMap<String, Object>();
  params.put("wss4j.action.map", wssConnfigMap);
  params.put("action", "Signature");
  params.put("signaturePropFile", "crypto.properties");
  params.put("signatureKeyIdentifier", "DirectReference");
  params.put("user", "tester"); // KeystorePasswordCallback должен вернуть пароль к ключу пользователя
  params.put("signatureUser", "123456789012_12345678901234567890"); // SignatureAction берет это имя
  params.put("passwordCallbackClass", "org.company.soap.KeystorePasswordCallback");
  params.put("signatureDigestAlgorithm", "http://www.w3.org/2001/04/xmldsig-more#gostr3411");
  params.put("signatureAlgorithm", "http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411");
  
  // параметры для SMEV
  params.put("actor", "http://smev.gosuslugi.ru/actors/smev");
  params.put("mustUnderstand", "false");
  
  WSS4JOutInterceptor interceptor = new WSS4JOutInterceptor(params);
  
  return interceptor;
 }
}

Работа с сервисом

Из WSDL файла генерируем набор объектов
      <plugin>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-codegen-plugin</artifactId>
        <version>${cxf.version}</version>
        <executions>
          <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
              <wsdlOptions>
                <wsdlOption>
                  <wsdl>${basedir}/src/main/resources/wsdl/SmevUnifoService.wsdl</wsdl>
                  <extraargs>
                    <extraarg>-b</extraarg>
                    <extraarg>${basedir}/src/main/xjb/binding.xjb</extraarg>
                  </extraargs>
                </wsdlOption>
              </wsdlOptions>
            </configuration>
            <goals>
              <goal>wsdl2java</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Наконец, то я дошел до клиента SOAP сервиса

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.company.soap.test;

import java.net.URL;
import java.util.Date;

import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Service;

import org.apache.cxf.configuration.security.ProxyAuthorizationPolicy;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.transports.http.configuration.ProxyServerType;
import org.junit.Test;

import org.company.soap.SoapFeatures;
import org.company.soap.impl.xml.security.XmlDSignTools;
import ru.gosuslugi.smev.rev111111.MessageDataType;
import ru.gosuslugi.smev.rev111111.MessageType;
import ru.roskazna.smevunifoservice.SmevUnifoService;
import ru.roskazna.smevunifoservice.UnifoTransferMsg;


public class TestSignSOAP {

 @Test
 public void test() throws Exception {
  XmlDSignTools.init();
  QName serviceName = new QName("http://roskazna.ru/SmevUnifoService/", "SmevUnifoService");  
  String serviceURL = "http://localhost:8080/test-smev/SmevUnifoService"; 
  
  URL wsdlURL = getClass().getClassLoader().getResource("wsdl/SmevUnifoService.wsdl");
  
  Service service = Service.create(wsdlURL, serviceName);
  SmevUnifoService port = service.getPort(SmevUnifoService.class);
  
  // перенаправляем на нужный сервер
  BindingProvider provider = (BindingProvider) port;
  provider.getRequestContext().put(
    BindingProvider.ENDPOINT_ADDRESS_PROPERTY, 
    serviceURL);
  
  // параметры прохождения корпоративного прокси сервера
  Client client = ClientProxy.getClient(port);
  HTTPConduit http = (HTTPConduit) client.getConduit();
  HTTPClientPolicy policy = new HTTPClientPolicy();
  policy.setAutoRedirect(true);
  policy.setAllowChunking(false);
  policy.setConnection(ConnectionType.KEEP_ALIVE);
  http.setClient(policy);
  
  
  // Настраиваем систему перехвата для подписания SOAP сообщения
  SoapFeatures features = new SoapFeatures();
  features.initializeCleint(port);
  
  MessageType message = createMessage();
  MessageDataType messageData = createMessageData();  
  UnifoTransferMsg request = new UnifoTransferMsg();
  request.setMessage(message);
  request.setMessageData(messageData);
  
  UnifoTransferMsg responce = port.unifoTransferMsg(request);
 }

 private MessageType createMessage() {
  MessageType message = new MessageType();
  return message;
 }
 
 private MessageDataType createMessageData() {
  MessageDataType message = new MessageDataType();
  return message;
 }

}

Надеюсь, демонстрация долгожданного SOAP клиента покажется читателям гораздо более приятным, нежели описание работы с SOAP Message API.

суббота, 13 октября 2012 г.

MyBatis-Guice, настройка системы

Настройка системы

В предыдущей статье я упомянул об использовании MyBatis-Guice. В этой статье я покажу способ настройки Guice модуля.

Для начала напишем базовые xml файлы настройки MyBatis. Можно было бы обойтись и без них, но мне не очень нравится избыточное количество аннотаций каждого метода.

mapping.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeAliases>
    <typeAlias type="org.myproject.model.ReceiptDoc" alias="ReceiptDoc"/>
    <typeAlias type="org.myproject.impl.model.entity.ReceiptDocEntity" alias="ReceiptDocEntity"/>
  </typeAliases>
  <mappers>
    <mapper resource="org/myproject/db/ReceiptDocMapper.xml"/>
  </mappers>
</configuration>
Файл с SQL запросами

org/myproject/db/ReceiptDocMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.myproject.impl.provider.ReceiptDocMapper">
  <resultMap id="docMap" type="ReceiptDocEntity">
    <id property="id" column="ID" jdbcType="VARCHAR"/>
    <result property="dateLoad" column="DATELOAD" jdbcType="TIMESTAMP"/>
    <result property="docUrl" column="DOC_URL" jdbcType="VARCHAR"/>
    <result property="docName" column="DOC_NAME" jdbcType="VARCHAR"/>
    <result property="fileName" column="FILE_NAME" jdbcType="VARCHAR"/>
  </resultMap>

  <!-- Поиск документа по коду -->
  <select id="findReceiptDoc" resultMap="docMap" parameterType="string">
  select ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  from  RECEIPT_DOC where ID = #{id}
  </select>

  <!-- Поиск документа по названию -->
  <select id="findReceiptDocByName" resultMap="docMap" parameterType="string">
  select ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  from  RECEIPT_DOC where DOC_NAME = #{name}
  </select>

  <!-- Добавление нового документа -->
  <insert id="insert" parameterType="ReceiptDoc">
  insert into RECEIPT_DOC (
  ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  ) values (
  #{id, jdbcType=VARCHAR},
  #{dateLoad, jdbcType=TIMESTAMP},
  #{docUrl, jdbcType=VARCHAR},
  #{docName, jdbcType=VARCHAR},
  #{fileName, jdbcType=VARCHAR}
  )
  </insert>
</mapper>

Далее, составим модель объектов.
ReceiptDoc - это интерфейс сохраняемого объекта.
ReceiptDocEntity - это сохраняемый объект.
ReceiptDocMapper - это интерфейс генерируемого объекта, за которым MyBatis прячет весь функционал взаимодействия с базой данных.

Интерфейс манипуляции с базой данных ReceiptDocMapper

package org.myproject.impl.provider;

import org.myproject.model.ReceiptDoc;

public interface ReceiptDocMapper {
    ReceiptDoc findReceiptDoc(String id);
    List<ReceiptDoc> findReceiptDocByName(String name);
    void insert(ReceiptDoc receiptDoc);
}

Интерфейс данных ReceiptDoc

package org.myproject.model;

import java.util.Date;

public interface ReceiptDoc {
    String getId();
    void setId(String id);
    Date getDateLoad();
    void setDateLoad(Date dateLoad);
    String getDocUrl();
    void setDocUrl(String docUrl);
    String getDocName();
    void setDocName(String docName);
    String getFileName();
    voi setFileName(String fileName);
}

Сохраняемый объект ReceiptDocEntity

package org.myproject.impl.model.entity;

import java.util.Date;
import org.myproject.model.ReceiptDoc;

public class ReceiptDocEntity implements ReceiptDoc, Serializable {
    private static final long serialVersionUID = 1L;

    // Далее идет реализация интерфейса ReceiptDoc
}

Теперь у нас есть данные для хранения, интерфейс поиска и добавления записей в базу данных. Так же, есть файлы настроек системы MyBatis. Далее, нам понадобится объект, методы которого будут работать с использованием транзакций.

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

package org.myproject.provider;

import org.apache.ibatis.session.SqlSessionFactory;
import org.myproject.model.ReceiptDoc;

public interface Provider {
    ReceiptDoc findReceiptDoc(String id);
    List<ReceiptDoc> findReceiptDocByName(String name);
    void insert(ReceiptDoc receiptDoc);

    SqlSessionFactory getSqlSessionFactory();
}
Теперь напишем реализацию данного интерфейса.
Реализация одного из методов интерфейса Provider могла бы выглядеть примерно так:
private SqlSessionManager sqlSessionManager;

@Override
public ReceiptDoc findReceiptDoc(String id) {
    boolean isSessionInherited = sqlSessionManager.isManagedSessionStarted();

    if(!isSessionInherited) {
        sqlSessionManager.startManagedSession();
    }
    try {
        ReceiptDocMapper mapper = sqlSessionManager.getMapper(ReceiptDocMapper.class);

        // основная задача данного метода
        ReceiptDoc receiptDoc = findReceiptDoc(id);

        if(!isSessionInherited) {
            sqlSessionManager.commit();
        }

        return receiptDoc;
    }
    catch(Exception e) {
        sqlSessionManager.rollback();
    }
    finnaly {
        if(!isSessionInherited) {
            sqlSessionManager.close();
        }        
    }
}
Теперь сделаем то же самое с использованием mybatis-guice
@Inject
private ReceiptDocMapper mapper;

@Override
@Transactional(force = true)
public ReceiptDoc findReceiptDoc(String id) {
    return mapper.findReceiptDoc(id);
}

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

Нечто подобное можно постараться реализовать на CDI JSR-299 Weld. Но меня напрягает тот факт, что ReceiptDocMapper - это интерфейс. Рисовать методы провайдера для каждого интерфейса не совсем правильно.

Осталось дело за малым - настроить нашу CDI систему. Напишем небольшой класс, помогающий нам выполнить настройки.

package org.myproject.impl.provider;

import javax.sql.DataSource;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class ServicesUtil {
    /**
     * Создание JSR-330 Guice Injector. Поднятие MyBatis.
     * @param dataSource соединение с базой данных
     * @param useXA учитывать систему распределенных транзакций или нет
     * @param environment окружение для MyBatis
     * @param mappings файл настроек для MyBatis
     * @return
     */
    public static Injector createInjector(DataSource dataSource, boolean useXA, String environment, String mappings) {
        // Нам нужен ClassLoader приватного класса в текущем OSGI Bundle.
        // В настройка MyBatis используются приватные классы.
        SessionModule module = new SessionModule(ServicesUtil.class.getClassLoader(), environment, mappings);
        Injector injector = Guice.createInjector(module);

        module.configure(
            injector.getInstance(Provider.class).getSqlSessionFactory(),
            dataSource,
             useXA);
  
        return injector;
    }
}
На следующий код надо запастись терпением. Он достаточно длинный.
package com.comita.esb.impl.aisgz.exchange.oos.model;

import java.util.Properties;

import javax.sql.DataSource;
import javax.transaction.UserTransaction;

import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.guice.XMLMyBatisModule;
import org.mybatis.transaction.jta.JtaTransactionFactory;

import com.google.inject.PrivateModule;

import org.myproject.provider.Provider;

public class SessionModule extends PrivateModule {

    private final String environmentId;
    private final ClassLoader classLoader;
    private final String classPathResource;

    public SessionModule(
        ClassLoader classLoader,
        String environmentId,
        String classPathResource) {

        this.environmentId = environmentId;
        this.classLoader = classLoader;
        this.classPathResource = classPathResource;
    }

    @Override
    protected void configure() {
        install(new XMLMyBatisModule(){
            @Override
            protected void initialize() {
                useResourceClassLoader(classLoader);
                setEnvironmentId(environmentId);
                setClassPathResource(classPathResource);
            }
        });

        bind(Provider.class).to(ProviderImpl.class);
        expose(Provider.class);
    }

    public void configure(
        SqlSessionFactory sqlSessionFactory,
        DataSource dataSource,
        boolean useXaDataSource) {

        Configuration configuration = sqlSessionFactory.getConfiguration();
        TransactionFactory transactionFactory = configTransaction(configuration, useXaDataSource);
        Environment env = new Environment(environmentId, transactionFactory, dataSource);

        configuration.setEnvironment(env);
    }

    private TransactionFactory configTransaction(Configuration configuration, boolean useXaDataSource) {
        TransactionFactory transactionFactory;

        if(useXaDataSource) {
            transactionFactory = new JtaTransactionFactory();

            // запрещаем данному менеджеру автоматически закрывать соединения
            Properties prop = new Properties();
            prop.setProperty("closeConnection", "false");
            prop.setProperty("UserTransaction", "osgi:service/" + UserTransaction.class.getName());

            transactionFactory.setProperties(prop);
        }
        else {
            transactionFactory = new JdbcTransactionFactory();
        }
        return transactionFactory;
    }
}
Обратите внимание, что в свойство "UserTransaction" я указываю название OSGi JNDI ресурс. В JavaEE 6 надо указывать другое название ресурса. Поскольку, весь проект ориентирован на OSGi, не было необходимости как-то усложнять данный функционал.

четверг, 11 октября 2012 г.

MyBatis и JTA UserTransaction

В нескольких проектах мне пришлось использовать очень хорошую библиотеку работы с базой данных MyBatis. Самая интересная часть MyBatis - это возможность спрятать всю работу с базой данных за простыми Java интерфейсами. Самая ужасная часть MyBatis - это система управления текущей сессией и транзакциями. Многие предлагают спрятать MyBatis за механизмом DAO объектов Spring Framework. Данный подход частично скроет от нас систему работы с текущей сессией и предоставит собственную наработку ведения транзакций.

Попробовав написать код, я окончательно запутался в xml описании компонентов (bean) для Spring. Да тут еще и появилась возможность реализовать систему на основе Apache Camel Framework. Изучая данный framework, я понял, что мне прямая дорога в OSGi.

В относительно новой редакции OSGi V4.2 есть своя реализация xml описании компонентов, похожая на Spring и очень сильно отличающаяся. Короче, мне не по душе стало склеивать OSGi Blueprint и Spring Framework. Но сама идея OSGi Blueprint не подразумевает AOP ориентированного подхода. В Spring за всех старается некий AOP Invoker при обнаружении аннотации @Transaction. Мне так нравятся эти аннотации. Значит, надо искать что-то из CDI подобных реализаций. Для MyBatis есть специальная библиотека MyBatis-Guice. Это как раз то, что мне надо. В OSGi пока только рассматривается возможность использования CDI совместно с xml описанием OSGi Blueprint. Проект Google Guice успешно работает под управлением OSGi.

Мои муки выбора остановились на использовании MyBatis-Guice. Главное достоинство - это лаконичный java код с использованием аннотаций @Transaction и @Injection для моих любимых интерфейсов, скрывающих все муки JDBC SQL запросов.

И так, приступим к самому интересному. Это транзакции. Как известно, транзакции бывают локальными и глобальными. Не буду вдаваясь в детали описаний. Локальные - это те, что происходят на уровне текущего соединения с базой данных java.sql.Connection. Глобальные - это те, что помогают выполнить действия разных систем (сервисов, служб) в рамках одной задачи.

Короче, в MyBatis напрочь отсутствует система взаимодействия с глобальными транзакциями. Ну, это частично понятно. Реализация данного функционала тянет за собой кусок Java EE спецификации и требует наличия менеджера управления транзакциями. В небольшом java проекте весь этот ворох дополнительных библиотек будет только вызывать одно раздражение.

Поскольку я решился взять за основу OSGi Enterprise V4.2 c OSGi Blueprint, то мне менеджер транзакций достается в качестве полезного бонуса. В старой версии iBatis была поддержка JTA транзакций. Мне надо всего лишь переработать старый код под новый функционал MyBatis.

Далее идет большое количество кода.

Класс JtaTransactionFactory

/*
 *    Copyright 2009-2012 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.transaction.jta;

import java.sql.Connection;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.transaction.UserTransaction;

import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;

public class JtaTransactionFactory implements TransactionFactory {

 private UserTransaction userTransaction;
 private boolean closeConnection = true;

 public JtaTransactionFactory() {
 }

 @Override
 public void setProperties(Properties props) {
  if (props != null) {
   String utxName = null;
   try {
    utxName = (String) props.get("UserTransaction");
    InitialContext initCtx = new InitialContext();
    userTransaction = (UserTransaction) initCtx.lookup(utxName);
   } catch (NamingException e) {
    throw ExceptionFactory.wrapException(
      "Error initializing JtaTransactionConfig while looking up UserTransaction (" + utxName + ").", e);
   }

   String closeConnectionProperty = props.getProperty("closeConnection");
   if (closeConnectionProperty != null) {
    closeConnection = Boolean.valueOf(closeConnectionProperty);
   }
  }
 }

 @Override
 public Transaction newTransaction(Connection conn) {
  return new JtaTransaction(userTransaction, conn, closeConnection);
 }

 @Override
 public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
  return new JtaTransaction(userTransaction, dataSource, level, autoCommit);
 }

}

Класс JtaTransaction

/*
 *    Copyright 2009-2012 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.transaction.jta;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.UserTransaction;

import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.managed.ManagedTransaction;

public class JtaTransaction implements Transaction {
 private static final Log log = LogFactory.getLog(ManagedTransaction.class);

 private final UserTransaction userTransaction;
 private DataSource dataSource;
 private TransactionIsolationLevel level;
 private Connection connection;
 private final boolean closeConnection;

 private boolean commmitted = false;
 private boolean newTransaction = false;

 public JtaTransaction(UserTransaction userTransaction, Connection connection, boolean closeConnection) {
  this.userTransaction = userTransaction;
  this.connection = connection;
  this.closeConnection = closeConnection;

  if(userTransaction == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. UserTransaction was null.", null);
  }
  if(connection == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. Connection was null.", null);
  }

  init();
 }

 public JtaTransaction(UserTransaction userTransaction, DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {
  this.userTransaction = userTransaction;
  this.dataSource = ds;
  this.level = level;
  this.closeConnection = closeConnection;

  if(userTransaction == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. UserTransaction was null.", null);
  }
  if(dataSource == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. DataSource was null.", null);
  }

  init();
 }

 private void init() {
  try {
   newTransaction = userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION;
   if (newTransaction) {
    userTransaction.begin();
   }
  } catch (Exception e) {
   throw ExceptionFactory.wrapException("JtaTransaction could not start transaction.  Cause: ", e);
  }
 }

 @Override
 public Connection getConnection() throws SQLException {
  if (this.connection == null) {
   openConnection();
  }
  return this.connection;
 }

 @Override
 public void commit() throws SQLException {
  if (commmitted) {
   throw ExceptionFactory.wrapException("JtaTransaction could not commit because this transaction has already been committed.", null);
  }
  try {
   if (newTransaction) {
    userTransaction.commit();
   }
  } catch (Exception e) {
   throw ExceptionFactory.wrapException("JtaTransaction could not commit.  Cause: ", e);
  }
  commmitted = true;
 }

 @Override
 public void rollback() throws SQLException {
  if (!commmitted) {
   try {
    if (userTransaction != null) {
     if (newTransaction) {
      userTransaction.rollback();
     } else {
      userTransaction.setRollbackOnly();
     }
    }
   } catch (Exception e) {
    throw ExceptionFactory.wrapException("JtaTransaction could not rollback.  Cause: ", e);
   }
  }
 }

 @Override
 public void close() throws SQLException {
  if (this.closeConnection && this.connection != null) {
   if (log.isDebugEnabled()) {
    log.debug("Closing JDBC Connection [" + this.connection + "]");
   }
   this.connection.close();
  }
 }

 protected void openConnection() throws SQLException {
  if (log.isDebugEnabled()) {
   log.debug("Openning JDBC Connection");
  }
  this.connection = this.dataSource.getConnection();
  if (level != null) {
   connection.setTransactionIsolation(level.getLevel());
  }
  if (connection.getAutoCommit()) {
   connection.setAutoCommit(false);
  }
 }

}
Теперь осталось дело за малым. Надо создать SqlSessionFactory и в конфигурации указать использование JtaTransactionFactory в качестве фабрики создания транзакций.