文字列のHEXダンプを作る

Apache の commons io の HEXダンプ、
org.apache.commons.io.HexDump の dump(byte[] data, long offset, OutputStream stream, int index)
結果は、左端に offset のサイズ目盛があって良いのだけれど、
以下のように少し横幅が長く、HEXの表示部と文字列部の区分が見にくい思う時がある。

00000000 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 30 123456789ABCDEF0
00000010 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 30 123456789ABCDEF0
00000020 31 32 33 34 35 36                               123456

offset のサイズ目盛を省いて

31323334 35363738 39414243 44454630  [123456789ABCDEF0]
31323334 35363738 39414243 44454630  [123456789ABCDEF0]
31323334 3536                        [123456]

このように結果を出して欲しいのだ。
2バイト文字の表示は諦めて、どうせならと、、自前で作る。
スピードを考えて原始的なところから自作する。
まず必要なのは、、任意の長さバイトで括るイテレータで以下を用意

import java.io.Serializable;
import java.util.Iterator;
/**
 * Hexbyterator.
 */
public class Hexbyterator implements Iterator<byte[]>, Serializable{
   private byte[] b;
   private boolean next;
   private long seek = 0;
   private int blength = 16;

   private Hexbyterator(byte[] b, int len){
      if (len < 1) new IllegalArgumentException("pack length must be greater than 0");
      this.b = b;
      blength = len;
      next = b != null && b.length > 0 ? true : false;
   }
   public static Hexbyterator of(byte[] bytes, int len){
      return new Hexbyterator(bytes, len);
   }
   @Override
   public boolean hasNext(){
      return next;
   }
   @Override
   public byte[] next(){
      if (next){
         long n = seek + blength;
         if ( n < b.length ){
            byte[] r = new byte[blength];
            for(int i=0;i < blength;i++){
               r[i] = b[Long.valueOf(seek).intValue()];
               seek++;
            }
            next = seek < b.length;
            return r;
         }
         byte[] r = new byte[ Long.valueOf(blength - (n - b.length)).intValue() ];
         for(int i=0;i < r.length;i++){
            r[i] = b[Long.valueOf(seek).intValue()];
            seek++;
         }
         next = false;
         return r;
      }
      return null;
   }
}

これを使用して、対象文字列からダンプ出力するクラス

import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
 * 16バイト単位で纏めてHEXダンプした結果テキストの Stream または List を生成する。
 * UTF-8 での HEXダンプである。
 * HEX表現ダンプとと2バイト文字以外を文字列表現でダンプする。
 * 2バイト文字は、1byte を '?'
 * 制御文字は、1 byte を '.' 文字
 */
public final class DumpHex{
   private String str;
   private DumpHex(String str){
      this.str = str;
   }
   /**
    * インスタンス生成
    * @param str ダンプ対象文字列
    * @return DumpHex
    */
   public static DumpHex of(String str){
      return new DumpHex(str);
   }
   /**
    * @return 16バイト単位ダンプ結果文字列のストリームを返す
    */
   public Stream<String> stream(){
      byte[] bs = str.getBytes();
      return StreamSupport.stream(Spliterators.spliteratorUnknownSize(Hexbyterator.of(bs, 16), 0), false)
      .map(b->{
         StringBuilder sb = new StringBuilder();
         for(int x=0;x < b.length;x++){
            if (x > 0 && x % 4 == 0) sb.append(" ");
            sb.append(String.format("%02x", b[x]));
         }
         String hexstring = sb.toString();
         if (35 > hexstring.length()){
            hexstring += "                                  ".substring(0, 35 - hexstring.length());
         }
         for(int n=0;n < b.length;n++){
            if (b[n] < 0){
               b[n] = 0x3f;
            }else if(b[n] < 0x20){
               b[n] = 0x2e;
            }
         }
         String ss = new String(b);
         return hexstring + "  [" + ss + "]";
      });
   }
   /**
    * @return 16バイト単位ダンプ結果文字列のリストを返す
    */
   public List<String> list(){
      return stream().collect(Collectors.toList());
   }
   /**
     * @return HEX表現だけのダンプ結果を区切り文字なしで文字列で返す。
    */
   public String hexstring(){
      return hexstring("");
   }
   /**
     * @return HEX表現だけのダンプ結果を1バイトずつの区切り文字を指定して文字列で返す。
    */
   public String hexstring(String splitstring){
      ByteArrayInputStream inst = new ByteArrayInputStream(str.getBytes());
      return Stream.generate(inst::read).limit(inst.available())
      .map(e->String.format("%02x", e)).collect(Collectors.joining(splitstring));
   }
}

使い方の例、、、
ロガーで出したい!!

Logger logger = LoggerFactory.getLogger(this.getClass());
DumpHex dump = DumpHex.of(string);
dump.stream().forEach(d->{
      logger.debug( d );
});

org.apache.commons.io.HexDump の dump メソッドを呼ぶ出すような
面倒くさい引数を渡す必要もない。