jsTree JSON データの変換

ツリービューの情報を管理するのに、ノードの親子関係の処理を新たにコーディングするのは、どんな言語であれ
労力が必要です。
できれば、画面でツリーの描画操作の結果をそのまま管理するのが楽です。
せっかく JSON というオブジェクトで表現するのですから、このJSONの考えで管理したいです。

jsTree 使用のコード上で!JSONを出力させる方法は、、get_jsonを実行することです。

.on('loaded.jstree', function(){
  var v = $(this).jstree(true).get_json('#', {flat:true});
  console.log( JSON.stringify(v, null, " ") );
});

でもここで出力されるのは、、jSTree を呼ぶ時に書いたそのままのJSONではないです。

[
 {
  "id": "1",
  "text": "Root",
  "icon": "jstree-folder",
  "li_attr": {
   "id": "1"
  },
  "a_attr": {
   "href": "#",
   "id": "1_anchor"
  },
  "state": {
   "loaded": true,
   "opened": true,
   "selected": false,
   "disabled": false
  },
  "data": {},
  "parent": "#"
 },
 {
   "id":"2"

この中で改めて再度jQueryでjsTree()を呼び出し、描画で必要な情報は、
"id", "text", "icon", "parent", と "state" の各boolean値です。
stateは最初表示し直すならもしかして要らないかもしれません。
そうすると、check_callback や、move_node.jstree のイベント捕捉で、
HTML-form の input type="hidden" フィールドにセットして
送信してあげれば、
 「ツリーの編集結果」→ サーバに送信
ということができるわけです。

(例)
input type="hidden" フィールドにセットを関数にしておきます。

var setHiddenJson = function(){
   var v = $('#tree').jstree(true).get_json('#', {flat:true});
   $("#jsondata").val(JSON.stringify(v));
};

check_callback のリネーム、削除の捕捉では setTimeout でcallする必要があります

"check_callback" : function(operation, node, node_parent, node_position, more){
   if (operation=="move_node"){
      if (node_parent.icon != "jstree-folder" && node_parent.id != "#") return false;
   }
   if (operation=="rename_node" || operation=="delete_node"){
      setTimeout("setHiddenJson()", 100);
   }
}

ドラッグ&ドロップ時はそのまま呼出します。

.on('move_node.jstree', function(e, data){
  setHiddenJson();
})

受け取りサーバでの管理、Java前提ですが、、
受信テキストJSONGoogle Gson で任意オブジェクトに変換します。
→ 以下、JstreeNode です。

public class JstreeNode implements Serializable{
   public String id;
   public String text;
   public String icon;
   public String parent;
   public Object state;
   public boolean loaded;
   public boolean opened;
   public boolean selected;
   public boolean disabled;

   public JstreeNode(){
   }
    // setter を用意します。getterメソッド は不要

肝になるのは、JSON → 任意オブジェクトのアダプタです。
デシリアライザが必要でシリアライザは不要なのですが、ついでなので書きます。

import java.lang.reflect.Type;
import java.util.HashMap;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
 * JstreeNodeAdapter
 */
public class JstreeNodeAdapter implements JsonDeserializer<JstreeNode>
                              , JsonSerializer<JstreeNode>{
   @Override
   public JstreeNode deserialize(JsonElement json, Type typeOfT
, JsonDeserializationContext context) throws JsonParseException{
      final JsonObject jo = json.getAsJsonObject();
      JstreeNode rtn = new JstreeNode();
      rtn.id = jo.get("id").getAsString();
      rtn.text = jo.get("text").getAsString();
      rtn.icon = Optional.ofNullable(jo.get("icon")).map(e->e.getAsString()).orElse(null);
      rtn.parent = Optional.ofNullable(jo.get("parent")).map(e->e.getAsString()).orElse(null);
      final JsonObject state = jo.getAsJsonObject("state");
      if (state != null){
             rtn.loaded = Optional.ofNullable(state.getAsJsonPrimitive("loaded")).map(e->e.getAsBoolean()).orElse(true);
             rtn.opened = Optional.ofNullable(state.getAsJsonPrimitive("opened")).map(e->e.getAsBoolean()).orElse(true);
             rtn.selected = Optional.ofNullable(state.getAsJsonPrimitive("selected")).map(e->e.getAsBoolean()).orElse(false);
             rtn.disabled = Optional.ofNullable(state.getAsJsonPrimitive("disabled")).map(e->e.getAsBoolean()).orElse(false);
     }
      return rtn;
   }
   @Override
   public JsonElement serialize(JstreeNode src, Type typeOfSrc
, JsonSerializationContext context){
      final JsonObject rtn = new JsonObject();
      rtn.add("id", context.serialize(src.id));
      rtn.add("text", context.serialize(src.text));
      rtn.add("icon", context.serialize(src.icon));
      rtn.add("parent", context.serialize(src.parent));
      HashMap<String, Object> map = new HashMap<>();
      map.put("loaded", src.loaded);
      map.put("opened", src.opened);
      map.put("selected", src.selected);
      map.put("disabled", src.disabled);
      rtn.add("state", context.serialize(map));
      return rtn;
   }
}

HTML-form 受信データをこのアダプタを使って、JstreeNodeリストを組み立てます。

Gson gson = new GsonBuilder().serializeNulls()
.registerTypeAdapter(JstreeNode.class, new JstreeNodeAdapter())
.create();

List<JstreeNode> list = gson.fromJson(jsonHidden.getModelObject()
, new TypeToken<List<JstreeNode>>(){}.getType());

TypeToken は、com.google.common.reflect.TypeToken です。

画面でツリー編集したものはこれで受け取り Javaオブジェクトから好きな形に変換
するなり、管理します。

問題は、JstreeNodeリストデータ→jsTree表示で、
このjsTree表示を実行するのは、JSON受信した時とは少し異なるJSONでなければ
なりません。
しかも、jsTree は、JSONのルールチェックを厳密に行います。

「少し異なるJSON」=子ノードは、"children" キーで書かなくてはならないです。

そこで、JstreeNodeリストデータ→jsTree 表示用、AJAX で表示するデータへの変換
という処理のクラスを用意します。
誰でも書くような、簡単な再帰ロジックです。

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
 * JstreeNode のリストの InputStream から
 *  jsTree jsTree AJAX に渡す JSONデータ を OutputStream に書き込む」
 *
 * 例)
 * try(InputStream in = new FileInputStream(file);
 *      ByteArrayOutputStream out = new ByteArrayOutputStream()){
 *     JstreeDataProvider p = JstreeDataProvider.of(in);
 *     p.write(out);
 * }catch(IOException e){
 *    e.printStackTrace();
 * }
 *
 */
public final class JstreeDataProvider{
   private InputStream in;
   private Gson gson;

   public JstreeDataProvider(InputStream in){
      this.in = in;
      gson = new GsonBuilder().serializeNulls()
      .registerTypeAdapter(JstreeNode.class, new JstreeNodeAdapter())
      .create();
   }
   public static JstreeDataProvider of(InputStream in){
      return new JstreeDataProvider(in);
   }

   public void write(OutputStream out) throws IOException{
      Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
      List<JstreeNode> list = gson.fromJson(reader, new TypeToken<List<JstreeNode>>(){}.getType());
      String result = list.stream().filter(e->e.parent.equals("#"))
      .map(root->{
         StringBuilder sb = new StringBuilder();
         sb.append("{");
sb.append("\"id\":\"" + root.id + "\",\"icon\":\"" + root.icon + "\",\"text\":\"" + root.text +"\"");
sb.append( childParse(list, list.stream()
.filter(e->e.parent.equals(root.id)).collect(Collectors.toList())) );
         sb.append("}");
         return sb.toString();
      }).collect(Collectors.joining(","));
      Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
      writer.write("[");
      writer.write(result);
      writer.write("]");
      writer.flush();
   }
   private String childParse(List<JstreeNode> orglist, List<JstreeNode> clist){
      if (clist.size()==0) return "";
      StringBuilder csb = new StringBuilder();
      csb.append(",\"children\":[");
      csb.append(
         clist.stream().map(e->{
            StringBuilder sb = new StringBuilder();
            sb.append("{");
sb.append("\"id\":\"" + e.id + "\",\"icon\":\"" + e.icon + "\",\"text\":\"" + e.text +"\"");
sb.append( childParse(orglist, orglist.stream()
.filter(t->t.parent.equals(e.id)).collect(Collectors.toList())) );
            sb.append("}");
            return sb.toString();
         }).collect(Collectors.joining(","))
      );
      csb.append("]");
      return csb.toString();
   }
}

これで、
「画面でツリー編集操作」→「サーバ送信」→「JSONをJava Object に変換」→
→「DBなどに管理」→「再表示要求」→「Java Object からJSON
→「jsTreeに送る」→「再描画」
のサイクルが作れます。わざわざ、JSONを別のデータ形式にするのは、
画面の描画以外の管理情報を付与して
jsTreeが抱える画面情報の表現と別の管理情報による管理が、現実は必要だからです。
つまり、jsTreeは、あくまでも描画における最低限の描画情報を持っているだけで、
「ツリー構造の情報管理」という、大きなものまでは、範疇にできないのです。