フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(3)

フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(2) - Oboe吹きプログラマの黙示録
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
これは、1階層のグルーピングしかサポートしない。
もっと階層が深い場合は、同じ処理ロジックのシリアライザではだめだ。
そこで、もっと階層の指定が深くできるように、シリアライズ後に想定するJSON
JSON-PATH を指定してあらゆる階層でもフラットからツリーに変換できるように考えた。
フィールドに付与するアノテーション

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * JSON パスをフィールドアノテーションで書く
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface JsonPath {
   String value();
}

このアノテーションが付いた場合、階層化する JsonSerializer

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
/**
 * AutoPathSerializer
 */
public class AutoPathSerializer<T> implements JsonSerializer<T>{

   @Override
   public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context){
      Map<String, JsonObject> jmap = new HashMap<>();
      JsonObject jo = new JsonObject();
      jmap.put("$", jo);
      Field[] fields = src.getClass().getDeclaredFields();
      for(Field f:fields) {
         try{
            f.setAccessible(true);
            Object obj = f.get(src);

            JsonPath jpath = f.getAnnotation(JsonPath.class);
            if (jpath != null) {
               List<String> plist = tokenToList(jpath.value(), '.', '\\');
               String path = plist.remove(0);
               for(String t:plist){
                  JsonObject parent = jmap.get(path);
                  path = path + "." + t;
                  JsonObject jt = jmap.get(path);
                  if (jt==null) {
                     jmap.put(path, new JsonObject());
                  }
                  jt = jmap.get(path);
                  parent.add(t, jt);
               }
               JsonObject addjo = jmap.get(path);
               if (obj instanceof String) {
                  addjo.addProperty(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                        , Optional.ofNullable(obj).map(e->e.toString()).orElse(null));
               }else {
                  addjo.add(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                        , context.serialize(obj, f.getType()));
               }
               jmap.put(path, addjo);
            }else{
               if (obj instanceof String) {
                  jo.addProperty(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                        , Optional.ofNullable(obj).map(e->e.toString()).orElse(null));
               }else {
                  jo.add(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                        , context.serialize(obj, f.getType()));
               }
            }
         }catch(IllegalAccessException e){
            throw new RuntimeException(e);
         }
      }
      return jo;
   }
   private List<String> tokenToList(String str, char sep, char escape){
      List<String> list = new ArrayList<>();
      String sp = new String(new char[]{ sep });
      String escapes = new String(new char[]{ escape, escape });
      StringTokenizer st = new StringTokenizer(str, sp, true);
      String s = "";
      while (st.hasMoreTokens()){
         String c = st.nextToken();
         if (c.equals(sp)) {
            if (s.charAt(s.length()-1)==escape) {
               s += c;
            }else{
               list.add(s.replaceAll(escapes, ""));
               s = "";
            }
         }else {
            s += c;
         }
      }
      list.add(s.replaceAll(escapes, ""));
      return list;
   }
}

使用例
アノテーションを付けた対象クラス

public class Datasample{

	public String a1;

	@JsonPath("$.aaa")
	public double a2;

	@JsonPath("$.aaa")
	public boolean a3;

	public String b1;

	@JsonPath("$.bbb")
	public int b2;

	@JsonPath("$.bbb.BBB")
	public boolean b3;

	@JsonPath("$.bbb.BBB")
	public LocalDate b4;

	@JsonPath("$.bbb.BBB")
	public String b5;

	public int c1;

	@JsonPath("$.ccc.CCC")
	public String c2;

	@JsonPath("$.ccc.CCC")
	public String c3";

	@JsonPath("$.ccc.CC10")
	public String c4";

	@JsonPath("$.ccc.CC10")
	public String c5;

	@JsonPath("$.ccc.CC20")
	public String c6;

	@JsonPath("$.ccc.CC20.cc33")
	public String c7;
}

AutoPathSerializer 指定

Gson gson = new GsonBuilder()
.serializeNulls()
.registerTypeAdapter(LocalDate.class, LocalDateAdapter.of("yyyy/MM/dd"))
.registerTypeAdapter(Datasample.class, new AutoPathSerializer<Datasample>())
.setPrettyPrinting()
.create();

シリアライズ結果例 JSON

{
  "a1": "A1",
  "aaa": {
    "a2": 13.09,
    "a3": true
  },
  "b1": "B1",
  "bbb": {
    "b2": 21,
    "BBB": {
      "b3": true,
      "b4": "2020/07/02",
      "b5": "B5"
    }
  },
  "c1": 323,
  "ccc": {
    "CCC": {
      "c2": "C2",
      "c3": "C3"
    },
    "CC10": {
      "c4": "C4",
      "c5": "C5"
    },
    "CC20": {
      "c6": "C6",
      "cc33": {
        "c7": "C7"
      }
    }
  }
}

フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(2)

フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(1)の続き。

属性フィールドに、グルーピング名をアノテーションで付与することで、この処理を汎用的に行う
リアライザを考えた。

public class Sample{
   public String title;
   @Grouping("Unit")
   public String name;
   @Grouping("Unit")
   public int value;
}

これで、 name と value を、以下のJSONのように、グループする。

{
    "title": "あいう",
    "Unit": {
      "name": "A",
      "value": 121
    }
}

アノテーション

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * Grouping
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Grouping {
     String value();
     boolean nullSafe() default true;
}

Grouping アノテーションが付いたフィールドを処理するシリアライザ

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
/**
 * GroupingSerializer
 */
public class GroupingSerializer<T> implements JsonSerializer<T>{

   @Override
   public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context){
      JsonObject jo = new JsonObject();
      Field[] fields = src.getClass().getDeclaredFields();
      Map<String, JsonObject> jmap = new HashMap<>();
      for(Field f:fields) {
         try{
            f.setAccessible(true);
            Object obj = f.get(src);
            JsonObject addjo;
            Grouping g = f.getAnnotation(Grouping.class);
            if (g != null) {
               String jkey = g.value();
               addjo = jmap.containsKey(jkey) ? jmap.get(jkey) : new JsonObject();
               jmap.put(jkey, addjo);
            }else{
               addjo = jo;
            }
            if (obj instanceof String) {
               addjo.addProperty(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                     , Optional.ofNullable(obj).map(e->e.toString()).orElse(null));
            }else {
               addjo.add(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName())
                     , context.serialize(obj, f.getType()));
            }
         }catch(IllegalAccessException e){
            throw new RuntimeException(e);
         }
      }
      jmap.entrySet().stream().forEach(e->jo.add(e.getKey(), e.getValue()));
      return jo;
   }

}

@SerializedName も働くようにしてある。

フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(1)

フラットに属性が並んだオブジェクト → 階層構造オブジェクト → JSON
という流れで処理するのは、階層構造オブジェクト の定義クラスを設計、配置するこのやり方は
非常にナンセンスと思う。
階層構造オブジェクト の定義クラスの存在無しで処理したい。

Google gson で解決してみる。

(例)Sample.class

public class Sample{
   public String title;
   public String name;
   public int value;
}

このうち、String nameint value"Unit" というキー名で括る。という要求
期待する JSON サンプルが、

{
  "title": "あいう",
  "Unit": {
    "name": "A",
    "value": 121
  }
}

JsonWriter だけで対応する場合

Sample s = new Sample();
// TODO  s に値をセットする。
try(JsonWriter writer = new JsonWriter(new PrintWriter(System.out))){
   writer.setIndent("  ");
   writer.beginObject();
   writer.name("title").value(s.title);

   JsonWriter writer2 = writer.name("Unit");
   writer2.beginObject();
   writer2.name("name").value(s.name);
   writer2.name("value").value(s.value);
   writer2.endObject();

   writer.endObject();
}catch(IOException e){
   e.printStackTrace();
}

子の階層 JsonWriter を生成して、 beginObject() と endObject() を忘れずに呼ばなければならず、
ちょっとコードが汚くなりやすい。

JsonSerializer を用意して、gson で処理

import java.lang.reflect.Type;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class SampleSerializer implements JsonSerializer<Sample>{

   @Override
   public JsonElement serialize(Sample src, Type typeOfSrc, JsonSerializationContext context){
      JsonObject jo = new JsonObject();
      jo.addProperty("title", src.title);
      JsonObject unit = new JsonObject();
      unit.addProperty("name", src.name);
      unit.addProperty("value", src.value);
      jo.add("Unit", unit);
      return jo;
   }

}
Gson gson = new GsonBuilder()
		.serializeNulls()
		.registerTypeAdapter(Sample.class, new SampleSerializer())
		.setPrettyPrinting()
		.create();
Sample s = new Sample();
// TODO  s に値をセットする。
String result = gson.toJson(s);

期待する JSON が以下のように、この Sample が、"sample" として入っている場合で、
JsonWriter の使用を組み合わせると、、、

{
  "name": "Uranus",
  "admin": false,
  "sample": {
    "title": "あいう",
    "Unit": {
      "name": "A",
      "value": 121
    }
  }
}
Gson gson = new GsonBuilder()
.serializeNulls()
.registerTypeAdapter(Sample.class, new SampleSerializer())
.create();

Sample sample = new Sample();
// TODO sample に値をセットする。

try(JsonWriter writer = new JsonWriter(new PrintWriter(System.out))){
   writer.setIndent("  ");
   writer.beginObject();

   writer.name("name").value("Uranus");
   writer.name("admin").value(false);

   gson.toJson(sample, TypeToken.get(Sample.class).getType(), writer.name("sample"));

   writer.endObject();
}catch(IOException e){
   e.printStackTrace();
}

Sample をセットする以下の部分は、

  gson.toJson(sample, TypeToken.get(Sample.class).getType(), writer.name("sample"));

JsonWriter#name(String) で作成した JsonWriter を toJsonに使用していることである、
つまり、

JsonWriter writer2 = writer.name("sample");
gson.toJson(sample, TypeToken.get(Sample.class).getType(), writer2);

を1つにまとめている。

フラットに属性が並んだオブジェクトから、階層のあるJSON への変換の
結論は、JsonSerializer を用意した方が良さそうである。

escape がある区切り文字による split

String の split(String regex) は、頻繁に良く使われるメジャーなメソッドである。
でも、区切り文字(デリミタ)に対してエスケープがあり、エスケープ文字を考慮して split でリストを求めたい。
サンプル文字列
 「$.aaa.\.bb\.bbb.cc\.\.c.\.\ddd\..ee\\ee.f\f」
デリミタ '.' に対してエスケープする文字は、`\` のケース
splitして 改行して並べた結果、以下を期待する。

$
aaa
.bb.bbb
cc..c
.ddd.
ee\ee
ff

StringTokenizer で実行する方法
ただし、デリミタ、エスケープ文字はメソッド引数で指定できない

public static List<String> splittolist(String str){
   List<String> list = new ArrayList<>();
   StringTokenizer st = new StringTokenizer(str, ".", true);
   String s = "";
   while (st.hasMoreTokens()) {
      String c = st.nextToken();
      if (c.equals(".")) {
         if (s.endsWith("\\")){
            s += c;
         }else {
            list.add(s.replaceAll("\\\\", ""));
            s = "";
         }
      }else {
         s += c;
      }
   }
   list.add(s.replaceAll("\\\\", ""));
   return list;
}

StringTokenizer で実行する方法
デリミタ、エスケープ文字を指定できるようにする。

public static List<String> tokenToList(String str, char sep, char escape){
   List<String> list = new ArrayList<>();
   String sp = new String(new char[]{ sep });
   String escapes = new String(new char[]{ escape, escape });
   StringTokenizer st = new StringTokenizer(str, sp, true);
   String s = "";
   while (st.hasMoreTokens()){
      String c = st.nextToken();
      if (c.equals(sp)) {
         if (s.charAt(s.length()-1)==escape) {
            s += c;
         }else{
            list.add(s.replaceAll(escapes, ""));
            s = "";
         }
      }else {
         s += c;
      }
   }
   list.add(s.replaceAll(escapes, ""));
   return list;
}

char 型で処理する方法

public static List<String> tokenToList(String str, char sep, char escape){
   List<String> list = new ArrayList<>();
   StringBuilder sb = new StringBuilder();
   boolean isEscape = false;
   for(char c:str.toCharArray()) {
      if (isEscape) {
         isEscape = false;
      }else if(c==escape){
         isEscape = true;
         continue;
      }else if(c==sep){
         list.add(sb.toString());
         sb.setLength(0);
         continue;
      }
      sb.append(c);
   }
   list.add(sb.toString());
   return list;
}
String str = "$.aaa.\\.bb\\.bbb.cc\\.\\.c.\\.\\ddd\\..ee\\\\ee.f\\f";
List<String> list = tokenToList(str, '.', '\\');
list.stream().forEach(e->{
   System.out.println("["+e+"]");
});

上の結果

[$]
[aaa]
[.bb.bbb]
[cc..c]
[.ddd.]
[ee\ee]
[ff]

Gson の toJson と JsonWriter を併用する

Google gsontoJson JsonWriter は、どちらか一方を使うことが多かった。
toJson(Object src, Type typeOfSrc) だけでなく、
toJson には、JsonWriter に結果を書き込むメソッドも用意されている。

JsonWriter で書きながら、途中で任意のクラスオブジェクトを突然、JSON 書込みに
割り込んで toJson で生成したJSONを割り込ませる。
ということが可能だ。

toJson(Object src, Type typeOfSrc, JsonWriter writer) 

以下のように使う。
サンプル

import java.io.IOException;
import java.io.PrintWriter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonWriter;
Gson gson = new GsonBuilder()
      .serializeNulls()
      .create();

try(JsonWriter writer = new JsonWriter(new PrintWriter(System.out))){
   writer.setIndent("  ");
   writer.beginObject();

   writer.name("name").value("Uranus");
   writer.name("admin").value(false);

   // name で作成した JsonWriter を toJson に渡す
   JsonWriter writer2 = writer.name("sample");

   gson.toJson(new Sample(), TypeToken.get(Sample.class).getType(), writer2);

   writer.endObject();
}catch(IOException e){
   e.printStackTrace();
}

JsonWriter の nameメソッド で作成した JsonWriter を toJson に渡す
結果、標準出力

{
  "name": "Uranus",
  "admin": false,
  "sample": {
    "title": "あいう",
    "name": "A",
    "value": 121
  }
}

JsonWriterで注意しなければならないのは、最後に、endObject() を実行するのを忘れてしまうと、
java.io.IOException: Incomplete document
at com.google.gson.stream.JsonWriter.close(JsonWriter.java:559)

と例外を発生させてしまうので、忘れてはいけない。

MySQL 複数のTIMESTAMP DEFAULT CURRENT_TIMESTAMP

昔、MySQL で、複数のTIMESTAMP で
DEFAULT CURRENT_TIMESTAMP を宣言しようとして、

DELIMITER //
DROP TABLE IF EXISTS branch;
//
CREATE TABLE branch (
  id            INT NOT NULL AUTO_INCREMENT
, branch_name   VARCHAR(64) NOT NULL 
, created_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
, update_at     TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
, PRIMARY KEY (id) 
) DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
//
DELIMITER ;


Error Code: 1293. Incorrect table definition; there can be only one TIMESTAMP column
 with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause

で、仕方なく、
https://oboe2uran.hatenablog.com/entry/2013/07/07/103023

https://oboe2uran.hatenablog.com/entry/2013/08/27/125127

と、していた。
でも、MySQL 5.7 以上なら、もうこのようなことはなく、
期待どおり、INSERT 時、UPDATE時、TIMESTAMP が自動で格納、更新されるようになる。

以下が、使える

TRUNCATE TABLE branch;
INSERT INTO branch (branch_name) VALUES ('a'),('b'),('c');
commit;

SELECT * FROM branch;
UPDATE branch SET branch_name = 'B' WHERE id = 2;
commit;

SELECT * FROM branch;

MySQL AUTO_INCREMENTエラーの時のエラーコード

MySQL AUTO_INCREMENT がMAXを超える時、どんなエラーコードだっけ?
と忘れていたので、簡単に用意して実行した結果は、

INSERT の発生させる AUTO_INCREMENT のエラー

Error Code: 1062. Duplicate entry '2147483647' for key 'PRIMARY'

INTのMAX 2147483647 としてキーが重複というエラー、たしかにそうである。

Error Code: 1264. Out of range value for column 

というエラーではない。

テーブルの各情報、

SELECT * FROM information_schema.tables 
WHERE TABLE_SCHEMA = 'testDB' AND TABLE_NAME = 'sample';

AUTO_INCREMENT 確認

SELECT AUTO_INCREMENT  FROM information_schema.tables 
WHERE TABLE_SCHEMA = 'testDB' AND TABLE_NAME = 'sample';

AUTO_INCREMENT 書換え

ALTER TABLE testDB.sample AUTO_INCREMENT=2147483647;