diff --git a/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/client/LongPollingBakeryClient.java b/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/client/LongPollingBakeryClient.java new file mode 100644 index 0000000000..be6a3ac54d --- /dev/null +++ b/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/client/LongPollingBakeryClient.java @@ -0,0 +1,42 @@ +package com.baeldung.longpolling.client; + +import io.netty.handler.timeout.ReadTimeoutException; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +import java.time.Duration; + +@Component +public class LongPollingBakeryClient { + + public String callBakeWithRestTemplate(RestTemplateBuilder restTemplateBuilder) { + RestTemplate restTemplate = restTemplateBuilder + .setConnectTimeout(Duration.ofSeconds(10)) + .setReadTimeout(Duration.ofSeconds(10)) + .build(); + + try { + return restTemplate.getForObject("/api/bake/cookie?bakeTime=1000", String.class); + } catch (ResourceAccessException e) { + throw e; + } + } + + public String callBakeWithWebClient() { + WebClient webClient = WebClient.create(); + + try { + return webClient.get() + .uri("/api/bake/cookie?bakeTime=1000") + .retrieve() + .bodyToFlux(String.class) + .timeout(Duration.ofSeconds(10)) + .blockFirst(); + } catch (ReadTimeoutException e) { + throw e; + } + } +} diff --git a/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/controller/BakeryController.java b/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/controller/BakeryController.java new file mode 100644 index 0000000000..51dfb6bbf2 --- /dev/null +++ b/spring-web-modules/spring-rest-http-2/src/main/java/com/baeldung/longpolling/controller/BakeryController.java @@ -0,0 +1,49 @@ +package com.baeldung.longpolling.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static java.lang.String.format; + +/** + * Long polling controller example. + */ +@RestController +@RequestMapping("/api") +public class BakeryController { + private final static Logger LOG = LoggerFactory.getLogger(BakeryController.class); + private final static Long LONG_POLLING_TIMEOUT = 5000L; + + private ExecutorService bakers; + + public BakeryController() { + bakers = Executors.newFixedThreadPool(5); + } + + @GetMapping("/bake/{bakedGood}") + public DeferredResult publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) { + + DeferredResult output = new DeferredResult<>(LONG_POLLING_TIMEOUT); + + bakers.execute(() -> { + try { + Thread.sleep(bakeTime); + output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood)); + } catch (Exception e) { + output.setErrorResult("Something went wrong with your order!"); + } + }); + + output.onTimeout(() -> output.setErrorResult("the bakery is not responding in allowed time")); + return output; + } +} diff --git a/spring-web-modules/spring-rest-http-2/src/test/com/baeldung/longpolling/integration/BakeryControllerIntegrationTest.java b/spring-web-modules/spring-rest-http-2/src/test/com/baeldung/longpolling/integration/BakeryControllerIntegrationTest.java new file mode 100644 index 0000000000..abc40d72ca --- /dev/null +++ b/spring-web-modules/spring-rest-http-2/src/test/com/baeldung/longpolling/integration/BakeryControllerIntegrationTest.java @@ -0,0 +1,72 @@ +package com.baeldung.longpolling.integration; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockAsyncContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.io.IOException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; + +@AutoConfigureMockMvc +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BakeryControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void givenDeferredResultTimesOut_ThenErrorResponseIsRecieved() throws Exception { + MvcResult asyncListener = mockMvc + .perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=6000")) + .andExpect(request().asyncStarted()) + .andReturn(); + + enableTimeout(asyncListener); + + String response = mockMvc + .perform(asyncDispatch(asyncListener)) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response) + .isEqualTo("the bakery is not responding in allowed time"); + } + + @Test + public void givenDeferredResultSuccessful_ThenSuccessResponseIsRecieved() throws Exception { + MvcResult asyncListener = mockMvc + .perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=1000")) + .andExpect(request().asyncStarted()) + .andReturn(); + + String response = mockMvc + .perform(asyncDispatch(asyncListener)) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response) + .isEqualTo("Bake for cookie complete and order dispatched. Enjoy!"); + } + + private static void enableTimeout(MvcResult asyncListener) throws IOException { + ((MockAsyncContext) asyncListener + .getRequest() + .getAsyncContext()) + .getListeners() + .get(0) + .onTimeout(null); + } +} diff --git a/spring-web-modules/spring-rest-http/pom.xml b/spring-web-modules/spring-rest-http/pom.xml index 422bcd32f7..94d1be7814 100644 --- a/spring-web-modules/spring-rest-http/pom.xml +++ b/spring-web-modules/spring-rest-http/pom.xml @@ -24,6 +24,10 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-webflux + org.springframework spring-oxm