XSLファイルを使って、SpringMVCでCSV出力をおこなう方法

SpringFrameWork

WEBアプリケーションにおいて、サーバでCSVを作成して、クライアント(ブラウザ)にダウンロードする、といった要件は頻繁に発生します。


SpringMVCでXSLファイルを上手に使えば、簡単にCSVファイルの作成とダウンロードの機能を作成することができます。


今回は、SpringMVCでCSV出力をおこなう方法を紹介します。
Javaで自力でゴリゴリ作成する方法ではなく、XSLファイルを使ってCSV出力おこなう方法となります。


XSLファイルとは、XMLファイルのスタイルを定義しているファイルになります。
出力するCSVファイルのレイアウトをXSLファイルに定義してあらかじめサーバ上に格納しておき、CSVファイルを出力する際に使用します。


XSLファイルとは何か?


XSL(eXtensible Style Language)ファイルは、XML 文書を他の文書タイプに変換したり、出力をフォーマットする際に使用できるスタイルシートです。
Styleという言葉が使われていることからもわかるように、XMLのスタイルシートがXSLになります。


このXSLファイルで便利なのが、テンプレート関数というちょっとしたロジックをXSLファイル内で使用することができる点です。
例えば、以下のような関数です。


  • テンプレート関数の定義の仕方(xsl:template、xsl:param)
  • 引数の値の参照の仕方($引数名)
  • 条件分けの仕方(xsl:if、xsl:choose)
  • XSLT関数(not、contains、substring-before、substring-after)
  • XSLT関数の使い方(<xsl:value-of select=”XSLT関数 (引数…)” />)
  • 定義されたテンプレート関数の呼び出し方(xsl:call-template、xsl:with-param:再帰呼び出しの箇所)

上記のような関数もあらかじめXSLファイル内に定義することができます。


以下に、よく使う関数を紹介していきます。


文字列の連結


concat(str1,str2,str3,・・・)
str1、str2、str3、を連結


文字列の調査


contains(str, substr)
strの中のsubstrを検索。
存在する場合はtrueを通知


数字のフォーマッティング


format-number(number, format)
format-number(number, format, formatType)
numberの数字を、formatで指定されたフォーマットに変換して出力します。


空白の除去


normalize-space(str)
strから空白を除外した文字列を返却します


先頭文字のチェック


starts-with(str, substr)
strがsubstrで始まっているかをチェックします。
始まっている場合はtrueを通知。


文字列への変換


string(val)
valを文字列に変換します。


文字列の長さを取得


string-length(str)
strの長友を取得します。


文字列の抜き出し


substring(str, start)
substring(str, start, length)
strに対して、start位置からlengthの長さの文字列を取得します。


文字列の抜き出し(前文字列の取得)


substring-before(str, substr)
strからsubstrの文字列を検索し、見つかったらその前の文字列を取得します。


substring-after(後文字列の取得)


substring-after(str, substr)
strからsubstrの文字列を検索し、見つかったらその後の文字列を取得します。


文字列の置換


translate(str, src, dest)
strに含まれるsrcという文字列をdestに変換します。


切り上げ


ceiling(val)
valの切り上げをおこないます。


ノード数の取得


count(node)
XML内に含まれるnodeで指定されたノードの数を取得します。

<a>
  <b></b>
  <b></b>
</a>

この例で、’b’のノード数は2です。


切り下げ


floor(val)
valの切り下げをおこないます。


数値に変換


number(any)
anyを数値に変換します。


四捨五入


round(val)
valを四捨五入します。


加算


sum(node)
nodeで指定されている値を加算し、数値で返却します。


<a>
    <b>10</b>
    <b>10</b>
</a>

sum(‘b’)の返却値は20です。


現在のノード取得


current()
現在位置のノードを取得します。


現在のノードの最下層ノードを取得


last()


nodeのローカル名(名前空間を外したもの)を取得


local-name(node)


現在のノード名を取得


name()


現在のノードの名前空間URIを取得


namespace-uri()


現在のノード位置を数値で取得


position()


boolean型への変換


boolean(any)
anyをboolean(true or false)に変換します。


falseの出力


false()
boolean型のfalseを出力します。


trueの出力


true()
boolean型のtrueを出力します。


否定


not(boolean)
booleanの否定値をbooleanで出力します。


ドキュメントルートの取得


document(uri)
uriのドキュメントルートを出力します。


xsltで解釈できるxlstノードかを判定


element-available(str)
返り値はbooleanです。


xsltで解釈できる関数かを判定


function-available(func)


ノードに固有の値を割り当て


generate-id(node)


該当するidのノードを取得


id(any)


キー値を返却


key(str any)
str内のハッシュに、anyで指定される表現を返却します。


XSLT固有値の取得


system-property(str)
str内のXSLT固有の値を取得します。


<!entity>タグ値を取得


unparsed-entity-uri()
<!entity>タグで指定されている値を取得します。


CSV出力のサンプルソース


CSV出力のサンプルソースを紹介します。


SpringMVCのコントローラクラスに実装することを想定しています。


@RequestMapping(params = "csvOut", produces = "text/csv")
public ModelAndView makeCsvOut(HttpServletResponse response, Form form)
        throws JAXBException, IOException {

    // DBから出力する情報を取得してDTOに格納
    CsvOutDTO dto = this.service.geCsvInfo(form);

    // DTOをXMLの形式に変換
    JAXBContext context = JAXBContext.newInstance(CsvOutDTO.class);
    Writer writer = new StringWriter();
    context.createMarshaller().marshal(dto, writer);

    // モデルに変換したXMLを格納
    String xslName = "CsxXsl";
    ModelAndView modelAndView = new ModelAndView(xslName);
    modelAndView.addObject(
        "xmlSource", new StringReader(writer.toString()));

    // HTTPレスポンスヘッダを作成
    response.setHeader("Content-Disposition", 
        "attachment; filename=csvFile.csv, "UTF-8"));

    // BOMを先頭に付加
    response.getOutputStream().write(CxFix.BOM);

    return modelAndView;
}

データベースからCSV出力する内容を取得することを想定しています。
「geCsvInfo」の中身は消略していますが、このメソッドの中身がDBアクセスしてDTOのインスタンスを返却するイメージです。


その後はサンプルの通りで、DTOを「JAXBContext」に渡せば、CSVファイルの出来上がり


次にXSLファイルのサンプルです。

上記のJavaソースで”CsvXsl”という名前で定義されたXSLファイルのサンプルになります。


<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns="http://www.w3.org/1999/xhtml">
    <xsl:output method="text" omit-xml-declaration="yes" 
        byte-order-mark="yes" encoding="UTF-8" />
  <xsl:template match="/csvList">
    <xsl:call-template name="template_csvList" />
  </xsl:template>
    <xsl:template name="template_csvList">
        <!-- ヘッダ情報 -->
        <xsl:text>カラム1,</xsl:text>
        <xsl:text>カラム2,</xsl:text>
        <xsl:text>カラム3,</xsl:text>
        <xsl:text>
    </xsl:text>
        <!-- データ情報 -->
        <xsl:value-of
            select="translate(translate(translate(
                ./entity/col1, '"', ' '), ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
        <xsl:value-of
            select="translate(translate(translate(
                ./entity/col2, '"', ' '), ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
        <xsl:value-of
            select="translate(
                translate(translate(./entity/col3, '"', ' '), 
                ',', ' '), '', ' ')" />
        <xsl:text>,</xsl:text>
    </xsl:template>
</xsl:stylesheet>

「entity」とは、前のJavaコードに記載していた、「CsvOutDTO」で使用しているエンティティクラスです。
1行データ毎に1エンティティのレコードを保持します。


CSVファイルは1行目は各項目名の説明で、実際のデータ出力は2行目からです。
XSLファイルで2行目以降を動的にすることで、「CsvOutDTO」に格納されているエンティティの数だけCSVデータ出力がおこなわれます


関数は「translate」しか使っていません。
ダブルクオテーションと改行コードを半角スペースに変換しています。


まとめ


いかがでしたでしょうか?


SpringMVCでCSV出力をおこなう場合に、参考にしてもらえればと思います。
特に、XSLの関数は助かります。
ちょっとした処理をJavaでおこなう必要がないので。


それではまた!


ファイルが削除されない原因は何?java.io.Fileとjava.nio.file.Filesの違いを検証

Java

この前、実際の業務でちょっと困ったことがおきました。
ちょっと前に作ったシステムなのですが、“ある特定のファイルが削除されない”という現象です。


今回は、実際におきた現象をもとに、「java.io.File」と「java.nio.file.Files」の違いを実際のプログラムを使って紹介していきます。


実際に起きた現象自体は解消されたのですが、一番困ったのは調査です。
原因は、削除するファイルが他プロセスにつかまれていることが原因だったのですが、その原因がすぐにはわからなかったです。


「java.io.File」を使ってファイル削除していたのですが、他プロセスが削除対象ファイルをつかんでいる場合は、返り値で失敗(false)を返却しているのみで、例外が発生しないです。
なので、削除されない原因がすぐにはわかりませんでした。


そのため、「java.io.File」ではなく「java.nio.file.Files」に切り替えて例外をログ出力するようにすることで、ようやく原因がわかりました。


サンプルプログラム

二つのプログラムで試してみます。
「java.io.File」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;

public class FileDelete {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        boolean isDone = false;
        try {
            File file = new File("C:\\Temp\\file.txt");
            isDone = file.delete();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("isDone=" + isDone + "\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.nio.file.Files」で削除するプログラムは以下。

import java.io.IOException;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class FileDeleteIO {

    public static void main(String[] args){
        System.out.print("start: main\r\n");
        try {
            Files.delete((Paths.get("C:\\Temp\\file.txt")));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.print("finally\r\n");
        }
        System.out.print("end: main\r\n");
    }
}

「java.io.File」は削除結果を返却してくれるので削除結果を標準出力していますが、「java.nio.file.Files」の方は削除結果を返却しないので何も標準出力していません。


この2つのプログラムを使用して、「java.io.File」と「java.nio.file.Files」の違いを検証します。
ファイルの削除について挙動の違いを検証します
以下の3パターンです。


  • 削除対象ファイルが存在しない場合
  • 削除対象ファイルに対して削除権限がない場合
  • 削除対象ファイルが他プロセスに開かれている場合

存在しないファイルを削除

削除対象のファイルが存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.NoSuchFileException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「NoSuchFileException」というファイルが存在しません、という例外です。
この例外を嫌う場合は「delete」ではなく「deleteIfExists」を使えばいいです。


削除権限がないファイルを削除

削除対象のファイルについて削除権限が存在しない場合の挙動を試してみます。
「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.AccessDeniedException: C:\Temp\file.txt
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:89)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「AccessDeniedException」という、ファイルへのアクセス権限が存在しません、という例外です。


他のプロセスが開いているファイルを削除

削除対象のファイルについて、他のプロセスがファイルを開いている場合の挙動について試してみます。


あえて他のプロセスがファイルをつかんでいる状態を発生させるために、簡単なプログラムを作りました。

import java.io.IOException;
import java.io.FileOutputStream;
import java.io.File;

import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLocker {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        // 削除対象ファイルを読み込み
        File file = new File("C:\\Temp\\file.txt");
        FileChannel fc = null;
        FileLock lock = null;

        try {
            // ファイルを読み込み、ファイルをロック
            FileOutputStream fos = new FileOutputStream(file);
            fc = fos.getChannel();
            lock = fc.tryLock();

            // ロックに失敗した場合、例外をスロー
            if (lock == null) {
                throw new Exception("lock failer");
            }

            // 1秒スリープ
            System.out.print("sleeping...\r\n");
            Thread.sleep(60000); 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (lock != null) {
                try {
                    lock.release();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        System.out.print("end: main\r\n");
    }
}

このプログラムでファイルをつかんでいる状態を作り出して、ファイル削除をためしてみます。


「java.io.File」の結果は以下になります。

start: main
isDone=false
end: main

「java.io.File」の結果がfalse(失敗)を返しました。


「java.nio.file.Files」の結果は以下になります。

start: main
java.nio.file.FileSystemException: C:\Temp\file.txt: プロセスはファイルにアクセスできません。別のプロセスが使用中です。
        at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:92)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
        at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
        at java.base/sun.nio.fs.WindowsFileSystemProvider.implDelete(WindowsFileSystemProvider.java:274)
        at java.base/sun.nio.fs.AbstractFileSystemProvider.delete(AbstractFileSystemProvider.java:105)
        at java.base/java.nio.file.Files.delete(Files.java:1144)
        at FileDeleteIO.main(FileDeleteIO.java:11)
finally
end: main

例外が発生しました。
「FileSystemException」という例外で、ファイルシステムとしてはこのファイルが他プロセスにロックされてるという例外です。


筆者が直面した問題がまさにこれでして、「java.io.File」だと「false」とだけ出力されるので、何か原因で削除されなかったのかがわからない。。。
「java.nio.file.Files」だと例外が出力されるので、原因についてある程度のあたりがつきます。


まとめ

「java.io.File」と「java.nio.file.Files」、どっちを使う方がいいのでしょうか?
実装するアプリケーションの特性にもよるので一概にはどちらがいい、とは言えないのですが、筆者は今後は極力「java.nio.file.Files」を使うようにしようと思っています。


理由はやはり、なにかしらエラー(削除失敗、など)が発生した場合の原因調査。
例外がトレースとして出力されるのとされないのでは、障害発生時の調査に掛かる時間が圧倒的に違います。


今後は「java.nio.file.Files」を使うようにしよう。。。


それではまた!