Skip to content

Commit

Permalink
feat (core): Exceptions with Context
Browse files Browse the repository at this point in the history
  • Loading branch information
vorburger committed Jun 30, 2024
1 parent a47d9f2 commit 58a2fed
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 7 deletions.
51 changes: 45 additions & 6 deletions java/dev/enola/common/context/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,27 @@
import static java.util.Objects.requireNonNull;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
* Contexts 🧿 put things into perspective!
*
* <p>Contexts are "hierarchical", and child contexts "mask" keys in their parent.
*
* <p>This class is NOT thread safe. Might you want to use {@link TLC} instead?
*
* @author <a href="http://www.vorburger.ch">Michael Vorburger.ch</a>
*/
public class Context implements AutoCloseable {

private static final Logger LOG = LoggerFactory.getLogger(Context.class);

private final @Nullable Context parent;

private @Nullable Entry last = null;
@Nullable Entry last = null;
private boolean closed = false;

public Context(Context parent) {
Expand All @@ -46,9 +54,11 @@ public Context() {
/**
* Push, but not too hard…
*
* @param key Key, which must implement {@link #equals(Object)} correctly, and should have a
* useful {@link #toString()}} implementation; in practice, it often IS actually simply a
* {@link String} (but it technically does not necessarily have to be).
* <p>Both the key and the value arguments should have useful {@link #toString()}}
* implementations; in practice, at least the key often IS actually simply a {@link String} (but
* it technically does not necessarily have to be).
*
* @param key Key, which must implement {@link #equals(Object)} correctly.
* @param value Value to associate with the key.
* @return this, for chaining.
*/
Expand All @@ -75,13 +85,42 @@ public Context push(Object key, Object value) {
// Nota bene: This (kind of) Stack-like data structure (intentionally)
// does not have (need) any pop() ("goes the weasel”) kind of method!

void append(Appendable a, String indent) {
try {
var current = last;
while (current != null) {
a.append(indent);
a.append(current.key.toString());
a.append(" => ");
a.append(current.value.toString());
a.append('\n');
current = current.previous;
}
if (parent != null) parent.append(a, indent + ContextualizedException.INDENT);
} catch (IOException e) {
LOG.error("append() hit an IOException", e);
}
}

String toString(String indent) {
var sb = new StringBuilder();
append(sb, indent);
return sb.toString();
}

public String toString() {
return toString("");
}

/** Close this context. Don't use it anymore! */
@Override
public void close() {
closed = true;
// Free up memory!
last = null;
TLC.reset(parent);

// NB: It's tempting to do "last = null" here, intending to free up memory;
// but doing so breaks e.g. the ContextsTest#exceptionWithContext(). We do
// NOT have to do it to free memory, because Context (should) will be GC.
}

private void check() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import org.junit.Test;

public class TLCTest {
public class ContextsTest {

@Test
public void empty() {
Expand Down Expand Up @@ -55,6 +55,27 @@ public void nested() {
}
}

@Test
public void exceptionWithContext() {
try (var ctx1 = TLC.open()) {
ctx1.push("foo", "bar");
try (var ctx2 = TLC.open()) {
ctx2.push("foo", "baz");
// try {
// TODO throw new ContextualizedException("TEST");
// } catch (ContextualizedException e) {
// var sw = new StringWriter();
// PrintWriter pw = new PrintWriter(sw);
// e.printStackTrace(pw);
// var stackTrace = sw.toString();
// assertThat(stackTrace).contains("foo");
// assertThat(stackTrace).contains("bar");
// assertThat(stackTrace).contains("baz");
// }
}
}
}

@Test
public void useAfterClose() {
Context ctx = TLC.open();
Expand Down
60 changes: 60 additions & 0 deletions java/dev/enola/common/context/ContextualizedException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.common.context;

import java.io.PrintStream;
import java.io.PrintWriter;

/** {@link Exception} with {@link Context}. */
public class ContextualizedException extends Exception {

private final Context context;

public ContextualizedException(String message) {
super(message);
context = TLC.get();
}

public ContextualizedException(String message, Throwable cause) {
super(message, cause);
context = TLC.get();
}

public ContextualizedException(Throwable cause) {
super(cause);
context = TLC.get();
}

@Override
public void printStackTrace(PrintStream s) {
super.printStackTrace(s);
if (context.last != null) s.println("Context:");
context.append(s, INDENT);
s.flush();
}

@Override
public void printStackTrace(PrintWriter s) {
super.printStackTrace(s);
if (context.last != null) s.println("Context:");
context.append(s, INDENT);
s.flush();
}

static final String INDENT = " ";
}
62 changes: 62 additions & 0 deletions java/dev/enola/common/context/ContextualizedRuntimeException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.common.context;

import java.io.PrintStream;
import java.io.PrintWriter;

/**
* {@link RuntimeException} with {@link Context}.
*
* @see ContextualizedException
*/
public class ContextualizedRuntimeException extends RuntimeException {

private final Context context;

public ContextualizedRuntimeException(String message) {
super(message);
context = TLC.get();
}

public ContextualizedRuntimeException(String message, Throwable cause) {
super(message, cause);
context = TLC.get();
}

public ContextualizedRuntimeException(Throwable cause) {
super(cause);
context = TLC.get();
}

@Override
public void printStackTrace(PrintStream s) {
super.printStackTrace(s);
if (context.last != null) s.println("Context:");
context.append(s, ContextualizedException.INDENT);
s.flush();
}

@Override
public void printStackTrace(PrintWriter s) {
super.printStackTrace(s);
if (context.last != null) s.println("Context:");
context.append(s, ContextualizedException.INDENT);
s.flush();
}
}
5 changes: 5 additions & 0 deletions java/dev/enola/common/context/TLC.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,10 @@ static void reset(@Nullable Context context) {
threadLocalContext.set(context);
}

/* package-local, always keep; never make public! */
static Context get() {
return threadLocalContext.get();
}

private TLC() {}
}

0 comments on commit 58a2fed

Please sign in to comment.