diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtils.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtils.java
new file mode 100644
index 0000000000..e0ec3548be
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtils.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.module.org_alfresco_module_rm.test.util;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * Utility class to help with Java exceptions, particularly in test code.
+ *
+ * @since 3.0
+ * @author Neil Mc Erlean
+ */
+public class ExceptionUtils
+{
+ /** This represents a situation where a throwable of an unexpected type was thrown. */
+ public static class UnexpectedThrowableException extends RuntimeException
+ {
+ private final Class extends Throwable> expected;
+ private final Throwable actual;
+
+ public UnexpectedThrowableException(Class extends Throwable> expected, Throwable actual)
+ {
+ this.expected = expected;
+ this.actual = actual;
+ }
+
+ public Class extends Throwable> getExpected() { return this.expected; }
+ public Throwable getActual() { return this.actual; }
+
+ @Override public String toString()
+ {
+ return String.join("Expected ", expected.getSimpleName(), " but ",
+ actual.getClass().getSimpleName(), " was thrown.");
+ }
+ }
+
+ /** This represents a situation where an expected throwable was not thrown. */
+ public static class MissingThrowableException extends RuntimeException
+ {
+ private final Class extends Throwable> expected;
+
+ public MissingThrowableException(Class extends Throwable> expected)
+ {
+ this.expected = expected;
+ }
+
+ public Class extends Throwable> getExpected() { return this.expected; }
+ @Override public String toString()
+ {
+ return String.join("Expected ", expected.getSimpleName(), " but nothing was thrown.");
+ }
+ }
+
+ /**
+ * Utility method to help with expected exceptions in test code. This can be used in place of {@code try/catch}
+ * blocks within test code and can sometimes make code more readable.
+ * A single expected exception would usually be let escape from the test method and be handled e.g. by JUnit's
+ * {@code @Test(expected="Exception.class")} pattern.
+ * However if you have multiple expected exceptions in a sequence, you need to either add a sequence of
+ * {@code try/catch} or use this method.
+ *
+ * Examples:
+ *
+ * -
+ * Calling a local method which throws a {@code RuntimeException}. (An expression lambda)
+ *
+ * intercept(RuntimeException.class, () -> badMethod() );
+ *
+ *
+ * -
+ * Executing a block of code. (Requires return statement)
+ *
+ * intercept(RuntimeException.class, () -> {
+ * for (int i = 0; i < 10; i++) {
+ * goodMethod();
+ * }
+ * badMethod();
+ * return "result";
+ * });
+ *
+ *
+ * -
+ * Examining the expected exception e.g. to assert the root cause is correct.
+ *
+ * UnsupportedOperationException e = intercept(UnsupportedOperationException.class, () -> badMethod2() );
+ * assertEquals(RuntimeException.class, e.getCause().getClass());
+ *
+ *
+ * -
+ * Note that if your lambda expression returns 'void' then you cannot use an expression
+ * and must explicitly return null from a lambda block.
+ *
+ * intercept(Exception.class, () -> { methodReturningVoid(); return null; } );
+ * intercept(Exception.class, () -> { methodReturningVoid("parameter"); return null; } );
+ *
+ *
+ *
+ *
+ * @param expected the class of the expected throwable (subtypes will also match).
+ * @param code a lambda containing the code block which should throw the expected throwable.
+ * @param the return type of the code block (which should not matter as it should not complete).
+ * @param the type of the expected throwable (subtypes will also match).
+ * @return the expected throwable object if it was thrown.
+ * @throws UnexpectedThrowableException if a non-matching throwable was thrown out of the code block.
+ * @throws MissingThrowableException if the expected throwable was not thrown out of the code block.
+ */
+ public static T intercept(final Class expected, final Supplier code)
+ {
+ // The code block may throw an exception or it may not.
+ Optional maybeThrownByCode;
+
+ try
+ {
+ // evaluate the lambda
+ code.get();
+
+ // It didn't throw an exception.
+ maybeThrownByCode = Optional.empty();
+ }
+ catch (Throwable t)
+ {
+ maybeThrownByCode = Optional.of(t);
+ }
+
+ Throwable thrownByCode = maybeThrownByCode.orElseThrow(() -> new MissingThrowableException(expected));
+
+ if (expected.isAssignableFrom(thrownByCode.getClass()))
+ {
+ return (T)thrownByCode;
+ }
+ else
+ {
+ throw new UnexpectedThrowableException(expected, thrownByCode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtilsUsageExamplesTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtilsUsageExamplesTest.java
new file mode 100644
index 0000000000..e69a07431a
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/test/util/ExceptionUtilsUsageExamplesTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.module.org_alfresco_module_rm.test.util;
+
+import org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.MissingThrowableException;
+import org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.UnexpectedThrowableException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.intercept;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests showing usage of {@link ExceptionUtils}.
+ *
+ * @since 3.0
+ * @author Neil Mc Erlean
+ */
+public class ExceptionUtilsUsageExamplesTest
+{
+ private String goodMethod() { return "hello"; }
+
+ private String badMethod1() { throw new RuntimeException("Bad method"); }
+
+ private String badMethod2() { throw new UnsupportedOperationException("Bad method", new RuntimeException("root cause")); }
+
+ @Test public void swallowExpectedExceptions()
+ {
+ // Calling a local method. (An expression lambda)
+ intercept(RuntimeException.class, () -> badMethod1() );
+
+ // Executing a block of code. (Requires return statement)
+ intercept(RuntimeException.class, () ->
+ {
+ for (int i = 0; i < 10; i++) {
+ goodMethod();
+ }
+ // Also works for subtypes of expected exception.
+ badMethod2();
+ return null;
+ });
+ }
+
+ @Test public void examineTheExpectedException()
+ {
+ UnsupportedOperationException e = intercept(UnsupportedOperationException.class, () -> badMethod2() );
+ assertEquals(RuntimeException.class, e.getCause().getClass());
+ }
+
+ @Test(expected=MissingThrowableException.class)
+ public void expectedExceptionNotThrown()
+ {
+ intercept(IOException.class, () ->
+ {
+ // Do nothing
+ return null;
+ });
+ }
+
+ @Test(expected=UnexpectedThrowableException.class)
+ public void unexpectedExceptionThrown()
+ {
+ intercept(IOException.class, () ->
+ {
+ throw new UnsupportedOperationException();
+ });
+ }
+
+ private void onlySideEffectsHere() { throw new IllegalStateException(); }
+
+ private void onlySideEffectsHere(String s) { throw new IllegalStateException(); }
+
+ // If you use lambdas that return void, then they cannot be lambda expressions. They must be blocks.
+ @Test public void usingVoidLambdas()
+ {
+ intercept(IllegalStateException.class, () -> {
+ onlySideEffectsHere();
+ return null;
+ });
+
+ intercept(IllegalStateException.class, () -> {
+ onlySideEffectsHere("hello");
+ return null;
+ });
+ }
+}
\ No newline at end of file