入れ子構造のBean の中の Obejct を取得する(2)

入れ子構造のBean の中の Obejct を取得する(1) - Oboe吹きプログラマの黙示録
の続き。。。

変数の宣言名でなく型を指定して、Bean の中を探索→入れ子構造も探索で指定型のインスタンスを求める。
ただし、探索に指定する型が重複して持っている場合はうまく取得できない。
List や、Map、Collection は指定して取得できない。
配列も指定して取得できない。
という限定ならば、
以下、Function<T, R> を生成するもので、目的を達成できる。

import java.lang.reflect.Field;
import java.util.function.Function;
/**
 * UniqueFieldfinder
 */
@FunctionalInterface
public interface UniqueFieldfinder<T, R>{
   R find(T u);

   @SuppressWarnings("unchecked")
   static <T, R> Function<T, R> of(Class<R> c){
      return t->(R)search(c, t);
   }

   static <T> Object search(Class<T> c, Object o){
      Field[] fs = o.getClass().getDeclaredFields();
      String cname = c.getName();
      for(Field f:fs){
         String typename = f.getType().getName();
         if (typename.startsWith("[")) continue;
         try{
            f.setAccessible(true);
            if (typename.startsWith("java.") || typename.startsWith("jdk.")
               || typename.startsWith("javax.")   || typename.startsWith("org.xml.")
               || typename.startsWith("org.w3c.")|| typename.startsWith("org.omg.")){
               if (cname.equals(f.getType().getName())){
                  return f.get(o);
               }
               continue;
            }
            if (typename.equals("double") || typename.equals("float")
               || typename.equals("long") || typename.equals("short")
               || typename.equals("int") || typename.equals("byte")
               || typename.equals("char") || typename.equals("boolean")){
               if (c.isPrimitive()){
                  return f.get(o);
               }
               continue;
            }
            if (cname.equals(f.getType().getName())){
               return f.get(o);
            }
            Object v = f.get(o);
            if (v==null) continue;
            return search(c, v);
         }catch(IllegalArgumentException e){
         }catch(IllegalAccessException e){
         }
      }
      return null;
   }
}

使用例、
入れ子構造のBean の中の Obejct を取得する(1) - Oboe吹きプログラマの黙示録
のクラスの場合、、

Aunit a = new Aunit();
Cunit c = UniqueFieldfinder.of(Cunit.class).apply(a);

とにかく、Bean や DTO の設計として、あまりにも入れ子構造が深いのは、
本当によくない。

入れ子構造のBean の中の Obejct を取得する(1)

入れ子構造のクラス、例えば以下のようなクラス、
(setterやgetterなどはここでは省略)

public class Aunit{
	private int id;
	private String name;
	private Bunit bunit;
}
public class Bunit{
	private String name;
	private Cunit cunit;
}
public class Cunit{
	public int id ;
	public String name;
}

書いているロジック内、先頭のクラスのインスタンスだけ持っていって、
一番下の階層のクラスインスタンスを抽出するのは、getterメソッドのチェインを書かなくてはならないか、
いちいち1つ1つ宣言とgetter による取得を書かなくてはならない。

フィールド数が多いと見るのも厭になるし、3階層、4階層とネストが深くなる設計をされると、
非難の的であろう。

タイプセーフではないし、配列、リスト、マップでは使えないが、
以下のような簡単なユーティリティを用意する。

( org.yipuran.util.Fieldgetter :
yipuran-core/Fieldgetter.java at master · yipuran/yipuran-core · GitHub
を使ってる。)

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.yipuran.util.Fieldgetter;

/**
 * GenericGetter
 */
public class GenericGetter{
   private List<String> nlist;

   private GenericGetter() {
      nlist = new ArrayList<>();
   }
   public static  GenericGetter of() {
      return new GenericGetter();
   }
   public GenericGetter with(Supplier<String> s) {
      nlist.add(s.get());
      return this;
   }
   public GenericGetter with(String s) {
      nlist.add(s);
      return this;
   }
   @SuppressWarnings("unchecked")
   public <T, R> R get(T t) {
      Object o = t;
      for(String s:nlist) {
         o = Fieldgetter.of(e->s).apply(o);
      }
      return (R)o;
   }
}

ちょっと大胆な名前だけど、サンプルなので、ご勘弁。

Aunit a = new Aunit();
//  Aunit 内をセットした後で、、、

int id = GenericGetter.of()
      .with(()->"bunit")
      .with(()->"cunit")
      .with(()->"id")
      .get(a);

String cname = GenericGetter.of()
      .with("bunit")
      .with("cunit")
      .with("name")
      .get(a);

with 順番で指定しなければならないが、パスとして確実な指定はできる。

今回は、フィールドの宣言名称を参照していく方法で、プロジェクトのコーディング規則、命名規則
影響を受けやすい。

つぎは、フィールドの宣言名でなく、クラス定義、型で参照する方法を考える。。

markdown ファイルをPDFに変換(ATOMで)

ATOM markdown-preview-enhanced でPDF保存する方法もあるが、
直接、md ファイルからPDF に変換するパッケージもある。

https://atom.io/packages/markdown-pdf

ATOMインストールする時のパッケージ検索キーは、markdown-pdf
Ctrl+Shft+C 押下で同じフォルダ内に、拡張子 pdf で作ってくれる。

インストールすると、AROMのメニューに、、、
f:id:posturan:20200105163711j:plain

右端に結果を書いた表のCSVから結果を求める

昨日は、表の最終行に結果を書いた表CSVだったが、右端に置いた表の場合での
プログラムはどうなるか、
f:id:posturan:20191231112948j:plain
とても簡単になる。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.yipuran.csv.CsvStreamProcessor;
import org.yipuran.csv4j.ParseException;
import org.yipuran.csv4j.ProcessingException;
/**
 * MatrixRight
 */
public class MatrixRight<T>{
   private Map<String, T> map = new HashMap<>();
   public MatrixRight(File file, Function<String, T> resultFunc){
      try(InputStream in = new FileInputStream(file)){
         Csvprocess csvproces = new Csvprocess();
         csvproces.read(new InputStreamReader(in, "MS932"), h->{}, (n, p)->{
            map.put(IntStream.range(1, p.size()-1).boxed()
                     .map(i->p.get(i)).collect(Collectors.joining("_"))
                  , resultFunc.apply(p.get(p.size()-1)));
         });
      }catch(IOException ex){
         ex.printStackTrace();
      }catch(ParseException ex){
         ex.printStackTrace();
      }catch(ProcessingException ex){
         ex.printStackTrace();
      }
   }
   public Optional<T> query(String...ary){
      return Optional.ofNullable(map.get(Arrays.stream(ary).collect(Collectors.joining("_"))));
   }
   public Optional<T> query(List<String> list){
      return Optional.ofNullable(map.get(list.stream().collect(Collectors.joining("_"))));
   }
}

Excel で書いた表をCSVにしてるので、"MS932" 文字コードエイリアスで読込む、
使用例

MatrixRight<Boolean> mr =   new MatrixRight<>(file2, e->Boolean.valueOf(e));
Boolean b = mr.query("Y", "N", "N").get();

問い合わせマトリクスの実装

複数の条件、条件の数にもよるが表にまとめないと管理が辛いことは、よくあるケースです。
条件数も少なめで、求めた結果も偏りがあるなら、if 文のネストを書いてもなんとか人の思考も
時間的に追いつくでしょう。
案外、条件1つ増えただけでも、数学的に結構な組み合わせ結果は増えていくもので、
if 文を連続して書いていくのって賢くないのでは?と思うことが多くあります。
f:id:posturan:20191230140004j:plain
↑のような表の条件表、条件A~Cの Y or N に対する、結果 Result TRUE/FALSE の表が存在する時、
if 文を書かないで、結果 Result を求める方法

結果の方向が少ない方だけを、A→B→C 順=縦順で Y/N を連結して最後に、Result TRUE/FALSEを
連結した String[] を用する。

private String[] patterns = {
   "Y_Y_N_true", "Y_N_N_true", "N_Y_Y_true"
};

これをクラスのコンストラクタ内で、A→B→Cの Y/Nの連結をキーに、Result TRUE/FALSEを値の
Map にする。

private Map<String, Boolean> map;
map = Arrays.stream(patterns).collect(()->new HashMap<String, Boolean>()
, (r, t)->r.put(t.substring(0, t.lastIndexOf("_")), Boolean.valueOf(t.substring(t.lastIndexOf("_") + 1)))
, (r, u)->{});

これで準備が整い、A,B.Cの条件の問い合わせは、

public boolean query(String a, String b, String c) {
   return Optional.ofNullable(map.get(a+"_"+b+"_"+c)).orElse(false);
}

と、完全にタイプセーフではないが、複雑なif 文を書かなくてすむ。

でも、条件数が多くて上の String[] patterns など、コードに大量に書いてられないという場合も
あるだろう。

では、条件表ファイルをプログラムに読込ませるという考え方を採用する。
Excel のまま読むこともできるが、プロジェクトによっては、Apache-POI で読込む処理の導入を禁止
することもあるだろう。管理上、そのまま Excel読込んだ方が良いとは思うが。。。
せめても、Excel からCSVにしたファイルで実装する。
yipuran-csvCsvprocess を使わせてもらう)
クラスと同じ場所に、pattern.csv というファイル名でCSVを用意した実装
まだ、Java8 使用のプロジェクトが世の中には多いので仕方なく Java8 で書く。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.yipuran.csv.Csvprocess;
import org.yipuran.csv4j.ParseException;
import org.yipuran.csv4j.ProcessingException;
/**
 * Matrix
 */
public class Matrix{
   private Map<String, Boolean> map;

   public Matrix(){
      Map<Integer, String> cmap = new HashMap<>();
      String csvpath = Matrix.class.getPackage().getName().replaceAll("\\.", "/") + "/pattern.csv";
      try(InputStream in = new FileInputStream(
             new File(ClassLoader.getSystemClassLoader().getResource(csvpath).toURI()))
      ){
         Csvprocess csvproces = new Csvprocess();
         csvproces.read(new InputStreamReader(in, "MS932"), h->{}, (n, p)->{
            IntStream.range(1, p.size()).boxed().forEach(i->{
               Optional.ofNullable(cmap.get(i)).<Runnable>map(e->()->{
                  cmap.put(i, e + "_" + p.get(i));
               }).orElse(()->{
                  cmap.put(i, p.get(i));
               }).run();
            });
         });
         map = cmap.values().stream().collect(()->new HashMap<String, Boolean>()
               , (r, t)->r.put(t.substring(0, t.lastIndexOf("_"))
                               , Boolean.valueOf(t.substring(t.lastIndexOf("_") + 1)))
               , (r, u)->{});
      }catch(IOException ex){
         ex.printStackTrace();
      }catch(URISyntaxException ex){
         ex.printStackTrace();
      }catch(ParseException ex){
         ex.printStackTrace();
      }catch(ProcessingException ex){
         ex.printStackTrace();
      }
   }
   public Optional<Boolean> query(List<String> list){
      return Optional.ofNullable(map.get(list.stream().collect(Collectors.joining("_"))));
   }
   public Optional<Boolean> query(String...ary){
      return Optional.ofNullable(map.get(Arrays.stream(ary).collect(Collectors.joining("_"))));
   }
}

query メソッドで、条件を順序を考慮してリストで呼び出す。

でも、↑は結果をBoolean に限定していて良くない!

結果Object を、総称型とし、CSVファイルは File で指定させる。
関数型インターフェース Function で、最終行の結果文字列 String から
結果Object を求めるラムダを指定させるようにする。

これも、まだ Java8 使用のプロジェクトが世の中には多いので仕方なく Java8 で書く。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.yipuran.csv.Csvprocess;
import org.yipuran.csv4j.ParseException;
import org.yipuran.csv4j.ProcessingException;

/**
 * MatrixModel
 */
public class MatrixModel<T>{
   private Map<String, T> map;

   public MatrixModel(File file, Function<String, T> resultFunc) {
      Map<Integer, String> cmap = new HashMap<>();
      try(InputStream in = new FileInputStream(file)){
         Csvprocess csvproces = new Csvprocess();
         csvproces.read(new InputStreamReader(in, "MS932"), h->{},(n, p)->{
            int cmax = p.size();
            IntStream.rangeClosed(1, cmax).boxed().forEach(i->{
               Optional.ofNullable(cmap.get(i)).<Runnable>map(e->()->{
                  cmap.put(i, e + "_" + p.get(i));
               }).orElse(()->{
                  cmap.put(i, p.get(i));
               }).run();
            });
         });
         map = cmap.values().stream().collect(()->new HashMap<String, T>()
               , (r, t)->r.put(t.substring(0, t.lastIndexOf("_"))
                           , resultFunc.apply(t.substring(t.lastIndexOf("_") + 1)))
               , (r, u)->{});
      }catch(IOException ex){
         ex.printStackTrace();
      }catch(ParseException ex){
         ex.printStackTrace();
      }catch(ProcessingException ex){
         ex.printStackTrace();
      }
   }
   public Optional<T> query(List<String> list){
      return Optional.ofNullable(map.get(list.stream().collect(Collectors.joining("_"))));
   }
   public Optional<T> query(String...ary){
      return Optional.ofNullable(map.get(Arrays.stream(ary).collect(Collectors.joining("_"))));
   }
}

インスタンス生成の都度、CSV読込みでそれが嫌ならシングルトン化でもすれば良いであろう。

呼出し、、、

MatrixModel<Boolean> m = new MatrixModel<>(file, e->Boolean.valueOf(e));

if (m.query("Y", "N", "N").orElse(false)){
      //  TODO  Result : True の処理、、、
}

main/resources に配置するテキストファイルの扱い

開発 project の main/resources に、UTF-8 で書いたテキストファイルを用意して
ビルドのクラスPATH target/classes などに配置されて
読込み実行して、
それを Windowsコマンドプロンプトで実行して、文字化けしないように標準出力する。
変な要件かもしれないけど、UTF-8 で全て記述管理する方針ならありだと考える。

ファイル読込みに注意して String にする必要がある。

CharsetNames.UTF_8 だと、UnsupportedEncodingException 捕捉の catch を書かなくてはならないので、
java.nio.charset.StandardCharsets を使う

public String readTextUTF8(String filename){
   try(InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(filename);
      ByteArrayOutputStream bo = new ByteArrayOutputStream()){
      byte[] b = new byte[1024];
      int len;
      while((len=in.read(b, 0, b.length)) >= 0){
         bo.write(b, 0, len);
      }
      bo.flush();
      bo.toByteArray();
      return new String(bo.toByteArray(), StandardCharsets.UTF_8);
   }catch(IOException ex){
      ex.printStackTrace();
      throw new RuntimeException(ex);
   }finally{
   }
}