jsTree ツリーを折りたたみ時、選択していたノードが存在したかチェックする

jsTree でノードを折りたたみをした時に
折りたたんだ中に選択していたノードが存在したかチェックして処理したい場合がある。

”折りたたみ”のイベントを探して処理を書く。
→ここで、探す。。 https://www.jstree.com/api/#/?q=.jstree%20Event&f=after_close.jstree

after_close.jstree

あらかじめ、選択したノード id を格納する hidden フィールドなどを準備

<input type="hidden" id="selectid">

after_close.jstree イベントで、node.children_d 配列に
存在するかチェックする。

$('#tree').jstree({
   'core':{
      'data':{
         "url":"./querytree",
         "dataType":"json"
      }
   }
}).on('after_close.jstree', function(e, data){
   var selectedID = $('#selectid').val();
   if ($.inArray(selectedID, data.node.children_d) >= 0){
      console.log("closeしたノードの中で 見つかった");
   }else{
      console.log("closeしたノードの中で 見つからない");
   }
}).on('select_node.jstree', function(e, data){
   // 選択したノードの id を、hiddenフィールドに保持しておく
   $('#selectid').val(data.node.id);
});

再帰メソッドとラムダ式

再帰呼出しをするように関数型インターフェースを書くまでもない、
単にメソッド宣言を省略するだけの目的で考える。
再帰と言っても、実際の仕事の機会では、フィボナッチ数列を求めるような機会よりも
処理が遅くてもいい探索目的の機会の方が多い。

例題(簡単に紹介するための例題)
以下 Item オブジェクトの親子関係のリストからルートを求める

public class Item{
   public int id;
   public String name;
   public Integer parent;
   public Item(int id, String name, Integer parent){
      this.id = id;
      this.name = name;
      this.parent = parent;
   }
}

要件発生の都度、再帰メソッドを書く場合、、

static Item getRoot(Item item, List<Item> list){
   if (item==null || item.parent==null) return item;
   return getRoot(list.stream().filter(e->e.id==item.parent).findAny().orElse(null), list);
}

ジェネリックとBiFunction , Predicate で再帰メソッドを定義

import java.util.function.BiFunction;
import java.util.function.Predicate;
/**
 * Refrain
 */
public interface Refrain{

   public static <T, U> T call(T t, U u, BiFunction<T, U, T> f, Predicate<T> p){
      T r = f.apply(t, u);
      if (p.test(r)) return r;
      return  call(r, u, f, p);
   }
}

呼出し例

List<Item> list = new ArrayList<>();
list.add(new Item(1, "A", null));
list.add(new Item(2, "B", 1));
list.add(new Item(3, "C", 2));
list.add(new Item(4, "D", 3));
Item root = Refrain.call(item, list
   , (t, u)->u.stream().filter(e->e.id==t.parent).findAny().orElse(null)
   , e->e==null || e.parent==null);

Refrain と、いいかげんに名前をつけました。
ふと、頭の中で浮かんだのが、ユーミンの「リフレインが、」・・・
世代がバレてしまいますね。

LazyModalPanel

先日、Wicket で、処理中をModalWindow で表現するを書いたのですが、、
oboe2uran.hatenablog.com

処理が終わった後に、正常終了か異常終了かのメッセージを表示したい
というのを対応しようと書き直しました。
f:id:posturan:20181206220011j:plain
というのを処理完了時に出すようにします。

Wicket-Panel のHTML を以下のように修正します。
処理開始トリガになる見えないボタン wicket:id="lazy_mocal_start" id="lazy_mocal_start" と
処理終了時に見せるボタン wicket:id="lazy_mocal_close" として、ボタンラベルも span で書きます。
必要な JavaScript もここに書きます(前回は、JSファイルに分けていた)

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
   <form wicket:id="lazy_mocal_form" id="lazy_mocal_form" >
      <section class="lazy-modal-panel">
         <div>
            <ul>
               <li id="lazy_modal_progress_container"><div id="lazy_modal_progress"></div></li>
               <li><span wicket:id="message"></span></li>
               <li id="lazy_modal_close_container" style="display:none">
                  <button wicket:id="lazy_mocal_close" type="button"><span wicket:id="closeLabel"></span></button>
               </li>
               <li style="display:none">
                  <button wicket:id="lazy_mocal_start" id="lazy_mocal_start" type="button"></button>
               </li>
            </ul>
         </div>
      </section>
   </form>
<script type="text/javascript">
var sizefitMessageModal = function(){
   $('.w_content_container').css("height", $('.lazy-modal-panel ul').outerHeight(true) + "px" );
   $('.wicket-modal').css("width", $('.lazy-modal-panel ul').outerWidth(true) + 22 + "px" );
};
var convergementLazy = function(){
   $('#lazy_modal_progress_container').remove();
   $('#lazy_modal_close_container').removeAttr('style');
   $('.lazy-modal-panel li span').addClass('reduce');
   $('.w_content_container').css("height", $('.lazy-modal-panel ul').outerHeight(true) + "px" );
   $('.wicket-modal').css("width", $('.lazy-modal-panel ul li span').outerWidth(true) + 40 + "px" );
};
$(function(){
   new Spinner().spin(document.getElementById('lazy_modal_progress'));
   setTimeout("$('#lazy_mocal_form').parent().parent().parent().parent().parent().prev().children('a').css('display','none');",100);
});
</script>
</wicket:panel>
</body>
</html>

スタイルシート CSS は、処理終了時、spin.js によるSPINアニメーションを表示しない分、調整します
.lazy-modal-panel li span.reduce

@CHARSET "UTF-8";
/* lazymodal.css */
.lazy-modal-panel ul{
   margin: 10px 0;
   padding: 10px;
}
.lazy-modal-panel li{
	list-style-type: none;
   white-space: nowrap;
   display: flex;
   align-items: center;
   justify-content: space-around;
}
.lazy-modal-panel li span{
   margin: 40px 0 5px 0;
}
.lazy-modal-panel li span.reduce{
	margin: 5px 20px !important;
}
#lazy_modal_progress{
	position: absolute;
	margin-top: 10px;
}

Panel の Javaソース
終了時メッセージ表示しない自動で閉じる場合も、コンストラクタとして残します。
メッセージ表示するコンストラクタは、ボタンラベル文字列やボタンのスタイル class属性を
付与できるものを用意します。省略もできます。

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.wicket.ajax.AjaxEventBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.core.util.string.JavaScriptUtils;
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.resource.CssResourceReference;
import org.apache.wicket.request.resource.JavaScriptResourceReference;
import org.danekja.java.util.function.serializable.SerializableConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;
import org.yipuran.wicketcustom.function.SerialThrowableConsumer;
/**
 * LazyModalPanel
 */
public class LazyModalPanel extends Panel{
   /**
    * コンストラクタ(自動で閉じる)
    * @param id Wicket-ID
    * @param model 処理中メッセージ
    * @param consumer 目的の処理実行する Throwableな Consumer
    *                  例外発生すると oncatch指定のConsumerが実行される
    * @param oncatch 目的の処理をするconsumerで例外を捕捉した時に実行するConsumer
    */
   public LazyModalPanel(String id, IModel<String> model
         , SerializableConsumer<AjaxRequestTarget> consumer
         , SerializableConsumer<Exception> oncatch){
      super(id, model);
      queue(new Form<Void>("lazy_mocal_form"));
      queue(new Label("message", Optional.ofNullable(model.getObject()).orElse("")));
      queue(new Label("closeLabel", ""));
      queue(new Button("lazy_mocal_start").add(AjaxEventBehavior.onEvent("click", SerialThrowableConsumer.of(t->{
         consumer.accept(t);
         ModalWindow.closeCurrent(t);
      },(t, x)->{
         oncatch.accept(x);
         ModalWindow.closeCurrent(t);
      }))));
      queue(new Button("lazy_mocal_close"));
   }
   public LazyModalPanel(String id, IModel<String> model, String closeLabel
         , SerializableFunction<AjaxRequestTarget, String> function
         , SerializableFunction<Exception, String> oncatch){
      this(id, model, closeLabel, null, function, oncatch);
   }
   public LazyModalPanel(String id, IModel<String> model
         , SerializableFunction<AjaxRequestTarget, String> function
         , SerializableFunction<Exception, String> oncatch){
      this(id, model, "OK", null, function, oncatch);
   }
   /**
    * コンストラクタ(終了後結果表示する場合)
    * @param id Wicket-ID
    * @param model 処理中メッセージ
    * @param closeLabel CLOSEボタンのラベル
    * @param stylelist CLOSEボタンに付けるclass属性、
    * @param function 目的の処理を実行して正常終了のメッセージを返す Throwable な Function
    *                  例外発生すると oncatch指定の Function が実行される
    * @param oncatch 目的の処理で例外を捕捉した時に実行する Function
    *                 表示する異常終了のメッセージを返す Function
    */
   public LazyModalPanel(String id, IModel<String> model
         , String closeLabel
         , List<String> stylelist
         , SerializableFunction<AjaxRequestTarget, String> function
         , SerializableFunction<Exception, String> oncatch){
      super(id, model);
      queue(new Form<Void>("lazy_mocal_form"));
      final Label mesageLabel = new Label("message", Optional.ofNullable(model.getObject()).orElse(""));
      mesageLabel.setEscapeModelStrings(false);
      mesageLabel.setOutputMarkupId(true);
      queue(mesageLabel);
      queue(new Label("closeLabel", closeLabel));
      queue(new Button("lazy_mocal_start")
      .add(AjaxEventBehavior.onEvent("click", SerialThrowableConsumer.of(t->{
         String s = function.apply(t);
         mesageLabel.setDefaultModelObject(Optional.ofNullable(s).orElse(null));
         t.appendJavaScript("convergementLazy();");
         t.add(mesageLabel);
      },(t, x)->{
         String s = oncatch.apply(x);
         mesageLabel.setDefaultModelObject(Optional.ofNullable(s).orElse(null));
         t.appendJavaScript("convergementLazy();");
         t.add(mesageLabel);
      }))));
      queue(new Button("lazy_mocal_close"){
         @Override
         protected void onComponentTag(ComponentTag tag){
            super.onComponentTag(tag);
            Optional.ofNullable(stylelist).filter(li->li.size() > 0).ifPresent(li->{
               tag.put("class", li.stream().collect(Collectors.joining(" ")));
            });
         }
      }.add(AjaxEventBehavior.onEvent("click", t->ModalWindow.closeCurrent(t))));
   }

   @Override
   protected void onAfterRender(){
      super.onAfterRender();
      JavaScriptUtils.writeJavaScript(getResponse()
      , "setTimeout('sizefitMessageModal();$(\"#lazy_mocal_start\").trigger(\"click\");', 100);");
   }
   @Override
   public void renderHead(IHeaderResponse response){
      super.renderHead(response);
      response.render(CssHeaderItem.forReference(
            new CssResourceReference(LazyModalPanel.class, "lazymodal.css")));
      response.render(JavaScriptHeaderItem.forReference(
            new JavaScriptResourceReference(LazyModalPanel.class, "spin.min.js")));
   }
}

処理終了でメッセージ表示する例の呼び出しコード
Arrays.asList を使えば、ボタンデザインのスタイルも簡単に記述できます
以下は、Bootstrap使用 のボタンの場合です。

final ModalWindow window = new ModalWindow("lazy_window").setResizable(true).setAutoSize(true);
queue(window);

queue(new Button("link").add(AjaxEventBehavior.onEvent("click", t->{
   window.setContent(new LazyModalPanel(window.getContentId(), Model.of("処理中...")
   , "OK", Arrays.asList("btn", "btn-primary")
   , u->{

      // 時間がかかる処理

      return "正常終了<br/>メッセージ";
   }, x->{

      // 例外捕捉

      return "異常終了";
   }));
   window.show(t);
})));

上は、Page クラスのHTMLで、wicket:head タグを以下のように書いてます。

<wicket:head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</wicket:head>

Apache POI 入力規則リストの注意

先日、以下を書いたが、この中の createExplicitListConstraint は注意が必要だ。
Apache POI 入力規則リストの生成 - Oboe吹きプログラマの黙示録

入力規則をシート参照でなく任意のリストで指定する createExplicitListConstraint は、
どうやら、引数で指定する配列の個数に制限あるようだ。
88個までOKで、89個指定するとNGだった。どこからこの数が決まるのか??

XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper(sheet);
List<String> alist
 = IntStream.rangeClosed(1, 89).boxed().map(e->Integer.toString(e)).collect(Collectors.toList());
XSSFDataValidationConstraint dvConstraint
 = (XSSFDataValidationConstraint)dvHelper.createExplicitListConstraint(alist.toArray(new String[alist.size()]));
CellRangeAddressList addressList = new CellRangeAddressList(0, 10, 0, 0);
XSSFDataValidation validation = (XSSFDataValidation)dvHelper.createValidation(dvConstraint, addressList);
validation.setShowErrorBox(true);
sheet.addValidationData(validation);

これは、89個でExcelを開くと エラーになる。
89個以上は、シートに書いてシートを参照 createFormulaListConstraint
使用すれば問題ない。
createFormulaListConstraint を使って、入力規則用に余計な書込みはしたくないのだが、
他に手段が見つからない。

リストの逆順

今更、Javaのおさらいです。

List<String> list = Arrays.asList("A", "B", "C", "D", "E");

java.util.Collectionsreverse をすれば簡単ですが元のリスト自体を逆順にしてしまいます。
そうじゃなくて、「リストの逆順の処理を短く記述したい」
逆順のリスト生成するしかないのか、
LinkedList として生成して Deque インターフェース、
両端キュー内の要素を逆順で繰り返し処理するイテレータを使う ⇒ descendingIterator

for(Iterator<String> it=new LinkedList<>(list).descendingIterator(); it.hasNext();){
   String s = it.next();
   // TODO
});

Stream API で。。。

list.stream().collect(ArrayList::new, (r, t)->r.add(0, t), (r, u)->r.addAll(0, u))
.stream().forEach(e->{
   // TODO 
});

どちらも新しい逆順のリストを生成することには変わりはない。

でも、逆順で連結をしたい時は、以下のように書くこともできる。

String str = list.stream().reduce((e1, e2)->e2 + "." + e1).get();

Apache POI 入力規則リストの生成

よく見かけるサンプルを基に書くと、以下のように固定リストを指定した書き方がある。

try(Workbook book = new XSSFWorkbook();
   OutputStream out = new FileOutputStream("a.xlsx")){

   XSSFSheet sheet = (XSSFSheet)book.createSheet("シート1");
   
   IntStream.rangeClosed(0, 20).boxed().forEach(i->{
      sheet.createRow(i).createCell(1).setCellValue(i);
   });
   
   // 入力規則 Helper
   XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper(sheet);
   
   // 入力データ
   XSSFDataValidationConstraint dvConstraint
     = (XSSFDataValidationConstraint)dvHelper.createExplicitListConstraint(new String[]{"11", "21", "31"});
   
   // 入力規則を適用させるセルの範囲   (int firstRow, int lastRow, int firstCol, int lastCol)
   CellRangeAddressList addressList = new CellRangeAddressList(0, 10, 0, 0);
   
   // 入力規則の定義
   XSSFDataValidation validation = (XSSFDataValidation)dvHelper.createValidation(dvConstraint, addressList);
   
   // ドロップダウンリストから選択できるようにする
   validation.setShowErrorBox(true);
   
   // データの入力規則を設定する
   sheet.addValidationData(validation);
   
   // 書込み
   book.write(out);
}catch(Exception e){

}

これは、これでいいんだけど、XSSFDataValidationConstraint としてシート内、別シートのセルの値になるように
したいことがあります。

createExplicitListConstraint ではなくて、createFormulaListConstraint を使うのです。

B列4行目~10行目のセルをプルダウン入力にするなら、、

XSSFDataValidationConstraint dvConstraint
 = (XSSFDataValidationConstraint)dvHelper.createFormulaListConstraint("$B4:$B10");

B列全部、、

XSSFDataValidationConstraint dvConstraint
 = (XSSFDataValidationConstraint)dvHelper.createFormulaListConstraint("$B:$B");

B列全部の指定なら行挿入しても入力規則プルダウンは追従するんですけど、$B4:$B10 の指定してしまうと
行挿入でいちいち修正メンテしなくてはならず不便です。
そこで、OFFSET関数を指定します。

XSSFDataValidationConstraint dvConstraint
 = (XSSFDataValidationConstraint)dvHelper.createFormulaListConstraint("OFFSET($B$3,0,0,COUNTA($B:$B)-1,1)");

こうすれば、B列3行目以降で値が存在する分、COUNTA関数で指定されて入力規則範囲になります。

OFFSET関数は、

OFFSET( startcell, toRow, toColumn, rangRow, rangeColumn )

startcellをスタートとして、
toRow行、toColumn列だけ移動したところを基準に、
rangRow行×rangeColumn列の範囲を選択する

Wicket Page で getResource と Test class での getResource

Webプロジェクト src/main/resources に置いたファイルを WebPage でも、
src/test/java で書くテストクラスでも読込みたい。

個別のクラスローダーで読みたく ClassLoader.getSystemClassLoader() を使ってしまうと
読めないので、WicketWebApplication クラスを getしてそのクラスローダーで
Webコンテナ起動で WEB-INF/classes に配置される src/main/resources に置いたファイルを
WebPage クラスでは、以下のように読込みます。

プロジェクト src/main/resources/a.json がある。

まずは、、java.nio.file.Path

Path path 
= Path.of(getApplication().getClass().getClassLoader().getResource("a.json").toURI());

でもこれは、URISyntaxException を発生させるので、、

↓yipuran-core で用意した Throwable な Supplier を使います。
https://github.com/yipuran/yipuran-core/blob/master/src/main/java/org/yipuran/function/ThrowableSupplier.java
( Java11 Files.readString も使ってます )

File file = new File(Optional.of(
   ThrowableSupplier.to(
()->getApplication().getClass().getClassLoader().getResource("a.json").toURI()).get()
).get());
String s = ThrowableSupplier.to(()->Files.readString(file.toPath(), Charset.forName("UTF-8"))
).get();


もっと短く書けるはずで、絶対に存在するファイルなら、以下のように書けます。

String s = ThrowableSupplier.to(()->Files.readString(
   Path.of(getApplication().getClass().getClassLoader().getResource("a.json").toURI())
, Charset.forName("UTF-8"))
).get();

それでも、ThrowableSupplier は、

static <R> Supplier<R> to(ThrowableSupplier<? extends R> supplier, Function<Exception, R> onCatch)

であるので、

String s = ThrowableSupplier.to(()->Files.readString(
   Path.of(getApplication().getClass().getClassLoader().getResource("a.json").toURI())
, Charset.forName("UTF-8"))
, x->{
   // ファイル読込み失敗時の処理をする
   return "file read error";
}).get();

とも書けます。


src/test/ の方で記述するコード
WicketWebApplication 継承クラスが、MyApplication だとして、、

String s = ThrowableSupplier.to(()->Files.readString(
   Path.of(MyApplication.class.getClassLoader().getResource("a.json").toURI())
, Charset.forName("UTF-8"))
, x->{
   return "Not Found  file";
}).get();

テスト用とWeb実装用、同じ src/main/resources に置いたファイルを読み込めます。