Javaでデータベースの検索結果をグルーピングしながらZipファイルを作成するには

環境: Java8

##説明
単純なSELECT文による値の繰り返しがあるソートされた二次元表をもとにキーごとにZipEntryを追加し、エントリの内容はテキストファイルとして、テキストファイルの内容もインデントでグループ化してあるというようなZipファイルを作る。

### 出力
1. キー部分の変化ごとにZipファイルのエントリを作成する。
2. 各エントリはテキストファイルを内容とする。
3. テキストファイルの内容もグループ化する。インデントでグループ化する。

### 入力
1. SELECT文は同一値の繰り返しがあるフラットな二次元表。

### 要点
1. ByteArrayOutputStreamを可変長のテキスト編集の一時領域として使用する。
2. Formatter(OutputStream, String)のコンストラクタを使い文字コード変換と整形処理をまとめて対応する。
3. 読みだした1レコード分の整形が終わったらFormatter#flush()を呼び出して編集したテキスト内容をbufferに転送する。
4. bufferの内容をZipOutputStreamに移し終わったらByteArrayOutputStream#reset()を呼び出してバッファの先頭に戻す。
5. 閉じない。

##### おまけ
1. 簡易的なRowクラスを作成し行を表現し, forステートメントでループできるようにし, カラム値には名前でアクセスできるようにする。これはわかりやすさのため。
2. 現在値(cur)と前回値(prev)の比較が常に成立するようにfor(Row prev = new Row(null)として前回値が存在しない1件目の処理でも eq(cur.a, prev.a) が有効になるようにする。
3. 文字列比較にはnullを許容してnullと文字列を比較できるようなコンビニエンスメソッドを用意する。これはnull.equals(s)やs.equals(null)でNullPointerExceptionやIllegalArgumentExceptionが発生しないようにするため。
4. prevの初期値は実際のデータと整合性をとる。今回はテーブルのデータとしてはnullがない前提である。大概これでいいはず。グループ化するためのカラム値がnullなんてことはないはずだ。

#### ソースコード

```java
Charset cs = Charset.defaultCharset();
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file), cs)) {
  try (ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024)) {
    try (Formatter formatter = new Formatter(buffer, cs.name())) {
      try (PreparedStatement select = con.prepareStatement("SELECT DISTINCT a, b, c, d, e, f FROM foo ORDER BY a, b, c desc, d, e, f")) {
        try (ResultSet rs = select.executeQuery()) {
          for (Row prev = new Row(null), cur = Row.next(rs); cur != null; prev = cur, cur = Row.next(rs)) {
            if (!eq(cur.a, prev.a) || !eq(cur.b, prev.b)) {
              zos.putNextEntry(new ZipEntry(String.format("%s-%s.txt", cur.a, cur.b)));
              formatter.format("%s%n", cur.c);
              formatter.format("  %s%n", cur.d);
            } else if (!eq(cur.c, prev.c)) {
              formatter.format("%s%n", cur.c);
              formatter.format("  %s%n", cur.d);
            } else if (!eq(cur.d, prev.d)) {
              formatter.format("  %s%n", cur.d);
            }
            formatter.format("    %s: %s%n", cur.e, cur.f);
            formatter.flush();
            zos.write(buffer.toByteArray());
            buffer.reset();
          }
        }
      }
    }
  }
}
```

##### null との比較を許すコンビニエンスメソッド

```java
public static boolean eq(String l, String r) {
  if (l == r) {
    return true;
  } else if (l != null && r == null) {
    return false;
  } else if (l == null && r != null) {
    return false;
  } else {
    return l.equals(r);
  }
}
```
##### ResultSet をループするための簡易的な仕組み

```java
public class Row {
  public static Row next(ResultSet rs) throws SQLException {
    return (rs.next()) ? new Row(rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6)) : null;
  }

  String a;
  String b;
  String c;
  String d;
  String e;
  String f;
  
  public Row(String v) {
    this(v, v, v, v, v, v);
  }

  public Row(String a, String b, String c, String d, String e, String f) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
    this.e = e;
    this.f = f;
  }
}
```

### 注意点
bufferとformatterはそれらより内側のコードからともに見えている必要がある。下記のように書いてしまうとZipOutputStream#write(byte[])のために素直にデータを取り出せずに困ることになる。やりようはあるけどそれならこんな書き方しなくていい。

```java
Formatter formatter = new Formatter(new ByteArrayOutputStream(1024), cs.name());
...
zos.write(???);//どうするの?
```