--- old/src/java.base/share/classes/sun/security/ssl/Debug.java 2018-05-11 15:10:12.347930100 -0700 +++ /dev/null 2018-05-11 10:42:23.849000000 -0700 @@ -1,250 +0,0 @@ -/* - * Copyright (c) 1999, 2016, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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 sun.security.ssl; - -import java.io.PrintStream; -import java.util.Locale; - -import sun.security.util.HexDumpEncoder; -import java.nio.ByteBuffer; - -import sun.security.action.GetPropertyAction; - -/** - * This class has be shamefully lifted from sun.security.util.Debug - * - * @author Gary Ellison - */ -public class Debug { - - private String prefix; - - private static String args; - - static { - args = GetPropertyAction.privilegedGetProperty("javax.net.debug", ""); - args = args.toLowerCase(Locale.ENGLISH); - if (args.equals("help")) { - Help(); - } - } - - public static void Help() - { - System.err.println(); - System.err.println("all turn on all debugging"); - System.err.println("ssl turn on ssl debugging"); - System.err.println(); - System.err.println("The following can be used with ssl:"); - System.err.println("\trecord enable per-record tracing"); - System.err.println("\thandshake print each handshake message"); - System.err.println("\tkeygen print key generation data"); - System.err.println("\tsession print session activity"); - System.err.println("\tdefaultctx print default SSL initialization"); - System.err.println("\tsslctx print SSLContext tracing"); - System.err.println("\tsessioncache print session cache tracing"); - System.err.println("\tkeymanager print key manager tracing"); - System.err.println("\ttrustmanager print trust manager tracing"); - System.err.println("\tpluggability print pluggability tracing"); - System.err.println(); - System.err.println("\thandshake debugging can be widened with:"); - System.err.println("\tdata hex dump of each handshake message"); - System.err.println("\tverbose verbose handshake message printing"); - System.err.println(); - System.err.println("\trecord debugging can be widened with:"); - System.err.println("\tplaintext hex dump of record plaintext"); - System.err.println("\tpacket print raw SSL/TLS packets"); - System.err.println(); - System.exit(0); - } - - /** - * Get a Debug object corresponding to whether or not the given - * option is set. Set the prefix to be the same as option. - */ - - public static Debug getInstance(String option) - { - return getInstance(option, option); - } - - /** - * Get a Debug object corresponding to whether or not the given - * option is set. Set the prefix to be prefix. - */ - public static Debug getInstance(String option, String prefix) - { - if (isOn(option)) { - Debug d = new Debug(); - d.prefix = prefix; - return d; - } else { - return null; - } - } - - /** - * True if the property "javax.net.debug" contains the - * string "option". - */ - public static boolean isOn(String option) - { - if (args == null) { - return false; - } else { - int n = 0; - option = option.toLowerCase(Locale.ENGLISH); - - if (args.indexOf("all") != -1) { - return true; - } else if ((n = args.indexOf("ssl")) != -1) { - if (args.indexOf("sslctx", n) == -1) { - // don't enable data and plaintext options by default - if (!(option.equals("data") - || option.equals("packet") - || option.equals("plaintext"))) { - return true; - } - } - } - return (args.indexOf(option) != -1); - } - } - - /** - * print a message to stderr that is prefixed with the prefix - * created from the call to getInstance. - */ - - public void println(String message) - { - System.err.println(prefix + ": "+message); - } - - /** - * Print a message to stdout. - */ - static void log(String message) { - System.out.println(Thread.currentThread().getName() + ": " + message); - } - - /** - * print a blank line to stderr that is prefixed with the prefix. - */ - - public void println() - { - System.err.println(prefix + ":"); - } - - /** - * print a message to stderr that is prefixed with the prefix. - */ - public static void println(String prefix, String message) - { - System.err.println(prefix + ": "+message); - } - - public static void println(PrintStream s, String name, byte[] data) { - s.print(name + ": { "); - if (data == null) { - s.print("null"); - } else { - for (int i = 0; i < data.length; i++) { - if (i != 0) s.print(", "); - s.print(data[i] & 0x0ff); - } - } - s.println(" }"); - } - - /** - * Return the value of the boolean System property propName. - * - * Note use of privileged action. Do NOT make accessible to applications. - */ - static boolean getBooleanProperty(String propName, boolean defaultValue) { - // if set, require value of either true or false - String b = GetPropertyAction.privilegedGetProperty(propName); - if (b == null) { - return defaultValue; - } else if (b.equalsIgnoreCase("false")) { - return false; - } else if (b.equalsIgnoreCase("true")) { - return true; - } else { - throw new RuntimeException("Value of " + propName - + " must either be 'true' or 'false'"); - } - } - - static String toString(byte[] b) { - return sun.security.util.Debug.toString(b); - } - - static void printHex(String prefix, byte[] bytes) { - HexDumpEncoder dump = new HexDumpEncoder(); - - synchronized (System.out) { - System.out.println(prefix); - try { - dump.encodeBuffer(bytes, System.out); - } catch (Exception e) { - // ignore - } - System.out.flush(); - } - } - - 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); - } catch (Exception e) { - // ignore - } - System.out.flush(); - } - } - - 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); - } catch (Exception e) { - // ignore - } - System.out.flush(); - } - } -} --- /dev/null 2018-05-11 10:42:23.849000000 -0700 +++ new/src/java.base/share/classes/sun/security/ssl/SSLLogger.java 2018-05-11 15:10:11.607573700 -0700 @@ -0,0 +1,609 @@ +/* + * Copyright (c) 2018, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 sun.security.ssl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.nio.ByteBuffer; +import java.security.cert.Certificate; +import java.security.cert.Extension; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; +import sun.security.action.GetPropertyAction; +import sun.security.util.HexDumpEncoder; +import sun.security.x509.*; + +/** + * Implementation of SSL logger. + * + * If the system property "javax.net.debug" is not defined, the debug logging + * is turned off. If the system property "javax.net.debug" is defined as + * empty, the debug logger is specified by System.getLogger("javax.net.ssl"), + * and applications can customize and configure the logger or use external + * logging mechanisms. If the system property "javax.net.debug" is defined + * and non-empty, a private debug logger implemented in this class is used. + */ +public final class SSLLogger { + private static final System.Logger logger; + private static final String property; + public static final boolean isOn; + + static { + String p = GetPropertyAction.privilegedGetProperty("javax.net.debug"); + if (p != null) { + if (p.isEmpty()) { + property = ""; + logger = System.getLogger("javax.net.ssl"); + } else { + property = p.toLowerCase(Locale.ENGLISH); + if (property.equals("help")) { + help(); + } + + logger = new SSLConsoleLogger("javax.net.ssl", p); + } + isOn = true; + } else { + property = null; + logger = null; + isOn = false; + } + } + + private static void help() { + System.err.println(); + System.err.println("help print the help messages"); + System.err.println("expand expand debugging information"); + System.err.println(); + System.err.println("all turn on all debugging"); + System.err.println("ssl turn on ssl debugging"); + System.err.println(); + System.err.println("The following can be used with ssl:"); + System.err.println("\trecord enable per-record tracing"); + System.err.println("\thandshake print each handshake message"); + System.err.println("\tkeygen print key generation data"); + System.err.println("\tsession print session activity"); + System.err.println("\tdefaultctx print default SSL initialization"); + System.err.println("\tsslctx print SSLContext tracing"); + System.err.println("\tsessioncache print session cache tracing"); + System.err.println("\tkeymanager print key manager tracing"); + System.err.println("\ttrustmanager print trust manager tracing"); + System.err.println("\tpluggability print pluggability tracing"); + System.err.println(); + System.err.println("\thandshake debugging can be widened with:"); + System.err.println("\tdata hex dump of each handshake message"); + System.err.println("\tverbose verbose handshake message printing"); + System.err.println(); + System.err.println("\trecord debugging can be widened with:"); + System.err.println("\tplaintext hex dump of record plaintext"); + System.err.println("\tpacket print raw SSL/TLS packets"); + System.err.println(); + System.exit(0); + } + + /** + * Return true if the "javax.net.debug" property contains the + * debug check points, or System.Logger is used. + */ + public static boolean isOn(String checkPoints) { + if (property == null) { // debugging is turned off + return false; + } else if (property.isEmpty()) { // use System.Logger + return true; + } // use provider logger + + String[] options = checkPoints.split(","); + for (String option : options) { + option = option.trim(); + if (!SSLLogger.hasOption(option)) { + return false; + } + } + + return true; + } + + private static boolean hasOption(String option) { + option = option.toLowerCase(Locale.ENGLISH); + if (property.contains("all")) { + return true; + } else { + int offset = property.indexOf("ssl"); + if (offset != -1 && property.indexOf("sslctx", offset) != -1) { + // don't enable data and plaintext options by default + if (!(option.equals("data") + || option.equals("packet") + || option.equals("plaintext"))) { + return true; + } + } + } + + return property.contains(option); + } + + public static void severe(String msg, Object... params) { + SSLLogger.log(Level.ERROR, msg, params); + } + + public static void warning(String msg, Object... params) { + SSLLogger.log(Level.WARNING, msg, params); + } + + public static void info(String msg, Object... params) { + SSLLogger.log(Level.INFO, msg, params); + } + + public static void fine(String msg, Object... params) { + SSLLogger.log(Level.DEBUG, msg, params); + } + + public static void finer(String msg, Object... params) { + SSLLogger.log(Level.TRACE, msg, params); + } + + public static void finest(String msg, Object... params) { + SSLLogger.log(Level.ALL, msg, params); + } + + private static void log(Level level, String msg, Object... params) { + if (logger.isLoggable(level)) { + if (params == null || params.length == 0) { + logger.log(level, msg); + } else { + try { + String formatted = + SSLSimpleFormatter.formatParameters(params); + logger.log(level, msg, formatted); + } catch (Exception exp) { + // ignore it, just for debugging. + } + } + } + } + + static String toString(Object... params) { + try { + return SSLSimpleFormatter.formatParameters(params); + } catch (Exception exp) { + return "unexpected exception thrown: " + exp.getMessage(); + } + } + + private static class SSLConsoleLogger implements Logger { + private final String loggerName; + private final boolean useCompactFormat; + + SSLConsoleLogger(String loggerName, String options) { + this.loggerName = loggerName; + options = options.toLowerCase(Locale.ENGLISH); + this.useCompactFormat = !options.contains("expand"); + } + + @Override + public String getName() { + return loggerName; + } + + @Override + public boolean isLoggable(Level level) { + return (level != Level.OFF); + } + + @Override + public void log(Level level, + ResourceBundle rb, String message, Throwable thrwbl) { + if (isLoggable(level)) { + try { + String formatted = + SSLSimpleFormatter.format(this, level, message, thrwbl); + System.err.write(formatted.getBytes("UTF-8")); + } catch (Exception exp) { + // ignore it, just for debugging. + } + } + } + + @Override + public void log(Level level, + ResourceBundle rb, String message, Object... params) { + if (isLoggable(level)) { + try { + String formatted = + SSLSimpleFormatter.format(this, level, message, params); + System.err.write(formatted.getBytes("UTF-8")); + } catch (Exception exp) { + // ignore it, just for debugging. + } + } + } + } + + private static class SSLSimpleFormatter { + private static final ThreadLocal dateFormat = + new ThreadLocal() { + @Override protected SimpleDateFormat initialValue() { + return new SimpleDateFormat( + "yyyy-MM-dd kk:mm:ss.SSS z", Locale.ENGLISH); + } + }; + + private static final MessageFormat basicCertFormat = new MessageFormat( + "\"version\" : \"v{0}\",\n" + + "\"serial number\" : \"{1}\",\n" + + "\"signature algorithm\": \"{2}\",\n" + + "\"issuer\" : \"{3}\",\n" + + "\"not before\" : \"{4}\",\n" + + "\"not after\" : \"{5}\",\n" + + "\"subject\" : \"{6}\",\n" + + "\"subject public key\" : \"{7}\"\n", + Locale.ENGLISH); + + private static final MessageFormat extendedCertFormart = + new MessageFormat( + "\"version\" : \"v{0}\",\n" + + "\"serial number\" : \"{1}\",\n" + + "\"signature algorithm\": \"{2}\",\n" + + "\"issuer\" : \"{3}\",\n" + + "\"not before\" : \"{4}\",\n" + + "\"not after\" : \"{5}\",\n" + + "\"subject\" : \"{6}\",\n" + + "\"subject public key\" : \"{7}\",\n" + + "\"extensions\" : [\n" + + "{8}\n" + + "]\n", + Locale.ENGLISH); + + // + // private static MessageFormat certExtFormat = new MessageFormat( + // "{0} [{1}] '{'\n" + + // " critical: {2}\n" + + // " value: {3}\n" + + // "'}'", + // Locale.ENGLISH); + // + + private static final MessageFormat messageFormatNoParas = + new MessageFormat( + "'{'\n" + + " \"logger\" : \"{0}\",\n" + + " \"level\" : \"{1}\",\n" + + " \"thread id\" : \"{2}\",\n" + + " \"thread name\" : \"{3}\",\n" + + " \"time\" : \"{4}\",\n" + + " \"caller\" : \"{5}\",\n" + + " \"message\" : \"{6}\"\n" + + "'}'\n", + Locale.ENGLISH); + + private static final MessageFormat messageCompactFormatNoParas = + new MessageFormat( + "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n", + Locale.ENGLISH); + + private static final MessageFormat messageFormatWithParas = + new MessageFormat( + "'{'\n" + + " \"logger\" : \"{0}\",\n" + + " \"level\" : \"{1}\",\n" + + " \"thread id\" : \"{2}\",\n" + + " \"thread name\" : \"{3}\",\n" + + " \"time\" : \"{4}\",\n" + + " \"caller\" : \"{5}\",\n" + + " \"message\" : \"{6}\",\n" + + " \"specifics\" : [\n" + + "{7}\n" + + " ]\n" + + "'}'\n", + Locale.ENGLISH); + + private static final MessageFormat messageCompactFormatWithParas = + new MessageFormat( + "{0}|{1}|{2}|{3}|{4}|{5}|{6} (\n" + + "{7}\n" + + ")\n", + Locale.ENGLISH); + + private static final MessageFormat keyObjectFormat = new MessageFormat( + "\"{0}\" : '{'\n" + + "{1}" + + "'}'\n", + Locale.ENGLISH); + + // INFO: [TH: 123450] 2011-08-20 23:12:32.3225 PDT + // log message + // log message + // ... + private static String format(SSLConsoleLogger logger, Level level, + String message, Object ... parameters) { + + if (parameters == null || parameters.length == 0) { + Object[] messageFields = { + logger.loggerName, + level.getName(), + Utilities.toHexString(Thread.currentThread().getId()), + Thread.currentThread().getName(), + dateFormat.get().format(new Date(System.currentTimeMillis())), + formatCaller(), + message + }; + + if (logger.useCompactFormat) { + return messageCompactFormatNoParas.format(messageFields); + } else { + return messageFormatNoParas.format(messageFields); + } + } + + Object[] messageFields = { + logger.loggerName, + level.getName(), + Utilities.toHexString(Thread.currentThread().getId()), + Thread.currentThread().getName(), + dateFormat.get().format(new Date(System.currentTimeMillis())), + formatCaller(), + message, + (logger.useCompactFormat ? + formatParameters(parameters) : + Utilities.indent(formatParameters(parameters))) + }; + + if (logger.useCompactFormat) { + return messageCompactFormatWithParas.format(messageFields); + } else { + return messageFormatWithParas.format(messageFields); + } + } + + private static String formatCaller() { + boolean isSSLLogger = false; + for (StackTraceElement ste: (new Throwable()).getStackTrace()) { + String cn = ste.getClassName(); + if (cn.equals("sun.security.ssl.SSLLogger")) { + // May be SSLLogger.log() or SSLLogger.fine(), etc. + isSSLLogger = true; + } else if (isSSLLogger) { + // Here is the caller to SSLLogger.fine(), etc. + return ste.getFileName() + ":" + ste.getLineNumber(); + } + } + + return "unknown caller"; + } + + private static boolean isLoggerImpl(StackTraceElement ste) { + String cn = ste.getClassName(); + System.out.println("class name: " + cn); + return cn.equals("sun.security.ssl.SSLLogger"); + } + + private static String formatParameters(Object ... parameters) { + StringBuilder builder = new StringBuilder(512); + boolean isFirst = true; + for (Object parameter : parameters) { + if (isFirst) { + isFirst = false; + } else { + builder.append(",\n"); + } + + if (parameter instanceof Throwable) { + builder.append(formatThrowable((Throwable)parameter)); + } else if (parameter instanceof Certificate) { + builder.append(formatCertificate((Certificate)parameter)); + } else if (parameter instanceof ByteArrayInputStream) { + builder.append(formatByteArrayInputStream( + (ByteArrayInputStream)parameter)); + } else if (parameter instanceof ByteBuffer) { + builder.append(formatByteBuffer((ByteBuffer)parameter)); + } else if (parameter instanceof byte[]) { + builder.append(formatByteArrayInputStream( + new ByteArrayInputStream((byte[])parameter))); + } else if (parameter instanceof Map.Entry) { + @SuppressWarnings("unchecked") + Map.Entry mapParameter = + (Map.Entry)parameter; + builder.append(formatMapEntry(mapParameter)); + } else { + builder.append(formatObject(parameter)); + } + } + + return builder.toString(); + } + + // "throwable": { + // ... + // } + private static String formatThrowable(Throwable throwable) { + StringBuilder builder = new StringBuilder(512); + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + try (PrintStream out = new PrintStream(bytesOut)) { + throwable.printStackTrace(out); + builder.append(Utilities.indent(bytesOut.toString())); + } + Object[] fields = { + "throwable", + builder.toString() + }; + + return keyObjectFormat.format(fields); + } + + // "certificate": { + // ... + // } + private static String formatCertificate(Certificate certificate) { + + if (!(certificate instanceof X509Certificate)) { + return Utilities.indent(certificate.toString()); + } + + StringBuilder builder = new StringBuilder(512); + try { + X509CertImpl x509 = + X509CertImpl.toImpl((X509Certificate)certificate); + X509CertInfo certInfo = + (X509CertInfo)x509.get(X509CertImpl.NAME + "." + + X509CertImpl.INFO); + CertificateExtensions certExts = (CertificateExtensions) + certInfo.get(X509CertInfo.EXTENSIONS); + if (certExts == null) { + Object[] certFields = { + x509.getVersion(), + Utilities.toHexString( + x509.getSerialNumber().toByteArray()), + x509.getSigAlgName(), + x509.getIssuerX500Principal().toString(), + dateFormat.get().format(x509.getNotBefore()), + dateFormat.get().format(x509.getNotAfter()), + x509.getSubjectX500Principal().toString(), + x509.getPublicKey().getAlgorithm() + }; + builder.append(Utilities.indent( + basicCertFormat.format(certFields))); + } else { + StringBuilder extBuilder = new StringBuilder(512); + boolean isFirst = true; + for (Extension certExt : certExts.getAllExtensions()) { + if (isFirst) { + isFirst = false; + } else { + extBuilder.append(",\n"); + } + extBuilder.append("{\n" + + Utilities.indent(certExt.toString()) + "\n}"); + } + Object[] certFields = { + x509.getVersion(), + Utilities.toHexString( + x509.getSerialNumber().toByteArray()), + x509.getSigAlgName(), + x509.getIssuerX500Principal().toString(), + dateFormat.get().format(x509.getNotBefore()), + dateFormat.get().format(x509.getNotAfter()), + x509.getSubjectX500Principal().toString(), + x509.getPublicKey().getAlgorithm(), + Utilities.indent(extBuilder.toString()) + }; + builder.append(Utilities.indent( + extendedCertFormart.format(certFields))); + } + } catch (Exception ce) { + // ignore the exception + } + + Object[] fields = { + "certificate", + builder.toString() + }; + + return Utilities.indent(keyObjectFormat.format(fields)); + } + + private static String formatByteArrayInputStream( + ByteArrayInputStream bytes) { + StringBuilder builder = new StringBuilder(512); + + try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) { + HexDumpEncoder hexEncoder = new HexDumpEncoder(); + hexEncoder.encodeBuffer(bytes, bytesOut); + + builder.append(Utilities.indent(bytesOut.toString())); + } catch (IOException ioe) { + // ignore it, just for debugging. + } + + return builder.toString(); + } + + private static String formatByteBuffer(ByteBuffer byteBuffer) { + StringBuilder builder = new StringBuilder(512); + try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) { + HexDumpEncoder hexEncoder = new HexDumpEncoder(); + hexEncoder.encodeBuffer(byteBuffer.duplicate(), bytesOut); + builder.append(Utilities.indent(bytesOut.toString())); + } catch (IOException ioe) { + // ignore it, just for debugging. + } + + return builder.toString(); + } + + private static String formatMapEntry(Map.Entry entry) { + String key = entry.getKey(); + Object value = entry.getValue(); + + String formatted; + if (value instanceof String) { + // "key": "value" + formatted = "\"" + key + "\": \"" + (String)value + "\""; + } else if (value instanceof String[]) { + // "key": [ "string a", + // "string b", + // "string c" + // ] + StringBuilder builder = new StringBuilder(512); + String[] strings = (String[])value; + builder.append("\"" + key + "\": [\n"); + for (String string : strings) { + builder.append(" \"" + string + "\""); + if (string != strings[strings.length - 1]) { + builder.append(","); + } + builder.append("\n"); + } + builder.append(" ]"); + + formatted = builder.toString(); + } else if (value instanceof byte[]) { + formatted = "\"" + key + "\": \"" + + Utilities.toHexString((byte[])value) + "\""; + } else if (value instanceof Byte) { + formatted = "\"" + key + "\": \"" + + Utilities.toHexString((byte)value) + "\""; + } else { + formatted = "\"" + key + "\": " + + "\"" + value.toString() + "\""; + } + + return Utilities.indent(formatted); + } + + private static String formatObject(Object obj) { + return obj.toString(); + } + } +}