import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
-import org.thymeleaf.exceptions.TemplateInputException;
+import org.thymeleaf.exceptions.TemplateProcessingException;
+
+import java.util.Optional;
@Controller
public class ExampleController
LoggerFactory.getLogger(ExampleController.class);
+ private final ExampleService service;
+
+
+ public ExampleController(ExampleService service)
+ {
+ this.service = service;
+ }
+
+
@RequestMapping("/")
public String controller(
- @RequestParam(defaultValue = "a") String template,
+ @RequestParam(required = false) Integer answer,
Model model
)
{
- model.addAttribute("template", template);
- return template;
+ Optional<Boolean> outcome =
+ answer == null ? null : service.checkAnswer(answer);
+
+ model.addAttribute("answer", answer);
+ model.addAttribute("outcome", outcome);
+ return "view";
}
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- @ExceptionHandler(TemplateInputException.class)
- public void templateInputException(TemplateInputException e)
+ /**
+ * This {@link ExceptionHandler @ExceptionHander} is never triggered,
+ * because the exception is not thrown inside the controller:
+ * <strong>It is functionless!</strong>
+ * <p>
+ * The exception is thrown by Thymeleaf, which is called by the
+ * {@link DispatcherServlet} after the controller has finished its
+ * work during the rendering of the outcome.
+ * </p>
+ * <p>
+ * {@link ExceptionHandler @ExceptionHander's} are not able, to catch
+ * and resolve exceptions, which are thrown outside of the scope of the
+ * controller.
+ * This is also true for {@link ControllerAdvice}.
+ * </p>
+ */
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(TemplateProcessingException.class)
+ public void templateInputException(TemplateProcessingException e)
{
- LOG.error("{}: {}", HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
+ LOG.error("{}: {}", HttpStatus.BAD_REQUEST, e.getMessage());
}
}
--- /dev/null
+package de.juplo.demo;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@Component
+public class ExampleService
+{
+ public Optional<Boolean> checkAnswer(int answer)
+ {
+ if (answer < 0)
+ return Optional.empty();
+
+ if (answer == 42)
+ return Optional.of(true);
+ else
+ return Optional.of(false);
+ }
+}
+++ /dev/null
-<!DOCTYPE HTML>
-<html xmlns:th="http://www.thymeleaf.org">
- <head>
- <title>Testing Exception-Handling - Template A</title>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- </head>
- <body>
- <h1>Template A</h1>
- <div>
- <p><strong th:text="'Serving with template ::' + ${template} + '::!'">TEXT</strong></p>
- <p>
- Type in <strong><code>a</code></strong> or <strong><code>b</code></strong>
- for an existing template (no exception). Type in any other string for a
- non-existent template: Thymeleaf will throw a
- <strong><code>TemplateInputException</code></strong>!
- </p>
- </div>
- <div>
- <form action="#" th:action="@{/}" method="get">
- <div>
- <label for="path">Remote-Path to fetch:</label>
- <input type="text" name="template" value="a" th:value="${template}"/>
- </div>
- <button type="submit">Submit</button>
- </form>
- </div>
- </body>
-</html>
+++ /dev/null
-<!DOCTYPE HTML>
-<html xmlns:th="http://www.thymeleaf.org">
- <head>
- <title>Testing Exception-Handling - Template B</title>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- </head>
- <body>
- <h1>Template B</h1>
- <div>
- <p><strong th:text="'Serving with template ::' + ${template} + '::!'">TEXT</strong></p>
- <p>
- Type in <strong><code>a</code></strong> or <strong><code>b</code></strong>
- for an existing template (no exception). Type in any other string for a
- non-existent template: Thymeleaf will throw a
- <strong><code>TemplateInputException</code></strong>!
- </p>
- </div>
- <div>
- <form action="#" th:action="@{/}" method="get">
- <div>
- <label for="path">Remote-Path to fetch:</label>
- <input type="text" name="template" value="a" th:value="${template}"/>
- </div>
- <button type="submit">Submit</button>
- </form>
- </div>
- </body>
-</html>
--- /dev/null
+<!DOCTYPE HTML>
+<html xmlns:th="http://www.thymeleaf.org">
+ <head>
+ <title>Template: view</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ </head>
+ <body>
+ <h1>Deep Thought</h1>
+ <div th:switch="${answer}">
+ <p th:case="null">
+ <strong>What is the answer to the ultimate question of life, the universe and everything?</strong>
+ </p>
+ <ul th:case="*">
+ <li>Presented answer: <strong th:text="${answer}">ANSWER</strong></li>
+ <li>Outcome: <strong th:text="${outcome.get()}">OUTCOME</strong></li>
+ </ul>
+ </div>
+ <div>
+ <form action="#" th:action="@{/}" method="get">
+ <div>
+ <label for="answer">Answer:</label>
+ <input type="text" name="answer" value="42" th:value="${answer}"/>
+ <button type="submit">Submit</button>
+ </div>
+ </form>
+ </div>
+ </body>
+</html>
package de.juplo.demo;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.net.URI;
+import java.util.Optional;
+import static org.mockito.AdditionalMatchers.geq;
+import static org.mockito.AdditionalMatchers.lt;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
private final static Logger LOG =
LoggerFactory.getLogger(ExceptionHandlingApplicationTests.class);
+ @MockBean
+ ExampleService service;
+
@Autowired
MockMvc mvc;
}
@Test
- void test200() throws Exception {
+ void test200ForNoAnswer() throws Exception {
mvc
- .perform(get(URI.create("http://FOO/?template=a")))
+ .perform(get(URI.create("http://FOO/")))
.andExpect(status().isOk());
+
+ verify(service, times(0)).checkAnswer(anyInt());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 41, 42, 43, 666, Integer.MAX_VALUE })
+ void test200ForPositiveAnswer(int number) throws Exception {
+ when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true));
+ when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false));
+ when(service.checkAnswer(lt(0))).thenReturn(Optional.empty());
+
mvc
- .perform(get(URI.create("http://FOO/?template=b")))
+ .perform(get(URI.create("http://FOO/?answer=" + number)))
.andExpect(status().isOk());
}
- @Test
- void test503_NOT_WORKING() throws Exception {
- // The expected behaviour of the following test is, that the
- // TemplateInputException, that is thrown by Thymeleaf because of the non-existent
- // template-resource, is catched and reported as 503 Internal Server Error, as it
- // happens in the real Spring-MVC environment, when you start the web-server and
- // trigger the error manually.
+ @ParameterizedTest
+ @ValueSource(ints = { -1, -2, Integer.MIN_VALUE })
+ void test400ForNegativeAnswer_NOT_WORKING(int number) throws Exception {
+ when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true));
+ when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false));
+ when(service.checkAnswer(lt(0))).thenReturn(Optional.empty());
+
+ // The expected behaviour of the following test is, that the NoSuchElementException
+ // with the message "No value present", that is raised, when the view calls .get()
+ // on the empty Optional and wrapped by Thymeleaf in a TemplateProcessingException
+ // is catched by the @ExceptionHandler, that is defined in the ExampleController
+ // and reported as 400: Bad Request.
// Instead, the exception bubbles up, becomes wrapped in a NestedServletException
// and is thrown in the call to perform()!
mvc
- .perform(get(URI.create("http://FOO/?template=foo")))
+ .perform(get(URI.create("http://FOO/?answer=" + number)))
.andExpect(status().isInternalServerError());
}
+
+ @Test
+ void test400ForStringInput() throws Exception {
+ when(service.checkAnswer(eq(42))).thenReturn(Optional.of(true));
+ when(service.checkAnswer(geq(0))).thenReturn(Optional.of(false));
+ when(service.checkAnswer(lt(0))).thenReturn(Optional.empty());
+
+ mvc
+ .perform(get(URI.create("http://FOO/?answer=bar")))
+ .andExpect(status().isBadRequest());
+ }
}