Jxls 使い方メモ

# Jxls とは
Excel 帳票を生成するための Java ライブラリ。

Excel ファイルをテンプレートファイルとして使用できる。

# Hello World
## インストール
```groovy:build.gradle
dependencies {
    compile 'org.jxls:jxls-poi:1.0.11'
}
```


## Excel テンプレート
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/072c2b15-c924-b3b8-0826-52159cf265ea.jpeg)

## 実装
```java
package sample.jxls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;

public class Main {

    public static void main(String[] args) throws Exception {
        try (InputStream in = new FileInputStream(new File("./excel/template.xlsx"));
             OutputStream out = new FileOutputStream(new File("./excel/out.xlsx"))) {
            
            Context context = new Context();
            context.putVar("message", "Hello Jxls!!");
            
            JxlsHelper.getInstance().processTemplate(in, out, context);
        }
    }
}
```

## 実行結果
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/10b7e5b0-ddff-b52a-84fc-113434edd0e9.jpeg)

## 説明
### 依存関係
```groovy:build.gradle
dependencies {
    compile 'org.jxls:jxls:2.2.9'
    compile 'org.jxls:jxls-poi:1.0.8'
}
```

- コア API と、具体的な Excel ライブラリとの連携を実装したモジュールを依存関係に追加する。
- Jxls のコア API は、具体的な Excel ライブラリには依存しない形になっている。
- Jxls は [Apache POI](https://poi.apache.org/) と [Java Excel API](http://jexcelapi.sourceforge.net/) の２つのライブラリをサポートしている。
- ロギング API に Slf4j を使って、具体的な実装に logback を使う、みたいな感じ。

### テンプレート
- テンプレートは Excel で作成する。
- `${...}` という形式の式言語で、変数の埋め込みなどを宣言できる。
    - [Apache の JEXL](http://commons.apache.org/proper/commons-jexl/) という式言語用のライブラリを使っているらしい。
- 式言語による動的な埋め込みは、 **XLS Area** という特定の領域内でのみ有効になる。
    - XLS Area を定義するための方法の一つとして、 Excel のセルコメントを使った方法が用意されている。
    - `jx:area=(lastCell="A1")` は、現在のセルから `A1` まで（つまり `A1` セルのみ）を XLS Area にするという宣言になる。

### 実行
- `JxlsHelper` というクラスを使ってテンプレートから Excel 帳票の生成を実行する。
- テンプレートには、 `Context` クラスを使って任意の値を渡すことができる。

# 式言語
## 実験
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/43f71006-992f-c1d0-f262-fb63eb28eb71.jpeg)

↓ 出力結果

![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/e6f45e2d-e6de-4200-71de-41c314d8b658.jpeg)

## まとめ
- `public` なフィールド、またはプロパティ（`Getter` 経由で取得できる値）のみ参照可能。
- 存在しないフィールドやメソッドを参照してもエラーにはならず、空文字扱いになる。
- プリミティブ型や `String`, `Date`, `BigInteger`, `BigDecimal` 型の値は、そのまま出力される。
    - `Date` 型は、セルの書式を日付にしておけば、日付形式で表示される。
    - （`null` を含む）それ以外の値は、セルに出力されない（罫線が消えてる。。。）
- オブジェクトの関連を `.` で連結させてたどることができる。
- `Map` のキーも、プロパティのように `.` 区切りで参照できる。

## 補足
```java:Hoge.java
package sample.jxls;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class Hoge {
    
    public String publicField = "Public Field";
    protected String protectedField = "Protected Field";
    String packageField = "Package Field";
    private String privateField = "Private Field";
    
    public String getPublicProperty() {
        return "Public Property";
    }
    
    protected String getProtectedProperty() {
        return "Protected Property";
    }
    
    String getPackageProperty() {
        return "Package Property";
    }
    
    private String getPrivateProperty() {
        return "Private Property";
    }
    
    public String getNull() {
        return null;
    }
    
    public int getInt() {
        return 10;
    }
    
    public long getLong() {
        return 100;
    }
    
    public float getFloat() {
        return 12.3f;
    }
    
    public double getDouble() {
        return 45.6d;
    }
    
    public boolean getBoolean() {
        return true;
    }
    
    public BigInteger getBigInteger() {
        return new BigInteger("1234");
    }
    
    public BigDecimal getBigDecimal() {
        return new BigDecimal("345.67");
    }
    
    public String getString() {
        return "String Value";
    }
    
    public String getEmptyString() {
        return "";
    }
    
    public Date getDate() {
        return new Date();
    }
    
    public LocalDate getLocalDate() {
        return LocalDate.now();
    }
    
    public String[] getArray() {
        return new String[] {"a", "b", "c"};
    }
    
    public List<String> getList() {
        return Arrays.asList("A", "B", "C");
    }
    
    public Map<String, String> getMap() {
        Map<String, String> map = new HashMap<>();
        map.put("foo", "FOO");
        return map;
    }
    
    public Optional<String> getOptional() {
        return Optional.of("Optional Value");
    }
    
    public String method() {
        return "Method Value";
    }
    
    public Fuga getFuga() {
        return new Fuga();
    }
}
```

```java:Fuga.java
package sample.jxls;

public class Fuga {
    
    public Piyo getPiyo() {
        return new Piyo();
    }
}
```

```java:Piyo.java
package sample.jxls;

public class Piyo {
    
    public int getId() {
        return 999;
    }
}
```

# Each-Command：繰り返し
## 基本

**テンプレート**

![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f633a24d-9c8b-ab40-2976-f8dad9ba35fb.jpeg)

**実装**

```java
package sample.jxls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;

public class Main {

    public static void main(String[] args) throws Exception {
        try (InputStream in = new FileInputStream(new File("./excel/template.xlsx"));
             OutputStream out = new FileOutputStream(new File("./excel/out.xlsx"))) {
            
            Context context = new Context();
            context.putVar("list", Arrays.asList(1, 2, 3));
            
            JxlsHelper.getInstance().processTemplate(in, out, context);
        }
    }
}
```

**実行結果**

![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1d453423-5f88-8b0e-66e5-f2a27fe7be7b.jpeg)

- 繰り返しには、 Each-Command を使用する。
- 記法は、 `jx:each(<options>)`
- `<options>` には、以下の値を設定している。
    - `items`
        - 繰り返し処理対象のコレクション。
    - `lastCell`
        - 繰り返しの範囲を指定するためのセル座標。
        - コメントのあるセルから、ここで指定したセルまでの領域が繰り返し処理される。
- Each-Command を実行する範囲が XLS Area に含まれるように `jx:area` で範囲を指定する必要がある。
    - もし `jx:area(lastCell="D1")` のように XLS Area が繰り返し範囲に収まっていないと、繰り返し処理は動作しなくなる。

## 繰り返しの範囲を限定する
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/bd5658fd-982b-4800-4c7c-04c65cdcfbd7.jpeg)

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b029b87e-591b-59df-3af5-265e9da54f67.jpeg)

- XLS Area に収まっていれば、 `lastCell` は `jx:area` と一致させる必要はない

## 繰り返し中の各要素を取得する
**テンプレート**
![jxl.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d7ceb84f-18ab-292d-7bb0-16bae2b18f8b.jpeg)

**出力結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/86260ec5-109c-cf21-9795-133b6879be3e.jpeg)

- `var` で繰り返し中の各要素を格納する一時変数名を定義できる。

## 繰り返しの向きを指定する
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/37e84f01-3c04-7676-f150-c4f102b0faf6.jpeg)

**出力結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b42becf0-3f17-55be-4834-13d3bbcda43c.jpeg)

- `directrion` で、繰り返しの向きを指定できる。
- `RIGHT` で右方向に繰り返す。
- デフォルトは `DOWN` で下方向に繰り返す。

## 結合されたセルを繰り返し処理
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/849bbebf-2db1-df3e-bceb-e6e6a608e1d6.jpeg)

**出力結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b5aa6c5f-b17e-ef59-7c8a-34f62afaf832.jpeg)

- 繰り返ししようとしているセルが結合されている場合は、「セルが結合されていない場合の位置」を `lastCell` に指定すると、うまく繰り返しできる。
    - `lastCell="C1"` とかにしちゃうと、残念なことになる。

# If-Command：条件による表示の切り替え
## 基本
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/9506ffd0-c2b4-15d3-52a8-f9b6debe8699.jpeg)

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/16043fcf-79ff-58e6-1c62-4283f90c5b43.jpeg)

- 条件分岐には If-Command を使用する。
- `condition` で条件式を指定する。
    - Java 側で `Context` に設定した値を参照することもできる。

## 条件が満たされたときの領域と満たされなかったときの領域を指定する
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/6ca39d73-7c73-d2ba-3cd3-a67d8c8215cf.jpeg)

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/221711a3-1a97-86d6-82e2-d43ada1e68a7.jpeg)

- `areas` で、条件が真のときと偽のときで、それぞれの参照先の領域を指定できる。
- 指定は配列形式で、 `["真のときの領域", "偽のときの領域"]` という形で指定する。

# Grid-Command：表を生成する
## 基本
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/16e47e59-6f76-353d-e47b-a84469cd1d09.jpeg)

**実装**

```java
package sample.jxls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;

public class Main {

    public static void main(String[] args) throws Exception {
        try (InputStream in = new FileInputStream(new File("./excel/template.xlsx"));
             OutputStream out = new FileOutputStream(new File("./excel/out.xlsx"))) {
            
            Context context = new Context();
            context.putVar("gridHeaders", Arrays.asList("ほげ", "ふが", "ぴよ"));
            context.putVar("gridData", Arrays.asList(
                new Main("hoge1", "fuga1", "piyo1"),
                new Main("hoge2", "fuga2", "piyo2"),
                new Main("hoge3", "fuga3", "piyo3")
            ));
            
            JxlsHelper.getInstance().processTemplate(in, out, context);
        }
    }
    
    private String hoge;
    private String fuga;
    private String piyo;
    
    public Main(String hoge, String fuga, String piyo) {
        this.hoge = hoge;
        this.fuga = fuga;
        this.piyo = piyo;
    }

    public String getHoge() {
        return hoge;
    }

    public String getFuga() {
        return fuga;
    }

    public String getPiyo() {
        return piyo;
    }
}
```

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/7651fa8e-5425-1599-ada8-438a4911cb17.jpeg)

- Grid-Command を使うと、表の出力をループなどを使わず少ない記述で実現できる。
- 表を出力する部分に、 `${header}` と `${cell}` を記述する。
    - `${header}` が、タイトルが出力される部分になり、
    - `${cell}` が、各値が出力される部分になる。
- `jx:grid` には以下の設定をする。
    - `headers` には、ヘッダーに出力するテキストをリストで渡す。
    - `data` には、データ領域に出力する情報をリストで渡す。
        - 各行をオブジェクトで設定する場合は、各行の値はプロパティとして取得できるようにしておかなければならない。
        - 単純にフィールドを `public` にしているだけだと、エラーになった。
    - `props` で、 `data` で指定したオブジェクトから各行の値を取得するときのプロパティ名を指定する。
        - 各プロパティの名前は、半角カンマで区切る。
    - `areas` で、処理対象となる範囲を指定する。
        - 配列形式で、 `["ヘッダー領域", "データ領域"]` という形で指定する。

## 型ごとにセル書式を指定する
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/bc531605-2fbf-0e02-1d90-308bcb121d37.jpeg)

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b28449dd-68af-574e-78c0-f5bdfe194a6c.jpeg)

- `formatCells` で、型ごとにセル書式を指定できる。
- `型名:参照するセル` という形で指定する。
- 「参照するセル」は、セル書式のコピー元となるセルの位置を指定する。
- 複数の型に対して書式を指定したい場合は、半角カンマ区切りで列挙する。

# Image-Command：画像を埋め込む
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/3d5f9493-6b2e-249c-a98f-4f313cc3353c.jpeg)

**貼り付ける画像**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/28302/7a9347c3-a47f-7f6d-90bf-33da3fa41a19.png)

**実装**

```java
package sample.jxls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;
import org.jxls.util.Util;

public class Main {

    public static void main(String[] args) throws Exception {
        try (InputStream in = new FileInputStream(new File("./excel/template.xlsx"));
             OutputStream out = new FileOutputStream(new File("./excel/out.xlsx"));
             InputStream image = new FileInputStream(new File("./img/image.png"))) {
            
            byte[] imageBytes = Util.toByteArray(image);
            
            Context context = new Context();
            context.putVar("myImage", imageBytes);
            
            JxlsHelper.getInstance().processTemplate(in, out, context);
        }
    }
}
```

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ffdbcbe0-0ade-55a4-c745-0903ab1823c6.jpeg)

- 画像を動的に貼り付けるには Image-Command を使用する。
- `jx:image` には、以下のオプションを指定する。
    - `src` には、貼り付ける画像の `byte` 配列を渡す。
        - `byte` 配列は、 Jxls が提供している `Util` を使うと作成できる。
    - `imageType` には、画像の種類を渡す。
    - `lastCell` で指定したセルの左上まで、画像を伸縮させたものが貼り付けられる。

# コマンドを自作する
**テンプレート**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/6b6e3376-116f-199e-59ba-02f85818bcac.jpeg)

**実装**

```java:MyCommand.java
package sample.jxls;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.jxls.area.Area;
import org.jxls.command.AbstractCommand;
import org.jxls.command.Command;
import org.jxls.common.CellRef;
import org.jxls.common.Context;
import org.jxls.common.Size;
import org.jxls.transform.poi.PoiTransformer;

public class MyCommand extends AbstractCommand {
    
    private Area area;
    private String value;
    
    @Override
    public String getName() {
        return "myCommand";
    }

    @Override
    public Size applyAt(CellRef cellRef, Context context) {
        PoiTransformer transformer = (PoiTransformer)area.getTransformer();
        Workbook wb = transformer.getWorkbook();
        Sheet sheet = wb.getSheet(cellRef.getSheetName());
        int rownum = cellRef.getRow();
        int colnum = cellRef.getCol();
        
        Row row = sheet.getRow(rownum);
        Cell cell = row.getCell(colnum);
        
        cell.setCellValue("<<" + this.value + ">>");
        
        return this.area.getSize();
    }
    
    @Override
    public Command addArea(Area area) {
        super.addArea(area);
        this.area = area;
        return this;
    }
    
    public void setValue(String value) {
        this.value = value;
    }
}
```

```java:Main.java
package sample.jxls;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import org.jxls.builder.xls.XlsCommentAreaBuilder;
import org.jxls.common.Context;
import org.jxls.util.JxlsHelper;

public class Main {

    public static void main(String[] args) throws Exception {
        try (InputStream in = new FileInputStream(new File("./excel/template.xlsx"));
             OutputStream out = new FileOutputStream(new File("./excel/out.xlsx"))) {
            
            XlsCommentAreaBuilder.addCommandMapping("myCommand", MyCommand.class);
            Context context = new Context();
            JxlsHelper.getInstance().processTemplate(in, out, context);
        }
    }
}
```

**実行結果**
![jxls.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/aa4dc4e1-c9dc-87b3-0b84-971f2c47f47f.jpeg)

- 詳細な説明は公式サイトにも載っていないので、デモにあった [GroupRowCommand](https://bitbucket.org/leonate/jxls-demo/src/bf8abea25a2e86b2e21d0067b1b6e7eb737a7274/src/main/java/org/jxls/demo/GroupRowCommand.java?at=master&fileviewer=file-view-default) から推測して作成している。
- 自作のコマンドは、 `AbstractCommand` クラスを継承して作成する。
- `addArea()` メソッドをオーバーロードして、追加された `Area` を自作のコマンドクラスのインスタンス変数として保存しておく。
- 任意のセッターメソッドを作成することで、パラメータとして値を渡せるようになる。
- `applyAt()` メソッドないで、コマンドの処理を実装する。
    - `Area` から POI の API にアクセスするための `Transformer` が取得できる。
    - `PoiTransformer.getWorkbook()` で `Workbook` オブジェクトが取得できる。
    - あとは、 POI の処理を実装する。
    - 最後に、コマンド処理を実行したあとの Area のサイズを返す。
    - 今回は特に大きさは変更していないので問題ないが、変更している場合は変更後の値を計算して返すようにする必要があるっぽい。
- `XlsCommentAreaBuilder.addCommandMapping()` で自作のコマンドを登録できる。

# 参考
- [JXLS – Jxls v2.2.9 is released!](http://jxls.sourceforge.net/index.html)