Tuesday, December 09, 2014

Java validation

Martin Fowler posted Replacing Throwing Exceptions with Notification in Validations discussing alternatives to data validation than throwing exceptions. There are off-the-shelf solutions such as Commons Validator (XML driven) or Bean Validation (annotation driven) which are complete frameworks.

There is more to these frameworks than I suggest, but to explore Fowler's post better I quickly coded up my own simple-minded approach:

public final class ValidationMain {
    public static void main(final String... args) {
        final Notices notices = new Notices();
        notices.add("Something went horribly wrong %d time(s)", 1);
        try {
            foo(null);
        } catch (final Exception e) {
            notices.add(e);
        }
        notices.forEach(err::println);
        notices.fail(IllegalArgumentException::new);
    }

    public static String foo(final Object missing) {
        return missing.toString();
    }
}

Output on stderr:

lab.Notices$Notice@27f8302d
lab.Notices$Notice@4d76f3f8
Exception in thread "main" java.lang.IllegalArgumentException: 2 notices:
 Something went horribly wrong 1 time(s)
 at lab.ValidationMain.main(ValidationMain.java:21)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:483)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
 Suppressed: java.lang.IllegalArgumentException: Only 1 reason(s)
  at lab.ValidationMain.main(ValidationMain.java:14)
 Suppressed: java.lang.IllegalArgumentException
  at lab.ValidationMain.main(ValidationMain.java:18)
 Caused by: java.lang.NullPointerException
  at lab.ValidationMain.foo(ValidationMain.java:25)
  at lab.ValidationMain.main(ValidationMain.java:16)

And the Notices class:

public final class Notices
        implements Iterable<Notice> {
    private final List<Notice> notices = new ArrayList<>(0);

    public void add(final String reason, final Object... args) {
        // Duplicate code so stack trace keeps same structure
        notices.add(new Notice(null, reason, args));
    }

    public void add(final Throwable cause) {
        // Duplicate code so stack trace keeps same structure
        notices.add(
                new Notice(cause, null == cause ? null : cause.getMessage()));
    }

    public void add(final Throwable cause, final String reason,
            final Object... args) {
        // Duplicate code so stack trace keeps same structure
        notices.add(new Notice(cause, reason, args));
    }

    public <E extends Exception> void fail(
            final BiFunction<String, Throwable, E> ctor)
            throws E {
        final E e = ctor.apply(toString(), null);
        notices.forEach(n -> e.addSuppressed(n.as(ctor)));
        final List<StackTraceElement> frames = asList(e.getStackTrace());
        // 2 is the magic number: lambda, current
        e.setStackTrace(frames.subList(2, frames.size())
                .toArray(new StackTraceElement[frames.size() - 2]));
        throw e;
    }

    @Override
    public Iterator<Notice> iterator() {
        return unmodifiableList(notices).iterator();
    }

    @Override
    public String toString() {
        if (notices.isEmpty())
            return "0 notices";
        final String sep = lineSeparator() + "\t";
        return notices.stream().
                map(Notice::reason).
                filter(Objects::nonNull).
                collect(joining(sep,
                        format("%d notices:" + sep, notices.size()), ""));
    }

    public static final class Notice {
        // 4 is the magic number: Thread, init, init, addError, finally the
        // user code
        private final StackTraceElement location = currentThread()
                .getStackTrace()[4];
        private final Throwable cause;
        private final String reason;
        private final Object[] args;

        private Notice(final Throwable cause, final String reason,
                final Object... args) {
            this.cause = cause;
            this.reason = reason;
            this.args = args;
        }

        public Throwable cause() {
            return cause;
        }

        public String reason() {
            return null == reason ? null : format(reason, args);
        }

        private <E extends Exception> E as(
                final BiFunction<String, Throwable, E> ctor) {
            final E e = ctor.apply(reason(), cause);
            e.setStackTrace(new StackTraceElement[]{location});
            return e;
        }
    }
}

Comments:

  • I manipulate the stack traces to focus on the caller's point of view. This is the opposite of, say, Spring Framework.
  • I haven't decided on what an intelligent toString() should look like for Notice
  • Java 8 lambdas really shine here. Being able to use exception constructors as method references is a win.

No comments: