Apache POI Excel Sheet の全行 ⇒ 読込みObjecのリスト

Apache POI を使用した Excel の操作はずいぶん昔からコードを書いていた。
PushbackInputStream を使用する - Oboe吹きプログラマの黙示録
Excel 拡張子 (xls) (xlsx) 両方に対応した操作 Apache POI (2) - Oboe吹きプログラマの黙示録
Excel 拡張子 (xls) (xlsx) 両方に対応した操作 Apache POI(1) - Oboe吹きプログラマの黙示録
Apache POI でExcel 日付読込み(2) - Oboe吹きプログラマの黙示録
Apache POI でExcel 日付読込み - Oboe吹きプログラマの黙示録
Apache POI 入力規則リストの生成 - Oboe吹きプログラマの黙示録
Apache POI Excel 入力規則の読み込み(2) - Oboe吹きプログラマの黙示録
Apache POI Excel入力規則の読み込み(1) - Oboe吹きプログラマの黙示録

先日書いた ExcelWorker にさらにメソッドを追加する。

/**
 * Sheet ⇒ 読込みObject
 * @param <R>
 * @param sheetName シート名
 * @param func  Function<Sheet, R>
 * @return R
 */
public <R> R readSheetValue(String sheetName, Function<Sheet, R> func) {
    return Optional.ofNullable(book.getSheet(sheetName)).map(func::apply).orElse(null);
}
/**
 * Sheet の全行 ⇒ 読込みObjecのリスト
 * @param <R>
 * @param sheetName シート名
 * @param func Function<Row, R>
 * @return List<R>
 */
public <R> List<R> readRowList(String sheetName, Function<Row, R> func) {
    Sheet sheet = book.getSheet(sheetName);
    if (sheet==null) return new ArrayList<>();
    int last = sheet.getLastRowNum();
    return IntStream.range(0, last).boxed().map(i->func.apply(sheet.getRow(i))).collect(Collectors.toList());
}
/**
 * Sheet の全行 ⇒ 読込みObjecのリスト(行番号認識して)
 * @param <R>
 * @param sheetName シート名
 * @param func Function<Integer, Row, R>
 * @return List<R>
 */
public <R> List<R> readRowList(String sheetName, BiFunction<Integer, Row, R> func) {
    Sheet sheet = book.getSheet(sheetName);
    if (sheet==null) return new ArrayList<>();
    int last = sheet.getLastRowNum();
    return IntStream.range(0, last).boxed().map(i->func.apply(i, sheet.getRow(i))).collect(Collectors.toList());
}

Excel 拡張子 (xls) (xlsx) 両方に対応した操作 Apache POI (2) - Oboe吹きプログラマの黙示録
で書いていた、 Excelworker もいいかげん、まとめたものにしておきたい。

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.util.IOUtils;
/**
 * ExcelWorker
 */
public class ExcelWorker implements Closeable{
    private Workbook book;

    private static final int MAX_PATTERN_LENGTH = 44;

    /**
    * コンストラクタ(File指定)
    * @param inst 読込み対象ExcelのInputStream
    * @throws IOException
    * @throws EncryptedDocumentException
    */
    public ExcelWorker(InputStream inst) throws EncryptedDocumentException, IOException {
       PushbackInputStream inp = new PushbackInputStream(inst, MAX_PATTERN_LENGTH);
       byte[] data = new byte[MAX_PATTERN_LENGTH];
       inp.read(data, 0, MAX_PATTERN_LENGTH);
       inp.unread(data);
       FileMagic fm = FileMagic.valueOf(data);
       if (FileMagic.OOXML==fm){
           book = new XSSFWorkbook(inp);
       }else if(FileMagic.OLE2==fm){
           book = new HSSFWorkbook(inp);
       }else {
           book = WorkbookFactory.create(inp);
       }
   }
    /**
    * コンストラクタ(File指定)
    * @param file 読込み対象ExcelのFile
    * @throws IOException
    * @throws EncryptedDocumentException
    */
   public ExcelWorker(File file) throws EncryptedDocumentException, IOException{
       FileMagic fm = valueFileMagic(file);
       if (fm==FileMagic.OOXML) {
           book = new XSSFWorkbook(new FileInputStream(file));
       }else if(fm==FileMagic.OLE2){
           book = new HSSFWorkbook(new FileInputStream(file));
       }else {
           book = WorkbookFactory.create(new FileInputStream(file));
       }
   }
   private FileMagic valueFileMagic(File file) throws IOException {
       try(FileInputStream fis = new FileInputStream(file)) {
           byte[] data = new byte[MAX_PATTERN_LENGTH];
           int read = IOUtils.readFully(fis, data, 0, MAX_PATTERN_LENGTH);
           if(read == -1) {
               return FileMagic.UNKNOWN;
           }
           data = Arrays.copyOf(data, read);
           return FileMagic.valueOf(data);
       }
   }

   /**
    * Excelバージョンを返す
    * @return SpreadsheetVersion,EXCEL2007 か。SpreadsheetVersion,EXCEL97 を返します
    */
    public SpreadsheetVersion getSpreadsheetVersion() {
        return book.getSpreadsheetVersion();
    }
    /**
     * Apche POI Workbook参照
     * @return
     */
    public Workbook getWorkbook() {
        return book;
    }
    /**
    * 先頭シート読み出し行(Row) Consumer を実行
    *
    * @param action Consumer<Row>
    */
    public void readFirstSheet(Consumer<Row> action) {
        Sheet sheet = book.getSheetAt(0);
        IntStream.rangeClosed(0, sheet.getLastRowNum()).mapToObj(n->sheet.getRow(n)).forEach(row->action.accept(row));
    }
    /**
    * 制限付き先頭シート読み出し行(Row) Consumer を実行
    *
    * @param pre 制限として指定する Predicate<Row>
    * @param action Consumer<Row>
    */
    public void readFirstSheet(Predicate<Row> pre, Consumer<Row> action) {
        Sheet sheet = book.getSheetAt(0);
        IntStream.rangeClosed(0, sheet.getLastRowNum()).mapToObj(n->sheet.getRow(n)).filter(pre).forEach(row->action.accept(row));
    }
   /**
    * 全ての Sheet Stream
    */
    public Stream<Sheet> getSheets(){
        return IntStream.range(0, book.getNumberOfSheets()).mapToObj(n->book.getSheetAt(n));
    }
   /**
    * 全てのSheet読み出しConsumerを実行
    * @param action Consumer<Sheet>
    */
    public void getSheets(Consumer<Sheet> action){
        IntStream.range(0, book.getNumberOfSheets()).mapToObj(n->book.getSheetAt(n)).forEach(action);
    }
    /**
     * Sheet ⇒ 読込みObject
     * @param <R>
     * @param sheetName シート名
     * @param func  Function<Sheet, R>
     * @return R
     */
    public <R> R readSheetValue(String sheetName, Function<Sheet, R> func) {
        return Optional.ofNullable(book.getSheet(sheetName)).map(func::apply).orElse(null);
    }
    /**
     * Sheet の全行 ⇒ 読込みObjecのリスト
     * @param <R>
     * @param sheetName シート名
     * @param func Function<Row, R>
     * @return List<R>
     */
    public <R> List<R> readRowList(String sheetName, Function<Row, R> func) {
        Sheet sheet = book.getSheet(sheetName);
        if (sheet==null) return new ArrayList<>();
        int last = sheet.getLastRowNum();
        return IntStream.range(0, last).boxed().map(i->func.apply(sheet.getRow(i))).collect(Collectors.toList());
    }
    /**
     * Sheet の全行 ⇒ 読込みObjecのリスト(行番号認識して)
     * @param <R>
     * @param sheetName シート名
     * @param func Function<Integer, Row, R>
     * @return List<R>
     */
    public <R> List<R> readRowList(String sheetName, BiFunction<Integer, Row, R> func) {
        Sheet sheet = book.getSheet(sheetName);
        if (sheet==null) return new ArrayList<>();
        int last = sheet.getLastRowNum();
        return IntStream.range(0, last).boxed().map(i->func.apply(i, sheet.getRow(i))).collect(Collectors.toList());
    }

    /**
    * 全てのSheet読み出しConsumerを実行
    * @param action Consumer<Sheet>
    */
    public static Stream<Row> getRowStream(Sheet sheet) {
        return IntStream.rangeClosed(0, sheet.getLastRowNum()).mapToObj(n->sheet.getRow(n));
    }
   /**
    * 行からセルのStreamを取得
    * @param row Row
    * @return Stream<Cell>
    */
    public static Stream<Cell> getCellStream(Row row){
        return IntStream.range(0, row.getLastCellNum()).mapToObj(n->row.getCell(n));
    }
   /**
    * シートデフォルト名で新規シート作成、先頭シートを size - 1 個クローンする。
    * @param book 作成対象Workbook
    * @param size 新規作成シート数
    * @return 作成したシートのStream
    */
    public static Stream<Sheet> createAndSheets(Workbook book, int size) {
        book.createSheet();
        for(int n=1; n < size;n++) {
            book.cloneSheet(0);
        }
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(book.sheetIterator(), Spliterator.ORDERED), false);
    }
    /**
    * シート名を指定した新規シート作成
    * @param book 作成対象Workbook
    * @param names 新規作成するシート名、可変長配列
    * @return 作成したシートのStream
    */
    public static Stream<Sheet> createAndSheets(Workbook book, String...names) {
        book.createSheet(names[0]);
        for(int n=1; n < names.length;n++) {
            book.createSheet(names[n]);
        }
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(book.sheetIterator(), Spliterator.ORDERED), false);
    }
    @Override
    public void close() throws IOException{
        book.close();
    }
}

mybatis で固定値をSQL Mapで指定するにはOGNL式を使う

開発プロジェクトの方針にSQL文に固定値を記述するのを絶対に禁止とか
融通の効かない、賛同できないことが時々ある。
プロジェクトによってそういうのもあって苦労するのだが、mybatis には OGNL書式が使える
OGNL(Object Graph Navigation Language) のことである。

例)

<select id="getItems" resultType="org.sample.dto.Gstore">
SELECT g.* FROM group_stores g 
LEFT JOIN (SELECT id FROM items
 WHERE view_code IN ( '10201', '10202' ) )  i 
ON g.code = i.id
</select>

このSQLMapでの view_code IN ( '10201', '10202' ) をパラメータで与えるのではなく、
動的が無理でも任意の定数に参照させるのに、OGNL書式を使う

定数として以下を定義

package org.sample;

public interface Access{
   public static final String WHITE = "10201";
   public static final String BLUE  = "10202";
}

${ } で、OGNL式を記載する。

<select id="getItems" resultType="org.sample.dto.Gstore">
SELECT g.* FROM group_stores g 
LEFT JOIN (SELECT id FROM items
 WHERE view_code IN ( '${@org.sample.Access@WHITE}', '${@org.sample.Access@BLUE}' ) ) i
ON g.code = i.id
</select>

#{ } の記述では想定どおりに動かない
view_code IN ( #{@org.sample.Access@WHITE} , #{@org.sample.Access@BLUE} )
と書いて良さそうなものだけど、これはダメだ

enum による定数定義を使いたい場合、

package org.sample;

public enum Color{
    WHITE("10201"), BLUE("10202"), RED("10203");

    private String value;
    private Color(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}

WHERE句も、以下のように書ける

 WHERE view_code IN ( '${@org.sample.Color@WHITE.getValue()}', '${@org.sample.Color@BLUE.getValue()}' )


Javaコードの中での OGNL式の取り扱い

try{
   Object obj = Ognl.getValue(Ognl.parseExpression("@org.sample.Access@WHITE"), Access.class);
 
   System.out.println(obj.toString());
}catch(OgnlException e){
    e.printStackTrace();
}

mybatis 可変長引数メソッドをSQLアノテーションで。。。

Javaメソッド可変長引数(variable length arguments)使用での
SQLアノテーション

リストを引数にした場合のサンプル

@Select("<script>SELECT * FROM tb_items WHERE 1=1"
      + " AND item_id IN <foreach collection='list' item='id'"
      + " separator=',' open='(' close=')'>#{id}</foreach>"
      + "</script>")
public List<Item> getItems(List<Integer> idlist);

list という予約された単語が使える。
配列なら array なのである。

@Select("<script>SELECT * FROM tb_items WHERE 1=1"
      + " AND item_id IN <foreach collection='array' item='id'"
      + " separator=',' open='(' close=')'>#{id}</foreach>"
      + "</script>")
public List<Item> getItems(int...args);

このままでは、引数無しで呼ばれたときに動かないSQLにになるので

@Select("<script>SELECT * FROM tb_items WHERE 1=1"
      + "<if test='array.length > 0'>"
      + " AND item_id IN <foreach collection='array' item='id'"
      + " separator=',' open='(' close=')'>#{id}</foreach>"
      + "</if></script>")
public List<Item> getItems(int...args);

list でも同じことで、if 文が必要ではある。
SQLアノテーションでこのように foreach を書いた時に、アノテーションに渡す文字列のエスケープ
 open=\"(\"
と書かなくてもシングルクォーテーションでXML属性値を書いても
 open='('
ちゃんと認識してくれる。

ThrowableFunction

Throwable 例外を投げて処理する関数型インターフェースを作っている。
yipuran-core/src/main/java/org/yipuran/function at master · yipuran/yipuran-core · GitHub

使い道はいっぱいある。

Strin str = "[ \"A\", \"B\", \"C\"  ]";

これを Jackson ObjectMapper で、List<String> を生成するコードを書くと、、

ObjectMapper mapper = new ObjectMapper();

try{
    List<String> list = mapper.readValue(str, new TypeReference<List<String>>(){});
}catch (JsonMappingException e){
    e.printStackTrace();
}catch (JsonProcessingException e){
    e.printStackTrace();
}

どうしても例外捕捉を書かなくてはならない。
org.yipuran.function.ThrowableFunction
を使えば、以下のように書ける。

Function<String, List<String>> function = ThrowableFunction.of(
    s->new ObjectMapper().readValue(s, new TypeReference<List<String>>(){})
);
List<String> list = function.apply(str);

1行で書くなら、、、

List<String> list =
ThrowableFunction.of(s->new ObjectMapper().readValue((String)s, new TypeReference<List<String>>(){}))
.apply(str);

JUnit リスト比較

リストの完全一致の比較検証を簡単に記述できないかを考える。
比較対象のリストの要素がデータオブジェクトで、特定のフィールドは
比較対象にしたくない。というケース

オブジェクトの比較対象にしたくないフィールドをコピーすることを以下を
使って簡単に記述すれば良い。
github.com

private <T> void assertList(List<T> actlist, List<T> expectedlist, String...excludes) {
    Assert.assertTrue(actlist.size()==expectedlist.size());
    List<String> expects = IntStream.range(0, actlist.size()).boxed().map(i->{
        T act = actlist.get(i);
        T exp = expectedlist.get(i);
        for(String name:excludes){
            FieldCopy.of(u->name, exp).accept(act);
        }
        return exp;
    }).map(t->t.toString())
    .sorted()
    .collect(Collectors.toList());
    List<String> reals = actlist.stream().map(t->t.toString()).sorted().collect(Collectors.toList());
    MatcherAssert.assertThat(reals, CoreMatchers.is(expects));
}

actlist = 検証したいリスト
expectedlist = 期待値リスト
excludes = 可変長引数で、比較対象から除外するフィールドの名称を指定する

以下がテストの条件になる。

  • リストの要素は、toString() を実装して比較することができる。
  • null でないこと。

果たしてこれでいくつものリストの比較検証を書くのにステップが多くならない
効果に見合うだろうか。。。

Maven ビルドリソースの指定

以前、こんなものを書いたけど。。
Maven ビルド実行前にファイルコピー - Oboe吹きプログラマの黙示録

改めてビルドでの指定は、こんな風に書く

  <build>

    <resources>
      <resource>
         <directory>${basedir}/src/main/resources</directory>
         <includes>
             <include>**/*.properties</include>
             <include>**/*.xml</include>
             <include>**/*.json</include>
         </includes>
      </resource>
    </resources>
    

JUnit でログ出力を検証する。

JUnit で先日の System.setOut による標準出力先の切り替えを利用する方法である。

logger.info("AuthLogicImpl name = "+name);

このロガーが標準出力する設定になっており、このログ出力をJUnit テストコードで検証する。
準備として便利な以下を使用できるように持ってくる。
https://github.com/yipuran/yipuran-core/wiki#returnalconsumert
この ReturnalConsumer は、
文字列パターンマッチに便利なのである。

String string = // TODO 対象文字列

String name = ReturnalConsumer.of(Matcher.class).with(Matcher::find)
.get(Pattern.compile("AuthLogicImpl name = \\w+").matcher(string))
.group().replaceFirst("AuthLogicImpl name = ", "");

これで、マッチした AuthLogicImpl name の name を抽出するのである。
JUnit テストケースで使用する例、、、

private ByteArrayOutputStream bo;
@Before
public void init() {
    bo = new ByteArrayOutputStream();
    System.setOut(new PrintStream(bo));
}

@Test
public void test() {
    // TODO テスト対象メソッド呼び出し

    String resout = bo.toString();

    Arrays.stream(resout.split(System.lineSeparator()))
    .filter(s->{
        Matcher m = Pattern.compile("AuthLogicImpl name = \\w+").matcher(s);
        return m.find();
    })
    .forEach(str->{
        MatcherAssert.assertThat(ReturnalConsumer.of(Matcher.class).with(Matcher::find)
                .get(Pattern.compile("AuthLogicImpl name = \\w+").matcher(str))
                .group().replaceFirst("AuthLogicImpl name = ", "")
            , CoreMatchers.is("uranus"));
    });

}
@After
public void after() {
    System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
}

@Before と @After メソッドで、標準出力先の切り替えをしているが、
テスト対象メソッド呼び出しから、ログの検証のスコープだけで良いような気もする。

でも、この System.setOut でコンソールに出力するログを切り替えられるのは、
logback を使った時であり、log4j2 を使っているときは、ダメであった。
いったん、
org.apache.log4j.LogManager.shutdown(); で接続を切って制御するしかない