├── .gitignore ├── README.md ├── docker-compose.yml ├── graphana.json ├── pom.xml ├── postman.json ├── prometheus.yml └── src ├── main ├── java │ └── org │ │ └── limadelrey │ │ └── vertx4 │ │ └── reactive │ │ └── rest │ │ └── api │ │ ├── Main.java │ │ ├── api │ │ ├── handler │ │ │ ├── BookHandler.java │ │ │ ├── BookValidationHandler.java │ │ │ └── ErrorHandler.java │ │ ├── model │ │ │ ├── Book.java │ │ │ ├── BookGetAllResponse.java │ │ │ └── BookGetByIdResponse.java │ │ ├── repository │ │ │ └── BookRepository.java │ │ ├── router │ │ │ ├── BookRouter.java │ │ │ ├── HealthCheckRouter.java │ │ │ └── MetricsRouter.java │ │ └── service │ │ │ └── BookService.java │ │ ├── utils │ │ ├── ConfigUtils.java │ │ ├── DbUtils.java │ │ ├── LogUtils.java │ │ ├── QueryUtils.java │ │ └── ResponseUtils.java │ │ └── verticle │ │ ├── ApiVerticle.java │ │ ├── MainVerticle.java │ │ └── MigrationVerticle.java └── resources │ ├── application.properties │ └── db │ └── migration │ ├── V1__initial_schema.sql │ └── V2__initial_data.sql └── test ├── java └── org │ └── limadelrey │ └── vertx4 │ └── reactive │ └── rest │ └── api │ ├── AbstractContainerBaseTest.java │ └── ComponentTests.java └── resources ├── application.properties ├── create ├── request.json └── response.json ├── readAll └── response.json ├── readOne └── response.json └── update ├── request.json └── response.json /.gitignore: -------------------------------------------------------------------------------- 1 | ### Vert.x ### 2 | .vertx/ 3 | 4 | ### Eclipse ### 5 | 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # Java annotation processor (APT) 31 | .factorypath 32 | 33 | # PDT-specific (PHP Development Tools) 34 | .buildpath 35 | 36 | # sbteclipse plugin 37 | .target 38 | 39 | # Tern plugin 40 | .tern-project 41 | 42 | # TeXlipse plugin 43 | .texlipse 44 | 45 | # STS (Spring Tool Suite) 46 | .springBeans 47 | 48 | # Code Recommenders 49 | .recommenders/ 50 | 51 | # Scala IDE specific (Scala & Java development for Eclipse) 52 | .cache-main 53 | .scala_dependencies 54 | .worksheet 55 | 56 | ### Intellij+iml ### 57 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 58 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 59 | 60 | # CMake 61 | cmake-buildTool-debug/ 62 | 63 | ## File-based project format: 64 | *.iws 65 | 66 | ## Plugin-specific files: 67 | 68 | # IntelliJ 69 | /out/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-buildTool.properties 78 | fabric.properties 79 | 80 | ### Intellij+iml Patch ### 81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 82 | 83 | *.iml 84 | modules.xml 85 | .idea/** 86 | *.ipr 87 | 88 | ### macOS ### 89 | *.DS_Store 90 | .AppleDouble 91 | .LSOverride 92 | 93 | # Icon must end with two \r 94 | Icon 95 | 96 | # Thumbnails 97 | ._* 98 | 99 | # Files that might appear in the root of a volume 100 | .DocumentRevisions-V100 101 | .fseventsd 102 | .Spotlight-V100 103 | .TemporaryItems 104 | .Trashes 105 | .VolumeIcon.icns 106 | .com.apple.timemachine.donotpresent 107 | 108 | # Directories potentially created on remote AFP share 109 | .AppleDB 110 | .AppleDesktop 111 | Network Trash Folder 112 | Temporary Items 113 | .apdisk 114 | 115 | ### Maven ### 116 | target/ 117 | pom.xml.tag 118 | pom.xml.releaseBackup 119 | pom.xml.versionsBackup 120 | pom.xml.next 121 | release.properties 122 | dependency-reduced-pom.xml 123 | buildNumber.properties 124 | .mvn/timing.properties 125 | 126 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 127 | !/.mvn/wrapper/maven-wrapper.jar 128 | 129 | ### Gradle ### 130 | .gradle 131 | /buildTool/ 132 | 133 | # Ignore Gradle GUI config 134 | gradle-app.setting 135 | 136 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 137 | !gradle-wrapper.jar 138 | 139 | # Cache of project 140 | .gradletasknamecache 141 | 142 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 143 | # gradle/wrapper/gradle-wrapper.properties 144 | 145 | ### NetBeans ### 146 | nbproject/private/ 147 | buildTool/ 148 | nbbuild/ 149 | dist/ 150 | nbdist/ 151 | .nb-gradle/ 152 | 153 | ### VisualStudioCode ### 154 | .vscode/* 155 | !.vscode/settings.json 156 | !.vscode/tasks.json 157 | !.vscode/launch.json 158 | !.vscode/extensions.json 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | https://medium.com/@limadelrey/vert-x-4-how-to-build-a-reactive-restful-web-service-dd845e3d0b66 2 | 3 | # Vert.x 4: How to build a reactive RESTful Web Service 4 | Eclipse Vert.x is a tool-​kit for building reactive applications on the JVM. Reactive applications are both scalable as workloads grow and resilient when failures arise, making them ideal for low-latency and high-throughput use cases. Its main advantage resides in the way it approaches multi-threading. 5 | Threads can live within a single process, perform concurrent work and share the same memory space. However, as workload grows, the OS kernel struggles when there is too much context switching work with in-flight requests. Because of that, some threads are blocked while they are waiting on I/O operations to complete while others are ready to handle I/O results. Vert.x takes a different approach to this problem by using event loops: 6 | 7 |

8 |

Event loop

9 | 10 | Instead of blocking a thread when an I/O operation occurs, it moves on to the next event (which is ready to progress) and resumes the initial event when it's the appropriate callback's turn in the queue. As long as the event loop is not blocked, there's a significant performance improvement, making Vert.x one of the fastest in the Java ecosystem. 11 | 12 | # What's new in Vert.x 4? 13 | Vert.x is backed by a large ecosystem of reactive modules that are useful to write modern applications: a comprehensive web stack, reactive database drivers, messaging, event streams, clustering, metrics, distributed tracing and more. This release adds a significant number of new features and improvements on top of that: 14 | - Futurisation - Implementation of future/callback hybrid model; 15 | - Monitoring - Vert.x Tracing supports both Opentracing and Zipkin. It also complements Vert.x Metrics; 16 | - Reactive SQL clients - High-​performance reactive SQL clients, fully integrated with Vert.x Metrics and Vert.x Tracing; 17 | - Reactive Redis client - The revamped client API now supports all Redis connection modes with extensible support for Redis commands; 18 | - SQL Templating - SQL Client Templates is a library designed to facilitate building SQL queries; 19 | - Web Validation - Extensible sync/async HTTP request validator providing a DSL to describe expected HTTP requests; 20 | - Web OpenAPI - New support for Contract Driven development based on OpenAPI; 21 | - Authentication and Authorization - A set of new modules are now available (e.g. vertx-auth-ldap, vertx-auth-sql & vertx-auth-webauth) and improvements were made to older modules (e.g. vertx-auth-oauth). 22 | 23 | # Building our reactive RESTful Web Service 24 | There's a good amount of documentation on how to use Vert.x and its modules. However, most of the code examples are focused on a specific feature and presented as a single class Java application (usually MainVerticle). Personally, I feel that it doesn't represent the full reach of its capabilities, so I decided to prepare an application that implements a fully-reactive web service capable of communicating with a relational database (e.g. PostgreSQL) and providing a CRUD API while addressing some production-ready requirements such as metrics, logging, health checks, database migrations, tests and so on. The codebase is organized in traditional N-Tier architecture and it uses some of the latest features such as SQL Templating and Web Validation. 25 |

26 |

Codebase package structure

27 | 28 | # Router 29 | A router takes an HTTP request, finds the first matching route for that request and proxies the request to that route. Each route can have multiple handlers and each handler should be responsible for a different requirement. You can use this layer to define your API endpoints, your API versioning and even enable request validation. Below you'll find several routes versioned by its URI path using three handlers: a Logger handler, a Web Validation handler and a custom business logic handler. 30 |

Router

31 | 32 | # Handler 33 | As I mentioned before, handlers should be used for different requirements. Vert.x provides many of them out of the box. Logger handlers are used to log requests information, the new Web Validation handlers are used to perform request schema validation, Error handlers are used to customize error messages, the list goes on. You can also implement your own handlers, just like the one that you can find below. This handler provides all the operations necessary by the routes that were defined previously. They extract information from the request (query parameters, path parameters and body) initially and return a JSON response with an appropriate HTTP status code at the end, leaving all the business logic to the service layer. 34 |

Handler

35 | 36 | # Service 37 | Services are a common abstraction to represent middleware. They are used to process data using some business logic and can be very useful to establish a set of available operations that coordinate the application's response. Our service layer is able to create, read, update and delete data. It's responsible for transactional control, for providing connections to the repository layer, for transforming entities into DTOs and for implementing specific features such as pagination. It uses the Reactive PostgreSQL client, which is non-blocking, allowing it to handle many database connections with a single thread. 38 |

Service

39 | 40 | # Repository 41 | Repositories are classes that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer. Below you can find multiple SQL statements as well as the new SQL templating, which can be very useful to map structured data to our entities. 42 |

Repository

43 | 44 | # Verticle 45 | Vert.x comes with a scalable concurrency model out of the box that you can use to save time writing your own. It shares similarities with the Actor model especially with respect to concurrency, scaling and deployment strategies. Instead of using actors, it uses verticles. There are two types of verticles: 46 | - **Standard** - Standard verticles are assigned an event loop thread when they are created and the start method is called with that event loop. This means you can write all the code in your application as single-threaded and let Vert.x worry about the threading and scaling; 47 | - **Worker** - A worker verticle is just like a standard verticle, but it's executed using a thread from the worker thread pool. They are designed for calling blocking code. 48 | 49 | Although the usage of verticles is entirely optional, it's generally a good idea to do it concurrency-wise. Below you'll find a verticle describing how to run an HTTP server with all of your configurations. 50 |

API Verticle

51 | 52 | # Database migrations 53 | Database management should be composed of incremental and reversible changes allowing us to control our relational database schema versions. By using a schema migration tool (e.g. Flyway) you should have the possibility to define your schema evolution and seed its initial data programmatically. This requirement has a blocking nature, so you should deploy a worker verticle. 54 |

Migrations

55 |

56 | 57 | # Health checks 58 | Health check endpoints enable us to periodically test the health of our service. Sometimes, applications transition to broken states and cannot recover except by being restarted. Other times applications are temporarily unable to serve traffic (e.g. an application might need to load large data during startup) and, in such case, it shouldn't restart the application and/or allow requests either. Providing liveness and readiness probes, respectively, allows us to detect and mitigate these situations. Below you'll find a way to express the current state of the application and the information on whether a connection to the database can be established. 59 |

Health check

60 |

61 | 62 | # Metrics 63 | Application monitoring provides detailed observability into the performance, availability and user experience of applications and their supporting infrastructure. By gathering statistics from the HTTP server, database, API or any existing module you're better prepared to recover from failures and also able to get insights into what is happening inside the application. Vert.x provides a convenient integration with Micrometer whose data can become available through an endpoint that is scraped periodically by Prometheus and consequently by Grafana in order to produce proper dashboards. 64 |

Metrics

65 |

66 | 67 | # Tests 68 | Finally, it's very important to check whether the actual software matches the expected requirements and/or create an automated way of identifying errors, gaps or missing requirements. There are many types of tests: unit tests, integration tests, component tests, end-to-end tests and so on. Component tests are interesting in the way they allow us to test our web service using the consumer perspective (e.g. API) as the main driver. At the same time, it allows us to test the interaction of the web service with the database, all as one unit. The main challenge is to ensure that the local environment is the same as the production environment. For that reason, you could use Testcontainers in order to mimic PostgreSQL or any other technology. 69 |

Tests

70 | 71 | # Final thoughts 72 | Modern kernels have very good schedulers, but we cannot expect them to deal with 50k threads as easily as they would do with 5k. It's important to recognize that threads aren't cheap: creating a thread takes a few milliseconds and consumes about 1MB of memory. Vert.x approach addresses these issues and provides a rich ecosystem that allows developers to build highly scalable and performant applications. There's just one caveat: code that runs on event loops should not perform block­ing I/O or lengthy processing. But don't worry if you have such code: Vert.x has worker threads and APIs to process events back on the event loop. You can find the codebase and all the necessary configurations on the following repository. 73 | 74 | # Sources 75 | [1] https://vertx.io/ 76 | [2] https://vertx.io/blog/whats-new-in-vert-x-4/ 77 | [3] https://www.techempower.com/benchmarks/ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | books-postgresql: 5 | container_name: book-store-postgresql 6 | image: postgres:12-alpine 7 | environment: 8 | POSTGRES_DB: books 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: AXEe263eqPFqwy4z 11 | ports: 12 | - "5432:5432" 13 | volumes: 14 | - book-store-postgresql-local:/var/lib/postgresql/data 15 | 16 | books-prometheus: 17 | container_name: book-store-prometheus 18 | image: prom/prometheus:v2.24.0 19 | volumes: 20 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 21 | ports: 22 | - "9090:9090" 23 | 24 | books-grafana: 25 | container_name: book-store-grafana 26 | image: grafana/grafana:7.3.7 27 | ports: 28 | - "3000:3000" 29 | 30 | volumes: 31 | book-store-postgresql-local: 32 | 33 | networks: 34 | default: 35 | external: 36 | name: vertx-4-reactive-rest-api-network 37 | -------------------------------------------------------------------------------- /graphana.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "4.6.2" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | } 31 | ], 32 | "annotations": { 33 | "list": [ 34 | { 35 | "builtIn": 1, 36 | "datasource": "-- Grafana --", 37 | "enable": true, 38 | "hide": true, 39 | "iconColor": "rgba(0, 211, 255, 1)", 40 | "name": "Annotations & Alerts", 41 | "type": "dashboard" 42 | } 43 | ] 44 | }, 45 | "editable": true, 46 | "gnetId": null, 47 | "graphTooltip": 0, 48 | "hideControls": false, 49 | "id": null, 50 | "links": [], 51 | "refresh": "10s", 52 | "rows": [ 53 | { 54 | "collapse": false, 55 | "height": 250, 56 | "panels": [ 57 | { 58 | "aliasColors": {}, 59 | "bars": false, 60 | "dashLength": 10, 61 | "dashes": false, 62 | "datasource": "${DS_PROMETHEUS}", 63 | "fill": 1, 64 | "id": 3, 65 | "legend": { 66 | "avg": false, 67 | "current": false, 68 | "max": false, 69 | "min": false, 70 | "show": true, 71 | "total": false, 72 | "values": false 73 | }, 74 | "lines": true, 75 | "linewidth": 1, 76 | "links": [], 77 | "nullPointMode": "null", 78 | "percentage": false, 79 | "pointradius": 5, 80 | "points": false, 81 | "renderer": "flot", 82 | "seriesOverrides": [], 83 | "spaceLength": 10, 84 | "span": 4, 85 | "stack": false, 86 | "steppedLine": false, 87 | "targets": [ 88 | { 89 | "expr": "vertx_http_server_connections", 90 | "format": "time_series", 91 | "intervalFactor": 2, 92 | "legendFormat": "{{local}}", 93 | "refId": "A" 94 | } 95 | ], 96 | "thresholds": [], 97 | "timeFrom": null, 98 | "timeShift": null, 99 | "title": "Active connections", 100 | "tooltip": { 101 | "shared": true, 102 | "sort": 0, 103 | "value_type": "individual" 104 | }, 105 | "type": "graph", 106 | "xaxis": { 107 | "buckets": null, 108 | "mode": "time", 109 | "name": null, 110 | "show": true, 111 | "values": [] 112 | }, 113 | "yaxes": [ 114 | { 115 | "format": "short", 116 | "label": null, 117 | "logBase": 1, 118 | "max": null, 119 | "min": null, 120 | "show": true 121 | }, 122 | { 123 | "format": "short", 124 | "label": null, 125 | "logBase": 1, 126 | "max": null, 127 | "min": null, 128 | "show": true 129 | } 130 | ] 131 | }, 132 | { 133 | "aliasColors": {}, 134 | "bars": false, 135 | "dashLength": 10, 136 | "dashes": false, 137 | "datasource": "${DS_PROMETHEUS}", 138 | "fill": 1, 139 | "id": 4, 140 | "legend": { 141 | "avg": false, 142 | "current": false, 143 | "max": false, 144 | "min": false, 145 | "show": true, 146 | "total": false, 147 | "values": false 148 | }, 149 | "lines": true, 150 | "linewidth": 1, 151 | "links": [], 152 | "nullPointMode": "null", 153 | "percentage": false, 154 | "pointradius": 5, 155 | "points": false, 156 | "renderer": "flot", 157 | "seriesOverrides": [], 158 | "spaceLength": 10, 159 | "span": 4, 160 | "stack": false, 161 | "steppedLine": false, 162 | "targets": [ 163 | { 164 | "expr": "round(sum(rate(vertx_http_server_requestCount_total[1m])) by (code), 0.001)", 165 | "format": "time_series", 166 | "intervalFactor": 2, 167 | "refId": "A" 168 | } 169 | ], 170 | "thresholds": [], 171 | "timeFrom": null, 172 | "timeShift": null, 173 | "title": "Request count rate by error code", 174 | "tooltip": { 175 | "shared": true, 176 | "sort": 0, 177 | "value_type": "individual" 178 | }, 179 | "type": "graph", 180 | "xaxis": { 181 | "buckets": null, 182 | "mode": "time", 183 | "name": null, 184 | "show": true, 185 | "values": [] 186 | }, 187 | "yaxes": [ 188 | { 189 | "format": "ops", 190 | "label": null, 191 | "logBase": 1, 192 | "max": null, 193 | "min": null, 194 | "show": true 195 | }, 196 | { 197 | "format": "short", 198 | "label": null, 199 | "logBase": 1, 200 | "max": null, 201 | "min": null, 202 | "show": true 203 | } 204 | ] 205 | }, 206 | { 207 | "aliasColors": {}, 208 | "bars": false, 209 | "dashLength": 10, 210 | "dashes": false, 211 | "datasource": "${DS_PROMETHEUS}", 212 | "fill": 1, 213 | "id": 5, 214 | "legend": { 215 | "avg": false, 216 | "current": false, 217 | "max": false, 218 | "min": false, 219 | "show": true, 220 | "total": false, 221 | "values": false 222 | }, 223 | "lines": true, 224 | "linewidth": 1, 225 | "links": [], 226 | "nullPointMode": "null", 227 | "percentage": false, 228 | "pointradius": 5, 229 | "points": false, 230 | "renderer": "flot", 231 | "seriesOverrides": [], 232 | "spaceLength": 10, 233 | "span": 4, 234 | "stack": false, 235 | "steppedLine": false, 236 | "targets": [ 237 | { 238 | "expr": "round(sum(rate(vertx_http_server_requestCount_total[1m])) by (local,code,path), 0.001)", 239 | "format": "time_series", 240 | "intervalFactor": 2, 241 | "refId": "A" 242 | } 243 | ], 244 | "thresholds": [], 245 | "timeFrom": null, 246 | "timeShift": null, 247 | "title": "Request count rate by code and path", 248 | "tooltip": { 249 | "shared": true, 250 | "sort": 0, 251 | "value_type": "individual" 252 | }, 253 | "type": "graph", 254 | "xaxis": { 255 | "buckets": null, 256 | "mode": "time", 257 | "name": null, 258 | "show": true, 259 | "values": [] 260 | }, 261 | "yaxes": [ 262 | { 263 | "format": "ops", 264 | "label": null, 265 | "logBase": 1, 266 | "max": null, 267 | "min": null, 268 | "show": true 269 | }, 270 | { 271 | "format": "short", 272 | "label": null, 273 | "logBase": 1, 274 | "max": null, 275 | "min": null, 276 | "show": true 277 | } 278 | ] 279 | } 280 | ], 281 | "repeat": null, 282 | "repeatIteration": null, 283 | "repeatRowId": null, 284 | "showTitle": false, 285 | "title": "Dashboard Row", 286 | "titleSize": "h6" 287 | }, 288 | { 289 | "collapse": false, 290 | "height": 250, 291 | "panels": [ 292 | { 293 | "aliasColors": {}, 294 | "bars": false, 295 | "dashLength": 10, 296 | "dashes": false, 297 | "datasource": "${DS_PROMETHEUS}", 298 | "fill": 1, 299 | "id": 6, 300 | "legend": { 301 | "avg": false, 302 | "current": false, 303 | "max": false, 304 | "min": false, 305 | "show": true, 306 | "total": false, 307 | "values": false 308 | }, 309 | "lines": true, 310 | "linewidth": 1, 311 | "links": [], 312 | "nullPointMode": "null", 313 | "percentage": false, 314 | "pointradius": 5, 315 | "points": false, 316 | "renderer": "flot", 317 | "seriesOverrides": [], 318 | "spaceLength": 10, 319 | "span": 6, 320 | "stack": false, 321 | "steppedLine": false, 322 | "targets": [ 323 | { 324 | "expr": "sum(irate(vertx_http_server_responseTime_seconds_sum[1m])) / sum(irate(vertx_http_server_responseTime_seconds_count[1m]))", 325 | "format": "time_series", 326 | "intervalFactor": 2, 327 | "legendFormat": "[avg]", 328 | "refId": "B" 329 | }, 330 | { 331 | "expr": "histogram_quantile(0.5, sum(rate(vertx_http_server_responseTime_seconds_bucket[1m])) by (le))", 332 | "format": "time_series", 333 | "intervalFactor": 2, 334 | "legendFormat": "[med]", 335 | "refId": "D" 336 | }, 337 | { 338 | "expr": "histogram_quantile(0.95, sum(rate(vertx_http_server_responseTime_seconds_bucket[1m])) by (le))", 339 | "format": "time_series", 340 | "intervalFactor": 2, 341 | "legendFormat": "[95p]", 342 | "refId": "A" 343 | }, 344 | { 345 | "expr": "histogram_quantile(0.99, sum(rate(vertx_http_server_responseTime_seconds_bucket[1m])) by (le))", 346 | "format": "time_series", 347 | "intervalFactor": 2, 348 | "legendFormat": "[99p]", 349 | "refId": "C" 350 | } 351 | ], 352 | "thresholds": [], 353 | "timeFrom": null, 354 | "timeShift": null, 355 | "title": "Global response time", 356 | "tooltip": { 357 | "shared": true, 358 | "sort": 0, 359 | "value_type": "individual" 360 | }, 361 | "type": "graph", 362 | "xaxis": { 363 | "buckets": null, 364 | "mode": "time", 365 | "name": null, 366 | "show": true, 367 | "values": [] 368 | }, 369 | "yaxes": [ 370 | { 371 | "format": "s", 372 | "label": null, 373 | "logBase": 1, 374 | "max": null, 375 | "min": null, 376 | "show": true 377 | }, 378 | { 379 | "format": "short", 380 | "label": null, 381 | "logBase": 1, 382 | "max": null, 383 | "min": null, 384 | "show": true 385 | } 386 | ] 387 | }, 388 | { 389 | "aliasColors": {}, 390 | "bars": false, 391 | "dashLength": 10, 392 | "dashes": false, 393 | "datasource": "${DS_PROMETHEUS}", 394 | "fill": 1, 395 | "id": 7, 396 | "legend": { 397 | "avg": false, 398 | "current": false, 399 | "max": false, 400 | "min": false, 401 | "show": true, 402 | "total": false, 403 | "values": false 404 | }, 405 | "lines": true, 406 | "linewidth": 1, 407 | "links": [], 408 | "nullPointMode": "null", 409 | "percentage": false, 410 | "pointradius": 5, 411 | "points": false, 412 | "renderer": "flot", 413 | "seriesOverrides": [], 414 | "spaceLength": 10, 415 | "span": 6, 416 | "stack": false, 417 | "steppedLine": false, 418 | "targets": [ 419 | { 420 | "expr": "sum(irate(vertx_http_server_responseTime_seconds_sum[1m])) by (path) / sum(irate(vertx_http_server_responseTime_seconds_count[1m])) by (path)", 421 | "format": "time_series", 422 | "intervalFactor": 2, 423 | "legendFormat": "{{path}} [avg]", 424 | "refId": "B" 425 | }, 426 | { 427 | "expr": "histogram_quantile(0.5, sum(rate(vertx_http_server_responseTime_seconds_bucket[1m])) by (le,path))", 428 | "format": "time_series", 429 | "intervalFactor": 2, 430 | "legendFormat": "{{path}} [med]", 431 | "refId": "A" 432 | }, 433 | { 434 | "expr": "histogram_quantile(0.99, sum(rate(vertx_http_server_responseTime_seconds_bucket[1m])) by (le,path))", 435 | "format": "time_series", 436 | "intervalFactor": 2, 437 | "legendFormat": "{{path}} [99p]", 438 | "refId": "C" 439 | } 440 | ], 441 | "thresholds": [], 442 | "timeFrom": null, 443 | "timeShift": null, 444 | "title": "Response time by path", 445 | "tooltip": { 446 | "shared": true, 447 | "sort": 0, 448 | "value_type": "individual" 449 | }, 450 | "type": "graph", 451 | "xaxis": { 452 | "buckets": null, 453 | "mode": "time", 454 | "name": null, 455 | "show": true, 456 | "values": [] 457 | }, 458 | "yaxes": [ 459 | { 460 | "format": "s", 461 | "label": null, 462 | "logBase": 1, 463 | "max": null, 464 | "min": null, 465 | "show": true 466 | }, 467 | { 468 | "format": "short", 469 | "label": null, 470 | "logBase": 1, 471 | "max": null, 472 | "min": null, 473 | "show": true 474 | } 475 | ] 476 | } 477 | ], 478 | "repeat": null, 479 | "repeatIteration": null, 480 | "repeatRowId": null, 481 | "showTitle": false, 482 | "title": "Dashboard Row", 483 | "titleSize": "h6" 484 | }, 485 | { 486 | "collapse": false, 487 | "height": "250px", 488 | "panels": [ 489 | { 490 | "aliasColors": {}, 491 | "bars": false, 492 | "dashLength": 10, 493 | "dashes": false, 494 | "datasource": "${DS_PROMETHEUS}", 495 | "fill": 1, 496 | "id": 1, 497 | "legend": { 498 | "avg": false, 499 | "current": false, 500 | "max": false, 501 | "min": false, 502 | "show": true, 503 | "total": false, 504 | "values": false 505 | }, 506 | "lines": true, 507 | "linewidth": 1, 508 | "links": [], 509 | "nullPointMode": "null", 510 | "percentage": false, 511 | "pointradius": 5, 512 | "points": false, 513 | "renderer": "flot", 514 | "seriesOverrides": [], 515 | "spaceLength": 10, 516 | "span": 6, 517 | "stack": false, 518 | "steppedLine": false, 519 | "targets": [ 520 | { 521 | "expr": "sum(rate(vertx_http_server_bytesSent_sum[1m])) / sum(rate(vertx_http_server_bytesSent_count[1m]))", 522 | "format": "time_series", 523 | "hide": false, 524 | "intervalFactor": 2, 525 | "legendFormat": "[avg]", 526 | "refId": "A" 527 | }, 528 | { 529 | "expr": "histogram_quantile(0.5, sum(rate(vertx_http_server_bytesSent_bucket[1m])) by (le))", 530 | "format": "time_series", 531 | "hide": false, 532 | "intervalFactor": 2, 533 | "legendFormat": "[med]", 534 | "refId": "B" 535 | }, 536 | { 537 | "expr": "histogram_quantile(0.95, sum(rate(vertx_http_server_bytesSent_bucket[1m])) by (le))", 538 | "format": "time_series", 539 | "hide": false, 540 | "intervalFactor": 2, 541 | "legendFormat": "[95p]", 542 | "refId": "C" 543 | }, 544 | { 545 | "expr": "histogram_quantile(0.99, sum(rate(vertx_http_server_bytesSent_bucket[1m])) by (le))", 546 | "format": "time_series", 547 | "hide": false, 548 | "intervalFactor": 2, 549 | "legendFormat": "[99p]", 550 | "refId": "D" 551 | } 552 | ], 553 | "thresholds": [], 554 | "timeFrom": null, 555 | "timeShift": null, 556 | "title": "HTTP Server bytes sent", 557 | "tooltip": { 558 | "shared": true, 559 | "sort": 0, 560 | "value_type": "individual" 561 | }, 562 | "type": "graph", 563 | "xaxis": { 564 | "buckets": null, 565 | "mode": "time", 566 | "name": null, 567 | "show": true, 568 | "values": [] 569 | }, 570 | "yaxes": [ 571 | { 572 | "decimals": null, 573 | "format": "bytes", 574 | "label": null, 575 | "logBase": 1, 576 | "max": null, 577 | "min": null, 578 | "show": true 579 | }, 580 | { 581 | "format": "short", 582 | "label": null, 583 | "logBase": 1, 584 | "max": null, 585 | "min": null, 586 | "show": true 587 | } 588 | ] 589 | }, 590 | { 591 | "aliasColors": {}, 592 | "bars": false, 593 | "dashLength": 10, 594 | "dashes": false, 595 | "datasource": "${DS_PROMETHEUS}", 596 | "fill": 1, 597 | "id": 14, 598 | "legend": { 599 | "avg": false, 600 | "current": false, 601 | "max": false, 602 | "min": false, 603 | "show": true, 604 | "total": false, 605 | "values": false 606 | }, 607 | "lines": true, 608 | "linewidth": 1, 609 | "links": [], 610 | "nullPointMode": "null", 611 | "percentage": false, 612 | "pointradius": 5, 613 | "points": false, 614 | "renderer": "flot", 615 | "seriesOverrides": [], 616 | "spaceLength": 10, 617 | "span": 6, 618 | "stack": false, 619 | "steppedLine": false, 620 | "targets": [ 621 | { 622 | "expr": "sum(rate(vertx_http_server_bytesReceived_sum[1m])) / sum(rate(vertx_http_server_bytesReceived_count[1m]))", 623 | "format": "time_series", 624 | "hide": false, 625 | "intervalFactor": 2, 626 | "legendFormat": "[avg]", 627 | "refId": "A" 628 | }, 629 | { 630 | "expr": "histogram_quantile(0.5, sum(rate(vertx_http_server_bytesReceived_bucket[1m])) by (le))", 631 | "format": "time_series", 632 | "hide": false, 633 | "intervalFactor": 2, 634 | "legendFormat": "[med]", 635 | "refId": "B" 636 | }, 637 | { 638 | "expr": "histogram_quantile(0.95, sum(rate(vertx_http_server_bytesReceived_bucket[1m])) by (le))", 639 | "format": "time_series", 640 | "hide": false, 641 | "intervalFactor": 2, 642 | "legendFormat": "[95p]", 643 | "refId": "C" 644 | }, 645 | { 646 | "expr": "histogram_quantile(0.99, sum(rate(vertx_http_server_bytesReceived_bucket[1m])) by (le))", 647 | "format": "time_series", 648 | "hide": false, 649 | "intervalFactor": 2, 650 | "legendFormat": "[99p]", 651 | "refId": "D" 652 | } 653 | ], 654 | "thresholds": [], 655 | "timeFrom": null, 656 | "timeShift": null, 657 | "title": "HTTP Server bytes received", 658 | "tooltip": { 659 | "shared": true, 660 | "sort": 0, 661 | "value_type": "individual" 662 | }, 663 | "type": "graph", 664 | "xaxis": { 665 | "buckets": null, 666 | "mode": "time", 667 | "name": null, 668 | "show": true, 669 | "values": [] 670 | }, 671 | "yaxes": [ 672 | { 673 | "decimals": null, 674 | "format": "bytes", 675 | "label": null, 676 | "logBase": 1, 677 | "max": null, 678 | "min": null, 679 | "show": true 680 | }, 681 | { 682 | "format": "short", 683 | "label": null, 684 | "logBase": 1, 685 | "max": null, 686 | "min": null, 687 | "show": true 688 | } 689 | ] 690 | } 691 | ], 692 | "repeat": null, 693 | "repeatIteration": null, 694 | "repeatRowId": null, 695 | "showTitle": false, 696 | "title": "Dashboard Row", 697 | "titleSize": "h6" 698 | }, 699 | { 700 | "collapse": false, 701 | "height": 250, 702 | "panels": [ 703 | { 704 | "aliasColors": {}, 705 | "bars": false, 706 | "dashLength": 10, 707 | "dashes": false, 708 | "datasource": "${DS_PROMETHEUS}", 709 | "fill": 1, 710 | "id": 8, 711 | "legend": { 712 | "avg": false, 713 | "current": false, 714 | "max": false, 715 | "min": false, 716 | "show": true, 717 | "total": false, 718 | "values": false 719 | }, 720 | "lines": true, 721 | "linewidth": 1, 722 | "links": [], 723 | "nullPointMode": "null", 724 | "percentage": false, 725 | "pointradius": 5, 726 | "points": false, 727 | "renderer": "flot", 728 | "seriesOverrides": [], 729 | "spaceLength": 10, 730 | "span": 3, 731 | "stack": false, 732 | "steppedLine": false, 733 | "targets": [ 734 | { 735 | "expr": "vertx_eventbus_handlers", 736 | "format": "time_series", 737 | "intervalFactor": 2, 738 | "legendFormat": "{{address}}", 739 | "refId": "A" 740 | } 741 | ], 742 | "thresholds": [], 743 | "timeFrom": null, 744 | "timeShift": null, 745 | "title": "Eventbus: handlers", 746 | "tooltip": { 747 | "shared": true, 748 | "sort": 0, 749 | "value_type": "individual" 750 | }, 751 | "type": "graph", 752 | "xaxis": { 753 | "buckets": null, 754 | "mode": "time", 755 | "name": null, 756 | "show": true, 757 | "values": [] 758 | }, 759 | "yaxes": [ 760 | { 761 | "format": "short", 762 | "label": null, 763 | "logBase": 1, 764 | "max": null, 765 | "min": null, 766 | "show": true 767 | }, 768 | { 769 | "format": "short", 770 | "label": null, 771 | "logBase": 1, 772 | "max": null, 773 | "min": null, 774 | "show": true 775 | } 776 | ] 777 | }, 778 | { 779 | "aliasColors": {}, 780 | "bars": false, 781 | "dashLength": 10, 782 | "dashes": false, 783 | "datasource": "${DS_PROMETHEUS}", 784 | "description": "", 785 | "fill": 1, 786 | "id": 9, 787 | "legend": { 788 | "avg": false, 789 | "current": false, 790 | "max": false, 791 | "min": false, 792 | "show": true, 793 | "total": false, 794 | "values": false 795 | }, 796 | "lines": true, 797 | "linewidth": 1, 798 | "links": [], 799 | "nullPointMode": "null", 800 | "percentage": false, 801 | "pointradius": 5, 802 | "points": false, 803 | "renderer": "flot", 804 | "seriesOverrides": [], 805 | "spaceLength": 10, 806 | "span": 3, 807 | "stack": false, 808 | "steppedLine": false, 809 | "targets": [ 810 | { 811 | "expr": "vertx_eventbus_pending", 812 | "format": "time_series", 813 | "intervalFactor": 2, 814 | "legendFormat": "{{address}}", 815 | "refId": "A" 816 | } 817 | ], 818 | "thresholds": [], 819 | "timeFrom": null, 820 | "timeShift": null, 821 | "title": "Eventbus: pending", 822 | "tooltip": { 823 | "shared": true, 824 | "sort": 0, 825 | "value_type": "individual" 826 | }, 827 | "type": "graph", 828 | "xaxis": { 829 | "buckets": null, 830 | "mode": "time", 831 | "name": null, 832 | "show": true, 833 | "values": [] 834 | }, 835 | "yaxes": [ 836 | { 837 | "format": "short", 838 | "label": null, 839 | "logBase": 1, 840 | "max": null, 841 | "min": null, 842 | "show": true 843 | }, 844 | { 845 | "format": "short", 846 | "label": null, 847 | "logBase": 1, 848 | "max": null, 849 | "min": null, 850 | "show": true 851 | } 852 | ] 853 | }, 854 | { 855 | "aliasColors": {}, 856 | "bars": false, 857 | "dashLength": 10, 858 | "dashes": false, 859 | "datasource": "${DS_PROMETHEUS}", 860 | "fill": 1, 861 | "id": 10, 862 | "legend": { 863 | "avg": false, 864 | "current": false, 865 | "max": false, 866 | "min": false, 867 | "show": true, 868 | "total": false, 869 | "values": false 870 | }, 871 | "lines": true, 872 | "linewidth": 1, 873 | "links": [], 874 | "nullPointMode": "null", 875 | "percentage": false, 876 | "pointradius": 5, 877 | "points": false, 878 | "renderer": "flot", 879 | "seriesOverrides": [], 880 | "spaceLength": 10, 881 | "span": 3, 882 | "stack": false, 883 | "steppedLine": false, 884 | "targets": [ 885 | { 886 | "expr": "sum(irate(vertx_eventbus_sent_total[1m]))", 887 | "format": "time_series", 888 | "intervalFactor": 2, 889 | "legendFormat": "1 minute instant rate", 890 | "refId": "A" 891 | }, 892 | { 893 | "expr": "sum(rate(vertx_eventbus_sent_total[1m]))", 894 | "format": "time_series", 895 | "intervalFactor": 2, 896 | "legendFormat": "1 minute rate", 897 | "refId": "C" 898 | }, 899 | { 900 | "expr": "sum(rate(vertx_eventbus_sent_total[5m]))", 901 | "format": "time_series", 902 | "intervalFactor": 2, 903 | "legendFormat": "5 minute rate", 904 | "refId": "B" 905 | } 906 | ], 907 | "thresholds": [], 908 | "timeFrom": null, 909 | "timeShift": null, 910 | "title": "Eventbus: sent rate", 911 | "tooltip": { 912 | "shared": true, 913 | "sort": 0, 914 | "value_type": "individual" 915 | }, 916 | "type": "graph", 917 | "xaxis": { 918 | "buckets": null, 919 | "mode": "time", 920 | "name": null, 921 | "show": true, 922 | "values": [] 923 | }, 924 | "yaxes": [ 925 | { 926 | "format": "ops", 927 | "label": null, 928 | "logBase": 1, 929 | "max": null, 930 | "min": null, 931 | "show": true 932 | }, 933 | { 934 | "format": "short", 935 | "label": null, 936 | "logBase": 1, 937 | "max": null, 938 | "min": null, 939 | "show": true 940 | } 941 | ] 942 | }, 943 | { 944 | "aliasColors": {}, 945 | "bars": false, 946 | "dashLength": 10, 947 | "dashes": false, 948 | "datasource": "${DS_PROMETHEUS}", 949 | "fill": 1, 950 | "id": 15, 951 | "legend": { 952 | "avg": false, 953 | "current": false, 954 | "max": false, 955 | "min": false, 956 | "show": true, 957 | "total": false, 958 | "values": false 959 | }, 960 | "lines": true, 961 | "linewidth": 1, 962 | "links": [], 963 | "nullPointMode": "null", 964 | "percentage": false, 965 | "pointradius": 5, 966 | "points": false, 967 | "renderer": "flot", 968 | "seriesOverrides": [], 969 | "spaceLength": 10, 970 | "span": 3, 971 | "stack": false, 972 | "steppedLine": false, 973 | "targets": [ 974 | { 975 | "expr": "sum(irate(vertx_eventbus_received_total[1m]))", 976 | "format": "time_series", 977 | "intervalFactor": 2, 978 | "legendFormat": "1 minute instant rate", 979 | "refId": "A" 980 | }, 981 | { 982 | "expr": "sum(rate(vertx_eventbus_received_total[1m]))", 983 | "format": "time_series", 984 | "intervalFactor": 2, 985 | "legendFormat": "1 minute rate", 986 | "refId": "C" 987 | }, 988 | { 989 | "expr": "sum(rate(vertx_eventbus_received_total[5m]))", 990 | "format": "time_series", 991 | "intervalFactor": 2, 992 | "legendFormat": "5 minute rate", 993 | "refId": "B" 994 | } 995 | ], 996 | "thresholds": [], 997 | "timeFrom": null, 998 | "timeShift": null, 999 | "title": "Eventbus: received rate", 1000 | "tooltip": { 1001 | "shared": true, 1002 | "sort": 0, 1003 | "value_type": "individual" 1004 | }, 1005 | "type": "graph", 1006 | "xaxis": { 1007 | "buckets": null, 1008 | "mode": "time", 1009 | "name": null, 1010 | "show": true, 1011 | "values": [] 1012 | }, 1013 | "yaxes": [ 1014 | { 1015 | "format": "ops", 1016 | "label": null, 1017 | "logBase": 1, 1018 | "max": null, 1019 | "min": null, 1020 | "show": true 1021 | }, 1022 | { 1023 | "format": "short", 1024 | "label": null, 1025 | "logBase": 1, 1026 | "max": null, 1027 | "min": null, 1028 | "show": true 1029 | } 1030 | ] 1031 | } 1032 | ], 1033 | "repeat": null, 1034 | "repeatIteration": null, 1035 | "repeatRowId": null, 1036 | "showTitle": false, 1037 | "title": "Dashboard Row", 1038 | "titleSize": "h6" 1039 | }, 1040 | { 1041 | "collapse": false, 1042 | "height": 250, 1043 | "panels": [ 1044 | { 1045 | "aliasColors": {}, 1046 | "bars": false, 1047 | "dashLength": 10, 1048 | "dashes": false, 1049 | "datasource": "${DS_PROMETHEUS}", 1050 | "fill": 1, 1051 | "id": 12, 1052 | "legend": { 1053 | "avg": false, 1054 | "current": false, 1055 | "max": false, 1056 | "min": false, 1057 | "show": true, 1058 | "total": false, 1059 | "values": false 1060 | }, 1061 | "lines": true, 1062 | "linewidth": 1, 1063 | "links": [], 1064 | "nullPointMode": "null", 1065 | "percentage": false, 1066 | "pointradius": 5, 1067 | "points": false, 1068 | "renderer": "flot", 1069 | "seriesOverrides": [], 1070 | "spaceLength": 10, 1071 | "span": 6, 1072 | "stack": false, 1073 | "steppedLine": false, 1074 | "targets": [ 1075 | { 1076 | "expr": "sum(irate(vertx_eventbus_processingTime_seconds_sum[1m])) / sum(irate(vertx_eventbus_processingTime_seconds_count[1m]))", 1077 | "format": "time_series", 1078 | "intervalFactor": 2, 1079 | "legendFormat": "[avg]", 1080 | "refId": "A" 1081 | }, 1082 | { 1083 | "expr": "histogram_quantile(0.5, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le))", 1084 | "format": "time_series", 1085 | "intervalFactor": 2, 1086 | "legendFormat": "[med]", 1087 | "refId": "B" 1088 | }, 1089 | { 1090 | "expr": "histogram_quantile(0.95, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le))", 1091 | "format": "time_series", 1092 | "intervalFactor": 2, 1093 | "legendFormat": "[95p]", 1094 | "refId": "C" 1095 | }, 1096 | { 1097 | "expr": "histogram_quantile(0.99, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le))", 1098 | "format": "time_series", 1099 | "intervalFactor": 2, 1100 | "legendFormat": "[99p]", 1101 | "refId": "D" 1102 | } 1103 | ], 1104 | "thresholds": [], 1105 | "timeFrom": null, 1106 | "timeShift": null, 1107 | "title": "Eventbus processing time", 1108 | "tooltip": { 1109 | "shared": true, 1110 | "sort": 0, 1111 | "value_type": "individual" 1112 | }, 1113 | "type": "graph", 1114 | "xaxis": { 1115 | "buckets": null, 1116 | "mode": "time", 1117 | "name": null, 1118 | "show": true, 1119 | "values": [] 1120 | }, 1121 | "yaxes": [ 1122 | { 1123 | "format": "s", 1124 | "label": null, 1125 | "logBase": 1, 1126 | "max": null, 1127 | "min": null, 1128 | "show": true 1129 | }, 1130 | { 1131 | "format": "short", 1132 | "label": null, 1133 | "logBase": 1, 1134 | "max": null, 1135 | "min": null, 1136 | "show": true 1137 | } 1138 | ] 1139 | }, 1140 | { 1141 | "aliasColors": {}, 1142 | "bars": false, 1143 | "dashLength": 10, 1144 | "dashes": false, 1145 | "datasource": "${DS_PROMETHEUS}", 1146 | "fill": 1, 1147 | "id": 16, 1148 | "legend": { 1149 | "avg": false, 1150 | "current": false, 1151 | "max": false, 1152 | "min": false, 1153 | "show": true, 1154 | "total": false, 1155 | "values": false 1156 | }, 1157 | "lines": true, 1158 | "linewidth": 1, 1159 | "links": [], 1160 | "nullPointMode": "null", 1161 | "percentage": false, 1162 | "pointradius": 5, 1163 | "points": false, 1164 | "renderer": "flot", 1165 | "seriesOverrides": [], 1166 | "spaceLength": 10, 1167 | "span": 6, 1168 | "stack": false, 1169 | "steppedLine": false, 1170 | "targets": [ 1171 | { 1172 | "expr": "sum(irate(vertx_eventbus_processingTime_seconds_sum[1m])) by (address) / sum(irate(vertx_eventbus_processingTime_seconds_count[1m])) by (address)", 1173 | "format": "time_series", 1174 | "intervalFactor": 2, 1175 | "legendFormat": "{{address}} [avg]", 1176 | "refId": "A" 1177 | }, 1178 | { 1179 | "expr": "histogram_quantile(0.5, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le,address))", 1180 | "format": "time_series", 1181 | "intervalFactor": 2, 1182 | "legendFormat": "{{address}} [med]", 1183 | "refId": "B" 1184 | }, 1185 | { 1186 | "expr": "histogram_quantile(0.95, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le,address))", 1187 | "format": "time_series", 1188 | "intervalFactor": 2, 1189 | "legendFormat": "{{address}} [95p]", 1190 | "refId": "C" 1191 | }, 1192 | { 1193 | "expr": "histogram_quantile(0.99, sum(rate(vertx_eventbus_processingTime_seconds_bucket[1m])) by (le,address))", 1194 | "format": "time_series", 1195 | "intervalFactor": 2, 1196 | "legendFormat": "{{address}} [99p]", 1197 | "refId": "D" 1198 | } 1199 | ], 1200 | "thresholds": [], 1201 | "timeFrom": null, 1202 | "timeShift": null, 1203 | "title": "Eventbus processing time by address", 1204 | "tooltip": { 1205 | "shared": true, 1206 | "sort": 0, 1207 | "value_type": "individual" 1208 | }, 1209 | "type": "graph", 1210 | "xaxis": { 1211 | "buckets": null, 1212 | "mode": "time", 1213 | "name": null, 1214 | "show": true, 1215 | "values": [] 1216 | }, 1217 | "yaxes": [ 1218 | { 1219 | "format": "s", 1220 | "label": null, 1221 | "logBase": 1, 1222 | "max": null, 1223 | "min": null, 1224 | "show": true 1225 | }, 1226 | { 1227 | "format": "short", 1228 | "label": null, 1229 | "logBase": 1, 1230 | "max": null, 1231 | "min": null, 1232 | "show": true 1233 | } 1234 | ] 1235 | } 1236 | ], 1237 | "repeat": null, 1238 | "repeatIteration": null, 1239 | "repeatRowId": null, 1240 | "showTitle": false, 1241 | "title": "Dashboard Row", 1242 | "titleSize": "h6" 1243 | } 1244 | ], 1245 | "schemaVersion": 14, 1246 | "style": "dark", 1247 | "tags": [], 1248 | "templating": { 1249 | "list": [] 1250 | }, 1251 | "time": { 1252 | "from": "now-5m", 1253 | "to": "now" 1254 | }, 1255 | "timepicker": { 1256 | "refresh_intervals": [ 1257 | "5s", 1258 | "10s", 1259 | "30s", 1260 | "1m", 1261 | "5m", 1262 | "15m", 1263 | "30m", 1264 | "1h", 1265 | "2h", 1266 | "1d" 1267 | ], 1268 | "time_options": [ 1269 | "5m", 1270 | "15m", 1271 | "1h", 1272 | "6h", 1273 | "12h", 1274 | "24h", 1275 | "2d", 1276 | "7d", 1277 | "30d" 1278 | ] 1279 | }, 1280 | "timezone": "", 1281 | "title": "Vertx-Prom", 1282 | "version": 18 1283 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.limadelrey 8 | vertx-4-reactive-rest-api 9 | 0.1-SNAPSHOT 10 | 11 | 12 | UTF-8 13 | 14 | 4.0.0 15 | 5.7.0 16 | 2.12.0 17 | 7.5.0 18 | 42.2.18 19 | 1.6.3 20 | 1.15.1 21 | 22 | 3.8.1 23 | 3.2.4 24 | 2.22.2 25 | 0.8.6 26 | 27 | org.limadelrey.vertx4.reactive.rest.api.verticle.MainVerticle 28 | io.vertx.core.Launcher 29 | 30 | 31 | 32 | 33 | 34 | io.vertx 35 | vertx-stack-depchain 36 | ${vertx.version} 37 | pom 38 | import 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | io.vertx 47 | vertx-web 48 | 49 | 50 | 51 | 52 | io.vertx 53 | vertx-web-validation 54 | 55 | 56 | 57 | 58 | io.vertx 59 | vertx-pg-client 60 | 61 | 62 | 63 | 64 | io.vertx 65 | vertx-sql-client-templates 66 | 67 | 68 | 69 | 70 | org.flywaydb 71 | flyway-core 72 | ${flyway.version} 73 | 74 | 75 | 76 | 77 | org.postgresql 78 | postgresql 79 | ${postgresql.version} 80 | 81 | 82 | 83 | 84 | io.vertx 85 | vertx-config 86 | 87 | 88 | 89 | 90 | io.vertx 91 | vertx-health-check 92 | 93 | 94 | 95 | 96 | io.vertx 97 | vertx-micrometer-metrics 98 | 99 | 100 | 101 | 102 | com.fasterxml.jackson.core 103 | jackson-databind 104 | ${jackson.version} 105 | 106 | 107 | 108 | 109 | com.fasterxml.jackson.core 110 | jackson-core 111 | ${jackson.version} 112 | 113 | 114 | 115 | 116 | com.fasterxml.jackson.core 117 | jackson-annotations 118 | ${jackson.version} 119 | 120 | 121 | 122 | 123 | io.micrometer 124 | micrometer-registry-prometheus 125 | ${micrometer.version} 126 | 127 | 128 | 129 | 130 | io.vertx 131 | vertx-junit5 132 | test 133 | 134 | 135 | 136 | 137 | io.vertx 138 | vertx-web-client 139 | test 140 | 141 | 142 | 143 | 144 | org.junit.jupiter 145 | junit-jupiter-api 146 | ${junit-jupiter.version} 147 | test 148 | 149 | 150 | 151 | 152 | org.junit.jupiter 153 | junit-jupiter-engine 154 | ${junit-jupiter.version} 155 | test 156 | 157 | 158 | 159 | 160 | org.junit.vintage 161 | junit-vintage-engine 162 | ${junit-jupiter.version} 163 | test 164 | 165 | 166 | 167 | 168 | org.testcontainers 169 | testcontainers 170 | ${testcontainers.version} 171 | test 172 | 173 | 174 | 175 | 176 | org.testcontainers 177 | junit-jupiter 178 | ${testcontainers.version} 179 | 180 | 181 | 182 | 183 | org.testcontainers 184 | postgresql 185 | ${testcontainers.version} 186 | test 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | maven-compiler-plugin 195 | ${maven-compiler-plugin.version} 196 | 197 | 11 198 | 199 | 200 | 201 | 202 | 203 | maven-shade-plugin 204 | ${maven-shade-plugin.version} 205 | 206 | 207 | package 208 | 209 | shade 210 | 211 | 212 | 213 | 215 | 216 | ${launcher.class} 217 | ${main.verticle} 218 | 219 | 220 | 222 | 223 | 224 | 225 | target/vertx-4-reactive-rest-api-0.1-SNAPSHOT-fat.jar 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | maven-surefire-plugin 235 | ${maven-surefire-plugin.version} 236 | 237 | 238 | 239 | 240 | org.jacoco 241 | jacoco-maven-plugin 242 | ${jacoco-plugin.version} 243 | 244 | 245 | 246 | prepare-agent 247 | 248 | 249 | 250 | 251 | report 252 | test 253 | 254 | report 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "5cfc01d3-fd7d-4527-a2b9-825ad368ff80", 4 | "name": "vertx-4-reactive-rest-api", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "Read all books.", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "http://localhost:8888/api/v1/books?", 15 | "protocol": "http", 16 | "host": [ 17 | "localhost" 18 | ], 19 | "port": "8888", 20 | "path": [ 21 | "api", 22 | "v1", 23 | "books" 24 | ], 25 | "query": [ 26 | { 27 | "key": "page", 28 | "value": "1", 29 | "disabled": true 30 | }, 31 | { 32 | "key": "limit", 33 | "value": "20", 34 | "disabled": true 35 | } 36 | ] 37 | } 38 | }, 39 | "response": [] 40 | }, 41 | { 42 | "name": "Read one book.", 43 | "request": { 44 | "method": "GET", 45 | "header": [], 46 | "url": { 47 | "raw": "http://localhost:8888/api/v1/books/1", 48 | "protocol": "http", 49 | "host": [ 50 | "localhost" 51 | ], 52 | "port": "8888", 53 | "path": [ 54 | "api", 55 | "v1", 56 | "books", 57 | "1" 58 | ] 59 | } 60 | }, 61 | "response": [] 62 | }, 63 | { 64 | "name": "Create book.", 65 | "request": { 66 | "method": "POST", 67 | "header": [ 68 | { 69 | "key": "Content-Type", 70 | "name": "Content-Type", 71 | "value": "application/json", 72 | "type": "text" 73 | } 74 | ], 75 | "body": { 76 | "mode": "raw", 77 | "raw": "{\n \"author\": \"José Saramago\",\n \"country\": \"Portugal\",\n \"image_link\": \"images/ensaio-sobre-a-cegueira.jpg\",\n \"language\": \"Portuguese\",\n \"link\": \"https://en.wikipedia.org/wiki/Blindness_(novel)\",\n \"pages\": 288,\n \"title\": \"Ensaio sobre a cegueira\",\n \"year\": 1995\n }", 78 | "options": { 79 | "raw": { 80 | "language": "json" 81 | } 82 | } 83 | }, 84 | "url": { 85 | "raw": "http://localhost:8888/api/v1/books", 86 | "protocol": "http", 87 | "host": [ 88 | "localhost" 89 | ], 90 | "port": "8888", 91 | "path": [ 92 | "api", 93 | "v1", 94 | "books" 95 | ] 96 | } 97 | }, 98 | "response": [] 99 | }, 100 | { 101 | "name": "Update book.", 102 | "request": { 103 | "method": "PUT", 104 | "header": [ 105 | { 106 | "key": "Content-Type", 107 | "name": "Content-Type", 108 | "type": "text", 109 | "value": "application/json" 110 | } 111 | ], 112 | "body": { 113 | "mode": "raw", 114 | "raw": "{\n \"author\": \"José Saramago\",\n \"country\": \"Portugal\",\n \"image_link\": \"images/ensaio-sobre-a-cegueira.jpg\",\n \"language\": \"Portuguese\",\n \"link\": \"https://en.wikipedia.org/wiki/Blindness_(novel)\",\n \"pages\": 288,\n \"title\": \"Ensaio sobre a cegueira\",\n \"year\": 1995\n }", 115 | "options": { 116 | "raw": { 117 | "language": "json" 118 | } 119 | } 120 | }, 121 | "url": { 122 | "raw": "http://localhost:8888/api/v1/books/1", 123 | "protocol": "http", 124 | "host": [ 125 | "localhost" 126 | ], 127 | "port": "8888", 128 | "path": [ 129 | "api", 130 | "v1", 131 | "books", 132 | "1" 133 | ] 134 | } 135 | }, 136 | "response": [] 137 | }, 138 | { 139 | "name": "Delete book.", 140 | "request": { 141 | "method": "DELETE", 142 | "header": [], 143 | "url": { 144 | "raw": "http://localhost:8888/api/v1/books/1", 145 | "protocol": "http", 146 | "host": [ 147 | "localhost" 148 | ], 149 | "port": "8888", 150 | "path": [ 151 | "api", 152 | "v1", 153 | "books", 154 | "1" 155 | ] 156 | } 157 | }, 158 | "response": [] 159 | }, 160 | { 161 | "name": "Read metrics.", 162 | "request": { 163 | "method": "GET", 164 | "header": [], 165 | "url": { 166 | "raw": "http://localhost:8888/metrics", 167 | "protocol": "http", 168 | "host": [ 169 | "localhost" 170 | ], 171 | "port": "8888", 172 | "path": [ 173 | "metrics" 174 | ] 175 | } 176 | }, 177 | "response": [] 178 | }, 179 | { 180 | "name": "Read health check.", 181 | "request": { 182 | "method": "GET", 183 | "header": [], 184 | "url": { 185 | "raw": "http://localhost:8888/health", 186 | "protocol": "http", 187 | "host": [ 188 | "localhost" 189 | ], 190 | "port": "8888", 191 | "path": [ 192 | "health" 193 | ] 194 | } 195 | }, 196 | "response": [] 197 | } 198 | ], 199 | "protocolProfileBehavior": {} 200 | } -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 60s 3 | 4 | scrape_configs: 5 | - job_name: 'books' 6 | scrape_interval: 15s 7 | metrics_path: /metrics 8 | static_configs: 9 | - targets: ['host.docker.internal:8888'] -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/Main.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api; 2 | 3 | import io.micrometer.core.instrument.binder.system.UptimeMetrics; 4 | import io.micrometer.prometheus.PrometheusConfig; 5 | import io.micrometer.prometheus.PrometheusMeterRegistry; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.core.VertxOptions; 8 | import io.vertx.micrometer.MicrometerMetricsOptions; 9 | import io.vertx.micrometer.VertxPrometheusOptions; 10 | import org.limadelrey.vertx4.reactive.rest.api.verticle.MainVerticle; 11 | 12 | public class Main { 13 | 14 | public static void main(String[] args) { 15 | System.setProperty("vertx.logger-delegate-factory-class-name", "io.vertx.core.logging.SLF4JLogDelegateFactory"); 16 | 17 | PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); 18 | new UptimeMetrics().bindTo(registry); 19 | 20 | final Vertx vertx = Vertx.vertx(new VertxOptions().setMetricsOptions( 21 | new MicrometerMetricsOptions() 22 | .setPrometheusOptions(new VertxPrometheusOptions().setEnabled(true)) 23 | .setJvmMetricsEnabled(true) 24 | .setMicrometerRegistry(registry) 25 | .setEnabled(true))); 26 | 27 | vertx.deployVerticle(MainVerticle.class.getName()) 28 | .onFailure(throwable -> System.exit(-1)); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/handler/BookHandler.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.handler; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.ext.web.RoutingContext; 5 | import org.limadelrey.vertx4.reactive.rest.api.api.model.Book; 6 | import org.limadelrey.vertx4.reactive.rest.api.api.model.BookGetAllResponse; 7 | import org.limadelrey.vertx4.reactive.rest.api.api.model.BookGetByIdResponse; 8 | import org.limadelrey.vertx4.reactive.rest.api.api.service.BookService; 9 | import org.limadelrey.vertx4.reactive.rest.api.utils.ResponseUtils; 10 | 11 | public class BookHandler { 12 | 13 | private static final String ID_PARAMETER = "id"; 14 | private static final String PAGE_PARAMETER = "page"; 15 | private static final String LIMIT_PARAMETER = "limit"; 16 | 17 | private final BookService bookService; 18 | 19 | public BookHandler(BookService bookService) { 20 | this.bookService = bookService; 21 | } 22 | 23 | /** 24 | * Read all books 25 | * It should return 200 OK in case of success 26 | * It should return 400 Bad Request, 404 Not Found or 500 Internal Server Error in case of failure 27 | * 28 | * @param rc Routing context 29 | * @return BookGetAllResponse 30 | */ 31 | public Future readAll(RoutingContext rc) { 32 | final String page = rc.queryParams().get(PAGE_PARAMETER); 33 | final String limit = rc.queryParams().get(LIMIT_PARAMETER); 34 | 35 | return bookService.readAll(page, limit) 36 | .onSuccess(success -> ResponseUtils.buildOkResponse(rc, success)) 37 | .onFailure(throwable -> ResponseUtils.buildErrorResponse(rc, throwable)); 38 | } 39 | 40 | /** 41 | * Read one book 42 | * It should return 200 OK in case of success 43 | * It should return 400 Bad Request, 404 Not Found or 500 Internal Server Error in case of failure 44 | * 45 | * @param rc Routing context 46 | * @return BookGetByIdResponse 47 | */ 48 | public Future readOne(RoutingContext rc) { 49 | final String id = rc.pathParam(ID_PARAMETER); 50 | 51 | return bookService.readOne(Integer.parseInt(id)) 52 | .onSuccess(success -> ResponseUtils.buildOkResponse(rc, success)) 53 | .onFailure(throwable -> ResponseUtils.buildErrorResponse(rc, throwable)); 54 | } 55 | 56 | /** 57 | * Create one book 58 | * It should return 201 Created in case of success 59 | * It should return 400 Bad Request, 404 Not Found or 500 Internal Server Error in case of failure 60 | * 61 | * @param rc Routing context 62 | * @return BookGetByIdResponse 63 | */ 64 | public Future create(RoutingContext rc) { 65 | final Book book = rc.getBodyAsJson().mapTo(Book.class); 66 | 67 | return bookService.create(book) 68 | .onSuccess(success -> ResponseUtils.buildCreatedResponse(rc, success)) 69 | .onFailure(throwable -> ResponseUtils.buildErrorResponse(rc, throwable)); 70 | } 71 | 72 | /** 73 | * Update one book 74 | * It should return 200 OK in case of success 75 | * It should return 400 Bad Request, 404 Not Found or 500 Internal Server Error in case of failure 76 | * 77 | * @param rc Routing context 78 | * @return BookGetByIdResponse 79 | */ 80 | public Future update(RoutingContext rc) { 81 | final String id = rc.pathParam(ID_PARAMETER); 82 | final Book book = rc.getBodyAsJson().mapTo(Book.class); 83 | 84 | return bookService.update(Integer.parseInt(id), book) 85 | .onSuccess(success -> ResponseUtils.buildOkResponse(rc, success)) 86 | .onFailure(throwable -> ResponseUtils.buildErrorResponse(rc, throwable)); 87 | } 88 | 89 | /** 90 | * Delete one book 91 | * It should return 204 No Content in case of success 92 | * It should return 400 Bad Request, 404 Not Found or 500 Internal Server Error in case of failure 93 | * 94 | * @param rc Routing context 95 | * @return BookGetByIdResponse 96 | */ 97 | public Future delete(RoutingContext rc) { 98 | final String id = rc.pathParam(ID_PARAMETER); 99 | 100 | return bookService.delete(Integer.parseInt(id)) 101 | .onSuccess(success -> ResponseUtils.buildNoContentResponse(rc)) 102 | .onFailure(throwable -> ResponseUtils.buildErrorResponse(rc, throwable)); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/handler/BookValidationHandler.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.handler; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.ext.web.validation.RequestPredicate; 5 | import io.vertx.ext.web.validation.ValidationHandler; 6 | import io.vertx.ext.web.validation.builder.Bodies; 7 | import io.vertx.ext.web.validation.builder.ParameterProcessorFactory; 8 | import io.vertx.ext.web.validation.builder.Parameters; 9 | import io.vertx.json.schema.SchemaParser; 10 | import io.vertx.json.schema.SchemaRouter; 11 | import io.vertx.json.schema.SchemaRouterOptions; 12 | import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder; 13 | 14 | import static io.vertx.json.schema.common.dsl.Keywords.maxLength; 15 | import static io.vertx.json.schema.common.dsl.Keywords.minLength; 16 | import static io.vertx.json.schema.common.dsl.Schemas.*; 17 | import static io.vertx.json.schema.draft7.dsl.Keywords.maximum; 18 | 19 | public class BookValidationHandler { 20 | 21 | private final Vertx vertx; 22 | 23 | public BookValidationHandler(Vertx vertx) { 24 | this.vertx = vertx; 25 | } 26 | 27 | /** 28 | * Build read all books request validation 29 | * 30 | * @return ValidationHandler 31 | */ 32 | public ValidationHandler readAll() { 33 | final SchemaParser schemaParser = buildSchemaParser(); 34 | 35 | return ValidationHandler 36 | .builder(schemaParser) 37 | .queryParameter(buildPageQueryParameter()) 38 | .queryParameter(buildLimitQueryParameter()) 39 | .build(); 40 | } 41 | 42 | /** 43 | * Build read one book request validation 44 | * 45 | * @return ValidationHandler 46 | */ 47 | public ValidationHandler readOne() { 48 | final SchemaParser schemaParser = buildSchemaParser(); 49 | 50 | return ValidationHandler 51 | .builder(schemaParser) 52 | .pathParameter(buildIdPathParameter()) 53 | .build(); 54 | } 55 | 56 | /** 57 | * Build create one book request validation 58 | * 59 | * @return ValidationHandler 60 | */ 61 | public ValidationHandler create() { 62 | final SchemaParser schemaParser = buildSchemaParser(); 63 | final ObjectSchemaBuilder schemaBuilder = buildBodySchemaBuilder(); 64 | 65 | return ValidationHandler 66 | .builder(schemaParser) 67 | .predicate(RequestPredicate.BODY_REQUIRED) 68 | .body(Bodies.json(schemaBuilder)) 69 | .build(); 70 | } 71 | 72 | /** 73 | * Build update one book request validation 74 | * 75 | * @return ValidationHandler 76 | */ 77 | public ValidationHandler update() { 78 | final SchemaParser schemaParser = buildSchemaParser(); 79 | final ObjectSchemaBuilder schemaBuilder = buildBodySchemaBuilder(); 80 | 81 | return ValidationHandler 82 | .builder(schemaParser) 83 | .predicate(RequestPredicate.BODY_REQUIRED) 84 | .body(Bodies.json(schemaBuilder)) 85 | .pathParameter(buildIdPathParameter()) 86 | .build(); 87 | } 88 | 89 | /** 90 | * Build delete one book request validation 91 | * 92 | * @return ValidationHandler 93 | */ 94 | public ValidationHandler delete() { 95 | final SchemaParser schemaParser = buildSchemaParser(); 96 | 97 | return ValidationHandler 98 | .builder(schemaParser) 99 | .pathParameter(buildIdPathParameter()) 100 | .build(); 101 | } 102 | 103 | private SchemaParser buildSchemaParser() { 104 | return SchemaParser.createDraft7SchemaParser(SchemaRouter.create(vertx, new SchemaRouterOptions())); 105 | } 106 | 107 | private ObjectSchemaBuilder buildBodySchemaBuilder() { 108 | return objectSchema() 109 | .requiredProperty("author", stringSchema().with(minLength(1)).with(maxLength(255))) 110 | .requiredProperty("country", stringSchema().with(minLength(1)).with(maxLength(255)).nullable()) 111 | .requiredProperty("image_link", stringSchema().with(minLength(1)).with(maxLength(255)).nullable()) 112 | .requiredProperty("language", stringSchema().with(minLength(1)).with(maxLength(255)).nullable()) 113 | .requiredProperty("link", stringSchema().with(minLength(1)).with(maxLength(255)).nullable()) 114 | .requiredProperty("pages", intSchema().with(maximum(10000)).nullable()) 115 | .requiredProperty("title", stringSchema().with(minLength(1)).with(maxLength(255))) 116 | .requiredProperty("year", intSchema().with(maximum(10000)).nullable()); 117 | } 118 | 119 | private ParameterProcessorFactory buildIdPathParameter() { 120 | return Parameters.param("id", intSchema()); 121 | } 122 | 123 | private ParameterProcessorFactory buildPageQueryParameter() { 124 | return Parameters.optionalParam("page", intSchema()); 125 | } 126 | 127 | private ParameterProcessorFactory buildLimitQueryParameter() { 128 | return Parameters.optionalParam("limit", intSchema()); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/handler/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.handler; 2 | 3 | import io.vertx.ext.web.Router; 4 | import io.vertx.ext.web.validation.BadRequestException; 5 | import io.vertx.ext.web.validation.BodyProcessorException; 6 | import io.vertx.ext.web.validation.ParameterProcessorException; 7 | import io.vertx.ext.web.validation.RequestPredicateException; 8 | import org.limadelrey.vertx4.reactive.rest.api.utils.ResponseUtils; 9 | 10 | public class ErrorHandler { 11 | 12 | private ErrorHandler() { 13 | 14 | } 15 | 16 | /** 17 | * Build error handler 18 | * It's useful to handle errors thrown by Web Validation 19 | * 20 | * @param router Router 21 | */ 22 | public static void buildHandler(Router router) { 23 | router.errorHandler(400, rc -> { 24 | if (rc.failure() instanceof BadRequestException) { 25 | if (rc.failure() instanceof ParameterProcessorException) { 26 | // Something went wrong while parsing/validating a parameter 27 | ResponseUtils.buildErrorResponse(rc, new IllegalArgumentException("Path parameter is invalid")); 28 | } else if (rc.failure() instanceof BodyProcessorException | rc.failure() instanceof RequestPredicateException) { 29 | // Something went wrong while parsing/validating the body 30 | ResponseUtils.buildErrorResponse(rc, new IllegalArgumentException("Request body is invalid")); 31 | } 32 | } 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/model/Book.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | public class Book implements Serializable { 9 | 10 | private static final long serialVersionUID = 1169010391380979103L; 11 | 12 | @JsonProperty(value = "id") 13 | private int id; 14 | 15 | @JsonProperty(value = "author") 16 | private String author; 17 | 18 | @JsonProperty(value = "country") 19 | private String country; 20 | 21 | @JsonProperty(value = "image_link") 22 | private String imageLink; 23 | 24 | @JsonProperty(value = "language") 25 | private String language; 26 | 27 | @JsonProperty(value = "link") 28 | private String link; 29 | 30 | @JsonProperty(value = "pages") 31 | private Integer pages; 32 | 33 | @JsonProperty(value = "title") 34 | private String title; 35 | 36 | @JsonProperty(value = "year") 37 | private Integer year; 38 | 39 | public int getId() { return id; 40 | } 41 | 42 | public void setId(int id) { 43 | this.id = id; 44 | } 45 | 46 | public String getAuthor() { 47 | return author; 48 | } 49 | 50 | public void setAuthor(String author) { 51 | this.author = author; 52 | } 53 | 54 | public String getCountry() { 55 | return country; 56 | } 57 | 58 | public void setCountry(String country) { 59 | this.country = country; 60 | } 61 | 62 | public String getImageLink() { 63 | return imageLink; 64 | } 65 | 66 | public void setImageLink(String imageLink) { 67 | this.imageLink = imageLink; 68 | } 69 | 70 | public String getLanguage() { 71 | return language; 72 | } 73 | 74 | public void setLanguage(String language) { 75 | this.language = language; 76 | } 77 | 78 | public String getLink() { 79 | return link; 80 | } 81 | 82 | public void setLink(String link) { 83 | this.link = link; 84 | } 85 | 86 | public Integer getPages() { 87 | return pages; 88 | } 89 | 90 | public void setPages(Integer pages) { 91 | this.pages = pages; 92 | } 93 | 94 | public String getTitle() { 95 | return title; 96 | } 97 | 98 | public void setTitle(String title) { 99 | this.title = title; 100 | } 101 | 102 | public Integer getYear() { 103 | return year; 104 | } 105 | 106 | public void setYear(Integer year) { 107 | this.year = year; 108 | } 109 | 110 | @Override 111 | public boolean equals(Object o) { 112 | if (this == o) return true; 113 | if (o == null || getClass() != o.getClass()) return false; 114 | Book book = (Book) o; 115 | return id == book.id; 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | return Objects.hash(id); 121 | } 122 | 123 | @Override 124 | public String toString() { 125 | return "Book{" + 126 | "id=" + id + 127 | ", author='" + author + '\'' + 128 | ", country='" + country + '\'' + 129 | ", imageLink='" + imageLink + '\'' + 130 | ", language='" + language + '\'' + 131 | ", link='" + link + '\'' + 132 | ", pages=" + pages + 133 | ", title='" + title + '\'' + 134 | ", year=" + year + 135 | '}'; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/model/BookGetAllResponse.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | import java.util.Objects; 8 | 9 | public class BookGetAllResponse implements Serializable { 10 | 11 | private static final long serialVersionUID = -8964658883487451260L; 12 | 13 | @JsonProperty(value = "total") 14 | private final int total; 15 | 16 | @JsonProperty(value = "limit") 17 | private final int limit; 18 | 19 | @JsonProperty(value = "page") 20 | private final int page; 21 | 22 | @JsonProperty(value = "books") 23 | private final List books; 24 | 25 | public BookGetAllResponse(int total, 26 | int limit, 27 | int page, 28 | List books) { 29 | this.total = total; 30 | this.limit = limit; 31 | this.page = page; 32 | this.books = books; 33 | } 34 | 35 | public int getTotal() { 36 | return total; 37 | } 38 | 39 | public int getLimit() { 40 | return limit; 41 | } 42 | 43 | public int getPage() { 44 | return page; 45 | } 46 | 47 | public List getBooks() { 48 | return books; 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | BookGetAllResponse that = (BookGetAllResponse) o; 56 | return total == that.total && 57 | limit == that.limit && 58 | page == that.page && 59 | books.equals(that.books); 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | return Objects.hash(total, limit, page, books); 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "BookGetAllResponse{" + 70 | "total=" + total + 71 | ", limit=" + limit + 72 | ", page=" + page + 73 | ", books=" + books + 74 | '}'; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/model/BookGetByIdResponse.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | public class BookGetByIdResponse implements Serializable { 9 | 10 | private static final long serialVersionUID = 7621071075786169611L; 11 | 12 | @JsonProperty(value = "id") 13 | private final int id; 14 | 15 | @JsonProperty(value = "author") 16 | private final String author; 17 | 18 | @JsonProperty(value = "country") 19 | private final String country; 20 | 21 | @JsonProperty(value = "image_link") 22 | private final String imageLink; 23 | 24 | @JsonProperty(value = "language") 25 | private final String language; 26 | 27 | @JsonProperty(value = "link") 28 | private final String link; 29 | 30 | @JsonProperty(value = "pages") 31 | private final Integer pages; 32 | 33 | @JsonProperty(value = "title") 34 | private final String title; 35 | 36 | @JsonProperty(value = "year") 37 | private final Integer year; 38 | 39 | public BookGetByIdResponse(Book book) { 40 | this.id = book.getId(); 41 | this.author = book.getAuthor(); 42 | this.country = book.getCountry(); 43 | this.imageLink = book.getImageLink(); 44 | this.language = book.getLanguage(); 45 | this.link = book.getLink(); 46 | this.pages = book.getPages(); 47 | this.title = book.getTitle(); 48 | this.year = book.getYear(); 49 | } 50 | 51 | public int getId() { 52 | return id; 53 | } 54 | 55 | public String getAuthor() { 56 | return author; 57 | } 58 | 59 | public String getCountry() { 60 | return country; 61 | } 62 | 63 | public String getImageLink() { 64 | return imageLink; 65 | } 66 | 67 | public String getLanguage() { 68 | return language; 69 | } 70 | 71 | public String getLink() { 72 | return link; 73 | } 74 | 75 | public Integer getPages() { 76 | return pages; 77 | } 78 | 79 | public String getTitle() { 80 | return title; 81 | } 82 | 83 | public Integer getYear() { 84 | return year; 85 | } 86 | 87 | @Override 88 | public boolean equals(Object o) { 89 | if (this == o) return true; 90 | if (o == null || getClass() != o.getClass()) return false; 91 | BookGetByIdResponse that = (BookGetByIdResponse) o; 92 | return id == that.id; 93 | } 94 | 95 | @Override 96 | public int hashCode() { 97 | return Objects.hash(id); 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return "BookGetByIdResponse{" + 103 | "id=" + id + 104 | ", author='" + author + '\'' + 105 | ", country='" + country + '\'' + 106 | ", imageLink='" + imageLink + '\'' + 107 | ", language='" + language + '\'' + 108 | ", link='" + link + '\'' + 109 | ", pages=" + pages + 110 | ", title='" + title + '\'' + 111 | ", year=" + year + 112 | '}'; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.repository; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.impl.logging.Logger; 5 | import io.vertx.core.impl.logging.LoggerFactory; 6 | import io.vertx.sqlclient.RowIterator; 7 | import io.vertx.sqlclient.SqlConnection; 8 | import io.vertx.sqlclient.templates.RowMapper; 9 | import io.vertx.sqlclient.templates.SqlTemplate; 10 | import org.limadelrey.vertx4.reactive.rest.api.api.model.Book; 11 | import org.limadelrey.vertx4.reactive.rest.api.utils.LogUtils; 12 | 13 | import java.util.*; 14 | 15 | public class BookRepository { 16 | 17 | private static final Logger LOGGER = LoggerFactory.getLogger(BookRepository.class); 18 | 19 | private static final String SQL_SELECT_ALL = "SELECT * FROM books LIMIT #{limit} OFFSET #{offset}"; 20 | private static final String SQL_SELECT_BY_ID = "SELECT * FROM books WHERE id = #{id}"; 21 | private static final String SQL_INSERT = "INSERT INTO books (author, country, image_link, language, link, pages, title, year) " + 22 | "VALUES (#{author}, #{country}, #{image_link}, #{language}, #{link}, #{pages}, #{title}, #{year}) RETURNING id"; 23 | private static final String SQL_UPDATE = "UPDATE books SET author = #{author}, country = #{country}, image_link = #{image_link}, " + 24 | "language = #{language}, link = #{link}, pages = #{pages}, title = #{title}, year = #{year} WHERE id = #{id}"; 25 | private static final String SQL_DELETE = "DELETE FROM books WHERE id = #{id}"; 26 | private static final String SQL_COUNT = "SELECT COUNT(*) AS total FROM books"; 27 | 28 | public BookRepository() { 29 | } 30 | 31 | /** 32 | * Read all books using pagination 33 | * 34 | * @param connection PostgreSQL connection 35 | * @param limit Limit 36 | * @param offset Offset 37 | * @return List 38 | */ 39 | public Future> selectAll(SqlConnection connection, 40 | int limit, 41 | int offset) { 42 | return SqlTemplate 43 | .forQuery(connection, SQL_SELECT_ALL) 44 | .mapTo(Book.class) 45 | .execute(Map.of("limit", limit, "offset", offset)) 46 | .map(rowSet -> { 47 | final List books = new ArrayList<>(); 48 | rowSet.forEach(books::add); 49 | 50 | return books; 51 | }) 52 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Read all books", SQL_SELECT_ALL))) 53 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Read all books", throwable.getMessage()))); 54 | } 55 | 56 | /** 57 | * Read one book 58 | * 59 | * @param connection PostgreSQL connection 60 | * @param id Book ID 61 | * @return Book 62 | */ 63 | public Future selectById(SqlConnection connection, 64 | int id) { 65 | return SqlTemplate 66 | .forQuery(connection, SQL_SELECT_BY_ID) 67 | .mapTo(Book.class) 68 | .execute(Collections.singletonMap("id", id)) 69 | .map(rowSet -> { 70 | final RowIterator iterator = rowSet.iterator(); 71 | 72 | if (iterator.hasNext()) { 73 | return iterator.next(); 74 | } else { 75 | throw new NoSuchElementException(LogUtils.NO_BOOK_WITH_ID_MESSAGE.buildMessage(id)); 76 | } 77 | }) 78 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Read book by id", SQL_SELECT_BY_ID))) 79 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Read book by id", throwable.getMessage()))); 80 | } 81 | 82 | /** 83 | * Create one book 84 | * 85 | * @param connection PostgreSQL connection 86 | * @param book Book 87 | * @return Book 88 | */ 89 | public Future insert(SqlConnection connection, 90 | Book book) { 91 | return SqlTemplate 92 | .forUpdate(connection, SQL_INSERT) 93 | .mapFrom(Book.class) 94 | .mapTo(Book.class) 95 | .execute(book) 96 | .map(rowSet -> { 97 | final RowIterator iterator = rowSet.iterator(); 98 | 99 | if (iterator.hasNext()) { 100 | book.setId(iterator.next().getId()); 101 | return book; 102 | } else { 103 | throw new IllegalStateException(LogUtils.CANNOT_CREATE_BOOK_MESSAGE.buildMessage()); 104 | } 105 | }) 106 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Insert book", SQL_INSERT))) 107 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Insert book", throwable.getMessage()))); 108 | } 109 | 110 | /** 111 | * Update one book 112 | * 113 | * @param connection PostgreSQL connection 114 | * @param book Book 115 | * @return Book 116 | */ 117 | public Future update(SqlConnection connection, 118 | Book book) { 119 | return SqlTemplate 120 | .forUpdate(connection, SQL_UPDATE) 121 | .mapFrom(Book.class) 122 | .execute(book) 123 | .flatMap(rowSet -> { 124 | if (rowSet.rowCount() > 0) { 125 | return Future.succeededFuture(book); 126 | } else { 127 | throw new NoSuchElementException(LogUtils.NO_BOOK_WITH_ID_MESSAGE.buildMessage(book.getId())); 128 | } 129 | }) 130 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Update book", SQL_UPDATE))) 131 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Update book", throwable.getMessage()))); 132 | } 133 | 134 | /** 135 | * Update one book 136 | * 137 | * @param connection PostgreSQL connection 138 | * @param id Book ID 139 | * @return Void 140 | */ 141 | public Future delete(SqlConnection connection, 142 | int id) { 143 | return SqlTemplate 144 | .forUpdate(connection, SQL_DELETE) 145 | .execute(Collections.singletonMap("id", id)) 146 | .flatMap(rowSet -> { 147 | if (rowSet.rowCount() > 0) { 148 | LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Delete book", SQL_DELETE)); 149 | return Future.succeededFuture(); 150 | } else { 151 | LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Delete book", LogUtils.NO_BOOK_WITH_ID_MESSAGE.buildMessage(id))); 152 | throw new NoSuchElementException(LogUtils.NO_BOOK_WITH_ID_MESSAGE.buildMessage(id)); 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * Count all books 159 | * 160 | * @param connection PostgreSQL connection 161 | * @return Integer 162 | */ 163 | public Future count(SqlConnection connection) { 164 | final RowMapper ROW_MAPPER = row -> row.getInteger("total"); 165 | 166 | return SqlTemplate 167 | .forQuery(connection, SQL_COUNT) 168 | .mapTo(ROW_MAPPER) 169 | .execute(Collections.emptyMap()) 170 | .map(rowSet -> rowSet.iterator().next()) 171 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Count books", SQL_COUNT))) 172 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Count book", throwable.getMessage()))); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/router/BookRouter.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.router; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.ext.web.Router; 5 | import io.vertx.ext.web.handler.BodyHandler; 6 | import io.vertx.ext.web.handler.LoggerFormat; 7 | import io.vertx.ext.web.handler.LoggerHandler; 8 | import org.limadelrey.vertx4.reactive.rest.api.api.handler.BookHandler; 9 | import org.limadelrey.vertx4.reactive.rest.api.api.handler.BookValidationHandler; 10 | 11 | public class BookRouter { 12 | 13 | private final Vertx vertx; 14 | private final BookHandler bookHandler; 15 | private final BookValidationHandler bookValidationHandler; 16 | 17 | public BookRouter(Vertx vertx, 18 | BookHandler bookHandler, 19 | BookValidationHandler bookValidationHandler) { 20 | this.vertx = vertx; 21 | this.bookHandler = bookHandler; 22 | this.bookValidationHandler = bookValidationHandler; 23 | } 24 | 25 | /** 26 | * Set books API routes 27 | * 28 | * @param router Router 29 | */ 30 | public void setRouter(Router router) { 31 | router.mountSubRouter("/api/v1", buildBookRouter()); 32 | } 33 | 34 | /** 35 | * Build books API 36 | * All routes are composed by an error handler, a validation handler and the actual business logic handler 37 | */ 38 | private Router buildBookRouter() { 39 | final Router bookRouter = Router.router(vertx); 40 | 41 | bookRouter.route("/books*").handler(BodyHandler.create()); 42 | bookRouter.get("/books").handler(LoggerHandler.create(LoggerFormat.DEFAULT)).handler(bookValidationHandler.readAll()).handler(bookHandler::readAll); 43 | bookRouter.get("/books/:id").handler(LoggerHandler.create(LoggerFormat.DEFAULT)).handler(bookValidationHandler.readOne()).handler(bookHandler::readOne); 44 | bookRouter.post("/books").handler(LoggerHandler.create(LoggerFormat.DEFAULT)).handler(bookValidationHandler.create()).handler(bookHandler::create); 45 | bookRouter.put("/books/:id").handler(LoggerHandler.create(LoggerFormat.DEFAULT)).handler(bookValidationHandler.update()).handler(bookHandler::update); 46 | bookRouter.delete("/books/:id").handler(LoggerHandler.create(LoggerFormat.DEFAULT)).handler(bookValidationHandler.delete()).handler(bookHandler::delete); 47 | 48 | return bookRouter; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/router/HealthCheckRouter.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.router; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.ext.healthchecks.HealthCheckHandler; 5 | import io.vertx.ext.healthchecks.Status; 6 | import io.vertx.ext.web.Router; 7 | import io.vertx.pgclient.PgPool; 8 | 9 | public class HealthCheckRouter { 10 | 11 | private HealthCheckRouter() { 12 | 13 | } 14 | 15 | /** 16 | * Set health check routes 17 | * 18 | * @param vertx Vertx context 19 | * @param router Router 20 | * @param dbClient PostgreSQL pool 21 | */ 22 | public static void setRouter(Vertx vertx, 23 | Router router, 24 | PgPool dbClient) { 25 | final HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); 26 | 27 | healthCheckHandler.register("database", 28 | promise -> 29 | dbClient.getConnection(connection -> { 30 | if (connection.failed()) { 31 | promise.fail(connection.cause()); 32 | } else { 33 | connection.result().close(); 34 | promise.complete(Status.OK()); 35 | } 36 | }) 37 | ); 38 | 39 | router.get("/health").handler(healthCheckHandler); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/router/MetricsRouter.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.router; 2 | 3 | import io.vertx.ext.web.Router; 4 | import io.vertx.micrometer.PrometheusScrapingHandler; 5 | 6 | public class MetricsRouter { 7 | 8 | private MetricsRouter() { 9 | 10 | } 11 | 12 | /** 13 | * Set metrics routes 14 | * 15 | * @param router Router 16 | */ 17 | public static void setRouter(Router router) { 18 | router.route("/metrics").handler(PrometheusScrapingHandler.create()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/api/service/BookService.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.api.service; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.impl.logging.Logger; 5 | import io.vertx.core.impl.logging.LoggerFactory; 6 | import io.vertx.pgclient.PgPool; 7 | import org.limadelrey.vertx4.reactive.rest.api.api.model.Book; 8 | import org.limadelrey.vertx4.reactive.rest.api.api.model.BookGetAllResponse; 9 | import org.limadelrey.vertx4.reactive.rest.api.api.model.BookGetByIdResponse; 10 | import org.limadelrey.vertx4.reactive.rest.api.api.repository.BookRepository; 11 | import org.limadelrey.vertx4.reactive.rest.api.utils.LogUtils; 12 | import org.limadelrey.vertx4.reactive.rest.api.utils.QueryUtils; 13 | 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | 17 | public class BookService { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(BookService.class); 20 | 21 | private final PgPool dbClient; 22 | private final BookRepository bookRepository; 23 | 24 | public BookService(PgPool dbClient, 25 | BookRepository bookRepository) { 26 | this.dbClient = dbClient; 27 | this.bookRepository = bookRepository; 28 | } 29 | 30 | /** 31 | * Read all books using pagination 32 | * 33 | * @param p Page 34 | * @param l Limit 35 | * @return BookGetAllResponse 36 | */ 37 | public Future readAll(String p, 38 | String l) { 39 | return dbClient.withTransaction( 40 | connection -> { 41 | final int page = QueryUtils.getPage(p); 42 | final int limit = QueryUtils.getLimit(l); 43 | final int offset = QueryUtils.getOffset(page, limit); 44 | 45 | return bookRepository.count(connection) 46 | .flatMap(total -> 47 | bookRepository.selectAll(connection, limit, offset) 48 | .map(result -> { 49 | final List books = result.stream() 50 | .map(BookGetByIdResponse::new) 51 | .collect(Collectors.toList()); 52 | 53 | return new BookGetAllResponse(total, limit, page, books); 54 | }) 55 | ); 56 | }) 57 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Read all books", success.getBooks()))) 58 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Read all books", throwable.getMessage()))); 59 | } 60 | 61 | /** 62 | * Read one book 63 | * 64 | * @param id Book ID 65 | * @return BookGetByIdResponse 66 | */ 67 | public Future readOne(int id) { 68 | return dbClient.withTransaction( 69 | connection -> { 70 | return bookRepository.selectById(connection, id) 71 | .map(BookGetByIdResponse::new); 72 | }) 73 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Read one book", success))) 74 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Read one book", throwable.getMessage()))); 75 | } 76 | 77 | /** 78 | * Create one book 79 | * 80 | * @param book Book 81 | * @return BookGetByIdResponse 82 | */ 83 | public Future create(Book book) { 84 | return dbClient.withTransaction( 85 | connection -> { 86 | return bookRepository.insert(connection, book) 87 | .map(BookGetByIdResponse::new); 88 | }) 89 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Create one book", success))) 90 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Create one book", throwable.getMessage()))); 91 | } 92 | 93 | /** 94 | * Update one book 95 | * 96 | * @param id Book ID 97 | * @param book Book 98 | * @return BookGetByIdResponse 99 | */ 100 | public Future update(int id, 101 | Book book) { 102 | book.setId(id); 103 | 104 | return dbClient.withTransaction( 105 | connection -> { 106 | return bookRepository.update(connection, book) 107 | .map(BookGetByIdResponse::new); 108 | }) 109 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Update one book", success))) 110 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Update one book", throwable.getMessage()))); 111 | } 112 | 113 | /** 114 | * Delete one book 115 | * 116 | * @param id Book ID 117 | * @return Void 118 | */ 119 | public Future delete(Integer id) { 120 | return dbClient.withTransaction( 121 | connection -> { 122 | return bookRepository.delete(connection, id); 123 | }) 124 | .onSuccess(success -> LOGGER.info(LogUtils.REGULAR_CALL_SUCCESS_MESSAGE.buildMessage("Delete one book", id))) 125 | .onFailure(throwable -> LOGGER.error(LogUtils.REGULAR_CALL_ERROR_MESSAGE.buildMessage("Delete one book", throwable.getMessage()))); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/utils/ConfigUtils.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.utils; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.Properties; 6 | 7 | public class ConfigUtils { 8 | 9 | private static ConfigUtils instance; 10 | 11 | private final Properties properties; 12 | 13 | private ConfigUtils() { 14 | this.properties = readProperties(); 15 | } 16 | 17 | public static ConfigUtils getInstance() { 18 | if (instance == null) { 19 | instance = new ConfigUtils(); 20 | } 21 | 22 | return instance; 23 | } 24 | 25 | public Properties getProperties() { 26 | return properties; 27 | } 28 | 29 | /** 30 | * Read application.properties file on resources folder 31 | * 32 | * @return Properties 33 | */ 34 | private Properties readProperties() { 35 | Properties properties = new Properties(); 36 | 37 | try { 38 | try (InputStream is = getClass().getClassLoader().getResourceAsStream("application.properties")) { 39 | try { 40 | properties.load(is); 41 | } catch (IOException e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | } catch (IOException e) { 46 | e.printStackTrace(); 47 | } 48 | 49 | return properties; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/utils/DbUtils.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.utils; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.pgclient.PgConnectOptions; 5 | import io.vertx.pgclient.PgPool; 6 | import io.vertx.sqlclient.PoolOptions; 7 | import org.flywaydb.core.api.configuration.Configuration; 8 | import org.flywaydb.core.api.configuration.FluentConfiguration; 9 | 10 | import java.util.Properties; 11 | 12 | public class DbUtils { 13 | 14 | private static final String HOST_CONFIG = "datasource.host"; 15 | private static final String PORT_CONFIG = "datasource.port"; 16 | private static final String DATABASE_CONFIG = "datasource.database"; 17 | private static final String USERNAME_CONFIG = "datasource.username"; 18 | private static final String PASSWORD_CONFIG = "datasource.password"; 19 | 20 | private DbUtils() { 21 | 22 | } 23 | 24 | /** 25 | * Build DB client that is used to manage a pool of connections 26 | * 27 | * @param vertx Vertx context 28 | * @return PostgreSQL pool 29 | */ 30 | public static PgPool buildDbClient(Vertx vertx) { 31 | final Properties properties = ConfigUtils.getInstance().getProperties(); 32 | 33 | final PgConnectOptions connectOptions = new PgConnectOptions() 34 | .setPort(Integer.parseInt(properties.getProperty(PORT_CONFIG))) 35 | .setHost(properties.getProperty(HOST_CONFIG)) 36 | .setDatabase(properties.getProperty(DATABASE_CONFIG)) 37 | .setUser(properties.getProperty(USERNAME_CONFIG)) 38 | .setPassword(properties.getProperty(PASSWORD_CONFIG)); 39 | 40 | final PoolOptions poolOptions = new PoolOptions().setMaxSize(5); 41 | 42 | return PgPool.pool(vertx, connectOptions, poolOptions); 43 | } 44 | 45 | /** 46 | * Build Flyway configuration that is used to run migrations 47 | * 48 | * @return Flyway configuration 49 | */ 50 | public static Configuration buildMigrationsConfiguration() { 51 | final Properties properties = ConfigUtils.getInstance().getProperties(); 52 | 53 | final String url = "jdbc:postgresql://" + properties.getProperty(HOST_CONFIG) + ":" + properties.getProperty(PORT_CONFIG) + "/" + properties.getProperty(DATABASE_CONFIG); 54 | 55 | return new FluentConfiguration().dataSource(url, properties.getProperty(USERNAME_CONFIG), properties.getProperty(PASSWORD_CONFIG)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/utils/LogUtils.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.utils; 2 | 3 | public enum LogUtils { 4 | 5 | REGULAR_CALL_SUCCESS_MESSAGE("%s called w/ success - %s"), 6 | REGULAR_CALL_ERROR_MESSAGE("%s called w/ error - %s"), 7 | NO_BOOK_WITH_ID_MESSAGE("No book with id %d"), 8 | CANNOT_CREATE_BOOK_MESSAGE("Cannot create a new book"), 9 | RUN_HTTP_SERVER_SUCCESS_MESSAGE("HTTP server running on port %s"), 10 | RUN_HTTP_SERVER_ERROR_MESSAGE("Cannot run HTTP server"), 11 | NULL_OFFSET_ERROR_MESSAGE("Offset can't be null. Page %s and limit %s"), 12 | RUN_APP_SUCCESSFULLY_MESSAGE("vertx-4-reactive-rest-api started successfully in %d ms"); 13 | 14 | private final String message; 15 | 16 | LogUtils(final String message) { 17 | this.message = message; 18 | } 19 | 20 | public String buildMessage(Object... argument) { 21 | return String.format(message, argument); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/utils/QueryUtils.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.utils; 2 | 3 | public class QueryUtils { 4 | 5 | private static final int DEFAULT_PAGE = 1; 6 | private static final int DEFAULT_LIMIT = 20; 7 | 8 | private QueryUtils() { 9 | 10 | } 11 | 12 | /** 13 | * Calculate page value 14 | * 15 | * @param page Page 16 | * @return Sanitized page 17 | */ 18 | public static int getPage(String page) { 19 | return (page == null) 20 | ? DEFAULT_PAGE 21 | : Math.max(Integer.parseInt(page), DEFAULT_PAGE); 22 | } 23 | 24 | /** 25 | * Calculate limit value 26 | * 27 | * @param limit Limit 28 | * @return Sanitized limit 29 | */ 30 | public static int getLimit(String limit) { 31 | return (limit == null) 32 | ? DEFAULT_LIMIT 33 | : Math.min(Integer.parseInt(limit), DEFAULT_LIMIT); 34 | } 35 | 36 | /** 37 | * Calculate offset 38 | * 39 | * @param page Sanitized page 40 | * @param limit Sanitized limit 41 | * @return Offset 42 | */ 43 | public static int getOffset(int page, 44 | int limit) { 45 | if ((page - 1) * limit >= 0) { 46 | return (page - 1) * limit; 47 | } else { 48 | throw new NumberFormatException(LogUtils.NULL_OFFSET_ERROR_MESSAGE.buildMessage(page, limit)); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/utils/ResponseUtils.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.utils; 2 | 3 | import io.vertx.core.json.Json; 4 | import io.vertx.core.json.JsonObject; 5 | import io.vertx.ext.web.RoutingContext; 6 | 7 | import java.util.NoSuchElementException; 8 | 9 | public class ResponseUtils { 10 | 11 | private static final String CONTENT_TYPE_HEADER = "Content-Type"; 12 | private static final String APPLICATION_JSON = "application/json"; 13 | 14 | private ResponseUtils() { 15 | 16 | } 17 | 18 | /** 19 | * Build success response using 200 OK as its status code and response as its body 20 | * 21 | * @param rc Routing context 22 | * @param response Response body 23 | */ 24 | public static void buildOkResponse(RoutingContext rc, 25 | Object response) { 26 | rc.response() 27 | .setStatusCode(200) 28 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 29 | .end(Json.encodePrettily(response)); 30 | } 31 | 32 | /** 33 | * Build success response using 201 Created as its status code and response as its body 34 | * 35 | * @param rc Routing context 36 | * @param response Response body 37 | */ 38 | public static void buildCreatedResponse(RoutingContext rc, 39 | Object response) { 40 | rc.response() 41 | .setStatusCode(201) 42 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 43 | .end(Json.encodePrettily(response)); 44 | } 45 | 46 | /** 47 | * Build success response using 204 No Content as its status code and no body 48 | * 49 | * @param rc Routing context 50 | */ 51 | public static void buildNoContentResponse(RoutingContext rc) { 52 | rc.response() 53 | .setStatusCode(204) 54 | .end(); 55 | } 56 | 57 | /** 58 | * Build error response using 400 Bad Request, 404 Not Found or 500 Internal Server Error 59 | * as its status code and throwable as its body 60 | * 61 | * @param rc Routing context 62 | * @param throwable Throwable 63 | */ 64 | public static void buildErrorResponse(RoutingContext rc, 65 | Throwable throwable) { 66 | final int status; 67 | final String message; 68 | 69 | if (throwable instanceof IllegalArgumentException || throwable instanceof IllegalStateException || throwable instanceof NullPointerException) { 70 | // Bad Request 71 | status = 400; 72 | message = throwable.getMessage(); 73 | } else if (throwable instanceof NoSuchElementException) { 74 | // Not Found 75 | status = 404; 76 | message = throwable.getMessage(); 77 | } else { 78 | // Internal Server Error 79 | status = 500; 80 | message = "Internal Server Error"; 81 | } 82 | 83 | rc.response() 84 | .setStatusCode(status) 85 | .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON) 86 | .end(new JsonObject().put("error", message).encodePrettily()); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/verticle/ApiVerticle.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.verticle; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.Promise; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.core.impl.logging.Logger; 7 | import io.vertx.core.impl.logging.LoggerFactory; 8 | import io.vertx.ext.web.Router; 9 | import io.vertx.pgclient.PgPool; 10 | import org.limadelrey.vertx4.reactive.rest.api.api.handler.BookHandler; 11 | import org.limadelrey.vertx4.reactive.rest.api.api.handler.BookValidationHandler; 12 | import org.limadelrey.vertx4.reactive.rest.api.api.handler.ErrorHandler; 13 | import org.limadelrey.vertx4.reactive.rest.api.api.repository.BookRepository; 14 | import org.limadelrey.vertx4.reactive.rest.api.api.router.BookRouter; 15 | import org.limadelrey.vertx4.reactive.rest.api.api.router.HealthCheckRouter; 16 | import org.limadelrey.vertx4.reactive.rest.api.api.router.MetricsRouter; 17 | import org.limadelrey.vertx4.reactive.rest.api.api.service.BookService; 18 | import org.limadelrey.vertx4.reactive.rest.api.utils.DbUtils; 19 | import org.limadelrey.vertx4.reactive.rest.api.utils.LogUtils; 20 | 21 | public class ApiVerticle extends AbstractVerticle { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(ApiVerticle.class); 24 | 25 | @Override 26 | public void start(Promise promise) { 27 | final PgPool dbClient = DbUtils.buildDbClient(vertx); 28 | 29 | final BookRepository bookRepository = new BookRepository(); 30 | final BookService bookService = new BookService(dbClient, bookRepository); 31 | final BookHandler bookHandler = new BookHandler(bookService); 32 | final BookValidationHandler bookValidationHandler = new BookValidationHandler(vertx); 33 | final BookRouter bookRouter = new BookRouter(vertx, bookHandler, bookValidationHandler); 34 | 35 | final Router router = Router.router(vertx); 36 | ErrorHandler.buildHandler(router); 37 | HealthCheckRouter.setRouter(vertx, router, dbClient); 38 | MetricsRouter.setRouter(router); 39 | bookRouter.setRouter(router); 40 | 41 | buildHttpServer(vertx, promise, router); 42 | } 43 | 44 | /** 45 | * Run HTTP server on port 8888 with specified routes 46 | * 47 | * @param vertx Vertx context 48 | * @param promise Callback 49 | * @param router Router 50 | */ 51 | private void buildHttpServer(Vertx vertx, 52 | Promise promise, 53 | Router router) { 54 | final int port = 8888; 55 | 56 | vertx.createHttpServer() 57 | .requestHandler(router) 58 | .listen(port, http -> { 59 | if (http.succeeded()) { 60 | promise.complete(); 61 | LOGGER.info(LogUtils.RUN_HTTP_SERVER_SUCCESS_MESSAGE.buildMessage(port)); 62 | } else { 63 | promise.fail(http.cause()); 64 | LOGGER.info(LogUtils.RUN_HTTP_SERVER_ERROR_MESSAGE.buildMessage()); 65 | } 66 | }); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/verticle/MainVerticle.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.verticle; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.DeploymentOptions; 5 | import io.vertx.core.Future; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.core.impl.logging.Logger; 8 | import io.vertx.core.impl.logging.LoggerFactory; 9 | import org.limadelrey.vertx4.reactive.rest.api.utils.LogUtils; 10 | 11 | public class MainVerticle extends AbstractVerticle { 12 | 13 | private static final Logger LOGGER = LoggerFactory.getLogger(MainVerticle.class); 14 | 15 | @Override 16 | public void start() { 17 | final long start = System.currentTimeMillis(); 18 | 19 | deployMigrationVerticle(vertx) 20 | .flatMap(migrationVerticleId -> deployApiVerticle(vertx)) 21 | .onSuccess(success -> LOGGER.info(LogUtils.RUN_APP_SUCCESSFULLY_MESSAGE.buildMessage(System.currentTimeMillis() - start))) 22 | .onFailure(throwable -> LOGGER.error(throwable.getMessage())); 23 | } 24 | 25 | private Future deployMigrationVerticle(Vertx vertx) { 26 | final DeploymentOptions options = new DeploymentOptions() 27 | .setWorker(true) 28 | .setWorkerPoolName("migrations-worker-pool") 29 | .setInstances(1) 30 | .setWorkerPoolSize(1); 31 | 32 | return vertx.deployVerticle(MigrationVerticle.class.getName(), options) 33 | .flatMap(vertx::undeploy); 34 | } 35 | 36 | private Future deployApiVerticle(Vertx vertx) { 37 | return vertx.deployVerticle(ApiVerticle.class.getName()); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/limadelrey/vertx4/reactive/rest/api/verticle/MigrationVerticle.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api.verticle; 2 | 3 | import io.vertx.core.AbstractVerticle; 4 | import io.vertx.core.Promise; 5 | import org.flywaydb.core.Flyway; 6 | import org.flywaydb.core.api.configuration.Configuration; 7 | import org.limadelrey.vertx4.reactive.rest.api.utils.DbUtils; 8 | 9 | public class MigrationVerticle extends AbstractVerticle { 10 | 11 | @Override 12 | public void start(Promise promise) { 13 | final Configuration config = DbUtils.buildMigrationsConfiguration(); 14 | final Flyway flyway = new Flyway(config); 15 | 16 | flyway.migrate(); 17 | 18 | promise.complete(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | datasource.host=localhost 2 | datasource.port=5432 3 | datasource.database=books 4 | datasource.username=postgres 5 | datasource.password=AXEe263eqPFqwy4z -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__initial_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS books ( id SERIAL NOT NULL 2 | , author VARCHAR(255) NOT NULL 3 | , country VARCHAR(255) 4 | , image_link VARCHAR(255) 5 | , language VARCHAR(255) 6 | , link VARCHAR(255) 7 | , pages INT4 8 | , title VARCHAR(255) NOT NULL 9 | , year INT4 10 | , PRIMARY KEY(id) 11 | ); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__initial_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO books(author, country, image_link, language, link, pages, title, year) 2 | VALUES 3 | ('Chinua Achebe', 'Nigeria', 'images/things-fall-apart.jpg', 'English', 'https://en.wikipedia.org/wiki/Things_Fall_Apart', 209, 'Things Fall Apart', 1958), 4 | ('Hans Christian Andersen', 'Denmark', 'images/fairy-tales.jpg', 'Danish', 'https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.', 784, 'Fairy tales', 1836), 5 | ('Dante Alighieri', 'Italy', 'images/the-divine-comedy.jpg', 'Italian', 'https://en.wikipedia.org/wiki/Divine_Comedy', 928, 'The Divine Comedy', 1315), 6 | ('Jane Austen', 'United Kingdom', 'images/pride-and-prejudice.jpg', 'English', 'https://en.wikipedia.org/wiki/Pride_and_Prejudice', 226, 'Pride and Prejudice', 1813), 7 | ('Honoré de Balzac', 'France', 'images/le-pere-goriot.jpg', 'French', 'https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot', 443, 'Le Père Goriot', 1835), 8 | ('Samuel Beckett', 'Republic of Ireland', 'images/molloy-malone-dies-the-unnamable.jpg', 'French, English', 'https://en.wikipedia.org/wiki/Molloy_(novel)', 256, 'Molloy, Malone Dies, The Unnamable, the trilogy', 1952), 9 | ('Giovanni Boccaccio', 'Italy', 'images/the-decameron.jpg', 'Italian', 'https://en.wikipedia.org/wiki/The_Decameron', 1024, 'The Decameron', 1351), 10 | ('Jorge Luis Borges', 'Argentina', 'images/ficciones.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/Ficciones', 224, 'Ficciones', 1965), 11 | ('Emily Brontë', 'United Kingdom', 'images/wuthering-heights.jpg', 'English', 'https://en.wikipedia.org/wiki/Wuthering_Heights', 342, 'Wuthering Heights', 1847), 12 | ('Albert Camus', 'Algeria, French Empire', 'images/l-etranger.jpg', 'French', 'https://en.wikipedia.org/wiki/The_Stranger_(novel)', 185, 'The Stranger', 1942), 13 | ('Paul Celan', 'Romania, France', 'images/poems-paul-celan.jpg', 'German', NULL, 320, 'Poems', 1952), 14 | ('Louis-Ferdinand Céline', 'France', 'images/voyage-au-bout-de-la-nuit.jpg', 'French', 'https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night', 505, 'Journey to the End of the Night', 1932), 15 | ('Miguel de Cervantes', 'Spain', 'images/don-quijote-de-la-mancha.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/Don_Quixote', 1056, 'Don Quijote De La Mancha', 1610), 16 | ('Geoffrey Chaucer', 'England', 'images/the-canterbury-tales.jpg', 'English', 'https://en.wikipedia.org/wiki/The_Canterbury_Tales', 544, 'The Canterbury Tales', 1450), 17 | ('Anton Chekhov', 'Russia', 'images/stories-of-anton-chekhov.jpg', 'Russian', 'https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov', 194, 'Stories', 1886), 18 | ('Joseph Conrad', 'United Kingdom', 'images/nostromo.jpg', 'English', 'https://en.wikipedia.org/wiki/Nostromo', 320, 'Nostromo', 1904), 19 | ('Charles Dickens', 'United Kingdom', 'images/great-expectations.jpg', 'English', 'https://en.wikipedia.org/wiki/Great_Expectations', 194, 'Great Expectations', 1861), 20 | ('Denis Diderot', 'France', 'images/jacques-the-fatalist.jpg', 'French', 'https://en.wikipedia.org/wiki/Jacques_the_Fatalist', 596, 'Jacques the Fatalist', 1796), 21 | ('Alfred Döblin', 'Germany', 'images/berlin-alexanderplatz.jpg', 'German', 'https://en.wikipedia.org/wiki/Berlin_Alexanderplatz', 600, 'Berlin Alexanderplatz', 1929), 22 | ('Fyodor Dostoevsky', 'Russia', 'images/crime-and-punishment.jpg', 'Russian', 'https://en.wikipedia.org/wiki/Crime_and_Punishment', 551, 'Crime and Punishment', 1866), 23 | ('Fyodor Dostoevsky', 'Russia', 'images/the-idiot.jpg', 'Russian', 'https://en.wikipedia.org/wiki/The_Idiot', 656, 'The Idiot', 1869), 24 | ('Fyodor Dostoevsky', 'Russia', 'images/the-possessed.jpg', 'Russian', 'https://en.wikipedia.org/wiki/Demons_(Dostoyevsky_novel)', 768, 'The Possessed', 1872), 25 | ('Fyodor Dostoevsky', 'Russia', 'images/the-brothers-karamazov.jpg', 'Russian', 'https://en.wikipedia.org/wiki/The_Brothers_Karamazov', 824, 'The Brothers Karamazov', 1880), 26 | ('George Eliot', 'United Kingdom', 'images/middlemarch.jpg', 'English', 'https://en.wikipedia.org/wiki/Middlemarch', 800, 'Middlemarch', 1871), 27 | ('Ralph Ellison', 'United States', 'images/invisible-man.jpg', 'English', 'https://en.wikipedia.org/wiki/Invisible_Man', 581, 'Invisible Man', 1952), 28 | ('Euripides', 'Greece', 'images/medea.jpg', 'Greek', 'https://en.wikipedia.org/wiki/Medea_(play)', 104, 'Medea', -431), 29 | ('William Faulkner', 'United States', 'images/absalom-absalom.jpg', 'English', 'https://en.wikipedia.org/wiki/Absalom, _Absalom!', 313, 'Absalom, Absalom!', 1936), 30 | ('William Faulkner', 'United States', 'images/the-sound-and-the-fury.jpg', 'English', 'https://en.wikipedia.org/wiki/The_Sound_and_the_Fury', 326, 'The Sound and the Fury', 1929), 31 | ('Gustave Flaubert', 'France', 'images/madame-bovary.jpg', 'French', 'https://en.wikipedia.org/wiki/Madame_Bovary', 528, 'Madame Bovary', 1857), 32 | ('Gustave Flaubert', 'France', 'images/l-education-sentimentale.jpg', 'French', 'https://en.wikipedia.org/wiki/Sentimental_Education', 606, 'Sentimental Education', 1869), 33 | ('Federico García Lorca', 'Spain', 'images/gypsy-ballads.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/Gypsy_Ballads', 218, 'Gypsy Ballads', 1928), 34 | ('Gabriel García Márquez', 'Colombia', 'images/one-hundred-years-of-solitude.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/One_Hundred_Years_of_Solitude', 417, 'One Hundred Years of Solitude', 1967), 35 | ('Gabriel García Márquez', 'Colombia', 'images/love-in-the-time-of-cholera.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/Love_in_the_Time_of_Cholera', 368, 'Love in the Time of Cholera', 1985), 36 | ('Johann Wolfgang von Goethe', 'Saxe-Weimar', 'images/faust.jpg', 'German', 'https://en.wikipedia.org/wiki/Goethe%27s_Faust', 158, 'Faust', 1832), 37 | ('Nikolai Gogol', 'Russia', 'images/dead-souls.jpg', 'Russian', 'https://en.wikipedia.org/wiki/Dead_Souls', 432, 'Dead Souls', 1842), 38 | ('Günter Grass', 'Germany', 'images/the-tin-drum.jpg', 'German', 'https://en.wikipedia.org/wiki/The_Tin_Drum', 600, 'The Tin Drum', 1959), 39 | ('João Guimarães Rosa', 'Brazil', 'images/the-devil-to-pay-in-the-backlands.jpg', 'Portuguese', 'https://en.wikipedia.org/wiki/The_Devil_to_Pay_in_the_Backlands', 494, 'The Devil to Pay in the Backlands', 1956), 40 | ('Knut Hamsun', 'Norway', 'images/hunger.jpg', 'Norwegian', 'https://en.wikipedia.org/wiki/Hunger_(Hamsun_novel)', 176, 'Hunger', 1890), 41 | ('Ernest Hemingway', 'United States', 'images/the-old-man-and-the-sea.jpg', 'English', 'https://en.wikipedia.org/wiki/The_Old_Man_and_the_Sea', 128, 'The Old Man and the Sea', 1952), 42 | ('Homer', 'Greece', 'images/the-iliad-of-homer.jpg', 'Greek', 'https://en.wikipedia.org/wiki/Iliad', 608, 'Iliad', -735), 43 | ('Homer', 'Greece', 'images/the-odyssey-of-homer.jpg', 'Greek', 'https://en.wikipedia.org/wiki/Odyssey', 374, 'Odyssey', -800), 44 | ('Henrik Ibsen', 'Norway', 'images/a-Dolls-house.jpg', 'Norwegian', 'https://en.wikipedia.org/wiki/A_Doll%27s_House', 68, 'A Doll''s House', 1879), 45 | ('James Joyce', 'Irish Free State', 'images/ulysses.jpg', 'English', 'https://en.wikipedia.org/wiki/Ulysses_(novel)', 228, 'Ulysses', 1922), 46 | ('Franz Kafka', 'Czechoslovakia', 'images/stories-of-franz-kafka.jpg', 'German', 'https://en.wikipedia.org/wiki/Franz_Kafka_bibliography#Short_stories', 488, 'Stories', 1924), 47 | ('Franz Kafka', 'Czechoslovakia', 'images/the-trial.jpg', 'German', 'https://en.wikipedia.org/wiki/The_Trial', 160, 'The Trial', 1925), 48 | ('Franz Kafka', 'Czechoslovakia', 'images/the-castle.jpg', 'German', 'https://en.wikipedia.org/wiki/The_Castle_(novel)', 352, 'The Castle', 1926), 49 | ('Kālidāsa', 'India', 'images/the-recognition-of-shakuntala.jpg', 'Sanskrit', 'https://en.wikipedia.org/wiki/Abhij%C3%B1%C4%81na%C5%9B%C4%81kuntalam', 147, 'The recognition of Shakuntala', 150), 50 | ('Yasunari Kawabata', 'Japan', 'images/the-sound-of-the-mountain.jpg', 'Japanese', 'https://en.wikipedia.org/wiki/The_Sound_of_the_Mountain', 288, 'The Sound of the Mountain', 1954), 51 | ('Nikos Kazantzakis', 'Greece', 'images/zorba-the-greek.jpg', 'Greek', 'https://en.wikipedia.org/wiki/Zorba_the_Greek', 368, 'Zorba the Greek', 1946), 52 | ('D. H. Lawrence', 'United Kingdom', 'images/sons-and-lovers.jpg', 'English', 'https://en.wikipedia.org/wiki/Sons_and_Lovers', 432, 'Sons and Lovers', 1913), 53 | ('Halldór Laxness', 'Iceland', 'images/independent-people.jpg', 'Icelandic', 'https://en.wikipedia.org/wiki/Independent_People', 470, 'Independent People', 1934), 54 | ('Giacomo Leopardi', 'Italy', 'images/poems-giacomo-leopardi.jpg', 'Italian', NULL, 184, 'Poems', 1818), 55 | ('Doris Lessing', 'United Kingdom', 'images/the-golden-notebook.jpg', 'English', 'https://en.wikipedia.org/wiki/The_Golden_Notebook', 688, 'The Golden Notebook', 1962), 56 | ('Astrid Lindgren', 'Sweden', 'images/pippi-longstocking.jpg', 'Swedish', 'https://en.wikipedia.org/wiki/Pippi_Longstocking', 160, 'Pippi Longstocking', 1945), 57 | ('Lu Xun', 'China', 'images/diary-of-a-madman.jpg', 'Chinese', 'https://en.wikipedia.org/wiki/A_Madman%27s_Diary', 389, 'Diary of a Madman', 1918), 58 | ('Naguib Mahfouz', 'Egypt', 'images/children-of-gebelawi.jpg', 'Arabic', 'https://en.wikipedia.org/wiki/Children_of_Gebelawi', 355, 'Children of Gebelawi', 1959), 59 | ('Thomas Mann', 'Germany', 'images/buddenbrooks.jpg', 'German', 'https://en.wikipedia.org/wiki/Buddenbrooks', 736, 'Buddenbrooks', 1901), 60 | ('Thomas Mann', 'Germany', 'images/the-magic-mountain.jpg', 'German', 'https://en.wikipedia.org/wiki/The_Magic_Mountain', 720, 'The Magic Mountain', 1924), 61 | ('Herman Melville', 'United States', 'images/moby-dick.jpg', 'English', 'https://en.wikipedia.org/wiki/Moby-Dick', 378, 'Moby Dick', 1851), 62 | ('Michel de Montaigne', 'France', 'images/essais.jpg', 'French', 'https://en.wikipedia.org/wiki/Essays_(Montaigne)', 404, 'Essays', 1595), 63 | ('Elsa Morante', 'Italy', 'images/history.jpg', 'Italian', 'https://en.wikipedia.org/wiki/History_(novel)', 600, 'History', 1974), 64 | ('Toni Morrison', 'United States', 'images/beloved.jpg', 'English', 'https://en.wikipedia.org/wiki/Beloved_(novel)', 321, 'Beloved', 1987), 65 | ('Murasaki Shikibu', 'Japan', 'images/the-tale-of-genji.jpg', 'Japanese', 'https://en.wikipedia.org/wiki/The_Tale_of_Genji', 1360, 'The Tale of Genji', 1006), 66 | ('Robert Musil', 'Austria', 'images/the-man-without-qualities.jpg', 'German', 'https://en.wikipedia.org/wiki/The_Man_Without_Qualities', 365, 'The Man Without Qualities', 1931), 67 | ('Vladimir Nabokov', 'Russia/United States', 'images/lolita.jpg', 'English', 'https://en.wikipedia.org/wiki/Lolita', 317, 'Lolita', 1955), 68 | ('George Orwell', 'United Kingdom', 'images/nineteen-eighty-four.jpg', 'English', 'https://en.wikipedia.org/wiki/Nineteen_Eighty-Four', 272, 'Nineteen Eighty-Four', 1949), 69 | ('Ovid', 'Roman Empire', 'images/the-metamorphoses-of-ovid.jpg', 'Classical Latin', 'https://en.wikipedia.org/wiki/Metamorphoses', 576, 'Metamorphoses', 100), 70 | ('Edgar Allan Poe', 'United States', 'images/tales-and-poems-of-edgar-allan-poe.jpg', 'English', 'https://en.wikipedia.org/wiki/Edgar_Allan_Poe_bibliography#Tales', 842, 'Tales', 1950), 71 | ('Marcel Proust', 'France', 'images/a-la-recherche-du-temps-perdu.jpg', 'French', 'https://en.wikipedia.org/wiki/In_Search_of_Lost_Time', 2408, 'In Search of Lost Time', 1920), 72 | ('François Rabelais', 'France', 'images/gargantua-and-pantagruel.jpg', 'French', 'https://en.wikipedia.org/wiki/Gargantua_and_Pantagruel', 623, 'Gargantua and Pantagruel', 1533), 73 | ('Juan Rulfo', 'Mexico', 'images/pedro-paramo.jpg', 'Spanish', 'https://en.wikipedia.org/wiki/Pedro_P%C3%A1ramo', 124, 'Pedro Páramo', 1955), 74 | ('Rumi', 'Sultanate of Rum', 'images/the-masnavi.jpg', 'Persian', 'https://en.wikipedia.org/wiki/Masnavi', 438, 'The Masnavi', 1236), 75 | ('Salman Rushdie', 'United Kingdom, India', 'images/midnights-children.jpg', 'English', 'https://en.wikipedia.org/wiki/Midnight%27s_Children', 536, 'Midnight''s Children', 1981), 76 | ('Saadi', 'Persia, Persian Empire', 'images/bostan.jpg', 'Persian', 'https://en.wikipedia.org/wiki/Bustan_(book)', 298, 'Bostan', 1257), 77 | ('Tayeb Salih', 'Sudan', 'images/season-of-migration-to-the-north.jpg', 'Arabic', 'https://en.wikipedia.org/wiki/Season_of_Migration_to_the_North', 139, 'Season of Migration to the North', 1966), 78 | ('José Saramago', 'Portugal', 'images/blindness.jpg', 'Portuguese', 'https://en.wikipedia.org/wiki/Blindness_(novel)', 352, 'Blindness', 1995), 79 | ('William Shakespeare', 'England', 'images/hamlet.jpg', 'English', 'https://en.wikipedia.org/wiki/Hamlet', 432, 'Hamlet', 1603), 80 | ('William Shakespeare', 'England', 'images/king-lear.jpg', 'English', 'https://en.wikipedia.org/wiki/King_Lear', 384, 'King Lear', 1608), 81 | ('William Shakespeare', 'England', 'images/othello.jpg', 'English', 'https://en.wikipedia.org/wiki/Othello', 314, 'Othello', 1609), 82 | ('Sophocles', 'Greece', 'images/oedipus-the-king.jpg', 'Greek', 'https://en.wikipedia.org/wiki/Oedipus_the_King', 88, 'Oedipus the King', -430), 83 | ('Stendhal', 'France', 'images/le-rouge-et-le-noir.jpg', 'French', 'https://en.wikipedia.org/wiki/The_Red_and_the_Black', 576, 'The Red and the Black', 1830), 84 | ('Laurence Sterne', 'England', 'images/the-life-and-opinions-of-tristram-shandy.jpg', 'English', 'https://en.wikipedia.org/wiki/The_Life_and_Opinions_of_Tristram_Shandy, _Gentleman', 640, 'The Life And Opinions of Tristram Shandy', 1760), 85 | ('Italo Svevo', 'Italy', 'images/confessions-of-zeno.jpg', 'Italian', 'https://en.wikipedia.org/wiki/Zeno%27s_Conscience', 412, 'Confessions of Zeno', 1923), 86 | ('Jonathan Swift', 'Ireland', 'images/gullivers-travels.jpg', 'English', 'https://en.wikipedia.org/wiki/Gulliver%27s_Travels', 178, 'Gulliver''s Travels', 1726), 87 | ('Leo Tolstoy', 'Russia', 'images/anna-karenina.jpg', 'Russian', 'https://en.wikipedia.org/wiki/Anna_Karenina', 864, 'Anna Karenina', 1877), 88 | ('Mark Twain', 'United States', 'images/the-adventures-of-huckleberry-finn.jpg', 'English', 'https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn', 224, 'The Adventures of Huckleberry Finn', 1884), 89 | ('Valmiki', 'India', 'images/ramayana.jpg', 'Sanskrit', 'https://en.wikipedia.org/wiki/Ramayana', 152, 'Ramayana', -450), 90 | ('Virgil', 'Roman Empire', 'images/the-aeneid.jpg', 'Classical Latin', 'https://en.wikipedia.org/wiki/Aeneid', 442, 'The Aeneid', -23), 91 | ('Vyasa', 'India', 'images/the-mahab-harata.jpg', 'Sanskrit', 'https://en.wikipedia.org/wiki/Mahabharata', 276, 'Mahabharata', -700), 92 | ('Walt Whitman', 'United States', 'images/leaves-of-grass.jpg', 'English', 'https://en.wikipedia.org/wiki/Leaves_of_Grass', 152, 'Leaves of Grass', 1855), 93 | ('Virginia Woolf', 'United Kingdom', 'images/mrs-dalloway.jpg', 'English', 'https://en.wikipedia.org/wiki/Mrs_Dalloway', 216, 'Mrs Dalloway', 1925), 94 | ('Virginia Woolf', 'United Kingdom', 'images/to-the-lighthouse.jpg', 'English', 'https://en.wikipedia.org/wiki/To_the_Lighthouse', 209, 'To the Lighthouse', 1927), 95 | ('Marguerite Yourcenar', 'France/Belgium', 'images/memoirs-of-hadrian.jpg', 'French', 'https://en.wikipedia.org/wiki/Memoirs_of_Hadrian', 408, 'Memoirs of Hadrian', 1951); -------------------------------------------------------------------------------- /src/test/java/org/limadelrey/vertx4/reactive/rest/api/AbstractContainerBaseTest.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api; 2 | 3 | import org.limadelrey.vertx4.reactive.rest.api.utils.ConfigUtils; 4 | import org.testcontainers.containers.GenericContainer; 5 | 6 | import java.util.Properties; 7 | 8 | abstract class AbstractContainerBaseTest { 9 | 10 | static final GenericContainer POSTGRESQL_CONTAINER; 11 | 12 | static { 13 | final Properties properties = ConfigUtils.getInstance().getProperties(); 14 | 15 | POSTGRESQL_CONTAINER = new GenericContainer<>("postgres:12-alpine") 16 | .withEnv("POSTGRES_DB", properties.getProperty("datasource.database")) 17 | .withEnv("POSTGRES_USER", properties.getProperty("datasource.username")) 18 | .withEnv("POSTGRES_PASSWORD", properties.getProperty("datasource.password")) 19 | .withExposedPorts(Integer.parseInt(properties.getProperty("datasource.port"))); 20 | 21 | POSTGRESQL_CONTAINER.start(); 22 | 23 | ConfigUtils.getInstance().getProperties().setProperty("datasource.port", String.valueOf(POSTGRESQL_CONTAINER.getMappedPort(5432))); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/org/limadelrey/vertx4/reactive/rest/api/ComponentTests.java: -------------------------------------------------------------------------------- 1 | package org.limadelrey.vertx4.reactive.rest.api; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.json.JsonObject; 5 | import io.vertx.ext.web.client.WebClient; 6 | import io.vertx.ext.web.codec.BodyCodec; 7 | import io.vertx.junit5.VertxExtension; 8 | import io.vertx.junit5.VertxTestContext; 9 | import org.junit.jupiter.api.*; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.limadelrey.vertx4.reactive.rest.api.verticle.ApiVerticle; 12 | import org.limadelrey.vertx4.reactive.rest.api.verticle.MigrationVerticle; 13 | 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.util.stream.Collectors; 19 | 20 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 21 | @ExtendWith(VertxExtension.class) 22 | public class ComponentTests extends AbstractContainerBaseTest { 23 | 24 | @BeforeAll 25 | static void setup(Vertx vertx, 26 | VertxTestContext testContext) { 27 | vertx.deployVerticle(new MigrationVerticle(), testContext.succeeding(migrationVerticleId -> 28 | vertx.deployVerticle(new ApiVerticle(), testContext.succeeding(apiVerticleId -> 29 | testContext.completeNow())))); 30 | } 31 | 32 | @Test 33 | @Order(1) 34 | @DisplayName("Read all books") 35 | void readAll(Vertx vertx, VertxTestContext testContext) { 36 | final WebClient webClient = WebClient.create(vertx); 37 | 38 | webClient.get(8888, "localhost", "/api/v1/books") 39 | .as(BodyCodec.jsonObject()) 40 | .send(testContext.succeeding(response -> { 41 | testContext.verify(() -> 42 | Assertions.assertAll( 43 | () -> Assertions.assertEquals(200, response.statusCode()), 44 | () -> Assertions.assertEquals(readFileAsJsonObject("src/test/resources/readAll/response.json"), response.body()) 45 | ) 46 | ); 47 | 48 | testContext.completeNow(); 49 | }) 50 | ); 51 | } 52 | 53 | @Test 54 | @Order(2) 55 | @DisplayName("Read one book") 56 | void readOne(Vertx vertx, 57 | VertxTestContext testContext) { 58 | final WebClient webClient = WebClient.create(vertx); 59 | 60 | webClient.get(8888, "localhost", "/api/v1/books/10") 61 | .as(BodyCodec.jsonObject()) 62 | .send(testContext.succeeding(response -> { 63 | testContext.verify(() -> 64 | Assertions.assertAll( 65 | () -> Assertions.assertEquals(200, response.statusCode()), 66 | () -> Assertions.assertEquals(readFileAsJsonObject("src/test/resources/readOne/response.json"), response.body()) 67 | ) 68 | ); 69 | 70 | testContext.completeNow(); 71 | }) 72 | ); 73 | } 74 | 75 | @Test 76 | @Order(3) 77 | @DisplayName("Create book") 78 | void create(Vertx vertx, 79 | VertxTestContext testContext) throws IOException { 80 | final WebClient webClient = WebClient.create(vertx); 81 | final JsonObject body = readFileAsJsonObject("src/test/resources/create/request.json"); 82 | 83 | webClient.post(8888, "localhost", "/api/v1/books") 84 | .as(BodyCodec.jsonObject()) 85 | .sendJsonObject(body, testContext.succeeding(response -> { 86 | testContext.verify(() -> 87 | Assertions.assertAll( 88 | () -> Assertions.assertEquals(201, response.statusCode()), 89 | () -> Assertions.assertEquals(readFileAsJsonObject("src/test/resources/create/response.json"), response.body()) 90 | ) 91 | ); 92 | 93 | testContext.completeNow(); 94 | }) 95 | ); 96 | } 97 | 98 | @Test 99 | @Order(4) 100 | @DisplayName("Update book") 101 | void update(Vertx vertx, 102 | VertxTestContext testContext) throws IOException { 103 | final WebClient webClient = WebClient.create(vertx); 104 | final JsonObject body = readFileAsJsonObject("src/test/resources/update/request.json"); 105 | 106 | webClient.put(8888, "localhost", "/api/v1/books/37") 107 | .as(BodyCodec.jsonObject()) 108 | .sendJsonObject(body, testContext.succeeding(response -> { 109 | testContext.verify(() -> 110 | Assertions.assertAll( 111 | () -> Assertions.assertEquals(200, response.statusCode()), 112 | () -> Assertions.assertEquals(readFileAsJsonObject("src/test/resources/update/response.json"), response.body()) 113 | ) 114 | ); 115 | 116 | testContext.completeNow(); 117 | }) 118 | ); 119 | } 120 | 121 | @Test 122 | @Order(5) 123 | @DisplayName("Delete book") 124 | void delete(Vertx vertx, 125 | VertxTestContext testContext) { 126 | final WebClient webClient = WebClient.create(vertx); 127 | 128 | webClient.delete(8888, "localhost", "/api/v1/books/23") 129 | .send(testContext.succeeding(response -> { 130 | testContext.verify(() -> 131 | Assertions.assertEquals(204, response.statusCode()) 132 | ); 133 | 134 | testContext.completeNow(); 135 | }) 136 | ); 137 | } 138 | 139 | private JsonObject readFileAsJsonObject(String path) throws IOException { 140 | return new JsonObject(Files.lines(Paths.get(path), StandardCharsets.UTF_8).collect(Collectors.joining("\n"))); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | datasource.host=localhost 2 | datasource.port=5432 3 | datasource.database=books 4 | datasource.username=postgres 5 | datasource.password=AXEe263eqPFqwy4z -------------------------------------------------------------------------------- /src/test/resources/create/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "José Saramago", 3 | "country": "Portugal", 4 | "image_link": "images/ensaio-sobre-a-cegueira.jpg", 5 | "language": "Portuguese", 6 | "link": "https://en.wikipedia.org/wiki/Blindness_(novel)", 7 | "pages": 288, 8 | "title": "Ensaio sobre a cegueira", 9 | "year": 1995 10 | } -------------------------------------------------------------------------------- /src/test/resources/create/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 94, 3 | "author": "José Saramago", 4 | "country": "Portugal", 5 | "image_link": "images/ensaio-sobre-a-cegueira.jpg", 6 | "language": "Portuguese", 7 | "link": "https://en.wikipedia.org/wiki/Blindness_(novel)", 8 | "pages": 288, 9 | "title": "Ensaio sobre a cegueira", 10 | "year": 1995 11 | } -------------------------------------------------------------------------------- /src/test/resources/readAll/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 93, 3 | "limit": 20, 4 | "page": 1, 5 | "books": [ 6 | { 7 | "id": 1, 8 | "author": "Chinua Achebe", 9 | "country": "Nigeria", 10 | "image_link": "images/things-fall-apart.jpg", 11 | "language": "English", 12 | "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart", 13 | "pages": 209, 14 | "title": "Things Fall Apart", 15 | "year": 1958 16 | }, 17 | { 18 | "id": 2, 19 | "author": "Hans Christian Andersen", 20 | "country": "Denmark", 21 | "image_link": "images/fairy-tales.jpg", 22 | "language": "Danish", 23 | "link": "https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.", 24 | "pages": 784, 25 | "title": "Fairy tales", 26 | "year": 1836 27 | }, 28 | { 29 | "id": 3, 30 | "author": "Dante Alighieri", 31 | "country": "Italy", 32 | "image_link": "images/the-divine-comedy.jpg", 33 | "language": "Italian", 34 | "link": "https://en.wikipedia.org/wiki/Divine_Comedy", 35 | "pages": 928, 36 | "title": "The Divine Comedy", 37 | "year": 1315 38 | }, 39 | { 40 | "id": 4, 41 | "author": "Jane Austen", 42 | "country": "United Kingdom", 43 | "image_link": "images/pride-and-prejudice.jpg", 44 | "language": "English", 45 | "link": "https://en.wikipedia.org/wiki/Pride_and_Prejudice", 46 | "pages": 226, 47 | "title": "Pride and Prejudice", 48 | "year": 1813 49 | }, 50 | { 51 | "id": 5, 52 | "author": "Honoré de Balzac", 53 | "country": "France", 54 | "image_link": "images/le-pere-goriot.jpg", 55 | "language": "French", 56 | "link": "https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot", 57 | "pages": 443, 58 | "title": "Le Père Goriot", 59 | "year": 1835 60 | }, 61 | { 62 | "id": 6, 63 | "author": "Samuel Beckett", 64 | "country": "Republic of Ireland", 65 | "image_link": "images/molloy-malone-dies-the-unnamable.jpg", 66 | "language": "French, English", 67 | "link": "https://en.wikipedia.org/wiki/Molloy_(novel)", 68 | "pages": 256, 69 | "title": "Molloy, Malone Dies, The Unnamable, the trilogy", 70 | "year": 1952 71 | }, 72 | { 73 | "id": 7, 74 | "author": "Giovanni Boccaccio", 75 | "country": "Italy", 76 | "image_link": "images/the-decameron.jpg", 77 | "language": "Italian", 78 | "link": "https://en.wikipedia.org/wiki/The_Decameron", 79 | "pages": 1024, 80 | "title": "The Decameron", 81 | "year": 1351 82 | }, 83 | { 84 | "id": 8, 85 | "author": "Jorge Luis Borges", 86 | "country": "Argentina", 87 | "image_link": "images/ficciones.jpg", 88 | "language": "Spanish", 89 | "link": "https://en.wikipedia.org/wiki/Ficciones", 90 | "pages": 224, 91 | "title": "Ficciones", 92 | "year": 1965 93 | }, 94 | { 95 | "id": 9, 96 | "author": "Emily Brontë", 97 | "country": "United Kingdom", 98 | "image_link": "images/wuthering-heights.jpg", 99 | "language": "English", 100 | "link": "https://en.wikipedia.org/wiki/Wuthering_Heights", 101 | "pages": 342, 102 | "title": "Wuthering Heights", 103 | "year": 1847 104 | }, 105 | { 106 | "id": 10, 107 | "author": "Albert Camus", 108 | "country": "Algeria, French Empire", 109 | "image_link": "images/l-etranger.jpg", 110 | "language": "French", 111 | "link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)", 112 | "pages": 185, 113 | "title": "The Stranger", 114 | "year": 1942 115 | }, 116 | { 117 | "id": 11, 118 | "author": "Paul Celan", 119 | "country": "Romania, France", 120 | "image_link": "images/poems-paul-celan.jpg", 121 | "language": "German", 122 | "link": null, 123 | "pages": 320, 124 | "title": "Poems", 125 | "year": 1952 126 | }, 127 | { 128 | "id": 12, 129 | "author": "Louis-Ferdinand Céline", 130 | "country": "France", 131 | "image_link": "images/voyage-au-bout-de-la-nuit.jpg", 132 | "language": "French", 133 | "link": "https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night", 134 | "pages": 505, 135 | "title": "Journey to the End of the Night", 136 | "year": 1932 137 | }, 138 | { 139 | "id": 13, 140 | "author": "Miguel de Cervantes", 141 | "country": "Spain", 142 | "image_link": "images/don-quijote-de-la-mancha.jpg", 143 | "language": "Spanish", 144 | "link": "https://en.wikipedia.org/wiki/Don_Quixote", 145 | "pages": 1056, 146 | "title": "Don Quijote De La Mancha", 147 | "year": 1610 148 | }, 149 | { 150 | "id": 14, 151 | "author": "Geoffrey Chaucer", 152 | "country": "England", 153 | "image_link": "images/the-canterbury-tales.jpg", 154 | "language": "English", 155 | "link": "https://en.wikipedia.org/wiki/The_Canterbury_Tales", 156 | "pages": 544, 157 | "title": "The Canterbury Tales", 158 | "year": 1450 159 | }, 160 | { 161 | "id": 15, 162 | "author": "Anton Chekhov", 163 | "country": "Russia", 164 | "image_link": "images/stories-of-anton-chekhov.jpg", 165 | "language": "Russian", 166 | "link": "https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov", 167 | "pages": 194, 168 | "title": "Stories", 169 | "year": 1886 170 | }, 171 | { 172 | "id": 16, 173 | "author": "Joseph Conrad", 174 | "country": "United Kingdom", 175 | "image_link": "images/nostromo.jpg", 176 | "language": "English", 177 | "link": "https://en.wikipedia.org/wiki/Nostromo", 178 | "pages": 320, 179 | "title": "Nostromo", 180 | "year": 1904 181 | }, 182 | { 183 | "id": 17, 184 | "author": "Charles Dickens", 185 | "country": "United Kingdom", 186 | "image_link": "images/great-expectations.jpg", 187 | "language": "English", 188 | "link": "https://en.wikipedia.org/wiki/Great_Expectations", 189 | "pages": 194, 190 | "title": "Great Expectations", 191 | "year": 1861 192 | }, 193 | { 194 | "id": 18, 195 | "author": "Denis Diderot", 196 | "country": "France", 197 | "image_link": "images/jacques-the-fatalist.jpg", 198 | "language": "French", 199 | "link": "https://en.wikipedia.org/wiki/Jacques_the_Fatalist", 200 | "pages": 596, 201 | "title": "Jacques the Fatalist", 202 | "year": 1796 203 | }, 204 | { 205 | "id": 19, 206 | "author": "Alfred Döblin", 207 | "country": "Germany", 208 | "image_link": "images/berlin-alexanderplatz.jpg", 209 | "language": "German", 210 | "link": "https://en.wikipedia.org/wiki/Berlin_Alexanderplatz", 211 | "pages": 600, 212 | "title": "Berlin Alexanderplatz", 213 | "year": 1929 214 | }, 215 | { 216 | "id": 20, 217 | "author": "Fyodor Dostoevsky", 218 | "country": "Russia", 219 | "image_link": "images/crime-and-punishment.jpg", 220 | "language": "Russian", 221 | "link": "https://en.wikipedia.org/wiki/Crime_and_Punishment", 222 | "pages": 551, 223 | "title": "Crime and Punishment", 224 | "year": 1866 225 | } 226 | ] 227 | } -------------------------------------------------------------------------------- /src/test/resources/readOne/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 10, 3 | "author": "Albert Camus", 4 | "country": "Algeria, French Empire", 5 | "image_link": "images/l-etranger.jpg", 6 | "language": "French", 7 | "link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)", 8 | "pages": 185, 9 | "title": "The Stranger", 10 | "year": 1942 11 | } -------------------------------------------------------------------------------- /src/test/resources/update/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "José Saramago", 3 | "country": "Portugal", 4 | "image_link": "images/ensaio-sobre-a-cegueira.jpg", 5 | "language": "Portuguese", 6 | "link": "https://en.wikipedia.org/wiki/Blindness_(novel)", 7 | "pages": 288, 8 | "title": "Ensaio sobre a cegueira", 9 | "year": 1995 10 | } -------------------------------------------------------------------------------- /src/test/resources/update/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 37, 3 | "author": "José Saramago", 4 | "country": "Portugal", 5 | "image_link": "images/ensaio-sobre-a-cegueira.jpg", 6 | "language": "Portuguese", 7 | "link": "https://en.wikipedia.org/wiki/Blindness_(novel)", 8 | "pages": 288, 9 | "title": "Ensaio sobre a cegueira", 10 | "year": 1995 11 | } --------------------------------------------------------------------------------