
hm.binkley.util.Notices Maven / Gradle / Ivy
package hm.binkley.util;
import org.intellij.lang.annotations.PrintFormat;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import static java.lang.String.format;
import static java.lang.System.lineSeparator;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.joining;
/**
* {@code Notices} is one possible implementation for Martin Fowler's
* suggestion, Replacing
* Throwing Exceptions with Notification in Validations.
*
* Takes great care to adjust stack traces, pinpointing:
- The place were
* a notice was added
- The place an exception was thrown if added as a
* notice
- The place where notices were checked for
*
* {@link #add(String, Object...) Text notices} are added as exception of
* type <E>. {@link #add(Exception) Exception notices} are
* added as themselves (preserving type). In both cases stack traces are
* adjusted.
*
* Produces a single, top-level exception of type <E> with
* each notice exception as suppressed exception. This permits code to
* {@link #proceedOrFail() check or fail}, {@link #returnOrFail(Object)
* return a value or fail} or {@link #returnOrFail(Supplier) compute and
* return a value or fail}, in all cases thrown a single, top-level exception
* summarizing notices.
*
* @param the top-level exception type for notices
*
* @author B. K. Oxley (binkley)
* @see An earlier version
* @todo I18N for summary
*/
public final class Notices
implements Iterable {
private final List notices;
private final BiFunction ctor;
/**
* Creates an empty set of notices based on {@code RuntimeException}. Thus
* checking for notices thrown an unchecked exception if there are
* any, requiring no exception handling.
*
* @return the empty notices, never missing
*
* @see #noticesAs(BiFunction)
*/
@Nonnull
public static Notices notices() {
return noticesAs(RuntimeException::new);
}
/**
* Creates an empty set of notices based on exceptions with the given
* 2-argument ctor.
*
* @param ctor the exception 2-argument constructor, never missing
* @param the exception type for notices
*
* @return the empty notices, never missing
*
* @see #notices()
*/
@Nonnull
public static Notices noticesAs(
@Nonnull final BiFunction ctor) {
return new Notices<>(new ArrayList<>(0), ctor);
}
private Notices(final List notices,
final BiFunction ctor) {
this.notices = notices;
this.ctor = ctor;
}
/**
* Converts these notices into a new set with a different exception type.
* Existing text notices retain the original exception type; new text
* notices and the top-level exception have the new excetpion type.
*
* @param ctor the new exception 2-argument constructor, never missing
* @param the new exception type for notices
*
* @return the same notices throwing a different exception, never missing
*/
@Nonnull
public Notices as(
@Nonnull final BiFunction ctor) {
return new Notices<>(new ArrayList<>(notices), ctor);
}
/**
* Checks if there are no notices.
*
* @return {@code true} if there are no notices
*/
public boolean isEmpty() {
return notices.isEmpty();
}
/**
* Gets the count of notices.
*
* @return the count of notices
*/
public int size() {
return notices.size();
}
/**
* An unmodifiable iterator of notices in the same order they were added.
*
* @return the notices iterator, never missing
*/
@Nonnull
@Override
public Iterator iterator() {
return unmodifiableList(notices).iterator();
}
/**
* Adds a new text notice, optionally formatting reason with
* args. Fixes the exception for this notice to show the caller
* at the top of the stack.
*
* @param reason the notice reason, never missing
* @param args formatting args to reason, if any
*/
public void add(@Nonnull @PrintFormat final String reason,
final Object... args) {
final E cause = ctor.apply(format(reason, args), null);
discard(cause, 2); // 2 is the magic number: lambda, current
notices.add(cause);
}
/**
* Adds a new exception notice for the given cause. Fixes the
* exception for this notice to show the caller at the top of the stack,
* followed by existing frames in cause.
*
* @param cause the exeption to note, never missing
*/
public void add(@Nonnull final Exception cause) {
enhance(cause, 2, 1, currentThread().getStackTrace());
notices.add(cause);
}
/**
* Throws a top-level exception if there are notices, else does nothing.
* Fixes the top-level exception to show the caller at the top of the
* stack.
*
* @throws E if there are notices
*/
public void proceedOrFail()
throws E {
if (isEmpty())
return;
throw fail();
}
/**
* Throws a top-level exception if there are notices, else returns
* value. Fixes the top-level exception to show the caller at
* the top of the stack.
*
* @param value the value to return if there are no notices
* @param the value type
*
* @return value if there are no notices
*
* @throws E if there are notices
*/
public T returnOrFail(@Nullable final T value)
throws E {
if (isEmpty())
return value;
throw fail();
}
/**
* Throws a top-level exception if there are notices, else computes and
* returns value. Fixes the top-level exception to show the
* caller at the top of the stack.
*
* @param value the value to compute and return if there are no notices
* @param the value type
*
* @return computation of value if there are no notices
*
* @throws E if there are notices
*/
public T returnOrFail(final Supplier value)
throws E {
if (isEmpty())
return value.get();
throw fail();
}
/**
* Creates a multi-line summary of notices, also used as the top-level
* exception message.
*
* @return a summary of notices
*/
@Nonnull
public String summary() {
if (notices.isEmpty())
return "0 notice(s)";
final String sep = lineSeparator() + "- ";
return notices.stream().
map(Throwable::getMessage).
filter(Objects::nonNull).
collect(joining(sep,
format("%d notice(s):" + sep, notices.size()), ""));
}
@Nonnull
@Override
public String toString() {
return super.toString() + ": " + notices; // TODO: How to show E?
}
private E fail() {
final E e = ctor.apply(summary(), null);
discard(e, 3); // 3 is the magic number: lambda, outer, current
notices.forEach(e::addSuppressed);
return e;
}
private static void discard(final Exception cause, final int n) {
final List frames = asList(cause.getStackTrace());
cause.setStackTrace(frames.subList(n, frames.size())
.toArray(new StackTraceElement[frames.size() - n]));
}
private static void enhance(final Exception cause, final int off,
final int n, final StackTraceElement... extras) {
final List frames = new ArrayList<>(
asList(cause.getStackTrace()));
frames.addAll(0, asList(extras).subList(off, off + n));
cause.setStackTrace(
frames.toArray(new StackTraceElement[frames.size()]));
}
}