/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Collections;
import org.junit.Test;
import org.springframework.beans.FatalBeanException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerWebExchange;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ClassUtils;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
/**
* {@code @ControllerAdvice} related tests for {@link RequestMappingHandlerAdapter}.
* @author Rossen Stoyanchev
*/
public class ControllerAdviceTests {
private final MockServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange();
@Test
public void resolveExceptionGlobalHandler() throws Exception {
testException(new IllegalAccessException(), "SecondControllerAdvice: IllegalAccessException");
}
@Test
public void resolveExceptionGlobalHandlerOrdered() throws Exception {
testException(new IllegalStateException(), "OneControllerAdvice: IllegalStateException");
}
@Test // SPR-12605
public void resolveExceptionWithHandlerMethodArg() throws Exception {
testException(new ArrayIndexOutOfBoundsException(), "HandlerMethod: handle");
}
@Test
public void resolveExceptionWithAssertionError() throws Exception {
AssertionError error = new AssertionError("argh");
testException(error, error.toString());
}
@Test
public void resolveExceptionWithAssertionErrorAsRootCause() throws Exception {
AssertionError cause = new AssertionError("argh");
FatalBeanException exception = new FatalBeanException("wrapped", cause);
testException(exception, cause.toString());
}
private void testException(Throwable exception, String expected) throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
RequestMappingHandlerAdapter adapter = createAdapter(context);
TestController controller = context.getBean(TestController.class);
controller.setException(exception);
Object actual = handle(adapter, controller, "handle").getReturnValue().orElse(null);
assertEquals(expected, actual);
}
@Test
public void modelAttributeAdvice() throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
RequestMappingHandlerAdapter adapter = createAdapter(context);
TestController controller = context.getBean(TestController.class);
Model model = handle(adapter, controller, "handle").getModel();
assertEquals(2, model.asMap().size());
assertEquals("lAttr1", model.asMap().get("attr1"));
assertEquals("gAttr2", model.asMap().get("attr2"));
}
@Test
public void initBinderAdvice() throws Exception {
ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
RequestMappingHandlerAdapter adapter = createAdapter(context);
TestController controller = context.getBean(TestController.class);
Validator validator = mock(Validator.class);
controller.setValidator(validator);
BindingContext bindingContext = handle(adapter, controller, "handle").getBindingContext();
WebExchangeDataBinder binder = bindingContext.createDataBinder(this.exchange, "name");
assertEquals(Collections.singletonList(validator), binder.getValidators());
}
private RequestMappingHandlerAdapter createAdapter(ApplicationContext context) throws Exception {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
return adapter;
}
private HandlerResult handle(RequestMappingHandlerAdapter adapter,
Object controller, String methodName) throws Exception {
Method method = controller.getClass().getMethod(methodName);
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
return adapter.handle(this.exchange, handlerMethod).block(Duration.ZERO);
}
@Configuration
static class TestConfig {
@Bean
public TestController testController() {
return new TestController();
}
@Bean
public OneControllerAdvice testExceptionResolver() {
return new OneControllerAdvice();
}
@Bean
public SecondControllerAdvice anotherTestExceptionResolver() {
return new SecondControllerAdvice();
}
}
@Controller
static class TestController {
private Validator validator;
private Throwable exception;
void setValidator(Validator validator) {
this.validator = validator;
}
void setException(Throwable exception) {
this.exception = exception;
}
@InitBinder
public void initDataBinder(WebDataBinder dataBinder) {
if (this.validator != null) {
dataBinder.addValidators(this.validator);
}
}
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("attr1", "lAttr1");
}
@GetMapping
public void handle() throws Throwable {
if (this.exception != null) {
throw this.exception;
}
}
}
@ControllerAdvice
@Order(1)
static class OneControllerAdvice {
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("attr1", "gAttr1");
model.addAttribute("attr2", "gAttr2");
}
@ExceptionHandler
public String handleException(IllegalStateException ex) {
return "OneControllerAdvice: " + ClassUtils.getShortName(ex.getClass());
}
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
public String handleWithHandlerMethod(HandlerMethod handlerMethod) {
return "HandlerMethod: " + handlerMethod.getMethod().getName();
}
@ExceptionHandler(AssertionError.class)
public String handleAssertionError(Error err) {
return err.toString();
}
}
@ControllerAdvice
@Order(2)
static class SecondControllerAdvice {
@ExceptionHandler({IllegalStateException.class, IllegalAccessException.class})
public String handleException(Exception ex) {
return "SecondControllerAdvice: " + ClassUtils.getShortName(ex.getClass());
}
}
}