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