この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

POIのXmlValueDisconnectedExceptionを解消する


XmlValueDisconnectedException。
POIでOOXMLを使う際、避けては通れない関門のようです。


僕の環境でも、いつの間にか発生するようになってしまいました。
単にCell#getCellStyle()を呼んでいるだけなのに発生する始末。

というわけで、半日かけて原因調査です。

原因


僕のケースでは、Workbook#write(OutputStream)を呼び出していたことが原因でした。


何故そのような仕様なのかわかりませんが、Workbook#write(OutputStream)を一度でも呼び出すと、そのワークブックは内部的に「使用済み」的な状態になる(クローズ済みのストリームのようなもの)ようです。


従ってそれ以降の処理でXmlValueDisconnectedExceptionが発生するようになってしまいます。


以下の様な使い方をしていました。

class FooReportingLogic {

    public void doSomething(final InputStream data) throws Exception {

        final Workbook original = new XSSFWorkbook(data);
        final Workbook cloned = WorkbookUtil.cloneBlankWorkbook(original);

        for (final Sheet sheet : sheetsOf(original)) {
            for (final Row row : sheet) {
                for (final Cell cell : row) {

                    // XmlValueDisconnectedExceptionが発生!!
                    final CellStyle orginalStyle = cell.getCellStyle();
                    
                    // ...(略)
                }
            }
        }

        // ...(略)
    }

    /** シートのリストを取得する */
    private static List<Sheet> sheets(Workbook book) {
        // ...(略)
    }
}

/** このクラスは何らかの事情で変更不可とする */
final class WorkbookUtil {
    /**
     * ワークブックを複製し、シートを全て削除してまっさらにする。
     */
    public static Workbook cloneBlankWorkbook(final Workbook book) throws Exception {

        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // ここで元のワークブックのwriteを呼び出してしまっているため、
        // 元のワークブックは「使用済み」になってしまう
        book.write(baos);

        final Workbook cloned = new XSSFWorkbook(
            new ByteArrayInputStream(baos.toByteArray()));
        
        for (int i = cloned.getNumberOfSheets(); 0 < i; i--) {
            cloned.removeSheetAt(i - 1);
        }

        return cloned;
    }
}

対応策

要するにWorkbook#writeを呼んでしまったワークブックを再利用しなければ良いだけということで、以下のようにしました。

class FooReportingLogic {

    public void doSomething(final InputStream data) throws Exception {

        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        IOUtils.copy(data, baos);

        final Workbook original = new XSSFWorkbook(new ByteArrayInputStream(baos.toByteArray()));

        // 複製元としてoriginalを使わないようにする
        final Workbook cloned = WorkbookUtil.cloneBlankWorkbook(
            new XSSFWorkbook(new ByteArrayInputStream(baos.toByteArray())));

        // ...(略)
    }

    // ...(略)
}