/*
* #%L
* Alfresco Records Management Module
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* 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 <http://www.gnu.org/licenses/>.
* #L%
*/
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.
*
* @author Neil Mc Erlean
* @since 2.4.a
*/
public class ExceptionUtils
{
/** This represents a situation where a throwable of an unexpected type was thrown. */
public static class UnexpectedThrowableException extends RuntimeException
{
/** serial version uid */
private static final long serialVersionUID = 3900164716673246207L;
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
{
/** serial version uid */
private static final long serialVersionUID = -988022536370047222L;
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 (unchecked - see below) 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. Likewise if you need to make assertions about state within the expected
* exception, such as root cause or other internal state, this method will be useful.
* <p/>
* Examples:
* <ul>
* <li>
* Calling a local method which throws a {@code RuntimeException}. (An expression lambda)
* <pre>
* expectedException(RuntimeException.class, () -> badMethod() );
* </pre>
* </li>
* <li>
* Executing a block of code. (Requires return statement)
* <pre>
* expectedException(RuntimeException.class, () -> {
* for (int i = 0; i < 10; i++) {
* goodMethod();
* }
* badMethod();
* return "result";
* });
* </pre>
* </li>
* <li>
* Examining the expected exception e.g. to assert the root cause is correct.
* <pre>
* UnsupportedOperationException e = expectedException(UnsupportedOperationException.class, () -> badMethod2() );
* assertEquals(RuntimeException.class, e.getCause().getClass());
* </pre>
* </li>
* <li>
* Note that if your lambda expression returns 'void' then you cannot use an expression
* and must explicitly return null from a lambda block.
* <pre>
* expectedException(Exception.class, () -> { methodReturningVoid(); return null; } );
* expectedException(Exception.class, () -> { methodReturningVoid("parameter"); return null; } );
* </pre>
* </li>
* </ul>
*
* A note on checked exceptions: currently this method does not provide any support for working around the normal
* integration of Java 8 lambdas and checked exceptions. If your {@code code} block must deal with checked exceptions,
* you must add {@code try}/{@code catch} blocks within your lambda which obviously makes this method less useful.
* This may change in the future.
*
*
* @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 <R> the return type of the code block (which should not matter as it should not complete).
* @param <T> 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.
*/
@SuppressWarnings("unchecked")
public static <R, T extends Throwable> T expectedException(final Class<T> expected, final Supplier<R> code)
{
// The code block may throw an exception or it may not.
Optional<Throwable> 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);
}
}
/**
* Helper method to work around the difficulties of working with lambdas and checked exceptions.
* Use as follows:
* <pre>
* expectedException(WebScriptException.class, () ->
* // "Wash away" any checked exceptions in the inner code block.
* smuggleCheckedExceptions( () -> methodThrowsException())
* );
* </pre>
* @param code a block of code which is declared to throw a checked exception.
* @param <R> the return type of the block of code.
* @param <T> the type of the checked exception.
* @return the value returned by the block of code.
* @throws SmuggledException if the code block threw an exception of type T.
*/
public static <R, T extends Exception> R smuggleCheckedExceptions(final ThrowingSupplier<R, T> code)
{
try
{
return code.get();
}
catch (RuntimeException e)
{
throw e;
}
catch (Exception e)
{
throw new SmuggledException(e);
}
}
/**
* Equivalent to `java.util.function.Supplier` but its method declares that it
* throws checked exceptions.
*
* @param <R> The result type of this supplier.
* @param <T> The exception type declared to be thrown by this supplier.
*/
@FunctionalInterface
public interface ThrowingSupplier<R, T extends Exception>
{
/** Gets the value */
R get() throws T;
}
/**
* A wrapper for checked exceptions so that they can be handled as unchecked exceptions, namely by not requiring
* try/catch blocks etc.
* <p/>
* This type is expected to be most useful when handling Java 8 lambdas containing code which throws checked
* exceptions.
*/
public static class SmuggledException extends RuntimeException
{
private static final long serialVersionUID = -606404592461576013L;
private final Exception e;
public SmuggledException(Exception e)
{
this.e = e;
}
public Exception getCheckedException()
{
return this.e;
}
}
}