jXLS Readerを使ってアップロードされたExcelファイルを読み込む

先日、jXLSを使ったExcelテンプレートをSpring MVCのビューとして利用する方法 - 達人プログラマーを目指してでjXLSを使ってExcelファイルを生成してダウンロードする方法について説明しました。ここでは逆に、アップロードされたExcelファイルを読み込んでPojoに変換する方法について説明します。

Sring MVCのファイルアップロードサポートを利用する

Spring MVCでファイルアップロード機能を利用するためには、まず、commons-fileuploadとcommons-ioへの依存を追加する必要があります。

<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.2.2</version>
</dependency>

<dependency>
	<groupId>commons-io</groupId>
	<artifactId>commons-io</artifactId>
	<version>2.0.1</version>
</dependency>

さらに、Spring MVCのBean定義ファイルに以下を追加します。

	<bean id="multipartResolver"
		class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

		<!-- one of the properties available; the maximum file size in bytes -->
		<property name="maxUploadSize" value="100000" />
	</bean>

以上の設定について、詳しくは以下の説明を参考にしてください。
15. Web MVC framework

jXLS Readerを利用してExcelファイルを自動的にPojoに読み込む

次に、jXLSの拡張機能であるjXLS Readerを依存関係ライブラリーとして追加します。

<dependency>
	<groupId>net.sf.jxls</groupId>
	<artifactId>jxls-reader</artifactId>
	<version>1.0-RC-1</version>
</dependency>

jXLS Readerについては、
JXLS -
に簡単な説明がありますが、以下のようなxmlファイルでExcelファイルを読み込むセルの場所を指定しておくことで、自動的に読み込んだExcelの値をPojoにバインドしてくれます。*1

<?xml version="1.0" encoding="utf-8"?>
<workbook>
	<worksheet name="Sheet1">
		<section startRow="0" endRow="6">
			<mapping cell="B1">department.name</mapping>
			<mapping cell="A4">department.chief.name</mapping>
			<mapping cell="B4">department.chief.age</mapping>
			<mapping cell="D4">department.chief.payment</mapping>
			<mapping row="3" col="4">department.chief.bonus</mapping>
		</section>
		<loop startRow="7" endRow="7" items="department.staff" var="employee"
			varType="com.github.ryoasai.spring_jxls.example.Employee">
			<section startRow="7" endRow="7">
				<mapping row="7" col="0">employee.name</mapping>
				<mapping row="7" col="1">employee.age</mapping>
				<mapping row="7" col="3">employee.payment</mapping>
				<mapping row="7" col="4">employee.bonus</mapping>
			</section>
			<loopbreakcondition>
				<rowcheck offset="0">
					<cellcheck offset="0">Employee Payment Totals:</cellcheck>
				</rowcheck>
			</loopbreakcondition>
		</loop>
	</worksheet>
</workbook>

ここで、私がはまった点を書いておくと、jXLS Readerには現状以下の制約(バグ?)があるようです。(開発チーム報告して直してもらいたいと思っていますが。)

  • たとえ、バインドする値がない場合でもsectionの領域は先頭行から定義しなくてはならない。(必要なら空のsectionを定義する)
  • loopbreakconditionで空セルのチェックがうまくいかない。上記の例のようにセルに値が入っている場合は問題ありません。

特に、後者の問題は普通ループの終端を空セルで行いたいことが多いのでちょっと不便です。OffsetCellCheckImplクラスの以下のメソッドでnullと空文字の比較がtrueとならないことが原因と思われます。(nullと空文字の比較をtrueと判定すべき)

    public boolean isCheckSuccessful(Cell cell) {
        Object obj = getCellValue(cell, value);
        if (value == null) {
            return obj == null;
        } else {
            return value.equals(obj);
        }
    }

次に、コントローラーから汎用的に利用できる以下のヘルパークラスを作成します。

package com.github.ryoasai.spring_jxsl;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

import net.sf.jxls.reader.ReaderBuilder;
import net.sf.jxls.reader.XLSReadStatus;
import net.sf.jxls.reader.XLSReader;

import org.apache.commons.io.IOUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.springframework.core.io.Resource;
import org.springframework.ui.Model;
import org.springframework.web.multipart.MultipartFile;
import org.xml.sax.SAXException;

public class JxlsFileReader {

	public XLSReadStatus read(Resource templateFile, MultipartFile file,
			Model model) throws InvalidFormatException, IOException, SAXException {
		
		return read(templateFile, file, model.asMap());
	}

		
	public XLSReadStatus read(Resource templateFile, MultipartFile file,
			Map<?, ?> model) throws InvalidFormatException, IOException, SAXException {

		InputStream inputXML = null;
		InputStream inputXLS = null;

		try {
			inputXML = new BufferedInputStream(templateFile.getInputStream());
			XLSReader mainReader = ReaderBuilder.buildFromXML(inputXML);

			inputXLS = new BufferedInputStream(file.getInputStream());

			return mainReader.read(inputXLS, model);

		} finally {
			IOUtils.closeQuietly(inputXML);
			IOUtils.closeQuietly(inputXLS);
		}
	}

}

次に、このクラスをBeanとしてDIコンテナに登録します。

<bean id="jxlsFileReader" class="com.github.ryoasai.spring_jxsl.JxlsFileReader" />

最後に、アップロードボタンに対応するコントローラーのメソッドを以下のように実装します。

	@Inject
	JxlsFileReader jxlsFileReader;
...

	private Resource uploadFileTemplate = new ClassPathResource(
			"department.xml", getClass());

	@RequestMapping(value = "/upload", method = RequestMethod.POST)
	public String handleFormUpload(@RequestParam("file") MultipartFile file,
			Model model) throws InvalidFormatException, IOException,
			SAXException {
                
                // バインドしておきたいPojoをモデルにつめておく。
		Department department = new Department();
		model.addAttribute(department);

		jxlsFileReader.read(uploadFileTemplate, file, model);

		System.out.println(department);
		System.out.println(department.getChief());
		System.out.println(department.getStaff());

		return "redirect:/";
	}

ちょっと準備は必要でしたが、このような仕組みを利用することで、ループ処理など低水準の処理をほとんどコーディングすることなく、Excelファイルの読み込みを行うことができます。
なお、サンプルのファイルは前回と同様にGitHub - ryoasai/spring-mvc-exts: Some extensions to the Spring MVC web application framework.に登録してありますので、ご利用ください。

*1:jXLS Readerのソースを読むとcommons-digesterを使ってxmlから定義を読み込んでいるようですが、原理的にはGroovyのビルダーとか別の手段で読み込むように改造することも簡単なはずです。現状のつくりだとちょっとXML地獄に陥りやすく、いまいちかもしれません。