mirror of
https://github.com/openjdk/jdk.git
synced 2026-02-11 19:08:23 +00:00
668 lines
26 KiB
Java
668 lines
26 KiB
Java
/*
|
|
* Copyright (c) 2018, 2025, 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.time.Instant;
|
|
import java.time.ZoneId;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.*;
|
|
|
|
import sun.security.util.HexDumpEncoder;
|
|
import sun.security.util.Debug;
|
|
import sun.security.x509.*;
|
|
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
|
|
/**
|
|
* 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 EnumSet<ComponentToken> activeComponents = EnumSet.noneOf(ComponentToken.class);
|
|
|
|
static {
|
|
String p = System.getProperty("javax.net.debug");
|
|
if (p != null) {
|
|
if (p.isEmpty()) {
|
|
property = "";
|
|
logger = System.getLogger("javax.net.ssl");
|
|
activeComponents.add(ComponentToken.EMPTYALL);
|
|
} else {
|
|
property = p.toLowerCase(Locale.ENGLISH);
|
|
if (property.contains("help")) {
|
|
help();
|
|
}
|
|
logger = new SSLConsoleLogger("javax.net.ssl", p);
|
|
if (property.contains("all")) {
|
|
activeComponents.add(ComponentToken.EMPTYALL);
|
|
} else {
|
|
String tmpProperty = property;
|
|
for (ComponentToken o : ComponentToken.values()) {
|
|
if (tmpProperty.contains(o.component)) {
|
|
activeComponents.add(o);
|
|
// remove the pattern to avoid it being reused
|
|
// e.g. "ssl,sslctx" parsing
|
|
tmpProperty = tmpProperty.replaceFirst(o.component, "");
|
|
}
|
|
}
|
|
// some rules to check
|
|
if ((activeComponents.contains(ComponentToken.PLAINTEXT)
|
|
|| activeComponents.contains(ComponentToken.PACKET))
|
|
&& !activeComponents.contains(ComponentToken.RECORD)) {
|
|
activeComponents.remove(ComponentToken.PLAINTEXT);
|
|
activeComponents.remove(ComponentToken.PACKET);
|
|
}
|
|
|
|
if (activeComponents.contains(ComponentToken.VERBOSE)
|
|
&& !activeComponents.contains(ComponentToken.HANDSHAKE)) {
|
|
activeComponents.remove(ComponentToken.VERBOSE);
|
|
}
|
|
}
|
|
}
|
|
isOn = activeComponents.contains(ComponentToken.EMPTYALL)
|
|
|| activeComponents.contains(ComponentToken.SSL);
|
|
} else {
|
|
property = null;
|
|
logger = null;
|
|
isOn = false;
|
|
}
|
|
}
|
|
|
|
private static void help() {
|
|
System.err.println();
|
|
System.err.println("help print this help message and exit");
|
|
System.err.println("expand expanded (less compact) output format");
|
|
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("\tdefaultctx print default SSL initialization");
|
|
System.err.println("\thandshake print each handshake message");
|
|
System.err.println("\tkeymanager print key manager tracing");
|
|
System.err.println("\trecord enable per-record tracing");
|
|
System.err.println("\trespmgr print OCSP response tracing");
|
|
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("\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, "all" or if the System.Logger is used.
|
|
*
|
|
* Specify all string tokens required when calling this method.
|
|
* E.g. since "plaintext" is a widened option of the "record" option,
|
|
* the call needs to be isOn("ssl,record,plaintext") to ensure
|
|
* correct use. It also ensures that the user specifies the correct
|
|
* system property value syntax as per help menu.
|
|
*/
|
|
public static boolean isOn(String checkPoints) {
|
|
if (!isOn) {
|
|
return false;
|
|
}
|
|
|
|
if (activeComponents.contains(ComponentToken.EMPTYALL)) {
|
|
// System.Logger in use or property = "all"
|
|
return true;
|
|
}
|
|
|
|
if (checkPoints.equals("ssl")) {
|
|
return !ComponentToken.isSslFilteringEnabled();
|
|
}
|
|
|
|
if (activeComponents.size() == 1 && !containsWidenOption(checkPoints)) {
|
|
// in ssl mode, we always log except for widen options
|
|
return true;
|
|
}
|
|
|
|
String[] options = checkPoints.split(",");
|
|
for (String option : options) {
|
|
option = option.trim().toLowerCase(Locale.ROOT);
|
|
if (!property.contains(option)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static boolean containsWidenOption(String options) {
|
|
return options.contains("verbose")
|
|
|| options.contains("plaintext")
|
|
|| options.contains("packet")
|
|
|| options.contains("expand");
|
|
}
|
|
|
|
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 != null && logger.isLoggable(level)) {
|
|
if (params == null || params.length == 0) {
|
|
logger.log(level, msg);
|
|
} else {
|
|
try {
|
|
logger.log(level, () -> msg + ":\n" + SSLSimpleFormatter.formatParameters(params));
|
|
} 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();
|
|
}
|
|
}
|
|
|
|
// Logs a warning message and always returns false. This method
|
|
// can be used as an OR Predicate to add a log in a stream filter.
|
|
public static boolean logWarning(String option, String s) {
|
|
if (SSLLogger.isOn && SSLLogger.isOn(option)) {
|
|
SSLLogger.warning(s);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
enum ComponentToken {
|
|
EMPTYALL,
|
|
DEFAULTCTX,
|
|
HANDSHAKE,
|
|
KEYMANAGER,
|
|
RECORD,
|
|
RESPMGR,
|
|
SESSION,
|
|
SSLCTX,
|
|
TRUSTMANAGER,
|
|
VERBOSE,
|
|
PLAINTEXT,
|
|
PACKET,
|
|
SSL; // define ssl last, helps with sslctx matching later.
|
|
|
|
final String component;
|
|
|
|
ComponentToken() {
|
|
this.component = this.toString().toLowerCase(Locale.ROOT);
|
|
}
|
|
|
|
static boolean isSslFilteringEnabled() {
|
|
return activeComponents.contains(DEFAULTCTX)
|
|
|| activeComponents.contains(HANDSHAKE)
|
|
|| activeComponents.contains(KEYMANAGER)
|
|
|| activeComponents.contains(RECORD)
|
|
|| activeComponents.contains(RESPMGR)
|
|
|| activeComponents.contains(SESSION)
|
|
|| activeComponents.contains(SSLCTX)
|
|
|| activeComponents.contains(TRUSTMANAGER);
|
|
}
|
|
}
|
|
|
|
|
|
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 String PATTERN = "yyyy-MM-dd kk:mm:ss.SSS z";
|
|
private static final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(PATTERN, Locale.ENGLISH)
|
|
.withZone(ZoneId.systemDefault());
|
|
|
|
private static final MessageFormat basicCertFormat = new MessageFormat(
|
|
"""
|
|
"version" : "v{0}",
|
|
"serial number" : "{1}",
|
|
"signature algorithm": "{2}",
|
|
"issuer" : "{3}",
|
|
"not before" : "{4}",
|
|
"not after" : "{5}",
|
|
"subject" : "{6}",
|
|
"subject public key" : "{7}"
|
|
""",
|
|
Locale.ENGLISH);
|
|
|
|
private static final MessageFormat extendedCertFormat =
|
|
new MessageFormat(
|
|
"""
|
|
"version" : "v{0}",
|
|
"serial number" : "{1}",
|
|
"signature algorithm": "{2}",
|
|
"issuer" : "{3}",
|
|
"not before" : "{4}",
|
|
"not after" : "{5}",
|
|
"subject" : "{6}",
|
|
"subject public key" : "{7}",
|
|
"extensions" : [
|
|
{8}
|
|
]
|
|
""",
|
|
Locale.ENGLISH);
|
|
|
|
private static final MessageFormat messageFormatNoParas =
|
|
new MessageFormat(
|
|
"""
|
|
'{'
|
|
"logger" : "{0}",
|
|
"level" : "{1}",
|
|
"thread id" : "{2}",
|
|
"thread name" : "{3}",
|
|
"time" : "{4}",
|
|
"caller" : "{5}",
|
|
"message" : "{6}"
|
|
'}'
|
|
""",
|
|
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(
|
|
"""
|
|
'{'
|
|
"logger" : "{0}",
|
|
"level" : "{1}",
|
|
"thread id" : "{2}",
|
|
"thread name" : "{3}",
|
|
"time" : "{4}",
|
|
"caller" : "{5}",
|
|
"message" : "{6}",
|
|
"specifics" : [
|
|
{7}
|
|
]
|
|
'}'
|
|
""",
|
|
Locale.ENGLISH);
|
|
|
|
private static final MessageFormat messageCompactFormatWithParas =
|
|
new MessageFormat(
|
|
"""
|
|
{0}|{1}|{2}|{3}|{4}|{5}|{6} (
|
|
{7}
|
|
)
|
|
""",
|
|
Locale.ENGLISH);
|
|
|
|
private static final MessageFormat keyObjectFormat = new MessageFormat(
|
|
"""
|
|
"{0}" : '{'
|
|
{1}'}'
|
|
""",
|
|
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().threadId()),
|
|
Thread.currentThread().getName(),
|
|
dateTimeFormat.format(Instant.now()),
|
|
formatCaller(),
|
|
message
|
|
};
|
|
|
|
if (logger.useCompactFormat) {
|
|
return messageCompactFormatNoParas.format(messageFields);
|
|
} else {
|
|
return messageFormatNoParas.format(messageFields);
|
|
}
|
|
}
|
|
|
|
Object[] messageFields = {
|
|
logger.loggerName,
|
|
level.getName(),
|
|
Utilities.toHexString(Thread.currentThread().threadId()),
|
|
Thread.currentThread().getName(),
|
|
dateTimeFormat.format(Instant.now()),
|
|
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() {
|
|
return StackWalker.getInstance().walk(s ->
|
|
s.dropWhile(f ->
|
|
f.getClassName().startsWith("sun.security.ssl.SSLLogger") ||
|
|
f.getClassName().startsWith("java.lang.System"))
|
|
.map(f -> f.getFileName() + ":" + f.getLineNumber())
|
|
.findFirst().orElse("unknown caller"));
|
|
}
|
|
|
|
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<String, ?> mapParameter =
|
|
(Map.Entry<String, ?>)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 = x509.getInfo();
|
|
CertificateExtensions certExts = certInfo.getExtensions();
|
|
if (certExts == null) {
|
|
Object[] certFields = {
|
|
x509.getVersion(),
|
|
Debug.toString(x509.getSerialNumber()),
|
|
x509.getSigAlgName(),
|
|
x509.getIssuerX500Principal().toString(),
|
|
dateTimeFormat.format(x509.getNotBefore().toInstant()),
|
|
dateTimeFormat.format(x509.getNotAfter().toInstant()),
|
|
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(),
|
|
Debug.toString(x509.getSerialNumber()),
|
|
x509.getSigAlgName(),
|
|
x509.getIssuerX500Principal().toString(),
|
|
dateTimeFormat.format(x509.getNotBefore().toInstant()),
|
|
dateTimeFormat.format(x509.getNotAfter().toInstant()),
|
|
x509.getSubjectX500Principal().toString(),
|
|
x509.getPublicKey().getAlgorithm(),
|
|
Utilities.indent(extBuilder.toString())
|
|
};
|
|
builder.append(Utilities.indent(
|
|
extendedCertFormat.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<String, ?> entry) {
|
|
String key = entry.getKey();
|
|
Object value = entry.getValue();
|
|
|
|
String formatted;
|
|
if (value instanceof String) {
|
|
// "key": "value"
|
|
formatted = "\"" + key + "\": \"" + value + "\"";
|
|
} else if (value instanceof String[] strings) {
|
|
// "key": [ "string a",
|
|
// "string b",
|
|
// "string c"
|
|
// ]
|
|
StringBuilder builder = new StringBuilder(512);
|
|
builder.append("\"" + key + "\": [\n");
|
|
int len = strings.length;
|
|
for (int i = 0; i < len; i++) {
|
|
String string = strings[i];
|
|
builder.append(" \"" + string + "\"");
|
|
if (i != len - 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 + "\": \"" +
|
|
HexFormat.of().toHexDigits((byte)value) + "\"";
|
|
} else {
|
|
formatted = "\"" + key + "\": " +
|
|
"\"" + value.toString() + "\"";
|
|
}
|
|
|
|
return Utilities.indent(formatted);
|
|
}
|
|
|
|
private static String formatObject(Object obj) {
|
|
return obj.toString();
|
|
}
|
|
}
|
|
}
|