# XLSBeans - Excel to POJO Mapping

## TODO

- ダイナミックアノテーション
  - メソッド・アノテーションの指定方法（引数）
  - アノテーションの初期値
- マッピング
  - JXLを使うのをやめる？
  - 書き出しのマッピングもできるようにしたいなぁ。
- 全般
  - 英語のドキュメントを用意する。
  - コメントを英語で書く（コントリビュートされた部分が一部日本語のまま）。

## XLSBeansの使い方

XLSBeansはアノテーションを付与してJavaBeansとExcelをマッピングするライブラリ（プロトタイプ）です。 以下のアノテーションが利用可能です。

### @Sheet

読み込むシートをシート番号、シート名、シート名に対する正規表現のいずれかで指定します。クラスに付与します。

    /** シート番号で指定する場合 */
    @Sheet(number=0)
    public class SheetObject {
      ...
    }

    /** シート名で指定する場合 */
    @Sheet(name="Users")
    public class SheetObject {
      ...
    }

    /** 正規表現で指定する場合 */
    @Sheet(regex="Sheet_[0-9]+")
    public class SheetObject {
      ...
    }

正規表現で指定する場合は`XLSBeans#loadMultiple()`メソッドを用いることでマッチしたシートの情報を一度に取得することができます。

### @Cell

セルの列、行を指定してBeanのプロパティにマッピングします。setterメソッドに対して付与します。

    @Sheet(name="Users")
    public class SheetObject {
      @Cell(column=0, row=0)
      public void setTitle(String title){
        ...
      }
    }

### @LabelledCell

セルの文字列を指定し、その左右もしくは下側のセルの値をマッピングします。setterメソッドに対して付与します。

    @Sheet(name="Users")
    public class SheetObject {
      @LabelledCell(label="Title", type=LabelledCellType.Right)
      public void setTitle(String title){
        ...
      }
    }

`range`パラメータを指定すると、`type`パラメータの方向に向かって指定したセル数分を検索し、 最初に発見した空白以外のセルの値を取得します。

    @Sheet(name="Users")
    public class SheetObject {
      @LabelledCell(label="Title", type=LabelledCellType.Right, range=3)
      public void setTitle(String title){
        ...
      }
    }

同じラベルのセルが複数ある場合は、領域の見出しを`headerLabel`パラメータで指定します。 `headerLabel`パラメータで指定されたセルから`label`パラメータで指定されたセルを下方向に検索し、 最初に見つかったセルをラベルセルとして使用します。

    @LabelledCell(label="クラス名", type=LabelledCellType.Right, headerLabel="アクション")
    public void setActionClassName(String actionClassName){
      ...
    }
    @LabelledCell(label="クラス名", type=LabelledCellType.Right, headerLabel="アクションフォーム")
    public void setFormClassName(String formClassName){
      ...
    }

セルが見つからなかった場合はエラーとなりますが、`optional`パラメータに `true`を指定しておくと、無視して処理を続行します。

### @SheetName

シート名を`String`型のプロパティにマッピングします。

    @Sheet(name="Users")
    public class SheetObject {
      @SheetName
      public void setSheetName(String sheetName){
        ...
      }
    }

### @HorizontalRecords

水平方向に連続する行を`List`または配列にマッピングします。 表には最上部にテーブルの名称と列名を記述した行が必要になります。

`tableLabel`パラメータでテーブルの名称を指定します。 `List`型または配列の引数を1つだけ取るsetterメソッドに対して付与します。

    @Sheet(name="Users")
    public class SheetObject {
      @HorizontalRecords(tableLabel="ユーザ一覧", recordClass=Record.class)
      public void setRecords(List<Record> records){
        ...
      }
    }

デフォルトでは行に1つもデータが存在しない場合、そのテーブルの終端となります。 行の一番左側の列のボーダーによってテーブルの終端を検出する方法もあります。 この場合は`@HorizontalRecords`の`terminal`パラメータに `RecordTerminal.Border`を指定してください。

    @Sheet(name="Users")
    public class SheetObject {
      @HorizontalRecords(tableLabel="Horizontal Records", recordClass=Record.class,
                terminal=RecordTerminal.Border)
      public void setRecords(List<Record> records){
        ...
      }
    }

テーブルが他のテーブルと連続しており`terminal`パラメータで`Border`、 `Empty`のいずれを指定しても終端を検出できない場合があります。 このような場合は`terminateLabel`パラメータで終端を示すセルの文字列を指定します。

    @Sheet(name="Users")
    public class SheetObject {
      @HorizontalRecords(tableLabel="Horizontal Records", recordClass=Record.class,
                terminateLabel="Terminate")
      public void setRecords(List<Record> records){
        ...
      }
    }

セルが見つからなかった場合はエラーとなりますが、`optional`パラメータに `true`を指定しておくと、無視して処理を続行します。

### @VerticalRecords

垂直方向に連続する列を`List`または配列にマッピングします。 要するに`@HorizontalRecords`を縦方向にしたものです。 `@HorizontalRecords`と同じく`List`型の引数を1つだけ取るsetterメソッドに対して付与します。

    @Sheet(name="Users")
    public class SheetObject {
      @VerticalRecords(tableLabel="Vertical Records", recordClass=Record.class)
      public void setRecords(List<Record> records){
        ...
      }
    }

`@HorizontalRecords`と同じく`terminal`パラメータ、 および`optional`パラメータを指定することもできます。

### @Column

`@HorizontalRecords`または`@VerticalRecords`で指定されたクラスのプロパティをカラム名にマッピングします。 setterメソッドに対して付与します。

    public class Record {
      @Column(columnName="ID")
      public void setId(String id){
        ...
      }
    }

同じ値がグループごとに結合されているカラムの場合は`merged`パラメータを`true`に設定します。 こうしておくと、前の列の値が引き継がれて設定されます。

    public class Record {
      @Column(columnName="Gender", merged=true)
      public void setId(String id){
        ...
      }
    }

見出し行が結合され、1つの見出しに対して複数の列が存在する場合は`headerMerged`プロパティを使用します。 `headerMerged`の値には列見出しから何セル分離れているかを指定します。

    public class User {
      @Column(columnName="連絡先")
      public void setMailAddress(String mailAddress){
        ...
      }
      @Column(columnName="連絡先", headerMerged=1)
      public void setTel(String tel){
        ...
      }
    }

### @MapColumns

`@HorozintalRecord`もしくは`@VerticalRecord`でカラム数が可変の場合に、 それらのカラムを`Map`として設定します。Beanには`Map`を引数に取る セッターメソッドを用意し、このアノテーションを記述します。

    @MapColumns(previousColumnName="名前")
    public void setAttributes(Map attributes){
      this.attributes = attributes
    }

`previousColumnName`パラメータで指定された次のカラム以降、カラム名をキーとした `Map`が生成され、Beanにセットされます。

### @PostProcess

`@Sheet`を付与したクラス、もしくは`@HorizontalRecords`や`@VerticalRecords` で指定したレコードクラスのメソッドに付与しておくと、シートの読み込みが終了した時点で呼び出されます。 なお、このアノテーションを付与するメソッドは引数を取らないpublicメソッドである必要があります。

    @Sheet(name="Users")
    public class SheetObject {
      @PostProcess
      public void postProcess(){
        ...
      }
    }

### @IterateTables

同一の構造の表がシート内で繰り返し出現する場合に使用します。 `tableLabel`プロパティで繰り返し部分の見出しラベル、 `tableClass`プロパティで繰り返し部分の情報を格納するJavaBeanを指定します。 また、`bottom`プロパティは`@IterateTables`内で `@HorizontalRecord`を使用する場合に、 テーブルの開始位置が`@IterateTables`の見出しセルからどれだけ離れているかを指定します。

    @Sheet(name="シート名")
    public class SheetObject {
      @IterateTables(tableLabel="部門情報", tableClass=Unit.class, bottom=2)
      public void setUnits(List<Unit> units){
        ...
      }
    }

繰り返し部分に対応するJavaBeanでは以下のように`@LabelledCell`や `@HorizontalRecord`などのアノテーションを使用することができます。 `@HorizontalRecord`を使用する場合、`tableLabel`プロパティには `@IterateTables`の`tableLabel`プロパティで指定したラベルと 同じラベルを指定する必要がある点に注意してください。

    public class Unit {
      @LabelledCell(label="部門名", type=LabelledCellType.Right)
      public void setUnitName(String unitName) {
        ...
      }
      @HorizontalRecords(tableLabel="部門情報", recordClass=UnitUser.class)
      public void setUnitUsers(List<UnitUser> unitUsers) {
        this.unitUsers = unitUsers;
      }
    }

繰り返し部分に対応するJavaBeanで`@HorizontalRecord`を使用した場合、 通常の場合と同じく`@Column`で列とのマッピングを行います。

    public class UnitUser {
      @Column(columnName="ID")
      public void setId(String id) {
        ...
      }
      @Column(columnName="名前")
      public void setName(String name) {
        ...
      }
    }

### 読み込み方

以下のようにして読み込みを行います。

    SheetObject sheet = new XLSBeans.load(
        new FileInputStream("example.xls"), SheetObject.class);

なお、`@Cell`, `@LabelledCell`, `@Column`アノテーションで マッピングするプロパティに関しては、現時点では`String`型、プリミティブ型、プリミティブ型の ラッパー型のいずれかである必要があります。より具体的な使用例はexampleフォルダ内のソースコードをご覧ください。

## シート上の位置情報の取得

アノテーションを付与したsetterメソッド名+Positionというメソッドを用意しておくと、 マッピングされたセル上の位置を取得することができます。

    @LabelledCell(label="Name")
    public void setName(String name){
      ...
    }

    public void setNamePosition(int x, int y){
      ...
    }

位置情報を取得用のsetterメソッドは以下のいずれかの引数を取る必要があります。

- int x, int y
- java.awt.Point

## XMLファイルによるマッピング

アノテーションだけではなく、外部XMLファイルでマッピングを行うことも可能です。 これはダイナミック・アノテーションという、アノテーションと同様の情報をXMLファイルで定義することで行います。 以下にクラスに対してアノテーションを付与するXMLファイルの例を示します。

    <?xml version="1.0" encoding="utf-8"?>
    <annotations>
      <class name="net.java.amateras.xlsbeans.example.SheetObject">
        <annotation name="net.java.amateras.xlsbeans.annotation.Sheet">
          <attribute name="name">'Users'</attribute>
        </annotation>
      </class>
    </annotations>

アノテーションの属性値の指定にはOGNL式を使用します。メソッドにアノテーションを付与する場合は次のようになります。

    <?xml version="1.0" encoding="utf-8"?>
    <annotations>
      <class name="net.java.amateras.xlsbeans.example.SheetObject">
        <annotation name="net.java.amateras.xlsbeans.annotation.Sheet">
          <attribute name="name">'Users'</attribute>
        </annotation>
        <method name="setTitle">
          <annotation name="net.java.amateras.xlsbeans.annotation.LabelledCell">
            <attribute name="label">'Title'</attribute>
            <attribute name="type">@net.java.amateras.xlsbeans.annotation.LabelledCellType@Right</attribute>
          </annotation>
        </method>
      </class>
    </annotations>

XLSBeansでは使用しませんが、フィールドにアノテーションを付与することも可能です。

    <?xml version="1.0" encoding="utf-8"?>
    <annotations>
      <class name="net.java.amateras.xlsbeans.example.SheetObject">
        <field name="setTitle">
          <annotation name="...">
            ...
          </annotation>
        </field>
      </class>
    </annotations>

外部XMLファイルを使う場合、ハードコードされたアノテーションを外部XMLファイルの内容でオーバーライドすることが可能です。 読み込み時は以下のようにExcelファイルとXMLファイルの両方をXMLBeans#load()メソッドに渡します。

    SheetObject sheet = XLSBeans.load(
      new FileInputStream("example.xls"),
      new FileInputStream("example.xml"), SheetObject.class);

なお、AnnotationReaderクラスを使用することで、XLSBeansのダイナミック・アノテーション 機能を別のプログラムでも利用することが可能です。

    // XMLファイルの読み込み
    XMLInfo info = XMLLoader.load(new FileInputStream("example.xml"));

    // AnnotationReaderのインスタンスを作成
    AnnotationReader reader = new AnnotationReader(info);

    // SheetObjectクラスに付与されたSheetアノテーションを取得
    Sheet sheet = reader.getAnnotation(SheetObject.class, Sheet.class);

`Class`や`Method`、`Field`オブジェクトから直接アノテーションを 取得する代わりに`AnnotationReader`を使えば、XMLで宣言されたアノテーションと、クラスに 埋め込まれているアノテーションを区別せずに取得することができます。`AnnotationReader`には この他にもメソッド、フィールドに付与されたアノテーションを取得するためのメソッドも用意されています。

## FieldProcessorの拡張

XLSBeansでは`FieldProcessor`インターフェースを実装したクラスによってアノテーションの処理を行います。 デフォルトではこのドキュメントに記述されているアノテーション用の`FieldProcessor`が用意されていますが、 独自アノテーションを定義しこれに対応した`FieldProcessor`をXLSBeansに追加することが可能です。

独自のアノテーションおよび、`FieldProcessor`をXLSBeansに登録するにはクラスパスのルートに `xlsbeans.properties` というプロパティファイルを作成し、 アノテーションのクラス名と`FieldProcessor`実装クラスのクラス名を以下のように列挙します。 なお、この方法で登録を行う場合、`FieldProcessor`実装クラスは引数なしのコンストラクタを持っている必要がある点に注意してください。

    # アノテーションのクラス名 = FieldProcessorのクラス名
    xlsbeans.sample.SampleAnnotation = xlsbeans.sample.SampleFieldProcessor

また、Javaソースコード中で直接ファクトリに`FieldProcessor`実装クラスを登録することも可能です。

    FieldProcessorFactory.registerProcessor(
        SampleAnnotation.class, new SampleFieldProcessor());