eclair ではスローされて froyo ではスルーされる(JSONException (Unterminated string at character...))

改行コードを含む JSON を返す Servlet
@Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
            throws ServletException, IOException {
        servletResponse.setContentType("application/json");
        servletResponse.setCharacterEncoding("UTF-8");
        PrintWriter writer = servletResponse.getWriter();
        writer.write("{\"str\" : \"HO\nGE\"}"); // !!!
        writer.flush();
        writer.close();
    }

文字列「HOGE」のど真ん中に改行コードを含む、 JSON 的によろしくない JSON を返す Servlet を作ります。

android クライアント側
public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override public void run() {
                HttpClient httpClient = new DefaultHttpClient();
                HttpGet httpGet = new HttpGet("http://192.168.0.5:8080/json");
                HttpResponse httpResponse = null;
                try {
                    httpResponse = httpClient.execute(httpGet);
                } catch(ClientProtocolException e) {
                    Log.e("AndroidClient", "ClientProtocolException! " + e.getMessage());
                } catch(IOException e) {
                    Log.e("AndroidClient", "IOException! " + e.getMessage());
                }
                if(httpResponse != null) {
                    try {
                        JSONObject json = new JSONObject(EntityUtils.toString(httpResponse.getEntity()));
                        String str = json.getString("str");
                        Log.d("AndroidClient", "str : " + str);
                    } catch(ParseException e) {
                        Log.e("AndroidClient", "ParseException! " + e.getMessage());
                    } catch(JSONException e) {
                        Log.e("AndroidClient", "JSONException! " + e.getMessage());
                    } catch(IOException e) {
                        Log.e("AndroidClient", "IOException! " + e.getMessage());
                    }
                }
            }
        }).start();
    }

んで、 Servlet にリクエスト飛ばしてレスポンスを org.json.JSONObject に変換するような android クライアントアプリを作ります。

それを eclair(2.1-update1) エミュレーター上で動かす
JSONException! Unterminated string at character 13 of {"str" : "HO
GE"}

「Unterminated string at character 13 of ...」と言われて落ちます。「android クライアント側」の 23 行目で落ちて、 29 行目のエラーログが吐かれます。

今度は froyo(2.2) エミュレーター上で動かす
str : HO
GE

あら不思議。 25 行目のログが出力されちゃうじゃありませんの!

eclair と froyo のソースを見比べる

「dalvik/libcore/json/src/main/java/org/json」以下です。

eclipse 上で「 compare with 」とかしてもあまり意味ありません。インターフェースというか public なメソッドのシグニチャーだけ同じで実装は随分違っている雰囲気です。

JSONObject に食わせた String をパースする org.json.JSONTokener を見比べてみると・・・

public class JSONTokener {
// (中略)
    public String nextString(char quote) throws JSONException {
        char c;
        StringBuilder sb = new StringBuilder();
        for (;;) {
            c = next();
            switch (c) {
            case 0:
            case '\n':
            case '\r':
                throw syntaxError("Unterminated string");

上は eclair

下が froyo (private な深いメソッドまで行ってますが、まあこの辺だと思います)

public class JSONTokener {
// (中略)
    private int nextCleanInternal() throws JSONException {
        while (pos < in.length()) {
            int c = in.charAt(pos++);
            switch (c) {
                case '\t':
                case ' ':
                case '\n':
                case '\r':
                    continue;

スルーとスローは随分違うと思いますが、 JSON を返す側(ここでは Servlet 側)がちゃんとエスケープすれば、どちらもなんの問題も起きません。

アクセサ( getter / setter)・不変クラス( immutable )・カプセル化( encapsulation )

public interface Radioactivity {
 /**
  * 年間放射線量を取得する。
  * @return ミリシーベルト
  */
 double getRadiation();
 /**
  * 設定されている線量が安全かどうか
  * @return true の場合、安全
  */
 boolean isSafe();
}
/**
 * 平常時
 */
public class Normal implements Radioactivity {
 private double radiation;
 public Normal(double radiation) {
  this.radiation = radiation;
 }
 /**
  * @see Radioactivity#getRadiation()
  */
 public double getRadiation() {
  return this.radiation;
 }
 /**
  * @see Radioactivity#isSafe()
  */
 public boolean isSafe() {
  return this.radiation < 1.0d;// ふつうはこのぐらい
 }
}
/**
 * 緊急時
 */
public class Emergency implements Radioactivity {
 private double radiation;
 public Emergency(double radiation) {
  this.radiation = radiation;
 }
 /**
  * @see Radioactivity#getRadiation()
  */
 public double getRadiation() {
  return this.radiation;
 }
 /**
  * @see Radioactivity#isSafe()
  */
 public boolean isSafe() {
  return this.radiation < 20.0d;// 緊急なので致し方なく
 }
}
  • アクセサを getter のみにすることで一度設定した年間の被曝線量を変更できなくする(不変クラス)( immutable )
  • #isSafe() メソッドは時の政府の都合により内部の処理を変更することができる(カプセル化)( encapsulation )
see also: Javaプログラマであるかを見分ける10の質問 5.フィールドのアクセス修飾子をprivateにしgetter/setterメソッドを提供する事でフィールドを参照する設計方針を取る主な理由を説明せよ