├── .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 blocking 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 | }
--------------------------------------------------------------------------------