├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── build-docker-images.sh ├── check-connectors-state.sh ├── connectors ├── avroconverter │ ├── elasticsearch-sink-customers.json │ ├── elasticsearch-sink-orders.json │ ├── elasticsearch-sink-products.json │ ├── mysql-source-customers.json │ ├── mysql-source-orders.json │ ├── mysql-source-orders_products.json │ └── mysql-source-products.json └── jsonconverter │ ├── elasticsearch-sink-customers.json │ ├── elasticsearch-sink-orders.json │ ├── elasticsearch-sink-products.json │ ├── mysql-source-customers.json │ ├── mysql-source-orders.json │ ├── mysql-source-orders_products.json │ └── mysql-source-products.json ├── create-connectors-avroconverter.sh ├── create-connectors-jsonconverter.sh ├── create-kafka-topics.sh ├── docker-compose.yml ├── docker ├── kafka-connect │ ├── Dockerfile │ ├── HOW-TO.txt │ ├── confluentinc-kafka-connect-elasticsearch-14.1.2.zip │ ├── confluentinc-kafka-connect-jdbc-10.8.1.zip │ └── jars │ │ └── mysql-connector-j-9.1.0.jar └── mysql │ └── init │ └── storedb.sql ├── documentation ├── kafka-connect-ui.jpeg ├── project-diagram.png ├── project-diagram.xml └── store-api-swagger.jpeg ├── mvnw ├── mvnw.cmd ├── pom.xml ├── remove-docker-images.sh ├── scripts └── my-functions.sh ├── start-apps.sh ├── stop-apps.sh ├── store-api ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── storeapi │ │ │ ├── StoreApiApplication.java │ │ │ ├── config │ │ │ ├── ErrorAttributesConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── customer │ │ │ ├── CustomerController.java │ │ │ ├── CustomerRepository.java │ │ │ ├── CustomerService.java │ │ │ ├── dto │ │ │ │ ├── AddCustomerRequest.java │ │ │ │ ├── CustomerResponse.java │ │ │ │ └── UpdateCustomerRequest.java │ │ │ ├── exception │ │ │ │ ├── CustomerDeletionException.java │ │ │ │ └── CustomerNotFoundException.java │ │ │ └── model │ │ │ │ └── Customer.java │ │ │ ├── order │ │ │ ├── OrderController.java │ │ │ ├── OrderRepository.java │ │ │ ├── OrderService.java │ │ │ ├── dto │ │ │ │ ├── CreateOrderRequest.java │ │ │ │ ├── OrderResponse.java │ │ │ │ └── UpdateOrderRequest.java │ │ │ ├── exception │ │ │ │ └── OrderNotFoundException.java │ │ │ └── model │ │ │ │ ├── Order.java │ │ │ │ ├── OrderProduct.java │ │ │ │ ├── OrderProductPk.java │ │ │ │ ├── OrderStatus.java │ │ │ │ └── PaymentType.java │ │ │ ├── product │ │ │ ├── ProductController.java │ │ │ ├── ProductRepository.java │ │ │ ├── ProductService.java │ │ │ ├── dto │ │ │ │ ├── AddProductRequest.java │ │ │ │ ├── ProductResponse.java │ │ │ │ └── UpdateProductRequest.java │ │ │ ├── exception │ │ │ │ ├── ProductDeletionException.java │ │ │ │ └── ProductNotFoundException.java │ │ │ └── model │ │ │ │ └── Product.java │ │ │ ├── runner │ │ │ └── LoadSamples.java │ │ │ └── simulation │ │ │ ├── SimulationController.java │ │ │ └── dto │ │ │ └── RandomOrdersRequest.java │ └── resources │ │ ├── application.yml │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── storeapi │ └── StoreApiApplicationTests.java └── store-streams ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── ivanfranchin │ │ ├── commons │ │ └── storeapp │ │ │ ├── avro │ │ │ ├── Customer.java │ │ │ ├── Order.java │ │ │ ├── OrderDetailed.java │ │ │ ├── OrderProduct.java │ │ │ ├── Product.java │ │ │ ├── ProductDetail.java │ │ │ └── ProductDetailList.java │ │ │ └── json │ │ │ ├── Customer.java │ │ │ ├── Order.java │ │ │ ├── OrderDetailed.java │ │ │ ├── OrderProduct.java │ │ │ ├── Product.java │ │ │ └── ProductDetail.java │ │ └── storestreams │ │ ├── StoreStreamsApplication.java │ │ ├── bus │ │ ├── StoreStreamsAvro.java │ │ └── StoreStreamsJson.java │ │ └── serde │ │ ├── avro │ │ ├── CustomerAvroSerde.java │ │ ├── OrderAvroSerde.java │ │ ├── OrderDetailedAvroSerde.java │ │ ├── OrderProductAvroSerde.java │ │ └── ProductAvroSerde.java │ │ └── json │ │ ├── CustomerJsonSerde.java │ │ ├── OrderDetailedJsonSerde.java │ │ ├── OrderJsonSerde.java │ │ ├── OrderProductJsonSerde.java │ │ └── ProductJsonSerde.java └── resources │ ├── application.yml │ ├── avro │ ├── customer-message.avsc │ ├── order-detailed-message.avsc │ ├── order-message.avsc │ ├── order_product-message.avsc │ ├── product-detail-list-message.avsc │ ├── product-detail-message.avsc │ └── product-message.avsc │ └── banner.txt └── test └── java └── com └── ivanfranchin └── storestreams └── StoreStreamsApplicationTests.java /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ivangfr 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### MAC OS ### 35 | *.DS_Store 36 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # springboot-kafka-connect-jdbc-streams 2 | 3 | The main goal of this project is to explore [`Kafka`](https://kafka.apache.org), [`Kafka Connect`](https://docs.confluent.io/platform/current/connect/index.html), and [`Kafka Streams`](https://docs.confluent.io/platform/current/streams/overview.html). The project includes: `store-api`, which inserts/updates records in [`MySQL`](https://www.mysql.com); `Source Connectors` that monitor these records in `MySQL` and push related messages to `Kafka`; `Sink Connectors` that listen to messages from `Kafka` and insert/update documents in [`Elasticsearch`](https://www.elastic.co); and `store-streams`, which listens to messages from `Kafka`, processes them using `Kafka Streams`, and pushes new messages back to `Kafka`. 4 | 5 | ## Proof-of-Concepts & Articles 6 | 7 | On [ivangfr.github.io](https://ivangfr.github.io), I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for. 8 | 9 | ## Additional Readings 10 | 11 | - \[**Medium**\] [**Streaming MySQL changes to Elasticsearch using Kafka Connect**](https://medium.com/@ivangfr/streaming-mysql-changes-to-elasticsearch-using-kafka-connect-fe22a5d0aa27) 12 | - \[**Medium**\] [**Enhancing a MySQL-KafkaConnect-Elasticsearch Setup with Spring Boot Applications**](https://medium.com/@ivangfr/enhancing-a-mysql-kafkaconnect-elasticsearch-setup-with-spring-boot-applications-257c65ff0965) 13 | 14 | ## Project Diagram 15 | 16 | ![project-diagram](documentation/project-diagram.png) 17 | 18 | ## Applications 19 | 20 | - ### store-api 21 | 22 | Monolithic [`Spring Boot`](https://docs.spring.io/spring-boot/index.html) application that exposes a REST API to manage `Customers`, `Products` and `Orders`. The data is stored in `MySQL`. 23 | 24 | ![store-api-swagger](documentation/store-api-swagger.jpeg) 25 | 26 | - ### store-streams 27 | 28 | `Spring Boot` application that connects to `Kafka` and uses `Kafka Streams API` to transform some _"input"_ topics into a new _"output"_ topic in `Kafka`. 29 | 30 | ## Prerequisites 31 | 32 | - [`Java 21`](https://www.oracle.com/java/technologies/downloads/#java21) or higher; 33 | - A containerization tool (e.g., [`Docker`](https://www.docker.com), [`Podman`](https://podman.io), etc.) 34 | 35 | ## (De)Serialization formats 36 | 37 | In order to run this project, you can use [`JSON`](https://www.json.org) or [`Avro`](https://avro.apache.org) format to serialize/deserialize data to/from the `binary` format used by Kafka. The default format is `JSON`. Throughout this document, I will point out what to do if you want to use `Avro`. 38 | 39 | ## Start Environment 40 | 41 | - Open a terminal and inside the `springboot-kafka-connect-jdbc-streams` root folder run: 42 | ```bash 43 | docker compose up -d 44 | ``` 45 | > **Note**: During the first run, an image for `kafka-connect` will be built with the name `springboot-kafka-connect-jdbc-streams_kafka-connect`. Use the command below to rebuild it. 46 | > ```bash 47 | > docker compose build 48 | > ``` 49 | 50 | - Wait for all Docker containers to be up and running. To check it, run: 51 | ```bash 52 | docker ps -a 53 | ``` 54 | 55 | ## Create Kafka Topics 56 | 57 | In order to have topics in `Kafka` with more than `1` partition, we have to create them manually and not let the connectors to create them for us. So, for it: 58 | 59 | - Open a new terminal and make sure you are in the `springboot-kafka-connect-jdbc-streams` root folder; 60 | 61 | - Run the script below: 62 | ```bash 63 | ./create-kafka-topics.sh 64 | ``` 65 | 66 | It will create the topics `mysql.storedb.customers`, `mysql.storedb.products`, `mysql.storedb.orders`, `mysql.storedb.orders_products` with `5` partitions. 67 | 68 | ## Create connectors 69 | 70 | Connectors use `Converters` for data serialization and deserialization. If you are configuring `For JSON (de)serialization`, the converter used is `JsonConverter`. On the other hand, the converter used is `AvroConverter`. 71 | 72 | > **Important**: If the `Source Connector Converter` serializes data (e.g., from `JSON` to `bytes` using `JsonConverter`), the `Sink Connector Converter` must also use `JsonConverter` to deserialize the `bytes`. Otherwise, an error will be thrown. The document [Kafka Connect Deep Dive – Converters and Serialization Explained](https://www.confluent.io/blog/kafka-connect-deep-dive-converters-serialization-explained) explains this in detail. 73 | 74 | Steps to create the connectors: 75 | 76 | - In a terminal, navigate to the `springboot-kafka-connect-jdbc-streams` root folder 77 | 78 | - Run the following script to create the connectors on `kafka-connect`: 79 | 80 | - **For JSON (de)serialization** 81 | 82 | ```bash 83 | ./create-connectors-jsonconverter.sh 84 | ``` 85 | 86 | - **For Avro (de)serialization** 87 | 88 | ```bash 89 | ./create-connectors-avroconverter.sh 90 | ``` 91 | 92 | - You can check the state of the connectors and their tasks on `Kafka Connect UI` or running the following script: 93 | ```bash 94 | ./check-connectors-state.sh 95 | ``` 96 | 97 | - Once the connectors and their tasks are ready (`RUNNING` state), you should see something like: 98 | ```text 99 | {"name":"mysql-source-customers","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"source"} 100 | {"name":"mysql-source-products","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"source"} 101 | {"name":"mysql-source-orders","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"source"} 102 | {"name":"mysql-source-orders_products","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"source"} 103 | {"name":"elasticsearch-sink-customers","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"sink"} 104 | {"name":"elasticsearch-sink-products","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"sink"} 105 | {"name":"elasticsearch-sink-orders","connector":{"state":"RUNNING","worker_id":"kafka-connect:8083"},"tasks":[{"id":0,"state":"RUNNING","worker_id":"kafka-connect:8083"}],"type":"sink"} 106 | ``` 107 | 108 | - On `Kafka Connect UI` (http://localhost:8086), you should see: 109 | 110 | ![kafka-connect-ui](documentation/kafka-connect-ui.jpeg) 111 | 112 | - If there is any problem, you can check `kafka-connect` container logs: 113 | ```bash 114 | docker logs kafka-connect 115 | ``` 116 | ## Running Applications with Maven 117 | 118 | - **store-api** 119 | 120 | - Open a new terminal and make sure you are in the `springboot-kafka-connect-jdbc-streams` root folder. 121 | 122 | - Run the command below to start the application: 123 | ```bash 124 | ./mvnw clean spring-boot:run --projects store-api \ 125 | -Dspring-boot.run.jvmArguments="-Dserver.port=9080" 126 | ``` 127 | > **Note** 128 | > 129 | > It will create all tables, such as: `customers`, `products`, `orders` and `orders_products`. We are using `spring.jpa.hibernate.ddl-auto=update` configuration. 130 | > 131 | > It will also insert some customers and products. If you don't want it, just set to `false` the properties `load-samples.customers.enabled` and `load-samples.products.enabled` in `application.yml`. 132 | 133 | - **store-streams** 134 | 135 | - Open a new terminal and inside the `springboot-kafka-connect-jdbc-streams` root folder. 136 | 137 | - To start application, run: 138 | 139 | - **For JSON (de)serialization** 140 | 141 | ```bash 142 | ./mvnw clean spring-boot:run --projects store-streams \ 143 | -Dspring-boot.run.jvmArguments="-Dserver.port=9081" 144 | ``` 145 | 146 | - **For Avro (de)serialization** 147 | 148 | > **Warning**: Unable to run in this mode on my machine! The application starts fine when using the `avro` profile, but when the first event arrives, the `org.apache.kafka.common.errors.SerializationException: Unknown magic byte!` is thrown. This problem does not occur when [Running Applications as Docker containers](#running-applications-as-docker-containers). 149 | ```bash 150 | ./mvnw clean spring-boot:run --projects store-streams \ 151 | -Dspring-boot.run.jvmArguments="-Dserver.port=9081" \ 152 | -Dspring-boot.run.profiles=avro 153 | ``` 154 | > The command below generates Java classes from Avro files present in `src/main/resources/avro` 155 | > ```bash 156 | > ./mvnw generate-sources --projects store-streams 157 | > ``` 158 | 159 | ## Running Applications as Docker containers 160 | 161 | ### Build Application’s Docker Image 162 | 163 | - In a terminal, make sure you are inside the `springboot-kafka-connect-jdbc-streams` root folder; 164 | 165 | - Run the following script to build the application's docker image: 166 | ```bash 167 | ./build-docker-images.sh 168 | ``` 169 | 170 | ### Application’s Environment Variables 171 | 172 | - **store-api** 173 | 174 | | Environment Variable | Description | 175 | |------------------------|-------------------------------------------------------------------| 176 | | `MYSQL_HOST` | Specify host of the `MySQL` database to use (default `localhost`) | 177 | | `MYSQL_PORT` | Specify port of the `MySQL` database to use (default `3306`) | 178 | 179 | - **store-streams** 180 | 181 | | Environment Variable | Description | 182 | |------------------------|-------------------------------------------------------------------------| 183 | | `KAFKA_HOST` | Specify host of the `Kafka` message broker to use (default `localhost`) | 184 | | `KAFKA_PORT` | Specify port of the `Kafka` message broker to use (default `29092`) | 185 | | `SCHEMA_REGISTRY_HOST` | Specify host of the `Schema Registry` to use (default `localhost`) | 186 | | `SCHEMA_REGISTRY_PORT` | Specify port of the `Schema Registry` to use (default `8081`) | 187 | 188 | ### Run Application’s Docker Container 189 | 190 | - In a terminal, make sure you are inside the `springboot-kafka-connect-jdbc-streams` root folder; 191 | 192 | - In order to run the application's docker containers, you can pick between `JSON` or `Avro`: 193 | 194 | - **For JSON (de)serialization** 195 | ```bash 196 | ./start-apps.sh 197 | ``` 198 | - **For Avro (de)serialization** 199 | ```bash 200 | ./start-apps.sh avro 201 | ``` 202 | 203 | ## Application's URL 204 | 205 | | Application | URL | 206 | |---------------|---------------------------------------| 207 | | store-api | http://localhost:9080/swagger-ui.html | 208 | | store-streams | http://localhost:9081/actuator/health | 209 | 210 | ## Testing 211 | 212 | 1. Let's simulate an order creation. In this example, customer with id `1` 213 | ```json 214 | {"id":1, "name":"John Gates", "email":"john.gates@test.com", "address":"street 1", "phone":"112233"} 215 | ``` 216 | will order one unit of the product with id `15` 217 | ```json 218 | {"id":15, "name":"iPhone Xr", "price":900.00} 219 | ``` 220 | 221 | In a terminal, run the following `curl` command: 222 | ```bash 223 | curl -i -X POST localhost:9080/api/orders \ 224 | -H 'Content-Type: application/json' \ 225 | -d '{"customerId": 1, "paymentType": "BITCOIN", "status": "OPEN", "products": [{"id": 15, "unit": 1}]}' 226 | ``` 227 | 228 | The response should be: 229 | ```text 230 | HTTP/1.1 201 231 | { 232 | "id": "47675629-4f0d-440d-b6df-c829874ee2a6", 233 | "customerId": 1, 234 | "paymentType": "BITCOIN", 235 | "status": "OPEN", 236 | "products": [{"id": 15, "unit": 1}] 237 | } 238 | ``` 239 | 240 | 2. Checking `Elasticsearch`: 241 | ```bash 242 | curl "localhost:9200/store.streams.orders/_search?pretty" 243 | ``` 244 | 245 | We should have one order with a customer and products names: 246 | ```json 247 | { 248 | "took" : 844, 249 | "timed_out" : false, 250 | "_shards" : { 251 | "total" : 1, 252 | "successful" : 1, 253 | "skipped" : 0, 254 | "failed" : 0 255 | }, 256 | "hits" : { 257 | "total" : { 258 | "value" : 1, 259 | "relation" : "eq" 260 | }, 261 | "max_score" : 1.0, 262 | "hits" : [ 263 | { 264 | "_index" : "store.streams.orders", 265 | "_type" : "order", 266 | "_id" : "47675629-4f0d-440d-b6df-c829874ee2a6", 267 | "_score" : 1.0, 268 | "_source" : { 269 | "payment_type" : "BITCOIN", 270 | "created_at" : 1606821792360, 271 | "id" : "47675629-4f0d-440d-b6df-c829874ee2a6", 272 | "customer_name" : "John Gates", 273 | "customer_id" : 1, 274 | "status" : "OPEN", 275 | "products" : [ 276 | { 277 | "unit" : 1, 278 | "price" : 900, 279 | "name" : "iPhone Xr", 280 | "id" : 15 281 | } 282 | ] 283 | } 284 | } 285 | ] 286 | } 287 | } 288 | ``` 289 | 290 | 3. In order to create random orders, we can use also the `simulation`: 291 | ```bash 292 | curl -i -X POST localhost:9080/api/simulation/orders \ 293 | -H 'Content-Type: application/json' \ 294 | -d '{"total": 10, "sleep": 100}' 295 | ``` 296 | 297 | ## Useful Links/Commands 298 | 299 | - **Kafka Topics UI** 300 | 301 | `Kafka Topics UI` can be accessed at http://localhost:8085 302 | 303 | - **Kafka Connect UI** 304 | 305 | `Kafka Connect UI` can be accessed at http://localhost:8086 306 | 307 | - **Schema Registry UI** 308 | 309 | `Schema Registry UI` can be accessed at http://localhost:8001 310 | 311 | - **Schema Registry** 312 | 313 | You can use `curl` to check the subjects in `Schema Registry` 314 | 315 | - Get the list of subjects 316 | ```bash 317 | curl localhost:8081/subjects 318 | ``` 319 | - Get the latest version of the subject `mysql.storedb.customers-value` 320 | ```bash 321 | curl localhost:8081/subjects/mysql.storedb.customers-value/versions/latest 322 | ``` 323 | 324 | - **Kafka Manager** 325 | 326 | `Kafka Manager` can be accessed at http://localhost:9000 327 | 328 | _Configuration_ 329 | - First, you must create a new cluster. Click on `Cluster` (dropdown on the header) and then on `Add Cluster`; 330 | - Type the name of your cluster in `Cluster Name` field, for example: `MyCluster`; 331 | - Type `zookeeper:2181` in `Cluster Zookeeper Hosts` field; 332 | - Enable checkbox `Poll consumer information (Not recommended for large # of consumers if ZK is used for offsets tracking on older Kafka versions)`; 333 | - Click on `Save` button at the bottom of the page. 334 | 335 | - **Elasticsearch** 336 | 337 | `Elasticsearch` can be accessed at http://localhost:9200 338 | 339 | - Get all indices: 340 | ```bash 341 | curl "localhost:9200/_cat/indices?v" 342 | ``` 343 | - Search for documents: 344 | ```bash 345 | curl "localhost:9200/mysql.storedb.customers/_search?pretty" 346 | curl "localhost:9200/mysql.storedb.products/_search?pretty" 347 | curl "localhost:9200/store.streams.orders/_search?pretty" 348 | ``` 349 | 350 | - **MySQL** 351 | 352 | ```bash 353 | docker exec -it -e MYSQL_PWD=secret mysql mysql -uroot --database storedb 354 | select * from orders; 355 | ``` 356 | 357 | ## Shutdown 358 | 359 | - To stop applications: 360 | - If they were started with `Maven`, go to the terminals where they are running and press `Ctrl+C`; 361 | - If they were started as Docker containers, go to a terminal and, inside the `springboot-kafka-connect-jdbc-streams` root folder, run the script below: 362 | ```bash 363 | ./stop-apps.sh 364 | ``` 365 | - To stop and remove docker compose containers, network and volumes, go to a terminal and, inside the `springboot-kafka-connect-jdbc-streams` root folder, run the following command: 366 | ```bash 367 | docker compose down -v 368 | ``` 369 | 370 | ## Cleanup 371 | 372 | To remove the Docker images created by this project, go to a terminal and, inside the `springboot-kafka-connect-jdbc-streams` root folder, run the script below: 373 | ```bash 374 | ./remove-docker-images.sh 375 | ``` 376 | 377 | ## Issues 378 | 379 | - Product `price` field, [numeric.mapping doesn't work for DECIMAL fields #563](https://github.com/confluentinc/kafka-connect-jdbc/issues/563). For now, the workaround is using `String` instead of `BigDecimal` as type for this field. -------------------------------------------------------------------------------- /build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCKER_IMAGE_PREFIX="ivanfranchin" 4 | APP_VERSION="1.0.0" 5 | 6 | STORE_API_APP_NAME="store-api" 7 | STORE_STREAMS_APP_NAME="store-streams" 8 | 9 | STORE_API_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${STORE_API_APP_NAME}:${APP_VERSION}" 10 | STORE_STREAMS_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${STORE_STREAMS_APP_NAME}:${APP_VERSION}" 11 | 12 | SKIP_TESTS="true" 13 | 14 | ./mvnw clean compile jib:dockerBuild \ 15 | --projects "$STORE_API_APP_NAME" \ 16 | -DskipTests="$SKIP_TESTS" \ 17 | -Dimage="$STORE_API_DOCKER_IMAGE_NAME" 18 | 19 | ./mvnw clean compile jib:dockerBuild \ 20 | --projects "$STORE_STREAMS_APP_NAME" \ 21 | -DskipTests="$SKIP_TESTS" \ 22 | -Dimage="$STORE_STREAMS_DOCKER_IMAGE_NAME" -------------------------------------------------------------------------------- /check-connectors-state.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-------------------------------" 4 | echo "Connector and their tasks state" 5 | echo "-------------------------------" 6 | 7 | curl localhost:8083/connectors/mysql-source-customers/status 8 | 9 | echo 10 | curl localhost:8083/connectors/mysql-source-products/status 11 | 12 | echo 13 | curl localhost:8083/connectors/mysql-source-orders/status 14 | 15 | echo 16 | curl localhost:8083/connectors/mysql-source-orders_products/status 17 | 18 | echo 19 | curl localhost:8083/connectors/elasticsearch-sink-customers/status 20 | 21 | echo 22 | curl localhost:8083/connectors/elasticsearch-sink-products/status 23 | 24 | echo 25 | curl localhost:8083/connectors/elasticsearch-sink-orders/status 26 | 27 | echo 28 | echo "You can also use Kafka Connect UI, link http://localhost:8086" 29 | echo -------------------------------------------------------------------------------- /connectors/avroconverter/elasticsearch-sink-customers.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-customers", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "mysql.storedb.customers", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "customer", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key converter (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false" 13 | } 14 | } -------------------------------------------------------------------------------- /connectors/avroconverter/elasticsearch-sink-orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-orders", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "store.streams.orders", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "order", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key converter (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false" 13 | } 14 | } -------------------------------------------------------------------------------- /connectors/avroconverter/elasticsearch-sink-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "mysql.storedb.products", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "product", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key converter (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false" 13 | } 14 | } -------------------------------------------------------------------------------- /connectors/avroconverter/mysql-source-customers.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-customers", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "customers", 9 | "mode": "timestamp+incrementing", 10 | "timestamp.column.name": "updated_at", 11 | "incrementing.column.name": "id", 12 | "topic.prefix": "mysql.storedb.", 13 | "tasks.max": "1", 14 | 15 | "_comment": "--- SMT (Single Message Transform) ---", 16 | "transforms": "setSchemaName, dropFields, maskFields, createKey, extractId", 17 | 18 | "_comment": "--- Change the schema name ---", 19 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 20 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.avro.Customer", 21 | 22 | "_comment": "--- Drop fields ---", 23 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 24 | "transforms.dropFields.blacklist": "updated_at", 25 | 26 | "_comment": "--- Mask fields ---", 27 | "transforms.maskFields.type":"org.apache.kafka.connect.transforms.MaskField$Value", 28 | "transforms.maskFields.fields":"phone", 29 | 30 | "_comment": "--- Add key to the message based on the entity id field ---", 31 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 32 | "transforms.createKey.fields": "id", 33 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 34 | "transforms.extractId.field": "id", 35 | 36 | "_comment": "--- Change Key converter (default is Avro) ---", 37 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 38 | "key.converter.schemas.enable": "false" 39 | } 40 | } -------------------------------------------------------------------------------- /connectors/avroconverter/mysql-source-orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-orders", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "orders", 9 | "mode": "timestamp", 10 | "timestamp.column.name": "updated_at", 11 | "topic.prefix": "mysql.storedb.", 12 | "tasks.max": "1", 13 | 14 | "_comment": "--- SMT (Single Message Transform) ---", 15 | "transforms": "setSchemaName, dropFields, createKey, extractId", 16 | 17 | "_comment": "--- Change the schema name ---", 18 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 19 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.avro.Order", 20 | 21 | "_comment": "--- Drop fields ---", 22 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 23 | "transforms.dropFields.blacklist": "updated_at", 24 | 25 | "_comment": "--- Add key to the message based on the entity id field ---", 26 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 27 | "transforms.createKey.fields": "id", 28 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 29 | "transforms.extractId.field": "id", 30 | 31 | "_comment": "--- Change Key converter (default is Avro) ---", 32 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 33 | "key.converter.schemas.enable": "false" 34 | } 35 | } -------------------------------------------------------------------------------- /connectors/avroconverter/mysql-source-orders_products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-orders_products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "orders_products", 9 | "mode": "timestamp", 10 | "timestamp.column.name": "updated_at", 11 | "topic.prefix": "mysql.storedb.", 12 | "tasks.max": "1", 13 | 14 | "_comment": "--- SMT (Single Message Transform) ---", 15 | "transforms": "setSchemaName, dropFields, createKey, extractId", 16 | 17 | "_comment": "--- Change the schema name ---", 18 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 19 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.avro.OrderProduct", 20 | 21 | "_comment": "--- Drop fields ---", 22 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 23 | "transforms.dropFields.blacklist": "created_at, updated_at", 24 | 25 | "_comment": "--- Add key to the message based on the entity id field ---", 26 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 27 | "transforms.createKey.fields": "order_id", 28 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 29 | "transforms.extractId.field": "order_id", 30 | 31 | "_comment": "--- Change Key converter (default is Avro) ---", 32 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 33 | "key.converter.schemas.enable": "false" 34 | } 35 | } -------------------------------------------------------------------------------- /connectors/avroconverter/mysql-source-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "products", 9 | "mode": "timestamp+incrementing", 10 | "timestamp.column.name": "updated_at", 11 | "incrementing.column.name": "id", 12 | "topic.prefix": "mysql.storedb.", 13 | "tasks.max": "1", 14 | 15 | "_comment": "--- SMT (Single Message Transform) ---", 16 | "transforms": "setSchemaName, dropFields, createKey, extractId", 17 | 18 | "_comment": "--- Change the schema name ---", 19 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 20 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.avro.Product", 21 | 22 | "_comment": "--- Drop fields ---", 23 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 24 | "transforms.dropFields.blacklist": "updated_at", 25 | 26 | "_comment": "--- Add key to the message based on the entity id field ---", 27 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 28 | "transforms.createKey.fields": "id", 29 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 30 | "transforms.extractId.field": "id", 31 | 32 | "_comment": "--- Change Key converter (default is Avro) ---", 33 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 34 | "key.converter.schemas.enable": "false", 35 | 36 | "_comment": "--- numeric.mapping doesn't work for DECIMAL fields #563 ---", 37 | "_comment": "--- https://github.com/confluentinc/kafka-connect-jdbc/issues/563 ---", 38 | "_comment": "--- using String as type for price field for now ---", 39 | "numeric.mapping": "best_fit" 40 | } 41 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/elasticsearch-sink-customers.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-customers", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "mysql.storedb.customers", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "customer", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false", 13 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 14 | "value.converter.schemas.enable": "false", 15 | 16 | "_comment": "--- The topic has no schema ---", 17 | "schema.ignore": "true" 18 | } 19 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/elasticsearch-sink-orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-orders", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "store.streams.orders", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "order", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false", 13 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 14 | "value.converter.schemas.enable": "false", 15 | 16 | "_comment": "--- The topic has no schema ---", 17 | "schema.ignore": "true" 18 | } 19 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/elasticsearch-sink-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elasticsearch-sink-products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector", 5 | "topics": "mysql.storedb.products", 6 | "connection.url": "http://elasticsearch:9200", 7 | "type.name": "product", 8 | "tasks.max": "1", 9 | 10 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 11 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 12 | "key.converter.schemas.enable": "false", 13 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 14 | "value.converter.schemas.enable": "false", 15 | 16 | "_comment": "--- The topic has no schema ---", 17 | "schema.ignore": "true" 18 | } 19 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/mysql-source-customers.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-customers", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "customers", 9 | "mode": "timestamp+incrementing", 10 | "timestamp.column.name": "updated_at", 11 | "incrementing.column.name": "id", 12 | "topic.prefix": "mysql.storedb.", 13 | "tasks.max": "1", 14 | 15 | "_comment": "--- SMT (Single Message Transform) ---", 16 | "transforms": "setSchemaName, dropFields, maskFields, createKey, extractId", 17 | 18 | "_comment": "--- Change the schema name ---", 19 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 20 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.events.Customer", 21 | 22 | "_comment": "--- Drop fields ---", 23 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 24 | "transforms.dropFields.blacklist": "updated_at", 25 | 26 | "_comment": "--- Mask fields ---", 27 | "transforms.maskFields.type":"org.apache.kafka.connect.transforms.MaskField$Value", 28 | "transforms.maskFields.fields":"phone", 29 | 30 | "_comment": "--- Add key to the message based on the entity id field ---", 31 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 32 | "transforms.createKey.fields": "id", 33 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 34 | "transforms.extractId.field": "id", 35 | 36 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 37 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 38 | "key.converter.schemas.enable": "false", 39 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 40 | "value.converter.schemas.enable": "false" 41 | } 42 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/mysql-source-orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-orders", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "orders", 9 | "mode": "timestamp", 10 | "timestamp.column.name": "updated_at", 11 | "topic.prefix": "mysql.storedb.", 12 | "tasks.max": "1", 13 | 14 | "_comment": "--- SMT (Single Message Transform) ---", 15 | "transforms": "setSchemaName, dropFields, createKey, extractId", 16 | 17 | "_comment": "--- Change the schema name ---", 18 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 19 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.events.Order", 20 | 21 | "_comment": "--- Drop fields ---", 22 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 23 | "transforms.dropFields.blacklist": "updated_at", 24 | 25 | "_comment": "--- Add key to the message based on the entity id field ---", 26 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 27 | "transforms.createKey.fields": "id", 28 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 29 | "transforms.extractId.field": "id", 30 | 31 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 32 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 33 | "key.converter.schemas.enable": "false", 34 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 35 | "value.converter.schemas.enable": "false" 36 | } 37 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/mysql-source-orders_products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-orders_products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "orders_products", 9 | "mode": "timestamp", 10 | "timestamp.column.name": "updated_at", 11 | "topic.prefix": "mysql.storedb.", 12 | "tasks.max": "1", 13 | 14 | "_comment": "--- SMT (Single Message Transform) ---", 15 | "transforms": "setSchemaName, dropFields, createKey, extractId", 16 | 17 | "_comment": "--- Change the schema name ---", 18 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 19 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.events.OrderProduct", 20 | 21 | "_comment": "--- Drop fields ---", 22 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 23 | "transforms.dropFields.blacklist": "created_at, updated_at", 24 | 25 | "_comment": "--- Add key to the message based on the entity id field ---", 26 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 27 | "transforms.createKey.fields": "order_id", 28 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 29 | "transforms.extractId.field": "order_id", 30 | 31 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 32 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 33 | "key.converter.schemas.enable": "false", 34 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 35 | "value.converter.schemas.enable": "false" 36 | } 37 | } -------------------------------------------------------------------------------- /connectors/jsonconverter/mysql-source-products.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql-source-products", 3 | "config": { 4 | "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector", 5 | "connection.url": "jdbc:mysql://mysql:3306/storedb?characterEncoding=UTF-8&serverTimezone=UTC", 6 | "connection.user": "root", 7 | "connection.password": "secret", 8 | "table.whitelist": "products", 9 | "mode": "timestamp+incrementing", 10 | "timestamp.column.name": "updated_at", 11 | "incrementing.column.name": "id", 12 | "topic.prefix": "mysql.storedb.", 13 | "tasks.max": "1", 14 | 15 | "_comment": "--- SMT (Single Message Transform) ---", 16 | "transforms": "setSchemaName, dropFields, createKey, extractId", 17 | 18 | "_comment": "--- Change the schema name ---", 19 | "transforms.setSchemaName.type": "org.apache.kafka.connect.transforms.SetSchemaMetadata$Value", 20 | "transforms.setSchemaName.schema.name": "com.ivanfranchin.commons.storeapp.events.Product", 21 | 22 | "_comment": "--- Drop fields ---", 23 | "transforms.dropFields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value", 24 | "transforms.dropFields.blacklist": "updated_at", 25 | 26 | "_comment": "--- Add key to the message based on the entity id field ---", 27 | "transforms.createKey.type": "org.apache.kafka.connect.transforms.ValueToKey", 28 | "transforms.createKey.fields": "id", 29 | "transforms.extractId.type": "org.apache.kafka.connect.transforms.ExtractField$Key", 30 | "transforms.extractId.field": "id", 31 | 32 | "_comment": "--- Change Key/Value converters (default is Avro) ---", 33 | "key.converter": "org.apache.kafka.connect.storage.StringConverter", 34 | "key.converter.schemas.enable": "false", 35 | "value.converter": "org.apache.kafka.connect.json.JsonConverter", 36 | "value.converter.schemas.enable": "false", 37 | 38 | "_comment": "--- numeric.mapping doesn't work for DECIMAL fields #563 ---", 39 | "_comment": "--- https://github.com/confluentinc/kafka-connect-jdbc/issues/563 ---", 40 | "_comment": "--- using String as type for price field for now ---", 41 | "numeric.mapping": "best_fit" 42 | } 43 | } -------------------------------------------------------------------------------- /create-connectors-avroconverter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-----------------------" 4 | echo "Creating connectors ..." 5 | echo "-----------------------" 6 | 7 | echo 8 | curl -i -X POST http://localhost:8083/connectors \ 9 | -H 'Content-Type: application/json' \ 10 | -H 'Accept: application/json' \ 11 | -d @connectors/avroconverter/mysql-source-customers.json 12 | 13 | echo 14 | curl -i -X POST http://localhost:8083/connectors \ 15 | -H 'Content-Type: application/json' \ 16 | -H 'Accept: application/json' \ 17 | -d @connectors/avroconverter/mysql-source-products.json 18 | 19 | echo 20 | curl -i -X POST http://localhost:8083/connectors \ 21 | -H 'Content-Type: application/json' \ 22 | -H 'Accept: application/json' \ 23 | -d @connectors/avroconverter/mysql-source-orders.json 24 | 25 | echo 26 | curl -i -X POST http://localhost:8083/connectors \ 27 | -H 'Content-Type: application/json' \ 28 | -H 'Accept: application/json' \ 29 | -d @connectors/avroconverter/mysql-source-orders_products.json 30 | 31 | echo 32 | curl -i -X POST http://localhost:8083/connectors \ 33 | -H 'Content-Type: application/json' \ 34 | -H 'Accept: application/json' \ 35 | -d @connectors/avroconverter/elasticsearch-sink-customers.json 36 | 37 | echo 38 | curl -i -X POST http://localhost:8083/connectors \ 39 | -H 'Content-Type: application/json' \ 40 | -H 'Accept: application/json' \ 41 | -d @connectors/avroconverter/elasticsearch-sink-products.json 42 | 43 | echo 44 | curl -i -X POST http://localhost:8083/connectors \ 45 | -H 'Content-Type: application/json' \ 46 | -H 'Accept: application/json' \ 47 | -d @connectors/avroconverter/elasticsearch-sink-orders.json 48 | 49 | echo 50 | echo "--------------------------------------------------------------" 51 | echo "Check state of connectors and their tasks by running script ./check-connectors-state.sh or at Kafka Connect UI, link http://localhost:8086" 52 | echo "--------------------------------------------------------------" -------------------------------------------------------------------------------- /create-connectors-jsonconverter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-----------------------" 4 | echo "Creating connectors ..." 5 | echo "-----------------------" 6 | 7 | echo 8 | curl -i -X POST http://localhost:8083/connectors \ 9 | -H 'Content-Type: application/json' \ 10 | -H 'Accept: application/json' \ 11 | -d @connectors/jsonconverter/mysql-source-customers.json 12 | 13 | echo 14 | curl -i -X POST http://localhost:8083/connectors \ 15 | -H 'Content-Type: application/json' \ 16 | -H 'Accept: application/json' \ 17 | -d @connectors/jsonconverter/mysql-source-products.json 18 | 19 | echo 20 | curl -i -X POST http://localhost:8083/connectors \ 21 | -H 'Content-Type: application/json' \ 22 | -H 'Accept: application/json' \ 23 | -d @connectors/jsonconverter/mysql-source-orders.json 24 | 25 | echo 26 | curl -i -X POST http://localhost:8083/connectors \ 27 | -H 'Content-Type: application/json' \ 28 | -H 'Accept: application/json' \ 29 | -d @connectors/jsonconverter/mysql-source-orders_products.json 30 | 31 | echo 32 | curl -i -X POST http://localhost:8083/connectors \ 33 | -H 'Content-Type: application/json' \ 34 | -H 'Accept: application/json' \ 35 | -d @connectors/jsonconverter/elasticsearch-sink-customers.json 36 | 37 | echo 38 | curl -i -X POST http://localhost:8083/connectors \ 39 | -H 'Content-Type: application/json' \ 40 | -H 'Accept: application/json' \ 41 | -d @connectors/jsonconverter/elasticsearch-sink-products.json 42 | 43 | echo 44 | curl -i -X POST http://localhost:8083/connectors \ 45 | -H 'Content-Type: application/json' \ 46 | -H 'Accept: application/json' \ 47 | -d @connectors/jsonconverter/elasticsearch-sink-orders.json 48 | 49 | echo 50 | echo "--------------------------------------------------------------" 51 | echo "Check state of connectors and their tasks by running script ./check-connectors-state.sh or at Kafka Connect UI, link http://localhost:8086" 52 | echo "--------------------------------------------------------------" -------------------------------------------------------------------------------- /create-kafka-topics.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo 4 | echo "Create topic mysql.storedb.customers" 5 | echo "------------------------------------" 6 | docker exec -t zookeeper kafka-topics --create --bootstrap-server kafka:9092 --replication-factor 1 --partitions 5 --topic mysql.storedb.customers 7 | 8 | echo 9 | echo "Create topic mysql.storedb.products" 10 | echo "-----------------------------------" 11 | docker exec -t zookeeper kafka-topics --create --bootstrap-server kafka:9092 --replication-factor 1 --partitions 5 --topic mysql.storedb.products 12 | 13 | echo 14 | echo "Create topic mysql.storedb.orders" 15 | echo "---------------------------------" 16 | docker exec -t zookeeper kafka-topics --create --bootstrap-server kafka:9092 --replication-factor 1 --partitions 5 --topic mysql.storedb.orders 17 | 18 | echo 19 | echo "Create topic mysql.storedb.orders_products" 20 | echo "------------------------------------------" 21 | docker exec -t zookeeper kafka-topics --create --bootstrap-server kafka:9092 --replication-factor 1 --partitions 5 --topic mysql.storedb.orders_products 22 | 23 | echo 24 | echo "List topics" 25 | echo "-----------" 26 | docker exec -t zookeeper kafka-topics --list --bootstrap-server kafka:9092 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | mysql: 4 | image: 'mysql:9.1.0' 5 | container_name: 'mysql' 6 | restart: 'unless-stopped' 7 | ports: 8 | - '3306:3306' 9 | environment: 10 | - 'MYSQL_ROOT_PASSWORD=secret' 11 | - 'MYSQL_DATABASE=storedb' 12 | volumes: 13 | - './docker/mysql/init:/docker-entrypoint-initdb.d' 14 | healthcheck: 15 | test: 'mysqladmin ping -u root -p$${MYSQL_ROOT_PASSWORD}' 16 | 17 | elasticsearch: 18 | image: 'docker.elastic.co/elasticsearch/elasticsearch:8.15.4' 19 | container_name: 'elasticsearch' 20 | restart: 'unless-stopped' 21 | ports: 22 | - '9200:9200' 23 | - '9300:9300' 24 | environment: 25 | - 'discovery.type=single-node' 26 | - 'xpack.security.enabled=false' 27 | - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' 28 | healthcheck: 29 | test: 'curl -f http://localhost:9200 || exit 1' 30 | 31 | zookeeper: 32 | image: 'confluentinc/cp-zookeeper:7.8.0' 33 | container_name: 'zookeeper' 34 | restart: 'unless-stopped' 35 | ports: 36 | - '2181:2181' 37 | environment: 38 | - 'ZOOKEEPER_CLIENT_PORT=2181' 39 | healthcheck: 40 | test: 'echo stat | nc localhost $$ZOOKEEPER_CLIENT_PORT' 41 | 42 | kafka: 43 | image: 'confluentinc/cp-kafka:7.8.0' 44 | container_name: 'kafka' 45 | restart: 'unless-stopped' 46 | depends_on: 47 | - 'zookeeper' 48 | ports: 49 | - '9092:9092' 50 | - '29092:29092' 51 | environment: 52 | - 'KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181' 53 | - 'KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' 54 | - 'KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092' 55 | - 'KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1' 56 | healthcheck: 57 | test: [ "CMD", "nc", "-z", "localhost", "9092" ] 58 | 59 | schema-registry: 60 | image: 'confluentinc/cp-schema-registry:7.8.0' 61 | container_name: 'schema-registry' 62 | restart: 'unless-stopped' 63 | depends_on: 64 | - 'kafka' 65 | ports: 66 | - '8081:8081' 67 | environment: 68 | - 'SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=kafka:9092' 69 | - 'SCHEMA_REGISTRY_HOST_NAME=schema-registry' 70 | - 'SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081' 71 | healthcheck: 72 | test: 'curl -f http://localhost:8081 || exit 1' 73 | 74 | schema-registry-ui: 75 | image: 'landoop/schema-registry-ui:0.9.5' 76 | container_name: 'kafka-schema-registry-ui' 77 | restart: 'unless-stopped' 78 | depends_on: 79 | - 'schema-registry' 80 | ports: 81 | - '8001:8000' 82 | environment: 83 | - 'SCHEMAREGISTRY_URL=http://schema-registry:8081' 84 | - 'PROXY=true' 85 | healthcheck: 86 | test: 'wget --quiet --tries=1 --spider http://localhost:8000 || exit 1' 87 | 88 | kafka-rest-proxy: 89 | image: 'confluentinc/cp-kafka-rest:7.8.0' 90 | container_name: 'kafka-rest-proxy' 91 | restart: 'unless-stopped' 92 | depends_on: 93 | - 'zookeeper' 94 | - 'kafka' 95 | ports: 96 | - '8082:8082' 97 | environment: 98 | - 'KAFKA_REST_BOOTSTRAP_SERVERS=PLAINTEXT://kafka:9092' 99 | - 'KAFKA_REST_ZOOKEEPER_CONNECT=zookeeper:2181' 100 | - 'KAFKA_REST_HOST_NAME=kafka-rest-proxy' 101 | - 'KAFKA_REST_LISTENERS=http://0.0.0.0:8082' 102 | - 'KAFKA_REST_SCHEMA_REGISTRY_URL=http://schema-registry:8081' 103 | - 'KAFKA_REST_CONSUMER_REQUEST_TIMEOUT_MS=30000' 104 | healthcheck: 105 | test: 'curl -f http://localhost:8082 || exit 1' 106 | 107 | kafka-topics-ui: 108 | image: 'landoop/kafka-topics-ui:0.9.4' 109 | container_name: 'kafka-topics-ui' 110 | restart: 'unless-stopped' 111 | depends_on: 112 | - 'kafka-rest-proxy' 113 | ports: 114 | - '8085:8000' 115 | environment: 116 | - 'KAFKA_REST_PROXY_URL=http://kafka-rest-proxy:8082' 117 | - 'PROXY=true' 118 | healthcheck: 119 | test: 'wget --quiet --tries=1 --spider http://localhost:8000 || exit 1' 120 | 121 | kafka-connect: 122 | build: 'docker/kafka-connect' 123 | container_name: 'kafka-connect' 124 | restart: 'unless-stopped' 125 | depends_on: 126 | - 'schema-registry' 127 | ports: 128 | - '8083:8083' 129 | environment: 130 | - 'CONNECT_BOOTSTRAP_SERVERS=kafka:9092' 131 | - 'CONNECT_REST_PORT=8083' 132 | - 'CONNECT_GROUP_ID=compose-connect-group' 133 | - 'CONNECT_CONFIG_STORAGE_TOPIC=docker-connect-configs' 134 | - 'CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR=1' 135 | - 'CONNECT_OFFSET_STORAGE_TOPIC=docker-connect-offsets' 136 | - 'CONNECT_OFFSET_STORAGE_PARTITIONS=3' 137 | - 'CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR=1' 138 | - 'CONNECT_STATUS_STORAGE_TOPIC=docker-connect-status' 139 | - 'CONNECT_STATUS_STORAGE_PARTITIONS=3' 140 | - 'CONNECT_STATUS_STORAGE_REPLICATION_FACTOR=1' 141 | - 'CONNECT_KEY_CONVERTER=io.confluent.connect.avro.AvroConverter' 142 | - 'CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL=http://schema-registry:8081' 143 | - 'CONNECT_VALUE_CONVERTER=io.confluent.connect.avro.AvroConverter' 144 | - 'CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL=http://schema-registry:8081' 145 | - 'CONNECT_INTERNAL_KEY_CONVERTER=org.apache.kafka.connect.json.JsonConverter' 146 | - 'CONNECT_INTERNAL_VALUE_CONVERTER=org.apache.kafka.connect.json.JsonConverter' 147 | - 'CONNECT_REST_ADVERTISED_HOST_NAME=kafka-connect' 148 | - 'CONNECT_LOG4J_ROOT_LOGLEVEL=INFO' 149 | - 'CONNECT_LOG4J_LOGGERS=org.apache.kafka.connect.runtime.rest=WARN,org.reflections=ERROR' 150 | - 'CONNECT_PLUGIN_PATH=/usr/share/java' 151 | healthcheck: 152 | test: 'curl -f http://localhost:$$CONNECT_REST_PORT || exit 1' 153 | 154 | kafka-connect-ui: 155 | image: 'landoop/kafka-connect-ui:0.9.7' 156 | container_name: 'kafka-connect-ui' 157 | restart: 'unless-stopped' 158 | depends_on: 159 | - 'kafka-connect' 160 | ports: 161 | - '8086:8000' 162 | environment: 163 | - 'CONNECT_URL=http://kafka-connect:8083' 164 | - 'PROXY=true' 165 | healthcheck: 166 | test: 'wget --quiet --tries=1 --spider http://localhost:8000 || exit 1' 167 | 168 | kafka-manager: 169 | image: 'hlebalbau/kafka-manager:3.0.0.5' 170 | container_name: 'kafka-manager' 171 | restart: 'unless-stopped' 172 | depends_on: 173 | - 'zookeeper' 174 | ports: 175 | - '9000:9000' 176 | environment: 177 | - 'ZK_HOSTS=zookeeper:2181' 178 | - 'APPLICATION_SECRET=random-secret' 179 | command: '-Dpidfile.path=/dev/null' 180 | healthcheck: 181 | test: 'curl -f http://localhost:9000 || exit 1' 182 | -------------------------------------------------------------------------------- /docker/kafka-connect/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM confluentinc/cp-kafka-connect:7.8.0 2 | 3 | LABEL maintainer="ivangfr@yahoo.com.br" 4 | 5 | USER root 6 | RUN yum install unzip -y 7 | 8 | COPY jars/*.jar /etc/kafka-connect/jars 9 | 10 | # confluentinc-kafka-connect-elasticsearch 11 | ADD confluentinc-kafka-connect-elasticsearch-14.1.2.zip /tmp/confluentinc-kafka-connect-elasticsearch.zip 12 | RUN unzip /tmp/confluentinc-kafka-connect-elasticsearch.zip -d /usr/share/java && rm /tmp/confluentinc-kafka-connect-elasticsearch.zip 13 | 14 | # confluentinc-kafka-connect-jdbc 15 | ADD confluentinc-kafka-connect-jdbc-10.8.1.zip /tmp/confluentinc-kafka-connect-jdbc.zip 16 | RUN unzip /tmp/confluentinc-kafka-connect-jdbc.zip -d /usr/share/java && rm /tmp/confluentinc-kafka-connect-jdbc.zip 17 | -------------------------------------------------------------------------------- /docker/kafka-connect/HOW-TO.txt: -------------------------------------------------------------------------------- 1 | Access https://www.confluent.io/hub/ to download updated version of 2 | confluentinc-kafka-connect-elasticsearch and confluentinc-kafka-connect-jdbc ZIP files 3 | 4 | Access https://dev.mysql.com/downloads/connector/j/ to download updated version of 5 | mysql-connector-java ZIP file 6 | To download the latest version, you must be logged in. 7 | Once downloaded, decompress the ZIP file and copy the JAR inside it to jars folder 8 | -------------------------------------------------------------------------------- /docker/kafka-connect/confluentinc-kafka-connect-elasticsearch-14.1.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/docker/kafka-connect/confluentinc-kafka-connect-elasticsearch-14.1.2.zip -------------------------------------------------------------------------------- /docker/kafka-connect/confluentinc-kafka-connect-jdbc-10.8.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/docker/kafka-connect/confluentinc-kafka-connect-jdbc-10.8.1.zip -------------------------------------------------------------------------------- /docker/kafka-connect/jars/mysql-connector-j-9.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/docker/kafka-connect/jars/mysql-connector-j-9.1.0.jar -------------------------------------------------------------------------------- /docker/mysql/init/storedb.sql: -------------------------------------------------------------------------------- 1 | USE storedb; 2 | 3 | CREATE TABLE `customers` ( 4 | `id` bigint NOT NULL AUTO_INCREMENT, 5 | `address` varchar(255) NOT NULL, 6 | `created_at` datetime(6) NOT NULL, 7 | `email` varchar(255) NOT NULL, 8 | `name` varchar(255) NOT NULL, 9 | `phone` varchar(255) NOT NULL, 10 | `updated_at` datetime(6) NOT NULL, 11 | PRIMARY KEY (`id`) 12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 13 | 14 | CREATE TABLE `products` ( 15 | `id` bigint NOT NULL AUTO_INCREMENT, 16 | `created_at` datetime(6) NOT NULL, 17 | `name` varchar(255) NOT NULL, 18 | `price` varchar(255) NOT NULL, 19 | `updated_at` datetime(6) NOT NULL, 20 | PRIMARY KEY (`id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 22 | 23 | CREATE TABLE `orders` ( 24 | `id` varchar(255) NOT NULL, 25 | `created_at` datetime(6) NOT NULL, 26 | `payment_type` varchar(255) NOT NULL, 27 | `status` varchar(255) NOT NULL, 28 | `updated_at` datetime(6) NOT NULL, 29 | `customer_id` bigint NOT NULL, 30 | PRIMARY KEY (`id`), 31 | KEY `FK_CUSTOMER` (`customer_id`), 32 | CONSTRAINT `FK_CUSTOMER` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 34 | 35 | CREATE TABLE `orders_products` ( 36 | `order_id` varchar(255) NOT NULL, 37 | `product_id` bigint NOT NULL, 38 | `created_at` datetime(6) NOT NULL, 39 | `unit` int NOT NULL, 40 | `updated_at` datetime(6) NOT NULL, 41 | PRIMARY KEY (`order_id`,`product_id`), 42 | KEY `FK_PRODUCT` (`product_id`), 43 | CONSTRAINT `FK_ORDER` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`), 44 | CONSTRAINT `FK_PRODUCT` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) 45 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 46 | -------------------------------------------------------------------------------- /documentation/kafka-connect-ui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/documentation/kafka-connect-ui.jpeg -------------------------------------------------------------------------------- /documentation/project-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/documentation/project-diagram.png -------------------------------------------------------------------------------- /documentation/project-diagram.xml: -------------------------------------------------------------------------------- 1 | 7V1bd6O2Fv41ebQXSFwfE8+k7Tnp6XTNQ6d96SIg25zBlgfkJO6vrwBxk4QBWzgksbtWxxYgQN+3P+29dckNXGxefoq93fpXHKDoBmjByw38dAOA7Wr0/2nBIS8woZsXrOIwyIv0quBr+A9ihey61T4MUNI4kWAckXDXLPTxdot80ijz4hg/N09b4qh51523QkLBV9+LxNI/woCs81LH1Kryn1G4Whd31jV25NHzv69ivN+y+90AuMw++eGNV9TFzk/WXoCfazeFn2/gIsaY5N82LwsUpU1bNFt+3X3L0fK5Y7QlfS5wvQed/PTtF/fl6dPvlhMG+x+zmWnm1Tx50R4V72FFtMK7ZOdt08cmB9ZU1o99+qx3S7wlsyQD8paeoBs7yoW76jj9tmL/ZhU9nlXLr4evvz8UVdG3e+Srp2X5o3J3jc98eNC4HlD4dulX/xCF2wDF9JzndUjQ153np+XP1Dpo2ZpsIvpLT58g5QYKHh7LgpIxv+0JrQWx8pIZWvqAYRQtcITj7LYwMJETGOlJJMbfUe2Iln3okScUk5AS+jYKV1t6jOAde1FmbXp1fUFxyr47j50foWX1xnU+MYql9aMXnrhUDxDeIBIf6CnsKDX+/BKmBZCx67kyLMOx87J13agKGnrMmFdl1RWh6RfGaTm/n7zn7cvPt+RTvIf48Jfl/2f3v5kJBH4nBMcoeBQAjnO4GAwd2La3pk+bL2NHHcgtTsG+i7xHFH3BSUhC3Di3gPCBOyGHkgf4EROCNwXG7A0kejAcP0tr4KcXIl0H0NZEAAu1PAc/qT7pmi4AWFh4ED5JTZy+LJkxOFIbL/mtDVWusyXwv97yu9chVLQ4e5MW3RlGy7qS+HgT+uwAZ+tHJKOkU/b6frhdPWTXfNIl6rRcIsv3j6pTtw4pIK7e5C2EIm8dKOEtsMciriV2rIwM2oJ5MmchzQHKQ8fjZKb/deJUK88/HIsa9JKx4yJYmw2sgUSjHM2UdDKaAqxhqxr193Ja5SY9+fRasn5t5u3CmrDkNQp689hDbfSBaiMowxIMVIYG08ZgjtFkji0SRwcSkbBU8MaQEKfFw6Se4fdmQ/8fEXJgTeXtCaZFOCZrvMJb6jPgVLwl9tm/CVHQiJHEBqw1kXmk+49R5JHwqRlZyZqN3eELDjPGyy0bOlzDJ3gf+4hdVI90uHoc2FER8eIVIkJFGYjlW/fD1WoVhLCXKYMOF6S3JrRVtDkkP6I5c3rn/p5+26A46RaJsFUkUgdL5g5z3q7EARacZN4R2YRBkN5GKj3NDlLwVhpqVNMWReoBOB/DFIMbXWYcQIV82Ff5GCofBlQkH0JFCuXDOSYfMiWoGfUgE+5RaSkSqivexTjY++S9a456jTE0icYYY2mMe9WYwRpju4o0hq9IncYUfHnfGoPj4P17NSMojGmICiMLglQojCHL5XEooG1wm47qpM0ZeUmSRoR1TNBLSL7Vvv+ZNs08TW3QNokP31hLZT+qY6eqTm7ON/XQPzfMm/bMT+EXTkSxgObOrdrHNhoE0DVtDusf+zRBA1qTWGVaRr2gSXL6t/VRn9y+v4hFvwhsG5Ry40dmPOQse2Q42oz5EikOR0xxyIzbUGDcclsQ45ZaBHoWGFzTtqYpxbxUOkR7DLVRgAGWPW9iU8psXXo1CTp8+KEOHTH6qFz1i4AzSlNDB0yvqUXnuvBY3m5DG+70OG2J3m7e0H9fmNoT0R1L74eRfkmMRD8wCzZmubMxa+8iivgzISjFZYfikD5S6nNnRV+q392d+QsqZiOdaUlN/kjwte/v79O74zj8h97GK56hNUxQgLvucsNkUBdRl6USVIx2yFEX3bYG6qK/1mqxk6RBO/Bj4gxNbWo4i2OkHTi39ISvgzJnzVPE3ORjrdfHXBze7IV5e6d8NfGqC7cmB7ds1POaKj6aKjaB06zh1FSxUJG6zIocbNnY4zvNH/99Hao6MZFsalAUJUdiXSoSyXKeysZS27PLrMklqWWWTmbJ5exImVquMs1/9vMQ6B3vw/RF2HEvJsUT4Kc0EsjL2DkiWeqdTCtq9TT10TxcZ/LampaGcj2fxSeV+2qozk0QNfmucWwN7TG2+mG56fTlJpgUN4HD5VX4SfC9h05sLlQ3uNUQI5PTFlNmV3JyedtucsJpkZNL/+i84PUmJ8dywC/VGZucw8aMPxQ5C0y7yWlMipxQ48jJr8LoTU4+yWldmJxiSvMYOY9OaBD5eXIy4gxGTcv9g4rcv5IX408Il/NEtmzkTfNkWq6YDhQRxeCnR4039VdOlB7LRN4WUablFpUZiXOJYtocUS6tKLJ1+2+aKBNzUVR1PTZXkXlpRbmm6oen6lUtPBMqGhvso6n6KaxGy9L3c8oQ5G2S+YeYtD3+UjRTthRNNs9Tt9ot46zcuy3m3q+r3cdY7W5JdjYwC2dA+Wp3OdZiLpuqb0JtJ0Fe7K8lg/0J7Vje2qwugTuLhaYtFseEQgXcoLmDjmWKeF92rL/owobC/cZndlwEbeowTA3tYQGoJN16LKna2sh9Mqj0tgxrqzha/z0kYdsKXWcmtRC/iTiukN/45lTPlRtotXtGKRQy71A7bZeekIzh2jrDAt6hxOyTwD+DcO3MP52KBdQToaJhcBmRU5ffn0hFZURr39FjIjsifq53v/Jdfa47I0p3Riw3zhOCP2Fu/kWce0dvUl0SxxnAlTj3QMF+iXL2ixmExgYyorM3v/SCvlGQ4AYSHd0RHbEiem6skQFjASFG1F1AvIe1e+UuFEeBkC1WGg0IMdxtJLEkQLz9lX2QG96QwwAvCIMrhqHDe9NRd97j+NCezfygu+9ZkF+JaEuWp4+2A5+cVQp2pGj37UeZetSKROcQWxG7dkcWzAmYSGTBxbi6xqc2T4wsXH7C5siRhQs+ENkK0+4mm3Ml2xhkUzHlqCKOboE6dWbaXIO2vH+xLK1vTuV4Si+7Wy0d3Ja67bWwpZOH7rTSKVaxZOPszJ7pzl1dty3D1kzHAc3tfyzozh0ATeAa2TnWZUmqYLrTmAw8lWxT4ZCjKiXHVWRfeF6Dq2C205UnrTyxufXkwmS23pOdtFfmSY/JTleenM4Tbq8JYS5bb56Yr8wTMbmWtUzHkPak9qmY5IA2Pwj0+gPa7tkrLysn2G6ET3pPSZjcwKE7rYjL4dfmntr7QJPzZTlX16RhTOUHw36ubvsId7+3cVmX2DqRnNuxkTuffsmfQKn+lZPDxk5FgEnkIty+K4yLXP9ELOPd5CJ6/PWC092yI8sYTnS3ugk1rVyBYaoiiu10aLEileQDBcc+rpL8c3HnD1VJ+rP6u5v56dXfNoWf/wU= -------------------------------------------------------------------------------- /documentation/store-api-swagger.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-kafka-connect-jdbc-streams/4e28fb38d72edd84232ae7e98bb6a4306aebd91c/documentation/store-api-swagger.jpeg -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.3 9 | 10 | 11 | com.ivanfranchin 12 | springboot-kafka-connect-jdbc-streams 13 | 1.0.0 14 | pom 15 | springboot-kafka-connect-jdbc-streams 16 | Demo project for Spring Boot 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 21 32 | 3.4.4 33 | 34 | 35 | 36 | 37 | 38 | com.google.cloud.tools 39 | jib-maven-plugin 40 | ${jib-maven-plugin.version} 41 | 42 | 43 | 44 | 45 | 46 | store-api 47 | store-streams 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /remove-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker rmi springboot-kafka-connect-jdbc-streams-kafka-connect:latest 4 | docker rmi ivanfranchin/store-api:1.0.0 5 | docker rmi ivanfranchin/store-streams:1.0.0 6 | -------------------------------------------------------------------------------- /scripts/my-functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TIMEOUT=120 4 | 5 | # -- wait_for_container_log -- 6 | # $1: docker container name 7 | # S2: spring value to wait to appear in container logs 8 | function wait_for_container_log() { 9 | local log_waiting="Waiting for string '$2' in the $1 logs ..." 10 | echo "${log_waiting} It will timeout in ${TIMEOUT}s" 11 | SECONDS=0 12 | 13 | while true ; do 14 | local log=$(docker logs $1 2>&1 | grep "$2") 15 | if [ -n "$log" ] ; then 16 | echo $log 17 | break 18 | fi 19 | 20 | if [ $SECONDS -ge $TIMEOUT ] ; then 21 | echo "${log_waiting} TIMEOUT" 22 | break; 23 | fi 24 | sleep 1 25 | done 26 | } -------------------------------------------------------------------------------- /start-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source scripts/my-functions.sh 4 | 5 | echo 6 | echo "Starting store-api ..." 7 | docker run -d --rm --name store-api -p 9080:8080 \ 8 | -e MYSQL_HOST=mysql \ 9 | --network springboot-kafka-connect-jdbc-streams_default \ 10 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9080)" ] || exit 1' \ 11 | ivanfranchin/store-api:1.0.0 12 | 13 | wait_for_container_log "store-api" "Started" 14 | 15 | echo 16 | echo "Starting store-streams ..." 17 | docker run -d --rm --name store-streams -p 9081:8080 \ 18 | -e SPRING_PROFILES_ACTIVE=${1:-default} \ 19 | -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 \ 20 | -e SCHEMA_REGISTRY_HOST=schema-registry \ 21 | --network springboot-kafka-connect-jdbc-streams_default \ 22 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9081)" ] || exit 1' \ 23 | ivanfranchin/store-streams:1.0.0 24 | 25 | wait_for_container_log "store-streams" "Started" 26 | 27 | # --- 28 | # In case you want 2 instances of store-streams running, uncomment the `docker run` below 29 | # --- 30 | 31 | #docker run -d --rm --name store-streams-2 -p 9082:8080 \ 32 | # -e SPRING_PROFILES_ACTIVE=${1:-default} \ 33 | # -e KAFKA_HOST=kafka -e KAFKA_PORT=9092 \ 34 | # -e SCHEMA_REGISTRY_HOST=schema-registry \ 35 | # --network springboot-kafka-connect-jdbc-streams_default \ 36 | # --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9082)" ] || exit 1' \ 37 | # ivanfranchin/store-streams:1.0.0 38 | 39 | # wait_for_container_log "store-streams-2" "Started" -------------------------------------------------------------------------------- /stop-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker stop store-api store-streams 4 | 5 | # In case you ran 2 instances of store-streams running, uncomment the `docker stop` below 6 | # --- 7 | #docker stop store-streams-2 8 | -------------------------------------------------------------------------------- /store-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | springboot-kafka-connect-jdbc-streams 8 | 1.0.0 9 | ../pom.xml 10 | 11 | store-api 12 | store-api 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 2.8.5 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-data-jpa 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-validation 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-web 46 | 47 | 48 | 49 | 50 | org.springdoc 51 | springdoc-openapi-starter-webmvc-ui 52 | ${springdoc-openapi.version} 53 | 54 | 55 | 56 | com.mysql 57 | mysql-connector-j 58 | runtime 59 | 60 | 61 | org.projectlombok 62 | lombok 63 | true 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-test 68 | test 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-compiler-plugin 77 | 78 | 79 | 80 | org.projectlombok 81 | lombok 82 | 83 | 84 | 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-maven-plugin 89 | 90 | 91 | 92 | org.projectlombok 93 | lombok 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/StoreApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StoreApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StoreApiApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | import com.ivanfranchin.storeapi.customer.dto.AddCustomerRequest; 5 | import com.ivanfranchin.storeapi.customer.dto.CustomerResponse; 6 | import com.ivanfranchin.storeapi.customer.dto.UpdateCustomerRequest; 7 | import jakarta.validation.Valid; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.DeleteMapping; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PatchMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.ResponseStatus; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import java.util.List; 21 | 22 | @RequiredArgsConstructor 23 | @RestController 24 | @RequestMapping("/api/customers") 25 | public class CustomerController { 26 | 27 | private final CustomerService customerService; 28 | 29 | @GetMapping 30 | public List getAllCustomers() { 31 | return customerService.getAllCustomers() 32 | .stream() 33 | .map(CustomerResponse::from) 34 | .toList(); 35 | } 36 | 37 | @GetMapping("/{id}") 38 | public CustomerResponse getCustomer(@PathVariable Long id) { 39 | Customer customer = customerService.validateAndGetCustomerById(id); 40 | return CustomerResponse.from(customer); 41 | } 42 | 43 | @ResponseStatus(HttpStatus.CREATED) 44 | @PostMapping 45 | public CustomerResponse addCustomer(@Valid @RequestBody AddCustomerRequest addCustomerRequest) { 46 | Customer customer = Customer.from(addCustomerRequest); 47 | customer = customerService.saveCustomer(customer); 48 | return CustomerResponse.from(customer); 49 | } 50 | 51 | @PatchMapping("/{id}") 52 | public CustomerResponse updateCustomer(@PathVariable Long id, @Valid @RequestBody UpdateCustomerRequest updateCustomerRequest) { 53 | Customer customer = customerService.validateAndGetCustomerById(id); 54 | Customer.updateFrom(updateCustomerRequest, customer); 55 | customer = customerService.saveCustomer(customer); 56 | return CustomerResponse.from(customer); 57 | } 58 | 59 | @DeleteMapping("/{id}") 60 | public CustomerResponse deleteCustomer(@PathVariable Long id) { 61 | Customer customer = customerService.validateAndGetCustomerById(id); 62 | customerService.deleteCustomer(customer); 63 | return CustomerResponse.from(customer); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CustomerRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer; 2 | 3 | import com.ivanfranchin.storeapi.customer.exception.CustomerDeletionException; 4 | import com.ivanfranchin.storeapi.customer.exception.CustomerNotFoundException; 5 | import com.ivanfranchin.storeapi.customer.model.Customer; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.dao.DataIntegrityViolationException; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | 12 | @RequiredArgsConstructor 13 | @Service 14 | public class CustomerService { 15 | 16 | private final CustomerRepository customerRepository; 17 | 18 | public List getAllCustomers() { 19 | return customerRepository.findAll(); 20 | } 21 | 22 | public Customer saveCustomer(Customer customer) { 23 | return customerRepository.save(customer); 24 | } 25 | 26 | public void deleteCustomer(Customer customer) { 27 | try { 28 | customerRepository.delete(customer); 29 | } catch (DataIntegrityViolationException e) { 30 | throw new CustomerDeletionException(customer.getId()); 31 | } 32 | } 33 | 34 | public Customer validateAndGetCustomerById(Long id) { 35 | return customerRepository.findById(id).orElseThrow(() -> new CustomerNotFoundException(id)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/dto/AddCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | 7 | public record AddCustomerRequest( 8 | @Schema(example = "Ivan Franchin") @NotBlank String name, 9 | @Schema(example = "ivan.franchin@test.com") @NotBlank @Email String email, 10 | @Schema(example = "Street Brooklyn 123") @NotBlank String address, 11 | @Schema(example = "445566") @NotBlank String phone) { 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/dto/CustomerResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.dto; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | 5 | public record CustomerResponse(Long id, String name, String email, String address, String phone) { 6 | 7 | public static CustomerResponse from(Customer customer) { 8 | return new CustomerResponse( 9 | customer.getId(), 10 | customer.getName(), 11 | customer.getEmail(), 12 | customer.getAddress(), 13 | customer.getPhone() 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/dto/UpdateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Email; 5 | 6 | public record UpdateCustomerRequest( 7 | @Schema(example = "Ivan Franchin 2") String name, 8 | @Schema(example = "ivan.franchin.2@test.com") @Email String email, 9 | @Schema(example = "Street Bronx 456") String address, 10 | @Schema(example = "778899") String phone) { 11 | } 12 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/exception/CustomerDeletionException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.CONFLICT) 7 | public class CustomerDeletionException extends RuntimeException { 8 | 9 | public CustomerDeletionException(Long id) { 10 | super(String.format("Customer with id '%s' cannot be deleted", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/exception/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class CustomerNotFoundException extends RuntimeException { 8 | 9 | public CustomerNotFoundException(Long id) { 10 | super(String.format("Customer with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/customer/model/Customer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.customer.model; 2 | 3 | import com.ivanfranchin.storeapi.customer.dto.AddCustomerRequest; 4 | import com.ivanfranchin.storeapi.customer.dto.UpdateCustomerRequest; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.PrePersist; 11 | import jakarta.persistence.PreUpdate; 12 | import jakarta.persistence.Table; 13 | import lombok.Data; 14 | 15 | import java.time.LocalDateTime; 16 | 17 | @Data 18 | @Entity 19 | @Table(name = "customers") 20 | public class Customer { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | @Column(nullable = false) 27 | private String name; 28 | 29 | @Column(nullable = false) 30 | private String email; 31 | 32 | @Column(nullable = false) 33 | private String address; 34 | 35 | @Column(nullable = false) 36 | private String phone; 37 | 38 | @Column(nullable = false) 39 | private LocalDateTime createdAt; 40 | 41 | @Column(nullable = false) 42 | private LocalDateTime updatedAt; 43 | 44 | @PrePersist 45 | public void onPrePersist() { 46 | createdAt = updatedAt = LocalDateTime.now(); 47 | } 48 | 49 | @PreUpdate 50 | public void onPreUpdate() { 51 | updatedAt = LocalDateTime.now(); 52 | } 53 | 54 | public static Customer from(AddCustomerRequest addCustomerRequest) { 55 | Customer customer = new Customer(); 56 | customer.setName(addCustomerRequest.name()); 57 | customer.setEmail(addCustomerRequest.email()); 58 | customer.setAddress(addCustomerRequest.address()); 59 | customer.setPhone(addCustomerRequest.phone()); 60 | return customer; 61 | } 62 | 63 | public static void updateFrom(UpdateCustomerRequest updateCustomerRequest, Customer customer) { 64 | if (updateCustomerRequest.name() != null) { 65 | customer.setName(updateCustomerRequest.name()); 66 | } 67 | if (updateCustomerRequest.email() != null) { 68 | customer.setEmail(updateCustomerRequest.email()); 69 | } 70 | if (updateCustomerRequest.address() != null) { 71 | customer.setAddress(updateCustomerRequest.address()); 72 | } 73 | if (updateCustomerRequest.phone() != null) { 74 | customer.setPhone(updateCustomerRequest.phone()); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order; 2 | 3 | import com.ivanfranchin.storeapi.order.dto.CreateOrderRequest; 4 | import com.ivanfranchin.storeapi.order.dto.OrderResponse; 5 | import com.ivanfranchin.storeapi.order.dto.UpdateOrderRequest; 6 | import com.ivanfranchin.storeapi.order.model.Order; 7 | import jakarta.validation.Valid; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PatchMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.ResponseStatus; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import java.util.List; 20 | import java.util.UUID; 21 | 22 | @RequiredArgsConstructor 23 | @RestController 24 | @RequestMapping("/api/orders") 25 | public class OrderController { 26 | 27 | private final OrderService orderService; 28 | 29 | @GetMapping 30 | public List getAllOrders() { 31 | return orderService.getAllOrders() 32 | .stream() 33 | .map(OrderResponse::from) 34 | .toList(); 35 | } 36 | 37 | @GetMapping("/{id}") 38 | public OrderResponse getOrder(@PathVariable UUID id) { 39 | Order order = orderService.validateAndGetOrderById(id.toString()); 40 | return OrderResponse.from(order); 41 | } 42 | 43 | @ResponseStatus(HttpStatus.CREATED) 44 | @PostMapping 45 | public OrderResponse createOrder(@Valid @RequestBody CreateOrderRequest createOrderRequest) { 46 | Order order = orderService.createOrderFrom(createOrderRequest); 47 | order = orderService.saveOrder(order); 48 | return OrderResponse.from(order); 49 | } 50 | 51 | @PatchMapping("/{id}") 52 | public OrderResponse updateOrder(@PathVariable UUID id, @Valid @RequestBody UpdateOrderRequest updateOrderRequest) { 53 | Order order = orderService.validateAndGetOrderById(id.toString()); 54 | Order.updateFrom(updateOrderRequest, order); 55 | order = orderService.saveOrder(order); 56 | return OrderResponse.from(order); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order; 2 | 3 | import com.ivanfranchin.storeapi.order.model.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface OrderRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/OrderService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order; 2 | 3 | import com.ivanfranchin.storeapi.customer.CustomerService; 4 | import com.ivanfranchin.storeapi.order.dto.CreateOrderRequest; 5 | import com.ivanfranchin.storeapi.order.exception.OrderNotFoundException; 6 | import com.ivanfranchin.storeapi.order.model.Order; 7 | import com.ivanfranchin.storeapi.order.model.OrderProduct; 8 | import com.ivanfranchin.storeapi.product.ProductService; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | @RequiredArgsConstructor 16 | @Service 17 | public class OrderService { 18 | 19 | private final OrderRepository orderRepository; 20 | private final CustomerService customerService; 21 | private final ProductService productService; 22 | 23 | public List getAllOrders() { 24 | return orderRepository.findAll(); 25 | } 26 | 27 | public Order saveOrder(Order order) { 28 | return orderRepository.save(order); 29 | } 30 | 31 | public Order validateAndGetOrderById(String id) { 32 | return orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); 33 | } 34 | 35 | public Order createOrderFrom(CreateOrderRequest createOrderRequest) { 36 | Order order = new Order(); 37 | order.setId(UUID.randomUUID().toString()); 38 | order.setPaymentType(createOrderRequest.paymentType()); 39 | order.setStatus(createOrderRequest.status()); 40 | order.setCustomer(customerService.validateAndGetCustomerById(createOrderRequest.customerId())); 41 | 42 | for (CreateOrderRequest.CreateOrderProductRequest p : createOrderRequest.products()) { 43 | OrderProduct orderProduct = new OrderProduct(); 44 | orderProduct.setOrder(order); 45 | orderProduct.setProduct(productService.validateAndGetProductById(p.id())); 46 | orderProduct.setUnit(p.unit()); 47 | order.getOrderProducts().add(orderProduct); 48 | } 49 | return order; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/dto/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.dto; 2 | 3 | import com.ivanfranchin.storeapi.order.model.OrderStatus; 4 | import com.ivanfranchin.storeapi.order.model.PaymentType; 5 | import io.swagger.v3.oas.annotations.media.Schema; 6 | import jakarta.validation.Valid; 7 | import jakarta.validation.constraints.NotEmpty; 8 | import jakarta.validation.constraints.NotNull; 9 | import jakarta.validation.constraints.Positive; 10 | 11 | import java.util.List; 12 | 13 | public record CreateOrderRequest( 14 | @Schema(example = "1") @NotNull Long customerId, 15 | @Schema(example = "BITCOIN") @NotNull PaymentType paymentType, 16 | @Schema(example = "OPEN") @NotNull OrderStatus status, 17 | @Valid @NotNull @NotEmpty List products) { 18 | 19 | public record CreateOrderProductRequest( 20 | @Schema(example = "15") @NotNull Long id, 21 | @Schema(example = "1") @NotNull @Positive Integer unit) { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/dto/OrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.dto; 2 | 3 | import com.ivanfranchin.storeapi.order.model.Order; 4 | import com.ivanfranchin.storeapi.order.model.OrderStatus; 5 | import com.ivanfranchin.storeapi.order.model.PaymentType; 6 | 7 | import java.util.List; 8 | 9 | public record OrderResponse(String id, Long customerId, PaymentType paymentType, OrderStatus status, 10 | List products) { 11 | 12 | public record ProductResponse(Long id, Integer unit) { 13 | } 14 | 15 | public static OrderResponse from(Order order) { 16 | return new OrderResponse( 17 | order.getId(), 18 | order.getCustomer().getId(), 19 | order.getPaymentType(), 20 | order.getStatus(), 21 | order.getOrderProducts() 22 | .stream() 23 | .map(op -> new ProductResponse(op.getProduct().getId(), op.getUnit())) 24 | .toList() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/dto/UpdateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.dto; 2 | 3 | import com.ivanfranchin.storeapi.order.model.OrderStatus; 4 | import com.ivanfranchin.storeapi.order.model.PaymentType; 5 | import io.swagger.v3.oas.annotations.media.Schema; 6 | 7 | public record UpdateOrderRequest( 8 | @Schema(example = "CASH") PaymentType paymentType, 9 | @Schema(example = "PAYED") OrderStatus status) { 10 | } 11 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/exception/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class OrderNotFoundException extends RuntimeException { 8 | 9 | public OrderNotFoundException(String id) { 10 | super(String.format("Order with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.model; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | import com.ivanfranchin.storeapi.order.dto.UpdateOrderRequest; 5 | import jakarta.persistence.CascadeType; 6 | import jakarta.persistence.Column; 7 | import jakarta.persistence.Entity; 8 | import jakarta.persistence.EnumType; 9 | import jakarta.persistence.Enumerated; 10 | import jakarta.persistence.FetchType; 11 | import jakarta.persistence.ForeignKey; 12 | import jakarta.persistence.Id; 13 | import jakarta.persistence.JoinColumn; 14 | import jakarta.persistence.ManyToOne; 15 | import jakarta.persistence.OneToMany; 16 | import jakarta.persistence.PrePersist; 17 | import jakarta.persistence.PreUpdate; 18 | import jakarta.persistence.Table; 19 | import lombok.Data; 20 | import lombok.EqualsAndHashCode; 21 | import lombok.ToString; 22 | 23 | import java.time.LocalDateTime; 24 | import java.util.LinkedHashSet; 25 | import java.util.Set; 26 | 27 | @Data 28 | @ToString(exclude = "orderProducts") 29 | @EqualsAndHashCode(exclude = "orderProducts") 30 | @Entity 31 | @Table(name = "orders") 32 | public class Order { 33 | 34 | @Id 35 | private String id; 36 | 37 | @ManyToOne(fetch = FetchType.LAZY) 38 | @JoinColumn(name = "customer_id", nullable = false, foreignKey = @ForeignKey(name = "FK_CUSTOMER")) 39 | private Customer customer; 40 | 41 | @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) 42 | private Set orderProducts = new LinkedHashSet<>(); 43 | 44 | @Column(nullable = false) 45 | @Enumerated(EnumType.STRING) 46 | private PaymentType paymentType; 47 | 48 | @Column(nullable = false) 49 | @Enumerated(EnumType.STRING) 50 | private OrderStatus status; 51 | 52 | @Column(nullable = false) 53 | private LocalDateTime createdAt; 54 | 55 | @Column(nullable = false) 56 | private LocalDateTime updatedAt; 57 | 58 | @PrePersist 59 | public void onPrePersist() { 60 | createdAt = updatedAt = LocalDateTime.now(); 61 | } 62 | 63 | @PreUpdate 64 | public void onPreUpdate() { 65 | updatedAt = LocalDateTime.now(); 66 | } 67 | 68 | public static void updateFrom(UpdateOrderRequest updateOrderRequest, Order order) { 69 | if (updateOrderRequest.paymentType() != null) { 70 | order.setPaymentType(updateOrderRequest.paymentType()); 71 | } 72 | if (updateOrderRequest.status() != null) { 73 | order.setStatus(updateOrderRequest.status()); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/model/OrderProduct.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.model; 2 | 3 | import com.ivanfranchin.storeapi.product.model.Product; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.ForeignKey; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.IdClass; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.PrePersist; 12 | import jakarta.persistence.PreUpdate; 13 | import jakarta.persistence.Table; 14 | import lombok.Data; 15 | 16 | import java.time.LocalDateTime; 17 | 18 | @Data 19 | @Entity 20 | @Table(name = "orders_products") 21 | @IdClass(OrderProductPk.class) 22 | public class OrderProduct { 23 | 24 | @Id 25 | @ManyToOne 26 | @JoinColumn(name = "order_id", nullable = false, foreignKey = @ForeignKey(name = "FK_ORDER")) 27 | private Order order; 28 | 29 | @Id 30 | @ManyToOne 31 | @JoinColumn(name = "product_id", nullable = false, foreignKey = @ForeignKey(name = "FK_PRODUCT")) 32 | private Product product; 33 | 34 | @Column(nullable = false) 35 | private Integer unit; 36 | 37 | @Column(nullable = false) 38 | private LocalDateTime createdAt; 39 | 40 | @Column(nullable = false) 41 | private LocalDateTime updatedAt; 42 | 43 | @PrePersist 44 | public void onPrePersist() { 45 | createdAt = updatedAt = LocalDateTime.now(); 46 | } 47 | 48 | @PreUpdate 49 | public void onPreUpdate() { 50 | updatedAt = LocalDateTime.now(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/model/OrderProductPk.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class OrderProductPk implements Serializable { 9 | 10 | private String order; 11 | private Long product; 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/model/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.model; 2 | 3 | public enum OrderStatus { 4 | 5 | OPEN, PENDING, PAYED, CANCELLED 6 | 7 | } 8 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/order/model/PaymentType.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.order.model; 2 | 3 | public enum PaymentType { 4 | 5 | CASH, CREDIT_CARD, PAYPAL, BITCOIN 6 | 7 | } 8 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product; 2 | 3 | import com.ivanfranchin.storeapi.product.model.Product; 4 | import com.ivanfranchin.storeapi.product.dto.AddProductRequest; 5 | import com.ivanfranchin.storeapi.product.dto.ProductResponse; 6 | import com.ivanfranchin.storeapi.product.dto.UpdateProductRequest; 7 | import jakarta.validation.Valid; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.DeleteMapping; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PatchMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.ResponseStatus; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import java.util.List; 21 | 22 | @RequiredArgsConstructor 23 | @RestController 24 | @RequestMapping("/api/products") 25 | public class ProductController { 26 | 27 | private final ProductService productService; 28 | 29 | @GetMapping 30 | public List getAllProducts() { 31 | return productService.getAllProducts() 32 | .stream() 33 | .map(ProductResponse::from) 34 | .toList(); 35 | } 36 | 37 | @GetMapping("/{id}") 38 | public ProductResponse getProduct(@PathVariable Long id) { 39 | Product product = productService.validateAndGetProductById(id); 40 | return ProductResponse.from(product); 41 | } 42 | 43 | @ResponseStatus(HttpStatus.CREATED) 44 | @PostMapping 45 | public ProductResponse addProduct(@Valid @RequestBody AddProductRequest addProductRequest) { 46 | Product product = Product.from(addProductRequest); 47 | product = productService.saveProduct(product); 48 | return ProductResponse.from(product); 49 | } 50 | 51 | @PatchMapping("/{id}") 52 | public ProductResponse updateProduct(@PathVariable Long id, @Valid @RequestBody UpdateProductRequest updateProductRequest) { 53 | Product product = productService.validateAndGetProductById(id); 54 | Product.updateFrom(updateProductRequest, product); 55 | product = productService.saveProduct(product); 56 | return ProductResponse.from(product); 57 | } 58 | 59 | @DeleteMapping("/{id}") 60 | public ProductResponse deleteProduct(@PathVariable Long id) { 61 | Product product = productService.validateAndGetProductById(id); 62 | productService.deleteProduct(product); 63 | return ProductResponse.from(product); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product; 2 | 3 | import com.ivanfranchin.storeapi.product.model.Product; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface ProductRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product; 2 | 3 | import com.ivanfranchin.storeapi.product.exception.ProductDeletionException; 4 | import com.ivanfranchin.storeapi.product.exception.ProductNotFoundException; 5 | import com.ivanfranchin.storeapi.product.model.Product; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.dao.DataIntegrityViolationException; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | 12 | @RequiredArgsConstructor 13 | @Service 14 | public class ProductService { 15 | 16 | private final ProductRepository productRepository; 17 | 18 | public List getAllProducts() { 19 | return productRepository.findAll(); 20 | } 21 | 22 | public Product saveProduct(Product product) { 23 | return productRepository.save(product); 24 | } 25 | 26 | public void deleteProduct(Product product) { 27 | try { 28 | productRepository.delete(product); 29 | } catch (DataIntegrityViolationException e) { 30 | throw new ProductDeletionException(product.getId()); 31 | } 32 | } 33 | 34 | public Product validateAndGetProductById(Long id) { 35 | return productRepository.findById(id).orElseThrow(() -> new ProductNotFoundException(id)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/dto/AddProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Positive; 7 | 8 | import java.math.BigDecimal; 9 | 10 | public record AddProductRequest( 11 | @Schema(example = "MacBook Pro") @NotBlank String name, 12 | @Schema(example = "2500") @NotNull @Positive BigDecimal price) { 13 | } 14 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/dto/ProductResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.dto; 2 | 3 | import com.ivanfranchin.storeapi.product.model.Product; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public record ProductResponse(Long id, String name, BigDecimal price) { 8 | 9 | public static ProductResponse from(Product product) { 10 | return new ProductResponse( 11 | product.getId(), 12 | product.getName(), 13 | new BigDecimal(product.getPrice()) 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/dto/UpdateProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Positive; 5 | 6 | import java.math.BigDecimal; 7 | 8 | public record UpdateProductRequest( 9 | @Schema(example = "MacBook Air") String name, 10 | @Schema(example = "2450") @Positive BigDecimal price) { 11 | } 12 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/exception/ProductDeletionException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.CONFLICT) 7 | public class ProductDeletionException extends RuntimeException { 8 | 9 | public ProductDeletionException(Long id) { 10 | super(String.format("Product with id '%s' cannot be deleted", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/exception/ProductNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class ProductNotFoundException extends RuntimeException { 8 | 9 | public ProductNotFoundException(Long id) { 10 | super(String.format("Product with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/product/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.product.model; 2 | 3 | import com.ivanfranchin.storeapi.product.dto.AddProductRequest; 4 | import com.ivanfranchin.storeapi.product.dto.UpdateProductRequest; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.PrePersist; 11 | import jakarta.persistence.PreUpdate; 12 | import jakarta.persistence.Table; 13 | import lombok.Data; 14 | 15 | import java.time.LocalDateTime; 16 | 17 | @Data 18 | @Entity 19 | @Table(name = "products") 20 | public class Product { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | @Column(nullable = false) 27 | private String name; 28 | 29 | @Column(nullable = false) 30 | // Using String as type for price field for now due to the issue below 31 | // numeric.mapping doesn't work for DECIMAL fields #563: https://github.com/confluentinc/kafka-connect-jdbc/issues/563 32 | // private BigDecimal price; 33 | private String price; 34 | 35 | @Column(nullable = false) 36 | private LocalDateTime createdAt; 37 | 38 | @Column(nullable = false) 39 | private LocalDateTime updatedAt; 40 | 41 | @PrePersist 42 | public void onPrePersist() { 43 | createdAt = updatedAt = LocalDateTime.now(); 44 | } 45 | 46 | @PreUpdate 47 | public void onPreUpdate() { 48 | updatedAt = LocalDateTime.now(); 49 | } 50 | 51 | public static Product from(AddProductRequest addProductRequest) { 52 | Product product = new Product(); 53 | product.setName(addProductRequest.name()); 54 | product.setPrice(addProductRequest.price().toString()); 55 | return product; 56 | } 57 | 58 | public static void updateFrom(UpdateProductRequest updateProductRequest, Product product) { 59 | if (updateProductRequest.name() != null) { 60 | product.setName(updateProductRequest.name()); 61 | } 62 | if (updateProductRequest.price() != null) { 63 | product.setPrice(updateProductRequest.price().toString()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/runner/LoadSamples.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.runner; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | import com.ivanfranchin.storeapi.product.model.Product; 5 | import com.ivanfranchin.storeapi.customer.CustomerService; 6 | import com.ivanfranchin.storeapi.product.ProductService; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.math.BigDecimal; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | @Slf4j 18 | @RequiredArgsConstructor 19 | @Component 20 | public class LoadSamples implements CommandLineRunner { 21 | 22 | @Value("${load-samples.customers.enabled}") 23 | private boolean loadCustomersEnabled; 24 | 25 | @Value("${load-samples.products.enabled}") 26 | private boolean loadProductsEnabled; 27 | 28 | private final CustomerService customerService; 29 | private final ProductService productService; 30 | 31 | @Override 32 | public void run(String... args) { 33 | 34 | if (loadCustomersEnabled || loadProductsEnabled) { 35 | 36 | log.info("## Start loading samples of customers and products ..."); 37 | 38 | if (loadCustomersEnabled) { 39 | if (customerService.getAllCustomers().isEmpty()) { 40 | customers.forEach(customerRecord -> { 41 | String[] customerArr = customerRecord.split(";"); 42 | Customer customer = new Customer(); 43 | customer.setName(customerArr[0]); 44 | customer.setEmail(customerArr[1]); 45 | customer.setAddress(customerArr[2]); 46 | customer.setPhone(customerArr[3]); 47 | customerService.saveCustomer(customer); 48 | log.info("Customer created: {}", customer); 49 | }); 50 | } else { 51 | log.info("Sample of customers already created"); 52 | } 53 | } 54 | 55 | if (loadProductsEnabled) { 56 | if (productService.getAllProducts().isEmpty()) { 57 | products.forEach(productsRecord -> { 58 | String[] productArr = productsRecord.split(";"); 59 | Product product = new Product(); 60 | product.setName(productArr[0]); 61 | product.setPrice(new BigDecimal(productArr[1]).toString()); 62 | productService.saveProduct(product); 63 | log.info("Product created: {}", product); 64 | }); 65 | } else { 66 | log.info("Sample of products already created"); 67 | } 68 | } 69 | log.info("## Finished successfully loading samples of customers and products!"); 70 | } 71 | } 72 | 73 | private static final List customers = Arrays.asList( 74 | "John Gates;john.gates@test.com;street 1;112233", 75 | "Mark Bacon;mark.bacon@test.com;street 2;112244", 76 | "Alex Stone;alex.stone@test.com;street 3;112255", 77 | "Susan Spice;susan.spice@test.com;street 4;112266", 78 | "Peter Lopes;peter.lopes@test.com;street 5;112277", 79 | "Mikael Lopes;mikael.lopes@test.com;street 6;112288", 80 | "Renato Souza;renato.souza@test.com;street 7;112299", 81 | "Paul Schneider;paul.schneider@test.com;street 8;113300", 82 | "Tobias Bohn;tobias.bohn@test.com;street 9;113311", 83 | "John Star;john.star@test.com;street 10;113322", 84 | "Rick Sander;rick.sander@test.com;street 11;113333", 85 | "Nakito Hashi;nakito.hashi@test.com;street 12;113344", 86 | "Kyo Lo;kyo.lo@test.com;street 13;113355", 87 | "David Cube;david.cube@test.com;street 14;113366"); 88 | 89 | private static final List products = Arrays.asList( 90 | "iPhone Xr;900", "iPhone Xs;1100", "iPhone X;1000", "iPhone 8;700", "iPhone 7;600", "iPhone SE;500", 91 | "iPad Pro;800", "iPad Air 2;700", "iPad Mini 4;600", 92 | "MacBook Pro;2500", "MacBook Air;2000", "Mac Mini;1000", "iMac;1500", "iMac Pro;2000", 93 | "Apple Watch Series 3;350", "Apple Watch Series 4;400", "Apple TV;350"); 94 | } 95 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/simulation/SimulationController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.simulation; 2 | 3 | import com.ivanfranchin.storeapi.customer.model.Customer; 4 | import com.ivanfranchin.storeapi.order.model.Order; 5 | import com.ivanfranchin.storeapi.order.model.OrderProduct; 6 | import com.ivanfranchin.storeapi.order.model.OrderStatus; 7 | import com.ivanfranchin.storeapi.order.model.PaymentType; 8 | import com.ivanfranchin.storeapi.product.model.Product; 9 | import com.ivanfranchin.storeapi.simulation.dto.RandomOrdersRequest; 10 | import com.ivanfranchin.storeapi.customer.CustomerService; 11 | import com.ivanfranchin.storeapi.order.OrderService; 12 | import com.ivanfranchin.storeapi.product.ProductService; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import java.security.SecureRandom; 22 | import java.util.ArrayList; 23 | import java.util.HashSet; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.Random; 27 | import java.util.Set; 28 | import java.util.UUID; 29 | 30 | @Slf4j 31 | @RequiredArgsConstructor 32 | @RestController 33 | @RequestMapping("/api/simulation") 34 | public class SimulationController { 35 | 36 | @Value("${simulation.orders.total}") 37 | private Integer total; 38 | 39 | @Value("${simulation.orders.sleep}") 40 | private Integer sleep; 41 | 42 | private final CustomerService customerService; 43 | private final ProductService productService; 44 | private final OrderService orderService; 45 | 46 | @PostMapping("/orders") 47 | public List createRandomOrders(@RequestBody RandomOrdersRequest randomOrdersRequest) throws InterruptedException { 48 | total = randomOrdersRequest.total() == null ? total : randomOrdersRequest.total(); 49 | sleep = randomOrdersRequest.sleep() == null ? sleep : randomOrdersRequest.sleep(); 50 | 51 | log.info("## Running order simulation - total: {}, sleep: {}", total, sleep); 52 | 53 | List orderIds = new ArrayList<>(); 54 | List customers = customerService.getAllCustomers(); 55 | List products = productService.getAllProducts(); 56 | 57 | for (int i = 0; i < total; i++) { 58 | Order order = new Order(); 59 | order.setId(UUID.randomUUID().toString()); 60 | order.setPaymentType(PaymentType.values()[random.nextInt(PaymentType.values().length)]); 61 | order.setStatus(OrderStatus.values()[random.nextInt(OrderStatus.values().length)]); 62 | 63 | Customer customer = customers.get(random.nextInt(customers.size())); 64 | order.setCustomer(customer); 65 | 66 | Set orderProducts = new HashSet<>(); 67 | int numProducts = random.nextInt(3) + 1; 68 | for (int j = 0; j < numProducts; j++) { 69 | Product product = products.get(random.nextInt(products.size())); 70 | int unit = random.nextInt(3) + 1; 71 | 72 | Optional orderProductOptional = orderProducts.stream() 73 | .filter(op -> op.getProduct().getId().equals(product.getId())) 74 | .findAny(); 75 | 76 | if (orderProductOptional.isPresent()) { 77 | OrderProduct existingOrderProduct = orderProductOptional.get(); 78 | existingOrderProduct.setUnit(existingOrderProduct.getUnit() + unit); 79 | } else { 80 | OrderProduct orderProduct = new OrderProduct(); 81 | orderProduct.setProduct(product); 82 | orderProduct.setUnit(unit); 83 | orderProduct.setOrder(order); 84 | orderProducts.add(orderProduct); 85 | } 86 | } 87 | order.setOrderProducts(orderProducts); 88 | 89 | order = orderService.saveOrder(order); 90 | orderIds.add(order.getId()); 91 | log.info("Order created: {}", order); 92 | 93 | Thread.sleep(sleep); 94 | } 95 | 96 | log.info("## Order simulation finished successfully!"); 97 | 98 | return orderIds; 99 | } 100 | 101 | private static final Random random = new SecureRandom(); 102 | } 103 | -------------------------------------------------------------------------------- /store-api/src/main/java/com/ivanfranchin/storeapi/simulation/dto/RandomOrdersRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi.simulation.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record RandomOrdersRequest( 6 | @Schema(example = "10") Integer total, 7 | @Schema(example = "100") Integer sleep) { 8 | } 9 | -------------------------------------------------------------------------------- /store-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: store-api 4 | datasource: 5 | url: jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/storedb?characterEncoding=UTF-8&serverTimezone=UTC 6 | username: root 7 | password: secret 8 | 9 | logging.level: 10 | org.hibernate: 11 | SQL: DEBUG 12 | # type.descriptor.sql.BasicBinder: TRACE 13 | 14 | management: 15 | endpoints: 16 | web: 17 | exposure.include: beans, env, health, info, metrics, mappings 18 | endpoint: 19 | health: 20 | show-details: always 21 | 22 | springdoc: 23 | show-actuator: true 24 | swagger-ui: 25 | groups-order: DESC 26 | disable-swagger-default-url: true 27 | 28 | load-samples: 29 | customers.enabled: true 30 | products.enabled: true 31 | 32 | simulation: 33 | orders: 34 | total: 10 35 | sleep: 100 36 | -------------------------------------------------------------------------------- /store-api/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | ___| |_ ___ _ __ ___ __ _ _ __ (_) 3 | / __| __/ _ \| '__/ _ \_____ / _` | '_ \| | 4 | \__ \ || (_) | | | __/_____| (_| | |_) | | 5 | |___/\__\___/|_| \___| \__,_| .__/|_| 6 | |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /store-api/src/test/java/com/ivanfranchin/storeapi/StoreApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storeapi; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class StoreApiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /store-streams/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | springboot-kafka-connect-jdbc-streams 8 | 1.0.0 9 | ../pom.xml 10 | 11 | store-streams 12 | store-streams 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 2024.0.0 29 | 1.11.4 30 | 7.8.0 31 | 2.8.5 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-web 41 | 42 | 43 | org.apache.kafka 44 | kafka-streams 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-stream 49 | 50 | 51 | org.springframework.cloud 52 | spring-cloud-stream-binder-kafka-streams 53 | 54 | 55 | 56 | 57 | org.apache.avro 58 | avro 59 | ${avro.version} 60 | 61 | 62 | 63 | 64 | io.confluent 65 | kafka-streams-avro-serde 66 | ${confluent.version} 67 | 68 | 69 | io.confluent 70 | kafka-avro-serializer 71 | ${confluent.version} 72 | 73 | 74 | 75 | 76 | org.springdoc 77 | springdoc-openapi-starter-webmvc-ui 78 | ${springdoc-openapi.version} 79 | 80 | 81 | 82 | org.projectlombok 83 | lombok 84 | true 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-test 89 | test 90 | 91 | 92 | org.springframework.cloud 93 | spring-cloud-stream-test-binder 94 | test 95 | 96 | 97 | 98 | 99 | 100 | org.springframework.cloud 101 | spring-cloud-dependencies 102 | ${spring-cloud.version} 103 | pom 104 | import 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.apache.avro 113 | avro-maven-plugin 114 | ${avro.version} 115 | 116 | 117 | generate-sources 118 | 119 | schema 120 | 121 | 122 | 124 | 125 | ${project.basedir}/src/main/resources/avro/product-detail-message.avsc 126 | 127 | ${project.basedir}/src/main/resources/avro 128 | ${project.basedir}/src/main/java 129 | 130 | 131 | 132 | 133 | 134 | org.springframework.boot 135 | spring-boot-maven-plugin 136 | 137 | 138 | 139 | org.projectlombok 140 | lombok 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | confluent 151 | https://packages.confluent.io/maven/ 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/avro/OrderProduct.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Autogenerated by Avro 3 | * 4 | * DO NOT EDIT DIRECTLY 5 | */ 6 | package com.ivanfranchin.commons.storeapp.avro; 7 | 8 | import org.apache.avro.generic.GenericArray; 9 | import org.apache.avro.specific.SpecificData; 10 | import org.apache.avro.util.Utf8; 11 | import org.apache.avro.message.BinaryMessageEncoder; 12 | import org.apache.avro.message.BinaryMessageDecoder; 13 | import org.apache.avro.message.SchemaStore; 14 | 15 | @org.apache.avro.specific.AvroGenerated 16 | public class OrderProduct extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { 17 | private static final long serialVersionUID = -3658315470325860768L; 18 | 19 | 20 | public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"OrderProduct\",\"namespace\":\"com.ivanfranchin.commons.storeapp.avro\",\"fields\":[{\"name\":\"order_id\",\"type\":\"string\"},{\"name\":\"product_id\",\"type\":\"long\"},{\"name\":\"unit\",\"type\":\"int\"}],\"connect.name\":\"com.ivanfranchin.commons.storeapp.avro.OrderProduct\"}"); 21 | public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } 22 | 23 | private static final SpecificData MODEL$ = new SpecificData(); 24 | 25 | private static final BinaryMessageEncoder ENCODER = 26 | new BinaryMessageEncoder<>(MODEL$, SCHEMA$); 27 | 28 | private static final BinaryMessageDecoder DECODER = 29 | new BinaryMessageDecoder<>(MODEL$, SCHEMA$); 30 | 31 | /** 32 | * Return the BinaryMessageEncoder instance used by this class. 33 | * @return the message encoder used by this class 34 | */ 35 | public static BinaryMessageEncoder getEncoder() { 36 | return ENCODER; 37 | } 38 | 39 | /** 40 | * Return the BinaryMessageDecoder instance used by this class. 41 | * @return the message decoder used by this class 42 | */ 43 | public static BinaryMessageDecoder getDecoder() { 44 | return DECODER; 45 | } 46 | 47 | /** 48 | * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. 49 | * @param resolver a {@link SchemaStore} used to find schemas by fingerprint 50 | * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore 51 | */ 52 | public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { 53 | return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); 54 | } 55 | 56 | /** 57 | * Serializes this OrderProduct to a ByteBuffer. 58 | * @return a buffer holding the serialized data for this instance 59 | * @throws java.io.IOException if this instance could not be serialized 60 | */ 61 | public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { 62 | return ENCODER.encode(this); 63 | } 64 | 65 | /** 66 | * Deserializes a OrderProduct from a ByteBuffer. 67 | * @param b a byte buffer holding serialized data for an instance of this class 68 | * @return a OrderProduct instance decoded from the given buffer 69 | * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class 70 | */ 71 | public static OrderProduct fromByteBuffer( 72 | java.nio.ByteBuffer b) throws java.io.IOException { 73 | return DECODER.decode(b); 74 | } 75 | 76 | private java.lang.CharSequence order_id; 77 | private long product_id; 78 | private int unit; 79 | 80 | /** 81 | * Default constructor. Note that this does not initialize fields 82 | * to their default values from the schema. If that is desired then 83 | * one should use newBuilder(). 84 | */ 85 | public OrderProduct() {} 86 | 87 | /** 88 | * All-args constructor. 89 | * @param order_id The new value for order_id 90 | * @param product_id The new value for product_id 91 | * @param unit The new value for unit 92 | */ 93 | public OrderProduct(java.lang.CharSequence order_id, java.lang.Long product_id, java.lang.Integer unit) { 94 | this.order_id = order_id; 95 | this.product_id = product_id; 96 | this.unit = unit; 97 | } 98 | 99 | @Override 100 | public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } 101 | 102 | @Override 103 | public org.apache.avro.Schema getSchema() { return SCHEMA$; } 104 | 105 | // Used by DatumWriter. Applications should not call. 106 | @Override 107 | public java.lang.Object get(int field$) { 108 | switch (field$) { 109 | case 0: return order_id; 110 | case 1: return product_id; 111 | case 2: return unit; 112 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 113 | } 114 | } 115 | 116 | // Used by DatumReader. Applications should not call. 117 | @Override 118 | @SuppressWarnings(value="unchecked") 119 | public void put(int field$, java.lang.Object value$) { 120 | switch (field$) { 121 | case 0: order_id = (java.lang.CharSequence)value$; break; 122 | case 1: product_id = (java.lang.Long)value$; break; 123 | case 2: unit = (java.lang.Integer)value$; break; 124 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 125 | } 126 | } 127 | 128 | /** 129 | * Gets the value of the 'order_id' field. 130 | * @return The value of the 'order_id' field. 131 | */ 132 | public java.lang.CharSequence getOrderId() { 133 | return order_id; 134 | } 135 | 136 | 137 | /** 138 | * Sets the value of the 'order_id' field. 139 | * @param value the value to set. 140 | */ 141 | public void setOrderId(java.lang.CharSequence value) { 142 | this.order_id = value; 143 | } 144 | 145 | /** 146 | * Gets the value of the 'product_id' field. 147 | * @return The value of the 'product_id' field. 148 | */ 149 | public long getProductId() { 150 | return product_id; 151 | } 152 | 153 | 154 | /** 155 | * Sets the value of the 'product_id' field. 156 | * @param value the value to set. 157 | */ 158 | public void setProductId(long value) { 159 | this.product_id = value; 160 | } 161 | 162 | /** 163 | * Gets the value of the 'unit' field. 164 | * @return The value of the 'unit' field. 165 | */ 166 | public int getUnit() { 167 | return unit; 168 | } 169 | 170 | 171 | /** 172 | * Sets the value of the 'unit' field. 173 | * @param value the value to set. 174 | */ 175 | public void setUnit(int value) { 176 | this.unit = value; 177 | } 178 | 179 | /** 180 | * Creates a new OrderProduct RecordBuilder. 181 | * @return A new OrderProduct RecordBuilder 182 | */ 183 | public static com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder newBuilder() { 184 | return new com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder(); 185 | } 186 | 187 | /** 188 | * Creates a new OrderProduct RecordBuilder by copying an existing Builder. 189 | * @param other The existing builder to copy. 190 | * @return A new OrderProduct RecordBuilder 191 | */ 192 | public static com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder newBuilder(com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder other) { 193 | if (other == null) { 194 | return new com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder(); 195 | } else { 196 | return new com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder(other); 197 | } 198 | } 199 | 200 | /** 201 | * Creates a new OrderProduct RecordBuilder by copying an existing OrderProduct instance. 202 | * @param other The existing instance to copy. 203 | * @return A new OrderProduct RecordBuilder 204 | */ 205 | public static com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder newBuilder(com.ivanfranchin.commons.storeapp.avro.OrderProduct other) { 206 | if (other == null) { 207 | return new com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder(); 208 | } else { 209 | return new com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder(other); 210 | } 211 | } 212 | 213 | /** 214 | * RecordBuilder for OrderProduct instances. 215 | */ 216 | @org.apache.avro.specific.AvroGenerated 217 | public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase 218 | implements org.apache.avro.data.RecordBuilder { 219 | 220 | private java.lang.CharSequence order_id; 221 | private long product_id; 222 | private int unit; 223 | 224 | /** Creates a new Builder */ 225 | private Builder() { 226 | super(SCHEMA$, MODEL$); 227 | } 228 | 229 | /** 230 | * Creates a Builder by copying an existing Builder. 231 | * @param other The existing Builder to copy. 232 | */ 233 | private Builder(com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder other) { 234 | super(other); 235 | if (isValidValue(fields()[0], other.order_id)) { 236 | this.order_id = data().deepCopy(fields()[0].schema(), other.order_id); 237 | fieldSetFlags()[0] = other.fieldSetFlags()[0]; 238 | } 239 | if (isValidValue(fields()[1], other.product_id)) { 240 | this.product_id = data().deepCopy(fields()[1].schema(), other.product_id); 241 | fieldSetFlags()[1] = other.fieldSetFlags()[1]; 242 | } 243 | if (isValidValue(fields()[2], other.unit)) { 244 | this.unit = data().deepCopy(fields()[2].schema(), other.unit); 245 | fieldSetFlags()[2] = other.fieldSetFlags()[2]; 246 | } 247 | } 248 | 249 | /** 250 | * Creates a Builder by copying an existing OrderProduct instance 251 | * @param other The existing instance to copy. 252 | */ 253 | private Builder(com.ivanfranchin.commons.storeapp.avro.OrderProduct other) { 254 | super(SCHEMA$, MODEL$); 255 | if (isValidValue(fields()[0], other.order_id)) { 256 | this.order_id = data().deepCopy(fields()[0].schema(), other.order_id); 257 | fieldSetFlags()[0] = true; 258 | } 259 | if (isValidValue(fields()[1], other.product_id)) { 260 | this.product_id = data().deepCopy(fields()[1].schema(), other.product_id); 261 | fieldSetFlags()[1] = true; 262 | } 263 | if (isValidValue(fields()[2], other.unit)) { 264 | this.unit = data().deepCopy(fields()[2].schema(), other.unit); 265 | fieldSetFlags()[2] = true; 266 | } 267 | } 268 | 269 | /** 270 | * Gets the value of the 'order_id' field. 271 | * @return The value. 272 | */ 273 | public java.lang.CharSequence getOrderId() { 274 | return order_id; 275 | } 276 | 277 | 278 | /** 279 | * Sets the value of the 'order_id' field. 280 | * @param value The value of 'order_id'. 281 | * @return This builder. 282 | */ 283 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder setOrderId(java.lang.CharSequence value) { 284 | validate(fields()[0], value); 285 | this.order_id = value; 286 | fieldSetFlags()[0] = true; 287 | return this; 288 | } 289 | 290 | /** 291 | * Checks whether the 'order_id' field has been set. 292 | * @return True if the 'order_id' field has been set, false otherwise. 293 | */ 294 | public boolean hasOrderId() { 295 | return fieldSetFlags()[0]; 296 | } 297 | 298 | 299 | /** 300 | * Clears the value of the 'order_id' field. 301 | * @return This builder. 302 | */ 303 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder clearOrderId() { 304 | order_id = null; 305 | fieldSetFlags()[0] = false; 306 | return this; 307 | } 308 | 309 | /** 310 | * Gets the value of the 'product_id' field. 311 | * @return The value. 312 | */ 313 | public long getProductId() { 314 | return product_id; 315 | } 316 | 317 | 318 | /** 319 | * Sets the value of the 'product_id' field. 320 | * @param value The value of 'product_id'. 321 | * @return This builder. 322 | */ 323 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder setProductId(long value) { 324 | validate(fields()[1], value); 325 | this.product_id = value; 326 | fieldSetFlags()[1] = true; 327 | return this; 328 | } 329 | 330 | /** 331 | * Checks whether the 'product_id' field has been set. 332 | * @return True if the 'product_id' field has been set, false otherwise. 333 | */ 334 | public boolean hasProductId() { 335 | return fieldSetFlags()[1]; 336 | } 337 | 338 | 339 | /** 340 | * Clears the value of the 'product_id' field. 341 | * @return This builder. 342 | */ 343 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder clearProductId() { 344 | fieldSetFlags()[1] = false; 345 | return this; 346 | } 347 | 348 | /** 349 | * Gets the value of the 'unit' field. 350 | * @return The value. 351 | */ 352 | public int getUnit() { 353 | return unit; 354 | } 355 | 356 | 357 | /** 358 | * Sets the value of the 'unit' field. 359 | * @param value The value of 'unit'. 360 | * @return This builder. 361 | */ 362 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder setUnit(int value) { 363 | validate(fields()[2], value); 364 | this.unit = value; 365 | fieldSetFlags()[2] = true; 366 | return this; 367 | } 368 | 369 | /** 370 | * Checks whether the 'unit' field has been set. 371 | * @return True if the 'unit' field has been set, false otherwise. 372 | */ 373 | public boolean hasUnit() { 374 | return fieldSetFlags()[2]; 375 | } 376 | 377 | 378 | /** 379 | * Clears the value of the 'unit' field. 380 | * @return This builder. 381 | */ 382 | public com.ivanfranchin.commons.storeapp.avro.OrderProduct.Builder clearUnit() { 383 | fieldSetFlags()[2] = false; 384 | return this; 385 | } 386 | 387 | @Override 388 | @SuppressWarnings("unchecked") 389 | public OrderProduct build() { 390 | try { 391 | OrderProduct record = new OrderProduct(); 392 | record.order_id = fieldSetFlags()[0] ? this.order_id : (java.lang.CharSequence) defaultValue(fields()[0]); 393 | record.product_id = fieldSetFlags()[1] ? this.product_id : (java.lang.Long) defaultValue(fields()[1]); 394 | record.unit = fieldSetFlags()[2] ? this.unit : (java.lang.Integer) defaultValue(fields()[2]); 395 | return record; 396 | } catch (org.apache.avro.AvroMissingFieldException e) { 397 | throw e; 398 | } catch (java.lang.Exception e) { 399 | throw new org.apache.avro.AvroRuntimeException(e); 400 | } 401 | } 402 | } 403 | 404 | @SuppressWarnings("unchecked") 405 | private static final org.apache.avro.io.DatumWriter 406 | WRITER$ = (org.apache.avro.io.DatumWriter)MODEL$.createDatumWriter(SCHEMA$); 407 | 408 | @Override public void writeExternal(java.io.ObjectOutput out) 409 | throws java.io.IOException { 410 | WRITER$.write(this, SpecificData.getEncoder(out)); 411 | } 412 | 413 | @SuppressWarnings("unchecked") 414 | private static final org.apache.avro.io.DatumReader 415 | READER$ = (org.apache.avro.io.DatumReader)MODEL$.createDatumReader(SCHEMA$); 416 | 417 | @Override public void readExternal(java.io.ObjectInput in) 418 | throws java.io.IOException { 419 | READER$.read(this, SpecificData.getDecoder(in)); 420 | } 421 | 422 | @Override protected boolean hasCustomCoders() { return true; } 423 | 424 | @Override public void customEncode(org.apache.avro.io.Encoder out) 425 | throws java.io.IOException 426 | { 427 | out.writeString(this.order_id); 428 | 429 | out.writeLong(this.product_id); 430 | 431 | out.writeInt(this.unit); 432 | 433 | } 434 | 435 | @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) 436 | throws java.io.IOException 437 | { 438 | org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); 439 | if (fieldOrder == null) { 440 | this.order_id = in.readString(this.order_id instanceof Utf8 ? (Utf8)this.order_id : null); 441 | 442 | this.product_id = in.readLong(); 443 | 444 | this.unit = in.readInt(); 445 | 446 | } else { 447 | for (int i = 0; i < 3; i++) { 448 | switch (fieldOrder[i].pos()) { 449 | case 0: 450 | this.order_id = in.readString(this.order_id instanceof Utf8 ? (Utf8)this.order_id : null); 451 | break; 452 | 453 | case 1: 454 | this.product_id = in.readLong(); 455 | break; 456 | 457 | case 2: 458 | this.unit = in.readInt(); 459 | break; 460 | 461 | default: 462 | throw new java.io.IOException("Corrupt ResolvingDecoder."); 463 | } 464 | } 465 | } 466 | } 467 | } 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/avro/ProductDetailList.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Autogenerated by Avro 3 | * 4 | * DO NOT EDIT DIRECTLY 5 | */ 6 | package com.ivanfranchin.commons.storeapp.avro; 7 | 8 | import org.apache.avro.generic.GenericArray; 9 | import org.apache.avro.specific.SpecificData; 10 | import org.apache.avro.util.Utf8; 11 | import org.apache.avro.message.BinaryMessageEncoder; 12 | import org.apache.avro.message.BinaryMessageDecoder; 13 | import org.apache.avro.message.SchemaStore; 14 | 15 | @org.apache.avro.specific.AvroGenerated 16 | public class ProductDetailList extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { 17 | private static final long serialVersionUID = -8587805408442332636L; 18 | 19 | 20 | public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"ProductDetailList\",\"namespace\":\"com.ivanfranchin.commons.storeapp.avro\",\"fields\":[{\"name\":\"products\",\"type\":{\"type\":\"array\",\"items\":{\"type\":\"record\",\"name\":\"ProductDetail\",\"fields\":[{\"name\":\"id\",\"type\":\"long\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"price\",\"type\":\"string\"},{\"name\":\"unit\",\"type\":\"int\"}],\"connect.name\":\"com.ivanfranchin.commons.storeapp.avro.ProductDetail\"}},\"default\":[]}],\"connect.name\":\"com.ivanfranchin.commons.storeapp.avro.ProductDetailList\"}"); 21 | public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } 22 | 23 | private static final SpecificData MODEL$ = new SpecificData(); 24 | 25 | private static final BinaryMessageEncoder ENCODER = 26 | new BinaryMessageEncoder<>(MODEL$, SCHEMA$); 27 | 28 | private static final BinaryMessageDecoder DECODER = 29 | new BinaryMessageDecoder<>(MODEL$, SCHEMA$); 30 | 31 | /** 32 | * Return the BinaryMessageEncoder instance used by this class. 33 | * @return the message encoder used by this class 34 | */ 35 | public static BinaryMessageEncoder getEncoder() { 36 | return ENCODER; 37 | } 38 | 39 | /** 40 | * Return the BinaryMessageDecoder instance used by this class. 41 | * @return the message decoder used by this class 42 | */ 43 | public static BinaryMessageDecoder getDecoder() { 44 | return DECODER; 45 | } 46 | 47 | /** 48 | * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. 49 | * @param resolver a {@link SchemaStore} used to find schemas by fingerprint 50 | * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore 51 | */ 52 | public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { 53 | return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); 54 | } 55 | 56 | /** 57 | * Serializes this ProductDetailList to a ByteBuffer. 58 | * @return a buffer holding the serialized data for this instance 59 | * @throws java.io.IOException if this instance could not be serialized 60 | */ 61 | public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { 62 | return ENCODER.encode(this); 63 | } 64 | 65 | /** 66 | * Deserializes a ProductDetailList from a ByteBuffer. 67 | * @param b a byte buffer holding serialized data for an instance of this class 68 | * @return a ProductDetailList instance decoded from the given buffer 69 | * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class 70 | */ 71 | public static ProductDetailList fromByteBuffer( 72 | java.nio.ByteBuffer b) throws java.io.IOException { 73 | return DECODER.decode(b); 74 | } 75 | 76 | private java.util.List products; 77 | 78 | /** 79 | * Default constructor. Note that this does not initialize fields 80 | * to their default values from the schema. If that is desired then 81 | * one should use newBuilder(). 82 | */ 83 | public ProductDetailList() {} 84 | 85 | /** 86 | * All-args constructor. 87 | * @param products The new value for products 88 | */ 89 | public ProductDetailList(java.util.List products) { 90 | this.products = products; 91 | } 92 | 93 | @Override 94 | public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } 95 | 96 | @Override 97 | public org.apache.avro.Schema getSchema() { return SCHEMA$; } 98 | 99 | // Used by DatumWriter. Applications should not call. 100 | @Override 101 | public java.lang.Object get(int field$) { 102 | switch (field$) { 103 | case 0: return products; 104 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 105 | } 106 | } 107 | 108 | // Used by DatumReader. Applications should not call. 109 | @Override 110 | @SuppressWarnings(value="unchecked") 111 | public void put(int field$, java.lang.Object value$) { 112 | switch (field$) { 113 | case 0: products = (java.util.List)value$; break; 114 | default: throw new IndexOutOfBoundsException("Invalid index: " + field$); 115 | } 116 | } 117 | 118 | /** 119 | * Gets the value of the 'products' field. 120 | * @return The value of the 'products' field. 121 | */ 122 | public java.util.List getProducts() { 123 | return products; 124 | } 125 | 126 | 127 | /** 128 | * Sets the value of the 'products' field. 129 | * @param value the value to set. 130 | */ 131 | public void setProducts(java.util.List value) { 132 | this.products = value; 133 | } 134 | 135 | /** 136 | * Creates a new ProductDetailList RecordBuilder. 137 | * @return A new ProductDetailList RecordBuilder 138 | */ 139 | public static com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder newBuilder() { 140 | return new com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder(); 141 | } 142 | 143 | /** 144 | * Creates a new ProductDetailList RecordBuilder by copying an existing Builder. 145 | * @param other The existing builder to copy. 146 | * @return A new ProductDetailList RecordBuilder 147 | */ 148 | public static com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder newBuilder(com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder other) { 149 | if (other == null) { 150 | return new com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder(); 151 | } else { 152 | return new com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder(other); 153 | } 154 | } 155 | 156 | /** 157 | * Creates a new ProductDetailList RecordBuilder by copying an existing ProductDetailList instance. 158 | * @param other The existing instance to copy. 159 | * @return A new ProductDetailList RecordBuilder 160 | */ 161 | public static com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder newBuilder(com.ivanfranchin.commons.storeapp.avro.ProductDetailList other) { 162 | if (other == null) { 163 | return new com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder(); 164 | } else { 165 | return new com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder(other); 166 | } 167 | } 168 | 169 | /** 170 | * RecordBuilder for ProductDetailList instances. 171 | */ 172 | @org.apache.avro.specific.AvroGenerated 173 | public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase 174 | implements org.apache.avro.data.RecordBuilder { 175 | 176 | private java.util.List products; 177 | 178 | /** Creates a new Builder */ 179 | private Builder() { 180 | super(SCHEMA$, MODEL$); 181 | } 182 | 183 | /** 184 | * Creates a Builder by copying an existing Builder. 185 | * @param other The existing Builder to copy. 186 | */ 187 | private Builder(com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder other) { 188 | super(other); 189 | if (isValidValue(fields()[0], other.products)) { 190 | this.products = data().deepCopy(fields()[0].schema(), other.products); 191 | fieldSetFlags()[0] = other.fieldSetFlags()[0]; 192 | } 193 | } 194 | 195 | /** 196 | * Creates a Builder by copying an existing ProductDetailList instance 197 | * @param other The existing instance to copy. 198 | */ 199 | private Builder(com.ivanfranchin.commons.storeapp.avro.ProductDetailList other) { 200 | super(SCHEMA$, MODEL$); 201 | if (isValidValue(fields()[0], other.products)) { 202 | this.products = data().deepCopy(fields()[0].schema(), other.products); 203 | fieldSetFlags()[0] = true; 204 | } 205 | } 206 | 207 | /** 208 | * Gets the value of the 'products' field. 209 | * @return The value. 210 | */ 211 | public java.util.List getProducts() { 212 | return products; 213 | } 214 | 215 | 216 | /** 217 | * Sets the value of the 'products' field. 218 | * @param value The value of 'products'. 219 | * @return This builder. 220 | */ 221 | public com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder setProducts(java.util.List value) { 222 | validate(fields()[0], value); 223 | this.products = value; 224 | fieldSetFlags()[0] = true; 225 | return this; 226 | } 227 | 228 | /** 229 | * Checks whether the 'products' field has been set. 230 | * @return True if the 'products' field has been set, false otherwise. 231 | */ 232 | public boolean hasProducts() { 233 | return fieldSetFlags()[0]; 234 | } 235 | 236 | 237 | /** 238 | * Clears the value of the 'products' field. 239 | * @return This builder. 240 | */ 241 | public com.ivanfranchin.commons.storeapp.avro.ProductDetailList.Builder clearProducts() { 242 | products = null; 243 | fieldSetFlags()[0] = false; 244 | return this; 245 | } 246 | 247 | @Override 248 | @SuppressWarnings("unchecked") 249 | public ProductDetailList build() { 250 | try { 251 | ProductDetailList record = new ProductDetailList(); 252 | record.products = fieldSetFlags()[0] ? this.products : (java.util.List) defaultValue(fields()[0]); 253 | return record; 254 | } catch (org.apache.avro.AvroMissingFieldException e) { 255 | throw e; 256 | } catch (java.lang.Exception e) { 257 | throw new org.apache.avro.AvroRuntimeException(e); 258 | } 259 | } 260 | } 261 | 262 | @SuppressWarnings("unchecked") 263 | private static final org.apache.avro.io.DatumWriter 264 | WRITER$ = (org.apache.avro.io.DatumWriter)MODEL$.createDatumWriter(SCHEMA$); 265 | 266 | @Override public void writeExternal(java.io.ObjectOutput out) 267 | throws java.io.IOException { 268 | WRITER$.write(this, SpecificData.getEncoder(out)); 269 | } 270 | 271 | @SuppressWarnings("unchecked") 272 | private static final org.apache.avro.io.DatumReader 273 | READER$ = (org.apache.avro.io.DatumReader)MODEL$.createDatumReader(SCHEMA$); 274 | 275 | @Override public void readExternal(java.io.ObjectInput in) 276 | throws java.io.IOException { 277 | READER$.read(this, SpecificData.getDecoder(in)); 278 | } 279 | 280 | @Override protected boolean hasCustomCoders() { return true; } 281 | 282 | @Override public void customEncode(org.apache.avro.io.Encoder out) 283 | throws java.io.IOException 284 | { 285 | long size0 = this.products.size(); 286 | out.writeArrayStart(); 287 | out.setItemCount(size0); 288 | long actualSize0 = 0; 289 | for (com.ivanfranchin.commons.storeapp.avro.ProductDetail e0: this.products) { 290 | actualSize0++; 291 | out.startItem(); 292 | e0.customEncode(out); 293 | } 294 | out.writeArrayEnd(); 295 | if (actualSize0 != size0) 296 | throw new java.util.ConcurrentModificationException("Array-size written was " + size0 + ", but element count was " + actualSize0 + "."); 297 | 298 | } 299 | 300 | @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) 301 | throws java.io.IOException 302 | { 303 | org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); 304 | if (fieldOrder == null) { 305 | long size0 = in.readArrayStart(); 306 | java.util.List a0 = this.products; 307 | if (a0 == null) { 308 | a0 = new SpecificData.Array((int)size0, SCHEMA$.getField("products").schema()); 309 | this.products = a0; 310 | } else a0.clear(); 311 | SpecificData.Array ga0 = (a0 instanceof SpecificData.Array ? (SpecificData.Array)a0 : null); 312 | for ( ; 0 < size0; size0 = in.arrayNext()) { 313 | for ( ; size0 != 0; size0--) { 314 | com.ivanfranchin.commons.storeapp.avro.ProductDetail e0 = (ga0 != null ? ga0.peek() : null); 315 | if (e0 == null) { 316 | e0 = new com.ivanfranchin.commons.storeapp.avro.ProductDetail(); 317 | } 318 | e0.customDecode(in); 319 | a0.add(e0); 320 | } 321 | } 322 | 323 | } else { 324 | for (int i = 0; i < 1; i++) { 325 | switch (fieldOrder[i].pos()) { 326 | case 0: 327 | long size0 = in.readArrayStart(); 328 | java.util.List a0 = this.products; 329 | if (a0 == null) { 330 | a0 = new SpecificData.Array((int)size0, SCHEMA$.getField("products").schema()); 331 | this.products = a0; 332 | } else a0.clear(); 333 | SpecificData.Array ga0 = (a0 instanceof SpecificData.Array ? (SpecificData.Array)a0 : null); 334 | for ( ; 0 < size0; size0 = in.arrayNext()) { 335 | for ( ; size0 != 0; size0--) { 336 | com.ivanfranchin.commons.storeapp.avro.ProductDetail e0 = (ga0 != null ? ga0.peek() : null); 337 | if (e0 == null) { 338 | e0 = new com.ivanfranchin.commons.storeapp.avro.ProductDetail(); 339 | } 340 | e0.customDecode(in); 341 | a0.add(e0); 342 | } 343 | } 344 | break; 345 | 346 | default: 347 | throw new java.io.IOException("Corrupt ResolvingDecoder."); 348 | } 349 | } 350 | } 351 | } 352 | } 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/Customer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | 7 | public record Customer(Long id, String name, String email, String address, String phone, 8 | @JsonProperty("created_at") Date createdAt) { 9 | } 10 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/Order.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | 7 | public record Order(String id, 8 | @JsonProperty("customer_id") Long customerId, 9 | @JsonProperty("payment_type") String paymentType, 10 | String status, 11 | @JsonProperty("created_at") Date createdAt) { 12 | } 13 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/OrderDetailed.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | @Data 10 | public class OrderDetailed { 11 | 12 | private String id; 13 | 14 | @JsonProperty("customer_id") 15 | private Long customerId; 16 | 17 | @JsonProperty("customer_name") 18 | private String customerName; 19 | 20 | @JsonProperty("payment_type") 21 | private String paymentType; 22 | 23 | private String status; 24 | 25 | @JsonProperty("created_at") 26 | private Date createdAt; 27 | 28 | private List products; 29 | } 30 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/OrderProduct.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record OrderProduct(@JsonProperty("order_id") String orderId, 6 | @JsonProperty("product_id") Long productId, 7 | Integer unit) { 8 | } 9 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/Product.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Date; 7 | 8 | public record Product(Long id, String name, BigDecimal price, 9 | @JsonProperty("created_at") Date createdAt) { 10 | } 11 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/commons/storeapp/json/ProductDetail.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.commons.storeapp.json; 2 | 3 | import lombok.Data; 4 | 5 | import java.math.BigDecimal; 6 | 7 | // If this class is converted to record, serialization exception will be thrown 8 | @Data 9 | public class ProductDetail { 10 | 11 | private Long id; 12 | private String name; 13 | private BigDecimal price; 14 | private Integer unit; 15 | } 16 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/StoreStreamsApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class StoreStreamsApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(StoreStreamsApplication.class, args); 11 | } 12 | } -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/bus/StoreStreamsAvro.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.bus; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.Customer; 4 | import com.ivanfranchin.commons.storeapp.avro.Order; 5 | import com.ivanfranchin.commons.storeapp.avro.OrderDetailed; 6 | import com.ivanfranchin.commons.storeapp.avro.OrderProduct; 7 | import com.ivanfranchin.commons.storeapp.avro.Product; 8 | import com.ivanfranchin.commons.storeapp.avro.ProductDetail; 9 | import com.ivanfranchin.commons.storeapp.avro.ProductDetailList; 10 | import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig; 11 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroDeserializer; 12 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerializer; 13 | import jakarta.annotation.PostConstruct; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.kafka.common.serialization.Serde; 16 | import org.apache.kafka.common.serialization.Serdes; 17 | import org.apache.kafka.streams.kstream.GlobalKTable; 18 | import org.apache.kafka.streams.kstream.JoinWindows; 19 | import org.apache.kafka.streams.kstream.KStream; 20 | import org.apache.kafka.streams.kstream.Materialized; 21 | import org.apache.kafka.streams.kstream.StreamJoined; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.context.annotation.Bean; 24 | import org.springframework.context.annotation.Profile; 25 | import org.springframework.stereotype.Component; 26 | 27 | import java.time.Duration; 28 | import java.util.Collections; 29 | import java.util.LinkedList; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.function.Function; 33 | 34 | @Slf4j 35 | @Component 36 | @Profile("avro") 37 | public class StoreStreamsAvro { 38 | 39 | @Value("${spring.cloud.stream.kafka.streams.binder.configuration.schema.registry.url}") 40 | private String schemaRegistryUrl; 41 | 42 | private Serde productDetailListSerde; 43 | private Serde orderDetailedSerde; 44 | 45 | @PostConstruct 46 | public void init() { 47 | Map serdeConfig = Collections.singletonMap(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, schemaRegistryUrl); 48 | 49 | SpecificAvroSerializer setSerializer = new SpecificAvroSerializer<>(); 50 | SpecificAvroDeserializer setDeserializer = new SpecificAvroDeserializer<>(); 51 | productDetailListSerde = Serdes.serdeFrom(setSerializer, setDeserializer); 52 | productDetailListSerde.configure(serdeConfig, false); 53 | 54 | SpecificAvroSerializer orderDetailedSerializer = new SpecificAvroSerializer<>(); 55 | SpecificAvroDeserializer orderDetailedDeserializer = new SpecificAvroDeserializer<>(); 56 | orderDetailedSerde = Serdes.serdeFrom(orderDetailedSerializer, orderDetailedDeserializer); 57 | orderDetailedSerde.configure(serdeConfig, false); 58 | } 59 | 60 | @Bean 61 | Function, 62 | Function, 63 | Function, 64 | Function, 65 | KStream>>>> process() { 66 | return orderKStream -> ( 67 | customerGlobalKTable -> ( 68 | orderProductKStream -> ( 69 | productGlobalKTable -> ( 70 | orderKStream 71 | .peek(this::logKeyValue) 72 | .join( 73 | customerGlobalKTable, 74 | (s, order) -> String.valueOf(order.getCustomerId()), 75 | this::toOrderDetailed 76 | ) 77 | .join( 78 | orderProductKStream 79 | .peek(this::logKeyValue) 80 | .join( 81 | productGlobalKTable, 82 | (s, orderProduct) -> String.valueOf(orderProduct.getProductId()), 83 | this::toProductDetail 84 | ) 85 | .groupByKey() 86 | .aggregate( 87 | ProductDetailList::new, 88 | (key, productDetail, productDetailList) -> addProductDetail(productDetail, productDetailList), 89 | Materialized.with(Serdes.String(), productDetailListSerde) 90 | ) 91 | .toStream() 92 | .peek(this::logKeyValue), 93 | (orderDetailed, productDetailList) -> setProductDetailList(productDetailList, orderDetailed), 94 | JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(1)), 95 | StreamJoined.with(Serdes.String(), orderDetailedSerde, productDetailListSerde) 96 | ) 97 | .peek(this::logKeyValue) 98 | ) 99 | ) 100 | ) 101 | ); 102 | } 103 | 104 | private OrderDetailed toOrderDetailed(Order order, Customer customer) { 105 | OrderDetailed orderDetailed = new OrderDetailed(); 106 | orderDetailed.setId(order.getId()); 107 | orderDetailed.setCustomerId(order.getCustomerId()); 108 | orderDetailed.setCustomerName(customer.getName()); 109 | orderDetailed.setStatus(order.getStatus()); 110 | orderDetailed.setPaymentType(order.getPaymentType()); 111 | orderDetailed.setCreatedAt(order.getCreatedAt()); 112 | orderDetailed.setProducts(Collections.emptyList()); 113 | return orderDetailed; 114 | } 115 | 116 | private ProductDetail toProductDetail(OrderProduct orderProduct, Product product) { 117 | ProductDetail productDetail = new ProductDetail(); 118 | productDetail.setId(orderProduct.getProductId()); 119 | productDetail.setName(product.getName()); 120 | productDetail.setPrice(product.getPrice()); 121 | productDetail.setUnit(orderProduct.getUnit()); 122 | return productDetail; 123 | } 124 | 125 | private ProductDetailList addProductDetail(ProductDetail productDetail, ProductDetailList productDetailList) { 126 | List products = productDetailList.getProducts(); 127 | if (products == null) { 128 | products = new LinkedList<>(); 129 | productDetailList.setProducts(products); 130 | } 131 | products.add(productDetail); 132 | return productDetailList; 133 | } 134 | 135 | private OrderDetailed setProductDetailList(ProductDetailList productDetailList, OrderDetailed orderDetailed) { 136 | orderDetailed.setProducts(productDetailList.getProducts()); 137 | return orderDetailed; 138 | } 139 | 140 | private void logKeyValue(String key, Object value) { 141 | log.info("==> key: {}, value: {}", key, value); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/bus/StoreStreamsJson.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.bus; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.Customer; 4 | import com.ivanfranchin.commons.storeapp.json.Order; 5 | import com.ivanfranchin.commons.storeapp.json.OrderDetailed; 6 | import com.ivanfranchin.commons.storeapp.json.OrderProduct; 7 | import com.ivanfranchin.commons.storeapp.json.Product; 8 | import com.ivanfranchin.commons.storeapp.json.ProductDetail; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.kafka.common.serialization.Serde; 11 | import org.apache.kafka.common.serialization.Serdes; 12 | import org.apache.kafka.streams.kstream.GlobalKTable; 13 | import org.apache.kafka.streams.kstream.JoinWindows; 14 | import org.apache.kafka.streams.kstream.KStream; 15 | import org.apache.kafka.streams.kstream.Materialized; 16 | import org.apache.kafka.streams.kstream.StreamJoined; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Profile; 19 | import org.springframework.kafka.support.serializer.JsonDeserializer; 20 | import org.springframework.kafka.support.serializer.JsonSerializer; 21 | import org.springframework.stereotype.Component; 22 | 23 | import java.time.Duration; 24 | import java.util.Collections; 25 | import java.util.LinkedList; 26 | import java.util.List; 27 | import java.util.function.Function; 28 | 29 | @Slf4j 30 | @Component 31 | @Profile("!avro") 32 | public class StoreStreamsJson { 33 | 34 | public static final Serde> productDetailListSerde; 35 | public static final Serde orderDetailedSerde; 36 | 37 | static { 38 | JsonSerializer> setSerializer = new JsonSerializer<>(); 39 | JsonDeserializer> setDeserializer = new JsonDeserializer<>(List.class); 40 | productDetailListSerde = Serdes.serdeFrom(setSerializer, setDeserializer); 41 | 42 | JsonSerializer orderDetailedSerializer = new JsonSerializer<>(); 43 | JsonDeserializer orderDetailedDeserializer = new JsonDeserializer<>(OrderDetailed.class); 44 | orderDetailedSerde = Serdes.serdeFrom(orderDetailedSerializer, orderDetailedDeserializer); 45 | } 46 | 47 | @Bean 48 | Function, 49 | Function, 50 | Function, 51 | Function, 52 | KStream>>>> process() { 53 | return orderKStream -> ( 54 | customerGlobalKTable -> ( 55 | orderProductKStream -> ( 56 | productGlobalKTable -> ( 57 | orderKStream 58 | .peek(this::logKeyValue) 59 | .join( 60 | customerGlobalKTable, 61 | (s, order) -> String.valueOf(order.customerId()), 62 | this::toOrderDetailed 63 | ) 64 | .join( 65 | orderProductKStream 66 | .peek(this::logKeyValue) 67 | .join( 68 | productGlobalKTable, 69 | (s, orderProduct) -> String.valueOf(orderProduct.productId()), 70 | this::toProductDetail 71 | ) 72 | .groupByKey() 73 | .aggregate( 74 | LinkedList::new, 75 | (key, productDetail, productDetailList) -> addProductDetail(productDetail, productDetailList), 76 | Materialized.with(Serdes.String(), productDetailListSerde) 77 | ) 78 | .toStream() 79 | .peek(this::logKeyValue), 80 | (orderDetailed, productDetailList) -> setProductDetailList(productDetailList, orderDetailed), 81 | JoinWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(1)), 82 | StreamJoined.with(Serdes.String(), orderDetailedSerde, productDetailListSerde) 83 | ) 84 | .peek(this::logKeyValue) 85 | ) 86 | ) 87 | ) 88 | ); 89 | } 90 | 91 | private OrderDetailed toOrderDetailed(Order order, Customer customer) { 92 | OrderDetailed orderDetailed = new OrderDetailed(); 93 | orderDetailed.setId(order.id()); 94 | orderDetailed.setCustomerId(order.customerId()); 95 | orderDetailed.setCustomerName(customer.name()); 96 | orderDetailed.setStatus(order.status()); 97 | orderDetailed.setPaymentType(order.paymentType()); 98 | orderDetailed.setCreatedAt(order.createdAt()); 99 | orderDetailed.setProducts(Collections.emptyList()); 100 | return orderDetailed; 101 | } 102 | 103 | private ProductDetail toProductDetail(OrderProduct orderProduct, Product product) { 104 | ProductDetail productDetail = new ProductDetail(); 105 | productDetail.setId(orderProduct.productId()); 106 | productDetail.setName(product.name()); 107 | productDetail.setPrice(product.price()); 108 | productDetail.setUnit(orderProduct.unit()); 109 | return productDetail; 110 | } 111 | 112 | private List addProductDetail(ProductDetail productDetail, List productDetailList) { 113 | productDetailList.add(productDetail); 114 | return productDetailList; 115 | } 116 | 117 | private OrderDetailed setProductDetailList(List productDetailList, OrderDetailed orderDetailed) { 118 | orderDetailed.setProducts(productDetailList); 119 | return orderDetailed; 120 | } 121 | 122 | private void logKeyValue(String key, Object value) { 123 | log.info("==> key: {}, value: {}", key, value); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/avro/CustomerAvroSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.avro; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.Customer; 4 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; 5 | 6 | public class CustomerAvroSerde extends SpecificAvroSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/avro/OrderAvroSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.avro; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.Order; 4 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; 5 | 6 | public class OrderAvroSerde extends SpecificAvroSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/avro/OrderDetailedAvroSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.avro; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.OrderDetailed; 4 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; 5 | 6 | public class OrderDetailedAvroSerde extends SpecificAvroSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/avro/OrderProductAvroSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.avro; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.OrderProduct; 4 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; 5 | 6 | public class OrderProductAvroSerde extends SpecificAvroSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/avro/ProductAvroSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.avro; 2 | 3 | import com.ivanfranchin.commons.storeapp.avro.Product; 4 | import io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde; 5 | 6 | public class ProductAvroSerde extends SpecificAvroSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/json/CustomerJsonSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.json; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.Customer; 4 | import org.springframework.kafka.support.serializer.JsonSerde; 5 | 6 | public class CustomerJsonSerde extends JsonSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/json/OrderDetailedJsonSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.json; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.OrderDetailed; 4 | import org.springframework.kafka.support.serializer.JsonSerde; 5 | 6 | public class OrderDetailedJsonSerde extends JsonSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/json/OrderJsonSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.json; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.Order; 4 | import org.springframework.kafka.support.serializer.JsonSerde; 5 | 6 | public class OrderJsonSerde extends JsonSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/json/OrderProductJsonSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.json; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.OrderProduct; 4 | import org.springframework.kafka.support.serializer.JsonSerde; 5 | 6 | public class OrderProductJsonSerde extends JsonSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/java/com/ivanfranchin/storestreams/serde/json/ProductJsonSerde.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams.serde.json; 2 | 3 | import com.ivanfranchin.commons.storeapp.json.Product; 4 | import org.springframework.kafka.support.serializer.JsonSerde; 5 | 6 | public class ProductJsonSerde extends JsonSerde { 7 | } 8 | -------------------------------------------------------------------------------- /store-streams/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: store-streams 4 | cloud: 5 | stream: 6 | bindings: 7 | process-in-0: 8 | destination: mysql.storedb.orders 9 | group: storeStreamsGroup 10 | consumer: 11 | useNativeDecoding: true 12 | process-in-1: 13 | destination: mysql.storedb.customers 14 | group: storeStreamsGroup 15 | consumer: 16 | useNativeDecoding: true 17 | process-in-2: 18 | destination: mysql.storedb.orders_products 19 | group: storeStreamsGroup 20 | consumer: 21 | useNativeDecoding: true 22 | process-in-3: 23 | destination: mysql.storedb.products 24 | group: storeStreamsGroup 25 | consumer: 26 | useNativeDecoding: true 27 | process-out-0: 28 | destination: store.streams.orders 29 | producer: 30 | useNativeEncoding: true 31 | kafka: 32 | streams: 33 | binder: 34 | brokers: ${KAFKA_HOST:localhost}:${KAFKA_PORT:29092} 35 | configuration: 36 | commit.interval.ms: 1000 37 | default.key.serde: org.apache.kafka.common.serialization.Serdes$StringSerde 38 | default.value.serde: org.apache.kafka.common.serialization.Serdes$StringSerde 39 | bindings: 40 | process-in-0: 41 | consumer: 42 | keySerde: org.apache.kafka.common.serialization.Serdes$StringSerde 43 | valueSerde: com.ivanfranchin.storestreams.serde.json.OrderJsonSerde 44 | process-in-1: 45 | consumer: 46 | keySerde: org.apache.kafka.common.serialization.Serdes$StringSerde 47 | valueSerde: com.ivanfranchin.storestreams.serde.json.CustomerJsonSerde 48 | materializedAs: ktable.customers 49 | process-in-2: 50 | consumer: 51 | keySerde: org.apache.kafka.common.serialization.Serdes$StringSerde 52 | valueSerde: com.ivanfranchin.storestreams.serde.json.OrderProductJsonSerde 53 | process-in-3: 54 | consumer: 55 | keySerde: org.apache.kafka.common.serialization.Serdes$StringSerde 56 | valueSerde: com.ivanfranchin.storestreams.serde.json.ProductJsonSerde 57 | materializedAs: ktable.products 58 | process-out-0: 59 | producer: 60 | keySerde: org.apache.kafka.common.serialization.Serdes$StringSerde 61 | valueSerde: com.ivanfranchin.storestreams.serde.json.OrderDetailedJsonSerde 62 | 63 | management: 64 | endpoints: 65 | web: 66 | exposure.include: beans, env, health, info, metrics, mappings 67 | endpoint: 68 | health: 69 | show-details: always 70 | 71 | --- 72 | spring: 73 | config: 74 | activate.on-profile: avro 75 | cloud: 76 | stream: 77 | kafka: 78 | streams: 79 | binder: 80 | configuration: 81 | schema.registry.url: http://${SCHEMA_REGISTRY_HOST:localhost}:${SCHEMA_REGISTRY_PORT:8081} 82 | bindings: 83 | process-in-0: 84 | consumer: 85 | valueSerde: com.ivanfranchin.storestreams.serde.avro.OrderAvroSerde 86 | process-in-1: 87 | consumer: 88 | valueSerde: com.ivanfranchin.storestreams.serde.avro.CustomerAvroSerde 89 | process-in-2: 90 | consumer: 91 | valueSerde: com.ivanfranchin.storestreams.serde.avro.OrderProductAvroSerde 92 | process-in-3: 93 | consumer: 94 | valueSerde: com.ivanfranchin.storestreams.serde.avro.ProductAvroSerde 95 | process-out-0: 96 | producer: 97 | valueSerde: com.ivanfranchin.storestreams.serde.avro.OrderDetailedAvroSerde 98 | -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/customer-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "Customer", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "id", 8 | "type": "long" 9 | }, 10 | { 11 | "name": "address", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "created_at", 16 | "type": { 17 | "type": "long", 18 | "connect.version": 1, 19 | "connect.name": "org.apache.kafka.connect.data.Timestamp", 20 | "logicalType": "timestamp-millis" 21 | } 22 | }, 23 | { 24 | "name": "email", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "name", 29 | "type": "string" 30 | }, 31 | { 32 | "name": "phone", 33 | "type": "string" 34 | } 35 | ], 36 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.Customer" 37 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/order-detailed-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "OrderDetailed", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "id", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "created_at", 12 | "type": { 13 | "type": "long", 14 | "connect.version": 1, 15 | "connect.name": "org.apache.kafka.connect.data.Timestamp", 16 | "logicalType": "timestamp-millis" 17 | } 18 | }, 19 | { 20 | "name": "payment_type", 21 | "type": "string" 22 | }, 23 | { 24 | "name": "status", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "customer_id", 29 | "type": "long" 30 | }, 31 | { 32 | "name": "customer_name", 33 | "type": ["null", "string"], 34 | "default": null 35 | }, 36 | { 37 | "name": "products", 38 | "type": { 39 | "type": "array", 40 | "items": "com.ivanfranchin.commons.storeapp.avro.ProductDetail" 41 | }, 42 | "default": [] 43 | } 44 | ], 45 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.OrderDetailed" 46 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/order-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "Order", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "id", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "created_at", 12 | "type": { 13 | "type": "long", 14 | "connect.version": 1, 15 | "connect.name": "org.apache.kafka.connect.data.Timestamp", 16 | "logicalType": "timestamp-millis" 17 | } 18 | }, 19 | { 20 | "name": "payment_type", 21 | "type": "string" 22 | }, 23 | { 24 | "name": "status", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "customer_id", 29 | "type": "long" 30 | } 31 | ], 32 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.Order" 33 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/order_product-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "OrderProduct", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "order_id", 8 | "type": "string" 9 | }, 10 | { 11 | "name": "product_id", 12 | "type": "long" 13 | }, 14 | { 15 | "name": "unit", 16 | "type": "int" 17 | } 18 | ], 19 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.OrderProduct" 20 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/product-detail-list-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "ProductDetailList", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "products", 8 | "type": { 9 | "type": "array", 10 | "items": "com.ivanfranchin.commons.storeapp.avro.ProductDetail" 11 | }, 12 | "default": [] 13 | } 14 | ], 15 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.ProductDetailList" 16 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/product-detail-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "ProductDetail", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "id", 8 | "type": "long" 9 | }, 10 | { 11 | "name": "name", 12 | "type": "string" 13 | }, 14 | { 15 | "name": "price", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "unit", 20 | "type": "int" 21 | } 22 | ], 23 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.ProductDetail" 24 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/avro/product-message.avsc: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "Product", 4 | "namespace": "com.ivanfranchin.commons.storeapp.avro", 5 | "fields": [ 6 | { 7 | "name": "id", 8 | "type": "long" 9 | }, 10 | { 11 | "name": "created_at", 12 | "type": { 13 | "type": "long", 14 | "connect.version": 1, 15 | "connect.name": "org.apache.kafka.connect.data.Timestamp", 16 | "logicalType": "timestamp-millis" 17 | } 18 | }, 19 | { 20 | "name": "name", 21 | "type": "string" 22 | }, 23 | { 24 | "name": "price", 25 | "type": "string" 26 | } 27 | ], 28 | "connect.name": "com.ivanfranchin.commons.storeapp.avro.Product" 29 | } -------------------------------------------------------------------------------- /store-streams/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | ___| |_ ___ _ __ ___ ___| |_ _ __ ___ __ _ _ __ ___ ___ 3 | / __| __/ _ \| '__/ _ \_____/ __| __| '__/ _ \/ _` | '_ ` _ \/ __| 4 | \__ \ || (_) | | | __/_____\__ \ |_| | | __/ (_| | | | | | \__ \ 5 | |___/\__\___/|_| \___| |___/\__|_| \___|\__,_|_| |_| |_|___/ 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /store-streams/src/test/java/com/ivanfranchin/storestreams/StoreStreamsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.storestreams; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class StoreStreamsApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | --------------------------------------------------------------------------------