From bdf672659cee13d95abc246de7cde469cf8fc07d Mon Sep 17 00:00:00 2001
From: Roger Riggs <rriggs@openjdk.org>
Date: Thu, 16 Apr 2020 15:45:37 -0400
Subject: [PATCH] 8243010: Test support: Customizable Hex Printer

Reviewed-by: lancea, dfuchs, weijun
---
 test/jdk/com/sun/jndi/ldap/Base64Test.java    |    3 -
 .../com/sun/security/sasl/ntlm/NTLMTest.java  |   11 +-
 .../javax/net/ssl/DTLS/DTLSOverDatagram.java  |   12 +-
 .../ClientHelloBufferUnderflowException.java  |    8 +-
 .../ssl/interop/ClientHelloChromeInterOp.java |    9 +-
 test/jdk/sun/security/krb5/auto/MSOID2.java   |    6 +-
 .../security/krb5/etype/KerberosAesSha2.java  |    7 +-
 .../sun/security/mscapi/PublicKeyInterop.java |   14 +-
 .../sun/security/pkcs/pkcs7/SignerOrder.java  |   11 +-
 .../sun/security/pkcs/pkcs8/PKCS8Test.java    |    9 +-
 .../security/pkcs/pkcs9/UnknownAttribute.java |    7 +-
 .../ssl/SSLSocketImpl/SSLSocketKeyLimit.java  |    6 +-
 .../security/x509/X500Name/NullX500Name.java  |   15 +-
 test/lib-test/TEST.ROOT                       |    9 +
 .../jdk/test/lib/hexdump/HexPrinterTest.java  |  409 ++++++
 test/lib/jdk/test/lib/hexdump/HexPrinter.java | 1181 +++++++++++++++++
 16 files changed, 1657 insertions(+), 60 deletions(-)
 create mode 100644 test/lib-test/TEST.ROOT
 create mode 100644 test/lib-test/jdk/test/lib/hexdump/HexPrinterTest.java
 create mode 100644 test/lib/jdk/test/lib/hexdump/HexPrinter.java

diff --git a/test/jdk/com/sun/jndi/ldap/Base64Test.java b/test/jdk/com/sun/jndi/ldap/Base64Test.java
index b6c47414b82..900e04ad123 100644
--- a/test/jdk/com/sun/jndi/ldap/Base64Test.java
+++ b/test/jdk/com/sun/jndi/ldap/Base64Test.java
@@ -163,9 +163,6 @@ public class Base64Test {
      */
     private static void deserialize(byte[] bytes) throws Exception {
 
-        //System.out.println("\nSerialized RefAddr object: ");
-        //System.out.println(new sun.security.util.HexDumpEncoder().encode(bytes));
-
         ObjectInputStream objectStream =
             new ObjectInputStream(new ByteArrayInputStream(bytes));
         Object object = objectStream.readObject();
diff --git a/test/jdk/com/sun/security/sasl/ntlm/NTLMTest.java b/test/jdk/com/sun/security/sasl/ntlm/NTLMTest.java
index aed713d598c..5b5841b0aae 100644
--- a/test/jdk/com/sun/security/sasl/ntlm/NTLMTest.java
+++ b/test/jdk/com/sun/security/sasl/ntlm/NTLMTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
 /*
  * @test
  * @bug 6911951 7150092
+ * @library /test/lib
  * @summary NTLM should be a supported Java SASL mechanism
  * @modules java.base/sun.security.util
  *          java.security.sasl
@@ -32,7 +33,7 @@ import java.io.IOException;
 import javax.security.sasl.*;
 import javax.security.auth.callback.*;
 import java.util.*;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class NTLMTest {
 
@@ -312,7 +313,7 @@ public class NTLMTest {
         byte[] response = (clnt.hasInitialResponse()
                 ? clnt.evaluateChallenge(EMPTY) : EMPTY);
         System.out.println("Initial:");
-        new HexDumpEncoder().encodeBuffer(response, System.out);
+        HexPrinter.simple().format(response);
         byte[] challenge;
 
         while (!clnt.isComplete() || !srv.isComplete()) {
@@ -320,12 +321,12 @@ public class NTLMTest {
             response = null;
             if (challenge != null) {
                 System.out.println("Challenge:");
-                new HexDumpEncoder().encodeBuffer(challenge, System.out);
+                HexPrinter.simple().format(challenge);
                 response = clnt.evaluateChallenge(challenge);
             }
             if (response != null) {
                 System.out.println("Response:");
-                new HexDumpEncoder().encodeBuffer(response, System.out);
+                HexPrinter.simple().format(response);
             }
         }
 
diff --git a/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java b/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java
index 6691aeba06b..4749dc0bb74 100644
--- a/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java
+++ b/test/jdk/javax/net/ssl/DTLS/DTLSOverDatagram.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -43,7 +43,7 @@ import jdk.test.lib.security.SSLContextBuilder;
 
 import java.util.concurrent.*;
 
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 /**
  * An example to show the way to use SSLEngine in datagram connections.
@@ -688,12 +688,11 @@ public class DTLSOverDatagram {
     }
 
     final static void printHex(String prefix, ByteBuffer bb) {
-        HexDumpEncoder  dump = new HexDumpEncoder();
 
         synchronized (System.out) {
             System.out.println(prefix);
             try {
-                dump.encodeBuffer(bb.slice(), System.out);
+                HexPrinter.simple().format(bb.slice());
             } catch (Exception e) {
                 // ignore
             }
@@ -704,13 +703,10 @@ public class DTLSOverDatagram {
     final static void printHex(String prefix,
             byte[] bytes, int offset, int length) {
 
-        HexDumpEncoder  dump = new HexDumpEncoder();
-
         synchronized (System.out) {
             System.out.println(prefix);
             try {
-                ByteBuffer bb = ByteBuffer.wrap(bytes, offset, length);
-                dump.encodeBuffer(bb, System.out);
+                HexPrinter.simple().format(bytes, offset, length);
             } catch (Exception e) {
                 // ignore
             }
diff --git a/test/jdk/javax/net/ssl/interop/ClientHelloBufferUnderflowException.java b/test/jdk/javax/net/ssl/interop/ClientHelloBufferUnderflowException.java
index 69778aa0045..ca5742f37b2 100644
--- a/test/jdk/javax/net/ssl/interop/ClientHelloBufferUnderflowException.java
+++ b/test/jdk/javax/net/ssl/interop/ClientHelloBufferUnderflowException.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -30,13 +30,15 @@
  * @test
  * @bug 8215790 8219389
  * @summary Verify exception
+ * @library /test/lib
  * @modules java.base/sun.security.util
  * @run main/othervm ClientHelloBufferUnderflowException
  */
 
-import sun.security.util.HexDumpEncoder;
 import javax.net.ssl.SSLHandshakeException;
 
+import jdk.test.lib.hexdump.HexPrinter;
+
 public class ClientHelloBufferUnderflowException extends ClientHelloInterOp {
     /*
      * Main entry point for this test.
@@ -75,7 +77,7 @@ public class ClientHelloBufferUnderflowException extends ClientHelloInterOp {
 
         System.out.println("The ClientHello message used");
         try {
-            (new HexDumpEncoder()).encodeBuffer(bytes, System.out);
+            HexPrinter.simple().format(bytes);
         } catch (Exception e) {
             // ignore
         }
diff --git a/test/jdk/javax/net/ssl/interop/ClientHelloChromeInterOp.java b/test/jdk/javax/net/ssl/interop/ClientHelloChromeInterOp.java
index 1157b6aeafc..f426cce33e3 100644
--- a/test/jdk/javax/net/ssl/interop/ClientHelloChromeInterOp.java
+++ b/test/jdk/javax/net/ssl/interop/ClientHelloChromeInterOp.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -30,13 +30,15 @@
  * @test
  * @bug 8169362
  * @summary Interop automated testing with Chrome
+ * @library /test/lib
  * @modules jdk.crypto.ec
  *          java.base/sun.security.util
  * @run main/othervm ClientHelloChromeInterOp
  */
 
 import java.util.Base64;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
+
 
 public class ClientHelloChromeInterOp extends ClientHelloInterOp {
     // The ClientHello message.
@@ -63,10 +65,9 @@ public class ClientHelloChromeInterOp extends ClientHelloInterOp {
 
         // Dump the hex codes of the ClientHello message so that developers
         // can easily check whether the message is captured correct or not.
-        HexDumpEncoder dump = new HexDumpEncoder();
         System.out.println("The ClientHello message used");
         try {
-            dump.encodeBuffer(bytes, System.out);
+            HexPrinter.simple().format(bytes);
         } catch (Exception e) {
             // ignore
         }
diff --git a/test/jdk/sun/security/krb5/auto/MSOID2.java b/test/jdk/sun/security/krb5/auto/MSOID2.java
index 3a3b3cf6c26..0b7545ffbe2 100644
--- a/test/jdk/sun/security/krb5/auto/MSOID2.java
+++ b/test/jdk/sun/security/krb5/auto/MSOID2.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -32,7 +32,7 @@
  */
 
 import sun.security.jgss.GSSUtil;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 // The basic krb5 test skeleton you can copy from
 public class MSOID2 {
@@ -72,7 +72,7 @@ public class MSOID2 {
                     nt[pos] = (byte)newLen;
                 }
                 t = nt;
-                new HexDumpEncoder().encodeBuffer(t, System.out);
+                HexPrinter.simple().format(t);
             }
             if (t != null || !s.x().isEstablished()) t = s.take(t);
             if (c.x().isEstablished() && s.x().isEstablished()) break;
diff --git a/test/jdk/sun/security/krb5/etype/KerberosAesSha2.java b/test/jdk/sun/security/krb5/etype/KerberosAesSha2.java
index 79c8053bf0a..c738e063e02 100644
--- a/test/jdk/sun/security/krb5/etype/KerberosAesSha2.java
+++ b/test/jdk/sun/security/krb5/etype/KerberosAesSha2.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,6 +23,7 @@
 /*
  * @test
  * @bug 8014628
+ * @library /test/lib
  * @modules java.base/sun.security.util
  *          java.security.jgss/sun.security.krb5.internal.crypto.dk:+open
  * @summary https://tools.ietf.org/html/rfc8009 Test Vectors
@@ -33,7 +34,7 @@ import java.lang.reflect.Method;
 import java.util.Arrays;
 
 import sun.security.krb5.internal.crypto.dk.AesSha2DkCrypto;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class KerberosAesSha2 {
 
@@ -204,6 +205,6 @@ public class KerberosAesSha2 {
     }
 
     private static void dump(byte[] data) throws Exception {
-        new HexDumpEncoder().encodeBuffer(data, System.err);
+        HexPrinter.simple().dest(System.err).format(data);
     }
 }
diff --git a/test/jdk/sun/security/mscapi/PublicKeyInterop.java b/test/jdk/sun/security/mscapi/PublicKeyInterop.java
index 85da7e041b1..f3b646bc372 100644
--- a/test/jdk/sun/security/mscapi/PublicKeyInterop.java
+++ b/test/jdk/sun/security/mscapi/PublicKeyInterop.java
@@ -35,7 +35,7 @@ import java.util.*;
 import javax.crypto.*;
 
 import jdk.test.lib.SecurityTools;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 /*
  * Confirm interoperability of RSA public keys between SunMSCAPI and SunJCE
@@ -84,29 +84,29 @@ public class PublicKeyInterop {
         System.out.println();
 
         byte[] plain = new byte[] {0x01, 0x02, 0x03, 0x04, 0x05};
-        HexDumpEncoder hde = new HexDumpEncoder();
-        System.out.println("Plaintext:\n" + hde.encode(plain) + "\n");
+        HexPrinter hp = HexPrinter.simple();
+        System.out.println("Plaintext:\n" + hp.toString(plain) + "\n");
 
         Cipher rsa = Cipher.getInstance("RSA/ECB/PKCS1Padding");
         rsa.init(Cipher.ENCRYPT_MODE, myPuKey);
         byte[] encrypted = rsa.doFinal(plain);
         System.out.println("Encrypted plaintext using RSA Cipher from " +
             rsa.getProvider().getName() + " JCE provider\n");
-        System.out.println(hde.encode(encrypted) + "\n");
+        System.out.println(hp.toString(encrypted) + "\n");
 
         Cipher rsa2 = Cipher.getInstance("RSA/ECB/PKCS1Padding", "SunMSCAPI");
         rsa2.init(Cipher.ENCRYPT_MODE, myPuKey);
         byte[] encrypted2 = rsa2.doFinal(plain);
         System.out.println("Encrypted plaintext using RSA Cipher from " +
             rsa2.getProvider().getName() + " JCE provider\n");
-        System.out.println(hde.encode(encrypted2) + "\n");
+        System.out.println(hp.toString(encrypted2) + "\n");
 
         Cipher rsa3 = Cipher.getInstance("RSA/ECB/PKCS1Padding", "SunMSCAPI");
         rsa3.init(Cipher.DECRYPT_MODE, myPrKey);
         byte[] decrypted = rsa3.doFinal(encrypted);
         System.out.println("Decrypted first ciphertext using RSA Cipher from " +
             rsa3.getProvider().getName() + " JCE provider\n");
-        System.out.println(hde.encode(decrypted) + "\n");
+        System.out.println(hp.toString(decrypted) + "\n");
         if (! Arrays.equals(plain, decrypted)) {
             throw new Exception("First decrypted ciphertext does not match " +
                 "original plaintext");
@@ -115,7 +115,7 @@ public class PublicKeyInterop {
         decrypted = rsa3.doFinal(encrypted2);
         System.out.println("Decrypted second ciphertext using RSA Cipher from "
             + rsa3.getProvider().getName() + " JCE provider\n");
-        System.out.println(hde.encode(decrypted) + "\n");
+        System.out.println(hp.toString(decrypted) + "\n");
         if (! Arrays.equals(plain, decrypted)) {
             throw new Exception("Second decrypted ciphertext does not match " +
                 "original plaintext");
diff --git a/test/jdk/sun/security/pkcs/pkcs7/SignerOrder.java b/test/jdk/sun/security/pkcs/pkcs7/SignerOrder.java
index ef247d5906a..875ca5935e4 100644
--- a/test/jdk/sun/security/pkcs/pkcs7/SignerOrder.java
+++ b/test/jdk/sun/security/pkcs/pkcs7/SignerOrder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
  * @test
  * @bug 8048357
  * @summary test PKCS7 data signing, encoding and verification
+ * @library /test/lib
  * @modules java.base/sun.security.pkcs
  *          java.base/sun.security.util
  *          java.base/sun.security.x509
@@ -40,7 +41,6 @@ import java.security.Signature;
 import java.security.SignatureException;
 import java.security.cert.X509Certificate;
 import java.util.Date;
-import sun.security.util.HexDumpEncoder;
 import sun.security.pkcs.ContentInfo;
 import sun.security.pkcs.PKCS7;
 import sun.security.pkcs.SignerInfo;
@@ -55,11 +55,10 @@ import sun.security.x509.X500Name;
 import sun.security.x509.X509CertImpl;
 import sun.security.x509.X509CertInfo;
 import sun.security.x509.X509Key;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class SignerOrder {
 
-    static final HexDumpEncoder hexDump = new HexDumpEncoder();
-
     //signer infos
     static final byte[] data1 = "12345".getBytes();
     static final byte[] data2 = "abcde".getBytes();
@@ -120,7 +119,7 @@ public class SignerOrder {
         signerInfo.derEncode(strm);
         System.out.println("SignerInfo, length: "
                 + strm.toByteArray().length);
-        System.out.println(hexDump.encode(strm.toByteArray()));
+        HexPrinter.simple().format(strm.toByteArray());
         System.out.println("\n");
         strm.reset();
     }
@@ -131,7 +130,7 @@ public class SignerOrder {
             signerInfos[i].derEncode(strm);
             System.out.println("SignerInfo[" + i + "], length: "
                     + strm.toByteArray().length);
-            System.out.println(hexDump.encode(strm.toByteArray()));
+            HexPrinter.simple().format(strm.toByteArray());
             System.out.println("\n");
             strm.reset();
         }
diff --git a/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java b/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java
index 6e893cd03ce..6b8a47e4793 100644
--- a/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java
+++ b/test/jdk/sun/security/pkcs/pkcs8/PKCS8Test.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
  * @test
  * @bug 8048357
  * @summary PKCS8 Standards Conformance Tests
+ * @library /test/lib
  * @requires (os.family != "solaris")
  * @modules java.base/sun.security.pkcs
  *          java.base/sun.security.util
@@ -42,18 +43,16 @@ import java.io.IOException;
 import java.math.BigInteger;
 import java.security.InvalidKeyException;
 import java.util.Arrays;
-import sun.security.util.HexDumpEncoder;
 import sun.security.pkcs.PKCS8Key;
 import sun.security.provider.DSAPrivateKey;
 import sun.security.util.DerOutputStream;
 import sun.security.util.DerValue;
 import sun.security.x509.AlgorithmId;
+import jdk.test.lib.hexdump.HexPrinter;
 import static java.lang.System.out;
 
 public class PKCS8Test {
 
-    static final HexDumpEncoder hexDump = new HexDumpEncoder();
-
     static final DerOutputStream derOutput = new DerOutputStream();
 
     static final String FORMAT = "PKCS#8";
@@ -281,6 +280,6 @@ public class PKCS8Test {
 
     static void dumpByteArray(String nm, byte[] bytes) throws IOException {
         out.println(nm + " length: " + bytes.length);
-        hexDump.encodeBuffer(bytes, out);
+        HexPrinter.simple().dest(out).format(bytes);
     }
 }
diff --git a/test/jdk/sun/security/pkcs/pkcs9/UnknownAttribute.java b/test/jdk/sun/security/pkcs/pkcs9/UnknownAttribute.java
index 1bfdc21e688..4ffc96833e8 100644
--- a/test/jdk/sun/security/pkcs/pkcs9/UnknownAttribute.java
+++ b/test/jdk/sun/security/pkcs/pkcs9/UnknownAttribute.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,7 @@
  * @test
  * @bug 8011867
  * @summary Accept unknown PKCS #9 attributes
+ * @library /test/lib
  * @modules java.base/sun.security.pkcs
  *          java.base/sun.security.util
  */
@@ -32,10 +33,10 @@
 import java.io.*;
 import java.util.Arrays;
 
-import sun.security.util.HexDumpEncoder;
 import sun.security.pkcs.PKCS9Attribute;
 import sun.security.util.DerValue;
 import sun.security.util.ObjectIdentifier;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class UnknownAttribute {
 
@@ -58,7 +59,7 @@ public class UnknownAttribute {
         }
         ByteArrayOutputStream bout = new ByteArrayOutputStream();
         p2.derEncode(bout);
-        new HexDumpEncoder().encodeBuffer(bout.toByteArray(), System.err);
+        HexPrinter.simple().dest(System.err).format(bout.toByteArray());
         if (!Arrays.equals(data, bout.toByteArray())) {
             throw new Exception();
         }
diff --git a/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketKeyLimit.java b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketKeyLimit.java
index 3a31cd3e4a4..8d2912cefaa 100644
--- a/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketKeyLimit.java
+++ b/test/jdk/sun/security/ssl/SSLSocketImpl/SSLSocketKeyLimit.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -59,7 +59,7 @@ import java.util.Arrays;
 import jdk.test.lib.process.OutputAnalyzer;
 import jdk.test.lib.process.ProcessTools;
 import jdk.test.lib.Utils;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class SSLSocketKeyLimit {
     SSLSocket socket;
@@ -212,7 +212,7 @@ public class SSLSocketKeyLimit {
                     if (b == 0x0A || b == 0x0D) {
                         continue;
                     }
-                    System.out.println("\nData invalid: " + new HexDumpEncoder().encode(buf));
+                    System.out.println("\nData invalid: " + HexPrinter.minimal().toString(buf));
                     break;
                 }
 
diff --git a/test/jdk/sun/security/x509/X500Name/NullX500Name.java b/test/jdk/sun/security/x509/X500Name/NullX500Name.java
index fe0b1d3874c..37e12d8a892 100644
--- a/test/jdk/sun/security/x509/X500Name/NullX500Name.java
+++ b/test/jdk/sun/security/x509/X500Name/NullX500Name.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2020, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
 /* @test
  * @bug 4118818
  * @summary allow null X.500 Names
+ * @library /test/lib
  * @modules java.base/sun.security.util
  *          java.base/sun.security.x509
  */
@@ -31,7 +32,7 @@
 import java.util.Arrays;
 import sun.security.util.DerOutputStream;
 import sun.security.x509.*;
-import sun.security.util.HexDumpEncoder;
+import jdk.test.lib.hexdump.HexPrinter;
 
 public class NullX500Name {
 
@@ -63,16 +64,16 @@ public class NullX500Name {
         subject.encode(dos);
         byte[] out = dos.toByteArray();
         byte[] enc = subject.getEncoded();
-        HexDumpEncoder e = new HexDumpEncoder();
+        HexPrinter e = HexPrinter.simple();
         if (Arrays.equals(out, enc))
-            System.out.println("Sucess: out:" + e.encodeBuffer(out));
+            System.out.println("Success: out:" + e.toString(out));
         else {
-            System.out.println("Failed: encode:" + e.encodeBuffer(out));
-            System.out.println("getEncoded:" + e.encodeBuffer(enc));
+            System.out.println("Failed: encode:" + e.toString(out));
+            System.out.println("getEncoded:" + e.toString(enc));
         }
         X500Name x = new X500Name(enc);
         if (x.equals(subject))
-            System.out.println("Sucess: X500Name(byte[]):" + x.toString());
+            System.out.println("Success: X500Name(byte[]):" + x.toString());
         else
             System.out.println("Failed: X500Name(byte[]):" + x.toString());
     }
diff --git a/test/lib-test/TEST.ROOT b/test/lib-test/TEST.ROOT
new file mode 100644
index 00000000000..4d0f7cd2baa
--- /dev/null
+++ b/test/lib-test/TEST.ROOT
@@ -0,0 +1,9 @@
+# This file identifies the root of the test-suite hierarchy.
+# It also contains test-suite configuration information.
+
+# Minimum jtreg version
+requiredVersion=5.0 b1
+
+# Path to libraries in the topmost test directory. This is needed so @library
+# does not need ../../ notation to reach them
+external.lib.roots = ../../
diff --git a/test/lib-test/jdk/test/lib/hexdump/HexPrinterTest.java b/test/lib-test/jdk/test/lib/hexdump/HexPrinterTest.java
new file mode 100644
index 00000000000..bad85a1e624
--- /dev/null
+++ b/test/lib-test/jdk/test/lib/hexdump/HexPrinterTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.test.lib.hexdump;
+
+import jdk.test.lib.hexdump.HexPrinter;
+
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+
+/*
+ * @test
+ * @summary Check HexPrinter formatting
+ * @library /test/lib
+ * @compile HexPrinterTest.java
+ * @run testng jdk.test.lib.hexdump.HexPrinterTest
+ */
+public class HexPrinterTest {
+
+    @Test
+    static void testMinimalToStringByteArray() {
+        int len = 16;
+        byte[] bytes = genData(len);
+        StringBuilder expected = new StringBuilder(bytes.length * 2);
+        for (int i = 0; i < len; i++)
+            expected.append(String.format("%02x", bytes[i]));
+        String actual = HexPrinter.minimal().toString(bytes);
+        System.out.println(actual);
+        Assert.assertEquals(actual, expected.toString(), "Minimal format incorrect");
+    }
+
+    @DataProvider(name = "ColumnParams")
+    Object[][] columnParams() {
+        return new Object[][]{
+                {"%4d: ", "%d ", 10, " ; ", 50, HexPrinter.Formatters.PRINTABLE, "\n"},
+                {"%03o: ", "%d ", 16, " ; ", 50, HexPrinter.Formatters.ofPrimitive(byte.class, ""), "\n"},
+                {"%5d: ", "%02x:", 16, " ; ", 50, HexPrinter.Formatters.ofPrimitive(byte.class, ""), "\n"},
+                {"%5d: ", "%3d", 16, " ; ", 50, HexPrinter.Formatters.ofPrimitive(byte.class, ""), "\n"},
+                {"%05o: ", "%3o", 8, " ; ", 50, HexPrinter.Formatters.ofPrimitive(byte.class, ""), "\n"},
+                {"%6x: ", "%02x", 8, " | ", 50, HexPrinter.Formatters.ofPrimitive(byte.class, "%d "), "\n"},
+                {"%2x: ", "%02x", 8, " | ", 50, HexPrinter.Formatters.PRINTABLE, "\n"},
+                {"%5d: ", "%02x", 16, " | ", 50, HexPrinter.Formatters.ofPrimitive(short.class, "%d "), "\n"},
+        };
+    }
+
+    @DataProvider(name = "BuiltinParams")
+    Object[][] builtinParams() {
+        return new Object[][]{
+                {"minimal", "", "%02x", 16, "", 64, HexPrinter.Formatters.NONE, ""},
+                {"canonical", "%08x  ", "%02x ", 16, "|", 31, HexPrinter.Formatters.ASCII, "|\n"},
+                {"simple", "%5d: ", "%02x ", 16, " // ", 64, HexPrinter.Formatters.PRINTABLE, "\n"},
+                {"source", "    ", "(byte)%3d, ", 8, " // ", 64, HexPrinter.Formatters.PRINTABLE,
+                        "\n"},
+        };
+    }
+
+    @Test(dataProvider = "BuiltinParams")
+    public void testBuiltins(String name, String offsetFormat, String binFormat, int colWidth,
+                             String annoDelim, int annoWidth,
+                             HexPrinter.Formatter mapper, String lineSep) {
+        HexPrinter f = switch (name) {
+            case "minimal" -> HexPrinter.minimal();
+            case "simple" -> HexPrinter.simple();
+            case "canonical" -> HexPrinter.canonical();
+            case "source" -> HexPrinter.source();
+            default -> throw new IllegalStateException("Unexpected value: " + name);
+        };
+
+        testParams(f, offsetFormat, binFormat, colWidth, annoDelim, annoWidth, mapper, lineSep);
+
+        String actual = f.toString();
+        HexPrinter f2 = HexPrinter.simple()
+                .withOffsetFormat(offsetFormat)
+                .withBytesFormat(binFormat, colWidth)
+                .formatter(mapper, annoDelim, annoWidth)
+                .withLineSeparator(lineSep);
+        String expected = f2.toString();
+        Assert.assertEquals(actual, expected, "toString of " + name + " does not match");
+    }
+
+    @Test(dataProvider = "ColumnParams")
+    public void testToStringTwoLines(String offsetFormat, String binFormat, int colWidth,
+                                     String annoDelim, int annoWidth,
+                                     HexPrinter.Formatter mapper, String lineSep) {
+        HexPrinter f = HexPrinter.simple()
+                .withOffsetFormat(offsetFormat)
+                .withBytesFormat(binFormat, colWidth)
+                .formatter(mapper, annoDelim, annoWidth)
+                .withLineSeparator(lineSep);
+        testParams(f, offsetFormat, binFormat, colWidth, annoDelim, annoWidth, mapper, lineSep);
+    }
+
+    public static void testParams(HexPrinter printer, String offsetFormat, String binFormat, int colWidth,
+                                  String annoDelim, int annoWidth,
+                                  HexPrinter.Formatter mapper, String lineSep) {
+        byte[] bytes = genData(colWidth * 2);
+        System.out.println("Params: " + printer.toString());
+        String out = printer.toString(bytes);
+        System.out.println(out);
+
+        // Compare the actual output with the expected output of each formatting element
+        int padToWidth = colWidth * String.format(binFormat, 0xff).length();
+        int ndx = 0;
+        int valuesStart = 0;
+        int l;
+        for (int i = 0; i < bytes.length; i++) {
+            if (i % colWidth == 0) {
+                String offset = String.format(offsetFormat, i);
+                l = offset.length();
+                Assert.assertEquals(out.substring(ndx, ndx + l), offset,
+                        "offset format mismatch: " + ndx);
+                ndx += l;
+                valuesStart = ndx;
+            }
+            String value = String.format(binFormat, (0xff & bytes[i]));
+            l = value.length();
+            Assert.assertEquals(out.substring(ndx, ndx + l), value,
+                    "value format mismatch: " + ndx + ", i: " + i);
+            ndx += l;
+            if (((i + 1) % colWidth) == 0) {
+                // Rest of line is for padding, delimiter, formatter
+                String padding = " ".repeat(padToWidth - (ndx - valuesStart));
+                Assert.assertEquals(out.substring(ndx, ndx + padding.length()), padding, "padding");
+                ndx += padding.length();
+                Assert.assertEquals(out.substring(ndx, ndx + annoDelim.length()), annoDelim,
+                        "delimiter mismatch");
+                ndx += annoDelim.length();
+
+                // Formatter output is tested separately
+                ndx = out.indexOf(lineSep, ndx) + lineSep.length();
+            }
+        }
+    }
+
+    @DataProvider(name = "PrimitiveFormatters")
+    Object[][] formatterParams() {
+        return new Object[][]{
+                {byte.class, ""},
+                {byte.class, "%02x: "},
+                {short.class, "%d "},
+                {int.class, "%08x, "},
+                {long.class, "%16x "},
+                {float.class, "%3.4f "},
+                {double.class, "%6.3g "},
+                {boolean.class, "%b "},
+        };
+    }
+
+    @Test(dataProvider = "PrimitiveFormatters")
+    public void testFormatter(Class<?> primClass, String fmtString) {
+        HexPrinter.Formatter formatter = HexPrinter.Formatters.ofPrimitive(primClass, fmtString);
+        // Create a byte array with data for two lines
+        int colWidth = 8;
+        byte[] bytes = genData(colWidth);
+        StringBuilder sb = new StringBuilder();
+        DataInputStream in = new DataInputStream(new ByteArrayInputStream(bytes));
+        DataInputStream in2 = new DataInputStream(new ByteArrayInputStream(bytes));
+        try {
+            while (true) {
+                formatter.annotate(in, sb);
+                Object n = readPrimitive(primClass, in2);
+                String expected = String.format(fmtString, n);
+                Assert.assertEquals(sb.toString(), expected, "mismatch");
+                sb.setLength(0);
+            }
+        } catch (IOException ioe) {
+            // EOF is done
+        }
+        try {
+            Assert.assertEquals(in.available(), 0, "not all input consumed");
+            Assert.assertEquals(in2.available(), 0, "not all 2nd stream input consumed");
+        } catch (IOException ioe) {
+            //
+        }
+    }
+
+    @Test(dataProvider = "PrimitiveFormatters")
+    static void testHexPrinterPrimFormatter(Class<?> primClass, String fmtString) {
+        // Create a byte array with data for two lines
+        int colWidth = 8;
+        byte[] bytes = genData(colWidth);
+
+        HexPrinter p = HexPrinter.simple()
+                .formatter(primClass, fmtString);
+        String actual = p.toString(bytes);
+        System.out.println(actual);
+        // The formatter should produce the same output as using the formatter method
+        // with an explicit formatter for the primitive
+        String expected = HexPrinter.simple()
+                .formatter(HexPrinter.Formatters.ofPrimitive(primClass, fmtString))
+                .toString(bytes);
+        Assert.assertEquals(actual, expected, "mismatch");
+    }
+
+    private static Object readPrimitive(Class<?> primClass, DataInputStream in) throws IOException {
+        if (int.class.equals(primClass)) {
+            return in.readInt();
+        } else if (byte.class.equals(primClass)) {
+            return (int) in.readByte();
+        } else if (short.class.equals(primClass)) {
+            return in.readShort();
+        } else if (char.class.equals(primClass)) {
+            return in.readChar();
+        } else if (long.class.equals(primClass)) {
+            return in.readLong();
+        } else if (float.class.equals(primClass)) {
+            return in.readFloat();
+        } else if (double.class.equals(primClass)) {
+            return in.readDouble();
+        } else if (boolean.class.equals(primClass)) {
+            return in.readBoolean();
+        } else {
+            throw new RuntimeException("unknown primitive class: " + primClass);
+        }
+    }
+
+    @DataProvider(name = "sources")
+    Object[][] sources() {
+        return new Object[][]{
+                {genBytes(21), 0, -1},
+                {genBytes(21), 5, 12},
+        };
+    }
+
+    public static byte[] genData(int len) {
+        // Create a byte array with data for two lines
+        byte[] bytes = new byte[len];
+        for (int i = 0; i < len / 2; i++) {
+            bytes[i] = (byte) (i + 'A');
+            bytes[i + len / 2] = (byte) (i + 'A' + 128);
+        }
+        return bytes;
+    }
+
+    public static byte[] genFloat(int len) {
+        byte[] bytes = null;
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+             DataOutputStream out = new DataOutputStream(baos)) {
+            for (int i = 0; i < len; i++) {
+                out.writeFloat(i);
+            }
+            bytes = baos.toByteArray();
+        } catch (IOException unused) {
+        }
+        return bytes;
+    }
+
+    public static byte[] genDouble(int len) {
+        byte[] bytes = null;
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+             DataOutputStream out = new DataOutputStream(baos)) {
+            for (int i = 0; i < len; i++) {
+                out.writeDouble(i);
+            }
+            bytes = baos.toByteArray();
+        } catch (IOException unused) {
+        }
+        return bytes;
+    }
+
+    public static byte[] genBytes(int len) {
+        byte[] bytes = new byte[len];
+        for (int i = 0; i < len; i++)
+            bytes[i] = (byte) ('A' + i);
+        return bytes;
+    }
+
+    public ByteBuffer genByteBuffer(int len) {
+        return ByteBuffer.wrap(genBytes(len));
+    }
+
+    public InputStream genInputStream(int len) {
+        return new ByteArrayInputStream(genBytes(len));
+    }
+
+    @Test
+    public void testNilPrinterBigBuffer() {
+        byte[] bytes = new byte[1024];
+        HexPrinter p = HexPrinter.minimal();
+        String r = p.toString(bytes);
+        Assert.assertEquals(r.length(), bytes.length * 2, "encoded byte wrong size");
+        Assert.assertEquals(r.replace("00", "").length(), 0, "contents not all zeros");
+    }
+
+    @Test(dataProvider = "sources")
+    public void testToStringByteBuffer(byte[] bytes, int offset, int length) {
+        if (length < 0)
+            length = bytes.length - offset;
+        ByteBuffer bb = ByteBuffer.wrap(bytes, 0, bytes.length);
+        System.out.printf("Source: %s, off: %d, len: %d%n",
+                bytes.getClass().getName(), offset, length);
+        String actual;
+        if (offset == 0 && length < 0) {
+            bb.position(offset);
+            bb.limit(length);
+            actual = HexPrinter.simple().toString(bb);
+        } else
+            actual = HexPrinter.simple().toString(bb, offset, length);
+        System.out.println(actual);
+        String expected = HexPrinter.simple().toString(bytes, offset, length);
+        Assert.assertEquals(actual, expected, "mismatch in format()");
+    }
+
+    @Test(dataProvider = "sources")
+    public void testFormatBytes(byte[] bytes, int offset, int length) {
+        int len = length >= 0 ? length : bytes.length;
+        System.out.printf("Source: %s, off: %d, len: %d%n",
+                "bytes", offset, len);
+        StringBuilder sb = new StringBuilder();
+        if (offset == 0 && length < 0)
+            HexPrinter.simple().dest(sb).format(bytes);
+        else
+            HexPrinter.simple().dest(sb).format(bytes, offset, len);
+        String actual = sb.toString();
+        System.out.println(actual);
+        String expected = HexPrinter.simple().toString(bytes, offset, len);
+        Assert.assertEquals(actual, expected, "mismatch in format()");
+    }
+
+    @Test(dataProvider = "sources")
+    public void testFormatByteBuffer(byte[] bytes, int offset, int length) {
+        if (length < 0)
+            length = bytes.length - offset;
+        ByteBuffer bb = ByteBuffer.wrap(bytes, 0, bytes.length);
+        System.out.printf("Source: %s, off: %d, len: %d%n",
+                bytes.getClass().getName(), offset, length);
+        StringBuilder sb = new StringBuilder();
+        if (offset == 0 && length < 0) {
+            bb.position(offset);
+            bb.limit(length);
+            HexPrinter.simple().dest(sb).format(bb);
+        } else
+            HexPrinter.simple().dest(sb).format(bb, offset, length);
+        String actual = sb.toString();
+        System.out.println(actual);
+        String expected = HexPrinter.simple().toString(bytes, offset, length);
+        Assert.assertEquals(actual, expected, "mismatch in format()");
+    }
+
+    @Test(dataProvider = "sources")
+    public void testFormatInputStream(byte[] bytes, int offset, int length) {
+        // Offset is ignored
+        InputStream is = new ByteArrayInputStream(bytes, 0, length);
+        StringBuilder sb = new StringBuilder();
+        System.out.printf("Source: %s, off: %d, len: %d%n",
+                bytes.getClass().getName(), offset, length);
+        HexPrinter.simple().dest(sb).format(is);
+        String actual = sb.toString();
+        System.out.println(actual);
+        String expected = HexPrinter.simple().toString(bytes, 0, length);
+        Assert.assertEquals(actual, expected, "mismatch in format()");
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullByteArray() {
+        HexPrinter.simple().dest(System.out).format((byte[]) null);
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullByteArrayOff() {
+        HexPrinter.simple().dest(System.out).format((byte[]) null, 0, 1);
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullByteBuffer() {
+        HexPrinter.simple().dest(System.out).format((ByteBuffer) null);
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullByteBufferOff() {
+        HexPrinter.simple().dest(System.out).format((ByteBuffer) null, 0, 1);
+    }
+
+    @Test(expectedExceptions = NullPointerException.class)
+    public void testNullInputStream() {
+        HexPrinter.simple().dest(System.out).format((InputStream) null);
+    }
+
+}
diff --git a/test/lib/jdk/test/lib/hexdump/HexPrinter.java b/test/lib/jdk/test/lib/hexdump/HexPrinter.java
new file mode 100644
index 00000000000..91de0498e55
--- /dev/null
+++ b/test/lib/jdk/test/lib/hexdump/HexPrinter.java
@@ -0,0 +1,1181 @@
+/*
+ * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.test.lib.hexdump;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.CharArrayWriter;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * Decode a sequence of bytes to a readable format.
+ * <p>
+ * The predefined formats are:
+ * <DL>
+ * <DT>{@link #minimal() Minimal format}:  {@code "Now is the time for Java.\n"}</DT>
+ * <DD><pre>    {@code
+ *     4e6f77206973207468652074696d6520666f72204a6176612e0a} </pre>
+ * </DD>
+ *
+ * <DT>{@link #simple() Simple format}: {@code "Now is the time for Java.\n"}</DT>
+ * <DD><pre>{@code
+ *     0: 4e 6f 77 20 69 73 20 74 68 65 20 74 69 6d 65 20  // Now is the time for Java.\n
+ *    16: 66 6f 72 20 4a 61 76 61 2e 0a} </pre>
+ * </DD>
+ *
+ * <DT>{@link #canonical() Canonical format}: {@code "Now is the time for Java.\n"}</DT>
+ * <DD><pre>{@code
+ *     00000000  4e 6f 77 20 69 73 20 74 68 65 20 74 69 6d 65 20 |Now is the time |
+ *     00000010  66 6f 72 20 4a 61 76 61 2e 0a                   |for Java.|} </pre>
+ * </DD>
+ * <DT>{@link #source() Byte array initialization source}: {@code "Now is the time for Java.\n"}</DT>
+ * <DD><pre>{@code
+ *     (byte) 78, (byte)111, (byte)119, (byte) 32, (byte)105, (byte)115, (byte) 32, (byte)116,  // Now is t
+ *     (byte)104, (byte)101, (byte) 32, (byte)116, (byte)105, (byte)109, (byte)101, (byte) 32,  // he time
+ *     (byte)102, (byte)111, (byte)114, (byte) 32, (byte) 74, (byte) 97, (byte)118, (byte) 97,  // for Java
+ *     (byte) 46, (byte) 10,                                                                    // .\n}</pre>
+ * </DD>
+ * </DL>
+ * <p>
+ * The static factories {@link #minimal minimal}, {@link #simple simple},
+ * {@link #canonical canonical}, and {@link #source() Java source}
+ * return predefined {@code HexPrinter}s for the formats above.
+ * HexPrinter holds the formatting parameters that control the width and formatting
+ * of each of the offset, byte values, and formatted output.
+ * New HexPrinters with different parameters are created using an existing HexPrinter
+ * as a template with the methods {@link #formatter(Formatter)},
+ * {@link #withBytesFormat(String, int)}, {@link #withOffsetFormat(String)},
+ * and {@link #withLineSeparator(String)}.
+ * <p>
+ * The source of the bytes includes byte arrays, InputStreams, and ByteBuffers.
+ * For example, {@link #toString(InputStream)} reads the input and returns a String.
+ * Each of the {@code toString(...)} methods immediately reads and
+ * formats all of the bytes from the source and returns a String.
+ * <p>
+ * Each of the  {@code format(...)} methods immediately reads and
+ * formats all of the bytes from the source and appends it to the destination.
+ * For example, {@link #format(InputStream)} reads the input and
+ * appends the output to {@link System#out System.out} unless the
+ * {@link #dest(Appendable) destination} is changed to an {@link Appendable}
+ * such as {@link PrintStream}, {@link StringBuilder}, or {@link Writer}.
+ * <p>
+ * {@linkplain Formatter Formatter} functions read and interpret the bytes to show the
+ * structure and content of a protocol or data stream.
+ * Built-in formatters include {@link HexPrinter#formatter(Class, String) primitives},
+ * {@link Formatters#PRINTABLE printable ascii},
+ * and {@link Formatters#utf8Parser(DataInputStream, Appendable) UTF-8 strings}.
+ * The {@link #formatter(Formatter, String, int) formatter} method sets the
+ * formatting function, the delimiter, and the width.
+ * Custom formatter functions can be implemented as a lambda, a method, an inner class, or a concrete class.
+ * <p>
+ * The format of each line is customizable.
+ * The {@link #withOffsetFormat(String) withOffsetFormat} method controls
+ * the format of the byte offset.
+ * The {@link #withBytesFormat(String, int) withBytesFormat} method controls
+ * the printing of each byte value including the separator,
+ * and the maximum number of byte values per line.
+ * The offset and byte values are formatted using the familiar
+ * {@link String#format String formats} with spacing
+ * and delimiters included in the format string.
+ * The {@link #withLineSeparator(String) withLineSeparator} method sets
+ * the line separator.
+ * <p>
+ * Examples:
+ * <UL>
+ * <LI> Encoding bytes to a minimal string.
+ * <pre>{@code
+ * byte[] bytes = new byte[] { ' ', 0x41, 0x42, '\n'};
+ * String s = HexPrinter.minimal().toString(bytes);
+ * Result: "2041420a"
+ * }</pre>
+ * <LI>Simple formatting of a byte array.
+ * <pre>{@code
+ * byte[] bytes = new byte[] { ' ', 0x41, 0x42, '\n'};
+ * String s = HexPrinter.simple().toString(bytes);
+ * Result:    0: 20 41 42 0a                                      //  AB\n
+ * }</pre>
+ * <LI>Simple formatting of a ByteBuffer.
+ * <pre>{@code
+ * ByteBuffer bb = ByteBuffer.wrap(bytes);
+ * String s = HexPrinter.simple().toString(bb);
+ * Result:    0: 20 41 42 0a                                      //  AB\n
+ * }</pre>
+ * <LI>Simple formatting of ranges of a byte array to System.err.
+ * <pre>{@code
+ * byte[] bytes = new byte[] { ' ', 0x41, 0x42, 0x43, 0x44, '\n'};
+ * HexPrinter hex = HexPrinter.simple()
+ *                            .dest(System.err);
+ *                            .format(bytes, 1, 2)
+ *                            .format(bytes, 3, 2);
+ * Result:
+ * 1: 41 42                                            // AB
+ * 3: 43 44                                            // CD
+ * }</pre>
+ * </UL>
+ * <p>
+ * This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
+ * class; use of identity-sensitive operations (including reference equality
+ * ({@code ==}), identity hash code, or synchronization) on instances
+ * may have unpredictable results and should be avoided.
+ * The {@code equals} method should be used for comparisons.
+ *
+ * <p>
+ * This class is immutable and thread-safe.
+ */
+public final class HexPrinter {
+
+    /**
+     * Mnemonics for control characters.
+     */
+    static final String[] CONTROL_MNEMONICS = {
+            "nul", "soh", "stx", "etx", "eot", "enq", "ack", "bel",
+            "b", "t", "n", "vt", "f", "r", "so", "si",
+            "dle", "dc1", "dc2", "dc3", "dc4", "nak", "syn", "etb",
+            "can", "em", "sub", "esc", "fs", "gs", "rs", "us"
+    };
+    private static final String initOffsetFormat = "%5d: ";
+    private static final int initBytesCount = 16;   // 16 byte values
+    private static final String initBytesFormat = "%02x ";
+    private static final int initAnnoWidth = initBytesCount * 4;
+    private static final String initAnnoDelim = " // ";
+
+    final Appendable dest;              // Final output target
+    final String offsetFormat;          // Byte offset Formatter String
+    final String bytesFormat;           // Hex bytes Formatter string
+    final int bytesCount;               // Maximum number of byte values per line
+    final String annoDelim;             // Annotation delimiter
+    final int annoWidth;                // Annotation field width (characters)
+    final String lineSeparator;         // End of line separator
+    final Formatter annoFormatter;      // formatter function
+
+    /**
+     * Construct a new HexPrinter with all new values.
+     *
+     * @param printer       the formatter
+     * @param offsetFormat  the offset format
+     * @param bytesFormat   the bytes format
+     * @param bytesCount    the count of bytes per line
+     * @param annoDelim     the delimiter before the annotation
+     * @param annoWidth     the width of the annotation
+     * @param lineSeparator the line separator
+     * @param dest          the destination
+     */
+    private HexPrinter(Formatter printer, String offsetFormat, String bytesFormat, int bytesCount,
+                       String annoDelim, int annoWidth,
+                       String lineSeparator, Appendable dest) {
+        this.annoFormatter = Objects.requireNonNull(printer, "formatter");
+        this.bytesCount = bytesCount;
+        this.bytesFormat = Objects.requireNonNull(bytesFormat, bytesFormat);
+        this.offsetFormat = Objects.requireNonNull(offsetFormat, "offsetFormat");
+        this.annoDelim = Objects.requireNonNull(annoDelim, "annoDelim");
+        this.annoWidth = annoWidth;
+        this.lineSeparator = Objects.requireNonNull(lineSeparator, "lineSeparator");
+        this.dest = Objects.requireNonNull(dest, "dest");
+    }
+
+    /**
+     * Returns a new HexPrinter setting the parameters to produce a minimal string.
+     * The parameters are set to:
+     * <UL>
+     * <LI>byte offset format: none {@code ""},
+     * <LI>each byte value is formatted as 2 hex digits: {@code "%02x"},
+     * <LI>maximum number of byte values per line: unbounded,
+     * <LI>delimiter for the annotation: none {@code ""},
+     * <LI>formatter: {@link Formatters#NONE does not output a formatted byte}, and
+     * <LI>destination: {@link System#out System.out}.
+     * </UL>
+     * Example,
+     * <pre>
+     * {@code     byte[] bytes = new byte[] { ' ', 0x41, 0x42, '\n'};
+     *     String s = HexPrinter.minimal()
+     *             .toString(bytes);
+     *     Result: "2041420a"
+     * }</pre>
+     *
+     * @return a new HexPrinter
+     */
+    public static HexPrinter minimal() {
+        return new HexPrinter(Formatters.NONE, "",
+                "%02x", initBytesCount,
+                "", initAnnoWidth, "",
+                System.out);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the parameters to produce canonical output.
+     * The parameters are set to:
+     * <UL>
+     * <LI>byte offset format: {@code "%08x  "},
+     * <LI>each byte value is formatted as 2 hex digits and a space: {@code "%02x "},
+     * <LI>maximum number of byte values per line: {@value initBytesCount},
+     * <LI>delimiter for the annotation: {@code "|"},
+     * <LI>formatter: {@link Formatters#ASCII ASCII bytes}, and
+     * <LI>line separator: "|" + {@link  System#lineSeparator()},
+     * <LI>destination: {@link System#out System.out}.
+     * </UL>
+     * Example,
+     * <pre>
+     * {@code     byte[] bytes = new byte[] { ' ', 0x41, 0x42, '\n'};
+     *     String s = HexPrinter.canonical()
+     *             .toString(bytes);
+     *
+     *     Result: "00000000  20 41 42 0a                                     | AB|"
+     * }</pre>
+     *
+     * @return a new HexPrinter
+     */
+    public static HexPrinter canonical() {
+        return new HexPrinter(Formatters.ASCII, "%08x  ",
+                "%02x ", initBytesCount,
+                "|", 31, "|" + System.lineSeparator(),
+                System.out);
+    }
+
+    /**
+     * Returns a new HexPrinter setting simple formatting parameters to output
+     * to a multi-line string.
+     * The parameters are set to:
+     * <UL>
+     * <LI>byte offset format: signed decimal width 5 and a space, {@code "%5d: "},
+     * <LI>each byte value is formatted as 2 hex digits and a space: {@code "%02x "},
+     * <LI>maximum number of byte values per line: {@value initBytesCount},
+     * <LI>delimiter for the annotation: {@code " // "},
+     * <LI>width for the annotation: {@value initAnnoWidth},
+     * <LI>line separator: {@link System#lineSeparator()},
+     * <LI>formatter: {@link Formatters#PRINTABLE printable ASCII}
+     * showing printable characters, mnemonics for control chars, and
+     * otherwise the decimal byte values,
+     * <LI>destination default: {@link System#out System.out}.
+     * </UL>
+     * Example,
+     * <pre>
+     * {@code    byte[] bytes = new byte[] { ' ', 0x41, 0x42, '\n'};
+     *    String s = HexPrinter.simple()
+     *            .toString(bytes);
+     *
+     *    Result: "    0: 20 41 42 0a                                      //  AB\n"
+     * }</pre>
+     *
+     * @return a new HexPrinter
+     */
+    public static HexPrinter simple() {
+        return new HexPrinter(Formatters.PRINTABLE, initOffsetFormat,
+                initBytesFormat, initBytesCount,
+                initAnnoDelim, initAnnoWidth, System.lineSeparator(),
+                System.out);
+    }
+
+    /**
+     * Returns a new HexPrinter setting formatting parameters to output
+     * to a multi-line string as a byte array initialization for Java source.
+     * The parameters are set to:
+     * <UL>
+     * <LI>byte offset format: 4 space indent: {@code "    "},
+     * <LI>each byte value is formatted as: {@code "(byte)%3d, "},
+     * <LI>maximum number of byte values per line: {@code 8},
+     * <LI>delimiter for the annotation: {@code " // "},
+     * <LI>width for the annotation: {@value initAnnoWidth},
+     * <LI>line separator: {@link System#lineSeparator()},
+     * <LI>formatter: {@link Formatters#PRINTABLE printable ASCII}
+     * showing printable characters, mnemonics for control chars, and
+     * otherwise the decimal byte values,
+     * <LI>destination default: {@link System#out System.out}.
+     * </UL>
+     *
+     * @return a new HexPrinter
+     */
+    public static HexPrinter source() {
+        return new HexPrinter(Formatters.PRINTABLE, "    ",
+                "(byte)%3d, ", 8,
+                " // ", initAnnoWidth, System.lineSeparator(),
+                System.out);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the destination to the Appendable.
+     * {@code Appendable} classes include: {@link PrintStream}, {@link Writer},
+     * {@link StringBuilder}, and {@link StringBuffer}.
+     *
+     * @param dest the Appendable destination for the output, non-null
+     * @return a new HexPrinter
+     * @throws UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter dest(Appendable dest) {
+        Objects.requireNonNull(dest, "dest");
+        return new HexPrinter(annoFormatter, offsetFormat,
+                bytesFormat, bytesCount, annoDelim,
+                annoWidth, lineSeparator, dest);
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * and append the output.
+     * All output is appended and flushed to the destination.
+     * <p>
+     * The result is equivalent to calling
+     * {@code format(new ByteArrayInputStream(source))}.
+     *
+     * @param source a non-null array of bytes.
+     * @return this HexPrinter
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter format(byte[] source) {
+        Objects.requireNonNull(source, "byte array must be non-null");
+        return format(new ByteArrayInputStream(source));
+    }
+
+    /**
+     * The formatter function is called repeatedly to read the bytes from offset
+     * for length and append the output.
+     * All output is appended and flushed to the destination.
+     * Only {@code length} bytes starting at the {@code offset} are formatted.
+     * <p>
+     * The result is equivalent to calling
+     * {@code format(new ByteArrayInputStream(source, offset, len))}.
+     *
+     * @param source a non-null array of bytes.
+     * @param offset the offset into the array to start
+     * @param length the length of bytes in the array to format
+     * @return this HexPrinter
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter format(byte[] source, int offset, int length) {
+        Objects.requireNonNull(source, "byte array must be non-null");
+        return format(new ByteArrayInputStream(source, offset, length), offset);
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * and append the output.
+     * All output is appended and flushed to the destination.
+     * <p>
+     * The {@code format} method invokes the {@code formatter} to read bytes from the
+     * source and append the formatted sequence of byte values to the destination.
+     * As the bytes are read they are printed using the {@link #withBytesFormat}
+     * to fill the bytes values of the output destination.
+     * The output of the {@code formatter} fills the annotation field.
+     * A new line is started when either the byte values or annotation
+     * is filled to its respective width. The offset of the first byte on the line
+     * is inserted at the beginning of each line using {@link #withOffsetFormat(String)}.
+     * <p>
+     * This method may block indefinitely reading from the input stream,
+     * or writing to the output stream. The behavior for the case where
+     * the input and/or output stream is asynchronously closed,
+     * or the thread interrupted during the transfer, is highly input
+     * and output stream specific, and therefore not specified.
+     * <p>
+     * If an I/O error occurs reading from the input stream or
+     * writing to the output stream, then it may do so after some bytes
+     * have been read or written. Consequently the input stream
+     * may not be at end of stream and one, or both, streams may be
+     * in an inconsistent state. It is strongly recommended that both streams
+     * be promptly closed if an I/O error occurs.
+     *
+     * @param source an InputStream to read from, the stream not closed and
+     *               is at end-of-file.
+     * @return this HexPrinter
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter format(InputStream source) {
+        return format(source, 0);
+    }
+
+    /**
+     * Format an InputStream and supply the initial offset.
+     *
+     * @param source an InputStream
+     * @param offset an offset
+     * @return this HexPrinter
+     */
+    private HexPrinter format(InputStream source, int offset) {
+        Objects.requireNonNull(source, "InputStream must be non-null");
+        try (AnnotationWriter writer =
+                     new AnnotationWriter(this, source, offset, dest)) {
+            writer.flush();
+            return this;
+        }
+    }
+
+    /**
+     * The formatter function is called repeatedly to read the bytes
+     * from the offset for the length and append the output.
+     * All annotation output is appended and flushed to the output destination.
+     * The ByteBuffer position and limit are unused and not modified.
+     *
+     * @param source a ByteBuffer
+     * @param offset the offset in the ByteBuffer
+     * @param length the length in the ByteBuffer
+     * @return this HexPrinter
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter format(ByteBuffer source, int offset, int length) {
+        Objects.requireNonNull(source, "ByteBuffer must be non-null");
+        ByteArrayInputStream bais;
+        if (source.hasArray() && !source.isReadOnly()) {
+            bais = new ByteArrayInputStream(source.array(), offset, length);
+        } else {
+            int size = source.limit() - source.position();
+            byte[] bytes = new byte[size];
+            source.get(bytes, offset, length);
+            bais = new ByteArrayInputStream(bytes);
+        }
+        return format(bais, offset);
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * in the source and append the output.
+     * The source bytes are from the {@code ByteBuffer.position()}
+     * to the {@code ByteBuffer.limit()}.
+     * The position is not modified.
+     * All output is appended and flushed to the destination.
+     *
+     * @param source a ByteBuffer
+     * @return this HexPrinter
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public HexPrinter format(ByteBuffer source) {
+        return format(source, source.position(), source.limit());
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * and return a String.
+     *
+     * @param source a non-null array of bytes.
+     * @return the output as a non-null {@code String}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public String toString(byte[] source) {
+        Objects.requireNonNull(source, "byte array must be non-null");
+        return toString(new ByteArrayInputStream(source));
+    }
+
+    /**
+     * The formatter function is called repeatedly to read the bytes from offset
+     * for length and return a String.
+     * Only {@code length} bytes starting at the {@code offset} are formatted.
+     *
+     * @param source a non-null array of bytes.
+     * @param offset the offset into the array to start
+     * @param length the length of bytes in the array to format
+     * @return the output as a non-null {@code String}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public String toString(byte[] source, int offset, int length) {
+        Objects.requireNonNull(source, "byte array must be non-null");
+        StringBuilder sb = new StringBuilder();
+        try (AnnotationWriter writer =
+                     new AnnotationWriter(this, new ByteArrayInputStream(source, offset, length),
+                             offset, sb)) {
+            writer.flush();
+            return sb.toString();
+        }
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * and return a String.
+     * <p>
+     * The {@code toString} method invokes the formatter to read bytes from the
+     * source and append the formatted sequence of byte values.
+     * As the bytes are read they are printed using the {@link #withBytesFormat}
+     * to fill the second field of the line.
+     * The output of the {@code formatter} fills the annotation field.
+     * A new line is started when either the byte values or annotation
+     * is filled to its respective width. The offset of the first byte on the line
+     * is inserted at the beginning of each line using {@link #withOffsetFormat(String)}.
+     * <p>
+     * This method may block indefinitely reading from the input stream,
+     * or writing to the output stream. The behavior for the case where
+     * the input and/or output stream is asynchronously closed,
+     * or the thread interrupted during the transfer, is highly input
+     * and output stream specific, and therefore not specified.
+     * <p>
+     * If an I/O error occurs reading from the input stream or
+     * writing to the output stream, then it may do so after some bytes
+     * have been read or written. Consequently the input stream
+     * may not be at end of stream and one, or both, streams may be
+     * in an inconsistent state. It is strongly recommended that both streams
+     * be promptly closed if an I/O error occurs.
+     *
+     * @param source an InputStream to read from, the stream not closed and
+     *               is at end-of-file upon return.
+     * @return the output as a non-null {@code String}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public String toString(InputStream source) {
+        Objects.requireNonNull(source, "InputStream must be non-null");
+        StringBuilder sb = new StringBuilder();
+        try (AnnotationWriter writer =
+                     new AnnotationWriter(this, source, 0, sb)) {
+            writer.flush();
+            return sb.toString();
+        }
+    }
+
+    /**
+     * The formatter function is called repeatedly to read the bytes
+     * from the offset for the length and return a String.
+     * The ByteBuffer position and limit are unused and not modified.
+     *
+     * @param source a ByteBuffer
+     * @param offset the offset in the ByteBuffer
+     * @param length the length in the ByteBuffer
+     * @return the output as a non-null {@code String}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public String toString(ByteBuffer source, int offset, int length) {
+        Objects.requireNonNull(source, "ByteBuffer must be non-null");
+        StringBuilder sb = new StringBuilder();
+        ByteArrayInputStream bais;
+        if (source.hasArray() && !source.isReadOnly()) {
+            bais = new ByteArrayInputStream(source.array(), offset, length);
+        } else {
+            byte[] bytes = new byte[length];
+            source.get(bytes, offset, length);
+            bais = new ByteArrayInputStream(bytes);
+        }
+        try (AnnotationWriter writer =
+                     new AnnotationWriter(this, bais, offset, sb)) {
+            writer.flush();
+            return sb.toString();
+        }
+    }
+
+    /**
+     * The formatter function is called repeatedly to read all of the bytes
+     * in the source and return a String.
+     * The source bytes are from the {@code ByteBuffer.position()}
+     * to the {@code ByteBuffer.limit()}.
+     * The position is not modified.
+     *
+     * @param source a ByteBuffer
+     * @return the output as a non-null {@code String}
+     * @throws java.io.UncheckedIOException if an I/O error occurs
+     */
+    public String toString(ByteBuffer source) {
+        return toString(source, source.position(), source.limit());
+    }
+
+    /**
+     * Returns a new HexPrinter setting the format for the byte offset.
+     * The format string is specified by {@link String#format String format}
+     * including any delimiters. For example, {@code "%3x: "}.
+     * If the format is an empty string, there is no offset in the output.
+     *
+     * @param offsetFormat a new format string for the byte offset.
+     * @return a new HexPrinter
+     */
+    public HexPrinter withOffsetFormat(String offsetFormat) {
+        Objects.requireNonNull(offsetFormat, "offsetFormat");
+        return new HexPrinter(annoFormatter, offsetFormat, bytesFormat, bytesCount,
+                annoDelim, annoWidth, lineSeparator, dest);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the format for each byte value and
+     * the maximum number of byte values per line.
+     * The format string is specified by {@link String#format String format},
+     * including any delimiters or padding. For example, {@code "%02x "}.
+     * If the byteFormat is an empty String, there are no byte values in the output.
+     *
+     * @param byteFormat a format string for each byte
+     * @param bytesCount the maximum number of byte values per line
+     * @return a new HexPrinter
+     */
+    public HexPrinter withBytesFormat(String byteFormat, int bytesCount) {
+        Objects.requireNonNull(bytesFormat, "bytesFormat");
+        return new HexPrinter(annoFormatter, offsetFormat, byteFormat, bytesCount,
+                annoDelim, annoWidth, lineSeparator, dest);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the line separator.
+     * The line separator can be set to an empty string or to
+     * a string to be added at the end of each line.  It should include the line
+     * separator {@link System#lineSeparator()} if a line break is to be output.
+     *
+     * @param separator the line separator
+     * @return a new HexPrinter
+     */
+    public HexPrinter withLineSeparator(String separator) {
+        return new HexPrinter(annoFormatter, offsetFormat, bytesFormat, bytesCount,
+                annoDelim, annoWidth, separator, dest);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the formatter.
+     * The widths, delimiters and other parameters are unchanged.
+     *
+     * @param formatter a non-null Formatter
+     * @return a new HexPrinter
+     */
+    public HexPrinter formatter(Formatter formatter) {
+        Objects.requireNonNull(formatter, "Formatter must be non-null");
+        return new HexPrinter(formatter, offsetFormat, bytesFormat, bytesCount,
+                annoDelim, annoWidth, lineSeparator, dest);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the formatter, delimiter, and width of the annotation.
+     * Note: The annotations may exceed the width.
+     *
+     * @param formatter a non-null Formatter
+     * @param delim     a string delimiter for the annotation
+     * @param width     the width of the annotation, non-negative
+     * @return a new HexPrinter
+     */
+    public HexPrinter formatter(Formatter formatter, String delim, int width) {
+        Objects.requireNonNull(formatter, "formatter");
+        Objects.requireNonNull(delim, "delim");
+        return new HexPrinter(formatter, offsetFormat, bytesFormat, bytesCount,
+                delim, width, lineSeparator, dest);
+    }
+
+    /**
+     * Returns a new HexPrinter setting the formatter to format a primitive type
+     * using the format string.
+     * The format string should include any pre or post spacing and delimiters.
+     * <p>
+     * This is a convenience function equivalent to finding a formatter using
+     * {@link HexPrinter.Formatters#ofPrimitive}.
+     * </p>
+     *
+     * @param primClass a primitive class, for example, {@code int.class}
+     * @param fmtString a {@link java.util.Formatter format string}.
+     * @return a new HexPrinter
+     * @throws IllegalArgumentException if the class is not a primitive class
+     */
+    public HexPrinter formatter(Class<?> primClass, String fmtString) {
+        Formatter formatter = getFormatter(primClass, fmtString);
+        return new HexPrinter(formatter, offsetFormat, bytesFormat, bytesCount,
+                annoDelim, annoWidth, lineSeparator, dest);
+    }
+
+    /**
+     * Returns a formatter for the primitive type using the format string.
+     * The formatter reads a value of the primitive type from the stream
+     * and formats it using the format string.
+     * The format string includes any pre or post spacing and delimiters.
+     *
+     * @param primClass a primitive class, for example, {@code int.class}
+     * @param fmtString a {@link java.util.Formatter format string}
+     * @return a Formatter for the primitive type using the format string
+     */
+    static Formatter getFormatter(Class<?> primClass, String fmtString) {
+        return new PrimitiveFormatter(primClass, fmtString);
+    }
+
+    /**
+     * Returns a string describing this HexPrinter.
+     * The string indicates the type of the destination and
+     * the formatting options.
+     *
+     * @return a String describing this HexPrinter
+     */
+    public String toString() {
+        return "formatter: " + annoFormatter
+                + ", dest: " + dest.getClass().getName()
+                + ", offset: \"" + offsetFormat
+                + "\", bytes: " + bytesCount
+                + " x \"" + bytesFormat + "\""
+                + ", delim: \"" + annoDelim + "\""
+                + ", width: " + annoWidth
+                + ", nl: \"" + expand(lineSeparator) + "\"";
+    }
+
+    private String expand(String sep) {
+        return sep.replace("\n", "\\n")
+                .replace("\r", "\\r");
+    }
+
+    private static class PrimitiveFormatter implements Formatter {
+
+        private final Class<?> primClass;
+        private final String fmtString;
+
+        PrimitiveFormatter(Class<?> primClass, String fmtString) {
+            Objects.requireNonNull(primClass, "primClass");
+            Objects.requireNonNull(fmtString, "fmtString");
+            if (!primClass.isPrimitive())
+                throw new IllegalArgumentException("Not a primitive type: " + primClass.getName());
+            this.primClass = primClass;
+            this.fmtString = fmtString;
+        }
+
+        public void annotate(DataInputStream in, Appendable out) throws IOException {
+            if (primClass == byte.class) {
+                int v = in.readByte();
+                out.append(String.format(fmtString, v));
+            } else if (primClass == boolean.class) {
+                boolean v = in.readByte() != 0;
+                out.append(String.format(fmtString, v));
+            } else if (primClass == short.class | primClass == char.class) {
+                int v = in.readShort();
+                out.append(String.format(fmtString, v));
+            } else if (primClass == float.class) {
+                float v = in.readFloat();
+                out.append(String.format(fmtString, v));
+            } else if (primClass == int.class) {
+                int v = in.readInt();
+                out.append(String.format(fmtString, v));
+            } else if (primClass == double.class) {
+                double v = in.readDouble();
+                out.append(String.format(fmtString, v));
+            } else if (primClass == long.class) {
+                long v = in.readLong();
+                out.append(String.format(fmtString, v));
+            } else {
+                throw new AssertionError("missing case on primitive class");
+            }
+        }
+
+        public String toString() {
+            return "(" + primClass.getName() + ", \"" + fmtString + "\")";
+        }
+    }
+
+    /**
+     * Formatter function reads bytes from a stream and
+     * appends a readable annotation to the output destination.
+     * <p>
+     * Each invocation of the {@link #annotate annotate} method reads and annotates
+     * a single instance of its protocol or data type.
+     * <p>
+     * Built-in formatting functions are provided in the {@link Formatters} class.
+     * <p>
+     * As described by the {@link HexPrinter#toString(InputStream)} method,
+     * the {@link #annotate annotate} method is called to read bytes and produce
+     * the descriptive annotation.
+     * <p>
+     * For example, a custom lambda formatter to read a float value (4 bytes) and
+     * print as a floating number could be written as a static method.
+     * <pre>{@code
+     *     // Format 4 bytes read from the input as a float 3.4.
+     *     static void annotate(DataInputStream in, Appendable out) throws IOException {
+     *         float f = in.readFloat();
+     *         out.append(String.format("%3.4f, ", f));
+     *     }
+     *
+     *     byte[] bytes = new byte[] {00 00 00 00 3f 80 00 00 40 00 00 00 40 40 00 00};
+     *     HexPrinter pp = HexPrinter.simple()
+     *         .withBytesFormat("%02x ", 8)
+     *         .formatter(Example::annotate)
+     *         .format(bytes);
+     *
+     * Result:
+     *     0: 00 00 00 00 3f 80 00 00  // 0.0000, 1.0000,
+     *     8: 40 00 00 00 40 40 00 00  // 2.0000, 3.0000,
+     * }</pre>
+     *
+     * <p>
+     * The details of the buffering and calling of the formatter {@code annotate}
+     * methods is roughly as follows.
+     * The bytes read by the {@code annotate} method are logically buffered
+     * for each line of output.
+     * The {@code annotate} method writes its description of the bytes read
+     * to the output, this output is also buffered.
+     * When the number of bytes read exceeds the
+     * {@link #withBytesFormat(String, int) byte values count per line},
+     * the buffered output exceeds the
+     * {@link #formatter(Formatter, String, int) width of the annotation field},
+     * or a new line {@code "\n"} character is found in the output then
+     * a line of output is assembled and written to the destination Appendable.
+     * The formatter's {@code annotate} method is called repeatedly
+     * until the input is completely consumed or an exception is thrown.
+     * Any remaining buffered bytes or description are flushed to the destination Appendable.
+     */
+    @FunctionalInterface
+    public interface Formatter {
+
+        /**
+         * Read bytes from the input stream and append a descriptive annotation
+         * to the output destination.
+         *
+         * @param in  a DataInputStream
+         * @param out an Appendable for the output
+         * @throws IOException if an I/O error occurs
+         */
+        void annotate(DataInputStream in, Appendable out) throws IOException;
+    }
+
+    /**
+     * Built-in formatters for printable byte, ASCII, UTF-8 and primitive types.
+     * Formatters for primitive types and different formatting options
+     * can be found by calling {@link #ofPrimitive(Class, String)}.
+     */
+    public enum Formatters implements Formatter {
+        /**
+         * Read a byte and if it is ASCII write it,
+         * otherwise, write its mnemonic or its decimal value.
+         */
+        PRINTABLE,
+        /**
+         * Read a byte, if it is ASCII write it, otherwise write a ".".
+         */
+        ASCII,
+        /**
+         * Read a modified UTF-8 string and write it.
+         */
+        UTF8,
+        /**
+         * Read a byte and write nothing.
+         */
+        NONE;
+
+        public void annotate(DataInputStream in, Appendable out) throws IOException {
+            switch (this) {
+                case PRINTABLE -> bytePrintable(in, out);
+                case ASCII -> byteASCII(in, out);
+                case UTF8 -> utf8Parser(in, out);
+                case NONE -> byteNoneParser(in, out);
+            }
+        }
+
+        /**
+         * Read a byte and write it as ASCII if it is printable,
+         * print its mnemonic if it is a control character,
+         * and print its decimal value otherwise.
+         * A space separator character is appended for control and decimal values.
+         *
+         * @param in  a DataInputStream
+         * @param out an Appendable to write to
+         * @throws IOException if an I/O error occurs
+         */
+        static void bytePrintable(DataInputStream in, Appendable out) throws IOException {
+            int v = in.readUnsignedByte();
+            if (v < 32) {
+                out.append("\\").append(CONTROL_MNEMONICS[v]);
+            } else if (v < 126 && Character.isDefined(v)) {
+                out.append((char) v);
+            } else {
+                out.append("\\").append(Integer.toString(v, 10));
+            }
+        }
+
+        /**
+         * Read a byte and write it as ASCII if it is printable, otherwise print ".".
+         *
+         * @param in  a DataInputStream
+         * @param out an Appendable to write to
+         * @throws IOException if an I/O error occurs
+         */
+        static void byteASCII(DataInputStream in, Appendable out) throws IOException {
+            int v = in.readUnsignedByte();
+            if (Character.isDefined(v)) {
+                out.append((char) v);
+            } else {
+                out.append('.');
+            }
+        }
+
+        /**
+         * Read a modified UTF-8 string and write it to the output destination.
+         *
+         * @param in  a DataInputStream
+         * @param out an Appendable to write the output to
+         * @throws IOException if an I/O error occurs
+         */
+        static void utf8Parser(DataInputStream in, Appendable out) throws IOException {
+            out.append(in.readUTF()).append(" ");
+        }
+
+        /**
+         * Read a a byte and write nothing.
+         *
+         * @param in  a DataInputStream
+         * @param out an Appendable to write the output to
+         * @throws IOException if an I/O error occurs
+         */
+        static void byteNoneParser(DataInputStream in, Appendable out) throws IOException {
+            in.readByte();
+        }
+
+        /**
+         * Returns a {@code Formatter} for a primitive using the format string.
+         * The format string includes any pre or post spacing or delimiters.
+         * A value of the primitive is read using the type specific methods
+         * of {@link DataInputStream}, formatted using the format string, and
+         * written to the output.
+         *
+         * @param primClass a primitive class, for example, {@code int.class}
+         * @param fmtString a {@link java.util.Formatter format string}.
+         * @return a Formatter
+         */
+        public static Formatter ofPrimitive(Class<?> primClass, String fmtString) {
+            Objects.requireNonNull(primClass, "primClass");
+            Objects.requireNonNull(fmtString, "fmtString");
+            return new PrimitiveFormatter(primClass, fmtString);
+        }
+    }
+
+    /**
+     * Internal implementation of the annotation output and processor of annotated output.
+     * Created for each new input source and discarded after each use.
+     * An OffsetInputStream is created to buffer and count the input bytes.
+     *
+     */
+    private static final class AnnotationWriter extends CharArrayWriter {
+        private final transient OffsetInputStream source;
+        private final transient DataInputStream in;
+        private final transient int baseOffset;
+        private final transient HexPrinter params;
+        private final transient int bytesColWidth;
+        private final transient int annoWidth;
+        private final transient Appendable dest;
+
+        /**
+         * Construct a new AnnotationWriter to process the source into the destination.
+         * Initializes the DataInputStream and marking of the input to keep track
+         * of bytes as they are read by the formatter.
+         * @param params formatting parameters
+         * @param source source InputStream
+         * @param baseOffset initial offset
+         * @param dest destination Appendable
+         */
+        AnnotationWriter(HexPrinter params, InputStream source, int baseOffset, Appendable dest) {
+            this.params = params;
+            this.baseOffset = baseOffset;
+            Objects.requireNonNull(source, "Source is null");
+            this.source = new OffsetInputStream(source);
+            this.source.mark(1024);
+            this.in = new DataInputStream(this.source);
+            this.bytesColWidth = params.bytesCount * String.format(params.bytesFormat, 255).length();
+            this.annoWidth = params.annoWidth;
+            this.dest = dest;
+        }
+
+        @Override
+        public void write(int c) {
+            super.write(c);
+            checkFlush();
+        }
+
+        @Override
+        public void write(char[] c, int off, int len) {
+            super.write(c, off, len);
+            for (int i = 0; i < len; i++) {
+                if (c[off+i] == '\n') {
+                    process();
+                    return;
+                }
+            }
+            checkFlush();
+        }
+
+        @Override
+        public void write(String str, int off, int len) {
+            super.write(str, off, len);
+            if (str.indexOf('\n') >=0 )
+                process();
+            else
+                checkFlush();
+        }
+
+        private void checkFlush() {
+            if (size() > annoWidth)
+                process();
+        }
+
+        /**
+         * The annotation printing function is called repeatedly to read all of the bytes
+         * in the source stream and annotate the stream.
+         * The annotated output is appended to the output dest or buffered.
+         * <p>
+         *     The HexPrinter is not closed and can be used as a template
+         *     to create a new formatter with a new Source or different formatting
+         *     options.
+         * </p>
+         */
+        @Override
+        public void flush() {
+            try {
+                while (true) {
+                    if (source.markedByteCount() >= params.bytesCount)
+                        process();
+                    params.annoFormatter.annotate(in, this);
+                    if (source.markedByteCount() > 256) {
+                        // Normally annotations would cause processing more often
+                        // Guard against overrunning the mark/reset buffer.
+                        process();
+                    }
+                }
+            } catch (IOException ioe) {
+                process();
+                if (!(ioe instanceof EOFException)) {
+                    throw new UncheckedIOException(ioe);
+                }
+            } catch (UncheckedIOException uio) {
+                process();      // clear out the buffers
+                throw uio;
+            }
+        }
+
+        /**
+         * Merge the buffered stream of annotations with the formatted bytes
+         * and append them to the dest.
+         * <p>
+         * The annotation mapping function has read some bytes and buffered
+         * some output that corresponds to those bytes.
+         * The un-formatted bytes are in the OffsetInputStream after the mark.
+         * The stream is reset and the bytes are read again.
+         * Each line of the produced one line at a time to the dest.
+         * The byte offset is formatted according to the offsetFormat.
+         * The bytes after the mark are read and formatted using the bytesFormat
+         * and written to the dest up to the bytesWidth.
+         * The annotation stream is appended to the dest, but only up to the
+         * first newline (if any). The alignment between the annotated stream
+         * and the formatted bytes is approximate.
+         * New line characters in the annotation cause a new line to be started
+         * without regard to the number of formatted bytes. The column of formatted
+         * bytes may be incomplete.
+         */
+        private void process() {
+            String info = toString();
+            reset();
+            int count = source.markedByteCount();
+            try {
+                source.reset();
+                long binColOffset = source.byteOffset();
+                while (count > 0 || info.length() > 0) {
+                    dest.append(String.format(params.offsetFormat, binColOffset + baseOffset));
+                    int colWidth = 0;
+                    int byteCount = Math.min(params.bytesCount, count);
+                    for (int i = 0; i < byteCount; i++) {
+                        int b = source.read();
+                        if (b == -1)
+                            throw new IllegalStateException("BUG");
+                        String s = String.format(params.bytesFormat, b);
+                        colWidth += s.length();
+                        dest.append(s);
+                    }
+                    binColOffset += byteCount;
+                    count -= byteCount;
+
+                    // Pad out the bytes column to its width
+                    dest.append(" ".repeat(Math.max(0, bytesColWidth - colWidth)));
+                    dest.append(params.annoDelim);
+
+                    // finish a line and prepare for next line
+                    // Add a line from annotation buffer
+                    if (info.length() > 0) {
+                        int nl = info.indexOf('\n');
+                        if (nl < 0) {
+                            dest.append(info);
+                            info = "";
+                        } else {
+                            dest.append(info, 0, nl);
+                            info = info.substring(nl + 1);
+                        }
+                    }
+                    dest.append(params.lineSeparator);
+                }
+            } catch (IOException ioe) {
+                try {
+                    dest.append("\nIOException during annotations: ")
+                        .append(ioe.getMessage())
+                        .append("\n");
+                } catch (IOException ignore) {
+                    // ignore
+                }
+            }
+            // reset the mark for the next line
+            source.mark(1024);
+        }
+    }
+
+    /**
+     * Buffered InputStream that keeps track of byte offset.
+     */
+    private static final class OffsetInputStream extends BufferedInputStream {
+        private long byteOffset;
+        private long markByteOffset;
+
+        OffsetInputStream(InputStream in) {
+            super(in);
+            byteOffset = 0;
+            markByteOffset = 0;
+        }
+
+        long byteOffset() {
+            return byteOffset;
+        }
+
+        @Override
+        public void reset() throws IOException {
+            super.reset();
+            byteOffset = markByteOffset;
+        }
+
+        @Override
+        public synchronized void mark(int readlimit) {
+            super.mark(readlimit);
+            markByteOffset = byteOffset;
+        }
+
+        int markedByteCount() {
+            if (markpos < 0)
+                return 0;
+            return pos - markpos;
+        }
+
+        @Override
+        public int read() throws IOException {
+            int b = super.read();
+            if (b >= 0)
+                byteOffset++;
+            return b;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            long size = super.skip(n);
+            byteOffset += size;
+            return size;
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            int size = super.read(b);
+            byteOffset += Math.max(size, 0);
+            return size;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int size = super.read(b, off, len);
+            byteOffset += Math.max(size, 0);
+            return size;
+        }
+    }
+}