『Webアプリケーション構築入門』の郵便番号検索でPostgreSQLを使う方法(Javaの場合)

追記:無料で使えるHeroku Postgres Devは、行数が1万行に制限されることが発表されたので、別の方法(例:SQLiteを組み込む)を検討した方がいいでしょう。

拙著『Webアプリケーション構築入門』では、開発プラットフォームとしてJavaとPHPを採用しており、ウェブアプリを実際に公開するときは、自前のサーバ(VPSを含む)を使える人にはJava・PHPの好きな方を、レンタルサーバを使う人にはPHPを使って貰うつもりで執筆していました。

最近、自前のサーバでもレンタルサーバでもない第3の選択肢としてのPaaSが普及してきており、その一部は無料で使えるようになっています。「サーバ管理技術」よりも「アプリケーション開発」に興味がある人にとっては、自前のサーバやレンタルサーバよりPaaSの方が便利でしょう。しかし、拙著はPaaSを意識した書き方にはなっていないので、そのあたりの情報をアフターサービスとして提供していきたいと考えている今日この頃です。

Javaを無料で使えるPaaSの一つにHerokuがあります。Javaが使えるレンタルサーバはあまりないので(私が知らないだけ?)、そういう意味でもHerokuは魅力的なのですが、無料で使えるデータベース管理システム(DBMS)が、拙著で採用しているMySQLではなくPostgreSQLだというのが、初心者には障害になるかもしれません。この障害を回避するために、拙著の第9章「ウェブアプリの実例」で作成する「郵便番号検索システム(Google MapsとのマッシュアップやAjaxによるリアルタイム検索を含む)」でPostgreSQLを使う方法を紹介しましょう。

まずPostgreSQLをインストールします。

sudo apt-get install postgresql

PostgreSQLの管理者postgresのパスワードを適当に設定します(例:pass)。

sudo passwd postgres
#ここでパスワードを入力

郵便番号辞書データベースを作ります。拙著9.1.1項のとおりにファイルをダウンロードして、次のコマンドで必要なデータだけを集めたCSVファイルを作ります。各コマンドの意味は「man gawk」などとして確認してください。

nkf -Lu -w ken_all.csv | gawk -F, -v OFS="," '{print $3,$7,$8,$9,"\"\"","\"\""}'  > /tmp/zip.csv
nkf -Lu -w jigyosyo.csv | gawk -F, -v OFS="," '{print $8,$4,$5,$6,$7,$3}' >> /tmp/zip.csv

管理者になって、データベースmydbを作ります。

su postgres
#ここでパスワードを入力

createdb mydb

MySQLのコマンド「mysql」に相当するコマンド「psql」を実行します。

psql mydb

テーブルを作成し、先に作成したCSVファイルをインポートします(拙著9.1.2項とは異なり、必要なデータだけをインポートすることにします)。

CREATE TABLE zip (
  code CHAR(7) NOT NULL,
  address1 VARCHAR(10) DEFAULT '' NOT NULL,
  address2 TEXT NOT NULL,
  address3 TEXT NOT NULL,
  address4 TEXT NOT NULL,
  office TEXT NOT NULL
);

COPY zip FROM '/tmp/zip.csv' WITH CSV;

インデックスを作成します。

CREATE INDEX code_idx ON zip (code);

Webアプリから接続する際のパスワードを設定します。これは、先に設定したOSのパスワードと同じである必要はありません。

ALTER USER postgres WITH PASSWORD 'pass';

「\q」と入力してコマンドpsqlを終了します。

拙著8.2.2項「Javaからデータベースへのアクセス」の場合と同様に、「PostgreSQL JDBC ドライバ」を追加します。

コードの変更はzip.jsp(p. 134あるいはサポートページを参照)のデータベース接続部分だけです。次のように書き換えます。ここで使うパスワードは、先のALTER USER文で設定したパスワードです。

//データベースに接続(Heroku非対応)
Class.forName("org.postgresql.Driver").newInstance();
String url = "jdbc:postgresql://localhost/mydb";
Connection conn = DriverManager.getConnection(url, "postgres", "pass");

以上の準備で、拙著第9章の郵便番号検索システムはPostgreSQLで動くようになります。このままではまだHerokuでは動きませんが、とりあえず、ローカルで動くようになったので一区切りです。

オープンソースデータベース標準教科書 -PostgreSQL-などというものもありますね。

jOOXを使えば、JavaでもjQueryのようにXMLを操作できる

拙著『Webアプリケーション構築入門』では、「Webアプリを作れるようになる前に、使えるようになろう」という考えから、いくつかのWeb APIの利用方法を早い段階で紹介しています。多くの場合、Web APIを使うためには、XMLやJSONなどのデータをプログラムから操作できなければなりません。本書では、Twitterのパブリックタイムラインを題材にして、JavaでXMLを処理する方法やPHPやJavaScript(jQuery)でJSONを処理する方法を紹介しています。

追記:TwitterのAPIが変わってしまったので、シミュレータを使うようにしました。(参考:Twitter APIのパブリックタイムラインが亡くなっていました

しかし、jOOXのようなライブラリを使うと、拙著で紹介したよりも簡単に、JavaでもjQuery風にXMLを操作できます。というのが、今日のお話。

p.72で紹介している、つぶやきの本文だけを取り出して表示するコードは次のようになります。(p.86などを参考に、joox-バージョン番号.jarを使えるようにしてから試してください。)

import java.net.*;
import org.joox.*;
import static org.joox.JOOX.*;

public class JooxPublicTimeline {

  public static void main(String[] args) throws Exception {
    URL url = new URL("http://labs.unfindable.net/public_timeline/xml.php");
    $(url.openStream()).find("text").each(new Each() {

      @Override
      public void each(Context cntxt) {
        System.out.println($(cntxt.element()).text());
      }
    });
  }
}

.each()の中にラムダ式を書けないのがつらいところで、この例のように無名インナークラス(p.181)を使わなければなりません。.find()のあとに.each()とすればListが返るので、それをfor文で回して・・・、としたくなるところですが、jQueryらしく書くとこんな感じでしょう。

p.73で紹介している、「名前: つぶやきの本文」という形式で表示するコードは次のように書き換えられます。

import java.net.*;
import org.joox.*;
import static org.joox.JOOX.*;

public class JooxPublicTimeline2 {

  public static void main(String[] args) throws Exception {
    URL url = new URL("http://labs.unfindable.net/public_timeline/xml.php");
    $(url.openStream()).find("status").each(new Each() {

      @Override
      public void each(Context cntxt) {
        Match status = $(cntxt.element());
        String text = status.child("text").text();
        String name = status.find("user>name").text();
        System.out.println(name + ": " + text);
      }
    });
  }
}

拙著で紹介しているJavaでXMLを処理する方法は標準的なものではありますが、よく言えばJavaらしい、悪く言えば冗長なコードです。「標準的」ということにこだわらなければ、ここで紹介したような、もっと簡単な方法があるのです。

はてなブックマークAtomAPIをOAuthで利用する方法(Java, scribe-java版)

はてなブックマークAtomAPIの使い方を紹介します。2010年末にOAuthをサポートしたので、ここでもそれを試します。言語はJava、ライブラリはscribe-javaを使います。前にoauth-signpostを使う方法を紹介しましたが、こちらのほうがおそらく簡単です(準備のためにクラスを1つ余計に作らなければなりませんが)。

アプリケーションの登録

アプリケーション登録ページでアプリケーションを登録し、Consumer keyとConsumer secretを取得します。

ライブラリの準備

scribe-バージョン番号.jarとcommons-codec-バージョン番号.jarを使えるようにします(このあたりの詳細については、拙著『Webアプリケーション』などを参照してください)。

APIを定義するクラス

TwitterのようなメジャーなAPIはあらかじめscribe-javaで定義されているので(一覧)、後のコードで「TwitterApi.class」などと書くだけでいいのですが、はてなはそこまでメジャーでもないようで、APIを定義するクラスを自分で作らなければなりません。

import org.scribe.builder.api.*;
import org.scribe.model.*;
import org.scribe.utils.OAuthEncoder;

public class HatenaApi extends DefaultApi10a {

  @Override
  public String getAccessTokenEndpoint() {
    return "https://www.hatena.com/oauth/token";
  }

  @Override
  public String getRequestTokenEndpoint() {
    String scope = "?scope=read_public%2Cwrite_public%2Cread_private%2Cwrite_private";
    return "https://www.hatena.com/oauth/initiate" + scope;
  }

  @Override
  public String getAuthorizationUrl(Token requestToken) {
    return String.format("https://www.hatena.ne.jp/oauth/authorize?oauth_token=%s",
            OAuthEncoder.encode(requestToken.getToken()));
  }
}

TwitterのOAuthと違うのは、どのような権利を求めるかを「score=...」という形で書いておくところです。

Access tokenとToken secretの取得

OAuthでユーザの権利を譲り受けるためのAccess tokenとToken secretを取得します。ここで紹介するコンソール上で行う方法の他に、ブラウザ上でリダイレクトを使う方法もありますが、両者の違いはTwitterの場合などと同じなので、Twitterでの方法を見れば、リダイレクトを使う方法も実現できるでしょう。

先に取得したConsumer keyとConsumer secretを下のコードに埋め込んでください。

import java.io.*;
import org.scribe.builder.*;
import org.scribe.model.*;
import org.scribe.oauth.*;

public class ScribeTokenCreator {

  public static void main(String[] args) throws Exception {
    // プロキシサーバの設定
    //System.setProperty("http.proxyHost", "proxy.example.net");
    //System.setProperty("http.proxyPort", "3128");
    //System.setProperty("https.proxyHost", "proxy.example.net");
    //System.setProperty("https.proxyPort", "3128");

    String consumerKey = ***** Consumer key *****;
    String consumerSecret = ***** Consumer secret *****;
    OAuthService service = new ServiceBuilder().provider(HatenaApi.class).apiKey(consumerKey).apiSecret(consumerSecret).build();

    Token requestToken = service.getRequestToken();
    //System.out.println(requestToken.getRawResponse());
    String authUrl = service.getAuthorizationUrl(requestToken);
    System.out.println("このURLにアクセスし、表示されるPINを入力してください。");
    System.out.println(authUrl);
    System.out.print("PIN:");

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String pin = br.readLine();

    Token accessToken = service.getAccessToken(requestToken, new Verifier(pin));
    System.out.println("Access token: " + accessToken.getToken());
    System.out.println("Token secret: " + accessToken.getSecret());
  }
}

上のコード実行するとURLが表示されるので、そのURLにブラウザでアクセスしてください。下のようなページが表示されます。scoreの記述によって内容は変わります。この例では、公開・非公開の両方の情報への読み書き権限を要求しています。

OAuthで許可を求められているところ

「許可」をクリックすると下のようなページが表示されるので、表示された文字列を先のコードからのプロンプトで入力してください。Access tokenとToken secretが得られます。

「許可」をクリックした結果

ブックマーク

譲り受けた権利を使って「http://b.hatena.ne.jp/atom/post」にURLをPOSTすれば、そのURLをはてなブックマークに登録できます。例として「http://www.google.com/」を登録するコードは次のようになります。先に取得したConsumer keyとConsumer secret、Access token、Token secretを埋め込んで実行してください。

import org.scribe.builder.*;
import org.scribe.model.*;
import org.scribe.oauth.*;

public class ScribeBookmark {

  public static void main(String[] args) throws Exception {
    // プロキシサーバの設定
    //System.setProperty("http.proxyHost", "proxy.example.net");
    //System.setProperty("http.proxyPort", "3128");

    // これはユーザによらない
    String consumerKey = ***** Consumer key *****;
    String consumerSecret = ***** Consumer secret *****;
    OAuthService service = new ServiceBuilder().provider(HatenaApi.class).apiKey(consumerKey).apiSecret(consumerSecret).build();

    // これはユーザごとに異なる
    Token accessToken = new Token(
            ***** Access token *****,
            ***** Token secret *****);

    // ターゲット
    String target = "http://www.google.com/";
    String xml = String.format("<entry xmlns='http://purl.org/atom/ns#'>"
            + "<title>dummy</title>"
            + "<link rel='related' type='text/html' href='%s' />"
            + "<summary type='text/plain'></summary>"
            + "</entry>",
            target);

    // HTTPリクエスト(POST)
    OAuthRequest request = new OAuthRequest(Verb.POST, "http://b.hatena.ne.jp/atom/post");
    request.addHeader("Content-Type", "application/octed-stream");
    service.signRequest(accessToken, request);
    request.addPayload(xml);
    Response response = request.send();
    
    // 結果の表示
    System.out.println(response.getCode());
    System.out.println(response.getBody());
  }
}

Twitterの場合との違うのは、リクエストボディにXMLを書かなければならないこと、それを送信するためにContent-Typeをapplication/octed-streamにしておくことです。

ブックマークの編集

ブックマークを編集したいときは、上記のPOSTのレスポンス・ヘッダ (Location) に対して、GETで現状を取得し、PUTで新しいデータを送信します。例として、現在のコメントに「♡」を追記するコードは次のようになります(javax.xml.parsers.*とorg.w3c.dom.*、org.xml.sax.*をインポートしておく必要があります)。タイトルははてな全体で共有されるので、よほどの理由がない限りは変更しない方がいいでしょう。

// 編集のためのURL
String editUrl = response.getHeader("Location");

// 現コメントの取得
// HTTPリクエスト(GET)
request = new OAuthRequest(Verb.GET, editUrl);
service.signRequest(accessToken, request);
response = request.send();
System.out.println(response.getCode());

// 結果(XML)の処理
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(response.getStream()));
String comment = doc.getElementsByTagName("summary").item(0).getTextContent();

// コメントの更新(「♡」を付加する)
String newComment = comment + "♡";
xml = String.format("<entry xmlns='http://purl.org/atom/ns#'>"
        //+ "<title>Google 2</title>"
        + "<summary type='text/plain'>%s</summary>"
        + "</entry>",
        newComment);

// HTTPリクエスト(PUT)
request = new OAuthRequest(Verb.PUT, editUrl);
request.addHeader("Content-Type", "application/octed-stream");
service.signRequest(accessToken, request);
request.addPayload(xml);
response = request.send();

// 結果の表示
System.out.println(response.getCode());
System.out.println(response.getBody());

補足

アプリケーションに与えた許可を取り消したいときは、「Myはてな」→「ユーザ設定」→「外部アプリケーション認証」→「外部のアプリケーションから、はてなのサービスを利用する」の順にクリックしてください。「https://www.hatena.ne.jp/はてなID/config/auth/provider」にアクセスしてもいいでしょう。

はてなブックマークAtomAPIをOAuthで利用する方法(Java, oauth-signpost版)

より簡単な方法:scribe-java版

はてなブックマークAtomAPIの使い方を紹介します。2010年末にOAuthをサポートしたので、ここでもそれを試します。言語はJava、ライブラリはoauth-signpostを使います。

アプリケーションの登録

アプリケーション登録ページでアプリケーションを登録し、Consumer keyとConsumer secretを取得します。

ライブラリの準備

signpost-core-バージョン番号.jar とcommons-codec-バージョン番号.jarを使えるようにします(このあたりの詳細については、拙著『Webアプリケーション』などを参照してください)。

Access tokenとToken secretの取得

OAuthでユーザの権利を譲り受けるためのAccess tokenとToken secretを取得します。ここで紹介するコンソール上で行う方法の他に、ブラウザ上でリダイレクトを使う方法もありますが、両者の違いはTwitterの場合などと同じなので、Twitterでの方法を見れば、リダイレクトを使う方法も実現できるでしょう。

先に取得したConsumer keyとConsumer secretを下のコードに埋め込んでください。

import java.io.*;
import oauth.signpost.*;
import oauth.signpost.basic.*;

public class HatenaTokenCreator {

  public static void main(String[] args) throws Exception {
    // プロキシサーバの設定
    //System.setProperty("http.proxyHost", "proxy.example.net");
    //System.setProperty("http.proxyPort", "3128");
    //System.setProperty("https.proxyHost", "proxy.example.net");
    //System.setProperty("https.proxyPort", "3128");

    OAuthConsumer consumer = new DefaultOAuthConsumer(
            ***** Consumer key *****,
            ***** Consumer secret *****);

    String scope = "?scope=read_public,write_public,read_private,write_private";
    OAuthProvider provider = new DefaultOAuthProvider(
            "https://www.hatena.com/oauth/initiate" + scope,
            "https://www.hatena.com/oauth/token",
            "https://www.hatena.ne.jp/oauth/authorize");

    String authUrl = provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND);
    System.out.println("このURLにアクセスし、表示されるPINを入力してください。");
    System.out.println(authUrl);
    System.out.print("PIN:");

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String pin = br.readLine();

    provider.retrieveAccessToken(consumer, pin);
    System.out.println("Access token: " + consumer.getToken());
    System.out.println("Token secret: " + consumer.getTokenSecret());
  }
}

TwitterのOAuthと違うのは、どのような権利を求めるかを「score=...」という形で書いておくところです。上のコード実行するとURLが表示されるので、そのURLにブラウザでアクセスしてください。下のようなページが表示されます。scoreの記述によって内容は変わります。この例では、公開・非公開の両方の情報への読み書き権限を要求しています。

OAuthで許可を求められているところ

「許可」をクリックすると下のようなページが表示されるので、表示された文字列を先のコードからのプロンプトで入力してください。Access tokenとToken secretが得られます。

「許可」をクリックした結果

ブックマーク

譲り受けた権利を使って「http://b.hatena.ne.jp/atom/post」にURLをPOSTすれば、そのURLをはてなブックマークに登録できます。例として「http://www.google.com/」を登録するコードは次のようになります。先に取得したConsumer keyとConsumer secret、Access token、Token secretを埋め込んで実行してください。

import java.io.*;
import java.net.*;
import oauth.signpost.*;
import oauth.signpost.basic.*;

public class SignpostBookmark {

  public static void main(String[] args) throws Exception {
    // プロキシサーバの設定
    //System.setProperty("http.proxyHost", "proxy.example.net");
    //System.setProperty("http.proxyPort", "3128");

    // これはユーザによらない
    OAuthConsumer consumer = new DefaultOAuthConsumer(
            ***** Consumer key *****,
            ***** Consumer secret *****);

    // これはユーザごとに異なる
    consumer.setTokenWithSecret(
            ***** Access token *****,
            ***** Token secret *****);

    // ターゲット
    String target = "http://www.google.com/";
    String xml = String.format("<entry xmlns='http://purl.org/atom/ns#'>"
            + "<title>dummy</title>"
            + "<link rel='related' type='text/html' href='%s' />"
            + "<summary type='text/plain'></summary>"
            + "</entry>",
            target);

    // HTTPリクエスト
    URL url = new URL("http://b.hatena.ne.jp/atom/post");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    connection.addRequestProperty("Content-Type", "application/octed-stream");
    consumer.sign(connection);

    // POSTによる送信
    connection.setDoOutput(true);
    OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
    writer.write(xml);
    writer.flush();
    writer.close();

    // 結果の表示
    System.out.printf("%s %s\n", connection.getResponseCode(), connection.getResponseMessage());
    BufferedReader br = new BufferedReader(new InputStreamReader(
            connection.getResponseCode() / 100 == 2
            ? connection.getInputStream()
            : connection.getErrorStream(),
            "UTF-8"));
    String line = null;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
  }
}

Twitterの場合との違うのは、リクエストボディにXMLを書かなければならないこと、それを送信するためにContent-Typeをapplication/octed-streamにしておくことです。

ブックマークの編集

scribe-java版で紹介した方法と同様です(手抜き)。

補足

アプリケーションに与えた許可を取り消したいときは、「Myはてな」→「ユーザ設定」→「外部アプリケーション認証」→「外部のアプリケーションから、はてなのサービスを利用する」の順にクリックしてください。「https://www.hatena.ne.jp/はてなID/config/auth/provider」にアクセスしてもいいでしょう。

(解決)Javaでサニタイズするときの注意—Apache Commons Lang 2.xと3.0の場合

Commons Lang 3.0.1で修正されています。

Java 5をサポートする「Apache Commons Lang 3.0」がリリース、というニュースがありました。正確には、Java 1.4以下をサポートしない「Apache Commons Lang 3.0」がリリース、だと思いますが、それはまあいいとして、これに関連する少し深刻な話を忘れないうちに書いておきましょう。

「Javaプログラマは文字列の操作方法を確認した方がいいかもしれない」のつづきです。

HTML文書を書く時には、「𠮷野家(>_<)」のような文字列は、「𠮷野家(&gt;_&lt;)」のように書かなければなりません。「<」や「>」、「&」などは特殊な文字なので、文書中にそのまま書くことはできないのです。

効率を気にしないなら、次のように書いて置換するのが簡単です。

str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = str.replace("&", "&amp;")
        .replace("<", "&lt;").replace(">", "&gt;");
System.out.println(str);
//𠮷野家(&gt;_&lt;)

しかし、このようなコードをいつも書くのは面倒ですし、順番を間違える危険もあります。実は「'」や「"」を置換したかったりもします。

そこで、HTML中に直接書けない文字を置換するためのライブラリを使います。Apache Commons Langはそのようなライブラリの一つで、拙著『Webアプリケーション構築入門』でも紹介しました。次のように使います。

まずは最近リリースされたCommons Lang 3.0の場合です。

String str = "文字列(>_<)";
str = org.apache.commons.lang3.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//文字列(&gt;_&lt;)

3.0は出たばかりですし、Java 5以上が必須なので、まだ2.xの方がほとんどでしょう。2.xでは次のようになります。

String str = "文字列(>_<)";
str = org.apache.commons.lang.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//&#25991;&#23383;&#21015;(&gt;_&lt;)

Commons Lang 2.xでは、ASCII以外の文字は「&#コードポイント(10進表記);」という形(数値文字参照)に置換されるので、人間にはわかりにくくなっていますが、ウェブブラウザでなら「文字列(>_<)」と表示されます。

2.xと3.0の間には、(1) パッケージ名(2.6はlang、3.0はlang3)(2) 数値文字参照の使用(2.6では使う、3.0では使わない)、に関して違いがあります。

ここまでは問題ありませんが、対象文字列がサロゲートペアを含むようになると、いやな感じになります。

まずは3.0の場合。

String str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = org.apache.commons.lang3.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//𠮷?野家(&gt;_&lt;

変なところに「?」が入って、最後の「)」が無くなっていますが、これは間違いです。数値文字参照を使わない3.0では、「𠮷野家(&gt;_&lt;)」にならなければなりません。

バグレポートを送ったので、この問題は3.0.1で解決されるはずです。(追記:修正されました。)

サロゲートペアを使う可能性が少しでもある人は、Commons Lang 3.0はスルーしましょう。

パッチを見てもらえばわかりますが、「1文字ずつ処理する」部分にバグがあったのです(参考:Javaプログラマは文字列の操作方法を確認した方がいいかもしれない)。

とりあえずの結論:Javaの文字列処理は難しすぎる。

次にCommons Lang 2.xの場合です。

str = "\uD842\uDFB7\u91CE\u5BB6(>_<)";
str = org.apache.commons.lang.StringEscapeUtils.escapeXml(str);
System.out.println(str);
//&#55362;&#57271;&#37326;&#23478;(&gt;_&lt;)

「𠮷」が「&#55362;&#57271;」という2つの数値文字参照になっていますが、これは間違いです。「&#134071;」という1つの数値文字参照にならなければなりません(参考:Using character escapes in markup and CSS)。

この問題を解決するパッチを送りましたが、「3.0で修正されている」としてスルーされてしまいました。間違いが2.xで修正されるとしたら、動作をそろえるためにStringEscapeUtils.unescapeXml(str)にも修正が必要だと思い、そのためのパッチも送ったのですが、こちらは現時点では間違った動作はしていないので、やはりスルーされてしまいました。

「3.0で修正されている」とは言っても、先に述べたように、2.xと3.0ではサポートするJavaのバージョンが違いますし、パッケージ名も変わっているので、2.xでも直しておいた方がいいと思ったのですが、まあ、仕方ありません。

Commons Lang 2.xを使っている人は、「サロゲートペアはサポートしない」と仕様書に明記しておきましょう。

最終結論:Javaの文字列処理は難しすぎる。