From c5ced2f9362fe738929c9ed13a6ade2dfc34b12e Mon Sep 17 00:00:00 2001 From: Philippe Sevestre Date: Wed, 21 Feb 2024 23:01:05 -0300 Subject: [PATCH 1/5] [BAEL-7524] API Code --- spring-boot-modules/pom.xml | 1 + .../spring-boot-openapi/pom.xml | 77 +++++++++++ .../openapi/quotes/QuotesApplication.java | 14 ++ .../quotes/controller/OrdersApiImpl.java | 9 ++ .../quotes/controller/QuotesApiImpl.java | 42 ++++++ .../openapi/quotes/service/BrokerService.java | 29 ++++ .../src/main/resources/api/quotes.yaml | 130 ++++++++++++++++++ .../src/main/resources/application.yaml | 4 + 8 files changed, 306 insertions(+) create mode 100644 spring-boot-modules/spring-boot-openapi/pom.xml create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml diff --git a/spring-boot-modules/pom.xml b/spring-boot-modules/pom.xml index 4c8102f1c8..cd8703f7f3 100644 --- a/spring-boot-modules/pom.xml +++ b/spring-boot-modules/pom.xml @@ -111,6 +111,7 @@ spring-boot-3-url-matching spring-boot-graalvm-docker spring-boot-validations + spring-boot-openapi diff --git a/spring-boot-modules/spring-boot-openapi/pom.xml b/spring-boot-modules/spring-boot-openapi/pom.xml new file mode 100644 index 0000000000..6612a429f9 --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + spring-boot-openapi + spring-boot-openapi + jar + OpenAPI Generator module + + + com.baeldung.spring-boot-modules + spring-boot-modules + 1.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + javax.validation + validation-api + + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator.version} + + + + generate + + + true + ${project.basedir}/src/main/resources/api/quotes.yaml + spring + ApiUtil.java + + custom + false + true + com.baeldung.tutorials.openapi.quotes.api + com.baeldung.tutorials.openapi.quotes.api.model + source + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + 7.3.0 + 2.2.20 + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..6a7e5fce12 --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/QuotesApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.tutorials.openapi.quotes; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import com.baeldung.tutorials.openapi.quotes.service.BrokerService; + +@SpringBootApplication +public class QuotesApplication { + + public static void main(String[] args) { + SpringApplication.run(QuotesApplication.class, args); + } +} 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 new file mode 100644 index 0000000000..991d765b97 --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000000..40a583d960 --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImpl.java @@ -0,0 +1,42 @@ +package com.baeldung.tutorials.openapi.quotes.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +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; + +@Component +public class QuotesApiImpl implements QuotesApiDelegate { + private final BrokerService broker; + + public QuotesApiImpl(BrokerService broker) { + this.broker = broker; + } + + + /** + * GET /quotes/{symbol} : Get current quote for a security + * + * @param symbol Security's symbol (required) + * @return OK (status code 200) + * @see QuotesApi#getQuote + */ + @Override + public ResponseEntity getQuote(String symbol) { + + var price = broker.getSecurityPrice(symbol); + + if ( price.isPresent()) { + var quote = new QuoteResponse(); + quote.setSymbol(symbol); + quote.setPrice(price.get()); + 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 new file mode 100644 index 0000000000..f55c0a8656 --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/service/BrokerService.java @@ -0,0 +1,29 @@ +package com.baeldung.tutorials.openapi.quotes.service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +@Service +public class BrokerService { + + private Map securities = new HashMap<>(); + + 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)); + } +} 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 new file mode 100644 index 0000000000..283395e27a --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/resources/api/quotes.yaml @@ -0,0 +1,130 @@ +openapi: 3.0.0 +info: + title: Quotes API + version: 1.0.0 +servers: + - description: Test server + url: http://localhost:8080 +paths: + /quotes/{symbol}: + get: + tags: + - quotes + summary: Get current quote for a security + operationId: getQuote + security: + - ApiKey: + - Quotes.Read + parameters: + - name: symbol + in: path + required: true + description: Security's symbol + schema: + type: string + pattern: '[A-Z0-9]+' + responses: + '200': + description: OK + content: + 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 + properties: + symbol: + type: string + description: security's symbol + price: + type: number + description: Quote value + OrderRequest: + description: Buy/Sell order details + type: object + properties: + clientRef: + 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' + + + + + + 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 new file mode 100644 index 0000000000..7f074c3a3b --- /dev/null +++ b/spring-boot-modules/spring-boot-openapi/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +logging: + level: + root: INFO + org.springframework: DEBUG \ No newline at end of file From 866af9970f59e3c1b7a0f7baf536aacd73509fad Mon Sep 17 00:00:00 2001 From: Philippe Sevestre Date: Sat, 24 Feb 2024 17:34:23 -0300 Subject: [PATCH 2/5] [BAEL-7525] Article code --- .../spring-boot-openapi/pom.xml | 11 ++- .../openapi/quotes/QuotesApplication.java | 2 + .../quotes/config/ClockConfiguration.java | 17 ++++ .../quotes/controller/OrdersApiImpl.java | 9 -- .../quotes/controller/QuotesApiImpl.java | 17 ++-- .../openapi/quotes/service/BrokerService.java | 17 ++-- .../src/main/resources/api/quotes.yaml | 84 +------------------ .../src/main/resources/application.yaml | 3 +- .../templates/JavaSpring/apiDelegate.mustache | 84 +++++++++++++++++++ .../QuotesApplicationIntegrationTest.java | 43 ++++++++++ .../controller/QuotesApiImplUnitTest.java | 55 ++++++++++++ 11 files changed, 237 insertions(+), 105 deletions(-) create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/config/ClockConfiguration.java delete mode 100644 spring-boot-modules/spring-boot-openapi/src/main/java/com/baeldung/tutorials/openapi/quotes/controller/OrdersApiImpl.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/main/resources/templates/JavaSpring/apiDelegate.mustache create mode 100644 spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/QuotesApplicationIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-openapi/src/test/java/com/baeldung/tutorials/openapi/quotes/controller/QuotesApiImplUnitTest.java 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 From 222416d78a8ec204188f5ef26c73ff6231149b24 Mon Sep 17 00:00:00 2001 From: Philippe Sevestre Date: Sun, 25 Feb 2024 19:40:57 -0300 Subject: [PATCH 3/5] Use project.basedir for template directory --- spring-boot-modules/spring-boot-openapi/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-openapi/pom.xml b/spring-boot-modules/spring-boot-openapi/pom.xml index fba77253cf..b38846cace 100644 --- a/spring-boot-modules/spring-boot-openapi/pom.xml +++ b/spring-boot-modules/spring-boot-openapi/pom.xml @@ -55,7 +55,7 @@ ${project.basedir}/src/main/resources/api/quotes.yaml spring ApiUtil.java - src/main/resources/templates/JavaSpring + ${project.basedir}/src/main/resources/templates/JavaSpring true From fc439ff99d9bef55dbd4f16c65f87bd523b47e4e Mon Sep 17 00:00:00 2001 From: Philippe Sevestre Date: Wed, 28 Feb 2024 23:41:04 -0300 Subject: [PATCH 4/5] [BAEL-7524] Revert bad merge --- .../quotes/QuotesApplicationIntegrationTest.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 index 22a5a9cebe..b1defb99b1 100644 --- 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 @@ -11,6 +11,7 @@ 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 org.springframework.http.HttpStatus; import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse; @@ -23,6 +24,12 @@ class QuotesApplicationIntegrationTest { @Autowired private TestRestTemplate restTemplate; + @Test + void whenGetQuote_thenSuccess() { + var response = restTemplate.getForEntity("http://localhost:" + port + "/quotes/BAEL", QuoteResponse.class); + assertThat(response.getStatusCode()) + .isEqualTo(HttpStatus.OK); + } @Test void whenGetQuoteMultipleTimes_thenResponseCached() { @@ -34,10 +41,5 @@ class QuotesApplicationIntegrationTest { .collect(Collectors.groupingBy((q -> q.hashCode()), Collectors.counting())); assertThat(quotes.size()).isEqualTo(1); - - - } - - } \ No newline at end of file From 256958034fe3bf48cb7189d015022841b9dcb3d5 Mon Sep 17 00:00:00 2001 From: Philippe Sevestre Date: Wed, 28 Feb 2024 23:59:26 -0300 Subject: [PATCH 5/5] [BAEL-7524] Force spring 2.x --- spring-boot-modules/spring-boot-openapi/pom.xml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/spring-boot-modules/spring-boot-openapi/pom.xml b/spring-boot-modules/spring-boot-openapi/pom.xml index b38846cace..f1cf98e4b5 100644 --- a/spring-boot-modules/spring-boot-openapi/pom.xml +++ b/spring-boot-modules/spring-boot-openapi/pom.xml @@ -9,11 +9,19 @@ OpenAPI Generator module - com.baeldung.spring-boot-modules - spring-boot-modules - 1.0.0-SNAPSHOT + org.springframework.boot + spring-boot-starter-parent + 2.7.11 + + + + + + + + org.springframework.boot @@ -79,6 +87,8 @@ + 17 + 17 7.3.0 2.2.20