diff --git a/src/main/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappers.java b/src/main/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappers.java index b8fa275c..49ff0f8c 100644 --- a/src/main/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappers.java +++ b/src/main/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappers.java @@ -4,6 +4,7 @@ import static org.kiwiproject.base.KiwiStrings.format; import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.core.server.AbstractServerFactory; import io.dropwizard.core.server.ServerFactory; import io.dropwizard.core.setup.Environment; import lombok.experimental.UtilityClass; @@ -29,9 +30,13 @@ public class StandardExceptionMappers { /** * Registers the "standard" set of exception mappers. + *

+ * This uses {@link #disableDefaultExceptionMapperRegistration(ServerFactory)} to prevent + * Dropwizard from registering any of its exception mappers. * * @param serverFactory the serverFactory so that the default exception mappers can be disabled * @param environment the Dropwizard environment + * @see #disableDefaultExceptionMapperRegistration(ServerFactory) */ public static void register(ServerFactory serverFactory, Environment environment) { var jersey = environment.jersey(); @@ -56,10 +61,37 @@ public static void register(ServerFactory serverFactory, Environment environment jersey.register(new JerseyViolationExceptionMapper()); } - private static void disableDefaultExceptionMapperRegistration(ServerFactory serverFactory) { + /** + * Disable registration of Dropwizard exception mappers if the {@link ServerFactory} supports it + * via a {@code setRegisterDefaultExceptionMappers} method which accepts a {@link Boolean} (the + * wrapper type, not a primitive {@code boolean}). + *

+ * Both Dropwizard implementations, {@link io.dropwizard.core.server.DefaultServerFactory DefaultServerFactory} + * and {@link io.dropwizard.core.server.SimpleServerFactory SimpleServerFactory}, support this option + * since they extend {@link io.dropwizard.core.server.AbstractServerFactory AbstractServerFactory}. + *

+ * This should only be used if you do not want any of Dropwizard's default exception mappers to be + * registered. The {@link io.dropwizard.core.setup.ExceptionMapperBinder ExceptionMapperBinder} registers + * Dropwizard's default set of exception mappers. + *

+ * Also see + * + * Overriding Default Exception Mappers + * + * in the Dropwizard reference manual. + * + * @param serverFactory + * @see io.dropwizard.core.setup.ExceptionMapperBinder + * @see io.dropwizard.core.server.AbstractServerFactory#setRegisterDefaultExceptionMappers(Boolean) + */ + public static void disableDefaultExceptionMapperRegistration(ServerFactory serverFactory) { LOG.info("Disabling Dropwizard registration of default exception mappers"); - var methodHandle = findRegistrationSetter(serverFactory); - invokeRegistrationSetter(methodHandle, serverFactory); + if (serverFactory instanceof AbstractServerFactory baseFactory) { + baseFactory.setRegisterDefaultExceptionMappers(false); + } else { + var methodHandle = findRegistrationSetter(serverFactory); + invokeRegistrationSetter(methodHandle, serverFactory); + } } @VisibleForTesting @@ -69,7 +101,7 @@ static MethodHandle findRegistrationSetter(ServerFactory serverFactory) { REGISTER_DEFAULT_EXCEPTION_MAPPERS_SETTER, methodType(Void.TYPE, Boolean.class)); } catch (NoSuchMethodException | IllegalAccessException ex) { throw new IllegalStateException( - format("ServerFactory class ({}) must respond to '{}' to disable default exception mapper registration!", + format("ServerFactory class ({}) must respond to '{}(Boolean)' to disable default exception mapper registration!", serverFactory.getClass(), REGISTER_DEFAULT_EXCEPTION_MAPPERS_SETTER), ex); } @@ -81,7 +113,7 @@ static void invokeRegistrationSetter(MethodHandle methodHandle, ServerFactory se methodHandle.invoke(serverFactory, Boolean.FALSE); } catch (Throwable throwable) { throw new IllegalStateException( - format("Unable to invoke '{}' using handle {} on {}. Cannot disable default exception mapper registration!", + format("Unable to invoke '{}(Boolean.FALSE)' using handle {} on {}. Cannot disable default exception mapper registration!", REGISTER_DEFAULT_EXCEPTION_MAPPERS_SETTER, methodHandle, serverFactory), throwable); } diff --git a/src/test/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappersTest.java b/src/test/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappersTest.java index 7a07bac1..44d6a591 100644 --- a/src/test/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappersTest.java +++ b/src/test/java/org/kiwiproject/dropwizard/util/exception/StandardExceptionMappersTest.java @@ -1,15 +1,20 @@ package org.kiwiproject.dropwizard.util.exception; +import static java.util.Objects.nonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import io.dropwizard.core.server.AbstractServerFactory; import io.dropwizard.core.server.DefaultServerFactory; import io.dropwizard.core.server.ServerFactory; +import io.dropwizard.core.server.SimpleServerFactory; import io.dropwizard.core.setup.Environment; import org.eclipse.jetty.server.Server; import org.junit.jupiter.api.DisplayName; @@ -62,6 +67,117 @@ public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionM } } + @Nested + class DisableDefaultExceptionMapperRegistration { + + @Test + void shouldDisableForDefaultServerFactory() { + var serverFactory = checkAbstractServerFactoryPrecondition(new DefaultServerFactory()); + + StandardExceptionMappers.disableDefaultExceptionMapperRegistration(serverFactory); + + assertThat(serverFactory.getRegisterDefaultExceptionMappers()).isFalse(); + } + + @Test + void shouldDisableForSimpleServerFactory() { + var serverFactory = checkAbstractServerFactoryPrecondition(new SimpleServerFactory()); + + StandardExceptionMappers.disableDefaultExceptionMapperRegistration(serverFactory); + + assertThat(serverFactory.getRegisterDefaultExceptionMappers()).isFalse(); + } + + private static AbstractServerFactory checkAbstractServerFactoryPrecondition(AbstractServerFactory serverFactory) { + assertThat(serverFactory.getRegisterDefaultExceptionMappers()) + .describedAs("precondition failed: expected registerDefaultExceptionMappers=true") + .isTrue(); + + return serverFactory; + } + + @Test + void shouldDisableForCustomServerFactory_WhichSupportsDisabling() { + var serverFactory = new SupportedCustomServerFactory(); + + StandardExceptionMappers.disableDefaultExceptionMapperRegistration(serverFactory); + + assertAll( + () -> assertThat(serverFactory.registerDefaultExceptionMappersCalled).isTrue(), + () -> assertThat(serverFactory.argumentHadCorrectValue).isTrue() + ); + } + + @Test + void shouldThrowIllegalState_IfServerFactoryDoesNotSupportDisabling() { + var serverFactory = new UnsupportedCustomServerFactory(); + + assertThatIllegalStateException() + .isThrownBy(() -> StandardExceptionMappers.disableDefaultExceptionMapperRegistration(serverFactory)); + } + + @Test + void shouldThrowIllegalState_IfServerFactoryHasCorrectlyNamedMethodThatAcceptsPrimitiveBoolean() { + var serverFactory = new UnsupportedPrimitiveBooleanCustomServerFactory(); + + assertThatIllegalStateException() + .isThrownBy(() -> StandardExceptionMappers.disableDefaultExceptionMapperRegistration(serverFactory)); + } + } + + public static class SupportedCustomServerFactory implements ServerFactory { + + boolean registerDefaultExceptionMappersCalled; + boolean argumentHadCorrectValue; + + public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionMappers) { + registerDefaultExceptionMappersCalled = true; + argumentHadCorrectValue = nonNull(registerDefaultExceptionMappers) && !registerDefaultExceptionMappers; + } + + @Override + public Server build(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + + @Override + public void configure(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + } + + public static class UnsupportedCustomServerFactory implements ServerFactory { + + @Override + public Server build(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + + @Override + public void configure(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + } + + public static class UnsupportedPrimitiveBooleanCustomServerFactory implements ServerFactory { + + boolean registerDefaultExceptionMappersCalled; + + public void setRegisterDefaultExceptionMappers(boolean registerDefaultExceptionMappers) { + registerDefaultExceptionMappersCalled = true; + } + + @Override + public Server build(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + + @Override + public void configure(Environment environment) { + throw new UnsupportedOperationException("Should never be called by tests"); + } + } + @Nested class FindRegistrationSetter { @@ -81,7 +197,7 @@ public void configure(Environment environment) { assertThatThrownBy(() -> StandardExceptionMappers.findRegistrationSetter(serverFactory)) .isExactlyInstanceOf(IllegalStateException.class) .hasMessageStartingWith("ServerFactory class") - .hasMessageEndingWith("must respond to 'setRegisterDefaultExceptionMappers' to disable default exception mapper registration!"); + .hasMessageEndingWith("must respond to 'setRegisterDefaultExceptionMappers(Boolean)' to disable default exception mapper registration!"); } @Test @@ -113,7 +229,7 @@ public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionM MethodHandle registrationSetter = StandardExceptionMappers.findRegistrationSetter(serverFactory); assertThatThrownBy(() -> StandardExceptionMappers.invokeRegistrationSetter(registrationSetter, serverFactory)) .isExactlyInstanceOf(IllegalStateException.class) - .hasMessageStartingWith("Unable to invoke 'setRegisterDefaultExceptionMappers' using handle") + .hasMessageStartingWith("Unable to invoke 'setRegisterDefaultExceptionMappers(Boolean.FALSE)' using handle") .hasMessageEndingWith("Cannot disable default exception mapper registration!"); }