├── .gitignore ├── LICENSE ├── rxjava-samples ├── .editorconfig ├── pom.xml └── src │ └── main │ └── java │ └── samples │ ├── RxCreateObservable.java │ ├── RxHello.java │ ├── RxMerge.java │ └── RxThreading.java └── vertx-samples ├── .editorconfig ├── pom.xml └── src └── main ├── java └── samples │ ├── BestOfferServiceVerticle.java │ ├── BiddingServiceVerticle.java │ ├── HttpApplication.java │ ├── MainVerticle.java │ ├── RXTwitterFeedApplication.java │ └── TwitterFeedApplication.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | target/ 3 | 4 | # Compiled class file 5 | *.class 6 | 7 | # Log file 8 | *.log 9 | 10 | # BlueJ files 11 | *.ctxt 12 | 13 | # Mobile Tools for Java (J2ME) 14 | .mtj.tmp/ 15 | 16 | # IntelliJ 17 | .idea/ 18 | *.iml 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.ear 24 | *.zip 25 | *.tar.gz 26 | *.rar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Clément Escoffier, Julien Ponge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rxjava-samples/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /rxjava-samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.jponge.javamag-rxjava-vertx 8 | rxjava-samples 9 | 1.0-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 14 | 15 | 16 | 17 | io.reactivex.rxjava2 18 | rxjava 19 | 2.1.6 20 | 21 | 22 | ch.qos.logback 23 | logback-classic 24 | 1.2.3 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 3.7.0 34 | 35 | 1.8 36 | 1.8 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /rxjava-samples/src/main/java/samples/RxCreateObservable.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.reactivex.Observable; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Random; 10 | 11 | /** 12 | * @author Julien Ponge 13 | */ 14 | public class RxCreateObservable { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(RxCreateObservable.class); 17 | 18 | public static void main(String[] args) { 19 | 20 | List data = Arrays.asList("foo", "bar", "baz"); 21 | Random random = new Random(); 22 | Observable source = Observable.create(subscriber -> { 23 | for (String s : data) { 24 | if (random.nextInt(6) == 0) { 25 | subscriber.onError(new RuntimeException("Bad luck for you...")); 26 | } 27 | subscriber.onNext(s); 28 | } 29 | subscriber.onComplete(); 30 | }); 31 | 32 | for (int i = 0; i < 10; i++) { 33 | logger.info("======================================="); 34 | source.subscribe(next -> logger.info("Next: {}", next), 35 | error -> logger.error("Whoops"), 36 | () -> logger.info("Done")); 37 | } 38 | 39 | logger.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 40 | source 41 | .retry(5) 42 | .subscribe(next -> logger.info("Next: {}", next), 43 | error -> logger.error("Whoops"), 44 | () -> logger.info("Done")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rxjava-samples/src/main/java/samples/RxHello.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.reactivex.Completable; 4 | import io.reactivex.Flowable; 5 | import io.reactivex.Maybe; 6 | import io.reactivex.Single; 7 | import io.reactivex.functions.Consumer; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * @author Julien Ponge 13 | */ 14 | public class RxHello { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(RxHello.class); 17 | 18 | public static void main(String[] args) { 19 | 20 | Single.just(1) 21 | .map(i -> i * 10) 22 | .map(Object::toString) 23 | .subscribe((Consumer) logger::info); 24 | 25 | Maybe.just("Something") 26 | .subscribe(logger::info); 27 | 28 | Maybe.never() 29 | .subscribe(o -> logger.info("Something is here...")); 30 | 31 | Completable.complete() 32 | .subscribe(() -> logger.info("Completed")); 33 | 34 | Flowable.just("foo", "bar", "baz") 35 | .filter(s -> s.startsWith("b")) 36 | .map(String::toUpperCase) 37 | .subscribe(logger::info); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rxjava-samples/src/main/java/samples/RxMerge.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.reactivex.Flowable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.UUID; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * @author Julien Ponge 13 | */ 14 | public class RxMerge { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(RxMerge.class); 17 | 18 | public static void main(String[] args) throws InterruptedException { 19 | 20 | Flowable intervals = Flowable 21 | .interval(100, TimeUnit.MILLISECONDS, Schedulers.computation()) 22 | .limit(10) 23 | .map(tick -> "Tick #" + tick) 24 | .subscribeOn(Schedulers.computation()); 25 | 26 | Flowable strings = Flowable.just("abc", "def", "ghi", "jkl") 27 | .subscribeOn(Schedulers.computation()); 28 | 29 | Flowable uuids = Flowable 30 | .generate(emitter -> emitter.onNext(UUID.randomUUID())) 31 | .limit(10) 32 | .subscribeOn(Schedulers.computation()); 33 | 34 | Flowable.merge(strings, intervals, uuids) 35 | .subscribe(obj -> logger.info("Received: {}", obj)); 36 | 37 | Thread.sleep(3000); 38 | 39 | logger.info("=================="); 40 | 41 | Flowable.zip(intervals, uuids, strings, 42 | (i, u, s) -> String.format("%s {%s} -> %s", i, u, s)) 43 | .subscribe(obj -> logger.info("Received: {}", obj)); 44 | 45 | Thread.sleep(3000); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rxjava-samples/src/main/java/samples/RxThreading.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.reactivex.Flowable; 4 | import io.reactivex.schedulers.Schedulers; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * @author Julien Ponge 10 | */ 11 | public class RxThreading { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(RxThreading.class); 14 | 15 | public static void main(String[] args) throws InterruptedException { 16 | 17 | Flowable.range(1, 5) 18 | .map(i -> i * 10) 19 | .map(i -> { 20 | logger.info("map({})", i); 21 | return i.toString(); 22 | }) 23 | .subscribe(logger::info); 24 | 25 | Thread.sleep(1000); 26 | logger.info("==================================="); 27 | 28 | Flowable.range(1, 5) 29 | .map(i -> i * 10) 30 | .map(i -> { 31 | logger.info("map({})", i); 32 | return i.toString(); 33 | }) 34 | .observeOn(Schedulers.single()) 35 | .subscribe(logger::info); 36 | 37 | Thread.sleep(1000); 38 | logger.info("==================================="); 39 | 40 | Flowable.range(1, 5) 41 | .map(i -> i * 10) 42 | .map(i -> { 43 | logger.info("map({})", i); 44 | return i.toString(); 45 | }) 46 | .observeOn(Schedulers.single()) 47 | .subscribeOn(Schedulers.computation()) 48 | .subscribe(logger::info); 49 | 50 | Thread.sleep(1000); 51 | logger.info("==================================="); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /vertx-samples/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | max_line_length = 70 11 | -------------------------------------------------------------------------------- /vertx-samples/pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | io.github.jponge.javamag-rxjava-vertx 6 | vertx-samples 7 | 1.0-SNAPSHOT 8 | 9 | UTF-8 10 | 11 | 1.8 12 | 1.8 13 | 14 | 3.5.0 15 | samples.MainVerticle 16 | 17 | 1.0.9 18 | 19 | 20 | 21 | 22 | io.vertx 23 | vertx-dependencies 24 | ${vertx.version} 25 | pom 26 | import 27 | 28 | 29 | 30 | 31 | 32 | io.vertx 33 | vertx-core 34 | 35 | 36 | io.vertx 37 | vertx-web 38 | 39 | 40 | io.vertx 41 | vertx-web-client 42 | 43 | 44 | io.vertx 45 | vertx-rx-java2 46 | 47 | 48 | ch.qos.logback 49 | logback-classic 50 | 1.2.3 51 | 52 | 53 | 54 | 55 | 56 | 57 | io.fabric8 58 | vertx-maven-plugin 59 | ${vertx-maven-plugin.version} 60 | 61 | 62 | vmp 63 | 64 | initialize 65 | package 66 | 67 | 68 | 69 | 70 | true 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/BestOfferServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.reactivex.Single; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.json.JsonArray; 6 | import io.vertx.core.json.JsonObject; 7 | import io.vertx.reactivex.core.AbstractVerticle; 8 | import io.vertx.reactivex.core.RxHelper; 9 | import io.vertx.reactivex.core.http.HttpServerRequest; 10 | import io.vertx.reactivex.ext.web.client.HttpResponse; 11 | import io.vertx.reactivex.ext.web.client.WebClient; 12 | import io.vertx.reactivex.ext.web.codec.BodyCodec; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.util.List; 17 | import java.util.concurrent.TimeUnit; 18 | import java.util.concurrent.atomic.AtomicLong; 19 | import java.util.stream.Collectors; 20 | 21 | public class BestOfferServiceVerticle extends AbstractVerticle { 22 | 23 | private static final JsonArray DEFAULT_TARGETS = new JsonArray() 24 | .add(new JsonObject() 25 | .put("host", "localhost") 26 | .put("port", 3000) 27 | .put("path", "/offer")) 28 | .add(new JsonObject() 29 | .put("host", "localhost") 30 | .put("port", 3001) 31 | .put("path", "/offer")) 32 | .add(new JsonObject() 33 | .put("host", "localhost") 34 | .put("port", 3002) 35 | .put("path", "/offer")); 36 | private final Logger logger = LoggerFactory 37 | .getLogger(BestOfferServiceVerticle.class); 38 | private List targets; 39 | private WebClient webClient; 40 | 41 | @Override 42 | public void start(Future startFuture) throws Exception { 43 | webClient = WebClient.create(vertx); 44 | 45 | targets = config().getJsonArray("targets", 46 | DEFAULT_TARGETS) 47 | .stream() 48 | .map(JsonObject.class::cast) 49 | .collect(Collectors.toList()); 50 | 51 | vertx.createHttpServer() 52 | .requestHandler(this::findBestOffer) 53 | .rxListen(8080) 54 | .subscribe((server, error) -> { 55 | if (error != null) { 56 | logger.error("Could not start the best offer " + 57 | "service", error); 58 | startFuture.fail(error); 59 | } else { 60 | logger.info("The best offer service is running " + 61 | "on port 8080"); 62 | startFuture.complete(); 63 | } 64 | }); 65 | } 66 | 67 | private final AtomicLong requestIds = new AtomicLong(); 68 | private static final JsonObject EMPTY_RESPONSE = new JsonObject() 69 | .put("empty", true) 70 | .put("bid", Integer.MAX_VALUE); 71 | 72 | private void findBestOffer(HttpServerRequest request) { 73 | String requestId = String.valueOf(requestIds.getAndIncrement()); 74 | 75 | List> responses = targets.stream() 76 | .map(t -> webClient 77 | .get(t.getInteger("port"), 78 | t.getString("host"), 79 | t.getString("path")) 80 | .putHeader("Client-Request-Id", 81 | String.valueOf(requestId)) 82 | .as(BodyCodec.jsonObject()) 83 | .rxSend() 84 | .retry(1) 85 | .timeout(500, TimeUnit.MILLISECONDS, 86 | RxHelper.scheduler(vertx)) 87 | .map(HttpResponse::body) 88 | .map(body -> { 89 | logger.info("#{} received offer {}", requestId, 90 | body.encodePrettily()); 91 | return body; 92 | }) 93 | .onErrorReturnItem(EMPTY_RESPONSE)) 94 | .collect(Collectors.toList()); 95 | 96 | Single.merge(responses) 97 | .reduce((acc, next) -> { 98 | if (next.containsKey("bid") && isHigher(acc, next)) { 99 | return next; 100 | } 101 | return acc; 102 | }) 103 | .flatMapSingle(best -> { 104 | if (!best.containsKey("empty")) { 105 | return Single.just(best); 106 | } else { 107 | return Single.error(new Exception("No offer " + 108 | "could be found for requestId=" + requestId)); 109 | } 110 | }) 111 | .subscribe(best -> { 112 | logger.info("#{} best offer: {}", requestId, 113 | best.encodePrettily()); 114 | request.response() 115 | .putHeader("Content-Type", 116 | "application/json") 117 | .end(best.encode()); 118 | }, error -> { 119 | logger.error("#{} ends in error", requestId, error); 120 | request.response() 121 | .setStatusCode(502) 122 | .end(); 123 | }); 124 | } 125 | 126 | private boolean isHigher(JsonObject acc, JsonObject next) { 127 | return acc.getInteger("bid") > next.getInteger("bid"); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/BiddingServiceVerticle.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.json.JsonObject; 6 | import io.vertx.ext.web.Router; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.Random; 11 | import java.util.UUID; 12 | 13 | public class BiddingServiceVerticle extends AbstractVerticle { 14 | 15 | private final Logger logger = LoggerFactory 16 | .getLogger(BiddingServiceVerticle.class); 17 | 18 | @Override 19 | public void start(Future verticleStartFuture) { 20 | Random random = new Random(); 21 | String myId = UUID.randomUUID().toString(); 22 | int portNumber = config().getInteger("port", 3000); 23 | 24 | Router router = Router.router(vertx); 25 | router.get("/offer").handler(context -> { 26 | String clientIdHeader = context.request() 27 | .getHeader("Client-Request-Id"); 28 | String clientId = 29 | (clientIdHeader != null) ? clientIdHeader : "N/A"; 30 | int myBid = 10 + random.nextInt(20); 31 | JsonObject payload = new JsonObject() 32 | .put("origin", myId) 33 | .put("bid", myBid); 34 | if (clientIdHeader != null) { 35 | payload.put("clientRequestId", clientId); 36 | } 37 | long artificialDelay = random.nextInt(1000); 38 | vertx.setTimer(artificialDelay, id -> { 39 | if (random.nextInt(20) == 1) { 40 | context.response() 41 | .setStatusCode(500) 42 | .end(); 43 | logger.error("{} injects an error (client-id={}, " 44 | + "artificialDelay={})", 45 | myId, myBid, clientId, artificialDelay); 46 | } else { 47 | context.response() 48 | .putHeader("Content-Type", 49 | "application/json") 50 | .end(payload.encode()); 51 | logger.info("{} offers {} (client-id={}, " + 52 | "artificialDelay={})", 53 | myId, myBid, clientId, artificialDelay); 54 | } 55 | }); 56 | }); 57 | 58 | vertx.createHttpServer() 59 | .requestHandler(router::accept) 60 | .listen(portNumber, ar -> { 61 | if (ar.succeeded()) { 62 | logger.info("Bidding service listening on HTTP " + 63 | "port {}", portNumber); 64 | verticleStartFuture.complete(); 65 | } else { 66 | logger.error("Bidding service failed to start", 67 | ar.cause()); 68 | verticleStartFuture.fail(ar.cause()); 69 | } 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/HttpApplication.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.vertx.core.Vertx; 4 | 5 | /** 6 | * Simple Java application using Vert.x to process HTTP requests. 7 | */ 8 | public class HttpApplication { 9 | 10 | public static void main(String[] args) { 11 | // 1 - Create a Vert.x instance 12 | Vertx vertx = Vertx.vertx(); 13 | 14 | // 2 - Create the HTTP server 15 | vertx.createHttpServer() 16 | // 3 - Attach a request handler processing the requests 17 | .requestHandler(req -> req.response() 18 | .end("Hello, request handled from " 19 | + Thread.currentThread().getName())) 20 | // 4 - Start the server on the port 8080 21 | .listen(8080); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/MainVerticle.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.DeploymentOptions; 5 | import io.vertx.core.json.JsonObject; 6 | 7 | public class MainVerticle extends AbstractVerticle { 8 | 9 | @Override 10 | public void start() { 11 | vertx.deployVerticle(new BiddingServiceVerticle()); 12 | 13 | vertx.deployVerticle(new BiddingServiceVerticle(), 14 | new DeploymentOptions() 15 | .setConfig(new JsonObject().put("port", 3001))); 16 | 17 | vertx.deployVerticle(new BiddingServiceVerticle(), 18 | new DeploymentOptions() 19 | .setConfig(new JsonObject().put("port", 3002))); 20 | 21 | vertx.deployVerticle("samples.BestOfferServiceVerticle", 22 | new DeploymentOptions().setInstances(2)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/RXTwitterFeedApplication.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.vertx.reactivex.core.Vertx; 4 | import io.vertx.reactivex.core.http.HttpServer; 5 | import io.vertx.reactivex.ext.web.client.HttpResponse; 6 | import io.vertx.reactivex.ext.web.client.WebClient; 7 | 8 | /** 9 | * Simple Java application using Vert.x to process HTTP requests. 10 | */ 11 | public class RXTwitterFeedApplication { 12 | 13 | public static void main(String[] args) { 14 | Vertx vertx = Vertx.vertx(); 15 | WebClient client = WebClient.create(vertx); 16 | HttpServer server = vertx.createHttpServer(); 17 | server 18 | // 1 - Transform the sequence of request into a stream 19 | .requestStream().toFlowable() 20 | // 2 - For each request, call the twitter API 21 | .flatMapCompletable(req -> 22 | client.getAbs("https://twitter.com/vertx_project") 23 | .rxSend() 24 | // 3 - Extract the body as string 25 | .map(HttpResponse::bodyAsString) 26 | // 4 - In case of a failure 27 | .onErrorReturn(t -> "Cannot access the twitter " + 28 | "feed: " + t.getMessage()) 29 | // 5 - Write the response 30 | .doOnSuccess(res -> req.response().end(res)) 31 | // 6 - Just transform the restul into a completable 32 | .toCompletable() 33 | ) 34 | // 7 - Never forget to subscribe to a reactive type, 35 | // or nothing happens 36 | .subscribe(); 37 | 38 | server.listen(8080); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /vertx-samples/src/main/java/samples/TwitterFeedApplication.java: -------------------------------------------------------------------------------- 1 | package samples; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.ext.web.client.WebClient; 5 | 6 | /** 7 | * Simple Java application using Vert.x to process HTTP requests. 8 | */ 9 | public class TwitterFeedApplication { 10 | 11 | public static void main(String[] args) { 12 | Vertx vertx = Vertx.vertx(); 13 | // 1 - Create a Web client 14 | WebClient client = WebClient.create(vertx); 15 | vertx.createHttpServer() 16 | .requestHandler(req -> { 17 | // 2 - In the request handler, retrieve a Twitter feed 18 | client 19 | .getAbs("https://twitter.com/vertx_project") 20 | .send(res -> { 21 | // 3 - Write the response based on the result 22 | if (res.failed()) { 23 | req.response().end("Cannot access " 24 | + "the twitter feed: " 25 | + res.cause().getMessage()); 26 | } else { 27 | req.response().end(res.result() 28 | .bodyAsString()); 29 | } 30 | }); 31 | }) 32 | .listen(8080); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vertx-samples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - 7 | %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------