[BAEL-7525] Article code
This commit is contained in:
parent
ee27de7b46
commit
866af9970f
@ -19,6 +19,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
@ -50,8 +55,12 @@
|
|||||||
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
|
<inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
|
||||||
<generatorName>spring</generatorName>
|
<generatorName>spring</generatorName>
|
||||||
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
|
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
|
||||||
|
<templateResourcePath>src/main/resources/templates/JavaSpring</templateResourcePath>
|
||||||
|
<globalProperties>
|
||||||
|
<debugOpenAPI>true</debugOpenAPI>
|
||||||
|
</globalProperties>
|
||||||
<configOptions>
|
<configOptions>
|
||||||
<dateLibrary>custom</dateLibrary>
|
<dateLibrary>java8</dateLibrary>
|
||||||
<openApiNullable>false</openApiNullable>
|
<openApiNullable>false</openApiNullable>
|
||||||
<delegatePattern>true</delegatePattern>
|
<delegatePattern>true</delegatePattern>
|
||||||
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
|
<apiPackage>com.baeldung.tutorials.openapi.quotes.api</apiPackage>
|
||||||
|
@ -2,10 +2,12 @@ package com.baeldung.tutorials.openapi.quotes;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
|
||||||
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableCaching
|
||||||
public class QuotesApplication {
|
public class QuotesApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
|
||||||
}
|
|
@ -1,9 +1,14 @@
|
|||||||
package com.baeldung.tutorials.openapi.quotes.controller;
|
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.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Component;
|
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.QuotesApiDelegate;
|
||||||
import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
|
import com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse;
|
||||||
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
||||||
@ -11,9 +16,11 @@ import com.baeldung.tutorials.openapi.quotes.service.BrokerService;
|
|||||||
@Component
|
@Component
|
||||||
public class QuotesApiImpl implements QuotesApiDelegate {
|
public class QuotesApiImpl implements QuotesApiDelegate {
|
||||||
private final BrokerService broker;
|
private final BrokerService broker;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
public QuotesApiImpl(BrokerService broker) {
|
public QuotesApiImpl(BrokerService broker, Clock clock) {
|
||||||
this.broker = broker;
|
this.broker = broker;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -29,14 +36,10 @@ public class QuotesApiImpl implements QuotesApiDelegate {
|
|||||||
|
|
||||||
var price = broker.getSecurityPrice(symbol);
|
var price = broker.getSecurityPrice(symbol);
|
||||||
|
|
||||||
if ( price.isPresent()) {
|
|
||||||
var quote = new QuoteResponse();
|
var quote = new QuoteResponse();
|
||||||
quote.setSymbol(symbol);
|
quote.setSymbol(symbol);
|
||||||
quote.setPrice(price.get());
|
quote.setPrice(price);
|
||||||
|
quote.setTimestamp(OffsetDateTime.now(clock));
|
||||||
return ResponseEntity.ok(quote);
|
return ResponseEntity.ok(quote);
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,23 +7,26 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class BrokerService {
|
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() {
|
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) {
|
public BigDecimal getSecurityPrice(@NonNull String symbol) {
|
||||||
return Optional.ofNullable(securities.get(symbol));
|
log.info("getSecurityPrice: {}", symbol);
|
||||||
|
// Just a mock value
|
||||||
|
return BigDecimal.valueOf(100.0 + rnd.nextDouble()*100.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ paths:
|
|||||||
- quotes
|
- quotes
|
||||||
summary: Get current quote for a security
|
summary: Get current quote for a security
|
||||||
operationId: getQuote
|
operationId: getQuote
|
||||||
|
x-spring-cacheable:
|
||||||
|
name: get-quotes
|
||||||
security:
|
security:
|
||||||
- ApiKey:
|
- ApiKey:
|
||||||
- Quotes.Read
|
- Quotes.Read
|
||||||
@ -30,57 +32,13 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/QuoteResponse'
|
$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:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
ApiKey:
|
ApiKey:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
in: header
|
in: header
|
||||||
name: X-API-KEY
|
name: X-API-KEY
|
||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
OrderType:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- BUY
|
|
||||||
- SELL
|
|
||||||
OrderStatus:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- PENDING
|
|
||||||
- REJECTED
|
|
||||||
- FULFILLED
|
|
||||||
QuoteResponse:
|
QuoteResponse:
|
||||||
description: Quote response
|
description: Quote response
|
||||||
type: object
|
type: object
|
||||||
@ -91,40 +49,6 @@ components:
|
|||||||
price:
|
price:
|
||||||
type: number
|
type: number
|
||||||
description: Quote value
|
description: Quote value
|
||||||
OrderRequest:
|
timestamp:
|
||||||
description: Buy/Sell order details
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
clientRef:
|
|
||||||
type: string
|
type: string
|
||||||
orderType:
|
format: date-time
|
||||||
$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'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
org.springframework: DEBUG
|
org.springframework: INFO
|
@ -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}}
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user