diff --git a/spring-boot-modules/spring-boot-openapi/pom.xml b/spring-boot-modules/spring-boot-openapi/pom.xml
index 6612a429f9..fba77253cf 100644
--- a/spring-boot-modules/spring-boot-openapi/pom.xml
+++ b/spring-boot-modules/spring-boot-openapi/pom.xml
@@ -19,6 +19,11 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
org.springframework.boot
spring-boot-starter-validation
@@ -50,8 +55,12 @@
${project.basedir}/src/main/resources/api/quotes.yaml
spring
ApiUtil.java
+ src/main/resources/templates/JavaSpring
+
+ true
+
- custom
+ java8
false
true
com.baeldung.tutorials.openapi.quotes.api
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java
index 6a7e5fce12..37d8278133 100644
--- a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java
+++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java
@@ -2,10 +2,12 @@ package com.baeldung.tutorials.openapi.quotes;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
@SpringBootApplication
+@EnableCaching
public class QuotesApplication {
public static void main(String[] args) {
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/config/ClockConfiguration.java b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/config/ClockConfiguration.java
new file mode 100644
index 0000000000..60eb6fc967
--- /dev/null
+++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/config/ClockConfiguration.java
@@ -0,0 +1,17 @@
+package com.baeldung.tutorials.openapi.quotes.config;
+
+import java.time.Clock;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ClockConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ Clock defaultClock() {
+ return Clock.systemDefaultZone();
+ }
+}
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java
deleted file mode 100644
index 991d765b97..0000000000
--- a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.baeldung.tutorials.openapi.quotes.controller;
-
-import org.springframework.stereotype.Component;
-
-import com.baeldung.tutorials.openapi.quotes.api.OrdersApiDelegate;
-
-@Component
-public class OrdersApiImpl implements OrdersApiDelegate {
-}
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java
index 40a583d960..f0e4d5c33f 100644
--- a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java
+++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java
@@ -1,9 +1,14 @@
package com.baeldung.tutorials.openapi.quotes.controller;
+import java.time.Clock;
+import java.time.OffsetDateTime;
+
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
+import com.baeldung.tutorials.openapi.quotes.api.QuotesApi;
import com.baeldung.tutorials.openapi.quotes.api.QuotesApiDelegate;
import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
@@ -11,9 +16,11 @@ import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
@Component
public class QuotesApiImpl implements QuotesApiDelegate {
private final BrokerService broker;
+ private final Clock clock;
- public QuotesApiImpl(BrokerService broker) {
+ public QuotesApiImpl(BrokerService broker, Clock clock) {
this.broker = broker;
+ this.clock = clock;
}
@@ -29,14 +36,10 @@ public class QuotesApiImpl implements QuotesApiDelegate {
var price = broker.getSecurityPrice(symbol);
- if ( price.isPresent()) {
var quote = new QuoteResponse();
quote.setSymbol(symbol);
- quote.setPrice(price.get());
+ quote.setPrice(price);
+ quote.setTimestamp(OffsetDateTime.now(clock));
return ResponseEntity.ok(quote);
- }
- else {
- return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
- }
}
}
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java
index f55c0a8656..f7520b098d 100644
--- a/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java
+++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java
@@ -7,23 +7,26 @@ import java.util.Map;
import java.util.Optional;
import java.util.Random;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@Service
public class BrokerService {
- private Map securities = new HashMap<>();
+ private final Logger log = LoggerFactory.getLogger(BrokerService.class);
+
+ private final Random rnd = new Random();
+
public BrokerService() {
- Random rnd = new Random();
- securities.put("GOOG", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
- securities.put("AA", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
- securities.put("BAEL", BigDecimal.valueOf(rnd.nextDouble() * 1000.00).setScale(4, RoundingMode.DOWN));
}
- public Optional getSecurityPrice(@NonNull String symbol) {
- return Optional.ofNullable(securities.get(symbol));
+ public BigDecimal getSecurityPrice(@NonNull String symbol) {
+ log.info("getSecurityPrice: {}", symbol);
+ // Just a mock value
+ return BigDecimal.valueOf(100.0 + rnd.nextDouble()*100.0);
}
}
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml b/spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml
index 283395e27a..590fe661ad 100644
--- a/spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml
+++ b/spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml
@@ -12,6 +12,8 @@ paths:
- quotes
summary: Get current quote for a security
operationId: getQuote
+ x-spring-cacheable:
+ name: get-quotes
security:
- ApiKey:
- Quotes.Read
@@ -30,57 +32,13 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/QuoteResponse'
- /orders/{symbol}:
- post:
- tags:
- - orders
- operationId: createOrder
- security:
- - ApiKey:
- - Orders.Write
- summary: Places a new order to buy a security
- parameters:
- - name: symbol
- in: path
- required: true
- description: Security's symbol
- schema:
- type: string
- pattern: '[A-Z0-9]'
- requestBody:
- required: true
- description: Order info
- content:
- 'application/json':
- schema:
- $ref: '#'#/components/schemas/OrderRequest'
- responses:
- '201':
- description: Order accepted
- content:
- 'application/json':
- schema:
- $ref: '#/components/schemas/OrderResponse'
-
components:
securitySchemes:
ApiKey:
type: apiKey
in: header
name: X-API-KEY
-
schemas:
- OrderType:
- type: string
- enum:
- - BUY
- - SELL
- OrderStatus:
- type: string
- enum:
- - PENDING
- - REJECTED
- - FULFILLED
QuoteResponse:
description: Quote response
type: object
@@ -91,40 +49,6 @@ components:
price:
type: number
description: Quote value
- OrderRequest:
- description: Buy/Sell order details
- type: object
- properties:
- clientRef:
+ timestamp:
type: string
- orderType:
- $ref: '#/components/schemas/OrderType'
- symbol:
- type: string
- description: security's symbol
- quantity:
- type: number
- price:
- type: number
- OrderResponse:
- description: Buy/Sell order response
- properties:
- clientRef:
- type: string
- orderType:
- $ref: '#/components/schemas/OrderType'
- symbol:
- type: string
- description: security's symbol
- quantity:
- type: number
- price:
- type: number
- orderStatus:
- $ref: '#/components/schemas/OrderStatus'
-
-
-
-
-
-
+ format: date-time
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml b/spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml
index 7f074c3a3b..c177283306 100644
--- a/spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml
+++ b/spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml
@@ -1,4 +1,5 @@
+
logging:
level:
root: INFO
- org.springframework: DEBUG
\ No newline at end of file
+ org.springframework: INFO
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-openapi/src/main/resources/templates/JavaSpring/apiDelegate.mustache b/spring-boot-modules/spring-boot-openapi/src/main/resources/templates/JavaSpring/apiDelegate.mustache
new file mode 100644
index 0000000000..a26fb3556d
--- /dev/null
+++ b/spring-boot-modules/spring-boot-openapi/src/main/resources/templates/JavaSpring/apiDelegate.mustache
@@ -0,0 +1,84 @@
+/*
+* Generated code: do not modify !
+* Custom template with support for x-spring-cacheable extension
+*/
+package {{package}};
+
+{{#imports}}import {{import}};
+{{/imports}}
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+{{#useResponseEntity}}
+ import org.springframework.http.ResponseEntity;
+{{/useResponseEntity}}
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.multipart.MultipartFile;
+{{#reactive}}
+ import org.springframework.web.server.ServerWebExchange;
+ import reactor.core.publisher.Flux;
+ import reactor.core.publisher.Mono;
+ import org.springframework.http.codec.multipart.Part;
+{{/reactive}}
+
+{{#useBeanValidation}}
+ import {{javaxPackage}}.validation.constraints.*;
+ import {{javaxPackage}}.validation.Valid;
+{{/useBeanValidation}}
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+{{#async}}
+ import java.util.concurrent.CompletableFuture;
+{{/async}}
+import {{javaxPackage}}.annotation.Generated;
+
+{{#operations}}
+ /**
+ * A delegate to be called by the {@link {{classname}}Controller}}.
+ * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
+ */
+ {{>generatedAnnotation}}
+ public interface {{classname}}Delegate {
+ {{#jdk8-default-interface}}
+
+ default Optional getRequest() {
+ return Optional.empty();
+ }
+ {{/jdk8-default-interface}}
+
+ {{#operation}}
+ /**
+ * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
+ {{#notes}}
+ * {{.}}
+ {{/notes}}
+ *
+ {{#allParams}}
+ * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
+ {{/allParams}}
+ * @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
+ * or {{/-last}}{{/responses}}
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ {{#externalDocs}}
+ * {{description}}
+ * @see {{summary}} Documentation
+ {{/externalDocs}}
+ * @see {{classname}}#{{operationId}}
+ */
+ {{#isDeprecated}}
+ @Deprecated
+ {{/isDeprecated}}
+ {{#vendorExtensions.x-spring-cacheable}}
+ @org.springframework.cache.annotation.Cacheable({{#name}}"{{.}}"{{/name}}{{^name}}"default"{{/name}})
+ {{/vendorExtensions.x-spring-cacheable}}
+ {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{>responseType}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#isArray}}List<{{/isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}}{{/isFile}} {{paramName}}{{^-last}},
+ {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
+ {{/hasParams}}ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#reactive}}, {{/reactive}}{{/hasParams}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
+ {{>methodBody}}
+ }{{/jdk8-default-interface}}
+
+ {{/operation}}
+ }
+{{/operations}}
diff --git a/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/QuotesApplicationIntegrationTest.java b/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/QuotesApplicationIntegrationTest.java
new file mode 100644
index 0000000000..22a5a9cebe
--- /dev/null
+++ b/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/QuotesApplicationIntegrationTest.java
@@ -0,0 +1,43 @@
+package com.baeldung.tutorials.openapi.quotes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.http.HttpEntity;
+
+import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class QuotesApplicationIntegrationTest {
+
+ @LocalServerPort
+ private int port;
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+
+ @Test
+ void whenGetQuoteMultipleTimes_thenResponseCached() {
+
+ // Call server a few times and collect responses
+ var quotes = IntStream.range(1, 10).boxed()
+ .map((i) -> restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class))
+ .map(HttpEntity::getBody)
+ .collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting()));
+
+ assertThat(quotes.size()).isEqualTo(1);
+
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImplUnitTest.java b/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImplUnitTest.java
new file mode 100644
index 0000000000..01e37ef104
--- /dev/null
+++ b/spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImplUnitTest.java
@@ -0,0 +1,55 @@
+package com.baeldung.tutorials.openapi.quotes.controller;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+
+import com.baeldung.tutorials.openapi.quotes.api.QuotesApi;
+
+@SpringBootTest
+class QuotesApiImplUnitTest {
+
+ @Autowired
+ private QuotesApi api;
+
+
+ private static Instant NOW = Instant.now();
+
+ @Test
+ void whenGetQuote_then_success() {
+
+ var response = api.getQuote("GOOG");
+ assertThat(response)
+ .isNotNull();
+
+ assertThat(response.getStatusCode().is2xxSuccessful())
+ .isTrue();
+
+ assertThat(response.getBody().getTimestamp())
+ .isEqualTo(OffsetDateTime.ofInstant(NOW, ZoneId.systemDefault()));
+ }
+
+
+ @TestConfiguration
+ @EnableCaching
+ static class TestConfig {
+
+ @Bean
+ @Primary
+ Clock fixedClock() {
+ return Clock.fixed(NOW, ZoneId.systemDefault());
+ }
+
+ }
+}
\ No newline at end of file