gson のシリアライズ用アノテーション

@Expose
GsonBuilder で、excludeFieldsWithoutExposeAnnotation() を指定すれば、
@Expose が付いたフィールドだけシリアライズする

Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation()
.create():

@SerializedName
フィールド名でなく任意の名称を JSONキーにする。
注意:@Expose を付けないで、excludeFieldsWithoutExposeAnnotation() で作成する Gson で
優先されることはない。

@JsonAdapter( Xyz.class )
JsonSerializer を実装したクラスを指定して
GsonBuilder registerTypeAdapter( class, typeAdapter )の代わりをする。

すると、yipuran-gsonhelper で作った
Home · yipuran/yipuran-gsonhelper Wiki · GitHub
interface として用意した
 org.yipuran.gsonhelper.LocalDateAdapter
 org.yipuran.gsonhelper.LocalDateTimeAdapter

は、無駄だったか?
これは、シリアライズ&デシリアライズ両方の目的に使用できるように作ったもののシリアライズが被る。
@JsonAdapter でこれは指定できない。
無駄ではない! JsonSerializer を extends した interface であるのだから。。。

import java.time.format.DateTimeFormatter;
import org.yipuran.gsonhelper.LocalDateAdapter;

public class MyLocalDateAdapter implements LocalDateAdapter{
   @Override
   public DateTimeFormatter getFormatter(){
      return DateTimeFormatter.ofPattern("yyyy/MM/dd");
   }
}

として、

   @JsonAdapter(MyLocalDateAdapter.class)
   public LocalDate date;

として使用ができる。
@Expose、@SerializedName、@JsonAdapter で、個々のオブジェクトの定義で制御するか、
GsonBuilder registerTypeAdapter( class, typeAdapter ) で一括制御するか、
Google Gson の柔軟さと言えるし、この柔軟さが使いやすくもあり、
センスなければ使いこなせないであろう。

@Expose に対して、除外を指定するアノテーションも考えて、yipuran-gsonhelper を用意している。
excludeanotate · yipuran/yipuran-gsonhelper Wiki · GitHub

LAST_INSERT_ID() の 0埋め

随分前に、MySQLUNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT のメモを書いていた。
https://oboe2uran.hatenablog.com/entry/2018/02/01/160016

CREATE TABLE branches (
  id           INT(4) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT
, branch_name  VARCHAR(60) NOT NULL
, PRIMARY KEY (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

この時、mybatis で、INSERT 実行時、<selectKey> で、LAST_INSERT_ID() をそのまま取ると
0埋めはされていない。

package org.labo.data;
import java.io.Serializable;
/**
 * Branch
 */
public class Branch implements Serializable{
	public String id;
	public String branch_name;
}
<insert id="insertBranch" parameterType="org.labo.data.Branch">
INSERT branches (branch_name) VALUES( #{branch_name} )
<selectKey keyProperty="id" resultType="string" order="AFTER"  >
  SELECT LAST_INSERT_ID()
</selectKey>
</insert>

これでは、0埋めはされない。ではどうするか、
(INSERT 後、通常のSELECTクエリでは、String で0埋めされてるが、LAST_INSERT_ID() では0埋めされない)
LPAD を使えばいい。
今回、INT(4) なので、LPAD で、4文字になるように指定する

<insert id="insertBranch" parameterType="org.labo.data.Branch">
INSERT branches (branch_name) VALUES( #{branch_name} )
<selectKey keyProperty="id" resultType="string" order="AFTER"  >
  SELECT LPAD(LAST_INSERT_ID() , 4, '0')
</selectKey>
</insert>


昔書いたもの(メモ)

DELIMITER //
DROP TABLE IF EXISTS barcode_seq
//
CREATE TABLE barcode_seq (
  id                 INT(4)  NOT NULL,
  barcode_no         INT(11) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
//
TRUNCATE TABLE barcode_seq
//
INSERT INTO barcode_seq (id, barcode_no) VALUES (1, 101)
//
commit
//
DROP FUNCTION IF EXISTS next_barcode
//
CREATE FUNCTION next_barcode(i INT) RETURNS char(8) CHARSET utf8
BEGIN
   UPDATE barcode_seq SET barcode_no=LAST_INSERT_ID(barcode_no+1)  WHERE id = i;
   RETURN LPAD(LAST_INSERT_ID(), 8, '0');
END
//
DELIMITER ;


DELIMITER //
DROP FUNCTION IF EXISTS next_barcode
//
CREATE FUNCTION next_barcode(i INT) RETURNS char(8) CHARSET utf8
BEGIN
	DECLARE res VARCHAR(8);
	DECLARE num INT;
	SELECT barcode_no + 1 INTO num FROM barcode_seq  WHERE id = i FOR UPDATE;
    UPDATE barcode_seq SET barcode_no = num WHERE id = i;
    SELECT LPAD(barcode_no, 8, '0') INTO res FROM barcode_seq  WHERE id = i;
	RETURN res;
END;
//
DELIMITER ;

無限Stream を limit でなく別の方法で STOP させる。

limit を使わずに Stream 止め方には、トリッキーではあるが、
filter(Predicate) 、findAny() で止めるか、anyMatch(Predicate) で、止める方法がある。

Stream.iterate(1, i->i+1)
.peek(System.out::println)
.filter(n->n > 9)
.findAny();

これの findAny() は、Optional<Integer> を返却として受け取れるので、

int ii = Stream.iterate(1, i->i+1).peek(System.out::println)
.filter(n->n > 9).findAny().get();

とも書ける。

anyMatch 1つで書けばもっと簡潔だ。

Stream.iterate(1, i->i+1)
.peek(System.out::println)
.anyMatch(n->n > 9);

普段、目的の処理を forEach(Consumer<T>) で書いていたと思うが、
peek(Consumer<T>) で目的の処理書いていくスタイルになる。

Gson での Big JSON の出力

Google Gson の場合、大きなサイズの JSON を出力、gson の toJson では一度作成元 Object を持ってしまうので、
大きなサイズの JSON は toJsonでは不向きである。
大きなサイズのJSON を出力をする場合、JsonWriter を使う。

例)

OutputStream out = // TODO 

try(JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))){

   // 開始
   writer.beginObject();

   writer.name("title").value("タイトル");

   writer.name("data");
   // 配列開始
   writer.beginArray();

   // TODO Loop により、大量の配列を出力 
   for(int n=0;n < 1000;n++){
      writer.beginObject();
      
      writer.name("id").value("11");
      writer.name("name").value("あいう");
      writer.name("flag").value(true);
      
      writer.endObject();
   }
   // 配列終了
   writer.endArray();

   // 終端
   writer.endObject();

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

これは、ラップする程のことでもない。素直にループで充分

ループの中を長くしたくないのであれば、ループ内で toJsonを書けば良い。

Gson gson = new GsonBuilder().serializeNulls().create();
OutputStream out = // TODO 

try(JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))){

   // 開始
   writer.beginObject();

   writer.name("title").value("タイトル");

   writer.name("data");
   // 配列開始
   writer.beginArray();

   // TODO Loop により、大量の配列を出力 
   for(int n=0;n < 1000;n++){
          
     gson.toJson(e, TypeToken.get(Item.class).getType(), writer);
      
    }
   // 配列終了
   writer.endArray();

   // 終端
   writer.endObject();

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

double の値が、整数か小数点有りか調べる

Java で案外、これを問われると簡単なようですぐに出てこない。
結果、boolean 値とする。
文字列にして小数点(.)文字を検証する方法

double d = 13.02;

boolean flg = Long.parseLong(String.valueOf(d).substring(String.valueOf(d).indexOf('.')+1))==0;

なんとも、短絡的というか動物的なコード。

long に cast できるかで検証

double d = 13.02;

boolean flg = d==(long)d;

Object に、Long / Integer / Double いずれかで格納する。

double d = 21.0;
Object obj

if (d==(long)d) {
   if (Integer.MIN_VALUE <= (long)d && (long)d <= Integer.MAX_VALUE) {
      obj = (int)d;
   }else {
      obj = (long)d;
   }
}else{
   obj = d;
}

なぜ、こんなことに気を配るかというと、
Google gson の JsonReader で読込む JsonToken での認識は、Long / Double という区別ではなく
NUMBER なので、小数点ある数値なのか整数なのかをチェックしたい時があるからだ。

Big JSON を読み込みを汎用化

先日の「Big JSON を読み込む JsonReader の実践」より汎用化を考えました。。
以下、抜粋のコードです。

public void execute(JsonReader reader, Consumer<T> consumer){
   boolean request = false;
   try{
      while((reader.hasNext() || reader.peek().equals(JsonToken.END_ARRAY)
         || reader.peek().equals(JsonToken.END_OBJECT)) && !reader.peek().equals(JsonToken.END_DOCUMENT)
      ){
            JsonToken token = reader.peek();
            if (token.equals(JsonToken.BEGIN_ARRAY)){
               reader.beginArray();
               if (targetptn.matcher(reader.getPath()).matches()){
                  request = true;
               }
            }else if(token.equals(JsonToken.END_ARRAY)){
               reader.endArray();
               if (request) request = true;
            }else if(token.equals(JsonToken.BEGIN_OBJECT)){
               if (request) {
                  consumer.accept(gson.fromJson(reader, type));
               }else{
                  reader.beginObject();
               }
            }else if(token.equals(JsonToken.END_OBJECT)){
               reader.endObject();
            }else if(token.equals(JsonToken.NAME)){
               reader.nextName();
            }else if(token.equals(JsonToken.STRING)){
               reader.nextString();
            }else if(token.equals(JsonToken.NUMBER)){
               reader.nextLong();
            }else if(token.equals(JsonToken.BOOLEAN)){
               reader.nextBoolean();
            }else if(token.equals(JsonToken.NULL)){
               reader.nextNull();
            }
      }
   }catch(Exception ex){
      throw new RuntimeException(ex.getMessage(), ex);
   }
}

配列の開始、JsonToken.BEGIN_ARRAY で、targetptn は、
JSONパス  $.key1.key2[0] 、$.key1.key2[1]、、、と
一致をチェックする正規表現 Pattern です。
consumer.accept(gson.fromJson(reader, type)); で、
予め用意する Google gson で配列要素を解析して Consumer で処理します。

この type=java.lang.reflect.Type をどうやって上を呼ぶ前のメソッドで指定させようかと悩みました。
com.google.gson.reflect.TypeToken を使って、

new TypeToken<T>(){}.getType()

と思ったのですが、それは T がどのクラスか実行時に認識できる時のみです。
わけのわからんままこのように getTypeで求めて
gson の fromJson に渡してしまうと ClassCastException になります。

java.lang.reflect.Type を求める方法は、いろいろあって、、Fooというクラスに対して、、、
com.google.common.reflect.TypeToken を使用

TypeToken.of(Foo.class).getType();

または、

new TypeToken<Foo>(){}.getType();

com.google.inject.TypeLiteral を使用

TypeLiteral.get(Foo.class).getType();

com.google.gson.reflect.TypeToken を使用

TypeToken.get(Foo.class).getType();

com.google.gson.reflect.TypeToken を選択して、大量のJSON 配列を処理するものを
以下、yipuran-gsonhelper にまとめました。

Stream<T> を取得するのも用意しました。

Wiki
jsonarrayparse · yipuran/yipuran-gsonhelper Wiki · GitHub

Source
https://github.com/yipuran/yipuran-gsonhelper/blob/master/src/main/java/org/yipuran/gsonhelper/array/JsonArrayParser.java

https://github.com/yipuran/yipuran-gsonhelper/blob/master/src/main/java/org/yipuran/gsonhelper/array/JsonArrayParseBuilder.java

JsonArrayParseBuilder を見てのとおり生成時に
Json-PATH 指定の文字列中に正規表現対象の文字列はエスケープします。