[BAEL-7525] Article code

This commit is contained in:
Philippe Sevestre 2024-02-24 17:34:23 -03:00
parent ee27de7b46
commit 866af9970f
11 changed files with 237 additions and 105 deletions

View File

@ -19,6 +19,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
@ -50,8 +55,12 @@
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
<generatorName>spring</generatorName>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<templateResourcePath>src/main/resources/templates/JavaSpring</templateResourcePath>
<globalProperties>
<debugOpenAPI>true</debugOpenAPI>
</globalProperties>
<configOptions>
<dateLibrary>custom</dateLibrary>
<dateLibrary>java8</dateLibrary>
<openApiNullable>false</openApiNullable>
<delegatePattern>true</delegatePattern>
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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 {
}

View File

@ -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();
}
}
}

View File

@ -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<String,BigDecimal> 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<BigDecimal> 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);
}
}

View File

@ -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

View File

@ -1,4 +1,5 @@
logging:
level:
root: INFO
org.springframework: DEBUG
org.springframework: INFO

View File

@ -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<NativeWebRequest> 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 <a href="{{url}}">{{summary}} Documentation</a>
{{/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<Part>{{/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}}

View File

@ -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);
}
}

View File

@ -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());
}
}
}