2013-12-25

apexからDynamoDB触ってみる。

巷で有名なDynamoDBさんにApexから触ってみました。

Signature Version 4はElastic Transcoderのときにやったのでそのまま流用。

リファレンスは以下↓

で、出来上がったのはこんな感じ。
/**
 * DynamoDBコモンクラス
 */
public with sharing class DynamoDBCommon {
    private final String SERVICE = 'dynamodb';

    /**
     * Access Key Id
     */
    private String AWSAccessKeyId = EnvSetting__c.getOrgDefaults().AWSAccessKeyID__c;

    /**
     * Access Key Secret
     */
    private String AWSAccessKeySecret = EnvSetting__c.getOrgDefaults().AWSAccessKeySecret__c;

    /**
     * table情報を取得
     */
    public String putItem(
        String region,
        String tableName,
        Map<String, Object> valueMap
    ) {
        Map<String, String> params = new Map<String, String>{

        };

        List<String> signedHeaders = new List<String>{
            'host', 
            'x-amz-date',
            'content-length',
            'content-type',
            'x-amz-target'
        };

        DateTime dt = DateTime.now();
        String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + 
                                 region + '/' + SERVICE + '/aws4_request';

        //Body
        Map<String, Object> jsonMap = new Map<String, Object>{
            'Item' => valueMap,
            'TableName' => tableName
        };
        String jsonBody = Json.serialize(jsonMap);

        String stringToSign = this.createStringToSign(
            'AWS4-HMAC-SHA256',
            dt, 
            credentialScope, 
            this.createHashedCanonicalRequest(
                'SHA256',
                'POST',
                '/',
                params,
                new Map<String, String>{
                    'Host' => 'dynamodb.' + region + '.amazonaws.com',
                    'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + 
                                    dt.formatGmt('HHmmss') + 'Z',
                    'content-length' => String.valueOf(jsonBody.length()),
                    'content-type' => 'application/x-amz-json-1.0',
                    'x-amz-target' => 'DynamoDB_20120810.PutItem'
                },
                signedHeaders,
                jsonBody
            )
        );

        String signature = this.createSignature(
            'hmacSHA256',
            dt,
            region,
            SERVICE,
            Blob.valueOf(stringToSign)
        );

        return this.callCommonDDBCall(
            dt,
            'dynamodb.' + region + '.amazonaws.com',
            credentialScope,
            signedHeaders,
            signature,
            'DynamoDB_20120810.PutItem',
            jsonBody
        );
    }

    /**
     * table情報を取得
     */
    public String describeTable(
        String region,
        String tableName
    ) {
        Map<String, String> params = new Map<String, String>{

        };

        List<String> signedHeaders = new List<String>{
            'host', 
            'x-amz-date',
            'content-length',
            'content-type',
            'x-amz-target'
        };

        DateTime dt = DateTime.now();
        String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + 
                                 region + '/' + SERVICE + '/aws4_request';

        //Body
        Map<String, Object> jsonMap = new Map<String, Object>{
            'TableName' => tableName
        };
        String jsonBody = Json.serialize(jsonMap);

        String stringToSign = this.createStringToSign(
            'AWS4-HMAC-SHA256',
            dt, 
            credentialScope, 
            this.createHashedCanonicalRequest(
                'SHA256',
                'POST',
                '/',
                params,
                new Map<String, String>{
                    'Host' => 'dynamodb.' + region + '.amazonaws.com',
                    'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + 
                                    dt.formatGmt('HHmmss') + 'Z',
                    'content-length' => String.valueOf(jsonBody.length()),
                    'content-type' => 'application/x-amz-json-1.0',
                    'x-amz-target' => 'DynamoDB_20120810.DescribeTable'
                },
                signedHeaders,
                jsonBody
            )
        );

        String signature = this.createSignature(
            'hmacSHA256',
            dt,
            region,
            SERVICE,
            Blob.valueOf(stringToSign)
        );

        return this.callCommonDDBCall(
            dt,
            'dynamodb.' + region + '.amazonaws.com',
            credentialScope,
            signedHeaders,
            signature,
            'DynamoDB_20120810.DescribeTable',
            jsonBody
        );
    }

    /**
     * tableを作成
     */
    public String createTable(
        String region,
        String tableName,
        String keyName
    ) {
        Map<String, String> params = new Map<String, String>{

        };

        List<String> signedHeaders = new List<String>{
            'host', 
            'x-amz-date',
            'content-length',
            'content-type',
            'x-amz-target'
        };

        DateTime dt = DateTime.now();
        String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + 
                                 region + '/' + SERVICE + '/aws4_request';

        //Body
        Map<String, Object> jsonMap = new Map<String, Object>{
            'AttributeDefinitions' => new List<Map<String, Object>>{
            	new Map<String, Object>{
            		'AttributeName' => keyName,
                    'AttributeType' => 'S'
            	}
            },
            'KeySchema' => new List<Map<String, Object>>{
                new Map<String, Object>{
                    'AttributeName' => keyName,
                    'KeyType' => 'HASH'
                }
            },
            'ProvisionedThroughput' => new Map<String, Object>{
            	'ReadCapacityUnits' => 1,
            	'WriteCapacityUnits' => 1
            },
            'TableName' => tableName
        };
        String jsonBody = Json.serialize(jsonMap);

        String stringToSign = this.createStringToSign(
            'AWS4-HMAC-SHA256',
            dt, 
            credentialScope, 
            this.createHashedCanonicalRequest(
                'SHA256',
                'POST',
                '/',
                params,
                new Map<String, String>{
                    'Host' => 'dynamodb.' + region + '.amazonaws.com',
                    'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + 
                                    dt.formatGmt('HHmmss') + 'Z',
                    'content-length' => String.valueOf(jsonBody.length()),
                    'content-type' => 'application/x-amz-json-1.0',
                    'x-amz-target' => 'DynamoDB_20120810.CreateTable'
                },
                signedHeaders,
                jsonBody
            )
        );

        String signature = this.createSignature(
            'hmacSHA256',
            dt,
            region,
            SERVICE,
            Blob.valueOf(stringToSign)
        );

        return this.callCommonDDBCall(
            dt,
            'dynamodb.' + region + '.amazonaws.com',
            credentialScope,
            signedHeaders,
            signature,
            'DynamoDB_20120810.CreateTable',
            jsonBody
        );
    }

    /**
     * AETへのAPIコールの共通メソッド
     */
    private String callCommonDDBCall(
        DateTime dt,
        String hostname,
        String credentialScope,
        List<String> signedHeaders,
        String signature,
        String target,
        String jsonBody
    ) {
        HttpRequest req = new HttpRequest();
        req.setHeader('Host', hostname);
        req.setHeader(
            'x-amz-date', 
            dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z'
        );

        req.setHeader('Content-Type', 'application/x-amz-json-1.0');
        req.setHeader('x-amz-target', target);
        req.setHeader('Content-Length', String.valueOf(jsonBody.length()));

        req.setHeader(
            'Authorization',
            'AWS4-HMAC-SHA256 Credential=' + this.AWSAccessKeyId + '/' + credentialScope + ',' +
            'SignedHeaders=' + String.join(Utility.getLowerCaseSortedList(signedHeaders), ';') + ',' + 
            'Signature=' + signature
        );
        req.setEndpoint('https://' + hostname + '/');
        req.setMethod('POST');

        req.setBody(jsonBody);

        Http http = new Http();
        HTTPResponse res = http.send(req);
        return res.getBody();
    }

    /**
     * HashedCanonicalRequestを作成
     */
    private String createHashedCanonicalRequest(
        String algorithm,
        String method,
        String url,
        Map<String, String> params,
        Map<String, String> headers,
        List<String> signedHeaders,
        String payload
    ) {     
        String signature = '';
        signature += method + Constants.LF;
        signature += url + Constants.LF;
        signature += Utility.getSortedParam(params) + Constants.LF;
        signature += Utility.createCanonicalHeaders(headers) + Constants.LF;
        signature += String.join(Utility.getLowerCaseSortedList(signedHeaders), ';') + Constants.LF;
        signature += EncodingUtil.convertToHex(
                        Crypto.generateDigest(
                            algorithm,
                            Blob.valueOf(payload)
                        )
                     );
        system.debug(signature);
        return EncodingUtil.convertToHex(
            Crypto.generateDigest(
                algorithm, 
                Blob.valueOf(signature)
            )
        ).toLowerCase();
    }

    /**
     * StringToSignを作成
     */
    private String createStringToSign(
        String algorithm,
        DateTime requestDateTime,
        String credentialScope,
        String hashedCanonicalRequest
    ) {
        String stringToSign = algorithm + Constants.LF;
        stringToSign += 
           requestDateTime.formatGmt('YYYYMMdd') + 'T' + 
           requestDateTime.formatGmt('HHmmss') + 'Z' + Constants.LF;
        stringToSign += credentialScope + Constants.LF;
        stringToSign += hashedCanonicalRequest;
        return stringToSign;        
    }

    private String createSignature(
        String algorithm,
        DateTime requestDateTime,
        String region,
        String service,
        Blob stringToSign
    ) {
        system.debug(stringToSign.toString());
        Blob kDate = Crypto.generateMac(
            algorithm,
            Blob.valueOf(requestDateTime.formatGmt('YYYYMMdd')),
            Blob.valueOf('AWS4' + this.AWSAccessKeySecret)
        );
        Blob kRegion = Crypto.generateMac(
            algorithm,
            Blob.valueOf(region),
            kDate
        );
        Blob kService = Crypto.generateMac(
            algorithm,
            Blob.valueOf(service),
            kRegion
        );

        Blob kSigning = Crypto.generateMac(
            algorithm,
            Blob.valueOf('aws4_request'),
            kService
        );

        return EncodingUtil.convertToHex(
            Crypto.generateMac(
                algorithm,
                stringToSign,
                kSigning
            )
        );
    }
}

/**
 * Utilityクラス
 */
public without sharing class Utility {
    /**
     * key=value&...でつないだ文字列を返却
     * @param mapParam 対象文字列=>文字列マップ
     * @return key=value&...でつないだ文字列
     */
    public static String getSortedParam(Map<String,String> mapParam){
        if (mapParam == null || mapParam.keySet().isEmpty()) {
            return '';
        }
        String param = '';
        List<String> sortedKey = getSortedList(mapParam.keySet());
        for(String key: sortedKey){
            param += percentEncodeRfc3986(key) + '=' + percentEncodeRfc3986(mapParam.get(key)) + '&';
        }
        return param.substring(0,param.length() - 1);
    }

    public static String percentEncodeRfc3986(String s) {
        return EncodingUtil.urlEncode(s, 'UTF-8')
                    .replace('+', '%20')
                      .replace('*', '%2A')    
                      .replace('%7E', '~');
    }

    public static String createCanonicalHeaders(Map<String, String> headers) {
    	if (headers == null || headers.keySet().isEmpty()) {
            return '';
        }

        Map<String, String> lowerCaseHeaders = new Map<String, String>();
    	for (String key : headers.keySet()) {
    		lowerCaseHeaders.put(key.toLowerCase(), headers.get(key));
    	}
    	String param = '';
        List<String> sortedKey = getSortedList(lowerCaseHeaders.keySet());
        for(String key: sortedKey){
            param += key + ':' + lowerCaseHeaders.get(key).trim() + '\n';
        }
        return param;
    }

    public static List<String> getLowerCaseSortedList(List<String> keys) {
    	List<String> sortedKeys = new List<String>();
        for (String key : keys) {
            sortedKeys.add(key.toLowerCase());
        }
        sortedKeys.sort();
        return sortedKeys;
    }
}

実は前回のElastic Transcoderのサンプルがちょっと間違っていたので修正しました。signedHeadersのソートがされてなかった…。

それ以外はBodyにJSON投げるところぐらいしか違いはないです。 サンプルではCreateTable, DescribeTable, PutItemの3種類の操作をしてます。

 

DynamoDBとSalesforceとの連携の可能性に関してですが、100MBまで無料なので分析スナップショット等のアナリティクス系オブジェクトの格納先として使えるかも(?)。

分析スナップショット→月次バッチでDynamoにインサート→別Webアプリで分析 とか。

DynamoDBは勉強中(っていうかNoSQL自体知らないに等しいw)なので、もうちょっと理解してからSalesforceとの連携を考えたいっす。

このエントリーをはてなブックマークに追加