< prev index next > src/java.base/share/classes/jdk/internal/vm/ThreadDumper.java
Print this page
/*
! * Copyright (c) 2020, 2023, 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
/*
! * Copyright (c) 2020, 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
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.vm;
! import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
! import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
! import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Thread dump support.
*
! * This class defines methods to dump threads to an output stream or file in plain
! * text or JSON format.
*/
public class ThreadDumper {
private ThreadDumper() { }
// the maximum byte array to return when generating the thread dump to a byte array
private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
/**
! * Generate a thread dump in plain text format to a byte array or file, UTF-8 encoded.
- *
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
*
* @param file the file path to the file, null or "-" to return a byte array
* @param okayToOverwrite true to overwrite an existing file
! * @return the UTF-8 encoded thread dump or message to return to the user
*/
public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
} else {
return dumpThreadsToFile(file, okayToOverwrite, false);
}
}
/**
! * Generate a thread dump in JSON format to a byte array or file, UTF-8 encoded.
- *
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
*
* @param file the file path to the file, null or "-" to return a byte array
* @param okayToOverwrite true to overwrite an existing file
! * @return the UTF-8 encoded thread dump or message to return to the user
*/
public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
} else {
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.vm;
! import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
! import java.io.OutputStreamWriter;
+ import java.io.UncheckedIOException;
+ import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
! import java.util.ArrayDeque;
+ import java.util.Arrays;
+ import java.util.Deque;
import java.util.Iterator;
import java.util.List;
+ import java.util.Objects;
+ import java.util.concurrent.locks.AbstractOwnableSynchronizer;
/**
* Thread dump support.
*
! * This class defines static methods to support the Thread.dump_to_file diagnostic command
! * and the HotSpotDiagnosticMXBean.dumpThreads API. It defines methods to generate a
+ * thread dump to a file or byte array in plain text or JSON format.
*/
public class ThreadDumper {
private ThreadDumper() { }
// the maximum byte array to return when generating the thread dump to a byte array
private static final int MAX_BYTE_ARRAY_SIZE = 16_000;
/**
! * Generate a thread dump in plain text format to a file or byte array, UTF-8 encoded.
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
*
* @param file the file path to the file, null or "-" to return a byte array
* @param okayToOverwrite true to overwrite an existing file
! * @return the UTF-8 encoded thread dump or message to return to the tool user
*/
public static byte[] dumpThreads(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(false, MAX_BYTE_ARRAY_SIZE);
} else {
return dumpThreadsToFile(file, okayToOverwrite, false);
}
}
/**
! * Generate a thread dump in JSON format to a file or byte array, UTF-8 encoded.
* This method is invoked by the VM for the Thread.dump_to_file diagnostic command.
*
* @param file the file path to the file, null or "-" to return a byte array
* @param okayToOverwrite true to overwrite an existing file
! * @return the UTF-8 encoded thread dump or message to return to the tool user
*/
public static byte[] dumpThreadsToJson(String file, boolean okayToOverwrite) {
if (file == null || file.equals("-")) {
return dumpThreadsToByteArray(true, MAX_BYTE_ARRAY_SIZE);
} else {
}
}
/**
* Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
*/
private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
! try (var out = new BoundedByteArrayOutputStream(maxSize);
! PrintStream ps = new PrintStream(out, true, StandardCharsets.UTF_8)) {
if (json) {
! dumpThreadsToJson(ps);
} else {
! dumpThreads(ps);
}
! return out.toByteArray();
}
}
/**
* Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
*/
private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
Path path = Path.of(file).toAbsolutePath();
OpenOption[] options = (okayToOverwrite)
? new OpenOption[0]
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
String reply;
! try (OutputStream out = Files.newOutputStream(path, options);
! BufferedOutputStream bos = new BufferedOutputStream(out);
! PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8)) {
! if (json) {
! dumpThreadsToJson(ps);
! } else {
! dumpThreads(ps);
}
! reply = String.format("Created %s%n", path);
- } catch (FileAlreadyExistsException e) {
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
! } catch (IOException ioe) {
! reply = String.format("Failed: %s%n", ioe);
}
return reply.getBytes(StandardCharsets.UTF_8);
}
/**
! * Generate a thread dump in plain text format to the given output stream,
! * UTF-8 encoded.
! *
- * This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
*/
! public static void dumpThreads(OutputStream out) {
! BufferedOutputStream bos = new BufferedOutputStream(out);
- PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
try {
! dumpThreads(ps);
! } finally {
! ps.flush(); // flushes underlying stream
}
}
/**
! * Generate a thread dump in plain text format to the given print stream.
*/
! private static void dumpThreads(PrintStream ps) {
! ps.println(processId());
! ps.println(Instant.now());
! ps.println(Runtime.version());
! ps.println();
! dumpThreads(ThreadContainers.root(), ps);
}
! private static void dumpThreads(ThreadContainer container, PrintStream ps) {
! container.threads().forEach(t -> dumpThread(t, ps));
! container.children().forEach(c -> dumpThreads(c, ps));
}
! private static void dumpThread(Thread thread, PrintStream ps) {
! String suffix = thread.isVirtual() ? " virtual" : "";
! ps.println("#" + thread.threadId() + " \"" + thread.getName() + "\"" + suffix);
! for (StackTraceElement ste : thread.getStackTrace()) {
! ps.print(" ");
! ps.println(ste);
}
! ps.println();
}
/**
* Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
- *
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
*/
! public static void dumpThreadsToJson(OutputStream out) {
! BufferedOutputStream bos = new BufferedOutputStream(out);
- PrintStream ps = new PrintStream(bos, false, StandardCharsets.UTF_8);
try {
! dumpThreadsToJson(ps);
! } finally {
! ps.flush(); // flushes underlying stream
}
}
/**
! * Generate a thread dump to the given print stream in JSON format.
*/
! private static void dumpThreadsToJson(PrintStream out) {
! out.println("{");
! out.println(" \"threadDump\": {");
!
! String now = Instant.now().toString();
! String runtimeVersion = Runtime.version().toString();
! out.format(" \"processId\": \"%d\",%n", processId());
! out.format(" \"time\": \"%s\",%n", escape(now));
! out.format(" \"runtimeVersion\": \"%s\",%n", escape(runtimeVersion));
!
! out.println(" \"threadContainers\": [");
! List<ThreadContainer> containers = allContainers();
! Iterator<ThreadContainer> iterator = containers.iterator();
! while (iterator.hasNext()) {
! ThreadContainer container = iterator.next();
! boolean more = iterator.hasNext();
! dumpThreadsToJson(container, out, more);
! }
- out.println(" ]"); // end of threadContainers
-
- out.println(" }"); // end threadDump
- out.println("}"); // end object
}
/**
! * Dump the given thread container to the print stream in JSON format.
*/
! private static void dumpThreadsToJson(ThreadContainer container,
! PrintStream out,
! boolean more) {
! out.println(" {");
- out.format(" \"container\": \"%s\",%n", escape(container.toString()));
-
- ThreadContainer parent = container.parent();
- if (parent == null) {
- out.format(" \"parent\": null,%n");
- } else {
- out.format(" \"parent\": \"%s\",%n", escape(parent.toString()));
- }
Thread owner = container.owner();
! if (owner == null) {
- out.format(" \"owner\": null,%n");
- } else {
- out.format(" \"owner\": \"%d\",%n", owner.threadId());
- }
long threadCount = 0;
! out.println(" \"threads\": [");
Iterator<Thread> threads = container.threads().iterator();
while (threads.hasNext()) {
Thread thread = threads.next();
! dumpThreadToJson(thread, out, threads.hasNext());
threadCount++;
}
! out.println(" ],"); // end of threads
// thread count
if (!ThreadContainers.trackAllThreads()) {
threadCount = Long.max(threadCount, container.threadCount());
}
! out.format(" \"threadCount\": \"%d\"%n", threadCount);
! if (more) {
! out.println(" },");
! } else {
! out.println(" }"); // last container, no trailing comma
- }
}
/**
! * Dump the given thread and its stack trace to the print stream in JSON format.
*/
! private static void dumpThreadToJson(Thread thread, PrintStream out, boolean more) {
! out.println(" {");
! out.println(" \"tid\": \"" + thread.threadId() + "\",");
! out.println(" \"name\": \"" + escape(thread.getName()) + "\",");
! out.println(" \"stack\": [");
!
! int i = 0;
! StackTraceElement[] stackTrace = thread.getStackTrace();
! while (i < stackTrace.length) {
! out.print(" \"");
! out.print(escape(stackTrace[i].toString()));
! out.print("\"");
! i++;
! if (i < stackTrace.length) {
! out.println(",");
! } else {
! out.println(); // last element, no trailing comma
}
}
! out.println(" ]");
! if (more) {
! out.println(" },");
! } else {
! out.println(" }"); // last thread, no trailing comma
}
- }
! /**
! * Returns a list of all thread containers that are "reachable" from
! * the root container.
! */
! private static List<ThreadContainer> allContainers() {
! List<ThreadContainer> containers = new ArrayList<>();
! collect(ThreadContainers.root(), containers);
! return containers;
! }
! private static void collect(ThreadContainer container, List<ThreadContainer> containers) {
- containers.add(container);
- container.children().forEach(c -> collect(c, containers));
}
/**
! * Escape any characters that need to be escape in the JSON output.
*/
! private static String escape(String value) {
! StringBuilder sb = new StringBuilder();
! for (int i = 0; i < value.length(); i++) {
! char c = value.charAt(i);
! switch (c) {
! case '"' -> sb.append("\\\"");
! case '\\' -> sb.append("\\\\");
! case '/' -> sb.append("\\/");
! case '\b' -> sb.append("\\b");
! case '\f' -> sb.append("\\f");
! case '\n' -> sb.append("\\n");
! case '\r' -> sb.append("\\r");
! case '\t' -> sb.append("\\t");
! default -> {
! if (c <= 0x1f) {
! sb.append(String.format("\\u%04x", c));
! } else {
! sb.append(c);
}
}
}
}
- return sb.toString();
}
/**
* A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
* written the subsequent bytes are discarded.
}
}
/**
* Generate a thread dump in plain text or JSON format to a byte array, UTF-8 encoded.
+ * This method is the implementation of the Thread.dump_to_file diagnostic command
+ * when a file path is not specified. It returns the thread and/or message to send
+ * to the tool user.
*/
private static byte[] dumpThreadsToByteArray(boolean json, int maxSize) {
! var out = new BoundedByteArrayOutputStream(maxSize);
! try (out; var writer = new TextWriter(out)) {
if (json) {
! dumpThreadsToJson(writer);
} else {
! dumpThreads(writer);
}
! } catch (Exception ex) {
+ if (ex instanceof UncheckedIOException ioe) {
+ ex = ioe.getCause();
+ }
+ String reply = String.format("Failed: %s%n", ex);
+ return reply.getBytes(StandardCharsets.UTF_8);
}
+ return out.toByteArray();
}
/**
* Generate a thread dump in plain text or JSON format to the given file, UTF-8 encoded.
+ * This method is the implementation of the Thread.dump_to_file diagnostic command.
+ * It returns the thread and/or message to send to the tool user.
*/
private static byte[] dumpThreadsToFile(String file, boolean okayToOverwrite, boolean json) {
Path path = Path.of(file).toAbsolutePath();
OpenOption[] options = (okayToOverwrite)
? new OpenOption[0]
: new OpenOption[] { StandardOpenOption.CREATE_NEW };
String reply;
! try (OutputStream out = Files.newOutputStream(path, options)) {
! try (var writer = new TextWriter(out)) {
! if (json) {
! dumpThreadsToJson(writer);
! } else {
! dumpThreads(writer);
! }
+ reply = String.format("Created %s%n", path);
+ } catch (UncheckedIOException e) {
+ reply = String.format("Failed: %s%n", e.getCause());
}
! } catch (FileAlreadyExistsException _) {
reply = String.format("%s exists, use -overwrite to overwrite%n", path);
! } catch (Exception ex) {
! reply = String.format("Failed: %s%n", ex);
}
return reply.getBytes(StandardCharsets.UTF_8);
}
/**
! * Generate a thread dump in plain text format to the given output stream, UTF-8
! * encoded. This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
! * @throws IOException if an I/O error occurs
*/
! public static void dumpThreads(OutputStream out) throws IOException {
! var writer = new TextWriter(out);
try {
! dumpThreads(writer);
! writer.flush();
! } catch (UncheckedIOException e) {
+ IOException ioe = e.getCause();
+ throw ioe;
}
}
/**
! * Generate a thread dump in plain text format to the given text stream.
+ * @throws UncheckedIOException if an I/O error occurs
*/
! private static void dumpThreads(TextWriter writer) {
! writer.println(processId());
! writer.println(Instant.now());
! writer.println(Runtime.version());
! writer.println();
! dumpThreads(ThreadContainers.root(), writer);
}
! private static void dumpThreads(ThreadContainer container, TextWriter writer) {
! container.threads().forEach(t -> dumpThread(t, writer));
! container.children().forEach(c -> dumpThreads(c, writer));
}
! private static void dumpThread(Thread thread, TextWriter writer) {
! ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
! Instant now = Instant.now();
! Thread.State state = snapshot.threadState();
! writer.println("#" + thread.threadId() + " \"" + snapshot.threadName()
! + "\" " + state + " " + now);
+
+ // park blocker
+ Object parkBlocker = snapshot.parkBlocker();
+ if (parkBlocker != null) {
+ writer.print(" // parked on " + Objects.toIdentityString(parkBlocker));
+ if (parkBlocker instanceof AbstractOwnableSynchronizer
+ && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
+ writer.print(", owned by #" + owner.threadId());
+ }
+ writer.println();
+ }
+
+ // blocked on monitor enter or Object.wait
+ if (state == Thread.State.BLOCKED) {
+ Object obj = snapshot.blockedOn();
+ if (obj != null) {
+ writer.println(" // blocked on " + Objects.toIdentityString(obj));
+ }
+ } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+ Object obj = snapshot.waitingOn();
+ if (obj != null) {
+ writer.println(" // waiting on " + Objects.toIdentityString(obj));
+ }
}
!
+ StackTraceElement[] stackTrace = snapshot.stackTrace();
+ int depth = 0;
+ while (depth < stackTrace.length) {
+ snapshot.ownedMonitorsAt(depth).forEach(obj -> {
+ writer.print(" // locked ");
+ writer.println(Objects.toIdentityString(obj));
+ });
+ writer.print(" ");
+ writer.println(stackTrace[depth]);
+ depth++;
+ }
+ writer.println();
}
/**
* Generate a thread dump in JSON format to the given output stream, UTF-8 encoded.
* This method is invoked by HotSpotDiagnosticMXBean.dumpThreads.
+ * @throws IOException if an I/O error occurs
*/
! public static void dumpThreadsToJson(OutputStream out) throws IOException {
! var writer = new TextWriter(out);
try {
! dumpThreadsToJson(writer);
! writer.flush();
! } catch (UncheckedIOException e) {
+ IOException ioe = e.getCause();
+ throw ioe;
}
}
/**
! * Generate a thread dump to the given text stream in JSON format.
+ * @throws UncheckedIOException if an I/O error occurs
*/
! private static void dumpThreadsToJson(TextWriter textWriter) {
! var jsonWriter = new JsonWriter(textWriter);
!
! jsonWriter.startObject(); // top-level object
!
! jsonWriter.startObject("threadDump");
!
! jsonWriter.writeProperty("processId", processId());
! jsonWriter.writeProperty("time", Instant.now());
! jsonWriter.writeProperty("runtimeVersion", Runtime.version());
!
! jsonWriter.startArray("threadContainers");
! dumpThreads(ThreadContainers.root(), jsonWriter);
! jsonWriter.endArray();
!
! jsonWriter.endObject(); // threadDump
!
! jsonWriter.endObject(); // end of top-level object
}
/**
! * Write a thread container to the given JSON writer.
+ * @throws UncheckedIOException if an I/O error occurs
*/
! private static void dumpThreads(ThreadContainer container, JsonWriter jsonWriter) {
! jsonWriter.startObject();
! jsonWriter.writeProperty("container", container);
! jsonWriter.writeProperty("parent", container.parent());
Thread owner = container.owner();
! jsonWriter.writeProperty("owner", (owner != null) ? owner.threadId() : null);
long threadCount = 0;
! jsonWriter.startArray("threads");
Iterator<Thread> threads = container.threads().iterator();
while (threads.hasNext()) {
Thread thread = threads.next();
! dumpThread(thread, jsonWriter);
threadCount++;
}
! jsonWriter.endArray(); // threads
// thread count
if (!ThreadContainers.trackAllThreads()) {
threadCount = Long.max(threadCount, container.threadCount());
}
! jsonWriter.writeProperty("threadCount", threadCount);
! jsonWriter.endObject();
!
! // the children of the thread container follow
! container.children().forEach(c -> dumpThreads(c, jsonWriter));
}
/**
! * Write a thread to the given JSON writer.
+ * @throws UncheckedIOException if an I/O error occurs
*/
! private static void dumpThread(Thread thread, JsonWriter jsonWriter) {
! Instant now = Instant.now();
! ThreadSnapshot snapshot = ThreadSnapshot.of(thread);
! Thread.State state = snapshot.threadState();
! StackTraceElement[] stackTrace = snapshot.stackTrace();
!
! jsonWriter.startObject();
! jsonWriter.writeProperty("tid", thread.threadId());
! jsonWriter.writeProperty("time", now);
! if (thread.isVirtual()) {
! jsonWriter.writeProperty("virtual", Boolean.TRUE);
! }
! jsonWriter.writeProperty("name", snapshot.threadName());
! jsonWriter.writeProperty("state", state);
!
! // park blocker
! Object parkBlocker = snapshot.parkBlocker();
+ if (parkBlocker != null) {
+ jsonWriter.startObject("parkBlocker");
+ jsonWriter.writeProperty("object", Objects.toIdentityString(parkBlocker));
+ if (parkBlocker instanceof AbstractOwnableSynchronizer
+ && snapshot.exclusiveOwnerThread() instanceof Thread owner) {
+ jsonWriter.writeProperty("exclusiveOwnerThreadId", owner.threadId());
}
+ jsonWriter.endObject();
}
!
! // blocked on monitor enter or Object.wait
! if (state == Thread.State.BLOCKED) {
! Object obj = snapshot.blockedOn();
! if (obj != null) {
+ jsonWriter.writeProperty("blockedOn", Objects.toIdentityString(obj));
+ }
+ } else if (state == Thread.State.WAITING || state == Thread.State.TIMED_WAITING) {
+ Object obj = snapshot.waitingOn();
+ if (obj != null) {
+ jsonWriter.writeProperty("waitingOn", Objects.toIdentityString(obj));
+ }
}
! // stack trace
! jsonWriter.startArray("stack");
! Arrays.stream(stackTrace).forEach(jsonWriter::writeProperty);
! jsonWriter.endArray();
!
! // monitors owned, skip if none
! if (snapshot.ownsMonitors()) {
! jsonWriter.startArray("monitorsOwned");
! int depth = 0;
+ while (depth < stackTrace.length) {
+ List<Object> objs = snapshot.ownedMonitorsAt(depth).toList();
+ if (!objs.isEmpty()) {
+ jsonWriter.startObject();
+ jsonWriter.writeProperty("depth", depth);
+ jsonWriter.startArray("locks");
+ snapshot.ownedMonitorsAt(depth)
+ .map(Objects::toIdentityString)
+ .forEach(jsonWriter::writeProperty);
+ jsonWriter.endArray();
+ jsonWriter.endObject();
+ }
+ depth++;
+ }
+ jsonWriter.endArray();
+ }
+
+ // thread identifier of carrier, when mounted
+ if (thread.isVirtual() && snapshot.carrierThread() instanceof Thread carrier) {
+ jsonWriter.writeProperty("carrier", carrier.threadId());
+ }
! jsonWriter.endObject();
}
/**
! * Simple JSON writer to stream objects/arrays to a TextWriter with formatting.
+ * This class is not intended to be a fully featured JSON writer.
*/
! private static class JsonWriter {
! private static class Node {
! final boolean isArray;
! int propertyCount;
! Node(boolean isArray) {
! this.isArray = isArray;
! }
! boolean isArray() {
! return isArray;
! }
! int propertyCount() {
! return propertyCount;
! }
! int getAndIncrementPropertyCount() {
! int old = propertyCount;
! propertyCount++;
! return old;
! }
+ }
+ private final Deque<Node> stack = new ArrayDeque<>();
+ private final TextWriter writer;
+
+ JsonWriter(TextWriter writer) {
+ this.writer = writer;
+ }
+
+ private void indent() {
+ int indent = stack.size() * 2;
+ writer.print(" ".repeat(indent));
+ }
+
+ /**
+ * Start of object or array.
+ */
+ private void startObject(String name, boolean isArray) {
+ if (!stack.isEmpty()) {
+ Node node = stack.peek();
+ if (node.getAndIncrementPropertyCount() > 0) {
+ writer.println(",");
+ }
+ }
+ indent();
+ if (name != null) {
+ writer.print("\"" + name + "\": ");
+ }
+ writer.println(isArray ? "[" : "{");
+ stack.push(new Node(isArray));
+ }
+
+ /**
+ * End of object or array.
+ */
+ private void endObject(boolean isArray) {
+ Node node = stack.pop();
+ if (node.isArray() != isArray)
+ throw new IllegalStateException();
+ if (node.propertyCount() > 0) {
+ writer.println();
+ }
+ indent();
+ writer.print(isArray ? "]" : "}");
+ }
+
+ /**
+ * Write a property.
+ * @param name the property name, null for an unnamed property
+ * @param obj the value or null
+ */
+ void writeProperty(String name, Object obj) {
+ Node node = stack.peek();
+ if (node.getAndIncrementPropertyCount() > 0) {
+ writer.println(",");
+ }
+ indent();
+ if (name != null) {
+ writer.print("\"" + name + "\": ");
+ }
+ switch (obj) {
+ // Long may be larger than safe range of JSON integer value
+ case Long _ -> writer.print("\"" + obj + "\"");
+ case Number _ -> writer.print(obj);
+ case Boolean _ -> writer.print(obj);
+ case null -> writer.print("null");
+ default -> writer.print("\"" + escape(obj.toString()) + "\"");
+ }
+ }
+
+ /**
+ * Write an unnamed property.
+ */
+ void writeProperty(Object obj) {
+ writeProperty(null, obj);
+ }
+
+ /**
+ * Start named object.
+ */
+ void startObject(String name) {
+ startObject(name, false);
+ }
+
+ /**
+ * Start unnamed object.
+ */
+ void startObject() {
+ startObject(null);
+ }
+
+ /**
+ * End of object.
+ */
+ void endObject() {
+ endObject(false);
+ }
+
+ /**
+ * Start named array.
+ */
+ void startArray(String name) {
+ startObject(name, true);
+ }
+
+ /**
+ * End of array.
+ */
+ void endArray() {
+ endObject(true);
+ }
+
+ /**
+ * Escape any characters that need to be escape in the JSON output.
+ */
+ private static String escape(String value) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '"' -> sb.append("\\\"");
+ case '\\' -> sb.append("\\\\");
+ case '/' -> sb.append("\\/");
+ case '\b' -> sb.append("\\b");
+ case '\f' -> sb.append("\\f");
+ case '\n' -> sb.append("\\n");
+ case '\r' -> sb.append("\\r");
+ case '\t' -> sb.append("\\t");
+ default -> {
+ if (c <= 0x1f) {
+ sb.append(String.format("\\u%04x", c));
+ } else {
+ sb.append(c);
+ }
}
}
}
+ return sb.toString();
}
}
/**
* A ByteArrayOutputStream of bounded size. Once the maximum number of bytes is
* written the subsequent bytes are discarded.
@Override
public void close() {
}
}
+ /**
+ * Simple Writer implementation for printing text. The print/println methods
+ * throw UncheckedIOException if an I/O error occurs.
+ */
+ private static class TextWriter extends Writer {
+ private final Writer delegate;
+
+ TextWriter(OutputStream out) {
+ delegate = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ delegate.write(cbuf, off, len);
+ }
+
+ void print(Object obj) {
+ String s = String.valueOf(obj);
+ try {
+ write(s, 0, s.length());
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
+ }
+ }
+
+ void println() {
+ print(System.lineSeparator());
+ }
+
+ void println(String s) {
+ print(s);
+ println();
+ }
+
+ void println(Object obj) {
+ print(obj);
+ println();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ delegate.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+ }
+
/**
* Returns the process ID or -1 if not supported.
*/
private static long processId() {
try {
< prev index next >