Google gson fromJson で、 Map<String, Object> への変換は落とし穴。。。

Google gson fromJson を使用する場合、通常変換先は自分で用意するクラスがほとんどのケースであり、
であり、Object に変換させることはあまりやらない。

Gson gson = new GsonBuilder().serializeNulls().create();

Map<String, Object> map = gson.fromJson("{a:0}", new TypeToken<Map<String, Object>>(){}.getType());

System.out.println(map);

この結果は、

{a=0.0}

に、なってしまう。 0 では、なく 0.0 になってしまう。

Map<String, String> map = gson.fromJson("{a:0}", new TypeToken<Map<String, String>>(){}.getType());

であれば、、、

{a=0}

で思いどおりではあるが、Map の value を Objectにしてしまうと、こうならならない。

それでは、Gson のデシリアライザを用意するということで、以下のデシリアライザを用意する。

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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;
/**
 * GenericMapDeserializer<T>  :  Map<String, T> へのデシリアライザ
 */
public final class GenericMapDeserializer<T> implements JsonDeserializer<Map<String, T>>{
   @Override
   public Map<String, T> deserialize(JsonElement jsonElement, Type typeOfT
, JsonDeserializationContext context) throws JsonParseException{

      if (!jsonElement.isJsonObject()){
         return null;
      }
      JsonObject jsonObject = jsonElement.getAsJsonObject();
      Set<Entry<String, JsonElement>> jsonEntrySet = jsonObject.entrySet();
      Map<String, T> deserializedMap = new HashMap<String, T>();

      for(Entry<String, JsonElement> entry : jsonEntrySet){
         try{
            if(entry.getValue().isJsonNull()){
               deserializedMap.put(entry.getKey(), null);
            }else if(entry.getValue().isJsonArray()){
               deserializedMap.put(entry.getKey(), (T)entry.getValue());
            }else if(entry.getValue().isJsonObject()){
               deserializedMap.put(entry.getKey(), (T)entry.getValue());
            }else if(entry.getValue().isJsonPrimitive()){
               deserializedMap.put(entry.getKey(), context.deserialize(entry.getValue(), String.class));
            }
         }catch(Exception e){
            throw new RuntimeException(e.getMessage(), e);
         }
      }
      return deserializedMap;
   }
}

これを GsonBuilder で Gson を作るとき、registerTypeAdapter で指定する。

Gson gson = new GsonBuilder()
.registerTypeAdapter(new TypeToken<Map<String, Object>>(){}.getType(),new GenericMapDeserializer<Object>())
.serializeNulls()
.create();

こうして、、

Map<String, Object> map = gson.fromJson("{a:0}", new TypeToken<Map<String, Object>>(){}.getType());

System.out.println(map);

とすれば、、、

{a=0}

が結果になる。

XStream null value を出力させるケース、修正

XStream null value を出力させるケース、先日の 
XStream を使うかどうかは、NULL value をどう扱うかが問題 - Oboe吹きプログラマの黙示録
の方法は、やはり良くない!!

書込み専用になってしまうからである。やはり、ReflectionConverterを継承して doMarshal を null 対応したものを
XStream の registerConverter メソッドで登録するしかない。

そこで、以下の CustomReflectionConverter を用意する。

import java.util.function.BiConsumer;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
/**
 * CustomReflectionConverter
 */
public class CustomReflectionConverter extends ReflectionConverter{
   private BiConsumer<Object, HierarchicalStreamWriter> consumer;
   public CustomReflectionConverter(Mapper mapper, ReflectionProvider reflectionProvider
                                   , BiConsumer<Object, HierarchicalStreamWriter> onMarchall){
      super(mapper, reflectionProvider);
      consumer = onMarchall;
   }
   protected void doMarshal(final Object source, final HierarchicalStreamWriter writer
                            , final MarshallingContext context) {
      super.doMarshal(source, writer, context);
      consumer.accept(source, writer);
   }
}

XStream を使うかどうかは、NULL value をどう扱うかが問題 - Oboe吹きプログラマの黙示録 で書いたように、

XML出力時専用のフィールドを文字列で用意なんてしない!!

public class Root{
   @XStreamAlias("date")
   @XStreamConverter(value=CustomLocalDateConverter.class, strings={"yyyy/MM/dd"})
   public LocalDate birthday;
     :
     :

このCustomReflectionConverter を registerConverterで指定する。

XStream stream = new XStream(new DomDriver("UTF-8"));
CustomReflectionConverter reflectionConverter
= new CustomReflectionConverter(stream.getMapper(), new SunUnsafeReflectionProvider(), (o, w)->{
   if (o instanceof Root){
      if (((Root)o).date==null){
         w.startNode("date");
         w.setValue("");
         w.endNode();
      }
   }
});
stream.registerConverter(reflectionConverter, XStream.PRIORITY_VERY_LOW);
stream.processAnnotations(Root.class);

try(OutputStream out = new FileOutputStream(new File("out.xml"))){
   out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".getBytes());
   stream.toXML(root, out);
}catch(Exception e){
   e.printStackTrace();
}finally{
   System.out.println("finish!");
}


Root の date が null の場合、Rootタグの最後になってしまうが、、

<?xml version="1.0" encoding="UTF-8"?>
<root>
      :
  <date></date>
</root>

が作れる。null でない場合は、@XStreamConverterアノテーションで書いたコンバータが使用される。

XStream を使うかどうかは、NULL value をどう扱うかが問題

XStream は、アノテーションによるXMLに対するJava Object のマッピング、コンバータの指定ができていいのだけど、
stackoverflow.com

ここで書かれたように NULL を 中身空のタグでXMLを書く場合に指定方法が文字列、String型でしか逃げ道がない。
stackoverflow に投稿されたように、ReflectionConverter を書き直して 配布されてる XStream ライブラリに手を加える
なんてやってられない。

日付や時刻のとき、困るだろう。

これとは論点が違う話で。。。

皆(特に日本だけ?)、なぜか、年月日の表現を、yyyy-MM-dd のハイフン区切りの書式を嫌がり、yyyy/MM/dd になってる
ことが多い。

XStream でこの書式を解決するのは、com.thoughtworks.xstream.converters.Converter 実装を用意するか
com.thoughtworks.xstream.converters.time.LocalDateConverter を継承して書式を任意に指定させるかである。


Converter 実装の例

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class CustomLocalDateConverter implements Converter{
   private String format;
   public CustomLocalDateConverter(String format){
      this.format = format;
   }
   @Override
   public boolean canConvert(Class type){
      return type.equals(LocalDate.class);
   }
   @Override
   public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context){
      LocalDate date = (LocalDate)value;
      writer.setValue(date.format(DateTimeFormatter.ofPattern(format)));
   }
   @Override
   public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context){
      try{
         return LocalDate.parse(reader.getValue(), DateTimeFormatter.ofPattern(format));
      }catch(Exception e){
         return null;
      }
   }
}


LocalDateConverter を継承の例

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.time.LocalDateConverter;

public class CustomLocalDateConverter extends LocalDateConverter{
   private String format;
   public CustomLocalDateConverter(String format){
      this.format = format;
   }
   @Override
   public Object fromString(final String str){
      try{
         return LocalDate.parse(str, DateTimeFormatter.ofPattern(format));
      }catch(Exception e){
         final ConversionException exception = new ConversionException("Cannot parse value as local date", e);
         exception.add("value", str);
         throw exception;
      }
   }
   @Override
   public String toString(final Object obj){
      if (obj==null){
         return "";
      }
      return DateTimeFormatter.ofPattern(format).format((LocalDate)obj);
   }
}

任意のフィールドを日付のタグ、 <date> に割り当てる場合

   @XStreamAlias("date")
   @XStreamConverter(value=CustomLocalDateConverter.class, strings={"yyyy/MM/dd"})
   public LocalDate birthday;


ここで、どうしても、 この LocalDate bithday が、null の時、このままでは、<date></date> と書かれるXML
生成できないので、、、

強引なトリックを使うなら、この bithday 宣言の前に、NULL の時に XML出力時専用のフィールドを文字列で用意することだ。

   @XStreamAlias("date")
   @XStreamConverter(value=CustomLocalDateConverter.class, strings={"yyyy/MM/dd"})
   public LocalDate birthday;

   @XStreamAlias("date")
   public String writeOnlyDate = "";


@XStreamAlias を2回やってしまうのである。これがなぜかエラーにならない。
上の例では、birthday が、NULL の時、writeOnlyDate = "" が有効になって、 <date></date> が出力される。



そもそも、XML離れが進んでる傾向が強いので、久々にこれらXML系を調べたけど、
相変わらず、これ1本で。。。というフレームワーク、ツール、ライブラリが見当たらない。。。

>>>つづき、、、
oboe2uran.hatenablog.com

XStream 読込時にタグを無視させる方法

XStream で XML を読み込むとき、タグに対する Java オブジェクトフィールドが存在しないと、
わりと丁寧に詳細なエラーを出してくれる。

com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter$UnknownFieldException: No such field sample.Root.date
---- Debugging information ----
message             : No such field sample.Root.date
field               : date
class               : sample.Root
required-type       : sample.Root
converter-type      : com.thoughtworks.xstream.converters.reflection.ReflectionConverter
path                : /root/date
line number         : 4
version             : not available
-------------------------------

XMLタグのパスと存在しなければならないJava Object のクラスに、どういう名称フィールドで存在すべきかまでのエラーまで
出してくれる。おせっかいに受け取る人もいるだろう。

このエラーを無視して読取り解析してほしい場合、全て無視するなら、読込開始実行前に、XStream インスタンスで、

 ignoreUnknownElements() を実行、


特定のタグ名やパターンで無視を指定するなら、

 ignoreUnknownElements(String)

あるいは、

 ignoreUnknownElements(Pattern)


を実行する。

XStream 使ったら、Security framework of XStream not initialized, XStream is probably vulnerable.

XStream を使ってみたところ、
XML読込みをしたら、読込みは成功するものの、標準エラー出力、System.err で

   Security framework of XStream not initialized, XStream is probably vulnerable.

が出力された。

使ったバージョンは、1.4.10

public static void setupDefaultSecurity(final XStream xstream) というメソッドが、XStream にあって
ここに説明が書いてあるのだが、2017-06-02 時点、まだ、1.5.x は出てない。

This method is a pure helper method for XStream 1.4.x. It initializes an XStream instance with a white list of
well-known and simply types of the Java runtime as it is done in XStream 1.5.x by default. This method will do
therefore nothing in XStream 1.5.

それで、このメソッドを呼べばいいのかと試したら、、com.thoughtworks.xstream.security.ForbiddenClassException に
なってしまう。

仕方ないので、インスタンス生成時に実行されている protected void setupSecurity() をオーバーライドして
セキュリティパーミッションを検査する起因のフラグがONにならないようにしてやる。

XStream stream = new XStream(){
   @Override
   protected void setupSecurity() {
      addPermission(AnyTypePermission.ANY);
   }
};

こうすると警告は出ない。

XStream - About XStream

Link.onClick static が消滅して欲しくない。それならば、、

Wicket8 でラムダによる AjaxButton.onSubmit static メソッドの登場と思いきや、8.0.0-M4 から M5 以降で消滅して
AjaxFormSubmitBehavior を使うのを納得したが、Link の onClick static メソッドでのラムダも消滅してるのは、
非常に残念だ。
Ajax ではなく、普通にリンク onclick に対するラムダ式記述効果の恩恵が欲しかったのに。。
  
http://oboe2uran.hatenablog.com/entry/2017/05/24/001329
http://oboe2uran.hatenablog.com/entry/2017/05/24/001329

HTML aタグに、Button コンポーネント充てて書きたくはないのである。

それなら、8.0.0-M4 で書かれていた方法で、自分用に org.apache.wicket.markup.html.link.Link を継承して
用意してしまえばいい。

import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.lang.Args;
import org.danekja.java.util.function.serializable.SerializableConsumer;
/**
 * Link を継承、static onClick を追加
 */
public abstract class XLink<T> extends Link<T>{

   public XLink(String id){
      super(id);
   }

   public XLink(String id, IModel<T> model){
      super(id, model);
   }

   public static <T> XLink<T> onClick(String id, SerializableConsumer<XLink<T>> onClick){
      Args.notNull(onClick, "onClick");
      return new XLink<T>(id){
         private static final long serialVersionUID = 1L;
         @Override
         public void onClick(){
            onClick.accept(this);
         }
      };
   }
}

これを使えば、以下のようなものが書けるし、ラムダ式でいろいろクリック時の処理が、シリアライズの制約があるものの
書ける。

queue(XLink.onClick("linkid", t->setResponsePage(SamplePage.class, new PageParameters().add("code", "ABC") )));


以下を考えた人が凄いんだと思う。

GitHub - danekja/jdk-serializable-functional: Library with serializable versions of java.util.function.* interfaces.

AjaxFormSubmitBehavior があるから心配ない。

先月、Wicket-8.0.0-M4 から M5 まで更新された時、 AjaxButton.onSubmit staticメソッドの BiConsumer 使用が
削除されていて、ショックだったけど、

Wicket 8.0.0-M4 から M5 で、AjaxButton.onSubmit が。。。 - Oboe吹きプログラマの黙示録


よーく調べると、org.apache.wicket.ajax.form.AjaxFormSubmitBehavior があるから、ビヘビアとして
onSubmit の振る舞いをラムダを書きたければ、
  
public static AjaxFormSubmitBehavior onSubmit(String eventName,
SerializableConsumer onSubmit)

を使えということになったみたいだ。
M4 の static AjaxButton#onSubmit は、SerializableBiConsumer で、ボタンコンポーネントも引数だったが、
ここで、自身のボタンコンポーネントを引数にもらっても、用はない。
AjaxRequestTarget だけで充分だ。


<button wicket:id="send" type="button">送信</button> のようなものがあった時、

queue(new Button("send").add(AjaxFormSubmitBehavior.onSubmit("click", t->{
   response.setDefaultModelObject(info.getValue());
   t.add(response);
})));


みたいに書くことになる。ただ、onError に対するラムダは無い。役にたたないから消えたのかも。。。
ビヘビアとして add するのが長いコードになる気もしたが、Wicket は、昔からビヘビアを書くことの重要さを
考えると納得もしてくる。
このビヘビアの書き方の方が、あらゆる事象で実行させたいものを埋め込むということができるような気がする。

すると、ルーツである Behavior には、

public static Behavior onAttribute(String name,
     SerializableFunction<String, CharSequence> onAttribute)

と、

public static Behavior onTag(SerializableBiConsumer<Component, ComponentTag> onTagConsumer)

があるではないですか。

onComponentTag オーバーライドや、AttrobuteModifier やらを書かなくてもこれらラムダで書けてしまうって
ことだとろうか。。。

時間がある時に、書いてみようと思う。

もう、M5から、Wicket-8.0.0-M6 になってる。

事の発端は、以下のこんな議論・検討があったからだけど、ラムダのファクトリ staticメソッドがコードを書く側から
見れば、やはり魅力的なんだけど、視点をコード書く側のことも考えてほしい。

apache-wicket.1842946.n4.nabble.com