├── .gitignore ├── README.adoc ├── docs ├── README.adoc └── index.html ├── stock-details ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── spring │ │ │ └── workshop │ │ │ └── stockdetails │ │ │ ├── StockDetailsApplication.java │ │ │ ├── TradingCompany.java │ │ │ ├── TradingCompanyCommandLineRunner.java │ │ │ ├── TradingCompanyController.java │ │ │ └── TradingCompanyRepository.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── spring │ └── workshop │ └── stockdetails │ ├── StockDetailsApplicationTests.java │ └── TradingCompanyControllerTests.java ├── stock-quotes ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── spring │ │ │ └── workshop │ │ │ └── stockquotes │ │ │ ├── Quote.java │ │ │ ├── QuoteGenerator.java │ │ │ ├── QuoteHandler.java │ │ │ ├── QuoteRouter.java │ │ │ └── StockQuotesApplication.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── spring │ └── workshop │ └── stockquotes │ └── StockQuotesApplicationTests.java ├── trading-service ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── spring │ │ │ └── workshop │ │ │ └── tradingservice │ │ │ ├── Quote.java │ │ │ ├── QuotesClient.java │ │ │ ├── QuotesController.java │ │ │ ├── TickerNotFoundException.java │ │ │ ├── TradingCompany.java │ │ │ ├── TradingCompanyClient.java │ │ │ ├── TradingCompanyController.java │ │ │ ├── TradingCompanySummary.java │ │ │ ├── TradingServiceApplication.java │ │ │ └── websocket │ │ │ ├── EchoWebSocketHandler.java │ │ │ ├── WebSocketController.java │ │ │ └── WebSocketRouter.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── spring │ └── workshop │ └── tradingservice │ ├── TradingServiceApplicationTests.java │ └── websocket │ └── EchoWebSocketHandlerTests.java └── ui ├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src ├── app │ ├── app.module.ts │ ├── app │ │ ├── app.component.html │ │ └── app.component.ts │ ├── core │ │ └── layout │ │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ └── footer.component.ts │ │ │ └── header │ │ │ ├── header.component.html │ │ │ └── header.component.ts │ └── quotes │ │ ├── quotes.component.html │ │ ├── quotes.component.ts │ │ └── quotes.service.ts ├── assets │ ├── .gitkeep │ ├── images │ │ └── spring-logo-dataflow.png │ └── scss │ │ └── normalize.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── tsconfig.app.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[op] 2 | .idea 3 | target 4 | 5 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Spring WebFlux Workshop 2 | 3 | This repository hosts a complete workshop on Spring Boot + Spring WebFlux. 4 | You can follow the script 5 | https://bclozel.github.io/webflux-workshop/[follow the script] 6 | (the source document is in the "docs" folder) to create your first 7 | WebFlux applications! 8 | Each step of this workshop has its companion commit in the git history with a detailed commit message. 9 | 10 | -------------------------------------------------------------------------------- /docs/README.adoc: -------------------------------------------------------------------------------- 1 | = Head-First Reactive Workshop 2 | Stephane Maldini, Ben Hale, Madhura Bhave - Pivotal 3 | :sectanchors: true 4 | :source-highlighter: prettify 5 | :icons: font 6 | :toc: 7 | :spring-boot-version: 2.0.0.RC2 8 | :spring-framework-version: 5.0.4.RELEASE 9 | :reactor-version: BISMUTH-SR6 10 | :spring-framework-doc-base: http://docs.spring.io/spring-framework/docs/{spring-framework-version} 11 | 12 | This repository hosts a complete workshop on Spring + Reactor. 13 | Just follow this README and create your first reactive Spring applications! 14 | Each step of this workshop has its companion commit in the git history with a detailed commit message. 15 | 16 | At the end of the workshop, we will have created three applications: 17 | 18 | * `stock-quotes` is a functional WebFlux app which streams stock quotes 19 | * `stock-details` is an annotation-based WebFlux app using a reactive datastore 20 | * `trading-service` is an annotation-based WebFlux app that consumes data from `stock-quotes` and `stock-details` 21 | 22 | Reference documentations can be useful while working on those apps: 23 | 24 | * http://projectreactor.io/docs[Reactor Core documentation] 25 | * https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html[API documentation for Flux] 26 | * https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[API documentation for Mono] 27 | * Spring WebFlux 28 | {spring-framework-doc-base}/spring-framework-reference/web.html#web-reactive[reference documentation] 29 | and {spring-framework-doc-base}/javadoc-api/[javadoc] 30 | 31 | Clone the repository: 32 | 33 | git clone https://github.com/reactor/head-first-reactive-with-spring-and-reactor.git 34 | 35 | We will start off by creating the Trading Service application which gets data from a pre-existing Stock Quotes application. 36 | 37 | == Trading Service application 38 | 39 | === Create this application 40 | 41 | Go to `https://start.spring.io` and create a Maven project with Spring Boot {spring-boot-version}, 42 | with groupId `io.spring.workshop` and artifactId `trading-service`. Select the `Reactive Web` and `Devtools` dependencies. 43 | Unzip the given file into a directory and import that application into your IDE. 44 | 45 | If generated right, you should have a main `Application` class that looks like this: 46 | 47 | [source,java] 48 | .stock-quotes/src/main/java/io/spring/workshop/tradingservice/TradingServiceApplication.java 49 | ---- 50 | include::../trading-service/src/main/java/io/spring/workshop/tradingservice/TradingServiceApplication.java[] 51 | ---- 52 | 53 | Note that, by default, `spring-boot-starter-webflux` transitively brings `spring-boot-starter-reactor-netty` 54 | and Spring Boot auto-configures Reactor Netty as a web server. 55 | 56 | Spring Boot supports Tomcat, Undertow and Jetty as well. 57 | 58 | === Use the WebClient to stream JSON to the browser 59 | 60 | In this section, we'll call our remote `stock-quotes` service to get Quotes from it, so we first need to: 61 | 62 | * copy over the `Quote` class to this application 63 | * add the Jackson JSR310 module dependency 64 | 65 | Create a `QuotesClient` annotated with `@Component` and inject a `WebClient.Builder`. Now, create a method in the `QuotesClient` called 66 | `quotesFeed` which will use the `webClient` to consume the stream of quotes via Server Sent Events (SSE). 67 | 68 | NOTE: There are two ways to use a `WebClient`, directly via the static factory or by injecting the `WebClient.Builder`. 69 | The latter is used by libraries such as Spring Cloud Sleuth that enrich `WebClient` with extra features. 70 | 71 | Now create a `QuotesController` annotated with `@Controller` and inject it with the `QuotesClient`. 72 | Add a method that responds to `"GET /quotes/feed"` requests with the `"text/event-stream"` content-type, 73 | with a `Flux` as the response body. The data can be retrieved from the `quotesFeed` method on `QuotesClient`. 74 | 75 | You can test it by starting both applications. First, start the Stock Quotes application. 76 | It can be started from your IDE or with `mvn spring-boot:run` and it should run a Netty server on port 8081. 77 | You should see in the logs something like: 78 | 79 | [source,bash] 80 | ---- 81 | INFO 2208 --- [ restartedMain] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8081 82 | INFO 2208 --- [ restartedMain] i.s.w.s.StockQuotesApplication : Started StockQuotesApplication in 1.905 seconds (JVM running for 3.075) 83 | ---- 84 | 85 | Start the Trading Service application from your IDE or with `mvn spring-boot:run`. This should run a Netty server on port 8080. 86 | 87 | [source,bash] 88 | ---- 89 | INFO 2208 --- [ restartedMain] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 90 | INFO 2208 --- [ restartedMain] i.s.w.t.TradingServiceApplication : Started TradingServiceApplication in 1.905 seconds (JVM running for 3.075) 91 | ---- 92 | 93 | You can hit http://localhost:8080/quotes/feed to consume the stream of quotes. 94 | 95 | Now, let's create another application that can provide the details for a trading company. 96 | 97 | == Stock Details application 98 | 99 | === Create this application 100 | 101 | Go to `https://start.spring.io` and create a Maven project with Spring Boot {spring-boot-version}, 102 | with groupId `io.spring.workshop` and artifactId `stock-details`. Select the `Reactive Web`, `Devtools` 103 | and `Reactive Mongo` dependencies. 104 | 105 | Unzip the given file into a directory and import that application into your IDE. 106 | 107 | If generated right, you should have a main `Application` class that looks like this: 108 | 109 | [source,java] 110 | .stock-details/src/main/java/io/spring/workshop/stockdetails/StockDetailsApplication.java 111 | ---- 112 | include::../stock-details/src/main/java/io/spring/workshop/stockdetails/StockDetailsApplication.java[] 113 | ---- 114 | 115 | Edit your `application.properties` file to start the server on port 8082. 116 | 117 | [source,properties] 118 | .stock-details/src/main/resources/application.properties 119 | ---- 120 | include::../stock-details/src/main/resources/application.properties[] 121 | ---- 122 | 123 | === Use a reactive datastore 124 | 125 | In this application, we’ll use a MongoDB datastore with its reactive driver; for this workshop, we’ll use an in-memory instance of MongoDB. So add the following: 126 | 127 | [source,xml] 128 | .stock-details/pom.xml 129 | ---- 130 | include::../stock-details/pom.xml[tags=inMemMongo] 131 | ---- 132 | 133 | We'd like to manage `TradingCompany` with our datastore. 134 | 135 | [source,java] 136 | .stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompany.java 137 | ---- 138 | include::../stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompany.java[] 139 | ---- 140 | 141 | Now create a `TradingCompanyRepository` interface that extends `ReactiveMongoRepository`. 142 | Add a `findByTicker(String ticker)` method that returns a single `TradingCompany` in a reactive fashion. 143 | 144 | We'd like to insert trading companies in our datastore when the application starts up. For that, create a `TradingCompanyCommandLineRunner` 145 | component that implements Spring Boot's `CommandLineRunner`. In the `run` method, use the reactive repository 146 | to insert `TradingCompany` instances in the datastore. 147 | 148 | NOTE: Since the `run` method returns void, it expects a blocking implementation. This is why you should use the 149 | `blockLast(Duration)` operator on the `Flux` returned by the repository when inserting data. 150 | You can also `then().block(Duration)` to turn that `Flux` into a `Mono` that waits for completion. 151 | 152 | === Create a JSON web service 153 | 154 | We're now going to expose `TradingCompany` through a Controller. 155 | First, create a `TradingCompanyController` annotated with `@RestController` and inject the `TradingCompanyRepository`. 156 | Then add two new Controller methods in order to handle: 157 | 158 | * GET requests to `"/details"`, returning all `TradingCompany` instances, serializing them with content-type `"application/json"` 159 | * GET requests to `"/details/{ticker}"`, returning a single `TradingCompany` instance, serializing it with content-type `"application/json"` 160 | 161 | It can be started from your IDE or with `mvn spring-boot:run` and it should run a Netty server on port 8082. 162 | You should see in the logs something like: 163 | 164 | [source,bash] 165 | ---- 166 | INFO 2208 --- [ restartedMain] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8082 167 | INFO 2208 --- [ restartedMain] i.s.w.s.StockDetailsApplication : Started StockDetailsApplication in 1.905 seconds (JVM running for 3.075) 168 | ---- 169 | 170 | Now that we have an application that can return the details for a company with a given ticker, we can update the Trading Service application 171 | to use those details and return a combination of the latest quote for that ticker along with the company's details. 172 | 173 | == Update Trading Service application 174 | 175 | We will first create a service that will use a `WebClient` to get data from the Stock Details application. 176 | Create a `TradingCompanyClient` annotated with `@Component`. 177 | Then add two methods: 178 | 179 | * `findAllCompanies` will return a `Flux` by using the webClient to get data from the `/details` endpoint from the Stock details application 180 | * `getTradingCompany` will return a `Mono` by using the webClient to get data from the `/details/{ticker}` endpoint from the Stock details application 181 | - If no trading company is found for the given ticker, the `Mono` will complete without any data. Instead, we will emit a `TickerNotFoundException` error using the `switchIfEmpty` operator. 182 | 183 | Let's expose the two `TradingCompanyClient` methods with a local `TradingCompanyController`. 184 | 185 | You might have updated an already running `TradingServiceApplication`. Since DevTools is present, you can just recompile to automatically restart the application. 186 | 187 | You can test the new endpoints by hitting: 188 | 189 | * http://localhost:8080/details to list all trading companies 190 | * http://localhost:8080/details/MSFT to get details for a particular ticker 191 | * http://localhost:8080/details/PIZZA to see what happens for an unknown ticker 192 | 193 | Let's add a method called `getLatestQuote(String ticker)` on the `QuotesClient` which will get the latest quote for a given ticker from the quotes feed. 194 | 195 | * Reuse `quotesFeed` to get the actual feed 196 | * Filter the stream of quotes with quotes matching the given ticker 197 | * Take the next matching quote 198 | * If no matching quote is found within 15 seconds, then return a fallback `Quote` for the ticker. 199 | 200 | Now, we want to combine data produced by `stock-quotes` and `stock-details` as a `TradingCompanySummary`. 201 | 202 | Copy the following class to your project. 203 | 204 | [source,java] 205 | .trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompanySummary.java 206 | ---- 207 | include::../trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompanySummary.java[] 208 | ---- 209 | 210 | Now, add another method to the `QuotesController` which can handle requests to `/quotes/summary/{ticker}`. 211 | 212 | * Use the `TradingCompanyClient` to get the details for the company with the given ticker 213 | * Create a `TradingCompanySummary` by composing the details with the latest quote from the `QuotesClient` 214 | 215 | TIP: You can compose two reactive results using `Mono.zip(monoA, monoB, biFunction)` or `monoA.zipWith(monoB, biFunction)`. 216 | 217 | We need to handle the `TickerNotFoundException` emitted by the `TradingCompanyClient` as an HTTP 404. 218 | You will need to create a method annotated with `@ExceptionHandler`. 219 | 220 | Again, because of DevTools we can just recompile and test by hitting: 221 | 222 | * http://localhost:8080/quotes/summary/MSFT to get the summary for a particular ticker 223 | * http://localhost:8080/quotes/summary/PIZZA to test the 404 NOT FOUND an unknown ticker 224 | 225 | Now, we will look at creating a functional-style WebFlux application by re-creating the Stock Quotes Application. 226 | Fasten your seatbelt and remove the stock-quotes directory! 227 | 228 | == Stock Quotes application 229 | 230 | === Create this application 231 | 232 | Go to `https://start.spring.io` and create a Maven project with Spring Boot {spring-boot-version}, 233 | with groupId `io.spring.workshop` and artifactId `stock-quotes`. Select the `Reactive Web` and `Devtools` 234 | dependencies. 235 | Unzip the given file into a directory and import that application into your IDE. 236 | 237 | If generated right, you should have a main `Application` class that looks like this: 238 | 239 | [source,java] 240 | .stock-quotes/src/main/java/io/spring/workshop/stockquotes/StockQuotesApplication.java 241 | ---- 242 | include::../stock-quotes/src/main/java/io/spring/workshop/stockquotes/StockQuotesApplication.java[] 243 | ---- 244 | 245 | Edit your `application.properties` file to start the server on port 8081. 246 | 247 | [source,properties] 248 | .stock-quotes/src/main/resources/application.properties 249 | ---- 250 | include::../stock-quotes/src/main/resources/application.properties[] 251 | ---- 252 | 253 | Launching it from your IDE or with `mvn spring-boot:run` should start a Netty server on port 8081. 254 | You should see in the logs something like: 255 | 256 | [source,bash] 257 | ---- 258 | INFO 2208 --- [ restartedMain] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8081 259 | INFO 2208 --- [ restartedMain] i.s.w.s.StockQuotesApplication : Started StockQuotesApplication in 1.905 seconds (JVM running for 3.075) 260 | ---- 261 | 262 | === Create a Quote Generator 263 | 264 | To simulate real stock values, we'll create a generator that emits such values at a specific interval. 265 | Copy the following classes to your project. 266 | 267 | [source,java] 268 | .stock-quotes/src/main/java/io/spring/workshop/stockquotes/Quote.java 269 | ---- 270 | include::../stock-quotes/src/main/java/io/spring/workshop/stockquotes/Quote.java[] 271 | ---- 272 | 273 | [source,java] 274 | .stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteGenerator.java 275 | ---- 276 | include::../stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteGenerator.java[] 277 | ---- 278 | 279 | Because we're working with `java.time.Instant` and Jackson, we should import the dedicated module in our app. 280 | 281 | NOTE: The `QuoteGenerator` instantiates a `Flux` that emits a `Quote` every 200 msec and can be **shared** between 282 | multiple subscribers (look at the `Flux` operators for that). This instance is kept as an attribute for reusability. 283 | 284 | [source,xml] 285 | .stock-quotes/pom.xml 286 | ---- 287 | include::../stock-quotes/pom.xml[tags=jacksonJSR310] 288 | ---- 289 | 290 | === Functional web applications with "WebFlux.fn" 291 | 292 | Spring WebFlux comes in two flavors of web applications: annotation based and functional. 293 | For this first application, we'll use the functional variant. 294 | 295 | Incoming HTTP requests are handled by a `HandlerFunction`, which is essentially a function 296 | that takes a ServerRequest and returns a `Mono`. The annotation counterpart 297 | to a handler function would be a Controller method. 298 | 299 | But how those incoming requests are routed to the right handler? 300 | 301 | We're using a `RouterFunction`, which is a function that takes a `ServerRequest`, and returns 302 | a `Mono`. If a request matches a particular route, a handler function is returned; 303 | otherwise it returns an empty `Mono`. The `RouterFunction` has a similar purpose as the `@RequestMapping` 304 | annotation in `@Controller` classes. 305 | 306 | Take a look at the code samples in 307 | {spring-framework-doc-base}/spring-framework-reference/web.html#web-reactive-server-functional[the Spring WebFlux.fn reference documentation] 308 | 309 | === Create your first HandlerFunction + RouterFunction 310 | 311 | First, create a `QuoteHandler` class and mark is as a `@Component`;this class will have all our handler functions as methods. 312 | Let's inject our `QuoteGenerator` instance in our `QuoteHandler`. 313 | 314 | Now create a `streamQuotes` handler that streams the generated quotes with the `"application/stream+json"` content type. 315 | 316 | To route requests to that handler, you need to expose a `RouterFunction` to Spring Boot. 317 | Create a `QuoteRouter` configuration class (i.e. annotated with `@Configuration`) 318 | that creates a bean of type `RouterFunction`. 319 | 320 | Modify that class so that GET requests to `"/quotes"` are routed to the handler you just implemented. 321 | 322 | TIP: Since `QuoteHandler` is a component, you can inject it in `@Bean` methods as a method parameter. 323 | 324 | Your application should now behave like this: 325 | 326 | [source,bash] 327 | ---- 328 | $ curl http://localhost:8081/quotes -i -H "Accept: application/stream+json" 329 | HTTP/1.1 200 OK 330 | transfer-encoding: chunked 331 | Content-Type: application/stream+json 332 | 333 | {"ticker":"CTXS","price":84.0,"instant":1494841666.633000000} 334 | {"ticker":"DELL","price":67.1,"instant":1494841666.834000000} 335 | {"ticker":"GOOG","price":869,"instant":1494841667.034000000} 336 | {"ticker":"MSFT","price":66.5,"instant":1494841667.231000000} 337 | {"ticker":"ORCL","price":46.13,"instant":1494841667.433000000} 338 | {"ticker":"RHT","price":86.9,"instant":1494841667.634000000} 339 | {"ticker":"VMW","price":93.7,"instant":1494841667.833000000} 340 | ---- 341 | 342 | === Integration tests with WebTestClient 343 | 344 | Spring WebFlux (actually the `spring-test` module) includes a `WebTestClient` 345 | that can be used to test WebFlux server endpoints with or without a running server. 346 | Tests without a running server are comparable to MockMvc from Spring MVC where mock request 347 | and response are used instead of connecting over the network using a socket. 348 | The WebTestClient however can also perform tests against a running server. 349 | 350 | You can check that your last endpoint is working properly with the following 351 | integration test: 352 | 353 | [source,java] 354 | .stock-quotes/src/test/java/io/spring/workshop/stockquotes/StockQuotesApplicationTests.java 355 | ---- 356 | include::../stock-quotes/src/test/java/io/spring/workshop/stockquotes/StockQuotesApplicationTests.java[] 357 | ---- 358 | 359 | == Additional Resources 360 | 361 | Talks on Spring Reactive: 362 | 363 | * https://www.youtube.com/watch?v=rdgJ8fOxJhc[Reactive Web Applications with Spring 5 (Rossen Stoyanchev)] 364 | * https://www.youtube.com/watch?v=Cj4foJzPF80[Developing Reactive applications with Reactive Streams and Java 8 (B.Clozel, S.Deleuze)] 365 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Spring WebFlux Workshop 10 | 11 | 420 | 421 | 422 | 423 | 457 |
458 |
459 |
460 |
461 |

This repository hosts a complete workshop on Spring Boot + Spring WebFlux. 462 | Just follow this README and create your first WebFlux applications! 463 | Each step of this workshop has its companion commit in the git history with a detailed commit message.

464 |
465 |
466 |

We’ll create two applications:

467 |
468 |
469 |
    470 |
  • 471 |

    stock-quotes is a functional WebFlux app which streams stock quotes

    472 |
  • 473 |
  • 474 |

    trading-service is an annotation-based WebFlux app using a datastore, HTML views, and several browser-related technologies

    475 |
  • 476 |
477 |
478 |
479 |

Reference documentations can be useful while working on those apps:

480 |
481 |
482 | 492 |
493 |
494 |
495 |
496 |

Stock Quotes application

497 |
498 |
499 |

Create this application

500 |
501 |

Go to https://start.spring.io and create a Maven project with Spring Boot 2.0.0.M1, 502 | with groupId io.spring.workshop and artifactId stock-quotes. Select the Reactive Web 503 | Boot starter. 504 | Unzip the given file into a directory and import that application into your IDE.

505 |
506 |
507 |

If generated right, you should have a main Application class that looks like this:

508 |
509 |
510 |
stock-quotes/src/main/java/io/spring/workshop/stockquotes/StockQuotesApplication.java
511 |
512 |
package io.spring.workshop.stockquotes;
 513 | 
 514 | import org.springframework.boot.SpringApplication;
 515 | import org.springframework.boot.autoconfigure.SpringBootApplication;
 516 | 
 517 | @SpringBootApplication
 518 | public class StockQuotesApplication {
 519 | 
 520 | 	public static void main(String[] args) {
 521 | 		SpringApplication.run(StockQuotesApplication.class, args);
 522 | 	}
 523 | }
524 |
525 |
526 |
527 |

Edit your application.properties file to start the server on a specific port.

528 |
529 |
530 |
stock-quotes/src/main/resources/application.properties
531 |
532 |
server.port=8081
533 |
534 |
535 |
536 |

Launching it from your IDE or with mvn spring-boot:run should start a Netty server on port 8081. 537 | You should see in the logs something like:

538 |
539 |
540 |
541 |
INFO 2208 --- [  restartedMain] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8081
 542 | INFO 2208 --- [  restartedMain] i.s.w.s.StockQuotesApplication           : Started StockQuotesApplication in 1.905 seconds (JVM running for 3.075)
543 |
544 |
545 |
546 |
547 |

Create a Quote Generator

548 |
549 |

To simulate real stock values, we’ll create a generator that emits such values at a specific interval. 550 | Copy the following classes to your project.

551 |
552 |
553 |
stock-quotes/src/main/java/io/spring/workshop/stockquotes/Quote.java
554 |
555 |
package io.spring.workshop.stockquotes;
 556 | 
 557 | import java.math.BigDecimal;
 558 | import java.math.MathContext;
 559 | import java.time.Instant;
 560 | 
 561 | public class Quote {
 562 | 
 563 | 	private static final MathContext MATH_CONTEXT = new MathContext(2);
 564 | 
 565 | 	private String ticker;
 566 | 
 567 | 	private BigDecimal price;
 568 | 
 569 | 	private Instant instant;
 570 | 
 571 | 	public Quote() {
 572 | 	}
 573 | 
 574 | 	public Quote(String ticker, BigDecimal price) {
 575 | 		this.ticker = ticker;
 576 | 		this.price = price;
 577 | 	}
 578 | 
 579 | 	public Quote(String ticker, Double price) {
 580 | 		this(ticker, new BigDecimal(price, MATH_CONTEXT));
 581 | 	}
 582 | 
 583 | 	public String getTicker() {
 584 | 		return ticker;
 585 | 	}
 586 | 
 587 | 	public void setTicker(String ticker) {
 588 | 		this.ticker = ticker;
 589 | 	}
 590 | 
 591 | 	public BigDecimal getPrice() {
 592 | 		return price;
 593 | 	}
 594 | 
 595 | 	public void setPrice(BigDecimal price) {
 596 | 		this.price = price;
 597 | 	}
 598 | 
 599 | 	public Instant getInstant() {
 600 | 		return instant;
 601 | 	}
 602 | 
 603 | 	public void setInstant(Instant instant) {
 604 | 		this.instant = instant;
 605 | 	}
 606 | 
 607 | 	@Override
 608 | 	public String toString() {
 609 | 		return "Quote{" +
 610 | 				"ticker='" + ticker + '\'' +
 611 | 				", price=" + price +
 612 | 				", instant=" + instant +
 613 | 				'}';
 614 | 	}
 615 | }
616 |
617 |
618 |
619 |
stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteGenerator.java
620 |
621 |
package io.spring.workshop.stockquotes;
 622 | 
 623 | import java.math.BigDecimal;
 624 | import java.math.MathContext;
 625 | import java.time.Duration;
 626 | import java.time.Instant;
 627 | import java.util.ArrayList;
 628 | import java.util.List;
 629 | import java.util.Random;
 630 | import java.util.function.BiFunction;
 631 | 
 632 | import reactor.core.publisher.Flux;
 633 | import reactor.core.publisher.SynchronousSink;
 634 | 
 635 | import org.springframework.stereotype.Component;
 636 | 
 637 | @Component
 638 | public class QuoteGenerator {
 639 | 
 640 | 	private final MathContext mathContext = new MathContext(2);
 641 | 
 642 | 	private final Random random = new Random();
 643 | 
 644 | 	private final List<Quote> prices = new ArrayList<>();
 645 | 
 646 | 	/**
 647 | 	 * Bootstraps the generator with tickers and initial prices
 648 | 	 */
 649 | 	public QuoteGenerator() {
 650 | 		this.prices.add(new Quote("CTXS", 82.26));
 651 | 		this.prices.add(new Quote("DELL", 63.74));
 652 | 		this.prices.add(new Quote("GOOG", 847.24));
 653 | 		this.prices.add(new Quote("MSFT", 65.11));
 654 | 		this.prices.add(new Quote("ORCL", 45.71));
 655 | 		this.prices.add(new Quote("RHT", 84.29));
 656 | 		this.prices.add(new Quote("VMW", 92.21));
 657 | 	}
 658 | 
 659 | 
 660 | 	public Flux<Quote> fetchQuoteStream(Duration period) {
 661 | 
 662 | 		// We use here Flux.generate to create quotes,
 663 |     // iterating on each stock starting at index 0
 664 | 		return Flux.generate(() -> 0,
 665 | 				(BiFunction<Integer, SynchronousSink<Quote>, Integer>) (index, sink) -> {
 666 | 					Quote updatedQuote = updateQuote(this.prices.get(index));
 667 | 					sink.next(updatedQuote);
 668 | 					return ++index % this.prices.size();
 669 | 				})
 670 | 				// We want to emit them with a specific period;
 671 |         // to do so, we zip that Flux with a Flux.interval
 672 | 				.zipWith(Flux.interval(period)).map(t -> t.getT1())
 673 | 				// Because values are generated in batches,
 674 |         // we need to set their timestamp after their creation
 675 | 				.map(quote -> {
 676 | 					quote.setInstant(Instant.now());
 677 | 					return quote;
 678 | 				})
 679 | 				.log("io.spring.workshop.stockquotes");
 680 | 	}
 681 | 
 682 | 	private Quote updateQuote(Quote quote) {
 683 | 		BigDecimal priceChange = quote.getPrice()
 684 | 				.multiply(new BigDecimal(0.05 * this.random.nextDouble()), this.mathContext);
 685 | 		return new Quote(quote.getTicker(), quote.getPrice().add(priceChange));
 686 | 	}
 687 | }
688 |
689 |
690 |
691 |

Because we’re working with java.time.Instant and Jackson, we should import the dedicated module in our app.

692 |
693 |
694 |
stock-quotes/pom.xml
695 |
696 |
		<dependency>
 697 | 			<groupId>com.fasterxml.jackson.datatype</groupId>
 698 | 			<artifactId>jackson-datatype-jsr310</artifactId>
 699 | 		</dependency>
700 |
701 |
702 |
703 |
704 |

Functional web applications with "WebFlux.fn"

705 |
706 |

Spring WebFlux comes in two flavors of web applications: annotation based and functional. 707 | For this first application, we’ll use the functional variant.

708 |
709 |
710 |

Incoming HTTP requests are handled by a HandlerFunction, which is essentially a function 711 | that takes a ServerRequest and returns a Mono<ServerResponse>. The annotation counterpart 712 | to a handler function would be a Controller method.

713 |
714 |
715 |

But how those incoming requests are routed to the right handler?

716 |
717 |
718 |

We’re using a RouterFunction, which is a function that takes a ServerRequest, and returns 719 | a Mono<HandlerFunction>. If a request matches a particular route, a handler function is returned; 720 | otherwise it returns an empty Mono. The RouterFunction has a similar purpose as the @RequestMapping 721 | annotation in @Controller classes.

722 |
723 |
724 |

Take a look at the code samples in 725 | the Spring WebFlux.fn reference documentation

726 |
727 |
728 |
729 |

Create your first HandlerFunction + RouterFunction

730 |
731 |

First, create a QuoteHandler class and mark is as a @Component;this class will have all our handler functions as methods. 732 | Now create a hello handler function in that class that always returns "text/plain" HTTP responses with "Hello Spring!" as body.

733 |
734 |
735 |

To route requests to that handler, you need to expose a RouterFunction to Spring Boot. 736 | Create a QuoteRouter configuration class (i.e. annotated with @Configuration) 737 | that creates a bean of type RouterFunction<ServerResponse>.

738 |
739 |
740 |

Modify that class so that GET requests to "/hello" are routed to the handler you just implemented.

741 |
742 |
743 | 744 | 745 | 748 | 751 | 752 |
746 | 747 | 749 | Since QuoteHandler is a component, you can inject it in @Bean methods as a method parameter. 750 |
753 |
754 |
755 |

Your application should now behave like this:

756 |
757 |
758 |
759 |
$ curl http://localhost:8081/hello -i
 760 | HTTP/1.1 200 OK
 761 | transfer-encoding: chunked
 762 | Content-Type: text/plain;charset=UTF-8
 763 | 
 764 | Hello Spring!%
765 |
766 |
767 |
768 |

Once done, add another endpoint:

769 |
770 |
771 |
    772 |
  • 773 |

    with a HandlerFunction echo that echoes the request body in the response, as "text/plain"

    774 |
  • 775 |
  • 776 |

    and an additional route in our existing RouterFunction that accepts POST requests on 777 | "/echo" with a "text/plain" body and returns responses with the same content type.

    778 |
  • 779 |
780 |
781 |
782 |

You can also use this new endpoint with:

783 |
784 |
785 |
786 |
$ curl http://localhost:8081/echo -i -d "WebFlux workshop" -H "Content-Type: text/plain"
 787 | HTTP/1.1 200 OK
 788 | transfer-encoding: chunked
 789 | Content-Type: text/plain
 790 | 
 791 | WebFlux workshop%
792 |
793 |
794 |
795 |
796 |

Expose the Flux<Quotes> as a web service

797 |
798 |

First, let’s inject our QuoteGenerator instance in our QuoteHandler, instantiate 799 | a Flux<Quote> from it that emits a Quote every 200 msec and can be shared between 800 | multiple subscribers (look at the Flux operators for that). This instance should be kept 801 | as an attribute for reusability.

802 |
803 |
804 |

Now create a streamQuotes handler that streams those generated quotes 805 | with the "application/stream+json" content type. Add the corresponding part in the RouterFunction, 806 | on the "/quotes" endpoint.

807 |
808 |
809 |
810 |
$ curl http://localhost:8081/quotes -i -H "Accept: application/stream+json"
 811 | HTTP/1.1 200 OK
 812 | transfer-encoding: chunked
 813 | Content-Type: application/stream+json
 814 | 
 815 | {"ticker":"CTXS","price":84.0,"instant":1494841666.633000000}
 816 | {"ticker":"DELL","price":67.1,"instant":1494841666.834000000}
 817 | {"ticker":"GOOG","price":869,"instant":1494841667.034000000}
 818 | {"ticker":"MSFT","price":66.5,"instant":1494841667.231000000}
 819 | {"ticker":"ORCL","price":46.13,"instant":1494841667.433000000}
 820 | {"ticker":"RHT","price":86.9,"instant":1494841667.634000000}
 821 | {"ticker":"VMW","price":93.7,"instant":1494841667.833000000}
822 |
823 |
824 |
825 |

Let’s now create a variant of that — instead of streaming all values (with an infinite stream), we can 826 | now take the last "n" elements of that Flux and return those as a collection of Quotes with 827 | the content type "application/json". Note that you should take the requested number of Quotes 828 | from the request itself, with the query parameter named "size" (or pick 10 as the default size 829 | if none was provided).

830 |
831 |
832 |
833 |
curl http://localhost:8081/quotes -i -H "Accept: application/json"
 834 | HTTP/1.1 200 OK
 835 | transfer-encoding: chunked
 836 | Content-Type: application/json
 837 | 
 838 | [{"ticker":"CTXS","price":85.8,"instant":1494842241.716000000},{"ticker":"DELL","price":64.69,"instant":1494842241.913000000},{"ticker":"GOOG","price":856.5,"instant":1494842242.112000000},{"ticker":"MSFT","price":68.2,"instant":1494842242.317000000},{"ticker":"ORCL","price":47.4,"instant":1494842242.513000000},{"ticker":"RHT","price":85.6,"instant":1494842242.716000000},{"ticker":"VMW","price":96.1,"instant":1494842242.914000000},{"ticker":"CTXS","price":85.5,"instant":1494842243.116000000},{"ticker":"DELL","price":64.88,"instant":1494842243.316000000},{"ticker":"GOOG","price":889,"instant":1494842243.517000000}]%
839 |
840 |
841 |
842 |
843 |

Integration tests with WebTestClient

844 |
845 |

Spring WebFlux (actually the spring-test module) includes a WebTestClient 846 | that can be used to test WebFlux server endpoints with or without a running server. 847 | Tests without a running server are comparable to MockMvc from Spring MVC where mock request 848 | and response are used instead of connecting over the network using a socket. 849 | The WebTestClient however can also perform tests against a running server.

850 |
851 |
852 |

You can check that your last endpoint is working properly with the following 853 | integration test:

854 |
855 |
856 |
stock-quotes/src/test/java/io/spring/workshop/stockquotes/StockQuotesApplicationTests.java
857 |
858 |
package io.spring.workshop.stockquotes;
 859 | 
 860 | import org.junit.Test;
 861 | import org.junit.runner.RunWith;
 862 | 
 863 | import org.springframework.beans.factory.annotation.Autowired;
 864 | import org.springframework.boot.test.context.SpringBootTest;
 865 | import org.springframework.http.MediaType;
 866 | import org.springframework.test.context.junit4.SpringRunner;
 867 | import org.springframework.test.web.reactive.server.WebTestClient;
 868 | 
 869 | import static org.assertj.core.api.Assertions.assertThat;
 870 | 
 871 | @RunWith(SpringRunner.class)
 872 | //  We create a `@SpringBootTest`, starting an actual server on a `RANDOM_PORT`
 873 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 874 | public class StockQuotesApplicationTests {
 875 | 
 876 | 	// Spring Boot will create a `WebTestClient` for you,
 877 | 	// already configure and ready to issue requests against "localhost:RANDOM_PORT"
 878 | 	@Autowired
 879 | 	private WebTestClient webTestClient;
 880 | 
 881 | 	@Test
 882 | 	public void fetchQuotes() {
 883 | 		webTestClient
 884 | 				// We then create a GET request to test an endpoint
 885 | 				.get().uri("/quotes?size=20")
 886 | 				.accept(MediaType.APPLICATION_JSON)
 887 | 				.exchange()
 888 | 				// and use the dedicated DSL to test assertions against the response
 889 | 				.expectStatus().isOk()
 890 | 				.expectHeader().contentType(MediaType.APPLICATION_JSON)
 891 | 				.expectBodyList(Quote.class)
 892 | 				.hasSize(20)
 893 | 				// Here we check that all Quotes have a positive price value
 894 | 				.consumeWith(allQuotes ->
 895 | 						assertThat(allQuotes).allSatisfy(quote -> assertThat(quote.getPrice()).isPositive()));
 896 | 	}
 897 | 
 898 | }
899 |
900 |
901 |
902 |
903 |
904 |
905 |

Trading Service application

906 |
907 |
908 |

Create this application

909 |
910 |

Go to https://start.spring.io and create a Maven project with Spring Boot 2.0.0.M1, 911 | with groupId io.spring.workshop and artifactId trading-service. Select the Reactive Web 912 | , Devtools, Thymeleaf and Reactive Mongo Boot starters. 913 | Unzip the given file into a directory and import that application into your IDE.

914 |
915 |
916 |
917 |

Use Tomcat as a web engine

918 |
919 |

By default, spring-boot-starter-webflux transitively brings spring-boot-starter-reactor-netty 920 | and Spring Boot auto-configures Reactor Netty as a web server. For this application, we’ll use 921 | Tomcat as an alternative.

922 |
923 |
924 |
trading-service/pom.xml
925 |
926 |
		<dependency>
 927 | 			<groupId>org.springframework.boot</groupId>
 928 | 			<artifactId>spring-boot-starter-tomcat</artifactId>
 929 | 		</dependency>
930 |
931 |
932 |
933 |

Note that Spring Boot supports as well Undertow and Jetty.

934 |
935 |
936 |
937 |

Use a reactive datastore

938 |
939 |

In this application, we’ll use a MongoDB datastore with its reactive driver; 940 | for this workshop, we’ll use an in-memory instance of MongoDB. So add the following:

941 |
942 |
943 |
trading-service/pom.xml
944 |
945 |
		<dependency>
 946 | 			<groupId>de.flapdoodle.embed</groupId>
 947 | 			<artifactId>de.flapdoodle.embed.mongo</artifactId>
 948 | 		</dependency>
949 |
950 |
951 |
952 |

We’d like to manage TradingUser with our datastore.

953 |
954 |
955 |
trading-service/src/main/java/io/spring/workshop/tradingservice/TradingUser.java
956 |
957 |
package io.spring.workshop.tradingservice;
 958 | 
 959 | import org.springframework.data.annotation.Id;
 960 | import org.springframework.data.mongodb.core.mapping.Document;
 961 | 
 962 | @Document
 963 | public class TradingUser {
 964 | 
 965 | 	@Id
 966 | 	private String id;
 967 | 
 968 | 	private String userName;
 969 | 
 970 | 	private String fullName;
 971 | 
 972 | 	public TradingUser() {
 973 | 	}
 974 | 
 975 | 	public TradingUser(String id, String userName, String fullName) {
 976 | 		this.id = id;
 977 | 		this.userName = userName;
 978 | 		this.fullName = fullName;
 979 | 	}
 980 | 
 981 | 	public TradingUser(String userName, String fullName) {
 982 | 		this.userName = userName;
 983 | 		this.fullName = fullName;
 984 | 	}
 985 | 
 986 | 	public String getId() {
 987 | 		return id;
 988 | 	}
 989 | 
 990 | 	public void setId(String id) {
 991 | 		this.id = id;
 992 | 	}
 993 | 
 994 | 	public String getUserName() {
 995 | 		return userName;
 996 | 	}
 997 | 
 998 | 	public void setUserName(String userName) {
 999 | 		this.userName = userName;
1000 | 	}
1001 | 
1002 | 	public String getFullName() {
1003 | 		return fullName;
1004 | 	}
1005 | 
1006 | 	public void setFullName(String fullName) {
1007 | 		this.fullName = fullName;
1008 | 	}
1009 | 
1010 | 	@Override
1011 | 	public boolean equals(Object o) {
1012 | 		if (this == o) return true;
1013 | 		if (o == null || getClass() != o.getClass()) return false;
1014 | 
1015 | 		TradingUser that = (TradingUser) o;
1016 | 
1017 | 		if (!id.equals(that.id)) return false;
1018 | 		return userName.equals(that.userName);
1019 | 	}
1020 | 
1021 | 	@Override
1022 | 	public int hashCode() {
1023 | 		int result = id.hashCode();
1024 | 		result = 31 * result + userName.hashCode();
1025 | 		return result;
1026 | 	}
1027 | }
1028 |
1029 |
1030 |
1031 |

Now create a TradingUserRepository interface that extends ReactiveMongoRepository. 1032 | Add a findByUserName(String userName) method that returns a single TradingUser in a reactive fashion.

1033 |
1034 |
1035 |

We’d like to insert users in our datastore when the application starts up. For that, create a UsersCommandLineRunner 1036 | component that implements Spring Boot’s CommandLineRunner. In the run method, use the reactive repository 1037 | to insert TradingUser instances in the datastore.

1038 |
1039 |
1040 | 1041 | 1042 | 1045 | 1050 | 1051 |
1043 | 1044 | 1046 | Since the run method returns void, it expects a blocking implementation. This is why you should use the 1047 | blockLast(Duration) operator on the Flux returned by the repository when inserting data. 1048 | You can also then().block(Duration) to turn that Flux into a Mono<Void> that waits for completion. 1049 |
1052 |
1053 |
1054 |
1055 |

Create a JSON web service

1056 |
1057 |

We’re now going to expose TradingUser through a Controller. 1058 | First, create a UserController annotated with @RestController. 1059 | Then add two new Controller methods in order to handle:

1060 |
1061 |
1062 |
    1063 |
  • 1064 |

    GET requests to "/users", returning all TradingUser instances, serializing them with content-type "application/json"

    1065 |
  • 1066 |
  • 1067 |

    GET requests to "/users/{username}", returning a single TradingUser instance, serializing it with content-type "application/json"

    1068 |
  • 1069 |
1070 |
1071 |
1072 |

You can now validate your implementation with the following test:

1073 |
1074 |
1075 |
trading-service/src/test/java/io/spring/workshop/tradingservice/UserControllerTests.java
1076 |
1077 |
package io.spring.workshop.tradingservice;
1078 | 
1079 | import org.junit.Test;
1080 | import org.junit.runner.RunWith;
1081 | import org.mockito.BDDMockito;
1082 | import reactor.core.publisher.Flux;
1083 | import reactor.core.publisher.Mono;
1084 | 
1085 | import org.springframework.beans.factory.annotation.Autowired;
1086 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
1087 | import org.springframework.boot.test.mock.mockito.MockBean;
1088 | import org.springframework.http.MediaType;
1089 | import org.springframework.test.context.junit4.SpringRunner;
1090 | import org.springframework.test.web.reactive.server.WebTestClient;
1091 | 
1092 | @RunWith(SpringRunner.class)
1093 | @WebFluxTest(UserController.class)
1094 | public class UserControllerTests {
1095 | 
1096 |   @Autowired
1097 |   private WebTestClient webTestClient;
1098 | 
1099 |   @MockBean
1100 |   private TradingUserRepository repository;
1101 | 
1102 |   @Test
1103 |   public void listUsers() {
1104 |     TradingUser juergen = new TradingUser("1", "jhoeller", "Juergen Hoeller");
1105 |     TradingUser andy = new TradingUser("2", "wilkinsona", "Andy Wilkinson");
1106 | 
1107 |     BDDMockito.given(this.repository.findAll())
1108 |         .willReturn(Flux.just(juergen, andy));
1109 | 
1110 |     this.webTestClient.get().uri("/users").accept(MediaType.APPLICATION_JSON)
1111 |         .exchange()
1112 |         .expectBodyList(TradingUser.class)
1113 |         .hasSize(2)
1114 |         .contains(juergen, andy);
1115 | 
1116 |   }
1117 | 
1118 |   @Test
1119 |   public void showUser() {
1120 |     TradingUser juergen = new TradingUser("1", "jhoeller", "Juergen Hoeller");
1121 | 
1122 |     BDDMockito.given(this.repository.findByUserName("jhoeller"))
1123 |         .willReturn(Mono.just(juergen));
1124 | 
1125 |     this.webTestClient.get().uri("/users/jhoeller").accept(MediaType.APPLICATION_JSON)
1126 |         .exchange()
1127 |         .expectBody(TradingUser.class)
1128 |         .isEqualTo(juergen);
1129 |   }
1130 | 
1131 | }
1132 |
1133 |
1134 |
1135 |
1136 |

Use Thymeleaf to render HTML views

1137 |
1138 |

We already added the Thymeleaf Boot starter when we created our trading application.

1139 |
1140 |
1141 |

First, let’s add a couple of WebJar dependencies to get static resources for our application:

1142 |
1143 |
1144 |
trading-service/pom.xml
1145 |
1146 |
		<dependency>
1147 | 			<groupId>org.webjars</groupId>
1148 | 			<artifactId>bootstrap</artifactId>
1149 | 			<version>3.3.7</version>
1150 | 		</dependency>
1151 | 		<dependency>
1152 | 			<groupId>org.webjars</groupId>
1153 | 			<artifactId>highcharts</artifactId>
1154 | 			<version>5.0.8</version>
1155 | 		</dependency>
1156 |
1157 |
1158 |
1159 |

We can now create HTML templates in our src/main/resources/templates folder and map them using controllers.

1160 |
1161 |
1162 |
trading-service/src/main/resources/templates/index.html
1163 |
1164 |
<!DOCTYPE html>
1165 | <html lang="en" xmlns:th="http://www.thymeleaf.org">
1166 | <head>
1167 |     <meta charset="utf-8"/>
1168 |     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
1169 |     <meta name="viewport" content="width=device-width, initial-scale=1"/>
1170 |     <meta name="description" content="Spring WebFlux Workshop"/>
1171 |     <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
1172 |     <title>Spring Trading application</title>
1173 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
1174 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
1175 | </head>
1176 | <body>
1177 | <nav class="navbar navbar-default">
1178 |     <div class="container-fluid">
1179 |         <div class="navbar-header">
1180 |             <a class="navbar-brand" href="/">Spring Trading application</a>
1181 |         </div>
1182 |         <div id="navbar" class="navbar-collapse collapse">
1183 |             <ul class="nav navbar-nav">
1184 |                 <li class="active"><a href="/">Home</a></li>
1185 |                 <li><a href="/quotes">Quotes</a></li>
1186 |                 <li><a href="/websocket">Websocket</a></li>
1187 |             </ul>
1188 |         </div>
1189 |     </div>
1190 | </nav>
1191 | <div class="container wrapper">
1192 |     <h2>Trading users</h2>
1193 |     <table class="table table-striped">
1194 |         <thead>
1195 |         <tr>
1196 |             <th>#</th>
1197 |             <th>User name</th>
1198 |             <th>Full name</th>
1199 |         </tr>
1200 |         </thead>
1201 |         <tbody>
1202 |         <tr th:each="user: ${users}">
1203 |             <th scope="row" th:text="${user.id}">42</th>
1204 |             <td th:text="${user.userName}">janedoe</td>
1205 |             <td th:text="${user.fullName}">Jane Doe</td>
1206 |         </tr>
1207 |         </tbody>
1208 |     </table>
1209 | </div>
1210 | <script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
1211 | <script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
1212 | </body>
1213 | </html>
1214 |
1215 |
1216 |
1217 |

As you can see in that template, we loop over the "users" attribute and write a row in our HTML table for each.

1218 |
1219 |
1220 |

Let’s display those users in our application:

1221 |
1222 |
1223 |
    1224 |
  • 1225 |

    Create a HomeController Controller

    1226 |
  • 1227 |
  • 1228 |

    Add a Controller method that handles GET requests to "/"

    1229 |
  • 1230 |
  • 1231 |

    Inject the Spring Model on that method and add a "users" attribute to it

    1232 |
  • 1233 |
1234 |
1235 |
1236 | 1237 | 1238 | 1241 | 1245 | 1246 |
1239 | 1240 | 1242 | Spring WebFlux will resolve automatically Publisher instances before rendering the view, 1243 | there’s no need to involve blocking code at all! 1244 |
1247 |
1248 |
1249 |
1250 |

Use the WebClient to stream JSON to the browser

1251 |
1252 |

In this section, we’ll call our remote stock-quotes service to get Quotes from it, so we first need to:

1253 |
1254 |
1255 |
    1256 |
  • 1257 |

    copy over the Quote class to this application

    1258 |
  • 1259 |
  • 1260 |

    add the Jackson JSR310 module dependency

    1261 |
  • 1262 |
1263 |
1264 |
1265 |

Add the following template file to your application:

1266 |
1267 |
1268 |
trading-service/src/main/resources/templates/quotes.html
1269 |
1270 |
<!DOCTYPE html>
1271 | <html lang="en" xmlns:th="http://www.thymeleaf.org">
1272 | <head>
1273 |     <meta charset="utf-8"/>
1274 |     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
1275 |     <meta name="viewport" content="width=device-width, initial-scale=1"/>
1276 |     <meta name="description" content="Spring WebFlux Workshop"/>
1277 |     <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
1278 |     <title>Spring Trading application</title>
1279 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
1280 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
1281 |     <link rel="stylesheet" href="/webjars/highcharts/5.0.8/css/highcharts.css"/>
1282 | </head>
1283 | <body>
1284 | <nav class="navbar navbar-default">
1285 |     <div class="container-fluid">
1286 |         <div class="navbar-header">
1287 |             <a class="navbar-brand" href="/">Spring Trading application</a>
1288 |         </div>
1289 |         <div id="navbar" class="navbar-collapse collapse">
1290 |             <ul class="nav navbar-nav">
1291 |                 <li><a href="/">Home</a></li>
1292 |                 <li class="active"><a href="/quotes">Quotes</a></li>
1293 |                 <li><a href="/websocket">Websocket</a></li>
1294 |             </ul>
1295 |         </div>
1296 |     </div>
1297 | </nav>
1298 | <div class="container wrapper">
1299 |     <div id="chart" style="height: 400px; min-width: 310px"></div>
1300 | </div>
1301 | <script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
1302 | <script type="text/javascript" src="/webjars/highcharts/5.0.8/highcharts.js"></script>
1303 | <script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
1304 | <script type="text/javascript">
1305 | 
1306 |     // Setting up the chart
1307 |     var chart = new Highcharts.chart('chart', {
1308 |         title: {
1309 |             text: 'My Stock Portfolio'
1310 |         },
1311 |         yAxis: {
1312 |             title: {
1313 |                 text: 'Stock Price'
1314 |             }
1315 |         },
1316 |         legend: {
1317 |             layout: 'vertical',
1318 |             align: 'right',
1319 |             verticalAlign: 'middle'
1320 |         },
1321 |         xAxis: {
1322 |             type: 'datetime',
1323 |         },
1324 |         series: [{
1325 |             name: 'CTXS',
1326 |             data: []
1327 |         }, {
1328 |             name: 'MSFT',
1329 |             data: []
1330 |         }, {
1331 |             name: 'ORCL',
1332 |             data: []
1333 |         }, {
1334 |             name: 'RHT',
1335 |             data: []
1336 |         }, {
1337 |             name: 'VMW',
1338 |             data: []
1339 |         }, {
1340 |             name: 'DELL',
1341 |             data: []
1342 |         }]
1343 |     });
1344 | 
1345 |     // This function adds the given data point to the chart
1346 |     var appendStockData = function (quote) {
1347 |         chart.series
1348 |             .filter(function (serie) {
1349 |                 return serie.name == quote.ticker
1350 |             })
1351 |             .forEach(function (serie) {
1352 |                 var shift = serie.data.length > 40;
1353 |                 serie.addPoint([quote.instant * 1000, quote.price], true, shift);
1354 |             });
1355 |     };
1356 | 
1357 |     // The browser connects to the server and receives quotes using ServerSentEvents
1358 |     // those quotes are appended to the chart as they're received
1359 |     var stockEventSource = new EventSource("/quotes/feed");
1360 |     stockEventSource.onmessage = function (e) {
1361 |         appendStockData(JSON.parse(e.data));
1362 |     };
1363 | </script>
1364 | </body>
1365 | </html>
1366 |
1367 |
1368 |
1369 |

As you can see in this template file, loading that HTML page will cause the browser to send a request 1370 | the server for Quotes using the Server Sent Event transport.

1371 |
1372 |
1373 |

Now create a QuotesController annotated with @Controller and add two methods. 1374 | One that renders the quotes.html template for incoming "GET /quotes" requests. 1375 | The other should response to "GET /quotes/feed" requests with the "text/event-stream" content-type, 1376 | with a Flux<Quote> as the response body. This data is already server by the stock-quotes application 1377 | , so you can use a WebClient to request the remote web service to retrieve that Flux.

1378 |
1379 |
1380 | 1381 | 1382 | 1385 | 1389 | 1390 |
1383 | 1384 | 1386 | You should avoid making a request to the stock-quotes service for every browser connecting to 1387 | that page — for that, you can use the Flux.share() operator. 1388 |
1391 |
1392 |
1393 |
1394 |

Create and Configure a WebSocketHandler

1395 |
1396 |

WebFlux includes functional reactive WebSocket client and server support.

1397 |
1398 |
1399 |

On the server side there are two main components: WebSocketHandlerAdapter will handle the incoming 1400 | requests by delegating to the configured WebSocketService and WebSocketHandler will be responsible 1401 | to handle WebSocket session.

1402 |
1403 |
1404 |

Take a look at the code samples in 1405 | Reactive WebSocket Support documentation

1406 |
1407 |
1408 |

First, create an EchoWebSocketHandler class; it has to implement WebSocketHandler. 1409 | Now implement handle(WebSocketSession session) method. The handler echoes the incoming messages with a delay of 1s.

1410 |
1411 |
1412 |

To route requests to that handler, you need to map the above WebSocket handler to a specific URL: here, "/websocket/echo". 1413 | Create a WebSocketRouter configuration class (i.e. annotated with @Configuration) that creates a bean of type HandlerMapping. 1414 | Create one additional bean of type WebSocketHandlerAdapter which will delegate the processing of 1415 | the incoming request to the default WebSocketService which is HandshakeWebSocketService.

1416 |
1417 |
1418 |

Now create a WebSocketController annotated with @Controller and add a method that renders the 1419 | websocket.html template for incoming "GET /websocket" requests.

1420 |
1421 |
1422 |

Add the following template file to your application:

1423 |
1424 |
1425 |
trading-service/src/main/resources/templates/websocket.html
1426 |
1427 |
<!DOCTYPE html>
1428 | <html lang="en" xmlns:th="http://www.thymeleaf.org">
1429 | <head>
1430 |     <meta charset="utf-8"/>
1431 |     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
1432 |     <meta name="viewport" content="width=device-width, initial-scale=1"/>
1433 |     <meta name="description" content="Spring WebFlux Workshop"/>
1434 |     <meta name="author" content="Violeta Georgieva and Brian Clozel"/>
1435 |     <title>Spring Trading application</title>
1436 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap-theme.min.css"/>
1437 |     <link rel="stylesheet" href="/webjars/bootstrap/3.3.7/css/bootstrap.min.css"/>
1438 | </head>
1439 | <body>
1440 | <nav class="navbar navbar-default">
1441 |     <div class="container-fluid">
1442 |         <div class="navbar-header">
1443 |             <a class="navbar-brand" href="/">Spring Trading application</a>
1444 |         </div>
1445 |         <div id="navbar" class="navbar-collapse collapse">
1446 |             <ul class="nav navbar-nav">
1447 |                 <li><a href="/">Home</a></li>
1448 |                 <li><a href="/quotes">Quotes</a></li>
1449 |                 <li class="active"><a href="/websocket">Websocket</a></li>
1450 |             </ul>
1451 |         </div>
1452 |     </div>
1453 | </nav>
1454 | <div class="container wrapper">
1455 |     <h2>Websocket Echo</h2>
1456 |     <form class="form-inline">
1457 |         <div class="form-group">
1458 |             <input class="form-control" type="text" id="input" value="type something">
1459 |             <input class="btn btn-default" type="submit" id="button" value="Send"/>
1460 |         </div>
1461 |     </form>
1462 |     <div id="output"></div>
1463 | </div>
1464 | <script type="text/javascript" src="/webjars/jquery/1.11.1/jquery.min.js"></script>
1465 | <script type="text/javascript" src="/webjars/bootstrap/3.3.7/js/bootstrap.min.js"></script>
1466 | <script type="text/javascript">
1467 |     $(document).ready(function () {
1468 |         if (!("WebSocket" in window)) WebSocket = MozWebSocket;
1469 |         var socket = new WebSocket("ws://localhost:8080/websocket/echo");
1470 | 
1471 |         socket.onopen = function (event) {
1472 |             var newMessage = document.createElement('p');
1473 |             newMessage.textContent = "-- CONNECTED";
1474 |             document.getElementById('output').appendChild(newMessage);
1475 | 
1476 |             socket.onmessage = function (e) {
1477 |                 var newMessage = document.createElement('p');
1478 |                 newMessage.textContent = "<< SERVER: " + e.data;
1479 |                 document.getElementById('output').appendChild(newMessage);
1480 |             }
1481 | 
1482 |             $("#button").click(function (e) {
1483 |                 e.preventDefault();
1484 |                 var message = $("#input").val();
1485 |                 socket.send(message);
1486 |                 var newMessage = document.createElement('p');
1487 |                 newMessage.textContent = ">> CLIENT: " + message;
1488 |                 document.getElementById('output').appendChild(newMessage);
1489 |             });
1490 |         }
1491 |     });
1492 | </script>
1493 | </body>
1494 | </html>
1495 |
1496 |
1497 |
1498 |
1499 |

Integration tests with WebSocketClient

1500 |
1501 |

WebSocketClient included in Spring WebFlux can be used to test your WebSocket endpoints.

1502 |
1503 |
1504 |

You can check that your WebSocket endpoint, created in the previous section, is working properly with the 1505 | following integration test:

1506 |
1507 |
1508 |
trading-service/src/test/java/io/spring/workshop/tradingservice/websocket/EchoWebSocketHandlerTests.java
1509 |
1510 |
package io.spring.workshop.tradingservice.websocket;
1511 | 
1512 | import static org.junit.Assert.assertEquals;
1513 | 
1514 | import java.net.URI;
1515 | import java.net.URISyntaxException;
1516 | import java.time.Duration;
1517 | 
1518 | import org.junit.Test;
1519 | import org.junit.runner.RunWith;
1520 | import org.springframework.boot.test.context.SpringBootTest;
1521 | import org.springframework.boot.web.server.LocalServerPort;
1522 | import org.springframework.test.context.junit4.SpringRunner;
1523 | import org.springframework.web.reactive.socket.WebSocketMessage;
1524 | import org.springframework.web.reactive.socket.client.StandardWebSocketClient;
1525 | import org.springframework.web.reactive.socket.client.WebSocketClient;
1526 | 
1527 | import reactor.core.publisher.Flux;
1528 | import reactor.core.publisher.ReplayProcessor;
1529 | 
1530 | @RunWith(SpringRunner.class)
1531 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
1532 | public class EchoWebSocketHandlerTests {
1533 | 
1534 | 	@LocalServerPort
1535 | 	private String port;
1536 | 
1537 | 	@Test
1538 | 	public void echo() throws Exception {
1539 | 		int count = 4;
1540 | 		Flux<String> input = Flux.range(1, count).map(index -> "msg-" + index);
1541 | 		ReplayProcessor<Object> output = ReplayProcessor.create(count);
1542 | 
1543 | 		WebSocketClient client = new StandardWebSocketClient();
1544 | 		client.execute(getUrl("/websocket/echo"),
1545 | 				session -> session
1546 | 						.send(input.map(session::textMessage))
1547 | 						.thenMany(session.receive().take(count).map(WebSocketMessage::getPayloadAsText))
1548 | 						.subscribeWith(output)
1549 | 						.then())
1550 | 				.block(Duration.ofMillis(5000));
1551 | 
1552 | 		assertEquals(input.collectList().block(Duration.ofMillis(5000)), output.collectList().block(Duration.ofMillis(5000)));
1553 | 	}
1554 | 
1555 | 	protected URI getUrl(String path) throws URISyntaxException {
1556 | 		return new URI("ws://localhost:" + this.port + path);
1557 | 	}
1558 | }
1559 |
1560 |
1561 |
1562 |
1563 |
1564 |
1565 |

Additional Resources

1566 |
1567 |
1568 |

Talks on Spring Reactive:

1569 |
1570 | 1580 |
1581 |
1582 |
1583 | 1588 | 1589 | 1590 | 1591 | 1592 | -------------------------------------------------------------------------------- /stock-details/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /stock-details/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/stock-details/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /stock-details/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /stock-details/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /stock-details/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /stock-details/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.spring.workshop 7 | stock-details 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | stock-details 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.BUILD-SNAPSHOT 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Bismuth-BUILD-SNAPSHOT 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-webflux 32 | 33 | 34 | 35 | com.fasterxml.jackson.datatype 36 | jackson-datatype-jsr310 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-data-mongodb-reactive 42 | 43 | 44 | 45 | de.flapdoodle.embed 46 | de.flapdoodle.embed.mongo 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-test 52 | test 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | 65 | 66 | 67 | spring-snapshots 68 | Spring Snapshots 69 | https://repo.spring.io/snapshot 70 | 71 | true 72 | 73 | 74 | 75 | spring-milestones 76 | Spring Milestones 77 | https://repo.spring.io/milestone 78 | 79 | false 80 | 81 | 82 | 83 | 84 | 85 | 86 | spring-snapshots 87 | Spring Snapshots 88 | https://repo.spring.io/snapshot 89 | 90 | true 91 | 92 | 93 | 94 | spring-milestones 95 | Spring Milestones 96 | https://repo.spring.io/milestone 97 | 98 | false 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /stock-details/src/main/java/io/spring/workshop/stockdetails/StockDetailsApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | 14 | @SpringBootApplication 15 | public class StockDetailsApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(StockDetailsApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompany.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import org.springframework.data.annotation.Id; 12 | import org.springframework.data.mongodb.core.mapping.Document; 13 | 14 | @Document 15 | public class TradingCompany { 16 | 17 | @Id 18 | private String id; 19 | 20 | private String description; 21 | 22 | private String ticker; 23 | 24 | public TradingCompany() { 25 | } 26 | 27 | public TradingCompany(String id, String description, String ticker) { 28 | this.id = id; 29 | this.description = description; 30 | this.ticker = ticker; 31 | } 32 | 33 | public TradingCompany(String description, String ticker) { 34 | this.description = description; 35 | this.ticker = ticker; 36 | } 37 | 38 | public String getId() { 39 | return id; 40 | } 41 | 42 | public void setId(String id) { 43 | this.id = id; 44 | } 45 | 46 | public String getDescription() { 47 | return description; 48 | } 49 | 50 | public void setDescription(String description) { 51 | this.description = description; 52 | } 53 | 54 | public String getTicker() { 55 | return ticker; 56 | } 57 | 58 | public void setTicker(String ticker) { 59 | this.ticker = ticker; 60 | } 61 | 62 | @Override 63 | public boolean equals(Object o) { 64 | if (this == o) return true; 65 | if (o == null || getClass() != o.getClass()) return false; 66 | 67 | TradingCompany that = (TradingCompany) o; 68 | 69 | if (!id.equals(that.id)) return false; 70 | return description.equals(that.description); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | int result = id.hashCode(); 76 | result = 31 * result + description.hashCode(); 77 | return result; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompanyCommandLineRunner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import java.time.Duration; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | import org.springframework.boot.CommandLineRunner; 16 | import org.springframework.stereotype.Component; 17 | 18 | @Component 19 | public class TradingCompanyCommandLineRunner implements CommandLineRunner { 20 | 21 | private final TradingCompanyRepository repository; 22 | 23 | public TradingCompanyCommandLineRunner(TradingCompanyRepository repository) { 24 | this.repository = repository; 25 | } 26 | 27 | @Override 28 | public void run(String... strings) { 29 | List companies = Arrays.asList( 30 | new TradingCompany("Citrix Systems", "CTXS"), 31 | new TradingCompany("Dell Technologies", "DELL"), 32 | new TradingCompany("Google", "GOOG"), 33 | new TradingCompany("Microsoft", "MSFT"), 34 | new TradingCompany("Oracle", "ORCL"), 35 | new TradingCompany("Red Hat", "RHT"), 36 | new TradingCompany("Vmware", "VMW") 37 | ); 38 | this.repository.insert(companies).blockLast(Duration.ofSeconds(30)); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompanyController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import java.time.Duration; 12 | 13 | import reactor.core.publisher.Flux; 14 | import reactor.core.publisher.Mono; 15 | 16 | import org.springframework.http.MediaType; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | @RestController 22 | public class TradingCompanyController { 23 | 24 | private final TradingCompanyRepository tradingCompanyRepository; 25 | 26 | public TradingCompanyController(TradingCompanyRepository tradingCompanyRepository) { 27 | this.tradingCompanyRepository = tradingCompanyRepository; 28 | } 29 | 30 | @GetMapping(path = "/details", produces = MediaType.APPLICATION_JSON_VALUE) 31 | public Flux listTradingCompanies() { 32 | return this.tradingCompanyRepository.findAll(); 33 | } 34 | 35 | @GetMapping(path = "/details/{ticker}", produces = MediaType.APPLICATION_JSON_VALUE) 36 | public Mono showTradingCompanies(@PathVariable String ticker) { 37 | return this.tradingCompanyRepository.findByTicker(ticker).delayElement(Duration.ofMillis(400)); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /stock-details/src/main/java/io/spring/workshop/stockdetails/TradingCompanyRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import reactor.core.publisher.Mono; 12 | 13 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository; 14 | 15 | public interface TradingCompanyRepository extends ReactiveMongoRepository { 16 | 17 | Mono findByTicker(String ticker); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /stock-details/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 -------------------------------------------------------------------------------- /stock-details/src/test/java/io/spring/workshop/stockdetails/StockDetailsApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import org.junit.runner.RunWith; 12 | 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | @RunWith(SpringRunner.class) 17 | // We create a `@SpringBootTest`, starting an actual server on a `RANDOM_PORT` 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 19 | public class StockDetailsApplicationTests { 20 | 21 | 22 | 23 | } -------------------------------------------------------------------------------- /stock-details/src/test/java/io/spring/workshop/stockdetails/TradingCompanyControllerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.stockdetails; 10 | 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.BDDMockito; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; 19 | import org.springframework.boot.test.mock.mockito.MockBean; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.reactive.server.WebTestClient; 23 | 24 | @RunWith(SpringRunner.class) 25 | @WebFluxTest(TradingCompanyController.class) 26 | public class TradingCompanyControllerTests { 27 | 28 | @Autowired 29 | private WebTestClient webTestClient; 30 | 31 | @MockBean 32 | private TradingCompanyRepository repository; 33 | 34 | @Test 35 | public void listTradingCompanies() { 36 | TradingCompany soo = new TradingCompany("1", "The Sooshi Company", "SOO"); 37 | TradingCompany pizza = new TradingCompany("2", "Pizza & friends", "PIZZA"); 38 | 39 | BDDMockito.given(this.repository.findAll()) 40 | .willReturn(Flux.just(soo, pizza)); 41 | 42 | this.webTestClient.get().uri("/details").accept(MediaType.APPLICATION_JSON) 43 | .exchange() 44 | .expectBodyList(TradingCompany.class) 45 | .hasSize(2) 46 | .contains(soo, pizza); 47 | 48 | } 49 | 50 | @Test 51 | public void getTradingCompany() { 52 | TradingCompany soo = new TradingCompany("1", "The Sooshi Company", "SOO"); 53 | 54 | BDDMockito.given(this.repository.findByTicker("SOO")) 55 | .willReturn(Mono.just(soo)); 56 | 57 | this.webTestClient.get().uri("/details/SOO").accept(MediaType.APPLICATION_JSON) 58 | .exchange() 59 | .expectBody(TradingCompany.class) 60 | .isEqualTo(soo); 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /stock-quotes/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /stock-quotes/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/stock-quotes/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /stock-quotes/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /stock-quotes/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /stock-quotes/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /stock-quotes/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.spring.workshop 7 | stock-quotes 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | stock-quotes 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.BUILD-SNAPSHOT 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Bismuth-BUILD-SNAPSHOT 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-webflux 32 | 33 | 34 | 35 | com.fasterxml.jackson.datatype 36 | jackson-datatype-jsr310 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-maven-plugin 51 | 52 | 53 | 54 | 55 | 56 | 57 | spring-snapshots 58 | Spring Snapshots 59 | https://repo.spring.io/snapshot 60 | 61 | true 62 | 63 | 64 | 65 | spring-milestones 66 | Spring Milestones 67 | https://repo.spring.io/milestone 68 | 69 | false 70 | 71 | 72 | 73 | 74 | 75 | 76 | spring-snapshots 77 | Spring Snapshots 78 | https://repo.spring.io/snapshot 79 | 80 | true 81 | 82 | 83 | 84 | spring-milestones 85 | Spring Milestones 86 | https://repo.spring.io/milestone 87 | 88 | false 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /stock-quotes/src/main/java/io/spring/workshop/stockquotes/Quote.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.time.Instant; 6 | 7 | public class Quote { 8 | 9 | private static final MathContext MATH_CONTEXT = new MathContext(2); 10 | 11 | private String ticker; 12 | 13 | private BigDecimal price; 14 | 15 | private Instant instant = Instant.now(); 16 | 17 | public Quote() { 18 | } 19 | 20 | public Quote(String ticker, BigDecimal price) { 21 | this.ticker = ticker; 22 | this.price = price; 23 | } 24 | 25 | public Quote(String ticker, Double price) { 26 | this(ticker, new BigDecimal(price, MATH_CONTEXT)); 27 | } 28 | 29 | public String getTicker() { 30 | return ticker; 31 | } 32 | 33 | public void setTicker(String ticker) { 34 | this.ticker = ticker; 35 | } 36 | 37 | public BigDecimal getPrice() { 38 | return price; 39 | } 40 | 41 | public void setPrice(BigDecimal price) { 42 | this.price = price; 43 | } 44 | 45 | public Instant getInstant() { 46 | return instant; 47 | } 48 | 49 | public void setInstant(Instant instant) { 50 | this.instant = instant; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "Quote{" + 56 | "ticker='" + ticker + '\'' + 57 | ", price=" + price + 58 | ", instant=" + instant + 59 | '}'; 60 | } 61 | } -------------------------------------------------------------------------------- /stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteGenerator.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Random; 10 | import java.util.stream.Collectors; 11 | 12 | import reactor.core.publisher.Flux; 13 | 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | public class QuoteGenerator { 18 | 19 | private final MathContext mathContext = new MathContext(2); 20 | 21 | private final Random random = new Random(); 22 | 23 | private final List prices = new ArrayList<>(); 24 | 25 | private final Flux quoteStream; 26 | 27 | /** 28 | * Bootstraps the generator with tickers and initial prices 29 | */ 30 | public QuoteGenerator() { 31 | initializeQuotes(); 32 | this.quoteStream = getQuoteStream(); 33 | } 34 | 35 | public Flux fetchQuoteStream() { 36 | return quoteStream; 37 | } 38 | 39 | private void initializeQuotes() { 40 | this.prices.add(new Quote("CTXS", 82.26)); 41 | this.prices.add(new Quote("DELL", 63.74)); 42 | this.prices.add(new Quote("GOOG", 847.24)); 43 | this.prices.add(new Quote("MSFT", 65.11)); 44 | this.prices.add(new Quote("ORCL", 45.71)); 45 | this.prices.add(new Quote("RHT", 84.29)); 46 | this.prices.add(new Quote("VMW", 92.21)); 47 | } 48 | 49 | 50 | private Flux getQuoteStream() { 51 | return Flux.interval(Duration.ofMillis(200)) 52 | .onBackpressureDrop() 53 | .map(this::generateQuotes) 54 | .flatMapIterable(quotes -> quotes) 55 | .share(); 56 | } 57 | 58 | private List generateQuotes(long i) { 59 | Instant instant = Instant.now(); 60 | return prices.stream() 61 | .map(baseQuote -> { 62 | BigDecimal priceChange = baseQuote.getPrice() 63 | .multiply(new BigDecimal(0.05 * this.random.nextDouble()), this.mathContext); 64 | 65 | Quote result = new Quote(baseQuote.getTicker(), baseQuote.getPrice().add(priceChange)); 66 | result.setInstant(instant); 67 | return result; 68 | }) 69 | .collect(Collectors.toList()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteHandler.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.reactive.function.server.ServerRequest; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | 9 | import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; 10 | import static org.springframework.web.reactive.function.server.ServerResponse.ok; 11 | 12 | @Component 13 | public class QuoteHandler { 14 | 15 | private final QuoteGenerator quoteGenerator; 16 | 17 | public QuoteHandler(QuoteGenerator quoteGenerator) { 18 | this.quoteGenerator = quoteGenerator; 19 | } 20 | 21 | public Mono streamQuotes(ServerRequest request) { 22 | return ok() 23 | .contentType(APPLICATION_STREAM_JSON) 24 | .body(this.quoteGenerator.fetchQuoteStream(), Quote.class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stock-quotes/src/main/java/io/spring/workshop/stockquotes/QuoteRouter.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.reactive.function.server.RouterFunction; 6 | import org.springframework.web.reactive.function.server.RouterFunctions; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | 9 | import static org.springframework.http.MediaType.APPLICATION_JSON; 10 | import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; 11 | import static org.springframework.http.MediaType.TEXT_PLAIN; 12 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 13 | import static org.springframework.web.reactive.function.server.RequestPredicates.POST; 14 | import static org.springframework.web.reactive.function.server.RequestPredicates.accept; 15 | import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; 16 | 17 | @Configuration 18 | public class QuoteRouter { 19 | 20 | @Bean 21 | public RouterFunction route(QuoteHandler quoteHandler) { 22 | return RouterFunctions 23 | .route(GET("/quotes").and(accept(APPLICATION_STREAM_JSON)), quoteHandler::streamQuotes) ; 24 | } 25 | } -------------------------------------------------------------------------------- /stock-quotes/src/main/java/io/spring/workshop/stockquotes/StockQuotesApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StockQuotesApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StockQuotesApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /stock-quotes/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 -------------------------------------------------------------------------------- /stock-quotes/src/test/java/io/spring/workshop/stockquotes/StockQuotesApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.stockquotes; 2 | 3 | import java.util.List; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | import org.springframework.test.web.reactive.server.WebTestClient; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @RunWith(SpringRunner.class) 17 | // We create a `@SpringBootTest`, starting an actual server on a `RANDOM_PORT` 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 19 | public class StockQuotesApplicationTests { 20 | 21 | // Spring Boot will create a `WebTestClient` for you, 22 | // already configure and ready to issue requests against "localhost:RANDOM_PORT" 23 | @Autowired 24 | private WebTestClient webTestClient; 25 | 26 | @Test 27 | public void fetchQuotes() { 28 | List result = 29 | webTestClient 30 | // We then create a GET request to test an endpoint 31 | .get().uri("/quotes") 32 | .accept(MediaType.APPLICATION_STREAM_JSON) 33 | .exchange() 34 | // and use the dedicated DSL to test assertions against the response 35 | .expectStatus().isOk() 36 | .expectHeader().contentType(MediaType.APPLICATION_STREAM_JSON) 37 | .returnResult(Quote.class) 38 | .getResponseBody() 39 | .take(20) 40 | .collectList() 41 | .block(); 42 | 43 | assertThat(result).allSatisfy(quote -> assertThat(quote.getPrice()).isPositive()); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /trading-service/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /trading-service/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/trading-service/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /trading-service/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /trading-service/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Migwn, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | echo $MAVEN_PROJECTBASEDIR 205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 206 | 207 | # For Cygwin, switch paths to Windows format before running java 208 | if $cygwin; then 209 | [ -n "$M2_HOME" ] && 210 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 211 | [ -n "$JAVA_HOME" ] && 212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 213 | [ -n "$CLASSPATH" ] && 214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 215 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 217 | fi 218 | 219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 220 | 221 | exec "$JAVACMD" \ 222 | $MAVEN_OPTS \ 223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 226 | -------------------------------------------------------------------------------- /trading-service/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /trading-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.spring.workshop 7 | trading-service 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | trading-service 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.BUILD-SNAPSHOT 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Bismuth-BUILD-SNAPSHOT 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-webflux 32 | 33 | 34 | 35 | com.fasterxml.jackson.datatype 36 | jackson-datatype-jsr310 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-devtools 42 | runtime 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | org.springframework.cloud 51 | spring-cloud-starter-sleuth 52 | 2.0.0.BUILD-SNAPSHOT 53 | 54 | 55 | org.springframework.cloud 56 | spring-cloud-sleuth-zipkin 57 | 2.0.0.BUILD-SNAPSHOT 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | 69 | 70 | 71 | 72 | spring-snapshots 73 | Spring Snapshots 74 | https://repo.spring.io/snapshot 75 | 76 | true 77 | 78 | 79 | 80 | spring-milestones 81 | Spring Milestones 82 | https://repo.spring.io/milestone 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | spring-snapshots 92 | Spring Snapshots 93 | https://repo.spring.io/snapshot 94 | 95 | true 96 | 97 | 98 | 99 | spring-milestones 100 | Spring Milestones 101 | https://repo.spring.io/milestone 102 | 103 | false 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/Quote.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.MathContext; 5 | import java.time.Instant; 6 | 7 | public class Quote { 8 | 9 | private static final MathContext MATH_CONTEXT = new MathContext(2); 10 | 11 | private String ticker; 12 | 13 | private BigDecimal price; 14 | 15 | private Instant instant; 16 | 17 | public Quote() { 18 | } 19 | 20 | public Quote(String ticker) { 21 | this.ticker = ticker; 22 | } 23 | 24 | public Quote(String ticker, BigDecimal price) { 25 | this.ticker = ticker; 26 | this.price = price; 27 | } 28 | 29 | public Quote(String ticker, Double price) { 30 | this(ticker, new BigDecimal(price, MATH_CONTEXT)); 31 | } 32 | 33 | public String getTicker() { 34 | return ticker; 35 | } 36 | 37 | public void setTicker(String ticker) { 38 | this.ticker = ticker; 39 | } 40 | 41 | public BigDecimal getPrice() { 42 | return price; 43 | } 44 | 45 | public void setPrice(BigDecimal price) { 46 | this.price = price; 47 | } 48 | 49 | public Instant getInstant() { 50 | return instant; 51 | } 52 | 53 | public void setInstant(Instant instant) { 54 | this.instant = instant; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "Quote{" + 60 | "ticker='" + ticker + '\'' + 61 | ", price=" + price + 62 | ", instant=" + instant + 63 | '}'; 64 | } 65 | } -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/QuotesClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | import java.time.Duration; 12 | 13 | import reactor.core.publisher.Flux; 14 | import reactor.core.publisher.Mono; 15 | 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.web.reactive.function.client.WebClient; 18 | 19 | import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; 20 | 21 | @Component 22 | public class QuotesClient { 23 | 24 | private final WebClient webClient; 25 | 26 | public QuotesClient(WebClient.Builder webclientBuilder) { 27 | this.webClient = webclientBuilder.build(); 28 | } 29 | 30 | public Flux quotesFeed() { 31 | return this.webClient.get().uri("http://localhost:8081/quotes") 32 | .accept(APPLICATION_STREAM_JSON) 33 | .retrieve() 34 | .bodyToFlux(Quote.class); 35 | } 36 | 37 | public Mono getLatestQuote(String ticker) { 38 | return quotesFeed() 39 | .filter(q -> q.getTicker().equalsIgnoreCase(ticker)) 40 | .next() 41 | .timeout(Duration.ofSeconds(15), Mono.just(new Quote(ticker))); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/QuotesController.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice; 2 | 3 | import reactor.core.publisher.Flux; 4 | import reactor.core.publisher.Mono; 5 | 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.ResponseBody; 12 | import org.springframework.web.bind.annotation.ResponseStatus; 13 | 14 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 15 | import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE; 16 | 17 | @Controller 18 | public class QuotesController { 19 | 20 | private final TradingCompanyClient tradingCompanyClient; 21 | 22 | private final QuotesClient quotesClient; 23 | 24 | public QuotesController(TradingCompanyClient tradingCompanyClient, QuotesClient quotesClient) { 25 | this.tradingCompanyClient = tradingCompanyClient; 26 | this.quotesClient = quotesClient; 27 | } 28 | 29 | @GetMapping(path = "/quotes/feed", produces = TEXT_EVENT_STREAM_VALUE) 30 | @ResponseBody 31 | public Flux quotesFeed() { 32 | return this.quotesClient.quotesFeed(); 33 | } 34 | 35 | @GetMapping(path = "/quotes/summary/{ticker}", produces = APPLICATION_JSON_VALUE) 36 | @ResponseBody 37 | public Mono quotesDetails(@PathVariable String ticker) { 38 | return tradingCompanyClient.getTradingCompany(ticker) 39 | .zipWith(this.quotesClient.getLatestQuote(ticker), 40 | TradingCompanySummary::new); 41 | } 42 | 43 | @ResponseStatus(HttpStatus.NOT_FOUND) 44 | @ExceptionHandler(TickerNotFoundException.class) 45 | public void onTickerNotFound() { 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TickerNotFoundException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | public class TickerNotFoundException extends RuntimeException { 12 | 13 | public TickerNotFoundException(String message) { 14 | super(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompany.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | public class TradingCompany { 12 | 13 | private String id; 14 | 15 | private String description; 16 | 17 | private String ticker; 18 | 19 | public TradingCompany() { 20 | } 21 | 22 | public TradingCompany(String id, String description, String ticker) { 23 | this.id = id; 24 | this.description = description; 25 | this.ticker = ticker; 26 | } 27 | 28 | public TradingCompany(String description, String ticker) { 29 | this.description = description; 30 | this.ticker = ticker; 31 | } 32 | 33 | public String getId() { 34 | return id; 35 | } 36 | 37 | public void setId(String id) { 38 | this.id = id; 39 | } 40 | 41 | public String getDescription() { 42 | return description; 43 | } 44 | 45 | public void setDescription(String description) { 46 | this.description = description; 47 | } 48 | 49 | public String getTicker() { 50 | return ticker; 51 | } 52 | 53 | public void setTicker(String ticker) { 54 | this.ticker = ticker; 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if (this == o) return true; 60 | if (o == null || getClass() != o.getClass()) return false; 61 | 62 | TradingCompany that = (TradingCompany) o; 63 | 64 | if (!id.equals(that.id)) return false; 65 | return description.equals(that.description); 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | int result = id.hashCode(); 71 | result = 31 * result + description.hashCode(); 72 | return result; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompanyClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | import org.springframework.http.MediaType; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | 18 | /** 19 | * @author Madhura Bhave 20 | */ 21 | @Component 22 | public class TradingCompanyClient { 23 | 24 | private final WebClient webClient; 25 | 26 | public TradingCompanyClient(WebClient.Builder builder) { 27 | this.webClient = builder.build(); 28 | } 29 | 30 | public Flux findAllCompanies() { 31 | return this.webClient.get() 32 | .uri("http://localhost:8082/details") 33 | .accept(MediaType.APPLICATION_JSON) 34 | .retrieve() 35 | .bodyToFlux(TradingCompany.class); 36 | } 37 | 38 | public Mono getTradingCompany(String ticker) { 39 | return this.webClient.get() 40 | .uri("http://localhost:8082/details/{ticker}", ticker) 41 | .accept(MediaType.APPLICATION_JSON) 42 | .retrieve() 43 | .bodyToMono(TradingCompany.class) 44 | .switchIfEmpty(Mono.error(new TickerNotFoundException("Unknown Ticker: "+ticker))); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompanyController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | import org.springframework.http.MediaType; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | @RestController 20 | public class TradingCompanyController { 21 | 22 | private final TradingCompanyClient tradingCompanyClient; 23 | 24 | public TradingCompanyController(TradingCompanyClient tradingCompanyClient) { 25 | this.tradingCompanyClient = tradingCompanyClient; 26 | } 27 | 28 | @GetMapping(path = "/details", produces = MediaType.APPLICATION_JSON_VALUE) 29 | public Flux listTradingCompanies() { 30 | return tradingCompanyClient.findAllCompanies(); 31 | } 32 | 33 | @GetMapping(path = "/details/{ticker}", produces = MediaType.APPLICATION_JSON_VALUE) 34 | public Mono showTradingCompanies(@PathVariable String ticker) { 35 | return tradingCompanyClient.getTradingCompany(ticker); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TradingCompanySummary.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018. Lorem ipsum dolor sit amet, consectetur adipiscing elit. 3 | * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. 4 | * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. 5 | * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. 6 | * Vestibulum commodo. Ut rhoncus gravida arcu. 7 | */ 8 | 9 | package io.spring.workshop.tradingservice; 10 | 11 | public class TradingCompanySummary { 12 | 13 | private final Quote latestQuote; 14 | 15 | private final TradingCompany tradingCompany; 16 | 17 | public TradingCompanySummary(TradingCompany tradingCompany, Quote latestQuote) { 18 | this.latestQuote = latestQuote; 19 | this.tradingCompany = tradingCompany; 20 | } 21 | 22 | public Quote getLatestQuote() { 23 | return latestQuote; 24 | } 25 | 26 | public TradingCompany getTradingCompany() { 27 | return tradingCompany; 28 | } 29 | } -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/TradingServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class TradingServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(TradingServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/websocket/EchoWebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice.websocket; 2 | 3 | import java.time.Duration; 4 | 5 | import org.springframework.web.reactive.socket.WebSocketHandler; 6 | import org.springframework.web.reactive.socket.WebSocketSession; 7 | 8 | import reactor.core.publisher.Mono; 9 | 10 | public class EchoWebSocketHandler implements WebSocketHandler { 11 | 12 | @Override 13 | public Mono handle(WebSocketSession session) { 14 | return session.send(session.receive().delayElements(Duration.ofSeconds(1)).log()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/websocket/WebSocketController.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice.websocket; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class WebSocketController { 8 | 9 | @GetMapping("/websocket") 10 | public String websocket() { 11 | return "websocket"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /trading-service/src/main/java/io/spring/workshop/tradingservice/websocket/WebSocketRouter.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice.websocket; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.reactive.HandlerMapping; 9 | import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; 10 | import org.springframework.web.reactive.socket.WebSocketHandler; 11 | import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; 12 | 13 | @Configuration 14 | public class WebSocketRouter { 15 | 16 | @Bean 17 | public HandlerMapping handlerMapping() { 18 | 19 | Map map = new HashMap<>(); 20 | map.put("/websocket/echo", new EchoWebSocketHandler()); 21 | 22 | SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); 23 | mapping.setOrder(10); 24 | mapping.setUrlMap(map); 25 | return mapping; 26 | } 27 | 28 | @Bean 29 | public WebSocketHandlerAdapter handlerAdapter() { 30 | return new WebSocketHandlerAdapter(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /trading-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=TradingService 2 | spring.sleuth.sampler.probability=1.0 3 | spring.reactor.stacktrace-mode.enabled=false -------------------------------------------------------------------------------- /trading-service/src/test/java/io/spring/workshop/tradingservice/TradingServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class TradingServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /trading-service/src/test/java/io/spring/workshop/tradingservice/websocket/EchoWebSocketHandlerTests.java: -------------------------------------------------------------------------------- 1 | package io.spring.workshop.tradingservice.websocket; 2 | 3 | import java.net.URI; 4 | import java.net.URISyntaxException; 5 | import java.time.Duration; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.ReplayProcessor; 11 | 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.web.server.LocalServerPort; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | import org.springframework.web.reactive.socket.WebSocketMessage; 16 | import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; 17 | import org.springframework.web.reactive.socket.client.WebSocketClient; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | @RunWith(SpringRunner.class) 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | public class EchoWebSocketHandlerTests { 24 | 25 | @LocalServerPort 26 | private String port; 27 | 28 | @Test 29 | public void echo() throws Exception { 30 | int count = 4; 31 | Flux input = Flux.range(1, count).map(index -> "msg-" + index); 32 | ReplayProcessor output = ReplayProcessor.create(count); 33 | 34 | WebSocketClient client = new ReactorNettyWebSocketClient(); 35 | client.execute(getUrl("/websocket/echo"), 36 | session -> session 37 | .send(input.map(session::textMessage)) 38 | .thenMany(session.receive().take(count).map(WebSocketMessage::getPayloadAsText)) 39 | .subscribeWith(output) 40 | .then()) 41 | .block(Duration.ofMillis(5000)); 42 | 43 | assertEquals(input.collectList().block(Duration.ofMillis(5000)), output.collectList().block(Duration.ofMillis(5000))); 44 | } 45 | 46 | protected URI getUrl(String path) throws URISyntaxException { 47 | return new URI("ws://localhost:" + this.port + path); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "Reactive Workshop" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "prefix": "app", 20 | "styles": [ 21 | "styles.scss" 22 | ], 23 | "scripts": [ "../node_modules/frappe-charts/dist/frappe-charts.min.iife.js"], 24 | "environmentSource": "environments/environment.ts", 25 | "environments": { 26 | "dev": "environments/environment.ts", 27 | "prod": "environments/environment.prod.ts" 28 | } 29 | } 30 | ], 31 | "lint": [ 32 | { 33 | "project": "src/tsconfig.app.json", 34 | "exclude": "**/node_modules/**" 35 | }, 36 | { 37 | "project": "src/tsconfig.spec.json", 38 | "exclude": "**/node_modules/**" 39 | }, 40 | { 41 | "project": "e2e/tsconfig.e2e.json", 42 | "exclude": "**/node_modules/**" 43 | } 44 | ], 45 | "defaults": { 46 | "styleExt": "scss", 47 | "component": {} 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | *.iml 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | package-lock.json 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Reactive Workshop 2 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trading-service-ui", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --proxy-config proxy.conf.json", 8 | "build": "ng build", 9 | "test": "ng test --browsers ChromeHeadless --single-run", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e --proxy-config proxy.conf.json", 12 | "doc": "./node_modules/.bin/compodoc -p tsconfig.json -n \"Reactive Workshop UI\"", 13 | "bundle-report": "webpack-bundle-analyzer dist/stats.json" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^5.2.4", 18 | "@angular/cdk": "github:angular/cdk-builds", 19 | "@angular/common": "5.2.4", 20 | "@angular/compiler": "5.2.4", 21 | "@angular/core": "5.2.4", 22 | "@angular/forms": "5.2.4", 23 | "@angular/http": "5.2.4", 24 | "@angular/material": "github:angular/material2-builds", 25 | "@angular/platform-browser": "5.2.4", 26 | "@angular/platform-browser-dynamic": "5.2.4", 27 | "@angular/router": "5.2.4", 28 | "angular-oboe": "^0.6.0", 29 | "core-js": "2.5.3", 30 | "eventsource": "^1.0.5", 31 | "frappe-charts": "0.0.8", 32 | "jshint": "2.9.5", 33 | "moment": "2.19.3", 34 | "rxjs": "5.5.6", 35 | "web-animations-js": "2.3.1", 36 | "zone.js": "0.8.20" 37 | }, 38 | "devDependencies": { 39 | "@angular/cli": "1.6.7", 40 | "@angular/compiler-cli": "5.2.4", 41 | "@angular/language-service": "5.2.4", 42 | "@types/node": "6.0.96", 43 | "@types/oboe": "^2.0.28", 44 | "codelyzer": "4.1.0", 45 | "ts-node": "4.1.0", 46 | "tslint": "5.9.1", 47 | "typescript": "2.6.1", 48 | "@compodoc/compodoc": "1.0.4", 49 | "webpack-bundle-analyzer": "2.9.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/quotes" : { 3 | "target": "http://localhost:8080/", 4 | "secure": false, 5 | "logLevel": "debug" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 4 | import {HeaderComponent} from './core/layout/header/header.component'; 5 | import {FooterComponent} from './core/layout/footer/footer.component'; 6 | import {MatButtonModule, MatCheckboxModule, MatTableModule} from '@angular/material'; 7 | import {QuotesComponent} from './quotes/quotes.component'; 8 | import {QuotesService} from './quotes/quotes.service'; 9 | import {HttpClientModule} from '@angular/common/http'; 10 | import {AppComponent} from './app/app.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | QuotesComponent, 16 | HeaderComponent, 17 | FooterComponent 18 | ], 19 | imports: [ 20 | BrowserModule, 21 | BrowserAnimationsModule, 22 | MatButtonModule, 23 | MatCheckboxModule, 24 | MatTableModule, 25 | HttpClientModule 26 | ], 27 | providers: [ 28 | QuotesService 29 | ], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule { 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /ui/src/app/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, DoCheck} from '@angular/core'; 2 | import {OnInit} from '@angular/core'; 3 | import {QuotesService} from '../quotes/quotes.service'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: 'app.component.html', 8 | }) 9 | export class AppComponent implements DoCheck, OnInit { 10 | 11 | constructor(private quotesService: QuotesService) { 12 | 13 | } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | ngDoCheck() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/core/layout/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 | Pivotal 2018 3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/core/layout/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, DoCheck } from '@angular/core'; 2 | import { OnInit } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-footer', 6 | templateUrl: './footer.component.html' 7 | }) 8 | export class FooterComponent implements DoCheck, OnInit { 9 | 10 | constructor() { 11 | 12 | } 13 | 14 | ngOnInit() { 15 | 16 | } 17 | 18 | ngDoCheck() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/core/layout/header/header.component.html: -------------------------------------------------------------------------------- 1 |

Spring Reactive Workshop

2 | -------------------------------------------------------------------------------- /ui/src/app/core/layout/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, DoCheck } from '@angular/core'; 2 | import { OnInit } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-header', 6 | templateUrl: './header.component.html' 7 | }) 8 | export class HeaderComponent implements DoCheck, OnInit { 9 | 10 | constructor() { 11 | 12 | } 13 | 14 | ngOnInit() { 15 | 16 | } 17 | 18 | ngDoCheck() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/quotes/quotes.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{quote.instant}} ; 5 | {{quote.ticker}} : 6 | {{quote.price}} 7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /ui/src/app/quotes/quotes.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, OnDestroy} from '@angular/core'; 2 | import {Observable} from 'rxjs/Observable'; 3 | import {Quote, QuotesService} from './quotes.service'; 4 | 5 | @Component({ 6 | selector: 'app-quotes', 7 | templateUrl: 'quotes.component.html', 8 | }) 9 | export class QuotesComponent implements OnInit, OnDestroy { 10 | 11 | displayedColumns = ['ticker', 'price', 'instant']; 12 | 13 | quotes$: Observable>; 14 | 15 | constructor(private quotesService: QuotesService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.quotes$ = this.quotesService.quotes(); 20 | } 21 | 22 | ngOnDestroy() { 23 | // this.quotes$.unsubscribe(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/quotes/quotes.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, NgZone} from '@angular/core'; 2 | import {Observable} from 'rxjs/Observable'; 3 | 4 | @Injectable() 5 | export class QuotesService { 6 | 7 | quotesCache: Array = []; 8 | 9 | eventSource: any = window['EventSource']; 10 | 11 | quotesObservable: Observable>; 12 | 13 | constructor(private ngZone: NgZone) { 14 | this.quotesObservable = new Observable>(obs => { 15 | const eventSource = new this.eventSource('/quotes/feed'); 16 | eventSource.onmessage = event => { 17 | if (this.quotesCache.length > 50) { 18 | this.quotesCache = this.quotesCache.slice(0, 50); 19 | } 20 | this.quotesCache = [JSON.parse(event.data)].concat(this.quotesCache); 21 | this.ngZone.run(() => obs.next(this.quotesCache)); 22 | }; 23 | return () => eventSource.close(); 24 | }); 25 | } 26 | 27 | quotes(): Observable> { 28 | return this.quotesObservable; 29 | } 30 | 31 | } 32 | 33 | export interface Ticker { 34 | key: string; 35 | current: Quote; 36 | quotes: Array; 37 | } 38 | 39 | export interface Quote { 40 | ticker: string; 41 | price: number; 42 | instant: number; 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/images/spring-logo-dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/ui/src/assets/images/spring-logo-dataflow.png -------------------------------------------------------------------------------- /ui/src/assets/scss/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css 3 | https://github.com/necolas/normalize.css/blob/master/normalize.css 4 | */ 5 | 6 | /* Document 7 | ========================================================================== */ 8 | 9 | /** 10 | * 1. Correct the line height in all browsers. 11 | * 2. Prevent adjustments of font size after orientation changes in iOS. 12 | */ 13 | 14 | html { 15 | line-height: 1.15; /* 1 */ 16 | -webkit-text-size-adjust: 100%; /* 2 */ 17 | } 18 | 19 | /* Sections 20 | ========================================================================== */ 21 | 22 | /** 23 | * Remove the margin in all browsers. 24 | */ 25 | 26 | body { 27 | margin: 0; 28 | } 29 | 30 | /** 31 | * Correct the font size and margin on `h1` elements within `section` and 32 | * `article` contexts in Chrome, Firefox, and Safari. 33 | */ 34 | 35 | h1 { 36 | font-size: 2em; 37 | margin: 0.67em 0; 38 | } 39 | 40 | /* Grouping content 41 | ========================================================================== */ 42 | 43 | /** 44 | * 1. Add the correct box sizing in Firefox. 45 | * 2. Show the overflow in Edge and IE. 46 | */ 47 | 48 | hr { 49 | box-sizing: content-box; /* 1 */ 50 | height: 0; /* 1 */ 51 | overflow: visible; /* 2 */ 52 | } 53 | 54 | /** 55 | * 1. Correct the inheritance and scaling of font size in all browsers. 56 | * 2. Correct the odd `em` font sizing in all browsers. 57 | */ 58 | 59 | pre { 60 | font-family: monospace, monospace; /* 1 */ 61 | font-size: 1em; /* 2 */ 62 | } 63 | 64 | /* Text-level semantics 65 | ========================================================================== */ 66 | 67 | /** 68 | * Remove the gray background on active links in IE 10. 69 | */ 70 | 71 | a { 72 | background-color: transparent; 73 | } 74 | 75 | /** 76 | * 1. Remove the bottom border in Chrome 57- 77 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 78 | */ 79 | 80 | abbr[title] { 81 | border-bottom: none; /* 1 */ 82 | text-decoration: underline; /* 2 */ 83 | text-decoration: underline dotted; /* 2 */ 84 | } 85 | 86 | /** 87 | * Add the correct font weight in Chrome, Edge, and Safari. 88 | */ 89 | 90 | b, 91 | strong { 92 | font-weight: bolder; 93 | } 94 | 95 | /** 96 | * 1. Correct the inheritance and scaling of font size in all browsers. 97 | * 2. Correct the odd `em` font sizing in all browsers. 98 | */ 99 | 100 | code, 101 | kbd, 102 | samp { 103 | font-family: monospace, monospace; /* 1 */ 104 | font-size: 1em; /* 2 */ 105 | } 106 | 107 | /** 108 | * Add the correct font size in all browsers. 109 | */ 110 | 111 | small { 112 | font-size: 80%; 113 | } 114 | 115 | /** 116 | * Prevent `sub` and `sup` elements from affecting the line height in 117 | * all browsers. 118 | */ 119 | 120 | sub, 121 | sup { 122 | font-size: 75%; 123 | line-height: 0; 124 | position: relative; 125 | vertical-align: baseline; 126 | } 127 | 128 | sub { 129 | bottom: -0.25em; 130 | } 131 | 132 | sup { 133 | top: -0.5em; 134 | } 135 | 136 | /* Embedded content 137 | ========================================================================== */ 138 | 139 | /** 140 | * Remove the border on images inside links in IE 10. 141 | */ 142 | 143 | img { 144 | border-style: none; 145 | } 146 | 147 | /* Forms 148 | ========================================================================== */ 149 | 150 | /** 151 | * 1. Change the font styles in all browsers. 152 | * 2. Remove the margin in Firefox and Safari. 153 | */ 154 | 155 | button, 156 | input, 157 | optgroup, 158 | select, 159 | textarea { 160 | font-family: inherit; /* 1 */ 161 | font-size: 100%; /* 1 */ 162 | line-height: 1.15; /* 1 */ 163 | margin: 0; /* 2 */ 164 | } 165 | 166 | /** 167 | * Show the overflow in IE. 168 | * 1. Show the overflow in Edge. 169 | */ 170 | 171 | button, 172 | input { /* 1 */ 173 | overflow: visible; 174 | } 175 | 176 | /** 177 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 178 | * 1. Remove the inheritance of text transform in Firefox. 179 | */ 180 | 181 | button, 182 | select { /* 1 */ 183 | text-transform: none; 184 | } 185 | 186 | /** 187 | * Correct the inability to style clickable types in iOS and Safari. 188 | */ 189 | 190 | button, 191 | [type="button"], 192 | [type="reset"], 193 | [type="submit"] { 194 | -webkit-appearance: button; 195 | } 196 | 197 | /** 198 | * Remove the inner border and padding in Firefox. 199 | */ 200 | 201 | button::-moz-focus-inner, 202 | [type="button"]::-moz-focus-inner, 203 | [type="reset"]::-moz-focus-inner, 204 | [type="submit"]::-moz-focus-inner { 205 | border-style: none; 206 | padding: 0; 207 | } 208 | 209 | /** 210 | * Restore the focus styles unset by the previous rule. 211 | */ 212 | 213 | button:-moz-focusring, 214 | [type="button"]:-moz-focusring, 215 | [type="reset"]:-moz-focusring, 216 | [type="submit"]:-moz-focusring { 217 | outline: 1px dotted ButtonText; 218 | } 219 | 220 | /** 221 | * Correct the padding in Firefox. 222 | */ 223 | 224 | fieldset { 225 | padding: 0.35em 0.75em 0.625em; 226 | } 227 | 228 | /** 229 | * 1. Correct the text wrapping in Edge and IE. 230 | * 2. Correct the color inheritance from `fieldset` elements in IE. 231 | * 3. Remove the padding so developers are not caught out when they zero out 232 | * `fieldset` elements in all browsers. 233 | */ 234 | 235 | legend { 236 | box-sizing: border-box; /* 1 */ 237 | color: inherit; /* 2 */ 238 | display: table; /* 1 */ 239 | max-width: 100%; /* 1 */ 240 | padding: 0; /* 3 */ 241 | white-space: normal; /* 1 */ 242 | } 243 | 244 | /** 245 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 246 | */ 247 | 248 | progress { 249 | vertical-align: baseline; 250 | } 251 | 252 | /** 253 | * Remove the default vertical scrollbar in IE 10+. 254 | */ 255 | 256 | textarea { 257 | overflow: auto; 258 | } 259 | 260 | /** 261 | * 1. Add the correct box sizing in IE 10. 262 | * 2. Remove the padding in IE 10. 263 | */ 264 | 265 | [type="checkbox"], 266 | [type="radio"] { 267 | box-sizing: border-box; /* 1 */ 268 | padding: 0; /* 2 */ 269 | } 270 | 271 | /** 272 | * Correct the cursor style of increment and decrement buttons in Chrome. 273 | */ 274 | 275 | [type="number"]::-webkit-inner-spin-button, 276 | [type="number"]::-webkit-outer-spin-button { 277 | height: auto; 278 | } 279 | 280 | /** 281 | * 1. Correct the odd appearance in Chrome and Safari. 282 | * 2. Correct the outline style in Safari. 283 | */ 284 | 285 | [type="search"] { 286 | -webkit-appearance: textfield; /* 1 */ 287 | outline-offset: -2px; /* 2 */ 288 | } 289 | 290 | /** 291 | * Remove the inner padding in Chrome and Safari on macOS. 292 | */ 293 | 294 | [type="search"]::-webkit-search-decoration { 295 | -webkit-appearance: none; 296 | } 297 | 298 | /** 299 | * 1. Correct the inability to style clickable types in iOS and Safari. 300 | * 2. Change font properties to `inherit` in Safari. 301 | */ 302 | 303 | ::-webkit-file-upload-button { 304 | -webkit-appearance: button; /* 1 */ 305 | font: inherit; /* 2 */ 306 | } 307 | 308 | /* Interactive 309 | ========================================================================== */ 310 | 311 | /* 312 | * Add the correct display in Edge, IE 10+, and Firefox. 313 | */ 314 | 315 | details { 316 | display: block; 317 | } 318 | 319 | /* 320 | * Add the correct display in all browsers. 321 | */ 322 | 323 | summary { 324 | display: list-item; 325 | } 326 | 327 | /* Misc 328 | ========================================================================== */ 329 | 330 | /** 331 | * Add the correct display in IE 10+. 332 | */ 333 | 334 | template { 335 | display: none; 336 | } 337 | 338 | /** 339 | * Add the correct display in IE 10. 340 | */ 341 | 342 | [hidden] { 343 | display: none; 344 | } 345 | -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbhave/reactive-workshop/18b1f86593ea7543462ca86e042546fa5b630450/ui/src/favicon.ico -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reactive Workshop UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading in progress ... 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /ui/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | -------------------------------------------------------------------------------- /ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "assets/scss/normalize"; 3 | @import "~@angular/material/theming"; 4 | 5 | body { 6 | background: white; 7 | } 8 | 9 | .application { 10 | 11 | } 12 | 13 | .console { 14 | background: black; 15 | color: white; 16 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 17 | font-size: 12px; 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "preserveWhiteSpaces": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "typeof-compare": true, 111 | "unified-signatures": true, 112 | "variable-name": false, 113 | "whitespace": [ 114 | true, 115 | "check-branch", 116 | "check-decl", 117 | "check-operator", 118 | "check-separator", 119 | "check-type" 120 | ], 121 | "directive-selector": [ 122 | true, 123 | "attribute", 124 | "app", 125 | "camelCase" 126 | ], 127 | "component-selector": [ 128 | true, 129 | "element", 130 | "app", 131 | "kebab-case" 132 | ], 133 | "angular-whitespace": [true, "check-interpolation"], 134 | "no-output-on-prefix": true, 135 | "use-input-property-decorator": true, 136 | "use-output-property-decorator": true, 137 | "use-host-property-decorator": true, 138 | "no-input-rename": true, 139 | "no-output-rename": true, 140 | "use-life-cycle-interface": true, 141 | "use-pipe-transform-interface": true, 142 | "component-class-suffix": true, 143 | "directive-class-suffix": true 144 | } 145 | } 146 | --------------------------------------------------------------------------------