DynamoDBデータ型記述子付きJSONをPOJOに変換する方法

[ref_low]:https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
[ref_type]:https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html#Programming.LowLevelAPI.DataTypeDescriptors
[ref_capture]:https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/streamsmain.html
[ref_mapper]:https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBMapper.html

## ユースケース

[Amazon DynamoDB の変更データキャプチャ][ref_capture]を通したり、Amazon Kinesis Data FirehoseでS3保存したりすると[DynamoDB 低レベル API][ref_low] のフォーマットで取り扱うことになります。

<details><summary>低レベルフォーマット例</summary><div>

```json
{
    "Age": {"N": "8"},
    "Colors": {
        "L": [
            {"S": "White"},
            {"S": "Brown"},
            {"S": "Black"}
        ]
    },
    "Name": {"S": "Fido"},
    "Vaccinations": {
        "M": {
            "Rabies": {
                "L": [
                    {"S": "2009-03-17"},
                    {"S": "2011-09-21"},
                    {"S": "2014-07-08"}
                ]
            },
            "Distemper": {"S": "2015-10-13"}
        }
    },
    "Breed": {"S": "Beagle"},
    "AnimalType": {"S": "Dog"}
}
```
</div></details>

しかし、JavaではSDKより[DynamoDBMapper][ref_mapper]という高レベルAPIが提供されており、それに合わせてPOJOを用意してることが多いかと思います。
低レベルAPIのフォーマットは「JSONなので、、、」と安直にJackson等でこのPOJOをそのまま使おうとすると「[データ型記述子(en:Data Type Descriptors)][ref_type]]」が邪魔になり扱えません。
そういった状況への対応の１つです。

## 方法

DynamoDBMapperで使える形式に変換してから渡す。

### 注釈
* 今回は単一レコードを処理対象とします。
* SomePOJO.classはDynamoDBMapperで使えるPOJOとして定義されてるものとします。
* dynamoDBMapperは任意のconfigを施したものを定義したものとします。

```java:import類
import static com.amazonaws.protocol.json.SdkStructuredPlainJsonFactory.JSON_CUSTOM_TYPE_UNMARSHALLERS;
import static com.amazonaws.protocol.json.SdkStructuredPlainJsonFactory.JSON_FACTORY;
import static com.amazonaws.protocol.json.SdkStructuredPlainJsonFactory.JSON_SCALAR_UNMARSHALLERS;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Map;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.transform.AttributeValueJsonUnmarshaller;
import com.amazonaws.transform.JsonUnmarshallerContext;
import com.amazonaws.transform.JsonUnmarshallerContextImpl;
import com.amazonaws.transform.MapUnmarshaller;
import com.fasterxml.jackson.core.JsonParser;
```

```java:処理部（メソッドの中）
//①
InputStream src = new ByteArrayInputStream("生JSON".getBytes());

//②
JsonParser jsonParser = JSON_FACTORY.createParser(src);

//③
JsonUnmarshallerContext context = new JsonUnmarshallerContextImpl(
	jsonParser,
	JSON_SCALAR_UNMARSHALLERS, JSON_CUSTOM_TYPE_UNMARSHALLERS,
	null
);

//④
MapUnmarshaller<String, AttributeValue> mapUnmarshaller = new MapUnmarshaller<>(
	context.getUnmarshaller(String.class),
	AttributeValueJsonUnmarshaller.getInstance()
);

//==========================================================
//⑤
Map<String, AttributeValue> itemAttributes = mapUnmarshaller.unmarshall(context);

//⑥
SomePOJO pojo = dynamoDBMapper.marshallIntoObject(SomePOJO.class, itemAttributes);
```

## 解説
大きく分けて2段階の⑤⑥とその準備の①～④に分かれる。


### ①ソースの準備
低レベルフォーマットを任意のInputStreamで用意する。
べた書きなら例にもあるようにByteArrayInputStream
ファイルから読むならFiles#newInputStreamなど

### ②パーサーの準備
パーサーへ①で作成したInputStreamを入力する。
この際、パーサーは使い捨てとなるので注意

### ③コンテキストの作成
②で用意したデータと定数を入力する。
この定数はcom.amazonaws.http.JsonResponseHandlerのsimpleTypeUnmarshallersとcustomTypeMarshallersの定義を逆引きしていくと記載の定数にたどり着く
第四引数は本来com.amazonaws.http.HttpResponseだが⑤の一次変換を行う上では使われない。

### ④Unmarshallerの準備
前段で使う変換定義となる。
今回はcom.amazonaws.services.dynamodbv2.model.transform.QueryResultJsonUnmarshallerの記述と合わせておく
なお、contextは前述のsimpleTypeUnmarshallersで固定されているため、contextを使用せずJSON_SCALAR_UNMARSHALLERSから直接記述しても同様の効果となると思われる。

### ⑤一次変換
dynamoDBMapperへ入力できるクラスへ変換する。
①-⑤まではすべてこのための準備となる。

### ⑥二次変換（POJO）
変換先POJOクラスと一次変換の結果を渡す。


## リンク

* [DynamoDB 低レベル API][ref_low]  
* [Amazon DynamoDB の変更データキャプチャ][ref_capture]
* https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/test/java/com/amazonaws/unmarshaller/JsonUnmarshallerTest.java
    * プロダクトは当然として、テストコードでも似たような手法であるため問題ないはず