JavaのCharsetを作る

# JavaのCharset
Javaは内部的にはUTF-16で文字を扱っており、UTF-16とその他の文字コードを変換するための機能を持っています。この記事では「Charsetを作る」、つまり独自の名前の文字コード・コンバーターの作り方をまとめます。

# Charsetの作り方の概要
基本的には、以下の4つのクラスおよび1つの設定ファイル（プロバイダー構成ファイル）を用意して、クラスパスに通します。
* java.nio.charset.Charset を拡張したクラス
* java.nio.charset.CharsetDecoder を拡張したクラス
* java.nio.charset.CharsetEncoder を拡張したクラス
* java.nio.charset.spi.CharsetProvider を拡張したクラス
* META-INF/services/java.nio.charset.spi.CharsetProvider

# レベル０：まずは肩ならし
まずは最小限の用意で新しい名前のCharsetを作ってみましょう。
* XMS932Xという名前のCharset
* 変換はMS932と全く同じ

ここでは、実際の変換を既存のMS932コンバーターに丸投げするので、Decoder/Encoderを作る必要もありません。必要なのはCharsetProviderとプロバイダー構成ファイルの２つです。（さきほどのリストの最後の２つです）

まずは下記のようにCharsetProviderを拡張したクラスを作成します。コードをよく見ていただくと、MS932 Charsetオブジェクトを作っているのがわかると思います。

```java:mycode.XMS932XProvider.java
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.util.* ;

public class XMS932XProvider extends CharsetProvider {
  private ArrayList<Charset> list = null ;
  private Iterator<Charset> getIter() {
    if (list == null) {
      list = new ArrayList(1) ;
      list.add(Charset.forName("MS932")) ;
    }
    return list.iterator() ;
  }

  public Iterator<Charset> charsets() {
    return getIter() ;
  }

  public Charset charsetForName(String charsetName) {
    if ("XMS932X".equals(charsetName) 
        || "MS932".equals(charsetName)) return (Charset)list.get(0) ;
	return null ;
  }
}
```

もうひとつ、プロバイダー構成ファイルを作成します。以下のファイル名で、作成したCharsetProviderの名前を含みます。
```properties:META-INF/services/java.nio.charset.spi.CharsetProvider 
mycode.XMS932XProvider
```

上記 javaプログラムをコンパイルした後、jarにパッケージします。
```sh
javac mycode/XMS932XProvider.java
jar cvf xms932x.jar META-INF/services/java.nio.charset.spi.CharsetProvider mycode/XMS932XProvider.class
```
この作成した xms932x.jar をクラスパスに含めれば、無事 XMS932X という Charsetが使えるようになります。当然ですが、MS932と全く同じ文字コード変換を行います。


# レベル１：とりあえず４つのクラスを実装してみよう
次に、XMS932XXというCharsetを作ります。上記レベル０と同じく、MS932変換をそのまま行うというのは変わりません。

しかし、ここでは実際に文字列のデータを変換する本体である、CharsetDecode/CharsetEncoder の実装を用意して、その中でMS932変換を呼び出してみます。

まずは、以下のようにCharsetEncoderを拡張したクラスを用意します。実装するのは `encoderLoop`メソッドのみです。`encodeLoop`の引数を見ていただくと、CharBuffer から文字を読み込んで、`ByteBuffer`にバイト列として出力するメソッドだというのがわかると思います。今回はMS932のEncoderを呼び出してそのまま使っています。

```java:mycode.XMS932XXEncoder.java
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;

public class XMS932XXEncoder extends CharsetEncoder {
  protected XMS932XXEncoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
    super(cs, averageCharsPerByte,maxCharsPerByte) ;
  }

  protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
    Charset charset = Charset.forName("MS932");
    CharsetEncoder ce = charset.newEncoder() ;
    ce.reset() ;
    return ce.encode(in, out, false) ;
  }
}
```

同様に、CharsetDecoderを拡張したクラスを用意します。`decodeLoop`を見ていただくと、先ほどとは逆に、バイト列を読み込んでCharとして出力するメソッドだということがわかります。


```java:mycode.XMS932XXDecoder.java
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;

public class XMS932XXDecoder extends CharsetDecoder {
  protected XMS932XXDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
    super(cs, averageCharsPerByte,maxCharsPerByte) ;
  }

  protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out){
    Charset charset = Charset.forName("MS932") ;
    CharsetDecoder cd = charset.newDecoder() ;
    cd.reset() ;
    return cd.decode(in, out, false) ;
  }
}
```

次に、Charsetを拡張したクラスを作成します。この中の、newDecoder/newEncoderメソッドでそれぞれのDecoder/Encoderのインスタンスを作成して返します。コンストラクターの２つの引数 `(float)2` は、バイト列（ネイティブコード）に変換したときの平均バイト数と最大バイト数を表します。MS932はSJISですから一文字は最大2バイトとなるため、2を指定しています。平均のほうはもっと小さくてもいいかもしれません。
`contains`メソッドは、指定されたCharsetの変換を包含していれば `true`を返します。自分自身はもちろん含んでいる必要があります。このサンプルの場合はMS932も含んでもいいかもしれません。

```java:mycode.XMS932XX.java
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;

public class XMS932XX extends Charset {
  public XMS932XX(String s, String[] ss) {
	super(s, ss) ;
  }
  public boolean contains(Charset cs)  {
    if (cs instanceof XMS932XX) return true ;
      return false ; 
    }

  public CharsetDecoder newDecoder() {
    return new XMS932XXDecoder(this, (float)2, (float)2) ;
  }

  public CharsetEncoder newEncoder() {
	return new XMS932XXEncoder(this, (float)2, (float)2) ;
  }
}

```

以下はCharsetProviderを拡張したクラスです。
レベル０のときは、Charsetとして "MS932"のインスタンスを返していましたが、ここでは XMS932XXのインスタンスを返すようにしています。

```java:mycode.XMS932XXProvider.java
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.util.* ;

public class XMS932XXProvider extends CharsetProvider {
  private ArrayList<Charset> list = null ;
  private Iterator ite = getIter() ;
  private Iterator<Charset> getIter() {
    if (list == null) {
      list = new ArrayList(1) ;
      list.add(new XMS932XX("XMS932XX", new String[]{"XMS932XX"})) ;
    }
    return list.iterator() ;
  }

  public Iterator<Charset> charsets() {
    return getIter() ;
  }
  
  public Charset charsetForName(String charsetName) {
    if ("XMS932XX".equals(charsetName)) return (Charset)list.get(0) ;
	return null ;
  }
}
```
最後にプロバイダー構成ファイルを用意します。

```properties:META-INF/services/java.nio.charset.spi.CharsetProvider 
mycode.XMS932XXProvider
```
あとは各Javaファイルをコンパイルした後、Jarに固めます。

```sh
javac mycode/XMS932XX*.java
jar cvf xms932xx.jar META-INF/services/java.nio.charset.spi.CharsetProvider mycode/XMS932XX*.class
```
この作成した xms932xx.jar をクラスパスに含めれば、XMS932XX という Charsetが使えるようになります。これも実際の変換はMS932と全く同じです。しかし、Decoder/Encoderを用意したことで、なんとなく実装の仕方を見えたのではないでしょうか。

# レベル２：独自の（ちょっとした）変換を実装！

仕組みがわかったところで、実際に独自の変換を行うCharsetを作ってみましょう。
ここでは、ネイティブコード（MS932）からUnicodeへの変換について、半角カタカナを全角カタカナに変換することを考えてみます。

一からすべてのファイルを用意するのは面倒なので、レベル１で作成したXMS932XXを改造しましょう。どれを改造すればいいかというと、Decoderですね。

```java:mycode.XMS932XXDecoder.java
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.nio.* ;
import java.io.* ;

public class XMS932XXDecoder extends CharsetDecoder {
  static final String hankana = "ｱｲｳｴｵｶｷｸｹｺｻｼｽｾｿﾀﾁﾂﾃﾄﾅﾆﾇﾈﾉﾊﾋﾌﾍﾎﾏﾐﾑﾒﾓﾔﾕﾖﾗﾘﾙﾚﾛﾜｦﾝﾟﾞ" ;
  static final char[] zenkana = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン゛゜".toCharArray() ;

  protected XMS932XXDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
    super(cs, averageCharsPerByte,maxCharsPerByte) ;
  }

  protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
    Charset charset = Charset.forName("MS932") ;
    CharsetDecoder cd = charset.newDecoder() ;
    CharBuffer tmp = CharBuffer.allocate(out.remaining()) ;//一旦ここに変換

    //このループでできるだけinをtmpにMS932変換する
    while (in.hasRemaining()) {
      if (in.remaining() == 1 &&
          ((int)(in.get(in.position()))&0xff)>0x80) break ;//SJIS1バイト目だけ残った場合
      CoderResult result = cd.decode(in, tmp, false);
      if (result.isError()) {
        return result;
      } else if (result.isOverflow()) {
        break;
      }
    }

    tmp.flip();
    int pos = -1 ;

    //半角カナを見つけたら全角カナに置き換えて出力
    while (tmp.hasRemaining()) {
      char c = tmp.get();
      if ((pos = hankana.indexOf(c)) >= 0) {
        c = zenkana[pos] ;
      }
      out.put(c) ;
    }

    //outがいっぱいのときはOVERFLOW、inの残りが無いときはUNDERFLOW
    if (out.remaining()==0) return CoderResult.OVERFLOW ;
    else return CoderResult.UNDERFLOW;
  }
}
```

ちょっと複雑なので、`decodeLoop`について解説します。

まず、最初の`while`ブロックで、与えられたバイト列 `in` を、MS932コンバーターを使用して `CharBuffer tmp` に変換しています。
注意が必要なのが `while`の次の行の `if`文で、これがないと無限ループに陥る可能性があります。このwhileブロックの終了条件は、
1. `in`から読み込むデータがなくなるか、
2. `tmp`がいっぱいになるか、
3. エラーになるか

のいずれかなのですが、`in`のデータがSJISの2バイト文字の一文字目で終わっている場合、MS932でデコードが完了しないため `in`のデータが一文字残り、かつ`tmp`もいっぱいではなく、エラーでもないという状況になり、終了条件を満たさないためです。この `if`文で、1バイト残っていて、かつ2バイト文字の1バイト目（0x80より大きい）であると判断した場合`while`から抜けるようにしています。

2つ目の`while`は、MS932で変換した文字列を一文字ずつチェックし、半角カタカナの場合は対応する全角カタカナを `out`に出力し、それ以外の場合はその文字をそのまま`out`に出力しています。

このクラスをコンパイルして、さきほどと同様 xms932xx.jarとしてパッケージすることで、半角カナを全角カナに変換するXMS932XX Charsetが使えるようになります。

SJISで半角カタカナを含むテキストファイルを用意し、XMS932XXで変換すると全角カタカナに変換されるのが確認できます。

```text:input-sjis.txt
アイウエオｱｲｳｴｵ
```
```java:Test.java
import java.io.* ;

public class Test{
  public static void main(String[] args) throws Exception {
    String enc = ( args.length > 0 ? args[0] : "XMS932XX") ;
    String line = null ;
    BufferedReader r = new BufferedReader(new InputStreamReader(System.in, enc)) ;
    while((line = r.readLine()) != null) {
      System.out.println("line = " + line) ;
    }
  }
}
```

実行結果
```sh
$ java -cp xms932xx.jar:. Test XMS932XX < input-sjis.txt
line = アイウエオアイウエオ
```

# おわりに
以前はネットを検索するとJavaのCharsetを作るための情報がちらほらあったと思うのですが、最近はあまり無いようでググっても見つからなかったので書いてみました。何かのご参考にどうぞ。