Java AmazonAPIを使って検索したものをとってくる

したいこと

書籍のジャンルの中で、「ペン」と調べた時に出てくる書籍のタイトルを表示させる。




サンプルコード

ItemLookupSample.java

package test;

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

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

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

public class ItemLookupSample {

    private static final String AWS_ACCESS_KEY_ID = "アクセスキー";
    private static final String AWS_SECRET_KEY = "シークレットキー";
    private static final String ASSOCIATE_TAG = "";
    private static final String ENDPOINT = "ecs.amazonaws.jp";

    public static void main(String[] args) {
        /*
         * Set up the signed requests helper 
         */
        SignedRequestsHelper helper;
        try {
            helper = SignedRequestsHelper.getInstance(ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_KEY, ASSOCIATE_TAG);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        
        String requestUrl = null;
        String title = null;

        System.out.println("Map form example:");
        Map<String, String> params = new HashMap<String, String>();
        params.put("Service", "AWSECommerceService");
        params.put("Version", "2009-03-31");
        params.put("Operation", "ItemSearch");
        params.put("ResponseGroup", "Small");
        params.put("AssociateTag", ASSOCIATE_TAG);
        params.put("SearchIndex", "Books");
        params.put("Keywords", "ペン");

        requestUrl = helper.sign(params);
        System.out.println("Signed Request is \"" + requestUrl + "\"");

        title = fetchTitle(requestUrl);
        System.out.println("Signed Title is \"" + title + "\"");
        System.out.println();

    }

    
    private static String fetchTitle(String requestUrl) {
        String titles = null;
        
            try {
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                DocumentBuilder db = dbf.newDocumentBuilder();
                Document doc = db.parse(requestUrl);
            
                NodeList titleNodes = doc.getElementsByTagName("Title");
                int titleNum = titleNodes.getLength();
                
                for(int i=0; i < titleNum; i++) {
                    Node titleNode = titleNodes.item(i);
                    String title = titleNode.getTextContent();
                    System.out.println(title);
                }
                
                

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

}




結果

Pen(ペン) 2016年 11/15 号 [ハリー・ポッター完全読本。]
Pen(ペン) 2016年 11/1号 [ゴッホ、君は誰?]
ペン太のこと(8): イブニング
【音声ペン付き】ペンがおしゃべり!  英検に役立つ 小学えいご絵じてん800 改訂版
30日できれいな字が書けるペン字練習帳 (TJMOOK)
【音声ペン付き】ペンがおしゃべり! ベビー&キッズえいご絵じてん500 改訂版
ワンランク上の美文字が書ける!!  極める!  ペン字・筆文字練習帳 (COSMIC MOOK)
Pen(ペン) 2016年 12/1 号 [雑誌]
【音声ペン付き】ペンがおしゃべり!  えいご絵じてんプレミアムセット 改訂版
大人の筆ペン字練習帳 春夏秋冬、季節のおたより篇
Signed Title is "null"




注意点

リクエスト制限が1時間に3600回以下らしいので
設計に工夫が必要になるね〜

列挙型 enum

あらかじめ要素数がわかっている定数の集合であるときにenumを使うといいらしい。

メリットはソースが読みやすくなる。



宣言方法

クラス修飾子 enum クラス名 {
        列挙定数, 列挙定数, ...
}

列挙定数は大文字で書く。
クラス修飾子は、piblicprivatestaticは使えるが、
abstractfinalは使えない。

javaでAmozon Product Advertising API

Amozon Product Advertising APIを使ってみる。

  1. Product Advertising API Signed Requests Sample Code - Java REST/QUERY : Sample Code & Libraries : Amazon Web Services
    ここからサンプルコードをダウンロードする。

  2. Codec – Home
    このサイトのReleasesにある
    Codec 1.10 (mirrors) requires Java 1.6を選択(最新のもの)。
    commons-codec-1.10-bin.zipをダウンロードする。

  3. commons-codec-1.9.jar
    commons-codec-1.9-javadoc.jar
    commons-codec-1.9-sources.jar
    eclipseに入れる。



導入方法→commons-langを入れる方法 - プログラミングMEMO




エラー

titleがnullになってしまいエラーが出る。



解決方法

AssociateTagがないのが原因みたい?

AssociateTagはアソシエイトIDの事。
AssociateTagが必要なところに追加してみたら解決した。



ItemLookupSample.java

package test;

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

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

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

/*
 * This class shows how to make a simple authenticated ItemLookup call to the
 * Amazon Product Advertising API.
 * 
 * See the README.html that came with this sample for instructions on
 * configuring and running the sample.
 */
public class ItemLookupSample {
    /*
     * Your AWS Access Key ID, as taken from the AWS Your Account page.
     */
    private static final String AWS_ACCESS_KEY_ID = "自分のID";

    /*
     * Your AWS Secret Key corresponding to the above ID, as taken from the AWS
     * Your Account page.
     */
    private static final String AWS_SECRET_KEY = "自分のID";
    
    private static final String ASSOCIATE_TAG = "自分のID-22"; //ここに追加

    /*
     * Use one of the following end-points, according to the region you are
     * interested in:
     * 
     *      US: ecs.amazonaws.com 
     *      CA: ecs.amazonaws.ca 
     *      UK: ecs.amazonaws.co.uk 
     *      DE: ecs.amazonaws.de 
     *      FR: ecs.amazonaws.fr 
     *      JP: ecs.amazonaws.jp
     * 
     */
    private static final String ENDPOINT = "ecs.amazonaws.jp";

    /*
     * The Item ID to lookup. The value below was selected for the US locale.
     * You can choose a different value if this value does not work in the
     * locale of your choice.
     */
    private static final String ITEM_ID = "0545010225";

    public static void main(String[] args) {
        /*
         * Set up the signed requests helper 
         */
        SignedRequestsHelper helper;
        try {
            helper = SignedRequestsHelper.getInstance(ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_KEY, ASSOCIATE_TAG);  //ここに追加
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        
        String requestUrl = null;
        String title = null;

        /* The helper can sign requests in two forms - map form and string form */
        
        /*
         * Here is an example in map form, where the request parameters are stored in a map.
         */
        System.out.println("Map form example:");
        Map<String, String> params = new HashMap<String, String>();
        params.put("Service", "AWSECommerceService");
        params.put("Version", "2009-03-31");
        params.put("Operation", "ItemLookup");
        params.put("ItemId", ITEM_ID);
        params.put("ResponseGroup", "Small");
        params.put("AssociateTag", ASSOCIATE_TAG); //ここに追加

        requestUrl = helper.sign(params);
        System.out.println("Signed Request is \"" + requestUrl + "\"");

        title = fetchTitle(requestUrl);
        System.out.println("Signed Title is \"" + title + "\"");
        System.out.println();

        /* Here is an example with string form, where the requests parameters have already been concatenated
         * into a query string. */
        System.out.println("String form example:");
        String queryString = "Service=AWSECommerceService&Version=2009-03-31&Operation=ItemLookup&ResponseGroup=Small&ItemId="
                + ITEM_ID;
        requestUrl = helper.sign(queryString);
        System.out.println("Request is \"" + requestUrl + "\"");

        title = fetchTitle(requestUrl);
        System.out.println("Title is \"" + title + "\"");
        System.out.println();

    }

    /*
     * Utility function to fetch the response from the service and extract the
     * title from the XML.
     */
    private static String fetchTitle(String requestUrl) {
        String title = null;
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(requestUrl);
            
            Node titleNode = doc.getElementsByTagName("Title").item(0);
            title = titleNode.getTextContent();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return title;
    }

}



SignedRequestsHelper.java

package test;

import java.io.UnsupportedEncodingException;

import java.net.URLDecoder;
import java.net.URLEncoder;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

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

/**
 * This class contains all the logic for signing requests
 * to the Amazon Product Advertising API.
 */
public class SignedRequestsHelper {
    /**
     * All strings are handled as UTF-8
     */
    private static final String UTF8_CHARSET = "UTF-8";
    
    /**
     * The HMAC algorithm required by Amazon
     */
    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
    
    /**
     * This is the URI for the service, don't change unless you really know
     * what you're doing.
     */
    private static final String REQUEST_URI = "/onca/xml";
    
    /**
     * The sample uses HTTP GET to fetch the response. If you changed the sample
     * to use HTTP POST instead, change the value below to POST. 
     */
    private static final String REQUEST_METHOD = "GET";

    private String endpoint = null;
    private String awsAccessKeyId = null;
    private String awsSecretKey = null;
    private String associateTag = null; //ここに追加

    private SecretKeySpec secretKeySpec = null;
    private Mac mac = null;

    /**
     * You must provide the three values below to initialize the helper.
     *  
     * @param endpoint          Destination for the requests.
     * @param awsAccessKeyId    Your AWS Access Key ID
     * @param awsSecretKey      Your AWS Secret Key
     * @param associateTag      Your Associate Tag //ここに追加
     */
    public static SignedRequestsHelper getInstance(
            String endpoint, 
            String awsAccessKeyId, 
            String awsSecretKey,
            String associateTag //ここに追加
    ) throws IllegalArgumentException, UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException
    {
        if (null == endpoint || endpoint.length() == 0)
            { throw new IllegalArgumentException("endpoint is null or empty"); }
        if (null == awsAccessKeyId || awsAccessKeyId.length() == 0) 
            { throw new IllegalArgumentException("awsAccessKeyId is null or empty"); }
        if (null == awsSecretKey || awsSecretKey.length() == 0)   
            { throw new IllegalArgumentException("awsSecretKey is null or empty"); }
        
        SignedRequestsHelper instance = new SignedRequestsHelper();
        instance.endpoint = endpoint.toLowerCase();
        instance.awsAccessKeyId = awsAccessKeyId;
        instance.awsSecretKey = awsSecretKey;
        instance.associateTag = associateTag; //ここに追加

        byte[] secretyKeyBytes = instance.awsSecretKey.getBytes(UTF8_CHARSET);
        instance.secretKeySpec = new SecretKeySpec(secretyKeyBytes, HMAC_SHA256_ALGORITHM);
        instance.mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        instance.mac.init(instance.secretKeySpec);

        return instance;
    }
    
    /**
     * The construct is private since we'd rather use getInstance()
     */
    private SignedRequestsHelper() {}

    /**
     * This method signs requests in hashmap form. It returns a URL that should
     * be used to fetch the response. The URL returned should not be modified in
     * any way, doing so will invalidate the signature and Amazon will reject
     * the request.
     */
    public String sign(Map<String, String> params) {
        // Let's add the AWSAccessKeyId and Timestamp parameters to the request.
        params.put("AWSAccessKeyId", this.awsAccessKeyId);
        params.put("Timestamp", this.timestamp());
        params.put("AssociateTag", this.associateTag);

        // The parameters need to be processed in lexicographical order, so we'll
        // use a TreeMap implementation for that.
        SortedMap<String, String> sortedParamMap = new TreeMap<String, String>(params);
        
        // get the canonical form the query string
        String canonicalQS = this.canonicalize(sortedParamMap);
        
        // create the string upon which the signature is calculated 
        String toSign = 
            REQUEST_METHOD + "\n" 
            + this.endpoint + "\n"
            + REQUEST_URI + "\n"
            + canonicalQS;

        // get the signature
        String hmac = this.hmac(toSign);
        String sig = this.percentEncodeRfc3986(hmac);

        // construct the URL
        String url = 
            "http://" + this.endpoint + REQUEST_URI + "?" + canonicalQS + "&Signature=" + sig;

        return url;
    }

    /**
     * This method signs requests in query-string form. It returns a URL that
     * should be used to fetch the response. The URL returned should not be
     * modified in any way, doing so will invalidate the signature and Amazon
     * will reject the request.
     */
    public String sign(String queryString) {
        // let's break the query string into it's constituent name-value pairs
        Map<String, String> params = this.createParameterMap(queryString);
        
        // then we can sign the request as before
        return this.sign(params);
    }

    /**
     * Compute the HMAC.
     *  
     * @param stringToSign  String to compute the HMAC over.
     * @return              base64-encoded hmac value.
     */
    private String hmac(String stringToSign) {
        String signature = null;
        byte[] data;
        byte[] rawHmac;
        try {
            data = stringToSign.getBytes(UTF8_CHARSET);
            rawHmac = mac.doFinal(data);
            Base64 encoder = new Base64();
            signature = new String(encoder.encode(rawHmac));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
        }
        return signature;
    }

    /**
     * Generate a ISO-8601 format timestamp as required by Amazon.
     *  
     * @return  ISO-8601 format timestamp.
     */
    private String timestamp() {
        String timestamp = null;
        Calendar cal = Calendar.getInstance();
        DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
        timestamp = dfm.format(cal.getTime());
        return timestamp;
    }

    /**
     * Canonicalize the query string as required by Amazon.
     * 
     * @param sortedParamMap    Parameter name-value pairs in lexicographical order.
     * @return                  Canonical form of query string.
     */
    private String canonicalize(SortedMap<String, String> sortedParamMap) {
        if (sortedParamMap.isEmpty()) {
            return "";
        }

        StringBuffer buffer = new StringBuffer();
        Iterator<Map.Entry<String, String>> iter = sortedParamMap.entrySet().iterator();

        while (iter.hasNext()) {
            Map.Entry<String, String> kvpair = iter.next();
            buffer.append(percentEncodeRfc3986(kvpair.getKey()));
            buffer.append("=");
            buffer.append(percentEncodeRfc3986(kvpair.getValue()));
            if (iter.hasNext()) {
                buffer.append("&");
            }
        }
        String cannoical = buffer.toString();
        return cannoical;
    }

    /**
     * Percent-encode values according the RFC 3986. The built-in Java
     * URLEncoder does not encode according to the RFC, so we make the
     * extra replacements.
     * 
     * @param s decoded string
     * @return  encoded string per RFC 3986
     */
    private String percentEncodeRfc3986(String s) {
        String out;
        try {
            out = URLEncoder.encode(s, UTF8_CHARSET)
                .replace("+", "%20")
                .replace("*", "%2A")
                .replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            out = s;
        }
        return out;
    }

    /**
     * Takes a query string, separates the constituent name-value pairs
     * and stores them in a hashmap.
     * 
     * @param queryString
     * @return
     */
    private Map<String, String> createParameterMap(String queryString) {
        Map<String, String> map = new HashMap<String, String>();
        String[] pairs = queryString.split("&");

        for (String pair: pairs) {
            if (pair.length() < 1) {
                continue;
            }

            String[] tokens = pair.split("=",2);
            for(int j=0; j<tokens.length; j++)
            {
                try {
                    tokens[j] = URLDecoder.decode(tokens[j], UTF8_CHARSET);
                } catch (UnsupportedEncodingException e) {
                }
            }
            switch (tokens.length) {
                case 1: {
                    if (pair.charAt(0) == '=') {
                        map.put("", tokens[0]);
                    } else {
                        map.put(tokens[0], "");
                    }
                    break;
                }
                case 2: {
                    map.put(tokens[0], tokens[1]);
                    break;
                }
            }
        }
        return map;
    }
}




参照:Amazon Product Advertising API を利用して世界各国の商品価格差リサーチツールを作ってみる | imagination-i

Java 基本の動き

Mainクラス

public class Main {
    public static void main(String[] args) {    //①
        Hoge moji = new Hoge();    //②
        moji.setMoji("ラーメン");    //③

        System.out.println(moji.getMoji());    //⑦
    }
}



Hogeクラス

public class Hoge{

    private String moji;    //⑥
                   ˜˜˜˜
    public void setMoji(String moji) {    //④
                               ¯¯¯¯
        this.moji = moji;    
             ˜˜˜˜   ¯¯¯¯
    }

    public String getMoji() {
        return moji;    //⑦
               ˜˜˜˜
    }
}



①実行するとMainクラスのmain()メソッドが動く。
②main()メソッドでHogeクラスをインスタンス化(new)する。
③変数mojiのsetMoji()メソッドにラーメンを引数とする。

HogeクラスのsetMoji(String moji)にラーメンが渡される。
⑤ラーメンが入っている変数mojiがthis.mojiに代入される。
this.mojiとフィールドのmojiが同じだからそれもラーメンということ。

⑦MainクラスのSystem.out.println(moji.getMoji());で変数moji(Hogeクラス)のgetMojiを呼び出しているため、 ラーメンが表示されることになる。




コンストラクタを作成する場合

Mainクラス

public class Main {
    public static void main(String[] args) {    //①
        Hoge moji = new Hoge("冷麺");    //②

        System.out.println(moji.getMoji());    //⑥
    }
}



Hogeクラス

public class Hoge{

    private String moji;    //⑤
                   ˜˜˜˜
    public Hoge(String moji) {    //③コンストラクタ
                       ¯¯¯¯
        this.moji = moji;    //④
             ˜˜˜˜   ¯¯¯¯
    }

    public String getMoji() {    //⑥
        return moji;    //⑥
               ˜˜˜˜
    }
}



①実行するとMainクラスのmain()メソッドが動く。
②main()メソッドでHogeクラスをインスタンス化(new)する。
冷麺を引数とする。

③コンストラクタのpublic Hoge(String moji)に冷麺が渡される。
④コンストラクタ冷麺が入っている変数mojiがthis.mojiに代入される。
⑤this.mojiとフィールドのmojiが同じだからそれもラーメンということ。

⑥MainクラスのSystem.out.println(moji.getMoji());で変数moji(Hogeクラス)のgetMojiを呼び出しているため、 冷麺が表示されることになる。

Eclipseの実行ログを見る方法

cd ~/Development/salt-inc/zero-one/zo-api/zo-tools/ で移動。

zo-tools.ymlファイルにある、appenders:archivedLogFilenamePattern:に書いてあるディレクトリをコピー。

tail -n 行数 /var/apps/logs/zo-tools.log(さっきコピったやつ)

を打てば実行ログが見れる。

Eclipseで実行できない問題

問題

EclipseでApplicationを実行しようとすると、

usage: java -jar project.jar [-h] [-v] {server,check} ...

positional arguments:
  {server,check}         available commands

optional arguments:
  -h, --help             show this help message and exit
  -v, --version          show the application version and exit

とコンソールに出てきて実行できなかった。




解決方法

  1. Run > DebugConfigurationsに移動。

  2. ArgumentsのProgram argumentsにymlファイルのディレクトリを書き込む。  今回は、server /Users/nanako/Development/salt-inc/zero-one/zo-api/zo-tools/zo-tools.yml

  3. ApplyをおしてDebugすると実行された。