Java11 HttpClient SSL サンプル

サンプルといっても完成形ではないが。。。

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
try{
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, new TrustManager[]{
        new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
        @Override
        public void checkClientTrusted(X509Certificate[] certs, String authType) {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] certs, String authType) {
        }
    }}, new SecureRandom());
    SSLSocketFactory sslSocketFactory = (javax.net.ssl.SSLSocketFactory)SSLSocketFactory.getDefault();
    HttpClient client = HttpClient.newBuilder()
                        .sslContext(sslContext)
                        .build();
    // TODO HttpRequest を作って送信

}catch (NoSuchAlgorithmException e){
    e.printStackTrace();
}catch (KeyManagementException e){
    e.printStackTrace();
}

本来、JKS形式キーストア形式の鍵ファイルとパスワードで
SSLContextを作るべきで、

private static SSLContext getSSLContext(String keyFilePath, String pass) throws KeyManagementException, KeyStoreException, NoSuchAlgorithmException,
CertificateException, IOException, UnrecoverableKeyException {
     // KeyStore
     KeyStore clientStore = KeyStore.getInstance("jks");
    // 指定された入力ストリームからこのキーストアをロード
    FileInputStream keyFileStream = new FileInputStream(keyFilePath);
    clientStore.load(keyFileStream, pass.toCharArray());
    keyFileStream.close();
    // KeyManager
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(clientStore, pass.toCharArray());
    // 鍵データの種類ごとに 1 つの鍵マネージャーを取得
    KeyManager[] kms = kmf.getKeyManagers();
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    sslContext.init(kms, null, new SecureRandom());
    return sslContext;
}

のようなメソッドを用意してSSLContextを指定すべきなのであろう。

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()
    ));
}

Guice Injector の mock

モックインスタンスguice でインジェクションさせるパターンは、ネット検索すれば、
たくさん見つかる。
しかし、Injector の生成メソッド Guice#createInjector をモックしてテスト用の Injector を生成させるパターンは
見たことがない。
MockedStatic で、Guice#createInjector のメソッドの引数のマッチング指定は、
createInjectorメソッドが、可変長引数の Module であることとメソッド形式 ポリモーフィズムのせいで、
難しくなる。
スタティックメソッドのモックなので、mockito-inline が必要

<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.13.1</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>4.4.0</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-inline</artifactId>
   <version>4.4.0</version>
   <scope>test</scope>
</dependency>

Guice#createInjector モックでテスト用の Injector を返す例
モックの指定前に、テスト用の Injector を作っておく( try文の前で作っておかなくてはならない)
必要な import

import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.AbstractModule;

@Before で Injector 生成しておくあるいは、モック定義の前に作成しておく
ArgumentMatchers.any() の指定の仕方が重要で、
  com.google.inject.Module[] を型として指定する

try'(){ } の中だけが有効である

private Injector injector;


@Before
public void before() {
    injector = Guice.createInjector(new AbstractModule() {
        @Override
        protected void configure(){
            // TODO テスト用のバインド定義
        }
    });
}

@Test
public void test() {
    try(MockedStatic<Guice> mock = Mockito.mockStatic(Guice.class)) {
        mock.when(()->Guice.createInjector(ArgumentMatchers.<com.google.inject.Module[]>any()))
        .thenReturn(injector);

        // TODO テスト対象を実行
    }

}

Objects.requireNonNull

安易というかまだまだ、if 文で null かどうかをチェックすることが多い
Objects.requireNonNull で、NullPointerException を発生させるスタイルは
なぜか、あまり見ない。もっと使えば良いと思うのだが。。。

java.util.Objects

public static T requireNonNull(T obj)
public static T requireNonNull(T obj, String message)

JavaDoc の説明 に使用例がある

public Foo(Bar bar) {
    this.bar = Objects.requireNonNull(bar);
}

NullPointerException に message をつけてくれる。

public Foo(Bar bar, Baz baz) {
    this.bar = Objects.requireNonNull(bar, "bar must not be null");
    this.baz = Objects.requireNonNull(baz, "baz must not be null");
}

hamcrest-json のチェック処理メソッドを使いやすくする

hamcrest-json を JUnit 以外で使用する - Oboe吹きプログラマの黙示録
と、書いたがもっと実践的なものにする。

Maven pom.xml に追加する

<dependency>
   <groupId>uk.co.datumedge</groupId>
   <artifactId>hamcrest-json</artifactId>
   <version>0.2</version>
</dependency>

AssertionError を捕まえてエラーメッセージを、ラムダ(Consumer)で処理する。
インターフェースでメソッドを提供

import java.util.function.Consumer;
import org.hamcrest.MatcherAssert;
import uk.co.datumedge.hamcrest.json.SameJSONAs;
/**
 * JsonCompare
 */
public interface JsonCompare{
    default boolean sameJson(String realJson, String expectJson, Consumer<String> diff){
        try{
            MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson));
            return true;
        }catch(AssertionError e){
            diff.accept(e.getMessage()
                    .replaceAll("\\\\r", "")
                    .replaceAll("\\\\n", "\n")
                    .replaceAll("\\\\\"", "\"")
            );
            return false;
        }
    }
    // realJson > expectJson でもOK
    default boolean sameJsonAnyKey(String realJson, String expectJson, Consumer<String> diff){
        try{
            MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson).allowingExtraUnexpectedFields());
            return true;
        }catch(AssertionError e){
            diff.accept(e.getMessage()
                    .replaceAll("\\\\r", "")
                    .replaceAll("\\\\n", "\n")
                    .replaceAll("\\\\\"", "\"")
            );
            return false;
        }
    }
    // 配列並びが異なってもOK
    default boolean sameJsonAnyOrder(String realJson, String expectJson, Consumer<String> diff){
        try{
            MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson).allowingAnyArrayOrdering());
            return true;
        }catch(AssertionError e){
            diff.accept(e.getMessage()
                    .replaceAll("\\\\r", "")
                    .replaceAll("\\\\n", "\n")
                    .replaceAll("\\\\\"", "\"")
            );
            return false;
        }
    }
    // ealJson > expectJson でもOK AND  配列並びが異なってもOK
    default boolean sameJsonAnyKeyOrder(String realJson, String expectJson, Consumer<String> diff){
        try{
            MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson)
                    .allowingExtraUnexpectedFields().allowingAnyArrayOrdering());
            return true;
        }catch(AssertionError e){
            diff.accept(e.getMessage()
                    .replaceAll("\\\\r", "")
                    .replaceAll("\\\\n", "\n")
                    .replaceAll("\\\\\"", "\"")
            );
            return false;
        }
    }
}

使い方

boolean res = sameJson(realJson, expectJson, diffstr->{
    // diffstr は、JSONの違いを説明を記述した String 
    
});

JSON に違いがあった時だけ、Consumer diffstr ->{ } が実行される

hamcrest-json を JUnit 以外で使用する

2つのJSONを比較するのに、JUnit だけで使用するのはもったいないと思った。

<dependency>
   <groupId>uk.co.datumedge</groupId>
   <artifactId>hamcrest-json</artifactId>
   <version>0.2</version>
</dependency>

このようにテストスコープを外して使用
安易であるが比較メソッドも以下のように用意する。

import org.hamcrest.MatcherAssert;
import uk.co.datumedge.hamcrest.json.SameJSONAs;
public static boolean sameJson(String realJson, String expectJson) {
    try{
        MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson));
        return true;
    }catch(AssertionError e) {
        return false;
    }
}
public static boolean sameJsonAnyKey(String realJson, String expectJson) {
    try{
        MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson).allowingExtraUnexpectedFields());
        return true;
    }catch(AssertionError e) {
        return false;
    }
}
public static boolean sameJsonAnyOrder(String realJson, String expectJson) {
    try{
        MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson).allowingAnyArrayOrdering());
        return true;
    }catch(AssertionError e){
        return false;
    }
}
public static boolean sameJsonAnyKeyOrder(String realJson, String expectJson) {
    try{
        MatcherAssert.assertThat(realJson, SameJSONAs.sameJSONAs(expectJson)
                .allowingExtraUnexpectedFields().allowingAnyArrayOrdering());
        return true;
    }catch(AssertionError e){
        return false;
    }
}

boolean sameJson(String realJson, String expectJson)
同じキー、値が同じかどうか。

boolean sameJsonAnyKey(String realJson, String expectJson)
expectJson より、realJson で、異なるキー値があるのはOK
つまり、realJson は、expectJson に含まれる。

boolean sameJsonAnyKeyOrder(String realJson, String expectJson)
値である配列の順番が異なっても良い。

boolean sameJsonAnyOrder(String realJson, String expectJson)
expectJson より、realJson で、異なるキー値があるのはOK、
AND 値である配列の順番が異なっても良い。

InputStream から読み取って String

ほとんど多くの現場で、ほぼ同じコードを書くことが多くて
煩わしいと思ったので、メモ。

ByteArrayOutputStream を使うのがいいのか?それとも BufferedReader を使うのが
いいのか?迷うところではある。

ByteArrayOutputStream を使う

// InputStream → toString   : InputStream close しないので呼び出し側でCLOSE
private String toStringFromInputStream(InputStream inst) throws IOException{
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    int len = 0;
    byte[] buf = new byte[1024];
    while((len=inst.read(buf, 0, buf.length)) >= 0){
        bo.write(buf, 0, len);
        bo.flush();
    }
    bo.close();
    return  bo.toString();
}

InputStreamReader & BufferedReader を使う

// InputStream → toString   : InputStream close しないので呼び出し側でCLOSE
private String toStringFromInputStream(InputStream inst) throws IOException{
    BufferedReader br = new BufferedReader(new InputStreamReader(inst, StandardCharsets.UTF_8));
    StringBuilder sb = new StringBuilder();
    String line;
    while((line = br.readLine()) != null){
        sb.append(line);
    }
    return sb.toString();
}