Powered by Google App Engine

アフィリエイト

 

Amazon Product Advertising API for java SOAP版で商品検索する

Amazon Product Advertising API for java SOAP版の環境構築を行います。クラスの自動生成から商品検索まで実装します。amazonでアフィリエイトをするなら、自動生成したコードでSOAP通信しましょう。jdom等で自力でjsonやxmlをパースするのに対し、開発効率は飛躍的に向上するので是非使ってみましょう。

新サイト、tree-mapsを公開しました!!

tree-maps: 地図のWEB TOOLの事ならtree-mapsにお任せ!

地図に関するWEB TOOL専門サイトです!!

大画面で大量の緯度経度を一気にプロット、ジオコーディング、DMS<->DEGの相互変換等ができます!

◯ 広告

まずはwsimportする際に指定するxmlを作成します。

ファイル名は「jaxws-custom.xml」とし、プロジェクトの直下に配置して下さい。

<jaxws:bindings
    wsdlLocation="http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl"
    xmlns:jaxws="http://java.sun.com/xml/ns/jaxws">
    <jaxws:enableWrapperStyle>false</jaxws:enableWrapperStyle>
</jaxws:bindings>

wsdlLocationは「http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl」とします。

このURLをブラウザ開くと、以下のようなxmlが返ります。

※ 一部抜粋
targetNamespace="http://webservices.amazon.com/AWSECommerceService/2011-08-01"

日付が指定されていますね。amazon product advertising apiにはバージョンがあり、ここでは2011-08-01が指定されています。

先ほどのwsdlLocationのURLを指定すると、自動的に最新バージョンが選択されます

以下のように敢えてバージョンを指定する事もできますが、最新版が自動的に選択された方がいいので以下の指定は不要です。

wsdlLocation="http://ecs.amazonaws.com/AWSECommerceService/2011-08-11/JP/AWSECommerceService.wsdl"

以下はseasarのdoltengでプロジェクトを生成した際の構成です。

src/main 以下に「aws」フォルダを作成し、そこに自動生成ファイルを出力するようにします。

tree-api/
├── jaxws-custom.xml ★ 前項で作成しました
├── lib
├── pom.xml
├── src
│   ├── main
│   │   ├── aws ★ ここに自動生成ソースコードを出力します
│   │   ├── java
│   │   │   └── tree
│   │   │       └── api
│   │   │           ├── condition
│   │   │           ├── dto
│   │   │           └── service
│   │   ├── resources
│   │   └── webapp
│   │       └── WEB-INF
│   │           ├── classes ※ ここに自動生成した際のclassファイルを出力します
│   │           └── lib
│   └── test
├── target
# まずはプロジェクトフォルダまで移動します
tree-macpro:workspace tree$ cd /Applications/eclipse/workspace/tree-api/

# 続いてwsimportコマンドを実行します
wsimport -d ./src/main/webapp/WEB-INF/classes -s ./src/main/aws -p com.ECS.client.jax http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl -b jaxws-custom.xml .
parsing WSDL...


generating code...


compiling code...

tree-macpro:tree-api tree$ 

プロジェクトのフォルダまでcdし、wsimportコマンドを実行するだけです。

引数の -dにはclassesのパスを、-sには出力先ソースパスを、 -pにはwsdlファイルのURLを、-bにはxmlファイルのパスを指定します。

wsimportコマンドに失敗する場合、エラーメッセージが表示されます。

wsimportコマンドが成功すると、↑のようにメッセージが表示され、以下のようにsrc/main/aws以下にファイル群が大量に自動生成されます。

src/
├── main
│   ├── aws
│   │   ├── com
│   │   │   └── ECS
│   │   │       └── client
│   │   │           └── jax
│   │   │               ├── AWSECommerceService.java
│   │   │               ├── AWSECommerceServicePortType.java
│   │   │               ├── Accessories.java
│   │   │               ├── Arguments.java
│   │   │               ├── Bin.java
│   │   │               ├── BrowseNode.java
│   │   │               ├── BrowseNodeLookup.java
│   │   │               ├── BrowseNodeLookupRequest.java
│   │   │               ├── BrowseNodeLookupResponse.java
・・・ 中略 ・・・
│   │   │               ├── TopSellers.java
│   │   │               ├── Tracks.java
│   │   │               ├── VariationAttribute.java
│   │   │               ├── VariationDimensions.java
│   │   │               ├── VariationSummary.java
│   │   │               ├── Variations.java
│   │   │               └── package-info.java

2009年8月15日を境に、新認証方式である署名認証が必須となりました。

署名認証は今までのようにAWSAccessKeyIdを指定しただけでは足りません。

前項でSOAP通信のためのソースを自動生成しましたが、なんと署名認証のためのソースが含まれていません!

AWS Discussion ForumsからAwsHandlerResolverをダウンロードできるのでダウンロードします。

いつフォーラムからAwsHandlerResolver.javaが削除されるか解らないので、ソースコードを貼っておきます

※ google guavaを使ってリファクタしたり、多少いじっています。

※ commons.codecのjarが必須になっているので適宜クラスパスに通して下さい。

package tree.api.aws;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
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.SOAPBody;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.HandlerResolver;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.PortInfo;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.google.common.collect.Lists;

/**
 * SOAP用リゾルバ
 * 
 * @see https://forums.aws.amazon.com/thread.jspa?messageID=147748𢼪
 * @author tree
 * 
 */
public class AwsHandlerResolver implements HandlerResolver {

    private String awsSecretKey;

    public AwsHandlerResolver(String awsSecretKey) {
        this.awsSecretKey = awsSecretKey;
    }

    @SuppressWarnings("rawtypes")
    public List<Handler> getHandlerChain(PortInfo portInfo) {
        List<Handler> handlerChain = Lists.newArrayList();
        QName qName = portInfo.getServiceName();
        if (qName.getLocalPart().equals("AWSECommerceService"))
            handlerChain.add(new AwsHandler(awsSecretKey));
        return handlerChain;
    }

    private static class AwsHandler implements SOAPHandler<SOAPMessageContext> {

        private byte[] secretBytes;

        public AwsHandler(String awsSecretKey) {
            secretBytes = stringToUtf8(awsSecretKey);
        }

        public void close(MessageContext messagecontext) {
        }

        public Set<QName> getHeaders() {
            return null;
        }

        public boolean handleFault(SOAPMessageContext messagecontext) {
            return true;
        }

        public boolean handleMessage(SOAPMessageContext messagecontext) {
            Boolean outbound = (Boolean) messagecontext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
            if (outbound) {
                try {
                    SOAPMessage soapMessage = messagecontext.getMessage();
                    SOAPBody soapBody = soapMessage.getSOAPBody();
                    Node firstChild = soapBody.getFirstChild();

                    String timeStamp = getTimestamp();
                    String signature = getSignature(firstChild.getLocalName(), timeStamp, secretBytes);

                    appendTextElement(firstChild, "Signature", signature);
                    appendTextElement(firstChild, "Timestamp", timeStamp);
                } catch (SOAPException se) {
                    throw new RuntimeException("SOAPException was thrown.", se);
                }
            }
            return true;
        }

        private String getSignature(String operation, String timeStamp, byte[] secretBytes) {
            try {
                String toSign = operation + timeStamp;
                byte[] toSignBytes = stringToUtf8(toSign);

                Mac signer = Mac.getInstance("HmacSHA256");
                SecretKeySpec keySpec = new SecretKeySpec(secretBytes, "HmacSHA256");

                signer.init(keySpec);
                signer.update(toSignBytes);
                byte[] signBytes = signer.doFinal();

                String signature = new String(Base64.encodeBase64(signBytes));
                return signature;
            } catch (NoSuchAlgorithmException nsae) {
                throw new RuntimeException("NoSuchAlgorithmException was thrown.", nsae);
            } catch (InvalidKeyException ike) {
                throw new RuntimeException("InvalidKeyException was thrown.", ike);
            }
        }

        private String getTimestamp() {
            Calendar calendar = Calendar.getInstance();
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            return dateFormat.format(calendar.getTime());
        }

        private byte[] stringToUtf8(String source) {
            try {
                return source.getBytes("UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("getBytes threw an UnsupportedEncodingException", e);
            }
        }

        private void appendTextElement(Node node, String elementName, String elementText) {
            Element element = node.getOwnerDocument().createElement(elementName);
            element.setTextContent(elementText);
            node.appendChild(element);
        }
    }
}

このファイルは任意の場所に配置して下さい。

ここまで準備してようやく検索を実行する事ができます。

package tree.api.api;

import javax.xml.ws.BindingProvider;

import tree.api.aws.AwsHandlerResolver;

import com.ECS.client.jax.AWSECommerceService;
import com.ECS.client.jax.AWSECommerceServicePortType;
import com.ECS.client.jax.Item;
import com.ECS.client.jax.ItemSearch;
import com.ECS.client.jax.ItemSearchRequest;
import com.ECS.client.jax.ItemSearchResponse;
import com.ECS.client.jax.Items;
import com.ECS.client.jax.Request;

public class TestClient {

    public static final String AWS_ACCESS_KEY_ID = "AWSアクセスキーを指定して下さい";

    public static final String ASSOCIATE_ID = "アフィリエイトで使うアソシエイトIDを指定して下さい";

    public static final String AWS_SECRET_ACCESS_KEY = "AWSアクセスキーの秘密鍵を指定して下さい";

    public static void main(String[] args) {
        TestClient testClient = new TestClient();
        testClient.itemSearch();
    }

    private void itemSearch() {
        ItemSearchRequest itemRequest = new ItemSearchRequest();
        itemRequest.setBrowseNode("2189356051");
        itemRequest.setSearchIndex("Hobbies");
        itemRequest.setKeywords("初音ミク");

        ItemSearch itemSearch = new ItemSearch();
        itemSearch.setAWSAccessKeyId(AWS_ACCESS_KEY_ID);
        itemSearch.setAssociateTag(ASSOCIATE_ID);
        itemSearch.getRequest().add(itemRequest);

        AWSECommerceService service = new AWSECommerceService();
        // 署名認証はここで設定する
        service.setHandlerResolver(new AwsHandlerResolver(AWS_SECRET_ACCESS_KEY));
        AWSECommerceServicePortType port = service.getAWSECommerceServicePort();
        // Endpointを日本に設定する
        ((BindingProvider) port).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                "https://ecs.amazonaws.jp/onca/soap?Service=AWSECommerceService");

        ItemSearchResponse response = port.itemSearch(itemSearch);
        for (Items items : response.getItems()) {
            Request requestElement = items.getRequest();
            // エラーがある場合はエラーメッセージを表示する
            if (requestElement.getErrors() != null) {
                for (com.ECS.client.jax.Errors.Error error : requestElement.getErrors().getError()) {
                    System.out.println(error.getMessage());
                }
            }

            for (Item item : items.getItem()) {
                System.out.println("商品タイトル:" + item.getItemAttributes().getTitle());
            }
        }
    }
}

署名認証については以下で指定しています。

        // 署名認証はここで設定する
        service.setHandlerResolver(new AwsHandlerResolver(AWS_SECRET_ACCESS_KEY));

その他にも、Endpointで日本を指定しないと日本語検索できないので注意です。

具体的には以下のコードが日本語検索するために必要なコードです。

        // Endpointを日本に設定する
        ((BindingProvider) port).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                "https://ecs.amazonaws.jp/onca/soap?Service=AWSECommerceService");

TestClient.javaを実行すると、以下のように商品を検索することができます。

商品タイトル:キャラクターボーカルシリーズ01 初音ミク (1/8スケールPVC塗装済み完成品)
商品タイトル:supercell feat. 初音ミク ワールドイズマイン [ブラウンフレーム]  (1/8スケールPVC塗装済み完成品)
商品タイトル:初音ミク PMフィギュア Angel Breeze 全1種
商品タイトル:初音ミク Project DIVA プレミアムフィギュア extend
商品タイトル:figma 初音ミク
商品タイトル:キャラクター・ボーカル・シリーズ01 初音ミク ねんどろいど ミクダヨー (ノンスケール ABS&PVC塗装済み可動フィギュア)
商品タイトル:初音ミク Lat式Ver. (1/8スケール PVC塗装済み完成品)
商品タイトル:RACINGミク 2011ver. (1/8スケール PVC塗装済み完成品)
商品タイトル:初音ミク PMフィギュア Angel Breeze
商品タイトル:カプセル 新千歳空港限定 北海道フィギュアみやげ 雪ミク単品

基本的にhttpステータスコードが200以外の場合、認証に関するエラーで、検索は実行できていません。

以下のように400が返る時は認証に失敗しています

大抵は署名認証をしていない場合に発生するので、AwsHandlerResolverで署名認証しましょう。

Exception in thread "main" com.sun.xml.internal.ws.client.ClientTransportException: The server sent HTTP status code 400: Bad Request
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.checkStatusCode(HttpTransportPipe.java:196)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:168)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:83)
	at com.sun.xml.internal.ws.transport.DeferredTransportPipe.processRequest(DeferredTransportPipe.java:105)
	at com.sun.xml.internal.ws.api.pipe.Fiber.__doRun(Fiber.java:587)
	at com.sun.xml.internal.ws.api.pipe.Fiber._doRun(Fiber.java:546)
	at com.sun.xml.internal.ws.api.pipe.Fiber.doRun(Fiber.java:531)
	at com.sun.xml.internal.ws.api.pipe.Fiber.runSync(Fiber.java:428)
	at com.sun.xml.internal.ws.client.Stub.process(Stub.java:211)
	at com.sun.xml.internal.ws.client.sei.SEIStub.doProcess(SEIStub.java:124)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:98)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:78)
	at com.sun.xml.internal.ws.client.sei.SEIStub.invoke(SEIStub.java:107)
	at $Proxy29.itemSearch(Unknown Source)
	at tree.api.api.TestClient.test1(TestClient.java:47)
	at tree.api.api.TestClient.main(TestClient.java:26)

以下のように403が返る時は認証に失敗しています

大抵はAWSAccessKeyIdを設定し忘れているだけなので、itemSearch.setAWSAccessKeyIdで指定しましょう。

Exception in thread "main" com.sun.xml.internal.ws.client.ClientTransportException: The server sent HTTP status code 403: Forbidden
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.checkStatusCode(HttpTransportPipe.java:196)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:168)
	at com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:83)
	at com.sun.xml.internal.ws.transport.DeferredTransportPipe.processRequest(DeferredTransportPipe.java:105)
	at com.sun.xml.internal.ws.api.pipe.Fiber.__doRun(Fiber.java:587)
	at com.sun.xml.internal.ws.api.pipe.Fiber._doRun(Fiber.java:546)
	at com.sun.xml.internal.ws.api.pipe.Fiber.doRun(Fiber.java:531)
	at com.sun.xml.internal.ws.api.pipe.Fiber.runSync(Fiber.java:428)
	at com.sun.xml.internal.ws.client.Stub.process(Stub.java:211)
	at com.sun.xml.internal.ws.client.sei.SEIStub.doProcess(SEIStub.java:124)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:98)
	at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:78)
	at com.sun.xml.internal.ws.client.sei.SEIStub.invoke(SEIStub.java:107)
	at $Proxy29.itemSearch(Unknown Source)
	at tree.api.api.TestClient.test1(TestClient.java:47)
	at tree.api.api.TestClient.main(TestClient.java:26)

httpステータスコードは200(正常終了)が返り、エラーメッセージが返ります。

例えば存在しないSearchIndexをした場合は、以下のようなエラーメッセージが返ります。

SearchIndexに指定した値は無効です。[
    'All','Apparel','Appliances','Automotive','Baby','Beauty','Blended','Books','Classical','DVD','Electronics','ForeignBooks','Grocery','HealthPersonalCare','Hobbies','HomeImprovement','Jewelry','KindleStore','Kitchen','Marketplace','MobileApps','Music','MP3Downloads','MusicalInstruments','MusicTracks','OfficeProducts','PCHardware','PetSupplies','Shoes','Software','SportingGoods','Toys','VHS','Video','VideoGames','Watches'
]などが有効な値の例です。
◯ 広告