GeekFactory

int128.hatenablog.com

google-api-java-clientの文字化けと対処法

Google Tasks APIにアクセスするコードを書いているのですが、App EngineのProduction環境で文字化けするパターンがあったのでまとめておきます。ちょっと変わった使い方をしていたので見つけました。

Tasks APIへのアクセスには google-api-java-client というライブラリを利用しています。このライブラリはTasks APIJSONをやり取りし、内容をモデルクラスに変換してくれる優れ物です。OAuth 2.0のコード以外はモデルの操作で完結してしまいます。

今回はJavaScript(ブラウザ)とTasks APIの間にサーバサイド層を挟み、JavaScriptで使いやすいようにJSONを料理する仕組みを考えました。つまり、Tasks APIから受け取ったJSONをモデル化し、またJSON化してブラウザに送り出すことになります。

AccessTokenResponse token = ...; // OAuth 2.0 token
GoogleAccessProtectedResource resource = new GoogleAccessProtectedResource(
    token.accessToken,
    httpTransport,
    jsonFactory,
    clientCredential.getClientId(),
    clientCredential.getClientSecret(),
    token.refreshToken);
Tasks taskService = new Tasks(resource.getTransport(), resource, resource.getJsonFactory());

com.google.api.services.tasks.model.Tasks tasks = taskService.tasks.list("@default").execute();
for (Task task : tasks.getItems()) {
  DateTime due = task.getDue();
  if (due != null) {
    task.put("dueTime", due.getValue());
  }
}
String json = tasks.toString();

このようにモデルクラスの toString() メソッドで再JSON化できるのですが、落とし穴がありました。このコードはApp Engineの開発環境では正しく動作しますが、本番環境では文字化けします。

よく調べてみるとJackson JSON Processorはマルチバイトを入念に正しく扱っていましたが、Google API Clientの文字列変換部分で ByteArrayOutputStream.toString() が使われていました。このメソッドはJava VMのデフォルト文字セットに依存します。App Engineの本番環境ではデフォルト文字セットが US-ASCII になっています。

// class com.google.api.client.json.JsonFactory (google-http-client-1.5.0-beta.jar)
  /**
   * Returns a serialized JSON string representation for the given item using
   * {@link JsonGenerator#serialize(Object)}.
   *
   * @param item data key/value pairs
   * @return serialized JSON string representation
   */
  public final String toString(Object item) {
    ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
    try {
      JsonGenerator generator = createJsonGenerator(byteStream, JsonEncoding.UTF8);
      generator.serialize(item);
      generator.flush();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    return byteStream.toString();
  }

とりあえずは以下の workaround で文字化けが解消されました。

/**     
 * Workaround for {@link JsonFactory#toString(Object)}.
 *       
 * @param item data key/value pairs
 * @return serialized JSON string representation
 */      
public static String toJsonString(GenericJson item)
{       
  ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
  try { 
    JsonGenerator generator = item.jsonFactory.createJsonGenerator(byteStream, JsonEncoding.UTF8);
    generator.serialize(item);
    generator.flush();
  }     
  catch (IOException e) {
    throw new RuntimeException(e);
  }     
  try { 
    return byteStream.toString("UTF-8");
  }     
  catch (UnsupportedEncodingException e) {
    throw new RuntimeException(e);
  }     
}       

せっかくなのでIssue Reportを上げておきます。google-api-java-clientプロジェクトに報告すればいいのかな。偉い人教えてください。