All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hm.binkley.util.Notices Maven / Gradle / Ivy

The newest version!
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:

  1. The place were * a notice was added
  2. The place an exception was thrown if added as a * notice
  3. 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()])); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy