core/src/main/java/com/github/olivergondza/dumpling/model/ProcessRuntime.java
public abstract class ProcessRuntime<
        RuntimeType extends ProcessRuntime<RuntimeType, SetType, ThreadType>,
        SetType extends ThreadSet<SetType, RuntimeType, ThreadType>,
        ThreadType extends ProcessThread<ThreadType, SetType, RuntimeType>
> extends ModelObject {

    private final @Nonnull SetType threads;
    private final @Nonnull SetType emptySet;

    public ProcessRuntime(@Nonnull Set<? extends ProcessThread.Builder<?>> builders) {
        this.threads = createThreads(builders);
        this.emptySet = createSet(Collections.<ThreadType>emptySet());
        checkSanity();
    }

    private @Nonnull SetType createThreads(@Nonnull Set<? extends ProcessThread.Builder<?>> builders) {
        Set<ThreadType> threads = new LinkedHashSet<ThreadType>(builders.size());
        for (ProcessThread.Builder<?> builder: builders) {
            threads.add(createThread(builder));
        }

        int buildersSize = builders.size();
        int threadsSize = threads.size();
        if (buildersSize != threadsSize) throw new IllegalRuntimeStateException(
                "%d builders produced %d threads", buildersSize, threadsSize
        );

        return createSet(Collections.unmodifiableSet(threads));
    }

    private void checkSanity() {
        // At most one thread should own the monitor/synchronizer
        HashMap<ThreadLock, ThreadType> monitors = new HashMap<ThreadLock, ThreadType>();
        HashMap<ThreadLock, ThreadType> synchronizers = new HashMap<ThreadLock, ThreadType>();
        for (ThreadType t: threads) {
            for (ThreadLock lock: t.getAcquiredMonitors()) {
                ThreadType existing = monitors.put(lock, t);
                if (existing != null) {
                    throw new IllegalRuntimeStateException(
                            "Multiple threads own the same monitor '%s':%n%s%n%nAND%n%n%s%n",
                            lock, existing, t
                    );
                }
            }

            for (ThreadLock lock: t.getAcquiredSynchronizers()) {
                ThreadType existing = synchronizers.put(lock, t);
                if (existing != null) {
                    throw new IllegalRuntimeStateException(
                            "Multiple threads own the same synchronizer '%s':%n%s%n%nAND%n%n%s%n",
                            lock, existing, t
                    );
                }
            }
        }
    }

    protected abstract @Nonnull SetType createSet(@Nonnull Set<ThreadType> threads);

    protected abstract @Nonnull ThreadType createThread(@Nonnull ProcessThread.Builder<?> builder);

    /**
     * All threads in current runtime.
     */
    public @Nonnull SetType getThreads() {
        return threads;
    }

    public @Nonnull SetType getEmptyThreadSet() {
        return emptySet;
    }

    /**
     * Instantiate {@link ThreadSet} scoped to this runtime.
     */
    public @Nonnull SetType getThreadSet(@Nonnull Collection<ThreadType> threads) {
        if (threads.isEmpty()) return emptySet;

        Set<ThreadType> threadSet = threads instanceof Set
                ? (Set<ThreadType>) threads
                : new LinkedHashSet<ThreadType>(threads)
        ;
        return createSet(threadSet);
    }

    /**
     * Run query against all threads in the runtime.
     *
     * @see ThreadSet#query(SingleThreadSetQuery)
     */
    public <T extends SingleThreadSetQuery.Result<SetType, RuntimeType, ThreadType>> T query(SingleThreadSetQuery<T> query) {
        return threads.query(query);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        threads.toString(stream, mode);
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/DisposeRule.java
public class DisposeRule implements TestRule {

    private final List<Disposable> discard = new ArrayList<Disposable>();

    public <T extends Thread> T register(final T thread) {
        if (thread == null) return thread;

        discard.add(new Disposable() {
            @Override @SuppressWarnings("deprecation")
            public void dispose() throws Exception {
                thread.stop();
            }
        });
        return thread;
    }

    public <T extends Process> T register(final T process) {
        if (process == null) return process;

        discard.add(new Disposable() {
            @Override
            public void dispose() throws Exception {
                process.destroy();
                process.waitFor();
            }
        });
        return process;
    }

    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                try {
                    base.evaluate();
                } finally {
                    for (Disposable item: discard) {
                        try {
                            item.dispose();
                        } catch (Exception ex) {
                            ex.printStackTrace(System.err);
                        }
                    }
                }
            }
        };
    }

    private interface Disposable {
        void dispose() throws Exception;
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public class Util {

    public static final @Nonnull String NL = System.getProperty("line.separator", "\n");

    public static InputStream resource(Class<?> testClass, String resource) {
        InputStream res = testClass.getResourceAsStream(testClass.getSimpleName() + "/" + resource);
        if (res == null) throw new AssertionError("Resource does not exist: " + testClass.getSimpleName() + "/" + resource);
        return res;
    }

    public static InputStream resource(String resource) {
        if (!resource.startsWith("/")) {
            resource = "/" + resource;
        }
        InputStream res = Util.class.getResourceAsStream(resource);
        if (res == null) throw new AssertionError("Resource does not exist: " + resource);
        return res;
    }

    public static File asFile(InputStream is, File file) throws IOException {
        FileWriter fw = new FileWriter(file);
        try {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) != -1) {
                fw.append(new String(buffer, 0, length));
            }
        } finally {
            fw.close();
        }

        return file;
    }

    public static File asFile(InputStream is) {
        File file = null;
        try {
            file = File.createTempFile("dumpling", "streamFile");
            file.deleteOnExit();
            return asFile(is, file);
        } catch (IOException ex) {
            if (file != null) {
                file.delete();
            }
            throw new AssertionError(ex);
        }
    }

    public static String asString(InputStream is) {
        StringBuilder sb = new StringBuilder();
        try {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) != -1) {
                sb.append(new String(buffer, 0, length));
            }
            return sb.toString();
        } catch (IOException ex) {
            throw new AssertionError(ex);
        }
    }

    public static void forwardStream(InputStream in, PrintStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }

    public static void pause(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException ex) {
        }
    }

    public static String formatTrace(String... frames) {
        StringBuilder sb = new StringBuilder();
        for (String frame: frames) {
            sb.append('\t').append(frame).append(NL);
        }

        return sb.toString();
    }

    public static String multiline(String... lines) {
        StringBuilder sb = new StringBuilder();
        for (String line: lines) {
            sb.append(line).append(NL);
        }

        return sb.toString();
    }

    public static int currentPid() {
        return Integer.parseInt(ManagementFactory.getRuntimeMXBean().getName().replaceAll("@.*", ""));
    }

    public static <T> T only(Iterable<T> set) {
        Iterator<T> it = set.iterator();
        T elem = it.next();
        if (elem == null || it.hasNext()) throw new AssertionError(
                "One set element expected: " + set
        );

        return elem;
    }

    public static String currentProcessOut(InputStream os) {
        final StringBuilder out = new StringBuilder();
        try {
            int length = os.available();
            byte[] buffer = new byte[length];
            int read = os.read(buffer, 0, length);
            if (read != -1) { // No out
                out.append(new String(buffer));
            }
        } catch (IOException ex) {
            throw new Error(ex);
        }

        return out.toString();
    }

    public static String fileContents(File file) throws IOException {
        Scanner scanner = null;
        try {
            scanner = new Scanner(file);
            return scanner.useDelimiter("\\A").next();
        } catch (NoSuchElementException ex) {
            return ""; // empty
        } finally {
            if (scanner != null) scanner.close();
        }
    }

    public static Error processTerminatedPrematurely(Process process, int exit, String line) {
        AssertionError error = new AssertionError(
                String.format(
                        "Process under test '%s' probably terminated prematurely. Exit code: %d%nSTDOUT: %s%nSTDERR: %s",
                        line,
                        exit,
                        Util.asString(process.getInputStream()), // Process exited - we are ok to blocking read the outs.
                        Util.asString(process.getErrorStream())
                )
        );
        return error;
    }

    public static ProcessBuilder processBuilder() {
        ProcessBuilder pb = new ProcessBuilder();
        try { // Inherit error out on JAVA 7 and above
            Class<?> redirect = Class.forName("java.lang.ProcessBuilder$Redirect");
            Object inherit = redirect.getDeclaredField("INHERIT").get(null);
            ProcessBuilder.class.getMethod("redirectError", redirect).invoke(pb, inherit);
            return pb;
        } catch (Throwable e) {
            System.err.println("Java 6 detected - unable to diagnose test process failures: " + e.toString());
        }

        return pb;
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/ProcessThread.java
public class ProcessThread<
        ThreadType extends ProcessThread<ThreadType, SetType, RuntimeType>,
        SetType extends ThreadSet<SetType, RuntimeType, ThreadType>,
        RuntimeType extends ProcessRuntime<RuntimeType, SetType, ThreadType>
> extends ModelObject {

    private final @Nonnull RuntimeType runtime;
    private final @Nonnull Builder<?> state;

    protected ProcessThread(@Nonnull RuntimeType runtime, @Nonnull Builder<?> builder) {
        this.runtime = runtime;
        this.state = builder.clone();

        checkSanity();
    }

    private void checkSanity() {
        if (state.name == null || state.name.isEmpty()) throw new IllegalRuntimeStateException("Thread name not set");
        if (state.status == null) throw new IllegalRuntimeStateException("Thread status not set");

        if (state.id == null && state.tid == null && state.nid == null) {
            throw new IllegalRuntimeStateException("No thread identifier set");
        }

        if (state.status.isBlocked() && state.waitingToLock == null) {
            throw new IllegalRuntimeStateException(
                    "Blocked thread does not declare monitor: >>>\n%s\n<<<\n", state
            );
        }
    }

    public @Nonnull RuntimeType getRuntime() {
        return runtime;
    }

    public @Nonnull String getName() {
        return state.name;
    }

    /**
     * Java thread id.
     *
     * @return <tt>null</tt> when not available in threaddump.
     */
    public @CheckForNull Long getId() {
        return state.id;
    }

    /**
     * Native thread id.
     *
     * @return <tt>null</tt> when not available in threaddump.
     */
    public @CheckForNull Long getNid() {
        return state.nid;
    }

    /**
     * @return <tt>null</tt> when not available in threaddump.
     */
    public @CheckForNull Long getTid() {
        return state.tid;
    }

    public @Nonnull ThreadStatus getStatus() {
        return state.status;
    }

    /**
     * {@link java.lang.Thread.State} of current thread.
     *
     * @return null if was not able to determine thread state.
     */
    public @CheckForNull Thread.State getState() {
        return state.status.getState();
    }

    public Integer getPriority() {
        return state.priority;
    }

    public boolean isDaemon() {
        return state.daemon;
    }

    public @Nonnull StackTrace getStackTrace() {
        return state.stackTrace;
    }

    /**
     * Monitor thread is waiting to be notified.
     *
     * @return null is the thread is not in {@link Object#wait()}.
     */
    public @CheckForNull ThreadLock getWaitingOnLock() {
        return state.waitingOnLock;
    }

    /**
     * Monitor thread is waiting to acquire.
     *
     * @return null then the thread is not <tt>BLOCKED</tt> acquiring the monitor.
     */
    public @CheckForNull ThreadLock getWaitingToLock() {
        return state.waitingToLock;
    }

    public @Nonnull Set<ThreadLock> getAcquiredLocks() {
        // Convert to Set not to expose duplicates
        LinkedHashSet<ThreadLock> locks = new LinkedHashSet<ThreadLock>(
                state.acquiredMonitors.size() + state.acquiredSynchronizers.size()
        );
        for (Monitor m: state.acquiredMonitors) {
            locks.add(m.getLock());
        }
        locks.addAll(state.acquiredSynchronizers);
        return locks;
    }

    public @Nonnull Set<ThreadLock> getAcquiredMonitors() {
        LinkedHashSet<ThreadLock> locks = new LinkedHashSet<ThreadLock>(state.acquiredMonitors.size());
        for (Monitor m: state.acquiredMonitors) {
            locks.add(m.getLock());
        }
        return locks;
    }

    public @Nonnull Set<ThreadLock> getAcquiredSynchronizers() {
        return new LinkedHashSet<ThreadLock>(state.acquiredSynchronizers);
    }

    /**
     * Get threads that are waiting for lock held by this thread.
     */
    public @Nonnull SetType getBlockedThreads() {
        Set<ThreadLock> acquiredMonitors = getAcquiredMonitors();

        Set<ThreadType> blocked = new LinkedHashSet<ThreadType>();
        for (ThreadType thread: runtime.getThreads()) {
            if (thread == this) continue;
            if (acquiredMonitors.contains(thread.getWaitingToLock()) || isParkingBlocking(this, thread)) {
                blocked.add(thread);
                assert thread.getBlockingThread() == this; // Verify consistency of back references
            } else {
                assert thread.getBlockingThread() != this; // Verify consistency of back references
            }
        }

        return runtime.getThreadSet(blocked);
    }

    /**
     * Some threads are blocked by other particular ones when parking, but not all parking threads are blocked by a
     * thread (we can identify). This naive implementation seems to work reasonably well. These notes might be of value:
     *
     * Not detectable:
     *
     * com.google.common.util.concurrent.AbstractFuture$Sync
     * java.util.concurrent.CountDownLatch$Sync
     * java.util.concurrent.FutureTask
     * java.util.concurrent.FutureTask$Sync
     * java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject
     * java.util.concurrent.Semaphore$NonfairSync
     * java.util.concurrent.SynchronousQueue$TransferStack (Idle ThreadPoolExecutor$Worker)
     *
     * Detectable in certain situations:
     *
     * java.util.concurrent.locks.ReentrantLock$NonfairSync
     * java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync (both write lock or write/read lock blockage)
     */
    // Cannot be a private instance method since ths and tht are statically distinct types javac fail to permit private access on - strange
    private static boolean isParkingBlocking(ProcessThread<?, ?, ?> ths, ProcessThread<?, ?, ?> tht) {
        return tht.getStatus().isParked() && ths.getAcquiredSynchronizers().contains(tht.getWaitingOnLock());
    }

    /**
     * Get threads holding lock this thread is trying to acquire.
     *
     * @return {@link ThreadSet} that contains blocked thread or empty set if this thread does not hold any lock.
     */
    public @Nonnull SetType getBlockingThreads() {
        final ThreadType blocking = getBlockingThread();
        if (blocking == null) return runtime.getEmptyThreadSet();

        return runtime.getThreadSet(Collections.singleton(blocking));
    }

    /**
     * Get thread blocking this threads execution.
     * @return Blocking thread or null if not block by a thread.
     */
    public @CheckForNull ThreadType getBlockingThread() {
        if (state.waitingToLock == null && state.waitingOnLock == null) {
            return null;
        }

        for (ThreadType thread: runtime.getThreads()) {
            if (thread == this) continue;
            Set<ThreadLock> acquired = thread.getAcquiredMonitors();
            if (acquired.contains(state.waitingToLock)) return thread;
            if (isParkingBlocking(thread, this)) return thread;
        }

        return null;
    }

    /**
     * @see #printHeader(PrintStream, Mode).
     */
    public @Nonnull String getHeader() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        toString(new PrintStream(baos), Mode.HUMAN);
        return baos.toString();
    }

    /**
     * Appends thread header to stream.
     *
     * Subclasses are encouraged to only append to the existing output rather than modifying it.
     *
     * @param stream Output.
     * @param mode Output mode.
     */
    public void printHeader(PrintStream stream, Mode mode) {
        state.printHeader(stream, mode);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        state.toString(stream, mode);
    }

    @Override
    public boolean equals(Object rhs) {
        if (rhs == null) return false;
        if (!rhs.getClass().equals(this.getClass())) return false;

        ProcessThread<?, ?, ?> other = (ProcessThread<?, ?, ?>) rhs;

        if (state.tid == null ? other.state.tid != null : !state.tid.equals(other.state.tid)) return false;
        if (state.nid == null ? other.state.nid != null : !state.nid.equals(other.state.nid)) return false;
        if (state.id == null ? other.state.id != null : !state.id.equals(other.state.id)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        Long tid = state.tid;
        if (tid == null) tid = 0L;

        Long nid = state.nid;
        if (nid == null) nid = 0L;

        Long id = state.id;
        if (id == null) id = 0L;

        return new Long(7 + 31 * tid + 17 * nid + 11 * id).hashCode();
    }

    public static class Builder<
            BuilderType extends Builder<BuilderType>
    > extends ModelObject implements Cloneable {

        private @Nonnull String name = "";
        private boolean daemon;
        // priority might not be present in threaddump
        private Integer priority;
        // https://gist.github.com/rednaxelafx/843622
        private Long id, nid, tid;
        private @Nonnull StackTrace stackTrace = new StackTrace();
        private @Nonnull ThreadStatus status = ThreadStatus.UNKNOWN;
        private @CheckForNull ThreadLock waitingToLock;
        private @CheckForNull ThreadLock waitingOnLock;
        private @Nonnull List<ThreadLock.Monitor> acquiredMonitors = Collections.emptyList();
        private @Nonnull List<ThreadLock> acquiredSynchronizers = Collections.emptyList();

        @Override
        protected @Nonnull BuilderType clone() {
            try {
                return (BuilderType) super.clone();
            } catch (CloneNotSupportedException ex) {
                throw new AssertionError();
            }
        }

        public @Nonnull BuilderType setName(@Nonnull String name) {
            this.name = name;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setId(long id) {
            this.id = id;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setNid(long nid) {
            this.nid = nid;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setTid(long tid) {
            this.tid = tid;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setDaemon(boolean daemon) {
            this.daemon = daemon;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setPriority(Integer priority) {
            this.priority = priority;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setStacktrace(@Nonnull StackTraceElement... stackTrace) {
            this.stackTrace = new StackTrace(stackTrace);
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setStacktrace(@Nonnull StackTrace stackTrace) {
            this.stackTrace = stackTrace;
            return (BuilderType) this;
        }

        public @Nonnull StackTrace getStacktrace() {
            return stackTrace;
        }

        public @Nonnull BuilderType setThreadStatus(@Nonnull ThreadStatus status) {
            this.status = status;
            return (BuilderType) this;
        }

        public ThreadStatus getThreadStatus() {
            return status;
        }

        public @Nonnull BuilderType setWaitingOnLock(ThreadLock lock) {
            this.waitingOnLock = lock;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setWaitingToLock(ThreadLock lock) {
            this.waitingToLock = lock;
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setAcquiredSynchronizers(List<ThreadLock> synchronizers) {
            this.acquiredSynchronizers = Collections.unmodifiableList(synchronizers);
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setAcquiredSynchronizers(ThreadLock... synchronizers) {
            List<ThreadLock> data = new ArrayList<ThreadLock>(synchronizers.length);
            Collections.addAll(data, synchronizers);
            return setAcquiredSynchronizers(data);
        }

        public @Nonnull BuilderType setAcquiredMonitors(List<ThreadLock.Monitor> monitors) {
            this.acquiredMonitors = Collections.unmodifiableList(monitors);
            return (BuilderType) this;
        }

        public @Nonnull BuilderType setAcquiredMonitors(ThreadLock.Monitor... monitors) {
            ArrayList<Monitor> data = new ArrayList<ThreadLock.Monitor>(monitors.length);
            Collections.addAll(data, monitors);
            return setAcquiredMonitors(data);
        }

        public @Nonnull List<Monitor> getAcquiredMonitors() {
            return new ArrayList<Monitor>(acquiredMonitors);
        }

        private List<ThreadLock> getMonitorsByDepth(int depth) {
            List<ThreadLock> monitors = new ArrayList<ThreadLock>();

            for (Monitor monitor: acquiredMonitors) {
                if (monitor.getDepth() == depth) {
                    monitors.add(monitor.getLock());
                }
            }

            return monitors;
        }

        @Override
        public void toString(@Nonnull PrintStream stream, @Nonnull Mode mode) {
            printHeader(stream, mode);
            stream.format("%n   java.lang.Thread.State: %s", status.getName());

            int depth = 0;
            for (StackTraceElement traceLine: stackTrace.getElements()) {
                printTraceElement(stream, traceLine);

                if (depth == 0) {
                    if (waitingToLock != null) {
                        stream.println();
                        stream.append("\t- ").append(waitingVerb()).append(' ');
                        waitingToLock.toString(stream, mode);
                    }
                    if (waitingOnLock != null) {
                        stream.println();
                        stream.append("\t- ").append(waitingVerb()).append(' ');
                        waitingOnLock.toString(stream, mode);
                    }
                }

                for (ThreadLock monitor: getMonitorsByDepth(depth)) {
                    stream.println();
                    stream.append("\t- locked ");
                    monitor.toString(stream, mode);
                }

                depth++;
            }

            if (!acquiredSynchronizers.isEmpty()) {
                stream.format("%n%n   Locked ownable synchronizers:%n");
                for (ThreadLock synchronizer: acquiredSynchronizers) {
                    stream.append("\t- ");
                    synchronizer.toString(stream, mode);
                    stream.println();
                }
            }
        }

        private String waitingVerb() {
            if (status.isParked()) return "parking to wait for";
            if (status.isWaiting()) return "waiting on";
            if (status.isBlocked()) return "waiting to lock";

            throw new AssertionError(status + " thread can not declare a lock: " + name);
        }

        // Stolen from StackTraceElement#toString() in Java 8 to prevent the new fields from Java 9+ to be printed
        // as they can not be parsed correctly at the moment. Note this affect JMX/JVM factory reparsing only - threaddump is fine
        private void printTraceElement(PrintStream stream, StackTraceElement traceLine) {
            String fileName = traceLine.getFileName();
            int lineNumber = traceLine.getLineNumber();
            String source = traceLine.isNativeMethod() ? "(Native Method)":
                    (fileName != null && lineNumber >= 0 ?
                            "(" + fileName + ":" + lineNumber + ")":
                            (fileName != null ? "(" + fileName + ")": "(Unknown Source)"));
            stream.format("%n\tat %s.%s%s", traceLine.getClassName(), traceLine.getMethodName(), source);
        }

        /**
         * Appends thread header to stream.
         *
         * Subclasses are encouraged to only append to the existing output rather than modifying it.
         *
         * @param stream Output.
         * @param mode Output mode.
         */
        protected void printHeader(PrintStream stream, Mode mode) {
            stream.append('"').append(name).append('"');
            if (id != null) stream.append(" #").append(id.toString());
            if (daemon) stream.append(" daemon");
            if (priority != null) stream.append(" prio=").append(priority.toString());

            if (tid != null) {
                String format = !mode.isHuman() ? "0x%016x": "0x%x";
                stream.append(" tid=").format(format, tid);
            }
            if (nid != null) {
                String format = mode.isHuman() ? "%d" : "0x%x";
                stream.append(" nid=").format(format, nid);
            }
        }
    }

    /**
     * {@link ProcessThread} predicate.
     *
     * @author ogondza
     * @see ThreadSet#where(ProcessThread.Predicate)
     */
    public interface Predicate {
        boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread);
    }

    /**
     * Match thread by name.
     */
    public static @Nonnull Predicate nameIs(final @Nonnull String name) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                return thread.getName().equals(name);
            }
        };
    }

    /**
     * Match thread its name contains pattern.
     */
    public static @Nonnull Predicate nameContains(final @Nonnull Pattern pattern) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                return pattern.matcher(thread.getName()).find();
            }
        };
    }

    /**
     * Match thread its name contains string.
     */
    public static @Nonnull Predicate nameContains(final @Nonnull String pattern) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                return thread.getName().contains(pattern);
            }
        };
    }

    /**
     * Match waiting thread waiting for given thread to be notified.
     */
    public static @Nonnull Predicate waitingOnLock(final @Nonnull String className) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                final ThreadLock lock = thread.getWaitingOnLock();
                return lock != null && lock.getClassName().equals(className);
            }
        };
    }

    /**
     * Match thread that is waiting on lock identified by <tt>className</tt>.
     */
    public static @Nonnull Predicate waitingToLock(final @Nonnull String className) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                final ThreadLock lock = thread.getWaitingToLock();
                return lock != null && lock.getClassName().equals(className);
            }
        };
    }

    /**
     * Match thread that has acquired lock identified by <tt>className</tt>.
     */
    public static @Nonnull Predicate acquiredLock(final @Nonnull String className) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                for (ThreadLock lock: thread.getAcquiredLocks()) {
                    if (lock.getClassName().equals(className)) return true;
                }
                return false;
            }
        };
    }

    /**
     * Match thread its stacktrace contains frame that exactly matches pattern.
     *
     * @param pattern Fully qualified method name like "com.github.olivergondza.dumpling.model.ProcessThread.evaluating".
     */
    public static @Nonnull Predicate evaluating(final @Nonnull String pattern) {
        return new Predicate() {
            @Override
            public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
                for (StackTraceElement element : thread.getStackTrace().getElements()) {
                    if ((element.getClassName() + "." + element.getMethodName()).equals(pattern)) return true;
                }
                return false;
            }
        };
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/dump/ThreadDumpRuntime.java
public final class ThreadDumpRuntime extends ProcessRuntime<ThreadDumpRuntime, ThreadDumpThreadSet, ThreadDumpThread> {

    /**
     * Threaddump header, either empty or terminated wit blank line so it can be prepended to the ThreadSet.
     */
    private final @Nonnull List<String> header;

    public ThreadDumpRuntime(@Nonnull Set<ThreadDumpThread.Builder> builders, @Nonnull List<String> header) {
        super(builders);
        this.header = new ArrayList<String>(header);
    }

    @Override
    protected ThreadDumpThreadSet createSet(Set<ThreadDumpThread> threads) {
        return new ThreadDumpThreadSet(this, threads);
    }

    @Override
    protected ThreadDumpThread createThread(Builder<?> builder) {
        return new ThreadDumpThread(this, (ThreadDumpThread.Builder) builder);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        if (!header.isEmpty()) {
            for (String line: header) {
                stream.println(line);
            }
            stream.println();
        }

        super.toString(stream, mode);
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/ThreadStatus.java
public enum ThreadStatus {
    NEW                 ("NEW",                                 State.NEW),
    RUNNABLE            ("RUNNABLE",                            State.RUNNABLE),
    SLEEPING            ("TIMED_WAITING (sleeping)",            State.TIMED_WAITING),

    /**
     * Thread in {@link Object#wait()}.
     *
     * @see java.lang.Thread.State#WAITING
     */
    IN_OBJECT_WAIT      ("WAITING (on object monitor)",         State.WAITING),

    /**
     * Thread in {@link Object#wait(long)}.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    IN_OBJECT_WAIT_TIMED("TIMED_WAITING (on object monitor)",   State.TIMED_WAITING),

    /**
     * Thread in {@link LockSupport#park()}.
     *
     * @see java.lang.Thread.State#WAITING
     */
    PARKED              ("WAITING (parking)",                   State.WAITING),

    /**
     * Thread in {@link LockSupport#parkNanos}.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    PARKED_TIMED        ("TIMED_WAITING (parking)",             State.TIMED_WAITING),

    BLOCKED             ("BLOCKED (on object monitor)",         State.BLOCKED),
    TERMINATED          ("TERMINATED",                          State.TERMINATED),

    /**
     * Runtime factory was not able to determine thread state.
     *
     * This can happen for service threads in threaddump.
     */
    UNKNOWN             ("UNKNOWN",                             null);

    /**
     * Description used in thread dump.
     */
    private final @Nonnull String name;

    /**
     * Matching java.lang.Thread.State.
     */
    private final State state;

    ThreadStatus(@Nonnull String name, State state) {
        this.name = name;
        this.state = state;
    }

    public @Nonnull String getName() {
        return name;
    }

    public @CheckForNull State getState() {
        return state;
    }

    /**
     * Newly create thread
     *
     * @see java.lang.Thread.State#NEW
     */
    public boolean isNew() {
        return this == NEW;
    }

    /**
     * Thread that is runnable / running.
     *
     * @see java.lang.Thread.State#RUNNABLE
     */
    public boolean isRunnable() {
        return this == RUNNABLE;
    }

    /**
     * Thread in {@link Thread#sleep(long)} or {@link Thread#sleep(long, int)} waiting to be notified.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isSleeping() {
        return this == SLEEPING;
    }

    /**
     * Thread in {@link Object#wait()} or {@link Object#wait(long)}.
     *
     * @see java.lang.Thread.State#WAITING
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isWaiting() {
        return this == IN_OBJECT_WAIT || this == IN_OBJECT_WAIT_TIMED;
    }

    /**
     * Thread in {@link LockSupport#park()} or {@link LockSupport#parkNanos}.
     *
     * @see java.lang.Thread.State#WAITING
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isParked() {
        return this == PARKED || this == PARKED_TIMED;
    }

    /**
     * Thread (re-)entering a synchronization block.
     *
     * @see java.lang.Thread.State#BLOCKED
     */
    public boolean isBlocked() {
        return this == BLOCKED;
    }

    /**
     * Thread that terminated execution.
     *
     * @see java.lang.Thread.State#TERMINATED
     */
    public boolean isTerminated() {
        return this == TERMINATED;
    }

    public static @Nonnull ThreadStatus fromString(String title) {
        try {

            @SuppressWarnings("null")
            final @Nonnull ThreadStatus value = Enum.valueOf(ThreadStatus.class, title);
            return value;
        } catch (IllegalArgumentException ex) {

            for (ThreadStatus value: values()) {
                if (value.getName().equals(title)) return value;
            }

            throw ex;
        }
    }

    public static @Nonnull ThreadStatus fromState(@Nonnull Thread.State state, @CheckForNull StackTraceElement head) {
        switch (state) {
            case NEW: return NEW;
            case RUNNABLE: return RUNNABLE;
            case BLOCKED: return BLOCKED;
            case WAITING: return waitingState(false, head); // Not exact
            case TIMED_WAITING: return waitingState(true, head); // Not exact
            case TERMINATED: return TERMINATED;
            default: return UNKNOWN;
        }
    }

    private static @Nonnull ThreadStatus waitingState(boolean timed, @CheckForNull StackTraceElement head) {
        if (head == null) return ThreadStatus.UNKNOWN;
        if ("sleep".equals(head.getMethodName()) && "java.lang.Thread".equals(head.getClassName())) return SLEEPING;
        if ("wait".equals(head.getMethodName()) && "java.lang.Object".equals(head.getClassName())) {
            return timed ? IN_OBJECT_WAIT_TIMED : IN_OBJECT_WAIT;
        }

        if ("park".equals(head.getMethodName()) && UNSAFE.contains(head.getClassName())) {
            return timed ? PARKED_TIMED : PARKED;
        }

        throw new AssertionError("Unable to infer ThreadStatus from WAITING state in " + head);
    }

    public static final List<String> UNSAFE = Arrays.asList(
            "sun.misc.Unsafe", // hotspot JDK 8 and older
            "jdk.internal.misc.Unsafe" // hotspot JDK 9
    );
}
core/src/main/java/com/github/olivergondza/dumpling/model/StackTrace.java
public class StackTrace extends ModelObject {

    public static StackTraceElement element(String declaringClass, String methodName, String fileName, int lineNumber) {
        return new StackTraceElement(declaringClass, methodName, fileName, lineNumber);
    }

    /**
     * Get {@link StackTraceElement} its source line number is unknown.
     */
    public static StackTraceElement element(String declaringClass, String methodName, String fileName) {
        return new StackTraceElement(declaringClass, methodName, fileName, -1);
    }

    /**
     * Get {@link StackTraceElement} its source (file name and line number) is unknown.
     */
    public static StackTraceElement element(String declaringClass, String methodName) {
        return new StackTraceElement(declaringClass, methodName, null, -1);
    }

    /**
     * Get {@link StackTraceElement} for native method.
     */
    public static StackTraceElement nativeElement(String declaringClass, String methodName) {
        return new StackTraceElement(declaringClass, methodName, null, -2);
    }

    /**
     * Get {@link StackTraceElement} for native method.
     *
     * Some native method {@link StackTraceElement}s have filename, even though it is not shown.
     */
    public static StackTraceElement nativeElement(String declaringClass, String methodName, String fileName) {
        return new StackTraceElement(declaringClass, methodName, fileName, -2);
    }

    private @Nonnull StackTraceElement[] elements;

    public StackTrace(@Nonnull StackTraceElement... elements) {
        this.elements = elements.clone(); // Shallow copy is ok here as StackTraceElement is immutable
    }

    public StackTrace(@Nonnull List<StackTraceElement> elements) {
        this.elements = elements.toArray(new StackTraceElement[elements.size()]);
    }

    public int size() {
        return elements.length;
    }

    /**
     * Get element of given stack depth.
     *
     * @return Stack element of null if not present.
     */
    public @CheckForNull StackTraceElement getElement(@Nonnegative int depth) {
        if (depth < 0) throw new ArrayIndexOutOfBoundsException(depth);

        return elements.length > depth
                ? elements[depth]
                : null
        ;
    }

    /**
     * Get innermost stack frame or null when there is no trace attached.
     */
    public @CheckForNull StackTraceElement getHead() {
        return getElement(0);
    }

    public @CheckForNull StackTraceElement head() {
        return getElement(0);
    }

    /**
     * Get all the stack trace elements.
     */
    public @Nonnull List<StackTraceElement> getElements() {
        return Arrays.asList(elements);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        for (StackTraceElement e: elements) {
            stream.println();
            stream.append("\tat ").append(e.toString());
        }

        stream.println();
    }

    @Override
    public int hashCode() {
        return 31 * Arrays.hashCode(elements);
    }

    @Override
    public boolean equals(Object rhs) {
        if (this == rhs) return true;
        if (rhs == null) return false;
        if (getClass() != rhs.getClass()) return false;

        StackTrace other = (StackTrace) rhs;
        if (!Arrays.equals(elements, other.elements)) return false;
        return true;
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/TestThread.java
public final class TestThread {

    public static final @Nonnull String JMX_HOST = "localhost";
    public static final @Nonnull String JMX_USER = "user";
    public static final @Nonnull String JMX_PASSWD = "secret_passwd";

    public static final String MARKER = "DUMPLING-SUT-IS-READY";

    // Observable process entry point - not to be invoked directly
    public static synchronized void main(String... args) throws InterruptedException {
        runThread();

        System.out.println(MARKER);

        // Block process forever
        TestThread.class.wait();
        throw new Error("Should never get here");
    }

    public static Thread runThread() {
        final CountDownLatch cdl = new CountDownLatch(1);
        Thread thread = new Thread("remotely-observed-thread") {
            @Override
            public synchronized void run() {
                try {
                    cdl.countDown();
                    this.wait();
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        };
        thread.setDaemon(true);
        thread.setPriority(7);
        thread.start();

        try {
            cdl.await();
        } catch (InterruptedException ex) {
            throw new AssertionError(ex);
        }

        return thread;
    }

    public static Thread setupSleepingThreadWithLock() {
        final ReentrantLock lock = new ReentrantLock();
        Thread thread = new Thread("sleepingThreadWithLock") {
            @Override
            public void run() {
                lock.lock();
                pause(10000);
            }
        };
        thread.start();
        while(!lock.isLocked()) {
            pause(1000);
        }
        return thread;
    }

    private static int getFreePort() throws IOException {
        ServerSocket serverSocket = new ServerSocket(0);
        try {
            return serverSocket.getLocalPort();
        } finally {
            serverSocket.close();
        }
    }

    /* Client is expected to dispose the thread */
    public static JMXProcess runJmxObservableProcess(boolean auth) throws Exception {
        int jmxPort = getFreePort();

        String //cp = "target/test-classes:target/classes"; // Current module
        // Use file to convert URI to a path platform FS would understand,
        cp = new File(TestThread.class.getProtectionDomain().getCodeSource().getLocation().toURI().getSchemeSpecificPart()).getAbsolutePath();
        List<String> args = new ArrayList<String>();
        args.add("java");
        args.add("-cp");
        args.add(cp);
        args.add("-Djava.util.logging.config.file=" + Util.asFile(Util.resource(TestThread.class, "logging.properties")).getAbsolutePath());
        args.add("-Dcom.sun.management.jmxremote");
        args.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
        args.add("-Dcom.sun.management.jmxremote.local.only=false");
        args.add("-Dcom.sun.management.jmxremote.authenticate=" + auth);
        args.add("-Dcom.sun.management.jmxremote.ssl=false");
        args.add("-Djava.rmi.server.hostname=127.0.0.1");
        if (auth) {
            args.add("-Dcom.sun.management.jmxremote.password.file=" + getCredFile("jmxremote.password"));
            args.add("-Dcom.sun.management.jmxremote.access.file=" + getCredFile("jmxremote.access"));
        }
        args.add("com.github.olivergondza.dumpling.TestThread");
        ProcessBuilder pb = processBuilder().command(args);
        String processLine = pb.command().toString();
        final Process process = pb.start();

        BufferedInputStream bis = new BufferedInputStream(process.getInputStream());
        bis.mark(1024 * 1024 * 1);
        String out = "never_tried";
        for (int i = 0; i < 10; i++) {
            out = isUp(bis);
            if (out != null) return new JMXProcess(process, jmxPort);

            try {
                int exit = process.exitValue();
                throw processTerminatedPrematurely(process, exit, processLine);
            } catch (IllegalThreadStateException ex) {
                // Still running
            }

            Thread.sleep(500);
        }

        throw new AssertionError("Unable to bring to SUT up in time: " + out);
    }

    public static final class JMXProcess extends Process {

        private final Process p;
        public final int JMX_PORT;
        public final @Nonnull String JMX_CONNECTION;
        public final @Nonnull String JMX_AUTH_CONNECTION;

        public JMXProcess(Process p, int port) {
            this.p = p;
            JMX_PORT = port;
            JMX_CONNECTION = JMX_HOST + ":" + JMX_PORT;
            JMX_AUTH_CONNECTION = JMX_USER + ":" + JMX_PASSWD + "@" + JMX_CONNECTION;
        }

        @Override public OutputStream getOutputStream() {
            return p.getOutputStream();
        }

        @Override public InputStream getInputStream() {
            return p.getInputStream();
        }

        @Override public InputStream getErrorStream() {
            return p.getErrorStream();
        }

        @Override public int waitFor() throws InterruptedException {
            return p.waitFor();
        }

        @Override public int exitValue() {
            return p.exitValue();
        }

        @Override public void destroy() {
            p.destroy();
        }

        public Process getNativeProcess() {
            return p;
        }

        // Copy&paste from PidRuntimeFactory
        public long pid() {
            Throwable problem = null;
            try {
                Method pidMethod = Process.class.getMethod("pid");
                return (long) (Long) pidMethod.invoke(p);
            } catch (NoSuchMethodException e) {
                // Not Java 9 - Fallback
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof UnsupportedOperationException) {
                    // Process impl might not support management - Fallback
                    problem = cause;
                } else if (cause instanceof SecurityException) {
                    // Access to monitoring is rejected - Fallback
                    problem = cause;
                } else {
                    throw new AssertionError(e);
                }
            }

            // Fallback for Java6+ on unix. This is known not to work for Java8 on Windows.
            if (!"java.lang.UNIXProcess".equals(p.getClass().getName())) throw new UnsupportedOperationException(
                    "Unknown java.lang.Process implementation: " + p.getClass().getName(),
                    problem
            );

            try {
                // Protected class
                Class<?>  clazz = Class.forName("java.lang.UNIXProcess");
                Field pidField = clazz.getDeclaredField("pid");
                pidField.setAccessible(true);
                return pidField.getLong(p);
            } catch (ClassNotFoundException e) {
                throw new UnsupportedOperationException("Unable to find java.lang.UNIXProcess", e);
            } catch (NoSuchFieldException e) {
                throw new UnsupportedOperationException("Unable to find java.lang.UNIXProcess.pid", e);
            } catch (IllegalAccessException e) {
                throw new UnsupportedOperationException("Unable to access java.lang.UNIXProcess.pid", e);
            }
        }
    }

    private static String isUp(BufferedInputStream is) throws IOException {
        is.reset();
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) != -1) {
            String line = new String(buffer, 0, length);
            if (line.contains(MARKER)) return line;
        }

        return null;
    }

    private static String getCredFile(String path) throws Exception {
        String file = Util.asFile(Util.resource(TestThread.class, path)).getAbsolutePath();
        // Workaround http://jira.codehaus.org/browse/MRESOURCES-132
        Process process = new ProcessBuilder("chmod", "600", file).start();
        if (process.waitFor() != 0) {
            throw new RuntimeException(
                    "Failed to adjust permissions: " + Util.asString(process.getErrorStream())
            );
        }
        return file;
    }
}
core/src/main/java/com/github/olivergondza/dumpling/factory/ThreadDumpFactory.java
public class ThreadDumpFactory {

    private static final Logger LOG = Logger.getLogger(ThreadDumpFactory.class.getName());

    private static final StackTraceElement WAIT_TRACE_ELEMENT = StackTrace.nativeElement("java.lang.Object", "wait");

    private static final String NL = "(?:\\r\\n|\\n)";
    private static final String LOCK_SUBPATTERN = "<(?:0x)?(\\w+)> \\(a ([^\\)]+)\\)";

    private static final Pattern THREAD_DELIMITER = Pattern.compile(NL + "(?:" + NL + "(?!\\s)|(?=\"))");
    // TODO the regex is ignoring module name and version at the time: java.lang.Thread.sleep(java.base@9-ea/Native Method)
    private static final Pattern STACK_TRACE_ELEMENT_LINE = Pattern.compile(" *at (\\S+)\\.(\\S+)\\((?:.+/)?([^:]+?)(\\:\\d+)?\\)");
    private static final Pattern ACQUIRED_LINE = Pattern.compile("- locked " + LOCK_SUBPATTERN);
    // Oracle/OpenJdk puts unnecessary space after 'parking to wait for'
    private static final Pattern WAITING_ON_LINE = Pattern.compile("- (?:waiting on|parking to wait for ?) " + LOCK_SUBPATTERN);
    private static final Pattern WAITING_TO_LOCK_LINE = Pattern.compile("- waiting to lock " + LOCK_SUBPATTERN);
    private static final Pattern OWNABLE_SYNCHRONIZER_LINE = Pattern.compile("- " + LOCK_SUBPATTERN);
    private static final Pattern THREAD_HEADER = Pattern.compile(
            "^\"(.*)\" ([^\\n\\r]+)(?:" + NL + "\\s+java.lang.Thread.State: ([^\\n\\r]+)(?:" + NL + "(.+))?)?",
            Pattern.DOTALL
    );

    private boolean failOnErrors = false;

    /**
     * Historically, dumpling tolerates some of the errors silently.
     *
     * Turning this on will replace log records for failures to parse the threaddump.
     */
    public ThreadDumpFactory failOnErrors(boolean failOnErrors) {
        this.failOnErrors = failOnErrors;
        return this;
    }

    /**
     * Create runtime from thread dump.
     *
     * @throws IOException File could not be loaded.
     */
    public @Nonnull ThreadDumpRuntime fromFile(@Nonnull File threadDump) throws IOException {
        FileInputStream fis = new FileInputStream(threadDump);
        try {
            return fromStream(fis);
        } finally {
            fis.close();
        }
    }

    public @Nonnull ThreadDumpRuntime fromStream(@Nonnull InputStream stream) {
        Set<ThreadDumpThread.Builder> threads = new LinkedHashSet<ThreadDumpThread.Builder>();
        List<String> header = new ArrayList<String>();

        Scanner scanner = new Scanner(stream);
        scanner.useDelimiter(THREAD_DELIMITER);
        try {
            while (scanner.hasNext()) {
                String singleChunk = scanner.next();
                // Java until 8 vs. Java after 9
                if (singleChunk.startsWith("JNI global references") || singleChunk.startsWith("JNI global refs")) {
                    // Nothing interesting is expected after this point. Also, this is a convenient way to eliminate the
                    // deadlock report that is spread over several chunks
                    break;
                }

                ThreadDumpThread.Builder thread = thread(singleChunk);
                if (thread != null) {
                    threads.add(thread);
                    continue;
                }

                if (header.isEmpty()) { // Still reading header
                    header.addAll(Arrays.asList(singleChunk.split(NL)));
                    continue;
                }

                // New info in Java 9
                if (singleChunk.startsWith("Threads class SMR info:")) {
                    continue;
                }

                String msg = "Skipping unrecognized chunk: >>>" + singleChunk + "<<<";
                if (failOnErrors) {
                    throw new IllegalRuntimeStateException(msg);
                } else {
                    LOG.warning(msg);
                }
            }
        } finally {
            scanner.close();
        }

        if (threads.isEmpty()) throw new IllegalRuntimeStateException(
                "No threads found in threaddump"
        );

        return new ThreadDumpRuntime(threads, header);
    }

    public @Nonnull ThreadDumpRuntime fromString(@Nonnull String runtime) {
        try {
            InputStream is = new ByteArrayInputStream(runtime.getBytes("UTF-8"));
            try {
                return fromStream(is);
            } finally {
                try {
                    is.close();
                } catch (IOException ex) {} // Ignore
            }
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex);
        }
    }

    private ThreadDumpThread.Builder thread(String singleThread) {

        Matcher matcher = THREAD_HEADER.matcher(singleThread);
        if (!matcher.find()) return null;

        ThreadDumpThread.Builder builder = new ThreadDumpThread.Builder();
        builder.setName(matcher.group(1));
        initHeader(builder, matcher.group(2));

        String status = matcher.group(3);
        if (status != null) {
            builder.setThreadStatus(ThreadStatus.fromString(status));
        }

        final String trace = matcher.group(4);
        if (trace != null) {
            builder = initStacktrace(builder, trace, singleThread);
        }

        return builder;
    }

    private Builder initStacktrace(Builder builder, String trace, String wholeThread) {
        ArrayList<StackTraceElement> traceElements = new ArrayList<StackTraceElement>();

        List<ThreadLock.Monitor> monitors = new ArrayList<ThreadLock.Monitor>();
        List<ThreadLock> synchronizers = new ArrayList<ThreadLock>();
        ThreadLock waitingToLock = null; // Block waiting on monitor
        ThreadLock waitingOnLock = null; // in Object.wait()
        int depth = -1;

        StringTokenizer tokenizer = new StringTokenizer(trace, "\n");
        while (tokenizer.hasMoreTokens()) {
            String line = tokenizer.nextToken();

            StackTraceElement elem = traceElement(line);
            if (elem != null) {
                traceElements.add(elem);
                depth++;
                continue;
            }

            Matcher acquiredMatcher = ACQUIRED_LINE.matcher(line);
            if (acquiredMatcher.find()) {
                monitors.add(new ThreadLock.Monitor(createLock(acquiredMatcher), depth));
                continue;
            }

            Matcher waitingToMatcher = WAITING_TO_LOCK_LINE.matcher(line);
            if (waitingToMatcher.find()) {
                if (waitingToLock != null) throw new IllegalRuntimeStateException(
                        "Waiting to lock reported several times per single thread >>>%n%s%n<<<%n", trace
                );
                waitingToLock = createLock(waitingToMatcher);
                continue;
            }

            Matcher waitingOnMatcher = WAITING_ON_LINE.matcher(line);
            if (waitingOnMatcher.find()) {
                if (waitingOnLock != null) throw new IllegalRuntimeStateException(
                        "Waiting on lock reported several times per single thread >>>%n%s%n<<<%n", trace
                );
                waitingOnLock = createLock(waitingOnMatcher);
                continue;
            }

            if (line.contains("Locked ownable synchronizers:")) {
                while (tokenizer.hasMoreTokens()) {
                    line = tokenizer.nextToken();

                    if (line.contains("- None")) break;
                    Matcher matcher = OWNABLE_SYNCHRONIZER_LINE.matcher(line);
                    if (matcher.find()) {
                        synchronizers.add(createLock(matcher));
                    } else {
                        throw new IllegalRuntimeStateException("Unable to parse ownable synchronizer: " + line);
                    }
                }
            }
        }

        builder.setStacktrace(new StackTrace(traceElements));

        ThreadStatus status = builder.getThreadStatus();
        StackTraceElement innerFrame = builder.getStacktrace().getElement(0);

        // Probably a bug in JVM/jstack but let's see what we can do
        if (waitingOnLock == null && !status.isRunnable() && WAIT_TRACE_ELEMENT.equals(innerFrame)) {
            HashSet<ThreadLock> acquiredLocks = new HashSet<ThreadLock>(monitors.size());
            for (Monitor m: monitors) {
                acquiredLocks.add(m.getLock());
            }
            if (acquiredLocks.size() == 1) {
                waitingOnLock = acquiredLocks.iterator().next();
                LOG.fine("FIXUP: Adjust lock state from 'locked' to 'waiting on' when thread entering Object.wait()");
                LOG.fine(wholeThread);
            }
        }

        if (waitingOnLock != null) {
            // Eliminate self lock that is presented in threaddumps when in Object.wait(). It is a matter or convenience - not really a FIXUP
            filterMonitors(monitors, waitingOnLock);

            // 'waiting on' is reported even when blocked re-entering the monitor. Convert it from waitingOn to waitingTo
            if (builder.getThreadStatus().isBlocked()) {
                LOG.fine("FIXUP: Adjust lock state from 'waiting on' to 'waiting to' when thread re-acquiring the monitor after Object.wait()");
                LOG.fine(wholeThread);
                waitingToLock = waitingOnLock;
                waitingOnLock = null;
            }
        }

        // https://github.com/olivergondza/dumpling/issues/43
        if (waitingOnLock != null && status.isRunnable()) {
            // Presumably when entering or leaving the parked state.
            // Remove the lock instead of fixing the thread status as there is
            // no general way to tell PARKED and PARKED_TIMED apart.
            LOG.fine("FIXUP: Remove 'waiting to' lock declared on RUNNABLE thread");
            LOG.fine(wholeThread);
            waitingOnLock = null;
        }

        // https://github.com/olivergondza/dumpling/issues/46
        // The lock state is changed ahead of the thread state while there can be other threads still holding the monitor
        if (status.isBlocked() && waitingToLock == null) {
            Monitor monitor = getMonitorJustAcquired(monitors);
            if (monitor != null) {
                LOG.fine("FIXUP: Adjust lock state from 'locked' to 'waiting to' on BLOCKED thread");
                LOG.fine(wholeThread);
                waitingToLock = monitor.getLock();
                monitors.remove(0);
            } else {
                LOG.fine("FIXUP: Adjust thread state from 'BLOCKED' to 'RUNNABLE' when monitor is missing");
                LOG.fine(wholeThread);
                builder.setThreadStatus(status = ThreadStatus.RUNNABLE);
            }
        }

        if (waitingToLock != null && !status.isBlocked()) throw new IllegalRuntimeStateException(
                "%s thread declares waitingTo lock: >>>%n%s%n<<<%n", status, wholeThread
        );
        if (waitingOnLock != null && !status.isWaiting() && !status.isParked()) throw new IllegalRuntimeStateException(
                "%s thread declares waitingOn lock: >>>%n%s%n<<<%n", status, wholeThread
        );

        builder.setAcquiredMonitors(monitors);
        builder.setAcquiredSynchronizers(synchronizers);
        builder.setWaitingToLock(waitingToLock);
        builder.setWaitingOnLock(waitingOnLock);

        return builder;
    }

    // get monitor acquired on current stackframe, null when it was acquired earlier or not monitor is held
    private Monitor getMonitorJustAcquired(List<ThreadLock.Monitor> monitors) {
        if (monitors.isEmpty()) return null;
        Monitor monitor = monitors.get(0);
        if (monitor.getDepth() != 0) return null;

        for (Monitor duplicateCandidate: monitors) {
            if (monitor.equals(duplicateCandidate)) continue; // skip first - equality includes monitor depth

            if (monitor.getLock().equals(duplicateCandidate.getLock())) return null; // Acquired earlier
        }

        return monitor;
    }

    private static final WeakHashMap<String, StackTraceElement> traceElementCache = new WeakHashMap<String, StackTraceElement>();
    private StackTraceElement traceElement(String line) {
        if (!line.startsWith("\tat ") && !line.matches("(  )+at .*")) return null;

        StackTraceElement cached = traceElementCache.get(line);
        if (cached != null) return cached;

        Matcher match = STACK_TRACE_ELEMENT_LINE.matcher(line);
        if (!match.find()) return null;

        String sourceFile = match.group(3);
        int sourceLine = match.group(4) == null
                ? -1
                : Integer.parseInt(match.group(4).substring(1))
        ;

        if (sourceLine == -1 && "Native Method".equals(match.group(3))) {
            sourceFile = null;
            sourceLine = -2; // Magic value for native methods
        }

        StackTraceElement element = StackTrace.element(
                match.group(1), match.group(2), sourceFile, sourceLine
        );
        traceElementCache.put(line, element);
        return element;
    }

    private void filterMonitors(List<ThreadLock.Monitor> monitors, ThreadLock lock) {
        for (Iterator<Monitor> it = monitors.iterator(); it.hasNext();) {
            Monitor m = it.next();

            if (m.getLock().equals(lock)) {
                it.remove();
            }
        }
    }

    private @Nonnull ThreadLock createLock(Matcher matcher) {
        return new ThreadLock(matcher.group(2), parseLong(matcher.group(1)));
    }

    private void initHeader(ThreadDumpThread.Builder builder, String attrs) {
        StringTokenizer tknzr = new StringTokenizer(attrs, " ");
        while (tknzr.hasMoreTokens()) {
            String token = tknzr.nextToken();
            if ("daemon".equals(token)) builder.setDaemon(true);
            else if (token.startsWith("prio=")) builder.setPriority(Integer.parseInt(token.substring(5)));
            else if (token.startsWith("tid=")) builder.setTid(parseLong(token.substring(4)));
            else if (token.startsWith("nid=")) builder.setNid(parseNid(token.substring(4)));
            else if (token.matches("#\\d+")) builder.setId(Integer.parseInt(token.substring(1)));
        }
    }

    private long parseNid(String value) {
        return value.startsWith("0x")
                ? parseLong(value.substring(2))
                : Long.parseLong(value) // Dumpling human readable output
        ;
    }

    /*package*/ static long parseLong(String value) {
        if (value.startsWith("0x")) {
            // Oracle JDK on OS X do not use prefix for tid - so we need to be able to read both
            // https://github.com/olivergondza/dumpling/issues/59
            value = value.substring(2);
        }

        // Long.parseLong is faster but unsuitable in some cases: https://github.com/olivergondza/dumpling/issues/71
        return new BigInteger(value, 16).longValue();
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/ProcessThread.java
public static @Nonnull Predicate nameIs(final @Nonnull String name) {
    return new Predicate() {
        @Override
        public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
            return thread.getName().equals(name);
        }
    };
}


Question: import org.junit.Rule;
import org.junit.Test;
import com.github.olivergondza.dumpling.DisposeRule;
import com.github.olivergondza.dumpling.Util;
import com.github.olivergondza.dumpling.TestThread;
import com.github.olivergondza.dumpling.factory.ThreadDumpFactory;
import com.github.olivergondza.dumpling.model.ProcessRuntime;
import com.github.olivergondza.dumpling.model.ProcessThread;
import com.github.olivergondza.dumpling.model.StackTrace;
import com.github.olivergondza.dumpling.model.ThreadStatus;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpRuntime;
import static com.github.olivergondza.dumpling.model.ProcessThread.nameIs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import java.io.ByteArrayInputStream;
/*
 * The MIT License
 *
 * Copyright (c) 2015 Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.olivergondza.dumpling.cli;





public class SourceTest extends AbstractCliTest {

    @Rule public DisposeRule disposer = new DisposeRule();

    @Test
    public void cliNoSuchFile() {
        run("deadlocks", "--in", "threaddump:/there_is_no_such_file");
        assertThat(exitValue, equalTo(-1));
        assertThat(out.toString(), equalTo(""));
        // Error message is platform specific
        assertThat(err.toString(), not(equalTo("")));
    }

    @Test
    public void jmxRemoteConnectViaCli() throws Exception {
        TestThread.JMXProcess process = disposer.register(TestThread.runJmxObservableProcess(false));
        stdin("runtime.threads.where(nameIs('remotely-observed-thread'))");
        run("groovy", "--in", "jmx:" + process.JMX_CONNECTION);

        assertThat(err.toString(), equalTo(""));
        // Reuse verification logic re-parsing the output as thread dump
Answer:         ThreadDumpRuntime reparsed = new ThreadDumpFactory().fromStream(new ByteArrayInputStream(out.toByteArray()));