プロジェクトでも必ずと言っていいほど「ファイルダウンロード」の要件は挙がります。
今回はExcel/CSV/PDFの3種のダウンロードについて実装方法を考えてみます。
ファイルダウンロードの設計
JAX-RSによるファイルのダウンロード
JAX-RSではリクエストMediaTypeに応じてある程度の型であれば型に応じてレスポンスに変換する機能があります。
Excel/CSV/PDFに関してデフォルトでは無いためjavax.ws.rs.ext.MessageBodyWriter
を実装したクラスを作成することでカスタマイズします。
一覧取得の処理を集約する
ダウンロードファイルの種類に合わせて1つずつ一覧取得の実装するとメンテナンスのコストが上がります。
一覧取得の処理1つで ダウンロードファイルの種類すべてをまかなえることが望ましいです。
実現するために以下のように実装します。
- Resourceクラスでダウンロード対象エンティティ一覧を取得
- WriterクラスでリクエストMediaTypeに合わせてダウンロードファイルを作成
すべての一覧取得をカバーする
プロジェクトでは1つではなく複数の一覧画面を作ることがほとんどかと思います。
一覧画面1つに対してファイル出力の処理を毎回書いていると、またまたメンテナンスのコストが上がることとなります。
つまりすべての一覧画面に対しても対応できることが理想形となります。
実現するために以下のように実装します。
- Writerクラスにて ダウンロード対象エンティティを総称型(ジェネリクス)で受ける
- 総称型(ジェネリクス)で受けたエンティティのプロパティにアクセスするためのクラスを作成する
実装
一覧の取得
@Produces
でダウンロードファイルのMediaTypeを追加し、リクエストを受け付けるようにします。
メソッドの返却型では一覧に出力するエンティティ型を指定します。
(なお EntityManagerの作成/検索クエリなどの処理は省略しています。)
@Path("jaxrs")
public class JaxRsResource {
@Inject
protected EntityManager em;
@GET
@Produces({
"text/csv",
"application/pdf",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"})
public Iterable<HogeEntity> list(...) {
// ...リクエストパラメータから検索条件を取得してqueryを組み立てる
// DBより一覧を取得することを想定
return em.createQuery(query)
.getResultList();
}
}
ダウンロード対象のエンティティ
以下サンプル@ListColumn
のようなアノテーションを作成しておき
ExcelでもCSVでも共通して使える仕掛けをあらかじめ作成しプロパティに付加しておきます。
public class HogeEntity {
@ListColumn(header="ID" , order = 0)
public String id;
@ListColumn(header="ほげ名称" , order = 2)
public String name;
@ListColumn(header="ふが日時" , order = 1)
public LocalDate hugaDate;
// ...
}
CSV
@Produces
でMediaTypeに対応した”text/csv”を設定し、レスポンスを変換できるようにします。
@Provider
@Produces("text/csv")
public class CsvWriter<E> implements MessageBodyWriter<List<E>> {
// ...
@Override
public void writeTo(List<E> entities, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
if (entities.isEmpty()) {
return;
}
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(entityStream, Charset.forName("Windows-31J")));
CSVPrinter printer = new CSVPrinter(writer, CSVFormat.RFC4180);) {
List<String> header = accessors.stream()
.map(acc -> /*ヘッダーラベルの取得*/)
.collect(Collectors.toList());
printer.printRecord(header);
printer.flush();
for(E entity : entities) {
List<String> row = accessors.stream()
.map(accessor -> /*値の取得*/)
.collect(Collectors.toList());
printer.printRecord(row);
printer.flush();
}
}
}
}
CSVダウンロードでは「org.apache.commons.commons-csv:1.1」ライブラリにて実装。
// TODO ジェネリクス型のフィールドアクセスについて別途記載する
Excel
@Produces
でMediaTypeに対応した”application/vnd.xxxxx”を設定し、レスポンスを変換できるようにします。
@Provider
@Produces("application/vnd.ms-excel")
public class XlsWriter<E> implements MessageBodyWriter<List<E>>{
// ...
@Override
public void writeTo(List<E> entities, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
if (entities.isEmpty()) {
return;
}
HSSFWorkbook book = new HSSFWorkbook();
Sheet sheet = book.createSheet();
Row headerRow = CellUtil.getRow(0, sheet);
IntStream.range(0, accessors.size())
.forEach(i -> {
CellUtil.createCell(headerRow, i, /*ヘッダーラベルの取得*/);
});
IntStream.range(0, entities.size())
.forEach(i -> {
Row row = CellUtil.getRow(i + 1, sheet);
IntStream.range(0, accessors.size())
.forEach(j -> {
CellUtil.createCell(row, j, /*値の取得*/);
});
});
book.write(entityStream);
}
}
@Provider
@Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public class XlsxWriter <E> implements MessageBodyWriter<List<E>> {
// ...
@Override
public void writeTo(List<E> entities, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
if (entities.isEmpty()) {
return;
}
SXSSFWorkbook book = new SXSSFWorkbook();
Sheet sheet = book.createSheet();
Row headerRow = CellUtil.getRow(0, sheet);
IntStream.range(0, accessors.size())
.forEach(i -> {
CellUtil.createCell(headerRow, i, /*ヘッダーラベルの取得*/);
});
IntStream.range(0, entities.size())
.forEach(i -> {
Row row = CellUtil.getRow(i + 1, sheet);
IntStream.range(0, accessors.size())
.forEach(j -> {
CellUtil.createCell(row, j, /*値の取得*/);
});
});
book.write(entityStream);
}
}
Excelダウンロードでは「org.apache.poi.poi:3.11」ライブラリにて実装。
@Produces
でMediaTypeに対応した”application/pdf”を設定し、レスポンスを変換できるようにします。
PDFのダウンロードは少し面倒です。以下を行う必要があります。
- ダウンロード対象エンティティごとに別途テンプレートを作成
- Propertyファイルにエンティティ↔︎PDFテンプレート間のマッピングを保持
@Provider
@Produces("application/pdf")
public class PdfWriter<E> implements MessageBodyWriter<List<E>> {
private final Properties templatePaths = new Properties();
private static final String PDF_MAPPING_FILE_NAME = "pdfTemplateMapping.properties";
@PostConstruct
public void initialize() throws IOException{
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream is = classLoader.getResourceAsStream(PDF_MAPPING_FILE_NAME)) {
templatePaths.load(is);
}
}
// ...
@Override
public void writeTo(List<E> entities, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
if(entities.isEmpty()){
return;
}
String entityName = entities.get(0).getClass().getName();
String templatePath = templatePaths.getProperty(entityName);
PdfCreator creator = new PdfCreator(templatePath);
creator.write(entities, entityStream);
}
}
public class PdfCreator {
private JasperReport jasperReport;
public PdfCreator(String templatePath) throws IOException, JRException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try (InputStream in = classLoader.getResourceAsStream(templatePath)){
jasperReport = JasperCompileManager.compileReport(in);
}
}
public void write(Collection entities, OutputStream output) throws IOException, JRException {
Map<String, Object> params = new HashMap<>();
JRDataSource dataSource = new JRBeanCollectionDataSource(entities);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource);
JasperExportManager.exportReportToPdfStream(jasperPrint, output);
}
}
PDFダウンロードでは「net.sf.jasperreports.jasperreport:5.6.1」ライブラリ 1 にて実装。
まとめ
Writerクラスさえ作ってしまえば よしなにファイルを作成してくれるので
- エンティティにアノテーションを付加する
- Resourceクラスの一覧取得メソッドにサポートしたいMediaTypeを追加する
だけでダウンロード処理を大量生産できます。
JAX-RSによるファイルダウンロードは比較的容易に実装できるためコピペして作ってしまいがちかと思います。
今回のように汎用的に作ることで「この一覧もダウンロードしたいナー」の要件に即時対応ができるようになります。そしてテストも比較的容易となるでしょう。
これからJavaEEを使って実装を始める人の助けになればと思います。