XStream の CDATAセクションの書き方

XStream で、CDATAセクションを出力するには、XStream のコンストラクタに、HierarchicalStreamDriver 実装の
Driver を指定し、指定する Driver が、createWriter で返す Writer が、直接テキストを判定して書かせるしか
ないみたい。

try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("out.xml"), "UTF-8")){
   writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");

   XStream xstream = new XStream(new XppDriver(){
      public HierarchicalStreamWriter createWriter(Writer out){
         return new PrettyPrintWriter(writer){
            protected void writeText(QuickWriter w, String text){
               if (text.indexOf("<") >= 0
                     || text.indexOf(">") >= 0
                     || text.indexOf("&") >= 0
                     || text.indexOf("!") >= 0
                     || text.indexOf("'") >= 0
                     || text.indexOf("\"") >= 0
                  ){
                  w.write("<![CDATA[");
                  w.write(text);
                  w.write("]]>");
               }else{
                  w.write(text);
               }
            }
         };
      }
   });

   // xstream toXML を実行する

}catch(Exception e){
   e.printStackTrace();
}finally{

}

ダサい。

読込みは特に何もしなくて良い。

PostgreSQL のアップサートとMySQLのアップサート

PostgreSQL に馴染みがなく、アップサートあるいは、SERIAL に困惑している。
MySQL には、AUTO INCREMENT があり、例えば、以下のようなテーブル

CREATE TABLE sample 
(
   id    INT NOT NULL AUTO_INCREMENT,
   point INT,
   price INT,
   PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1

これと同等な PostgreSQL でのテーブルは、

CREATE TABLE sample 
(
   id SERIAL NOT NULL,
   point INT,
   price INT,
   CONSTRAINT sample_pkey PRIMARY KEY (id)
)

制約を見る。。
SELECT table_name, constraint_name, constraint_type
FROM   information_schema.table_constraints
WHERE  table_name='sample';

 table_name |    constraint_name    | constraint_type
------------+-----------------------+-----------------
 sample     | sample_pkey           | PRIMARY KEY
 sample     | 2200_16824_1_not_null | CHECK

まず、MySQLの AUTO INCREMENT 同様、PostgreSQL の 動き、

SELECT * FROM sample;

 id | point | price
----+-------+-------
  1 |    10 |   100
  2 |    20 |   200
(2 行)

ここで、、、INSERT INTO sample (point, price) VALUES (30, 300)
を実行する。MySQL も同じく、PostgreSQL でも、

SELECT * FROM sample;

 id | point | price
----+-------+-------
  1 |    10 |   100
  2 |    20 |   200
  3 |    30 |   300
(3 行)

ここまでは、別に違和感もなく特になにもなく納得。

アップサートを行う!!
まず、存在するレコードに対して、、、
MySQL では、以下のように書く。

INSERT INTO sample (id, `point`, price) VALUES (3, 40, 400)
ON DUPLICATE KEY UPDATE
 `point` = VALUES(`point`)
, price  = VALUES(price)

PostgreSQL では、以下のように書く

INSERT INTO sample (id, `point`, price) VALUES (3, 40, 400)
ON CONFLICT ON CONSTRAINT sample_pkey
DO UPDATE SET point = 40, price = 400

どちらも、id = 3 のレコードだけが更新される。

実践では、動的SQL文生成でこのアップサート文を挿入か更新か処理設計の都合で
実行時にならないと定まらないまま使う。
「アップサート」という本来の目的からすれば、挿入か更新か実行時に判断されることを
期待している。
だから、MySQL では、アップサートが以下のように、制約キー id が NULL の時、、

INSERT INTO sample (id, `point`, price) VALUES (null, 40, 400)
ON DUPLICATE KEY UPDATE
 `point` = VALUES(`point`)
, price  = VALUES(price)

これは、更新でなく新しいレコードが挿入される。AUTO INCREMENT が効いてくる。

しかし、、、PostgreSQL では、、

INSERT INTO sample (id, `point`, price) VALUES (null, 40, 400)
ON CONFLICT ON CONSTRAINT sample_pkey
DO UPDATE SET point = 40, price = 400

ERROR:  列"id"内のNULL値はNOT NULL制約違反です

エラーになり、新しいレコード挿入にならない!!・・・あれ?上に書いたように、

INSERT INTO sample (point, price) VALUES (30, 300)

が成功する PostgreSQL に AUTO INCREMENT の代わりとされてる SERIAL は、何なの?

PostgreSQL のアップサート文で以下のようにすると成功する。

INSERT INTO sample (id, point, price) VALUES (nextval('sample_id_seq'::regclass), 40, 400)
ON CONFLICT ON CONSTRAINT sample_pkey
DO UPDATE SET point = 40, price = 400

これじゃ、実行するSQL生成として、 VALUES 文に、null を入れる代わりに、
nextval('sample_id_seq'::regclass) を入れるなんて、変なことをしなくてはならない。

挿入でも更新でも1つのアップサートSQLでという本来の目的に合わない!

実践の処理として考えると既存データを編集した登録のSQLは、制約キーIDの値が存在するデータで
SQL文にパラメータ渡しで生成するであろうし、
新しいデータである場合、NULL をSQL文生成に渡すことになる。この実行時にならないと
決定しない時に、アップサートのSQL文が威力を発揮するはずで、
PostgreSQL のアップサートとされている文は、動的変化に耐えられない。

PostgreSQL より、MySQL の方に軍配が上がる。

そもそも、そんなDBの比較なんて無意味なのか。。。

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