POIは、JavaからMicrosoftの2007形式のExcel(xlsx)やWord(docx)のファイルも扱えるAPIで、オープンソースのため誰でも無償で利用できます。

最初に、クラス「ReportData 」は各納入先に対応するデータを保持するオブジェクトで、クラス「HeaderData 」と「DetailsData 」で構成します。Excelファイルを作成する前にSetterによりデータを設定し、ワークシートのループ形成時に「$変数名」や「$変数名[]」をkeyとして、Getterにより取得し、対応するセルに指定された値を設定します。

public class HeaderData {
    private String reportName = null;
    private Map<String, String> dataMap = new HashMap<String, String>();

    各メンバ変数のSetter/Getterの定義
}

public class DetailsData {
    private int numOfDtails = 0;
    private Map<String, Object[]> dataListMap = new HashMap<String, Object[]>();

    各メンバ変数のSetter/Getterの定義
}

public class ReportData {
    private HeaderData header;
    private DetailsData details;

    各メンバ変数のSetter/Getterの定義
}

それでは、POIを使用して、テンプレートシートに基づき、上記で定義したクラス「ReportData 」の値をエクセルのワークシートに設定します。

まず、テンプレートシートのシート名である「TEMPLATE」を引数に渡して、テンプレートシートのシートインデックスをWorkbookインターフェースのメソッド「getSheetIndex」で取得後、同じくWorkbookインターフェースのメソッド「getPrintArea」を利用してテンプレートシートに設定されている印刷範囲を取得します。印刷範囲はPOIの仕様で「TEMPLATE!$A$1:$D$27」のようにシート名が自動で追加されているため、シート名を除いた印刷範囲「$A$1:$D$27」を取得する処理が続きます。

// 印刷範囲を取得
String printArea = wb.getPrintArea(wb.getSheetIndex("TEMPLATE"));
if (printArea != null) {
    int sheetPosition = printArea.indexOf("!");
    if (sheetPosition != -1) {
        printArea = printArea.substring(sheetPosition + 1);
    } else {
        printArea = null;
    }
}

納入先分シートをコピーし、ワークシート名として納入先の名称を設定する。

if (reportList.size() > 1) {
    for (int reportIndex = 0; reportIndex < reportList.size(); reportIndex++) {
	// テンプレートシートを納入先数分シートコピーする
	Sheet cloneSheet = wb.cloneSheet(wb.getSheetIndex("TEMPLATE"));
	// ワークシート名として納入先の名称を設定
	wb.setSheetName(wb.getSheetIndex(cloneSheet), 
          reportList.get(reportIndex).getHeader().getReportName());
	// 取得した印刷範囲をシートコピーして作成したワークシートの印刷範囲として設定
	if (printArea != null) {
	     wb.setPrintArea(wb.getSheetIndex(cloneSheet), printArea);
        }
    }
    // テンプレートシートの削除
    wb.removeSheetAt(wb.getSheetIndex("TEMPLATE"));
}

ワークブックオブジェクトに作成されているワークシート単位(納入先単位)に納品書の作成処理をするようにループ処理をしています。
設計として、伝票番号のように納品書の作成ごとに値が変動する箇所には「$変数名」の形式でセルに値を設定しています。さらに、納入する商品情報のように作成ごとに値と表示する数が変動する箇所には「$変数名[]」の形式でセルに、文字列(String)の値をあらかじめ設定しておきます。

「headerMap」はテンプレートシートに設定した「$変数名」をkeyとして持ち、「$変数名」への置き換えの値をvalueとして格納しているマップです。「detailsMap」はテンプレートシートに設定した「$変数名[]」をkeyとして持ち、置き換え用の一連の明細の値をオブジェクトの配列形式でvalueに格納しているマップです。

印刷範囲に含まれるセルで、セルが文字列型の値を格納している場合は、該当のセルがデータを置き換えるために用意された「$変数名」または「$変数名[]」かどうかを確認し、該当する場合は、「$変数名」はsetCellValueメソッドを、「$変数名[]」はsetCellValuesメソッドをそれぞれ呼んでいます。

ReportData reportData = null;
Map<String, String> headerMap = null;
Map<String, Object[]> detailsMap = null;

// ワークシート単位の繰返し処理
int numOfSheet = wb.getNumberOfSheets();
for (int sheetIndex = 0; sheetIndex < numOfSheet; sheetIndex++) {
    Sheet sheet = wb.getSheetAt(sheetIndex);

    // ワークシートに対応するデータを取得
    reportData = reportList.get(sheetIndex);
    headerMap = reportData.getHeader().getDataMap();
    detailsMap = reportData.getDetails().getDataListMap();
    if (reportData.getDetails().getNumOfDetails() != 0) {
        numOfDetails = reportData.getDetails().getNumOfDetails();
    }

    // 行単位の繰返し処理
    int lastRow = sheet.getLastRowNum();
    for (int rowIndex = 0; rowIndex <= lastRow; rowIndex++) {
        Row row = sheet.getRow(rowIndex);
        if (row == null) {
           continue;
        }

	// セル単位の繰返し処理
        int lastColumn = row.getLastCellNum();
        for (int columnIndex = 0; columnIndex < lastColumn; columnIndex++) {
           Cell cell = row.getCell(columnIndex);
           if (cell == null) {
                continue;
           }

           if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
                // 値を置換える為に準備したセルであればデータを設定
                String key = cell.getStringCellValue();
                if (headerMap.containsKey(key)) {
                    setCellValue(cell, headerMap.get(key));
                } else if (detailsMap.containsKey(key)) {
                    setCellValues(cell, sheet, detailsMap.get(key));
                }
           }
        }
     }
}

セルに設定したいオブジェクトの型に応じてキャストし直し、セルに値を設定しています。この例ではすべてString valueとなります。Numberクラスは、Byte、Short、Integer、Long、Float、Double、BigInteger、BigDecimalのスーパークラスです。

// セルに値を設定
private void setCellValue(Cell cell, Object value) {
    CellStyle style = cell.getCellStyle();
    if (value != null) {
        if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof Number) {
	    Number numValue = (Number) value;
            if (numValue instanceof Float) {
                Float floatValue = (Float) numValue;
                numValue = new Double(String.valueOf(floatValue));
            }
            cell.setCellValue(numValue.doubleValue());
        } else if (value instanceof Date) {
            Date dateValue = (Date) value;
            cell.setCellValue(dateValue);
        } else if (value instanceof Boolean) {
            Boolean boolValue = (Boolean) value;
            cell.setCellValue(boolValue);
        }
    } else {
        cell.setCellType(Cell.CELL_TYPE_BLANK);
        cell.setCellStyle(style);
    }
}

setCellValuesメソッドは、明細データの数(この例では納品する商品の種類)分、ループ処理をしています。この場合、行に対して配列で指定されたvalueの値を順次設定します。実際のcellへの設定は、上記で説明したsetCellValueメソッドを使用します。

[dropcap style=”box”]注[/dropcap]なぜか、複数のワークシートを作成するとき、ループをさせると、Sheet cloneSheet = wb.cloneSheet(wb.getSheetIndex(“TEMPLATE”))で、次の例外が発生します。Poiのバージョンはpoi-bin-3.10-FINAL-20140208を使用しています。後日調査します。

Exception in thread “main” java.lang.IllegalArgumentException: Sheet index (-1) is out of range (0..0)
	at org.apache.poi.xssf.usermodel.XSSFWorkbook.validateSheetIndex(XSSFWorkbook.java:1061)
	at org.apache.poi.xssf.usermodel.XSSFWorkbook.cloneSheet(XSSFWorkbook.java:436)
	at org.apache.poi.xssf.usermodel.XSSFWorkbook.cloneSheet(XSSFWorkbook.java:81)
	at controller.SimpleReportCreator.create(SimpleReportCreator.java:46)
	at main.sample.main(sample.java:84)