行儀の悪いAPIのレスポンスを自前のPOJOにバインドする(2)

前回の投稿、[行儀の悪いAPIのレスポンスを自前のPOJOにバインドする(1)](http://qiita.com/uich/items/f40dbe36b7b52f9d2011)でイケてない`HttpMessageConverter`を書いてしまったので、もう少し汎用的な作りに変えてみます。

実は前回の記事のAPI、ユーザーが見つからなければこんなレスポンスが返ってきます。

```
Status Code: 200 OK
Response-Body: ErrorCode=A012&ErrorMessage=指定のユーザーが存在しません。
```

あら、前回の投稿で書いた`HttpMessageConverter`でヌルポ発生決定ですね。
しかもエラー情報拾えない。

そこで、このサービスが提供するAPIのレスポンスの共通的なインターフェースを作ることにしました。

## レスポンスの共通実装

インターフェースはこんな感じかな。

```java
public interface ApiResponse<R> {
  ErrorInfo getErrorInfo();
  R getResource();
}
```

エラー情報の宣言を全リソースクラスで宣言したくないからベースクラスも作っておく。

```java
public abstract class ApiResponseBase<R> implements ApiResponse<R> {
  private ErrorInfo errorInfo;

  public boolean hasError() {
    return errorInfo != null;
  }
  
  // getter and setter
}
```

準備OK。じゃあ、前回投稿したクソコードを直していこうか。

## ユーザー取得APIのレスポンスクラスを実装

`User`クラスをそのまま使うとエラー情報が取れないから、ちゃんとレスポンスクラスを作ろう。

```java
public class GetUserResponse extends ApiResponseBase<User> {
  private User user;

  @Override
  public User getResource() {
    return user;
  }

  // getter and setter
}
```

よしよし。ここまで実装したら`HttpMessageConverter`の中で`User`を意識しなくて済むだろう。

・・・

Σ(￣o￣;)

いやいや、`user.setXXX(hogehoge);`していくのは変わらない。。
ここを共通化したいんだよ！

## ConversionServiceによる変換処理の実装

Springにはこういうときのために便利なSPIを用意してくれているんですよね。

### ConversionService

決まった型から型に変換する`Converter`を複数保持し、変換指定時に適切な`Converter`で変換してくれる。
なので、`MaltiValueMap`から`GetUserResponse`に変換してくれる`Converter`を作ります。
で、それを`ConversionService`に登録します。

### UserConverterの実装

```java
public enum MaltiValueMapToUserConverter implements<MultiValueMap<String, String>, GetUserResponse> {
  // singleton
  INSTANCE;

  public GetUserResponse convert(MultValueMap<String, String> source) {
    GetUserResponse response = new GetUserResponse();

    if(source.containsKey("ErrorCode")) {
        response.setErrorInfo(new ErrorInfo(source.get("ErrorCode"), source.get("ErrorMessage")));
        return response;
    }
   
    User user = new User();
    // user.setXXX(hogehoge);
    
    response.setUser(user);
    
    return response;
  }
}
```

* スレッドセーフでイミュータブルなのでシングルトンパターンで実装

### ConversionServiceへの登録

`ConversionService`自体はインターフェースなので実装します。
って、結構実装しなきゃいけないメソッドがあるな。。

しかも`GetUserResponse`以外の`Converter`どうやって用意すりゃいいんだ？

って、困らなくても実装クラスを検索してみるといい感じのクラスがありました。
`DefaultFormattingConversionService`です。
こいつに色々デフォルトで用意されている`Converter`を登録させて使うには`FormattingConversionServiceFactoryBean`経由でインスタンスを作ると良いみたいです。
こいつは`FactoryBean`で`InitializingBean`なので、こんな感じで`ConversionService`を生成します。

```java
FormattingConversionServiceFactoryBean factoryBean = new FormattingConversionServiceFactoryBean();

Set<?> converters = new HashSet<?>();
converters.add(MaltiValueMapToUserConverter.INSTANCE);
factoryBean.setConverters(converters);

factoryBean.afterPropertiesSet();

FormattingConversionService conversionSerivce = factoryBean.getObject();
```

### HttpMessageConverterの処理を修正

前回の投稿で書いた`UserFormHttpMessageConverter`を`ApiResponse`用に修正して、`ConversionService`を使うように修正していみます。

```java
public class CustomApiFormHttpMessageConverter implements HttpMessageConverter<ApiResponse<?>> {
  private FormHttpMessageConverter formHttpMessageConverter;
  private ConversionService conversionService;

  public UserFormHttpMessageConverter(FormHttpMessageConverter formHttpMessageConverter, ConversionService conversionService) {
    this.formHttpMessageConverter = formHttpMessageConverter;
    this.conversionService = conversionService;
  }

  @Override
  public boolean canRead(Class<?> clazz, MediaType mediaType) {
    // ApiResponseの派生クラスだったらOK。MediaTypeの判定はめんどくさいから
    // FormHttpMessageConverterにやらせる。(clazzはMultiValueMapで来たと嘘をつく。)
    return ApiResponse.class.isAssignableFrom(clazz) && formHttpMessageConverter.canRead(MultiValueMap.class, mediaType);
  }

  @Override
  public ApiResponse<?> read(Class<? exntends ApiResponse<?>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    @SuppressWarnings("unchecked")
    final MultiValueMap<String, String> response = formHttpMessageConverter.read((Class<? extends MultiValueMap<String, ?>>) MultiValueMap.class, inputMessage);

    ApiResponse<?> apiResponse = conversionService.convert(response, clazz);
    return apiResponse;
  }
```

おぉ。`User`クラスに関する記述が消えた！

うーん、でもまだ`Converter`の中が`setXXX(hogehge)`地獄でヤだ。

## BeanWrapperによるデータバインド処理の共通化

### BeanWrapper

POJOのフィールドへ値を設定するためのユーティリティ。
その名の通り対象オブジェクトをラップして使う。

### GetUserResponseに付加情報追加

`BeanWrapper`で処理しやすいようにバインドルールを定義するMixinを作っておく。

```java
@Mixin(GetUserResponse.class)
public abstract class GetUserResponseMixin {
  @Property(accessKey = "user.id", paramName = "User_ID")
  private Long userId;
  @Property(accessKey = "user.name", paramName = "User_Name")
  private String userName;
  @Property(accessKey = "user.birthday", paramName = "User_Birthdaty")
  @DateTimeFormat(pattern = "yyyyMMdd")
  private LocalDate userBirthday;

  @Property(accessKey = "errorInfo.code", paramName = "ErrorCode")
  private errorCode;
  @Property(accessKey = "errorInfo.message", maraName = "ErrorMessage")
  private errorMessage;
}
```
まぁ、フィード名は何でも良いです。
必要なのは`@Property`と付与されているフィールドの型が重要になってきます。

ちなみに使用したアノテーションは自前で用意しました。
`@Mixin`アノテーションはMixinクラスを特定するためのアノテーションです。
特定する際の処理は割愛するので悪しからず。

### ConverterFactory

変換できる対象を`User`決め打ちにするのではなく、`ApiResponse`の実装クラス全部に適応させたい。
そんなときに動的に`Coverter`を生成してくれるのがこれ。`CoverterFactory`。
これで`ApiResponse`の各実装クラスの`Coverter`を生成してくれる`CoverterFactory`を実装する。

```java
@AllArgsConstructor // lombok
public class ApiResponseConverterFactory implements ConverterFactory<MultiValueMap<String, String>, ApiResponse<?>> {

  private ConversionService conversionService;

  @Override
  public <T extends ApiResponse<?>> Converter<MultiValueMap<String, String>, T> getConverter(Class<T> targetType) {
    return new ApiResponseConverter<>(conversionService, targetType);
  }

  @AllArgsConstructor // lombok
  private static class ApiResponseConverter<T> implements Converter<MultiValueMap<String, String>, T> {
  
    private static final TypeDescriptor STRING_TYPE_DESC = TypeDescriptor.valueOf(String.class);
    
    private ConversionService conversionService;
    private Class<T> targetType;

    @Override
    public T convert(MultiValueMap<String, String> source) {
      T targetNewInstance = BeanUtils.instantiate(targetType);
      BeanWrapper targetWrapper = PropertyAccessorFactory.forBeanPropertyAccess(targetNewInstance);
      
      // "user.id"でアクセスした際にUserインスタンスを自動で生成させる
      targetWrapper.setAutoGrowNestedPaths(true);

      Class<?> mixinType = getMixinType(targetType); // 実装は割愛しします
      
      for(Field mixinField : mixinType.getDeclaredFields()) {
        Property property = mixinField.getAnnotation(Property.class);
        
        if(property == null) {
          continue;
        }
        
        if(targetWrapper.isWritableProperty(property.accessKey()) == false) {
          continue;
        }
        
        String value = source.getFirst(property.paramName());
        
        if(value == null) {
          continue;
        }
        
        TypeDescriptor targetFieldDesc = new TypeDescriptor(mixinField);
        
        Object convertedValue = conversionService.convert(value, STRING_TYPE_DESC, targetFieldDesc);
        
        targetWrapper.setPropertyValue(property.accessKey(), convertedValue);
      }
      
      return (T) targetWrapper.getWrappedInstance();
    }
}
```

`BeanWrapper`を使うことでフィールドへの値のセットが楽に書けます。
そしてミソが`TypeDescriptor`。こいつは型、総称型、フィールドに付与されたアノテーション情報全て持っています。
こいつを使ってコンバートすることで`@DateTimeFormat`なんかも見てくれてMixinで定義したとおりに変換してくれます。

これで`MultiValueMap`から`ApiResponse`の実装クラスへの変換クラスを動的に生成できるようになりました。これを`ConversionService`に登録しておくだけでOKです。

```java
conversionService.setConverters(Arrays.asList(new ApiResponseConverterFactory(conversionService)));
```

## 結果

`user.setXXX(hogehoge);`書かなくて良くなりました。
他にも`GetItemResponse`とか実装した場合もMixinを用意して、`getMixinType(targetType)`で取得できるようにしておけば、あとはこんな風に書けます。

```java
GetItemResponse response = restTemplate.getForObject(apiUrl, GetItemResponse.class);

if(response.hasError()) {
  throw new ApiException(response.getErrorInfo());
}

return response.getResource(); // returned Item instance.
```

## CustomApiFormHttpMessageConverterを更に修正

`#write(Object obj)`とか有効にして、リクエストパラメータにも対応したら完璧ですね。
その場合は`HttpMessageConverter`の総称型を`Object`にして実装すれば良いと思います。

[サンプルコード](https://github.com/uich/spring.converter.sample)