ラベル Javaプログラム の投稿を表示しています。 すべての投稿を表示
ラベル Javaプログラム の投稿を表示しています。 すべての投稿を表示

2014年2月16日日曜日

JavaによるProduct Advertising APIのSOAP呼び出し(WS-Securityを利用してSOAPリクエストを処理する方法)

なにかアフィリエイトのサイトを作ろうと、まずは、JavaによるProduct Advertising APIのSOAP呼び出し方法を調べてみた。
ドキュメントには、言語に特化した詳しい説明があまりないので思った以上に苦労したので、その内容(主にJavaによる認証部分の実装手順)を備忘録代わりに残しておこうと思う。
(ただし、利用しているツール、ライブラリ、及びProduct Advertising APIの詳細な内容にはあまり触れないのでそれぞれのサイトを確認してください。)

まず、Product Advertising APIをSOAPによる呼び出し方法(認証の方式)には次の2種類がある。

  • WS-Securityを利用せずにSOAPリクエストを処理する方法
  • WS-Securityを利用してSOAPリクエストを処理する方法

本稿では、WS-Securityを利用してSOAPリクエストを処理する方法を説明する。この方法は、WS-Security1.0を利用し認証を行う。JDK単体ではWS-Seurityまでサポートされていないようなので、JAX-WSの実装としてApache CXFを利用する。また、署名に使用する証明書は、keytoolが公開鍵と秘密鍵が別になった証明書のインポートができない(ここは調査不足の可能性も)ようなので、amazonから提供される証明書ではなく、keytoolで生成した自己署名証明書を使用する。

なお、「WS-Securityを利用せずにSOAPリクエストを処理する方法」はこちらを参照。
今回使用したツール、ライブラリは以下の通りです。

ツール、ライブラリ名バージョン

Java Development Kit
7u51(1.7.0_51)
Eclipse IDE for Java EE Developers
4.3.1
Apache CXF
2.7.8
Tomcat
7.5.0

(1)自己署名証明書(秘密鍵・公開鍵のキーペア)の準備


amazonで有効なアルゴリズムはRSAなので、キーアルゴリズムとしてRSAを指定し、自己署名証明書(秘密鍵・公開鍵のキーペア)を生成する。有効期間は、amazonで生成可能な証明書の期間と同じ1年(365日)を設定している。
>keytool -genkeypair -keyalg RSA -dname <X.500識別名> -alias <キーエイリアス> -keypass <キーパスワード> -keystore <キーストアファイル> -storepass <キーストアパスワード> -validity 365

次に生成した自己署名証明書(秘密鍵・公開鍵のキーペア)から、amazonにアップロードするための公開鍵を同じくkeytoolを使用しRFC 1421 証明書符号化のフォーマット(-rfcオプション)で生成する。

>keytool -exportcert -alias <キーエイリアス> -keystore  <キーストアファイル> -file  <公開鍵ファイル> -storepass <キーストアパスワード> -rfc

生成された公開鍵ファイルは、AWS Management ConsoleのSecurity Credentialsからamazonにアップロードする。

X.500識別名
とりあえず適当に"cn=<名前(英字)>, c=JP"とか指定しておく。
キーエイリアス
証明書(秘密鍵・公開鍵のキーペア)にアクセスするためのエイリアス名
キーストアパスワード
キーストアにアクセスするためのパスワード
キーパスワード
証明書(秘密鍵)にアクセスするためのパスワード
キーストアファイル
証明書(秘密鍵・公開鍵のキーペア)を格納するキーストアファイル
キーストアファイル
amazonにアップロードする公開鍵ファイル(RFC 1421 証明書符号化のフォーマット)
※ここで設定したキーストアファイル、キーストアパスワード、キーエイリアス、キーパスワードは、実際のプログラムでも使用するため、以降ここで定義した名前を使用する。

(2)EclipseにApache CXF、Tomcatを設定する

Apache CXFによりコード生成をするため、まずEclipseにApache CXFの設定を行う。また、クライアントコードを生成するだけなのに、なぜかDynamic web Project(Web Serverの実装がないと)でないとApache CXFのコード生成ができないようなのでTomcatも併せてEclipseに設定する。

Apache CXFの設定
Apache CXFをダウンロードし、適当な場所に展開する。Eclipseの「Window」→「Preferences」→「Web Services」→「CXF 2.x Preferences」を開き、「add」ボタンを押下し、展開した場所を設定する。

追加後、一覧に追加したApache CXFの実装がバージョンと共に表示されるので、追加したApache CXFにチェックを入れる。そのほかの設定はとりあえずデフォルトのままでよい。


Tomcatの設定
同様にTomcatダウンロードし、適当な場所に展開する。Eclipseの「Window」→「Preferences」→「Server」→「Runtime Environments」を開き、「add」ボタンを押下する。


New Server Runtime Environmentが開くので「Apache」→「Apache Tomcat v7.0」を選択し、展開した場所を設定する。


追加後、一覧に追加したTomcatの実装がバージョンと共に表示される。


(3)WSDLからSOAPのクライアントコードを生成する


プロジェクト(Dynamic Web Project)の作成
クライアントコードを生成するためプロジェクトをEclipseのDynamic Web Projectとして作成し、プロジェクトのPropertiesを開き、「Java Build Path」の「Add Library」を押下し、「CXF Runtime」を追加する。


追加後、一覧に追加したApache CXF Libraryが表示される。


クライアントコードの生成
Eclipseの「File」→「New」→「Other」を選択し、Newウィザードで「Web Services」→「Web Service Client」を選択する。「Web Service Client」ウィザードが開くので、「Server definition」にProduct Advertising APIのWSDLのURLを設定し、「Configuration」のServer Runtimeに「Tomcat v7.0 Server」、Web service runtimeに「Apache CXF 2.x」を設定し「Next」をクリックする。


次に、WSDL2Javaの設定ウィザードが開き、自動生成されたパッケージ名が「com.amazon.webservices.awsecommerceservice.2011-08-01」となっており、このままだとJavaの規約違反となるため「com.amazon.webservices.awsecommerceservice._2011_08_01」に変更し「Finish」ボタンを押下する。


「Finish」ボタンを押下後、パッケージcom.amazon.webservices.awsecommerceservice._2011_08_01配下に次のソースが生成される。

Accessories.java
Arguments.java
AWSECommerceService.java
AWSECommerceServicePortType.java
AWSECommerceServicePortTypeImpl.java
AWSECommerceServicePortTypeImpl1.java
AWSECommerceServicePortTypeImpl10.java
AWSECommerceServicePortTypeImpl2.java
AWSECommerceServicePortTypeImpl3.java
AWSECommerceServicePortTypeImpl4.java
AWSECommerceServicePortTypeImpl5.java
AWSECommerceServicePortTypeImpl6.java
AWSECommerceServicePortTypeImpl7.java
AWSECommerceServicePortTypeImpl8.java
AWSECommerceServicePortTypeImpl9.java
AWSECommerceServicePortType_AWSECommerceServicePortCA_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortCN_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortDE_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortES_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortFR_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortIN_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortIT_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortJP_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortUK_Client.java
AWSECommerceServicePortType_AWSECommerceServicePortUS_Client.java
AWSECommerceServicePortType_AWSECommerceServicePort_Client.java
Bin.java
BrowseNode.java
BrowseNodeLookup.java
BrowseNodeLookupRequest.java
BrowseNodeLookupResponse.java
BrowseNodes.java
Cart.java
CartAdd.java
CartAddRequest.java
CartAddResponse.java
CartClear.java
CartClearRequest.java
CartClearResponse.java
CartCreate.java
CartCreateRequest.java
CartCreateResponse.java
CartGet.java
CartGetRequest.java
CartGetResponse.java
CartItem.java
CartItems.java
CartModify.java
CartModifyRequest.java
CartModifyResponse.java
Collections.java
CorrectedQuery.java
CustomerReviews.java
DecimalWithUnits.java
EditorialReview.java
EditorialReviews.java
Errors.java
HTTPHeaders.java
Image.java
ImageSet.java
Item.java
ItemAttributes.java
ItemLink.java
ItemLinks.java
ItemLookup.java
ItemLookupRequest.java
ItemLookupResponse.java
Items.java
ItemSearch.java
ItemSearchRequest.java
ItemSearchResponse.java
LoyaltyPoints.java
Merchant.java
NewReleases.java
NonNegativeIntegerWithUnits.java
ObjectFactory.java
Offer.java
OfferAttributes.java
OfferListing.java
Offers.java
OfferSummary.java
OperationRequest.java
OtherCategoriesSimilarProducts.java
package-info.java
Price.java
Promotion.java
Promotions.java
Property.java
RelatedItem.java
RelatedItems.java
Request.java
SavedForLaterItems.java
SearchBinSet.java
SearchBinSets.java
SearchResultsMap.java
SimilarityLookup.java
SimilarityLookupRequest.java
SimilarityLookupResponse.java
SimilarProducts.java
SimilarViewedProducts.java
StringWithUnits.java
TopItemSet.java
TopSellers.java
Tracks.java
VariationAttribute.java
VariationDimensions.java
Variations.java
VariationSummary.java

(4)Product Advertising APIを呼び出す

Product Advertising APIを呼び出すときに、WS-Securityを利用するためのApache WSS4J用のプロパティファイル(client_sign.properties)と、暗号化を行う際に、(1)で生成した証明書(秘密鍵)へアクセスするためのキーパスワードを設定するコールバッククラス(ClientPasswordCallback)を先に作成する。

Apache WSS4J用のプロパティファイル(client_sign.properties)
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.password=<キーストアパスワード>
org.apache.ws.security.crypto.merlin.keystore.alias=<キーエイリアス>
org.apache.ws.security.crypto.merlin.keystore.file=<キーストアファイル>

コールバッククラス(ClientPasswordCallback)クラス
import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class ClientPasswordCallback implements CallbackHandler {

    /** 証明書(秘密鍵)にアクセスするためのキーパスワード */
    private static String keyPassword = "<キーパスワード>";

    public void handle(Callback[] callbacks) throws IOException,
            UnsupportedCallbackException {

        WSPasswordCallback pc = (WSPasswordCallback) callbacks[0];

        // キーパスワードの設定
        pc.setPassword(keyPassword);
    }

}

最後にProduct Advertising APIを実行するmain()メソッドを持つクラスを実装する。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.ws.WebServiceRef;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.handler.WSHandlerConstants;

import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceService;
import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceServicePortType;
import com.amazon.webservices.awsecommerceservice._2011_08_01.Item;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearch;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchRequest;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchResponse;

public class AmazonService {

    @WebServiceRef
    private static AWSECommerceService AWSECommerceService;

    /** WS-Security(シグネチャのキー) */
    private final static String WS_SECURITY_SIGNATURE_KEY_IDENTIFIER = "DirectReference";
    /** WS-Security(シグネチャのキーアルゴリズム) */
    private final static String WS_SECURITY_SIGNATURE_ALGORITHM = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";

    /** AWS アクセスキー識別子 */
    private static String awsAccessKeyId = "xxxxxxxxxxxxxxxxxxxx";
    /** amazonアソシエイトのアカウントID */
    private static String associateTag = "XXXXXXXXXX-nn";
    /** 共通のリクエストパラメータ(Validate) */
    private static String validate = "False";
    /** 共通のリクエストパラメータ(XMLEscaping) */
    private static String xmlEscaping = "Single";

    public static void main(String[] args) throws Exception {

        // サービスのエンドポイントの取得
        AWSECommerceService = new AWSECommerceService();
        AWSECommerceServicePortType endPoint = AWSECommerceService
                .getAWSECommerceServicePortJP();
        Client client = ClientProxy.getClient(endPoint);

        // WS-Securityを使用するためのプロパティ設定
        Map<String, Object> outProps = new HashMap<String, Object>();
        outProps.put(WSHandlerConstants.USER, "<キーエイリアス>");
        outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.TIMESTAMP
                + " " + WSHandlerConstants.SIGNATURE);
        outProps.put(WSHandlerConstants.SIG_KEY_ID,
                WS_SECURITY_SIGNATURE_KEY_IDENTIFIER);
        outProps.put(WSHandlerConstants.SIG_ALGO,
                WS_SECURITY_SIGNATURE_ALGORITHM);
        outProps.put(WSHandlerConstants.SIG_PROP_FILE, "client_sign.properties");
        outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS,
                ClientPasswordCallback.class.getName());
        WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps);
        client.getOutInterceptors().add(wssOut);

        // ItemSearch用のリクエストオブジェクトの生成
        List<ItemSearchRequest> request = new ArrayList<ItemSearchRequest>();
        String marketplaceDomain = null;
        ItemSearchRequest itemSearchRequest = new ItemSearchRequest();
        itemSearchRequest.setKeywords("新居昭乃");
        itemSearchRequest.setAvailability("Available");
        itemSearchRequest.setSearchIndex("Blended");
        itemSearchRequest.getResponseGroup().add("Medium");

        itemSearchRequest.getResponseGroup().add("ItemAttributes");
        request.add(itemSearchRequest);

        ItemSearch itemSearch = new ItemSearch();
        itemSearch.setAssociateTag(associateTag);
        itemSearch.setAWSAccessKeyId(awsAccessKeyId);
        itemSearch.setMarketplaceDomain(marketplaceDomain);
        itemSearch.setXMLEscaping(xmlEscaping);
        itemSearch.setValidate(validate);
        itemSearch.getRequest().add(itemSearchRequest);

        // サービス(itemSearch)の実行
        ItemSearchResponse itemSearchResponse = endPoint.itemSearch(itemSearch);

        // 検索結果の表示
        for (Item item : itemSearchResponse.getItems().get(0).getItem()) {
            System.out.println(item.getItemAttributes().getTitle());
            System.out.println("\t" + item.getDetailPageURL());
        }

    }

}

41~45行目
(1)で生成したサービスクラスから、エンドポイント(ここでは日本サイト)を取得する。
47~60行目
WS-Securityを使用するためのプロパティの設定する。WS-Securityのアクション(WSHandlerConstants.ACTION)には、WSHandlerConstants.TIMESTAMPとWSHandlerConstants.SIGNATUREを指定し、WS-Securityのユーザー(WSHandlerConstants.USER)には、(1)で生成したキーストアの証明書のキーエイリアスを指定し、シグネチャを生成するアルゴリズム(WSHandlerConstants.SIG_ALGO)には「http://www.w3.org/2000/09/xmldsig#rsa-sha1」を指定し、シグネチャーの識別子(WSHandlerConstants.SIG_KEY_ID)にはメッセージに埋め込む方式の「DirectReference」を指定する。
62~80行目
itemSearchを呼び出すのに必要なオブジェクトを初期化・設定する。設定する内容はProduct Advertising APIを参照。
82~83行目
itemSearchの実行
85~89行目
検索結果を表示
これで、WS-Securityを利用する方法で実行が可能。
実際のSOAP呼び出しの際、SOAPメッセージに必要な要素がない場合、だいたい400:Bad Requestとなり、IDなどの認証情報に誤りがある場合403:Forbiddenとなる。

2014年2月15日土曜日

JavaによるProduct Advertising APIのSOAP呼び出し(WS-Securityを利用せずにSOAPリクエストを処理する方法)

なにかアフィリエイトのサイトを作ろうと、まずは、JavaによるProduct Advertising APIのSOAP呼び出し方法を調べてみた。
ドキュメントには、言語に特化した詳しい説明があまりないので思った以上に苦労したので、その内容(主にJavaによる認証部分の実装手順)を備忘録代わりに残しておこうと思う。
(ただし、利用しているツール、ライブラリ、及びProduct Advertising APIの詳細な内容にはあまり触れないのでそれぞれのサイトを確認してください。)

まず、Product Advertising APIをSOAPによる呼び出し方法(認証の方式)には次の2種類がある。

  • WS-Securityを利用せずにSOAPリクエストを処理する方法
  • WS-Securityを利用してSOAPリクエストを処理する方法

本稿では、WS-Securityを利用せずにSOAPリクエストを処理する方法を説明する。この方法は、AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダに埋め込む方法で、Javaの標準機能(JAX-WS)だけで簡単に実装が可能(ただ、実装上Base64エンコードが必要となるため、
その部分だけは、Apache Commons Codecを使用しました)。なお、WS-Securityを利用してSOAPリクエストを処理する方法はこちらを参照。
今回使用したツール、ライブラリは以下の通りです。

ツール、ライブラリ名バージョン

Java Development Kit
7u51(1.7.0_51)
Eclipse IDE for Java EE Developers
4.3.1
Apache Commons Codec
1.3


(1)WSDLからSOAPのクライアントコードを生成する

JDKに付属しているwsimportを使用し下記コマンドを実行し、Product Advertising APIが公開されているWSDLからクライアントコードを生成する。あとで、生成したコードを一部修正するので、ここではソースコードのみ生成する。
(WSDLのURLは、日本語ドキュメントだと内容が古いままの箇所があるので、英語ドキュメントも確認すること。)

>wsimport -Xnocompile -s . <Product Advertising API WSDLのURL>

実行後、パッケージcom.amazon.webservices.awsecommerceservice._2011_08_01配下に次のソースが生成される。

Accessories.java
Arguments.java
AWSECommerceService.java
AWSECommerceServicePortType.java
Bin.java
BrowseNode.java
BrowseNodeLookup.java
BrowseNodeLookupRequest.java
BrowseNodeLookupResponse.java
BrowseNodes.java
Cart.java
CartAdd.java
CartAddRequest.java
CartAddResponse.java
CartClear.java
CartClearRequest.java
CartClearResponse.java
CartCreate.java
CartCreateRequest.java
CartCreateResponse.java
CartGet.java
CartGetRequest.java
CartGetResponse.java
CartItem.java
CartItems.java
CartModify.java
CartModifyRequest.java
CartModifyResponse.java
Collections.java
CorrectedQuery.java
CustomerReviews.java
DecimalWithUnits.java
EditorialReview.java
EditorialReviews.java
Errors.java
HTTPHeaders.java
Image.java
ImageSet.java
Item.java
ItemAttributes.java
ItemLink.java
ItemLinks.java
ItemLookup.java
ItemLookupRequest.java
ItemLookupResponse.java
Items.java
ItemSearch.java
ItemSearchRequest.java
ItemSearchResponse.java
LoyaltyPoints.java
Merchant.java
NewReleases.java
NonNegativeIntegerWithUnits.java
ObjectFactory.java
Offer.java
OfferAttributes.java
OfferListing.java
Offers.java
OfferSummary.java
OperationRequest.java
OtherCategoriesSimilarProducts.java
package-info.java
Price.java
Promotion.java
Promotions.java
Property.java
RelatedItem.java
RelatedItems.java
Request.java
SavedForLaterItems.java
SearchBinSet.java
SearchBinSets.java
SearchResultsMap.java
SimilarityLookup.java
SimilarityLookupRequest.java
SimilarityLookupResponse.java
SimilarProducts.java
SimilarViewedProducts.java
StringWithUnits.java
TopItemSet.java
TopSellers.java
Tracks.java
VariationAttribute.java
VariationDimensions.java
Variations.java
VariationSummary.java

(2)AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダを埋め込むSOAPハンドラを実装する

AWS アクセスキー識別子のHMAC-SHA256によるハッシュ情報をSOAPヘッダを埋め込むSOAPハンドラを実装する。SOAPヘッダには次の3つの要素を埋め込む必要がある。
AWSAccessKeyId
AWS アクセスキー識別子(Product Advertising APIのアカウントID)
Timestamp
リクエストに含めるタイムスタンプ
Signatur
AWS 秘密キー(Product Advertising APIのアカウントIDに対応する秘密キー)を元に、Action および Timestamp パラメータの連結した文字列のHMAC-SHA256のハッシュ値

SOAPヘッダイメージ
<S:Header xmlns:aws="http://security.amazonaws.com/doc/2007-01-01/">
 <aws:AWSAccessKeyId">xxxxxxxxxxxxxxxxxxxx</aws:AWSAccessKeyId">
 <aws:Timestamp">2014-02-15T10:07:43Z</aws:Timestamp">
 <aws:Signature">zI0LD8X7yclKaBzk0b5g8L0G5P2TbC3ncxCD2ng2J3E=</aws:Signature">
</S:Header">

実際の実装は、SOAPのメッセージ・ハンドラ(AWSECommerceServiceProxyHandler) クラスを実装し、SOAPのメッセージ・ハンドラのhandleMessage()メソッド内でヘッダへの埋め込み処理を実装する。
package com.amazon.webservices.awsecommerceservice._2011_08_01;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Set;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.namespace.QName;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.apache.commons.codec.binary.Base64;

public class AWSECommerceServiceProxyHandler implements
        SOAPHandler<SOAPMessageContext> {

    /** シグネチャ(ハッシュ計算)用のアルゴリズム */
    private static final String SIGNATURE_ALGORITHM = "HmacSHA256";
    /** AWSの認証用のネームスペース */
    private static final String AWS_SECURITY_NS = "http://security.amazonaws.com/doc/2007-01-01/";
    /** AWSの認証用のネームスペースのプレフィックス */
    private static final String AWS_SECURITY_NS_PREFIX = "aws";

    /** AWSの認証用のヘッダパラメータ(AWSAccessKeyId) */
    private static final String AWS_HEADER_AWS_PARAMETER_ACCESSKEYID = "AWSAccessKeyId";
    /** AWSの認証用のヘッダパラメータ(Timestamp) */
    private static final String AWS_HEADER_AWS_PARAMETER_TIMESTAMP = "Timestamp";
    /** AWSの認証用のヘッダパラメータ(Signature) */
    private static final String AWS_HEADER_AWS_PARAMETER_SIGNATURE = "Signature";

    /** AWS アクセスキー識別子 */
    private String awsAccessKeyId = null;
    /** AWS 秘密キー */
    private String awsSecretKey = null;
    /** 秘密鍵 */
    private SecretKeySpec keySpec = null;
    /** メッセージ認証コード */
    private Mac mac = null;

    /**
     * コンストラクタ
     */
    public AWSECommerceServiceProxyHandler() {
        init();
    }

    @Override
    public void close(MessageContext mc) {
    }

    @Override
    public boolean handleFault(SOAPMessageContext mc) {
        return false;
    }

    @Override
    public boolean handleMessage(SOAPMessageContext mc) {

        // アウトバウンド以外のメッセージは無視する。
        if (!((Boolean) mc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY))) {
            return false;
        }

        String action = getAction(mc);
        String timestamp = getTimestamp();
        String signature = null;
        try {
            signature = calculateSignature(action, timestamp);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        try {
            SOAPMessageContext smc = (SOAPMessageContext) mc;
            SOAPMessage message = smc.getMessage();
            SOAPEnvelope envelope = message.getSOAPPart().getEnvelope();
            // メッセージにSOAPヘッダーが存在していない場合、SOAPヘッダを生成する。
            if (envelope.getHeader() == null) {
                envelope.addHeader();
            }
            SOAPHeader header = envelope.getHeader();
            header.addNamespaceDeclaration(AWS_SECURITY_NS_PREFIX,
                    AWS_SECURITY_NS);

            Name awsidName = envelope.createName(
                    AWS_HEADER_AWS_PARAMETER_ACCESSKEYID,
                    AWS_SECURITY_NS_PREFIX, AWS_SECURITY_NS);
            Name tsName = envelope.createName(
                    AWS_HEADER_AWS_PARAMETER_TIMESTAMP, AWS_SECURITY_NS_PREFIX,
                    AWS_SECURITY_NS);
            Name sigName = envelope.createName(
                    AWS_HEADER_AWS_PARAMETER_SIGNATURE, AWS_SECURITY_NS_PREFIX,
                    AWS_SECURITY_NS);

            SOAPHeaderElement akidElement = header.addHeaderElement(awsidName);
            SOAPHeaderElement tsElement = header.addHeaderElement(tsName);
            SOAPHeaderElement sigElement = header.addHeaderElement(sigName);

            akidElement.addTextNode(awsAccessKeyId);
            tsElement.addTextNode(timestamp);
            sigElement.addTextNode(signature);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return true;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<QName> getHeaders() {
        return java.util.Collections.EMPTY_SET;
    }

    /**
     * 初期化メソッド
     */
    private void init() {

        // AWS アクセスキー識別子、AWS 秘密キーの設定(必要に応じて外出しにする)
        awsAccessKeyId = "xxxxxxxxxxxxxxxxxxxxxx";
        awsSecretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

        try {
            byte[] bytes = awsSecretKey.getBytes("UTF-8");
            this.keySpec = new SecretKeySpec(bytes, SIGNATURE_ALGORITHM);
            this.mac = Mac.getInstance(SIGNATURE_ALGORITHM);
            this.mac.init(keySpec);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * エンドポイントのURLからAction名を取得する。
     * 
     * @param mc
     * @return アクション
     */
    private String getAction(SOAPMessageContext mc) {
        String action = null;
        try {
            String endpointAddress = (String) mc
                    .get(BindingProvider.SOAPACTION_URI_PROPERTY);
            String actionUri = (new URL(endpointAddress)).getPath();
            String tokens[] = actionUri.split("/");
            action = tokens[tokens.length - 1];
        } catch (Exception e) {
            new RuntimeException(e);
        }
        return action;
    }

    /**
     * UTCによるタイムスタンプ(ISO8601フォーマット)の生成
     * 
     * @return UTCによるタイムスタンプ(ISO8601フォーマット)
     */
    private String getTimestamp() {
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat is08601 = new SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ss'Z'");

        is08601.setTimeZone(TimeZone.getTimeZone("UTC"));
        return is08601.format(calendar.getTime());
    }

    /**
     * シグネチャ(ハッシュ値)の計算 アクションとタイムスタンプを連結した文字列を元にハッシュ値を計算する。
     * 
     * @param action
     *            実行するアクション
     * @param timestamp
     *            UTCによるタイムスタンプ(ISO8601フォーマット)
     * @return シグネチャ(ハッシュ値)
     * @throws Exception
     *             If there were errors or missing, required classes when trying
     *             to calculate the hash.
     */
    private String calculateSignature(String action, String timestamp) {
        String toSign = (action + timestamp);

        byte[] sigBytes = mac.doFinal(toSign.getBytes());
        return new String(Base64.encodeBase64(sigBytes));
    }

}


AWSECommerceServiceProxyHandlerの説明
70~121行目
ハッシュ情報のヘッダへの埋め込みを行う部分。本実装では72~75行目でアウトバウンド(リクエストメッセージ)以外は無視するようにしている。77~84行目ではヘッダに埋め込むためのアクション、タイムスタンプ、ハッシュ値を取得し、86~118行目で取得した値のヘッダへの埋め込みを行う。
125~127行目
ヘッダはhandleMessage()メソッドで動的な内容を含め設定するので、ここでは一旦空のヘッダを返している。
132~151行目
AWS アクセスキー識別子、AWS 秘密キーの設定と、それらを元にしたハッシュ計算用の秘密鍵を設定している。ここで自分のAWS アクセスキー識別子、AWS 秘密キーの設定をする。また、AWS アクセスキー識別子、AWS 秘密キーは外出しにして、設定ファイル等から取得するようにしてもよい。
159~171行目
エンドポイントのURLから実行するアクション(エンドポイントのURLの一番最後の階層)を取得する。
178~185行目
現在時刻(UTC)からタイムスタンプの文字列(ISO8601フォーマット)を生成する。

(3)実装したSOAPハンドラ(AWSECommerceServiceProxyHandler)をハンドラ・チェーンに設定する

ハンドラ・チェーンの設定ファイルの作成
(2)で実装したAWSECommerceServiceProxyHandlerをハンドラ・チェーンに設定するために、パッケージ内にハンドラ・チェーンの設定ファイル(AWSECommerceService-HandlerChain.xml)を作成(下記参照)し、パッケージ(com.amazon.webservices.awsecommerceservice._2011_08_01)のフォルダ内に配置する。

<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
    <handler-chain>
        <handler>
            <handler-name>proxyHandler</handler-name>
            <handler-class>com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceServiceProxyHandler</handler-class>
        </handler>
    </handler-chain>
</handler-chains>

ハンドラ・チェーンの設定
作成したハンドラ・チェーンの設定ファイルを実装に組み込むために、(1)で生成したProduct Advertising APIのAWSECommerceServiceクラスのAWSECommerceService()メソッドにハンドラ・チェーンのアノテーションを追加する(下記参照)。

@WebServiceClient(name = "AWSECommerceService", targetNamespace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01", wsdlLocation = "http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl")
public class AWSECommerceService
    extends Service

↓

@WebServiceClient(name = "AWSECommerceService", targetNamespace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01", wsdlLocation = "http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl")
@HandlerChain(file="AWSECommerceService-HandlerChain.xml") ←この行を追加する。
public class AWSECommerceService
    extends Service


以上で機能的な部分の実装は完了。

(4)Product Advertising APIを呼び出す

ここでは単純にmain()メソッドからProduct Advertising APIを実行するサンプルを実装する。

import java.util.ArrayList;
import java.util.List;

import javax.xml.ws.Holder;
import javax.xml.ws.WebServiceRef;

import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceService;
import com.amazon.webservices.awsecommerceservice._2011_08_01.AWSECommerceServicePortType;
import com.amazon.webservices.awsecommerceservice._2011_08_01.Item;
import com.amazon.webservices.awsecommerceservice._2011_08_01.ItemSearchRequest;
import com.amazon.webservices.awsecommerceservice._2011_08_01.Items;
import com.amazon.webservices.awsecommerceservice._2011_08_01.OperationRequest;

public class AmazonService {

    @WebServiceRef
    private static AWSECommerceService AWSECommerceService;

    /** AWS アクセスキー識別子 */
    private static String awsAccessKeyId = "xxxxxxxxxxxxxxxxxxxx";
    /** AmazonアソシエイトのアカウントID */
    private static String associateTag = "XXXXXXXXXX-nn";
    /** 共通のリクエストパラメータ(Validate) */
    private static String validate = "False";
    /** 共通のリクエストパラメータ(XMLEscaping) */
    private static String xmlEscaping = "Single";

    public static void main(String[] args) {

        // サービスのエンドポイントの取得
        AWSECommerceService = new AWSECommerceService();
        AWSECommerceServicePortType endPoint = AWSECommerceService
                .getAWSECommerceServicePortJP();

        // 変数の初期化
        String marketplaceDomain = null;
        ItemSearchRequest shared = null;
        List<ItemSearchRequest> request = new ArrayList<ItemSearchRequest>();
        Holder<OperationRequest> operationRequest = new Holder<OperationRequest>();
        Holder<List<Items>> items = new Holder<List<Items>>();

        // ItemSearch用のリクエストオブジェクトの生成
        ItemSearchRequest itemSearchRequest = new ItemSearchRequest();
        itemSearchRequest.setKeywords("新居昭乃");
        itemSearchRequest.setAvailability("Available");
        itemSearchRequest.setSearchIndex("Blended");
        itemSearchRequest.getResponseGroup().add("Medium");

        itemSearchRequest.getResponseGroup().add("ItemAttributes");
        request.add(itemSearchRequest);

        // サービス(itemSearch)の実行
        endPoint.itemSearch(marketplaceDomain, awsAccessKeyId, associateTag,
                xmlEscaping, validate, shared, request, operationRequest, items);

        // 検索結果の表示
        for (Item item : items.value.get(0).getItem()) {
            System.out.println(item.getItemAttributes().getTitle());
            System.out.println("\t" + item.getDetailPageURL());
        }
    }
}

19~26行目
AWSアクセス識別子、amazonアソシエイトIDなど必要な初期値を設定する。(認証はヘッダ情報で処理されるため、ここ設定されるAWSアクセス識別子は実際見ていない模様。また、共通のリクエストパラメータの設定が効いてないかも、Validateの設定を変えても結果が変わらなかった。この点はちょっと不明)
30~33行目
(1)で生成したサービスクラスから、エンドポイント(ここでは日本サイト)を取得する。
35~50行目
itemSearchを呼び出すのに必要なオブジェクトを初期化・設定する。設定する内容はProduct Advertising APIを参照。
52~55行目
itemSearchの実行
56~60行目
検索結果を表示
これで、WS-Securityを利用しない方法で実行が可能。
実際のSOAP呼び出しの際、SOAPメッセージに必要な要素がない場合、だいたい400:Bad Requestとなり、IDなどの認証情報に誤りがある場合403:Forbiddenとなるようだ。

2011年9月4日日曜日

getResourceAsStream()でリソースが読み込めない

getResource(),getResourceAsStream()でちょっとはまったので忘れないうちにメモっとく。
まずは動作の基本から、クラスローダ(ClassLoader)に対するgetResource(),getResourceAsStream()は、クラスパスのルートのパスを検索するようになっており、引数で渡した文字列でそのままクラスパスのルートから検索する。なので、文字列の頭に基本的に"/"は付けない。
一方、クラス(Class)に対するgetResource(),getResourceAsStream()は、引き数の文字列の頭に"/"が付いてる場合は"/"を以降の文字列で、引数の文字列の頭に"/"がついていない場合はパッケージ名の"."を"/"に置き換え引数の文字列と"/"とで連結した文字列を、クラスパスのルートから検索する。
例えば、foo.bar.TestMainというクラスがあり、同じパッケージのフォルダ内にTestMain.propertiesというリソースがあった場合、以下の3つの方法でアクセスが出来る(すべて同じ動作)。


TestMain.class.getClassLoader()
    .getResource("foo/bar/test.properties"));

TestMain.class
    .getResource("/foo/bar/test.properties"));

TestMain.class
    .getResource("test.properties"));

ここまでは、位置に依存しない方法でのリソースへのアクセスの仕様を見ればわかるのだけれど、実行時にクラスがjarファイルなどに格納されている場合、実行環境によりなぜかgetResourceAsStream()でリソースが読み込めない場合があるようだ。どうやら、内部的にファイルとしてアクセスしようとして、zip内のファイルを読み取ることが出来ずに例外が発生しているらしい。ちなみにgetResource()でURLはちゃんと戻るからファイルの存在は認識しているようなので不思議だ。
今回、Windowsで開発していたときは問題なかったのだが、LinuxのWebLogic上で実行した際に、リソースにアクセス出来ないという現象が発生していた(WebLogicはwarをデプロイすると/WEB-INF/classesがjarにまとめられる)。


結局のところ実行環境によらずにリソースにアクセスするための解決方法としては、下記のようにするのが良いようだ。
URL resource
    = new URL(TestMain.class.getResource("."),
              "test.properties");

InputStream in 
    = resource
      .openConnection().getInputStream();
:


※2014/02/09 ソースコードの表示方法を変えました。




2009年5月30日土曜日

Java 6 Mustang のXML機能(JAXB)によるXmlDAO

Java 6 MustangのXMLの機能(JAXB)の機能がかなり拡張され、これを利用してXML用のDAOクラスを作ってみました。

まず、Java 6に付属しているSchemagenというツールにより、DAOで扱うクラスのスキーマファイルを生成します。
たとえば次のようなSampleクラスを作成します。schemaのルートノードとなるクラス(ここではSampleクラス)には
アノテーションXmlRootElementを指定します。

Sampleクラス:

import javax.xml.bind.annotation.XmlRootElement;

/**
* サンプルクラス
*/
@XmlRootElement
public class Sample {

/* 名前 */
private String name;

/* 年齢 */
private int age;

/**
* 名前を取得する。
*
* @return 名前
*/
public String getName() {
return name;
}

/**
* 名前を設定する。
*
* @param 名前
*/
public void setName(String name) {
this.name = name;
}

/**
* 年齢を取得する。
*
* @return 年齢
*/
public int getAge() {
return age;
}

/**
* 年齢を設定する。
*
* @param 年齢
*/
public void setAge(int age) {
this.age = age;
}
}



Sampleクラスに対するスキーマを作成する場合以下のようにコマンドを実行します。

> schemagen -classpath <クラスパス> Sample.java

※なぜかMacでは動作しなかった(InvocationTargetExceptionが発生してしまう)ので、Windowsでschemagenを実行しました。

schemagenを実行するとschema1.xsdというファイルが生成されますので、これをSample.xsdという名前に変更し保存します。


今回作成したXmlDAOにより、上記のSampleクラスの保存および読み込みのロジックは記述すると次のようなコードになります。
XmlDAOのテストコード:

import java.io.File;

import jp.aarkiton.xml.dao.XmlDAO;

/**
* XmlDAO用テストソース
*/
public class XmlDAOTest {

public static void main(String args[]) throws Exception {
// 保存対象のテスト用オブジェクト
Sample sample = new Sample();
sample.setName("tama");
sample.setAge(10);

// XmlDAOのインスタンス生成
XmlDAO<Sample> xmlDAO = XmlDAO.getInstance(Sample.class, "Sample.xsd");

// オブジェクトの保存
xmlDAO.save(sample, new File("sample.xml"));
// 一旦オブジェクトを解放
sample = null;

// オブジェクトの読み込み
sample = xmlDAO.load(new File("sample.xml"));

System.out.println(sample.getName());
System.out.println(sample.getAge());

}

}


上記コードを実行した結果得られるsample.xmlは以下のようになります。
sample.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sample>
<age>10</age>
<name>tama</name>
</sample>


最後に今回作成したClassUtils、XmlDAO、のクラスを記載します。
ClassUtils:

/*
* Copyright 2009 (C) Aarkiton Soft. All Rights Reserved.
*
* This software is open source.
*/
package jp.aarkiton.util;

import java.io.InputStream;
import java.net.URL;

/**
* クラス、パッケージ関連のユーティリティクラス
* @author Takanori
* @version 1.0.0
* @since 1.0.0
*/
public class ClassUtils {

/**
* 指定したクラスのパッケージパスから、指定したファイルのURLを取得する。
* @param cls クラス
* @param name ファイル名
* @param URL
*/
@SuppressWarnings("unchecked")
public static URL getResource(Class cls,String name){
return cls.getClassLoader().getResource(getPackagePath(cls)+"/"+name);
}

/**
* 指定したクラスのパッケージパスから、指定したファイルの入力ストリームを取得する。
* @param cls クラス
* @param name ファイル名
* @param 入力ストリーム
*/
@SuppressWarnings("unchecked")
public static InputStream getResourceAsStream(Class cls,String name){
return cls.getClassLoader().getResourceAsStream(getPackagePath(cls)+"/"+name);
}

/**
* クラスを指定してパッケージのパスを取得する。
* @param cls クラス
* @return パッケージのパス
*/
@SuppressWarnings("unchecked")
private static String getPackagePath(Class cls){
return cls.getPackage().getName().replace('.','/');
}

}


XmlDAO:

/*
* Copyright 2009 (C) Aarkiton Soft. All Rights Reserved.
*
* This software is open source.
*/
package jp.aarkiton.xml.dao;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import jp.aarkiton.util.ClassUtils;

import org.xml.sax.SAXException;

/**
* Java 6 Mustang のXML機能を使用したJavaオブジェクトのシリアライザークラス

* Java 6 から同梱されたschemagenにより、Javaのクラスからスキーマを生成することができ、
* その生成されたスキーマを使用しオブジェクトのロード、セーブを行う。
* スキーマファイル名を指定しない場合、スキーマは対象となるクラスのパッケージパスの
* 「クラス名.xsd」という名前のスキーマファイルを使用する。
*
*

* (1)スキーマの生成
* Usage: schemagen [-options ...] <java files>
* Options:
* -d <path> : 出力先ディレクトリ
* -cp <path> : ユーザ定義ファイルの検索パス
* -classpath <path> : ユーザ定義ファイルの検索パス
* -version : バージョン情報
*
* (2)クラスの使用例
* 例1)SampleクラスのインスタンスをXMLスキーマ定義(Sample.xsd)に基づき、
*     XMLファイル(sample.xml)として保存する。
*
* Sample sample = new Sample();
* :
* XmlDAO<Sample> xmldao = XmlDAO.getInstance(Sample.class,"Sample.xsd");
* xmldao.save(sample, new File("sample.xml"));
*
* 例2)SampleクラスのインスタンスをXMLファイル(sample.xml)として保存する。
*     (XMLスキーマ定義を指定しなかった場合、パッケージフォルダにある<クラス名>.xsdファイルが使用される)
* Sample sample = new Sample();
* :
* XmlDAO<Sample> xmldao = XmlDAO.getInstance(Sample.class);
* xmldao.save(sample, new File("sample.xml"));
*
* 例3)SampleクラスのインスタンスをXMLスキーマ定義(Sample.xsd)に基づき、
*     XMLファイル(sample.xml)からロードする。
*
* XmlDAO<Sample> xmldao = XmlDAO.getInstance(Sample.class,"Sample.xsd");
* Sample sample = xmldao.load(new File("sample.xml"));
*
* 例4)SampleクラスのインスタンスをXMLファイル(sample.xsd)からロードする。
*     (XMLスキーマ定義を指定しなかった場合、パッケージフォルダにある<クラス名>.xsdファイルが使用される)
* XmlDAO<Sample> xmldao = XmlDAO.getInstance(Sample.class);
* Sample sample = xmldao.load(new File("sample.xml"));
*
*


* @author Takanori
* @version 1.0.0
* @since 1.0.0
*
* @param
*/
public class XmlDAO {

/** 永続化クラス */
protected Class targetClass;

/** Marshallerオブジェクトに渡すプロパティ */
protected Map properties;

/** スキーマファイル */
protected File schemaFile = null;

/**
* クラスオブジェクトを入力としてXmlDAOのインスタスを生成するファクトリメソッド。
*
* @param targetClass
* ターゲットクラス
*/
public static XmlDAO getInstance(Class targetClass) {
return new XmlDAO(targetClass);
}

/**
* クラスオブジェクト、スキーマファイル名を入力としてXmlDAOのインスタスを生成するファクトリメソッド。
*
* @param targetClass
* ターゲットクラス
* @param schemaFilename
* スキーマファイル名
*/
public static XmlDAO getInstance(Class targetClass,
String schemaFilename) {
return new XmlDAO(targetClass, schemaFilename);
}

/**
* クラスオブジェクト、スキーマファイル名を入力としてXmlDAOのインスタスを生成するファクトリメソッド。
*
* @param targetClass
* ターゲットクラス
* @param schemaFile
* スキーマファイル
*/
public static XmlDAO getInstance(Class targetClass,
File schemaFile) {
return new XmlDAO(targetClass, schemaFile);
}

/**
* クラスオブジェクトを入力としてXmlDAOのインスタスを生成するコンストラクタ。
*
* @param targetClass
* ターゲットクラス
*/
protected XmlDAO(Class targetClass) {
this(targetClass, (File) null);
}

/**
* クラスオブジェクト、スキーマファイル名を入力としてXmlDAOのインスタスを生成するコンストラクタ。
*
* @param targetClass
* ターゲットクラス
* @param schemaFile
* スキーマファイル名
*/
protected XmlDAO(Class targetClass, String schemaFilename) {
this(targetClass, new File(schemaFilename));
}

/**
* クラスオブジェクト、スキーマファイルを入力としてXmlDAOのインスタスを生成するコンストラクタ。
*
* @param targetClass
* ターゲットクラス
* @param schemaFile
* スキーマファイル
*/
protected XmlDAO(Class targetClass, File schemaFile) {
this.targetClass = targetClass;
this.schemaFile = schemaFile;
this.properties = getDefaultProperties();
}

/**
* Marshallerオブジェクトに渡すデフォルトのプロパティを取得する。
*
* @return デフォルトのプロパティ
*/
protected Map getDefaultProperties() {
Map defualtProperteis = new HashMap();
defualtProperteis.put(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
return defualtProperteis;
}

/**
* オブジェクトをセーブする。
*
* @param o
* ターゲットオブジェクト
* @param output
* 出力ファイル
*/
public void save(T o, File output) throws JAXBException, SAXException,
IOException {
save(o, new FileOutputStream(output));
}

/**
* オブジェクトをセーブする。
*
* @param o
* ターゲットオブジェクト
* @param output
* 出力ストリーム
*/
public void save(T o, OutputStream output) throws JAXBException,
SAXException, IOException {
// ターゲットクラスのためのコンテキストを作成.
JAXBContext context = JAXBContext.newInstance(targetClass);

// 使用する Schema オブジェクトを作成.
Schema schema = getSchema();

// Java オブジェクトから XML に変換するための Marshaller を作成.
Marshaller marshaller = context.createMarshaller();
marshaller.setSchema(schema);
for (Map.Entry entry : properties.entrySet()) {
marshaller.setProperty(entry.getKey(), entry.getValue());
}

// XML に変換.
marshaller.marshal(o, output);
}

/**
* オブジェクトをロードする。
*
* @param input
* 入力ファイル
* @return オブジェクト
*/
public T load(File input) throws JAXBException, SAXException, IOException {
return load(new FileInputStream(input));
}

/**
* オブジェクトをロードする。
*
* @param input
* 入力ストリーム
* @return オブジェクト
*/
@SuppressWarnings("unchecked")
public T load(InputStream input) throws JAXBException, SAXException,
IOException {
// ターゲットクラスのためのコンテキストを作成.
JAXBContext context = JAXBContext.newInstance(targetClass);

// 使用する Schema オブジェクトを作成.
Schema schema = getSchema();

// XML から Java オブジェクトに変換するための Unmarshaller を作成.
Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setSchema(schema);

// XML から Java オブジェクトに変換.
return (T) unmarshaller.unmarshal(input);

}

/**
* オブジェクトを入力にストリームに変換する。
*
* @param o
* ターゲットオブジェクト
*/
public InputStream getInputStream(T o) throws JAXBException, SAXException,
IOException {
// ターゲットクラスのためのコンテキストを作成.
JAXBContext context = JAXBContext.newInstance(targetClass);

// 使用する Schema オブジェクトを作成.
Schema schema = getSchema();

// XML から Java オブジェクトに変換するための Unmarshaller を作成.
Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setSchema(schema);

// Java オブジェクトから XML に変換するための Marshaller を作成.
Marshaller marshaller = context.createMarshaller();
marshaller.setSchema(schema);
for (Map.Entry entry : properties.entrySet()) {
marshaller.setProperty(entry.getKey(), entry.getValue());
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
// XML に変換.
marshaller.marshal(o, output);

return new ByteArrayInputStream(output.toByteArray());
}

/* クラスと同一フォルダからスキーマを読み込む。 */
private Schema getSchema() throws SAXException {
SchemaFactory factory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
if (schemaFile != null) {
return factory.newSchema(schemaFile);
} else {
return factory.newSchema(ClassUtils.getResource(targetClass,
targetClass.getSimpleName() + ".xsd"));
}
}

}

2009年5月27日水曜日

汎用Diffライブラリ

仕事上の必要性にかられ、汎用Diffライブラリを作ってみました。



Diff関連の資料をいろいろと調べたところ、「An O(NP) Sequence Comparison Algorithm」のアルゴリズムがよさそうだったのでこれをもとに実装しています。
実装にあたっては、可読性>メモリ使用量>性能の優先順位をおいて実装しているので性能的には改善の余地があるとは思っています。ただ、自分としてはとにかくシンプルに拡張できることが重要だったのであまり凝った作りにはしていません。

実際に使う場合は、以下の図にあるDiffSourceを実装することで様々なDiffをとることができます。標準の実装として文字列用のDiffStringSourceというクラスが用意してあるので文字列のDiffであればこちらを使用してください。



以下は文字列用のDiffStringSourceを使用したときの結果イメージを示しています。図のようにDiffSequenceに開始、終了インデックスが格納されDiffSequenceのリストとして結果を取得できます。



ソースと結果イメージは次のようになります。
サンプルソース:

public class DiffTest {

public static void main(String[] args)
throws Exception
{

DiffSource s1 = new DiffStringSource("abcdefg");
DiffSource s2 = new DiffStringSource("abzge");
Diff diff = new Diff();
List diffList = diff.diff(s1, s2);
int i = 0;
for(DiffSequence seq : diffList){
if(seq instanceof CommonSequence){
System.out.println(i+":共通部分");
System.out.println(
" source("+seq.getSourceStartIndex()
+","+seq.getSourceEndIndex()
+") = "+seq.getSourceElements());
System.out.println(
" target("+seq.getTargetStartIndex()
+","+seq.getTargetEndIndex()
+") = "+seq.getTargetElements());
}else{
System.out.println(i+":差異部分");
System.out.println(
" source("+seq.getSourceStartIndex()
+","+seq.getSourceEndIndex()
+") = "+seq.getSourceElements());
System.out.println(
" target("+seq.getTargetStartIndex()
+","+seq.getTargetEndIndex()
+") = "+seq.getTargetElements());
}
i++;
}
}
}


結果:
0:共通部分
source(0,2) = ab
target(0,2) = ab
1:差異部分
source(2,4) = cd
target(2,4) = zg
2:共通部分
source(4,5) = e
target(4,5) = e
3:差異部分
source(5,7) = fg
target(5,5) =