src/java.base/share/classes/sun/security/ssl/SSLLogger.java
changeset 50768 68fa3d4026ea
parent 47216 71c04702a3d5
child 59024 b046ba510bbc
equal deleted inserted replaced
50767:356eaea05bf0 50768:68fa3d4026ea
       
     1 /*
       
     2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package sun.security.ssl;
       
    27 
       
    28 import java.io.ByteArrayInputStream;
       
    29 import java.io.ByteArrayOutputStream;
       
    30 import java.io.IOException;
       
    31 import java.io.PrintStream;
       
    32 import java.lang.System.Logger;
       
    33 import java.lang.System.Logger.Level;
       
    34 import java.nio.ByteBuffer;
       
    35 import java.security.cert.Certificate;
       
    36 import java.security.cert.Extension;
       
    37 import java.security.cert.X509Certificate;
       
    38 import java.text.MessageFormat;
       
    39 import java.text.SimpleDateFormat;
       
    40 import java.util.Date;
       
    41 import java.util.Locale;
       
    42 import java.util.Map;
       
    43 import java.util.ResourceBundle;
       
    44 import sun.security.action.GetPropertyAction;
       
    45 import sun.security.util.HexDumpEncoder;
       
    46 import sun.security.x509.*;
       
    47 
       
    48 /**
       
    49  * Implementation of SSL logger.
       
    50  *
       
    51  * If the system property "javax.net.debug" is not defined, the debug logging
       
    52  * is turned off.  If the system property "javax.net.debug" is defined as
       
    53  * empty, the debug logger is specified by System.getLogger("javax.net.ssl"),
       
    54  * and applications can customize and configure the logger or use external
       
    55  * logging mechanisms.  If the system property "javax.net.debug" is defined
       
    56  * and non-empty, a private debug logger implemented in this class is used.
       
    57  */
       
    58 public final class SSLLogger {
       
    59     private static final System.Logger logger;
       
    60     private static final String property;
       
    61     public static final boolean isOn;
       
    62 
       
    63     static {
       
    64         String p = GetPropertyAction.privilegedGetProperty("javax.net.debug");
       
    65         if (p != null) {
       
    66             if (p.isEmpty()) {
       
    67                 property = "";
       
    68                 logger = System.getLogger("javax.net.ssl");
       
    69             } else {
       
    70                 property = p.toLowerCase(Locale.ENGLISH);
       
    71                 if (property.equals("help")) {
       
    72                     help();
       
    73                 }
       
    74 
       
    75                 logger = new SSLConsoleLogger("javax.net.ssl", p);
       
    76             }
       
    77             isOn = true;
       
    78         } else {
       
    79             property = null;
       
    80             logger = null;
       
    81             isOn = false;
       
    82         }
       
    83     }
       
    84 
       
    85     private static void help() {
       
    86         System.err.println();
       
    87         System.err.println("help           print the help messages");
       
    88         System.err.println("expand         expand debugging information");
       
    89         System.err.println();
       
    90         System.err.println("all            turn on all debugging");
       
    91         System.err.println("ssl            turn on ssl debugging");
       
    92         System.err.println();
       
    93         System.err.println("The following can be used with ssl:");
       
    94         System.err.println("\trecord       enable per-record tracing");
       
    95         System.err.println("\thandshake    print each handshake message");
       
    96         System.err.println("\tkeygen       print key generation data");
       
    97         System.err.println("\tsession      print session activity");
       
    98         System.err.println("\tdefaultctx   print default SSL initialization");
       
    99         System.err.println("\tsslctx       print SSLContext tracing");
       
   100         System.err.println("\tsessioncache print session cache tracing");
       
   101         System.err.println("\tkeymanager   print key manager tracing");
       
   102         System.err.println("\ttrustmanager print trust manager tracing");
       
   103         System.err.println("\tpluggability print pluggability tracing");
       
   104         System.err.println();
       
   105         System.err.println("\thandshake debugging can be widened with:");
       
   106         System.err.println("\tdata         hex dump of each handshake message");
       
   107         System.err.println("\tverbose      verbose handshake message printing");
       
   108         System.err.println();
       
   109         System.err.println("\trecord debugging can be widened with:");
       
   110         System.err.println("\tplaintext    hex dump of record plaintext");
       
   111         System.err.println("\tpacket       print raw SSL/TLS packets");
       
   112         System.err.println();
       
   113         System.exit(0);
       
   114     }
       
   115 
       
   116     /**
       
   117      * Return true if the "javax.net.debug" property contains the
       
   118      * debug check points, or System.Logger is used.
       
   119      */
       
   120     public static boolean isOn(String checkPoints) {
       
   121         if (property == null) {              // debugging is turned off
       
   122             return false;
       
   123         } else if (property.isEmpty()) {     // use System.Logger
       
   124             return true;
       
   125         }                                   // use provider logger
       
   126 
       
   127         String[] options = checkPoints.split(",");
       
   128         for (String option : options) {
       
   129             option = option.trim();
       
   130             if (!SSLLogger.hasOption(option)) {
       
   131                 return false;
       
   132             }
       
   133         }
       
   134 
       
   135         return true;
       
   136     }
       
   137 
       
   138     private static boolean hasOption(String option) {
       
   139         option = option.toLowerCase(Locale.ENGLISH);
       
   140         if (property.contains("all")) {
       
   141             return true;
       
   142         } else {
       
   143             int offset = property.indexOf("ssl");
       
   144             if (offset != -1 && property.indexOf("sslctx", offset) != -1) {
       
   145                 // don't enable data and plaintext options by default
       
   146                 if (!(option.equals("data")
       
   147                         || option.equals("packet")
       
   148                         || option.equals("plaintext"))) {
       
   149                     return true;
       
   150                 }
       
   151             }
       
   152         }
       
   153 
       
   154         return property.contains(option);
       
   155     }
       
   156 
       
   157     public static void severe(String msg, Object... params) {
       
   158         SSLLogger.log(Level.ERROR, msg, params);
       
   159     }
       
   160 
       
   161     public static void warning(String msg, Object... params) {
       
   162         SSLLogger.log(Level.WARNING, msg, params);
       
   163     }
       
   164 
       
   165     public static void info(String msg, Object... params) {
       
   166         SSLLogger.log(Level.INFO, msg, params);
       
   167     }
       
   168 
       
   169     public static void fine(String msg, Object... params) {
       
   170         SSLLogger.log(Level.DEBUG, msg, params);
       
   171     }
       
   172 
       
   173     public static void finer(String msg, Object... params) {
       
   174         SSLLogger.log(Level.TRACE, msg, params);
       
   175     }
       
   176 
       
   177     public static void finest(String msg, Object... params) {
       
   178         SSLLogger.log(Level.ALL, msg, params);
       
   179     }
       
   180 
       
   181     private static void log(Level level, String msg, Object... params) {
       
   182         if (logger.isLoggable(level)) {
       
   183             if (params == null || params.length == 0) {
       
   184                 logger.log(level, msg);
       
   185             } else {
       
   186                 try {
       
   187                     String formatted =
       
   188                             SSLSimpleFormatter.formatParameters(params);
       
   189                     logger.log(level, msg, formatted);
       
   190                 } catch (Exception exp) {
       
   191                     // ignore it, just for debugging.
       
   192                 }
       
   193             }
       
   194         }
       
   195     }
       
   196 
       
   197     static String toString(Object... params) {
       
   198         try {
       
   199             return SSLSimpleFormatter.formatParameters(params);
       
   200         } catch (Exception exp) {
       
   201             return "unexpected exception thrown: " + exp.getMessage();
       
   202         }
       
   203     }
       
   204 
       
   205     private static class SSLConsoleLogger implements Logger {
       
   206         private final String loggerName;
       
   207         private final boolean useCompactFormat;
       
   208 
       
   209         SSLConsoleLogger(String loggerName, String options) {
       
   210             this.loggerName = loggerName;
       
   211             options = options.toLowerCase(Locale.ENGLISH);
       
   212             this.useCompactFormat = !options.contains("expand");
       
   213         }
       
   214 
       
   215         @Override
       
   216         public String getName() {
       
   217             return loggerName;
       
   218         }
       
   219 
       
   220         @Override
       
   221         public boolean isLoggable(Level level) {
       
   222             return (level != Level.OFF);
       
   223         }
       
   224 
       
   225         @Override
       
   226         public void log(Level level,
       
   227                 ResourceBundle rb, String message, Throwable thrwbl) {
       
   228             if (isLoggable(level)) {
       
   229                 try {
       
   230                     String formatted =
       
   231                         SSLSimpleFormatter.format(this, level, message, thrwbl);
       
   232                     System.err.write(formatted.getBytes("UTF-8"));
       
   233                 } catch (Exception exp) {
       
   234                     // ignore it, just for debugging.
       
   235                 }
       
   236             }
       
   237         }
       
   238 
       
   239         @Override
       
   240         public void log(Level level,
       
   241                 ResourceBundle rb, String message, Object... params) {
       
   242             if (isLoggable(level)) {
       
   243                 try {
       
   244                     String formatted =
       
   245                         SSLSimpleFormatter.format(this, level, message, params);
       
   246                     System.err.write(formatted.getBytes("UTF-8"));
       
   247                 } catch (Exception exp) {
       
   248                     // ignore it, just for debugging.
       
   249                 }
       
   250             }
       
   251         }
       
   252     }
       
   253 
       
   254     private static class SSLSimpleFormatter {
       
   255         private static final ThreadLocal<SimpleDateFormat> dateFormat =
       
   256             new ThreadLocal<SimpleDateFormat>() {
       
   257                 @Override protected SimpleDateFormat initialValue() {
       
   258                     return new SimpleDateFormat(
       
   259                             "yyyy-MM-dd kk:mm:ss.SSS z", Locale.ENGLISH);
       
   260                 }
       
   261             };
       
   262 
       
   263         private static final MessageFormat basicCertFormat = new MessageFormat(
       
   264                 "\"version\"            : \"v{0}\",\n" +
       
   265                 "\"serial number\"      : \"{1}\",\n" +
       
   266                 "\"signature algorithm\": \"{2}\",\n" +
       
   267                 "\"issuer\"             : \"{3}\",\n" +
       
   268                 "\"not before\"         : \"{4}\",\n" +
       
   269                 "\"not  after\"         : \"{5}\",\n" +
       
   270                 "\"subject\"            : \"{6}\",\n" +
       
   271                 "\"subject public key\" : \"{7}\"\n",
       
   272                 Locale.ENGLISH);
       
   273 
       
   274         private static final MessageFormat extendedCertFormart =
       
   275             new MessageFormat(
       
   276                 "\"version\"            : \"v{0}\",\n" +
       
   277                 "\"serial number\"      : \"{1}\",\n" +
       
   278                 "\"signature algorithm\": \"{2}\",\n" +
       
   279                 "\"issuer\"             : \"{3}\",\n" +
       
   280                 "\"not before\"         : \"{4}\",\n" +
       
   281                 "\"not  after\"         : \"{5}\",\n" +
       
   282                 "\"subject\"            : \"{6}\",\n" +
       
   283                 "\"subject public key\" : \"{7}\",\n" +
       
   284                 "\"extensions\"         : [\n" +
       
   285                 "{8}\n" +
       
   286                 "]\n",
       
   287                 Locale.ENGLISH);
       
   288 
       
   289         //
       
   290         // private static MessageFormat certExtFormat = new MessageFormat(
       
   291         //         "{0} [{1}] '{'\n" +
       
   292         //         "  critical: {2}\n" +
       
   293         //         "  value: {3}\n" +
       
   294         //         "'}'",
       
   295         //         Locale.ENGLISH);
       
   296         //
       
   297 
       
   298         private static final MessageFormat messageFormatNoParas =
       
   299             new MessageFormat(
       
   300                 "'{'\n" +
       
   301                 "  \"logger\"      : \"{0}\",\n" +
       
   302                 "  \"level\"       : \"{1}\",\n" +
       
   303                 "  \"thread id\"   : \"{2}\",\n" +
       
   304                 "  \"thread name\" : \"{3}\",\n" +
       
   305                 "  \"time\"        : \"{4}\",\n" +
       
   306                 "  \"caller\"      : \"{5}\",\n" +
       
   307                 "  \"message\"     : \"{6}\"\n" +
       
   308                 "'}'\n",
       
   309                 Locale.ENGLISH);
       
   310 
       
   311         private static final MessageFormat messageCompactFormatNoParas =
       
   312             new MessageFormat(
       
   313                 "{0}|{1}|{2}|{3}|{4}|{5}|{6}\n",
       
   314                 Locale.ENGLISH);
       
   315 
       
   316         private static final MessageFormat messageFormatWithParas =
       
   317             new MessageFormat(
       
   318                 "'{'\n" +
       
   319                 "  \"logger\"      : \"{0}\",\n" +
       
   320                 "  \"level\"       : \"{1}\",\n" +
       
   321                 "  \"thread id\"   : \"{2}\",\n" +
       
   322                 "  \"thread name\" : \"{3}\",\n" +
       
   323                 "  \"time\"        : \"{4}\",\n" +
       
   324                 "  \"caller\"      : \"{5}\",\n" +
       
   325                 "  \"message\"     : \"{6}\",\n" +
       
   326                 "  \"specifics\"   : [\n" +
       
   327                 "{7}\n" +
       
   328                 "  ]\n" +
       
   329                 "'}'\n",
       
   330                 Locale.ENGLISH);
       
   331 
       
   332         private static final MessageFormat messageCompactFormatWithParas =
       
   333             new MessageFormat(
       
   334                 "{0}|{1}|{2}|{3}|{4}|{5}|{6} (\n" +
       
   335                 "{7}\n" +
       
   336                 ")\n",
       
   337                 Locale.ENGLISH);
       
   338 
       
   339         private static final MessageFormat keyObjectFormat = new MessageFormat(
       
   340                 "\"{0}\" : '{'\n" +
       
   341                 "{1}" +
       
   342                 "'}'\n",
       
   343                 Locale.ENGLISH);
       
   344 
       
   345         // INFO: [TH: 123450] 2011-08-20 23:12:32.3225 PDT
       
   346         //     log message
       
   347         //     log message
       
   348         //     ...
       
   349         private static String format(SSLConsoleLogger logger, Level level,
       
   350                     String message, Object ... parameters) {
       
   351 
       
   352             if (parameters == null || parameters.length == 0) {
       
   353                 Object[] messageFields = {
       
   354                     logger.loggerName,
       
   355                     level.getName(),
       
   356                     Utilities.toHexString(Thread.currentThread().getId()),
       
   357                     Thread.currentThread().getName(),
       
   358                     dateFormat.get().format(new Date(System.currentTimeMillis())),
       
   359                     formatCaller(),
       
   360                     message
       
   361                 };
       
   362 
       
   363                 if (logger.useCompactFormat) {
       
   364                     return messageCompactFormatNoParas.format(messageFields);
       
   365                 } else {
       
   366                     return messageFormatNoParas.format(messageFields);
       
   367                 }
       
   368             }
       
   369 
       
   370             Object[] messageFields = {
       
   371                     logger.loggerName,
       
   372                     level.getName(),
       
   373                     Utilities.toHexString(Thread.currentThread().getId()),
       
   374                     Thread.currentThread().getName(),
       
   375                     dateFormat.get().format(new Date(System.currentTimeMillis())),
       
   376                     formatCaller(),
       
   377                     message,
       
   378                     (logger.useCompactFormat ?
       
   379                         formatParameters(parameters) :
       
   380                         Utilities.indent(formatParameters(parameters)))
       
   381                 };
       
   382 
       
   383             if (logger.useCompactFormat) {
       
   384                 return messageCompactFormatWithParas.format(messageFields);
       
   385             } else {
       
   386                 return messageFormatWithParas.format(messageFields);
       
   387             }
       
   388         }
       
   389 
       
   390         private static String formatCaller() {
       
   391             return StackWalker.getInstance().walk(s ->
       
   392                 s.dropWhile(f ->
       
   393                     f.getClassName().startsWith("sun.security.ssl.SSLLogger") ||
       
   394                     f.getClassName().startsWith("java.lang.System"))
       
   395                 .map(f -> f.getFileName() + ":" + f.getLineNumber())
       
   396                 .findFirst().orElse("unknown caller"));
       
   397         }
       
   398 
       
   399         private static String formatParameters(Object ... parameters) {
       
   400             StringBuilder builder = new StringBuilder(512);
       
   401             boolean isFirst = true;
       
   402             for (Object parameter : parameters) {
       
   403                 if (isFirst) {
       
   404                     isFirst = false;
       
   405                 } else {
       
   406                     builder.append(",\n");
       
   407                 }
       
   408 
       
   409                 if (parameter instanceof Throwable) {
       
   410                     builder.append(formatThrowable((Throwable)parameter));
       
   411                 } else if (parameter instanceof Certificate) {
       
   412                     builder.append(formatCertificate((Certificate)parameter));
       
   413                 } else if (parameter instanceof ByteArrayInputStream) {
       
   414                     builder.append(formatByteArrayInputStream(
       
   415                         (ByteArrayInputStream)parameter));
       
   416                 } else if (parameter instanceof ByteBuffer) {
       
   417                     builder.append(formatByteBuffer((ByteBuffer)parameter));
       
   418                 } else if (parameter instanceof byte[]) {
       
   419                     builder.append(formatByteArrayInputStream(
       
   420                         new ByteArrayInputStream((byte[])parameter)));
       
   421                 } else if (parameter instanceof Map.Entry) {
       
   422                     @SuppressWarnings("unchecked")
       
   423                     Map.Entry<String, ?> mapParameter =
       
   424                         (Map.Entry<String, ?>)parameter;
       
   425                     builder.append(formatMapEntry(mapParameter));
       
   426                 } else {
       
   427                     builder.append(formatObject(parameter));
       
   428                 }
       
   429             }
       
   430 
       
   431             return builder.toString();
       
   432         }
       
   433 
       
   434         // "throwable": {
       
   435         //   ...
       
   436         // }
       
   437         private static String formatThrowable(Throwable throwable) {
       
   438             StringBuilder builder = new StringBuilder(512);
       
   439             ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
       
   440             try (PrintStream out = new PrintStream(bytesOut)) {
       
   441                 throwable.printStackTrace(out);
       
   442                 builder.append(Utilities.indent(bytesOut.toString()));
       
   443             }
       
   444             Object[] fields = {
       
   445                     "throwable",
       
   446                     builder.toString()
       
   447                 };
       
   448 
       
   449             return keyObjectFormat.format(fields);
       
   450         }
       
   451 
       
   452         // "certificate": {
       
   453         //   ...
       
   454         // }
       
   455         private static String formatCertificate(Certificate certificate) {
       
   456 
       
   457             if (!(certificate instanceof X509Certificate)) {
       
   458                 return Utilities.indent(certificate.toString());
       
   459             }
       
   460 
       
   461             StringBuilder builder = new StringBuilder(512);
       
   462             try {
       
   463                 X509CertImpl x509 =
       
   464                     X509CertImpl.toImpl((X509Certificate)certificate);
       
   465                 X509CertInfo certInfo =
       
   466                         (X509CertInfo)x509.get(X509CertImpl.NAME + "." +
       
   467                                                        X509CertImpl.INFO);
       
   468                 CertificateExtensions certExts = (CertificateExtensions)
       
   469                         certInfo.get(X509CertInfo.EXTENSIONS);
       
   470                 if (certExts == null) {
       
   471                     Object[] certFields = {
       
   472                         x509.getVersion(),
       
   473                         Utilities.toHexString(
       
   474                                 x509.getSerialNumber().toByteArray()),
       
   475                         x509.getSigAlgName(),
       
   476                         x509.getIssuerX500Principal().toString(),
       
   477                         dateFormat.get().format(x509.getNotBefore()),
       
   478                         dateFormat.get().format(x509.getNotAfter()),
       
   479                         x509.getSubjectX500Principal().toString(),
       
   480                         x509.getPublicKey().getAlgorithm()
       
   481                         };
       
   482                     builder.append(Utilities.indent(
       
   483                             basicCertFormat.format(certFields)));
       
   484                 } else {
       
   485                     StringBuilder extBuilder = new StringBuilder(512);
       
   486                     boolean isFirst = true;
       
   487                     for (Extension certExt : certExts.getAllExtensions()) {
       
   488                         if (isFirst) {
       
   489                             isFirst = false;
       
   490                         } else {
       
   491                             extBuilder.append(",\n");
       
   492                         }
       
   493                         extBuilder.append("{\n" +
       
   494                             Utilities.indent(certExt.toString()) + "\n}");
       
   495                     }
       
   496                     Object[] certFields = {
       
   497                         x509.getVersion(),
       
   498                         Utilities.toHexString(
       
   499                                 x509.getSerialNumber().toByteArray()),
       
   500                         x509.getSigAlgName(),
       
   501                         x509.getIssuerX500Principal().toString(),
       
   502                         dateFormat.get().format(x509.getNotBefore()),
       
   503                         dateFormat.get().format(x509.getNotAfter()),
       
   504                         x509.getSubjectX500Principal().toString(),
       
   505                         x509.getPublicKey().getAlgorithm(),
       
   506                         Utilities.indent(extBuilder.toString())
       
   507                         };
       
   508                     builder.append(Utilities.indent(
       
   509                             extendedCertFormart.format(certFields)));
       
   510                 }
       
   511             } catch (Exception ce) {
       
   512                 // ignore the exception
       
   513             }
       
   514 
       
   515             Object[] fields = {
       
   516                     "certificate",
       
   517                     builder.toString()
       
   518                 };
       
   519 
       
   520             return Utilities.indent(keyObjectFormat.format(fields));
       
   521         }
       
   522 
       
   523         private static String formatByteArrayInputStream(
       
   524                 ByteArrayInputStream bytes) {
       
   525             StringBuilder builder = new StringBuilder(512);
       
   526 
       
   527             try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) {
       
   528                 HexDumpEncoder hexEncoder = new HexDumpEncoder();
       
   529                 hexEncoder.encodeBuffer(bytes, bytesOut);
       
   530 
       
   531                 builder.append(Utilities.indent(bytesOut.toString()));
       
   532             } catch (IOException ioe) {
       
   533                 // ignore it, just for debugging.
       
   534             }
       
   535 
       
   536             return builder.toString();
       
   537         }
       
   538 
       
   539         private static String formatByteBuffer(ByteBuffer byteBuffer) {
       
   540             StringBuilder builder = new StringBuilder(512);
       
   541             try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream()) {
       
   542                 HexDumpEncoder hexEncoder = new HexDumpEncoder();
       
   543                 hexEncoder.encodeBuffer(byteBuffer.duplicate(), bytesOut);
       
   544                 builder.append(Utilities.indent(bytesOut.toString()));
       
   545             } catch (IOException ioe) {
       
   546                 // ignore it, just for debugging.
       
   547             }
       
   548 
       
   549             return builder.toString();
       
   550         }
       
   551 
       
   552         private static String formatMapEntry(Map.Entry<String, ?> entry) {
       
   553             String key = entry.getKey();
       
   554             Object value = entry.getValue();
       
   555 
       
   556             String formatted;
       
   557             if (value instanceof String) {
       
   558                 // "key": "value"
       
   559                 formatted = "\"" + key + "\": \"" + (String)value + "\"";
       
   560             } else if (value instanceof String[]) {
       
   561                 // "key": [ "string a",
       
   562                 //          "string b",
       
   563                 //          "string c"
       
   564                 //        ]
       
   565                 StringBuilder builder = new StringBuilder(512);
       
   566                 String[] strings = (String[])value;
       
   567                 builder.append("\"" + key + "\": [\n");
       
   568                 for (String string : strings) {
       
   569                     builder.append("      \"" + string + "\"");
       
   570                     if (string != strings[strings.length - 1]) {
       
   571                         builder.append(",");
       
   572                     }
       
   573                     builder.append("\n");
       
   574                 }
       
   575                 builder.append("      ]");
       
   576 
       
   577                 formatted = builder.toString();
       
   578             } else if (value instanceof byte[]) {
       
   579                 formatted = "\"" + key + "\": \"" +
       
   580                     Utilities.toHexString((byte[])value) + "\"";
       
   581             } else if (value instanceof Byte) {
       
   582                 formatted = "\"" + key + "\": \"" +
       
   583                     Utilities.toHexString((byte)value) + "\"";
       
   584             } else {
       
   585                 formatted = "\"" + key + "\": " +
       
   586                     "\"" + value.toString() + "\"";
       
   587             }
       
   588 
       
   589             return Utilities.indent(formatted);
       
   590         }
       
   591 
       
   592         private static String formatObject(Object obj) {
       
   593             return obj.toString();
       
   594         }
       
   595     }
       
   596 }