XStream を使う

XML読み書き、Java Object との相互変換に何を使う?で問われてだいたいは、JAXB を挙げる。
他の選択肢は?で XStream を挙げてくれる人は少ない。
5年前に、XStream を知ってその頃は、あまり活発でなかった。しばらく触れる機会がなかったが、
2022年1月には、バージョン 1.4.19 になっていたんですね。

<dependency>
   <groupId>com.thoughtworks.xstream</groupId>
   <artifactId>xstream</artifactId>
   <version>1.4.19</version>
</dependency>

XStream の チュートリアル
https://www.tutorialspoint.com/xstream/index.htm

Java Object → XML
対象Object lombok を使う。

package org.example.entity;
import java.util.List;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import lombok.Data;
/**
 * Person
 */
@XStreamAlias("person")
@Data
public class Person {
    private String firstname;
    private String lastname;
    private PhoneNumber phone;
    private PhoneNumber fax;
    private List<Integer> checkYear;
    private String body;
}
package org.example.entity;
import lombok.Data;
/**
 * PhoneNumber
 */
 @Data
public class PhoneNumber{
   private int code;
   private String number;
}

XStream インスタンスの準備
@XStreamAlias を使う場合は、XStream#processAnnotations で対象クラスを指定
@XStreamAlias を使わない場合は、各クラス、タグに対して
XStream#alias(タグ名, クラス) を実行しなければならない
null に対して空タグでシリアライズしたいのでカスタムの Converter を用意して0
設定する。
XStream null value を出力するケース、再び書き直す。 - Oboe吹きプログラマの黙示録 参照

XStream xstream = new XStream();

// null の場合のカスタム Converter
CustomEmptyConverter emptyConverter = new CustomEmptyConverter(xstream.getMapper(), e->{
    if (e instanceof Person) {
        // Person クラス上だから。。
        return "body";
    }
    return null;
});
xstream.registerConverter(emptyConverter,  XStream.PRIORITY_VERY_LOW);

xstream.processAnnotations(Person.class);
xstream.alias("year", int.class);

CustomEmptyConverter のコード

import java.util.function.Function;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.SunUnsafeReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
/**
 * CustomEmptyConverter
 */
public class CustomEmptyConverter extends ReflectionConverter{
    private Function<Object, String> emptywriter;
    /**
     * コンストラクタ.
     */
    public CustomEmptyConverter(Mapper mapper, Function<Object, String> emptywriter){
        super(mapper, new SunUnsafeReflectionProvider());
        this.emptywriter = emptywriter;
    }
    @Override
    protected void doMarshal(final Object object, final HierarchicalStreamWriter writer, final MarshallingContext context){
        super.doMarshal(object, writer, context);
        String tagname = emptywriter.apply(object);
        if (tagname != null){
            writer.startNode(tagname);
            writer.setValue("");
            writer.endNode();
        }
    }
}

シリアライズ実行

String xml = xstream.toXML(person);

結果の例

<person>
  <firstname>太郎</firstname>
  <lastname>山田</lastname>
  <phone>
    <code>110</code>
    <number>000-0000-1111</number>
  </phone>
  <fax>
    <code>120</code>
    <number>03-1111-1112</number>
  </fax>
  <checkYear>
    <year>2017</year>
    <year>2021</year>
    <year>2022</year>
  </checkYear>
  <body></body>
</person>

XMLJava Object

タグ名に対する Java Object のクラス名を解決する必要があり、
XStream#allowTypesByWildcard
で、パッケージ名+".**" を指定しなければならない。

String[] なので、複数を指定できる。

XStream xstream = new XStream();
xstream.processAnnotations(Person.class);
xstream.allowTypesByWildcard(new String[]{
    "org.example.entity.**"
});

シリアライズ時同様に、processAnnotationsを忘れずに。

シリアライズ実行

try(InputStream inst = getSourceInputStream(this.getClass(), "test.xml")){
     Person tp = (Person)xstream.fromXML(inst);
     // TODO
}catch(IOException e){
     e.printStackTrace();
}catch(URISyntaxException e){
     e.printStackTrace();
}

指定する Class と同じ場所のファイルの InputStream を取得するメソッド

public InputStream getSourceInputStream(Class<?> cls, String fileName) throws IOException, URISyntaxException {
    return new FileInputStream(new File(Thread.currentThread()
            .getContextClassLoader()
            .getResource(cls.getPackageName().replaceAll("\\.", "/") + "/" + fileName)
            .toURI()
    ));
}