[DDD]Apache POIでExcelドメインを作成する

## はじめに
DDD×Apache POI でエクセル出力ドメインを作成していきます

## Apache POIとは
> Apache POI(以前はJakarta POIと呼ばれていました)はJavaアプリケーションからExcelやWordなどのMicrosoft製品のフォーマットファイルを読み書きするためのAPIです。

https://www.javadrive.jp/poi/

## 環境
Java 11
Spring Boot 2.6.1
Gradle 2.24
Apache POI 4.1.2

## 設定
poi-ooxmlも追加することで、 `xlsx`、`dock`といった2007形式のファイルの読み書きが可能となります。

```build.gradle
dependencies {

    implementation('org.apache.poi:poi:4.1.2')
    implementation('org.apache.poi:poi-ooxml:4.1.2')

}
```

## エクセルドメイン
ここからDDDの出番です
まずは、`Excel出力`の業務仕様をモデル化し、コードで表記していきます
今回実現したい業務は以下です
・ Excelファイルを作成する
・ Excelファイルをダウンロードする

必要な構成要素は以下です
・拡張子
・ファイル名
・シート名
・タイトル一覧
・タイトル行フォント種別
・タイトル行太字設定
・タイトル行横位置
・タイトル行縦位置
・データ一覧
・データ行フォント種別

`Excel出力`クラスをただのデータの入れ物ではなく、モデルを正確に表すため、
属性以外の情報(ファイル作成ロジック等)も作成していきます
出力内容毎に構成要素を追加できるよう抽象クラスにしています


```Excel.java
@RequiredArgsConstructor
public abstract class Excel {

    /** ファイル拡張子 */
    private static final String EXTENSION = ".xlsx";
    /** 日時フォーマッタ */
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
    /** タイトル行フォント種別 */
    private static final FontType titleFontType = FontType.MSP_GOTHIC;
    /** タイトル行太文字フラグ */
    private static final boolean isBoldTitle = true;
    /** タイトル行横位置 */
    private static final HorizontalAlignment titleHorizontalAlignment = HorizontalAlignment.CENTER;
    /** タイトル行縦位置 */
    private static final VerticalAlignment titleVerticalAlignment = VerticalAlignment.CENTER;
    /** データ行フォント種別 */
    private static final FontType bodyFontType = FontType.MSP_GOTHIC;

    /** タイムスタンプ */
    private final String TIMESTAMP = LocalDateTime.now().format(FORMATTER);

    private final ServletContext context;
    private final HttpServletRequest request;

    /**
     * ファイル名（先頭のみ）を取得します。
     * @return ファイル名（先頭のみ）
     */
    abstract public String getFileNamePrefix();

    /**
     * シート名を取得します。
     * @return シート名
     */
    abstract public String getSheetName();

    /**
     * タイトル行に出力するタイトル一覧を取得します。
     * @return タイトル一覧
     */
    abstract public List<String> getTitles();

    /**
     * データ行に出力する行列の値一覧を取得します。
     * @return 行列の値一覧
     */
    abstract public List<List<String>> getMatrixValues();

    /**
     * ファイルを作成します。
     * @return ファイル作成成功の場合はtrue
     */
    public boolean createFile() {
        final FileOutputStream outputStream;
        try {
            outputStream = new FileOutputStream(getFilePath());
        } catch (FileNotFoundException e) {
            return false;
        }

        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFSheet sheet = workbook.createSheet(getSheetName());

        int rowNum = 0;

        // タイトル行書き込み
        Row headerRow = sheet.createRow(rowNum);
        XSSFCellStyle titleStyle = createTitleStyle(workbook);
        for (int i = 0; i < getTitles().size(); i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(getTitles().get(i));
            cell.setCellStyle(titleStyle);
        }

        // データ行書き込み
        for (List<String> columnValues : getMatrixValues()) {
            rowNum++;

            // フォント設定
            XSSFFont font = workbook.createFont();
            font.setFontName(bodyFontType.getText());
            CellStyle bodyStyle = workbook.createCellStyle();
            bodyStyle.setFont(font);
            bodyStyle.setLocked(false);

            // データ書き込み
            XSSFRow row = sheet.createRow(rowNum);
            for (int j = 0; j < columnValues.size(); j++) {
                row.createCell(j).setCellValue(columnValues.get(j));
            }
        }

        // 列サイズ自動調整
        for (int i = 0; i < sheet.getRow(0).getPhysicalNumberOfCells(); i++) {
            sheet.autoSizeColumn(i);
        }

        // ファイル出力
        try {
            workbook.write(outputStream);
            outputStream.flush();
            outputStream.close();
            workbook.close();
        } catch (IOException e) {
            return false;
        }

        return true;
    }

    /**
     * ファイルをダウンロードします。
     * @return ファイルデータ
     */
    public HttpEntity<byte[]> downloadFile() {
        File file = new File(getFilePath());
        if (file.exists()) {
            try {
                FileInputStream inputStream = new FileInputStream(file);
                val fileData = IOUtils.toByteArray(inputStream);

                val fileTypeMap = new MimetypesFileTypeMap();
                val headers = new HttpHeaders();
                val mimeType = fileTypeMap.getContentType(getFilePath());
                val encodedFilename = URLEncoder.encode(getFileName(), StandardCharsets.UTF_8);
                headers.setContentType(MediaType.valueOf(mimeType));
                headers.setContentDisposition(ContentDisposition.parse(
                        "attachment; filename=\"" + getFileName() + "\";filename*=utf-8''" + encodedFilename
                ));
                return new HttpEntity<>(fileData, headers);

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                file.delete();
            }
        }

        return new HttpEntity<>(new byte[]{});
    }

    /**
     * ファイル名を取得します。
     * @return ファイル名
     */
    private String getFileName() {
        return getFileNamePrefix() + "-" + TIMESTAMP + EXTENSION;
    }

    /**
     * ファイルパスを取得します。
     * @return ファイルパス
     */
    private String getFilePath() {
        String contextPath = context.getRealPath(request.getContextPath());
        File contextDirectory = new File(contextPath);
        if (!contextDirectory.exists()) {
            contextDirectory.mkdirs();
        }
        return contextDirectory + "/" + getFileName();
    }

    /**
     * タイトル行のセルスタイルを設定します。
     * @param workbook 対象ブック
     * @return セルスタイル
     */
    private XSSFCellStyle createTitleStyle(XSSFWorkbook workbook) {
        XSSFFont font = workbook.createFont();
        font.setBold(isBoldTitle);
        font.setFontName(titleFontType.getText());
        XSSFCellStyle style = workbook.createCellStyle();
        style.setAlignment(titleHorizontalAlignment);
        style.setVerticalAlignment(titleVerticalAlignment);
        style.setFont(font);
        return style;
    }


    /**
     * フォント種別
     */
    @AllArgsConstructor
    @Getter
    public enum FontType {
        MSP_GOTHIC("ＭＳ Ｐゴシック (本文)"),
        ;
        private final String text;
    }
}
```

## Excelドメインを継承する
Excelドメインを継承し、ファイル名のセットや、出力データのセットをしていきます。

```SampleExcel.java
public class SampleExcel extends Excel {

    private static final String FILE_NAME = "サンプルファイル";
    private static final String SHEET_NAME = "サンプル一覧";
    private static final List<String> TITLES = List.of("名称", "登録日時");
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

    @Setter
    private List<SampleDto> dtoList;

    public SampleExcel(ServletContext context, HttpServletRequest request) {
        super(context, request);
    }

    @Override
    public String getFileNamePrefix() {
        return FILE_NAME;
    }

    @Override
    public String getSheetName() {
        return SHEET_NAME;
    }

    @Override
    public List<String> getTitles() {
        return TITLES;
    }

    @Override
    public List<List<String>> getMatrixValues() {
        val rows = new ArrayList<List<String>>();
        for (SampleDto dto : dtoList) {
            rows.add(getColumnValues(dto));
        }
        return rows;
    }

    /**
     * カラムに出力する値一覧を取得します。
     * @param dto サンプルDTO
     * @return カラムに出力する値一覧
     */
    private List<String> getColumnValues(SampleDto dto) {
        val columns = new ArrayList<String>();
        columns.add(dto.getName());
        columns.add(dto.getRegisterTime().format(FORMATTER));
        return columns;
    }
}

```

## 最後に
内容的に至らない部分もあると思います。
間違っている、気になる等ありましたら、優しく諭していただけるととても喜びます。