├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.adoc ├── build-docker-images.sh ├── create-index-alias-reindex.adoc ├── create-index.sh ├── docker-compose.yml ├── documentation ├── demo-user-interaction.gif ├── project-diagram.excalidraw └── project-diagram.jpeg ├── elasticsearch ├── mapping-v1.json ├── mapping-v2.json └── products.json ├── insert-products.sh ├── mvnw ├── mvnw.cmd ├── pom.xml ├── product-api ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── productapi │ │ │ ├── ProductApiApplication.java │ │ │ ├── config │ │ │ ├── ErrorAttributesConfig.java │ │ │ ├── SpringDataWebSupportConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── product │ │ │ ├── ProductController.java │ │ │ ├── ProductRepository.java │ │ │ ├── ProductService.java │ │ │ ├── dto │ │ │ │ ├── CreateProductRequest.java │ │ │ │ ├── SearchRequest.java │ │ │ │ └── UpdateProductRequest.java │ │ │ ├── exception │ │ │ │ └── ProductNotFoundException.java │ │ │ ├── model │ │ │ │ └── Product.java │ │ │ └── review │ │ │ │ ├── AddReviewRequest.java │ │ │ │ ├── ProductReviewController.java │ │ │ │ └── Review.java │ │ │ └── util │ │ │ └── DateTimeUtil.java │ └── resources │ │ ├── application.properties │ │ └── banner.txt │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── productapi │ └── ProductApiApplicationTests.java ├── product-ui ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── productui │ │ │ ├── ProductUiApplication.java │ │ │ ├── client │ │ │ ├── ProductApiClient.java │ │ │ ├── ProductApiClientConfig.java │ │ │ └── dto │ │ │ │ ├── MyPage.java │ │ │ │ ├── Product.java │ │ │ │ ├── ProductDto.java │ │ │ │ ├── Review.java │ │ │ │ └── SearchDto.java │ │ │ ├── config │ │ │ └── ErrorAttributesConfig.java │ │ │ └── controller │ │ │ ├── ProductController.java │ │ │ └── ProductReviewController.java │ └── resources │ │ ├── application.properties │ │ ├── banner.txt │ │ └── templates │ │ ├── error.html │ │ ├── fragments │ │ ├── footer.html │ │ └── header.html │ │ ├── productCreate.html │ │ ├── productEdit.html │ │ ├── productView.html │ │ └── products.html │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── productui │ └── ProductUiApplicationTests.java ├── reindex.sh ├── remove-docker-images.sh ├── scripts └── my-functions.sh ├── start-apps.sh └── stop-apps.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ivangfr -------------------------------------------------------------------------------- /.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.adoc: -------------------------------------------------------------------------------- 1 | = springboot-elasticsearch-thymeleaf 2 | 3 | The goal of this project is to implement an application called `product-app`. It consists of two https://docs.spring.io/spring-boot/index.html[`Spring Boot`] services: `product-api` (backend) and `product-ui` (frontend). The data will be stored in https://www.elastic.co/elasticsearch[`Elasticsearch`]. 4 | 5 | == Proof-of-Concepts & Articles 6 | 7 | On https://ivangfr.github.io[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 | == Project diagram 10 | 11 | image::documentation/project-diagram.jpeg[] 12 | 13 | == Applications 14 | 15 | * **product-api** 16 | + 17 | `Spring Boot` Web Java application that exposes a REST API to manage products. Product information is stored in `Elasticsearch`. `product-api` uses https://docs.spring.io/spring-data/elasticsearch/reference/[`Spring Data Elasticsearch`] to persist/query/delete data in `Elasticsearch`. 18 | 19 | * **product-ui** 20 | + 21 | `Spring Boot` Web application that was implemented using https://www.thymeleaf.org/[`Thymeleaf`] as HTML template. Also, it uses https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface[`Http Interfaces`] to simplify HTTP remote access to `product-api`. 22 | 23 | == Prerequisites 24 | 25 | * https://www.oracle.com/java/technologies/downloads/#java21[`Java 21`] or higher; 26 | * A containerization tool (e.g., https://www.docker.com[`Docker`], https://podman.io[`Podman`], etc.) 27 | 28 | == Start Environment 29 | 30 | * Open a terminal and navigate to the `springboot-elasticsearch-thymeleaf` root folder run: 31 | + 32 | [,bash] 33 | ---- 34 | docker compose up -d 35 | ---- 36 | 37 | * Wait for `Elasticsearch` Docker container to be up and running. To verify it, run: 38 | + 39 | [,bash] 40 | ---- 41 | docker ps -a 42 | ---- 43 | 44 | == Initialize Elasticsearch 45 | 46 | NOTE: In the following steps, we will create an index, an alias and do a reindex using pre-defined scripts. In case you prefer to do it step-by-step calling `Elasticsearch` API, refer to link:create-index-alias-reindex.adoc[Creating indexes, alias and reindexing using Elasticsearch API]. 47 | 48 | * In a terminal, make sure you are in the `springboot-elasticsearch-thymeleaf` root folder; 49 | 50 | * Run the following script to create the index `ecommerce.products.v1` with the alias `ecommerce.products` (you can use the default values by just pressing `Enter` on every user input): 51 | + 52 | [,bash] 53 | ---- 54 | ./create-index.sh 55 | ---- 56 | 57 | * If you want to insert some products, run: 58 | + 59 | [,bash] 60 | ---- 61 | ./insert-products.sh 62 | ---- 63 | 64 | * If you want to fix the `reference` property mapping error (explained below), run: 65 | + 66 | [,bash] 67 | ---- 68 | ./reindex.sh 69 | ---- 70 | + 71 | The script `./reindex.sh` is used to reindex one index to another. The default will reindex from `ecommerce.products.v1` to `ecommerce.products.v2`. The only difference between `elasticsearch/mapping-v1.json` (used by `ecommerce.products.v1`) and `elasticsearch/mapping-v2.json` (used by `ecommerce.products.v2`) is the `type` of the `reference` property. In the former, it is set the type `text` and, in the latter, the type `keyword`. 72 | + 73 | It's interesting because the `reference` property has some special characters. An example of `reference` code is `SBES@DDR4-10000`. As it has the type `text`, `Elasticsearch` (using the `standard` analyzer) splits the content in tokens ['SBES', 'DDR4', 10000]. So, for example, if you are looking for a product with `DDR4` RAM and, for some reason, the string `DDR4` is present in the reference code of some product X, the product X will be selected, even if it doesn't have `DDR4` in its description. 74 | + 75 | So, the script `./reindex.sh` aims to fix it, setting the type `keyword` to the `reference` property. The `DDR4` search issue won't happen again because, from now on, `Elasticsearch` won't tokenize the content present in the `reference` property. 76 | 77 | == Running applications using Maven 78 | 79 | Below are the steps to start and run the applications using `Maven`. We will need to open a terminal for each one. Make sure you are in the `springboot-elasticsearch-thymeleaf` root folder while running the commands. 80 | 81 | * **product-api** 82 | + 83 | [,bash] 84 | ---- 85 | ./mvnw clean spring-boot:run --projects product-api 86 | ---- 87 | 88 | * **product-ui** 89 | + 90 | [,bash] 91 | ---- 92 | ./mvnw clean spring-boot:run --projects product-ui -Dspring-boot.run.jvmArguments="-Dserver.port=9080" 93 | ---- 94 | 95 | == Running applications as Docker containers 96 | 97 | * Build Docker Images 98 | ** In a terminal, make sure you are in the `springboot-elasticsearch-thymeleaf` root folder; 99 | ** Run the following script: 100 | + 101 | [,bash] 102 | ---- 103 | ./build-docker-images.sh 104 | ---- 105 | 106 | * Environment Variables 107 | 108 | ** **product-api** 109 | + 110 | |=== 111 | |Environment Variable |Description 112 | 113 | |`ELASTICSEARCH_URIS` 114 | |Specify uris of the `Elasticsearch` search engine to use (default `localhost:9200`) 115 | 116 | |=== 117 | 118 | ** **product-ui** 119 | + 120 | |=== 121 | |Environment Variable |Description 122 | 123 | |`PRODUCT_API_URL` 124 | |Specify url of the `product-api` service to use (default `http://localhost:8080`) 125 | 126 | |=== 127 | 128 | * Run Docker containers 129 | ** In a terminal, make sure you are in the `springboot-elasticsearch-thymeleaf` root folder; 130 | ** Run the following script: 131 | + 132 | [,bash] 133 | ---- 134 | ./start-apps.sh 135 | ---- 136 | 137 | == Application's URL 138 | 139 | |=== 140 | |Application |URL 141 | 142 | |product-api 143 | |http://localhost:8080/swagger-ui.html 144 | 145 | |product-ui 146 | |http://localhost:9080 147 | |=== 148 | 149 | == Demo 150 | 151 | * Below is a simple demo showing a user interacting with `product-ui`: 152 | + 153 | image::documentation/demo-user-interaction.gif[] 154 | 155 | == Shutdown 156 | 157 | * To stop applications: 158 | ** If they were started with `Maven`, go to `product-api` and `product-ui` terminals and press `Ctrl+C`; 159 | ** If they were started as Docker containers, go to a terminal and, inside the `springboot-elasticsearch-thymeleaf` root folder, run the script below: 160 | + 161 | [,bash] 162 | ---- 163 | ./stop-apps.sh 164 | ---- 165 | * To stop and remove docker compose containers, network and volumes, go to a terminal and, inside the `springboot-elasticsearch-thymeleaf` root folder, run the following command: 166 | + 167 | [,bash] 168 | ---- 169 | docker compose down -v 170 | ---- 171 | 172 | == Cleanup 173 | 174 | To remove the Docker images created by this project, go to a terminal and, inside the `springboot-elasticsearch-thymeleaf` root folder, run the script below: 175 | [,bash] 176 | ---- 177 | ./remove-docker-images.sh 178 | ---- 179 | -------------------------------------------------------------------------------- /build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCKER_IMAGE_PREFIX="ivanfranchin" 4 | APP_VERSION="1.0.0" 5 | 6 | PRODUCT_API_APP_NAME="product-api" 7 | PRODUCT_UI_APP_NAME="product-ui" 8 | 9 | PRODUCT_API_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${PRODUCT_API_APP_NAME}:${APP_VERSION}" 10 | PRODUCT_UI_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${PRODUCT_UI_APP_NAME}:${APP_VERSION}" 11 | 12 | SKIP_TESTS="true" 13 | 14 | ./mvnw clean spring-boot:build-image \ 15 | --projects "$PRODUCT_API_APP_NAME" \ 16 | -DskipTests="$SKIP_TESTS" \ 17 | -Dspring-boot.build-image.imageName="$PRODUCT_API_DOCKER_IMAGE_NAME" 18 | 19 | ./mvnw clean spring-boot:build-image \ 20 | --projects "$PRODUCT_UI_APP_NAME" \ 21 | -DskipTests="$SKIP_TESTS" \ 22 | -Dspring-boot.build-image.imageName="$PRODUCT_UI_DOCKER_IMAGE_NAME" 23 | -------------------------------------------------------------------------------- /create-index-alias-reindex.adoc: -------------------------------------------------------------------------------- 1 | == Creating indexes, alias and reindexing using Elasticsearch API 2 | 3 | In the following steps, we are going to manually create an index called `ecommerce.products.v1` using the `Elasticsearch` API, associate an alias called `ecommerce.products` with it, and then reindex to another index called `ecommerce.products.v2`. 4 | 5 | Make sure your `Elasticsearch` instance is clean and does not contain the previously mentioned indexes or alias. Also, the following `curl` commands must be executed in `springboot-elasticsearch-thymeleaf` root folder. 6 | 7 | * Check `Elasticsearch` is up and running 8 | + 9 | [,bash] 10 | ---- 11 | curl localhost:9200 12 | ---- 13 | + 14 | It should return something similar to: 15 | + 16 | [,json] 17 | ---- 18 | { 19 | "name" : "99fdd70d5915", 20 | "cluster_name" : "docker-cluster", 21 | "cluster_uuid" : "1HUDp8N3SF2WLtzZYOGgxA", 22 | "version" : { 23 | "number" : "8.5.3", 24 | "build_flavor" : "default", 25 | "build_type" : "docker", 26 | "build_hash" : "4ed5ee9afac63de92ec98f404ccbed7d3ba9584e", 27 | "build_date" : "2022-12-05T18:22:22.226119656Z", 28 | "build_snapshot" : false, 29 | "lucene_version" : "9.4.2", 30 | "minimum_wire_compatibility_version" : "7.17.0", 31 | "minimum_index_compatibility_version" : "7.0.0" 32 | }, 33 | "tagline" : "You Know, for Search" 34 | } 35 | ---- 36 | 37 | * Create `ecommerce.products.v1` index 38 | + 39 | [,bash] 40 | ---- 41 | curl -X PUT localhost:9200/ecommerce.products.v1 -H "Content-Type: application/json" -d @elasticsearch/mapping-v1.json 42 | ---- 43 | + 44 | It should return: 45 | + 46 | [,json] 47 | ---- 48 | {"acknowledged":true,"shards_acknowledged":true,"index":"ecommerce.products.v1"} 49 | ---- 50 | 51 | * Check indexes 52 | + 53 | [,bash] 54 | ---- 55 | curl "localhost:9200/_cat/indices?v" 56 | ---- 57 | + 58 | It should return something like: 59 | + 60 | [,text] 61 | ---- 62 | health status index uuid pri rep docs.count docs.deleted store.size pri.store.size 63 | yellow open ecommerce.products.v1 qgIIfyD1TUCN2s0wiDlmzA 1 1 0 0 225b 225b 64 | ---- 65 | 66 | * Check `ecommerce.products.v1` index mapping 67 | + 68 | [,bash] 69 | ---- 70 | curl "localhost:9200/ecommerce.products.v1/_mapping?pretty" 71 | ---- 72 | + 73 | It should return: 74 | + 75 | [, json] 76 | ---- 77 | { 78 | "ecommerce.products.v1" : { 79 | "mappings" : { 80 | "properties" : { 81 | "categories" : { 82 | "type" : "keyword" 83 | }, 84 | "created" : { 85 | "type" : "date", 86 | "format" : "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 87 | }, 88 | "description" : { 89 | "type" : "text", 90 | "analyzer" : "my_analyzer", 91 | "search_analyzer" : "my_search_analyzer" 92 | }, 93 | "name" : { 94 | "type" : "text", 95 | "analyzer" : "my_analyzer", 96 | "search_analyzer" : "my_search_analyzer" 97 | }, 98 | "price" : { 99 | "type" : "float" 100 | }, 101 | "reference" : { 102 | "type" : "text" 103 | }, 104 | "reviews" : { 105 | "properties" : { 106 | "comment" : { 107 | "type" : "text" 108 | }, 109 | "created" : { 110 | "type" : "date", 111 | "format" : "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 112 | }, 113 | "stars" : { 114 | "type" : "short" 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | ---- 123 | 124 | * Create alias for `ecommerce.products.v1` index 125 | + 126 | [,bash] 127 | ---- 128 | curl -X POST localhost:9200/_aliases -H 'Content-Type: application/json' \ 129 | -d '{ "actions": [{ "add": {"alias": "ecommerce.products", "index": "ecommerce.products.v1" }}]}' 130 | ---- 131 | + 132 | It should return: 133 | + 134 | [,json] 135 | ---- 136 | {"acknowledged":true} 137 | ---- 138 | 139 | * Check aliases 140 | + 141 | [,bash] 142 | ---- 143 | curl "localhost:9200/_aliases?pretty" 144 | ---- 145 | + 146 | It should return: 147 | + 148 | [,json] 149 | ---- 150 | { 151 | "ecommerce.products.v1" : { 152 | "aliases" : { 153 | "ecommerce.products" : { } 154 | } 155 | } 156 | } 157 | ---- 158 | 159 | * Create `ecommerce.products.v2` index 160 | + 161 | [,bash] 162 | ---- 163 | curl -X PUT localhost:9200/ecommerce.products.v2 -H "Content-Type: application/json" -d @elasticsearch/mapping-v2.json 164 | ---- 165 | + 166 | It should return: 167 | + 168 | [,json] 169 | ---- 170 | {"acknowledged":true,"shards_acknowledged":true,"index":"ecommerce.products.v2"} 171 | ---- 172 | + 173 | Check the indexes again 174 | + 175 | [,bash] 176 | ---- 177 | curl "localhost:9200/_cat/indices?v" 178 | ---- 179 | + 180 | It should return something like: 181 | + 182 | [,text] 183 | ---- 184 | health status index uuid pri rep docs.count docs.deleted store.size pri.store.size 185 | yellow open ecommerce.products.v2 pGzs5rfCR32aBVukwmEu6Q 1 1 0 0 225b 225b 186 | yellow open ecommerce.products.v1 qgIIfyD1TUCN2s0wiDlmzA 1 1 0 0 225b 225b 187 | ---- 188 | 189 | * Reindex from `ecommerce.products.v1` to `ecommerce.products.v2` 190 | + 191 | [,bash] 192 | ---- 193 | curl -X POST localhost:9200/_reindex -H 'Content-Type: application/json' \ 194 | -d '{ "source": { "index": "ecommerce.products.v1" }, "dest": { "index": "ecommerce.products.v2" }}' 195 | ---- 196 | + 197 | It should return something like: 198 | + 199 | [,json] 200 | ---- 201 | {"took":13,"timed_out":false,"total":0,"updated":0,"created":0,"deleted":0,"batches":0,"version_conflicts":0,"noops":0,"retries":{"bulk":0,"search":0},"throttled_millis":0,"requests_per_second":-1.0,"throttled_until_millis":0,"failures":[]} 202 | ---- 203 | 204 | * Adjust the alias after reindexing from `ecommerce.products.v1` to `ecommerce.products.v2` 205 | + 206 | [,bash] 207 | ---- 208 | curl -X POST localhost:9200/_aliases -H 'Content-Type: application/json' \ 209 | -d '{ "actions": [{ "remove": {"alias": "ecommerce.products", "index": "ecommerce.products.v1" }}, { "add": {"alias": "ecommerce.products", "index": "ecommerce.products.v2" }}]}' 210 | ---- 211 | + 212 | It should return: 213 | + 214 | [,json] 215 | ---- 216 | {"acknowledged":true} 217 | ---- 218 | + 219 | Check the aliases again 220 | + 221 | [,bash] 222 | ---- 223 | curl "localhost:9200/_aliases?pretty" 224 | ---- 225 | + 226 | It should return something like: 227 | + 228 | [,json] 229 | ---- 230 | { 231 | "ecommerce.products.v2" : { 232 | "aliases" : { 233 | "ecommerce.products" : { } 234 | } 235 | }, 236 | "ecommerce.products.v1" : { 237 | "aliases" : { } 238 | } 239 | } 240 | ---- 241 | 242 | * Delete `ecommerce.products.v1` index 243 | + 244 | [,bash] 245 | ---- 246 | curl -X DELETE localhost:9200/ecommerce.products.v1 247 | ---- 248 | + 249 | It should return: 250 | + 251 | [,json] 252 | ---- 253 | {"acknowledged":true} 254 | ---- 255 | + 256 | Check the aliases again 257 | + 258 | [,bash] 259 | ---- 260 | curl "localhost:9200/_aliases?pretty" 261 | ---- 262 | + 263 | It should return: 264 | + 265 | [,json] 266 | ---- 267 | { 268 | "ecommerce.products.v2" : { 269 | "aliases" : { 270 | "ecommerce.products" : { } 271 | } 272 | } 273 | } 274 | ---- 275 | 276 | * Simple search 277 | + 278 | [,bash] 279 | ---- 280 | curl "localhost:9200/ecommerce.products/_search?pretty" 281 | ---- 282 | + 283 | It should return something like: 284 | + 285 | [,json] 286 | ---- 287 | { 288 | "took" : 5, 289 | "timed_out" : false, 290 | "_shards" : { 291 | "total" : 1, 292 | "successful" : 1, 293 | "skipped" : 0, 294 | "failed" : 0 295 | }, 296 | "hits" : { 297 | "total" : { 298 | "value" : 0, 299 | "relation" : "eq" 300 | }, 301 | "max_score" : null, 302 | "hits" : [ ] 303 | } 304 | } 305 | ---- 306 | + 307 | > Since there are no products, the `hits` array field is empty. -------------------------------------------------------------------------------- /create-index.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-----------------------" 4 | echo "Fix High Disk Watermark" 5 | echo "-----------------------" 6 | # Reference: https://stackoverflow.com/questions/63880017/elasticsearch-docker-flood-stage-disk-watermark-95-exceeded 7 | 8 | curl -X PUT http://localhost:9200/_cluster/settings \ 9 | -H "Content-Type: application/json" \ 10 | -d '{ "transient": { "cluster.routing.allocation.disk.threshold_enabled": false } }' 11 | 12 | curl -X PUT http://localhost:9200/_all/_settings \ 13 | -H "Content-Type: application/json" \ 14 | -d '{ "index.blocks.read_only_allow_delete": null }' 15 | 16 | echo 17 | echo "-----------------" 18 | echo "Configuring index" 19 | echo "-----------------" 20 | 21 | read -p "Type index alias (ecommerce.products): " index_alias 22 | index_alias=${index_alias:-ecommerce.products} 23 | 24 | read -p "Type index name (ecommerce.products.v1): " index_name 25 | index_name=${index_name:-ecommerce.products.v1} 26 | 27 | read -p "Inform the path to mapping json file (elasticsearch/mapping-v1.json): " mapping_file 28 | mapping_file=${mapping_file:-elasticsearch/mapping-v1.json} 29 | 30 | echo "------------" 31 | echo "Create index" 32 | echo "------------" 33 | curl -X PUT localhost:9200/${index_name} -H "Content-Type: application/json" -d @${mapping_file} 34 | 35 | echo 36 | echo "------------" 37 | echo "Create alias" 38 | echo "------------" 39 | curl -X POST localhost:9200/_aliases -H 'Content-Type: application/json' \ 40 | -d '{ "actions": [{ "add": {"alias": "'${index_alias}'", "index": "'${index_name}'" }}]}' 41 | echo -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | elasticsearch: 4 | image: 'docker.elastic.co/elasticsearch/elasticsearch:8.15.5' 5 | container_name: 'elasticsearch' 6 | ports: 7 | - '9200:9200' 8 | - '9300:9300' 9 | environment: 10 | - 'discovery.type=single-node' 11 | - 'xpack.security.enabled=false' 12 | - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' 13 | healthcheck: 14 | test: 'curl -f http://localhost:9200 || exit 1' 15 | -------------------------------------------------------------------------------- /documentation/demo-user-interaction.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-elasticsearch-thymeleaf/ff0f4a56a88f68dfde0bd1471fd4e96462bb9dcf/documentation/demo-user-interaction.gif -------------------------------------------------------------------------------- /documentation/project-diagram.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 692, 9 | "versionNonce": 290032610, 10 | "isDeleted": false, 11 | "id": "LF8GW9IPL_GFv-IztvS9e", 12 | "fillStyle": "hachure", 13 | "strokeWidth": 1, 14 | "strokeStyle": "solid", 15 | "roughness": 1, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": 639.6104041541084, 19 | "y": 120.24569702148438, 20 | "strokeColor": "#000000", 21 | "backgroundColor": "#82c91e", 22 | "width": 209.18356323242188, 23 | "height": 99.67071533203125, 24 | "seed": 521969459, 25 | "groupIds": [], 26 | "roundness": { 27 | "type": 3 28 | }, 29 | "boundElements": [ 30 | { 31 | "type": "text", 32 | "id": "_OetYLRC4Z5TgAYd9XEfr" 33 | }, 34 | { 35 | "id": "gxDaAxiK9vlXNqvOjW4hR", 36 | "type": "arrow" 37 | }, 38 | { 39 | "id": "S2vEJVRV77fKJD57e9pov", 40 | "type": "arrow" 41 | } 42 | ], 43 | "updated": 1679413025878, 44 | "link": null, 45 | "locked": false 46 | }, 47 | { 48 | "type": "text", 49 | "version": 673, 50 | "versionNonce": 2025689459, 51 | "isDeleted": false, 52 | "id": "_OetYLRC4Z5TgAYd9XEfr", 53 | "fillStyle": "hachure", 54 | "strokeWidth": 1, 55 | "strokeStyle": "solid", 56 | "roughness": 1, 57 | "opacity": 100, 58 | "angle": 0, 59 | "x": 651.5782233679756, 60 | "y": 153.2810546875, 61 | "strokeColor": "#000000", 62 | "backgroundColor": "transparent", 63 | "width": 185.2479248046875, 64 | "height": 33.6, 65 | "seed": 542041697, 66 | "groupIds": [], 67 | "roundness": null, 68 | "boundElements": [], 69 | "updated": 1678779512188, 70 | "link": null, 71 | "locked": false, 72 | "fontSize": 28, 73 | "fontFamily": 1, 74 | "text": "Elasticsearch", 75 | "textAlign": "center", 76 | "verticalAlign": "middle", 77 | "containerId": "LF8GW9IPL_GFv-IztvS9e", 78 | "originalText": "Elasticsearch" 79 | }, 80 | { 81 | "type": "rectangle", 82 | "version": 820, 83 | "versionNonce": 718215998, 84 | "isDeleted": false, 85 | "id": "htH4DvpAlw_lK0WCfkn_y", 86 | "fillStyle": "hachure", 87 | "strokeWidth": 1, 88 | "strokeStyle": "solid", 89 | "roughness": 1, 90 | "opacity": 100, 91 | "angle": 0, 92 | "x": 302.92591857910156, 93 | "y": 120.24569702148438, 94 | "strokeColor": "#000000", 95 | "backgroundColor": "#fd7e14", 96 | "width": 209.18356323242188, 97 | "height": 99.67071533203125, 98 | "seed": 457720019, 99 | "groupIds": [], 100 | "roundness": { 101 | "type": 3 102 | }, 103 | "boundElements": [ 104 | { 105 | "type": "text", 106 | "id": "2X2Ld8TDu-NPJPkdyJl5g" 107 | }, 108 | { 109 | "id": "gxDaAxiK9vlXNqvOjW4hR", 110 | "type": "arrow" 111 | }, 112 | { 113 | "id": "LC_fwtJR91uw8tsfA6VGV", 114 | "type": "arrow" 115 | }, 116 | { 117 | "id": "S2vEJVRV77fKJD57e9pov", 118 | "type": "arrow" 119 | } 120 | ], 121 | "updated": 1679413025878, 122 | "link": null, 123 | "locked": false 124 | }, 125 | { 126 | "type": "text", 127 | "version": 823, 128 | "versionNonce": 1946685619, 129 | "isDeleted": false, 130 | "id": "2X2Ld8TDu-NPJPkdyJl5g", 131 | "fillStyle": "hachure", 132 | "strokeWidth": 1, 133 | "strokeStyle": "solid", 134 | "roughness": 1, 135 | "opacity": 100, 136 | "angle": 0, 137 | "x": 330.92374420166016, 138 | "y": 153.2810546875, 139 | "strokeColor": "#000000", 140 | "backgroundColor": "transparent", 141 | "width": 153.1879119873047, 142 | "height": 33.6, 143 | "seed": 200323745, 144 | "groupIds": [], 145 | "roundness": null, 146 | "boundElements": [], 147 | "updated": 1678779512191, 148 | "link": null, 149 | "locked": false, 150 | "fontSize": 28, 151 | "fontFamily": 1, 152 | "text": "product-api", 153 | "textAlign": "center", 154 | "verticalAlign": "middle", 155 | "containerId": "htH4DvpAlw_lK0WCfkn_y", 156 | "originalText": "product-api" 157 | }, 158 | { 159 | "type": "rectangle", 160 | "version": 1192, 161 | "versionNonce": 28426146, 162 | "isDeleted": false, 163 | "id": "NKmNZxYxWMCKh3prRiPwX", 164 | "fillStyle": "hachure", 165 | "strokeWidth": 1, 166 | "strokeStyle": "solid", 167 | "roughness": 1, 168 | "opacity": 100, 169 | "angle": 0, 170 | "x": -33.33060997490509, 171 | "y": 120.24569702148438, 172 | "strokeColor": "#000000", 173 | "backgroundColor": "#4c6ef5", 174 | "width": 209.18356323242188, 175 | "height": 99.67071533203125, 176 | "seed": 13545075, 177 | "groupIds": [], 178 | "roundness": { 179 | "type": 3 180 | }, 181 | "boundElements": [ 182 | { 183 | "id": "LC_fwtJR91uw8tsfA6VGV", 184 | "type": "arrow" 185 | }, 186 | { 187 | "type": "text", 188 | "id": "GrVT2PZuYu1cRv3sXwFoI" 189 | }, 190 | { 191 | "id": "oy43fHR6VZhZNW2ajzclU", 192 | "type": "arrow" 193 | } 194 | ], 195 | "updated": 1679413025878, 196 | "link": null, 197 | "locked": false 198 | }, 199 | { 200 | "type": "text", 201 | "version": 77, 202 | "versionNonce": 1172745405, 203 | "isDeleted": false, 204 | "id": "GrVT2PZuYu1cRv3sXwFoI", 205 | "fillStyle": "hachure", 206 | "strokeWidth": 1, 207 | "strokeStyle": "solid", 208 | "roughness": 0, 209 | "opacity": 100, 210 | "angle": 0, 211 | "x": 2.9552100324191315, 212 | "y": 153.2810546875, 213 | "strokeColor": "#000000", 214 | "backgroundColor": "transparent", 215 | "width": 136.61192321777344, 216 | "height": 33.6, 217 | "seed": 294979727, 218 | "groupIds": [], 219 | "roundness": null, 220 | "boundElements": [], 221 | "updated": 1678779512193, 222 | "link": null, 223 | "locked": false, 224 | "fontSize": 28, 225 | "fontFamily": 1, 226 | "text": "product-ui", 227 | "textAlign": "center", 228 | "verticalAlign": "middle", 229 | "containerId": "NKmNZxYxWMCKh3prRiPwX", 230 | "originalText": "product-ui" 231 | }, 232 | { 233 | "type": "ellipse", 234 | "version": 1101, 235 | "versionNonce": 2063926142, 236 | "isDeleted": false, 237 | "id": "zYllgBlgP7S7-phqNnnEr", 238 | "fillStyle": "hachure", 239 | "strokeWidth": 2, 240 | "strokeStyle": "solid", 241 | "roughness": 1, 242 | "opacity": 100, 243 | "angle": 0, 244 | "x": 219.03036719794687, 245 | "y": -14.891655178718906, 246 | "strokeColor": "#000000", 247 | "backgroundColor": "transparent", 248 | "width": 26.930389404296875, 249 | "height": 27.545562744140625, 250 | "seed": 700979517, 251 | "groupIds": [ 252 | "4D1ojplACrlVIaNZ7P0FH" 253 | ], 254 | "roundness": { 255 | "type": 2 256 | }, 257 | "boundElements": [], 258 | "updated": 1679413025878, 259 | "link": null, 260 | "locked": false 261 | }, 262 | { 263 | "type": "line", 264 | "version": 1120, 265 | "versionNonce": 1717402466, 266 | "isDeleted": false, 267 | "id": "iW_3iMfYwsgECYYtnEK33", 268 | "fillStyle": "hachure", 269 | "strokeWidth": 2, 270 | "strokeStyle": "solid", 271 | "roughness": 1, 272 | "opacity": 100, 273 | "angle": 0, 274 | "x": 230.4682028913058, 275 | "y": 13.108985690421719, 276 | "strokeColor": "#000000", 277 | "backgroundColor": "transparent", 278 | "width": 0.473419189453125, 279 | "height": 40.3687744140625, 280 | "seed": 1665919389, 281 | "groupIds": [ 282 | "4D1ojplACrlVIaNZ7P0FH" 283 | ], 284 | "roundness": { 285 | "type": 2 286 | }, 287 | "boundElements": [], 288 | "updated": 1679413025878, 289 | "link": null, 290 | "locked": false, 291 | "startBinding": null, 292 | "endBinding": null, 293 | "lastCommittedPoint": null, 294 | "startArrowhead": null, 295 | "endArrowhead": null, 296 | "points": [ 297 | [ 298 | 0, 299 | 0 300 | ], 301 | [ 302 | -0.473419189453125, 303 | 40.3687744140625 304 | ] 305 | ] 306 | }, 307 | { 308 | "type": "line", 309 | "version": 1071, 310 | "versionNonce": 1423172542, 311 | "isDeleted": false, 312 | "id": "8JTNvN86yjteIVYunsqxA", 313 | "fillStyle": "hachure", 314 | "strokeWidth": 2, 315 | "strokeStyle": "solid", 316 | "roughness": 1, 317 | "opacity": 100, 318 | "angle": 0, 319 | "x": 230.17270118232142, 320 | "y": 54.91263559276547, 321 | "strokeColor": "#000000", 322 | "backgroundColor": "transparent", 323 | "width": 17.21380615234375, 324 | "height": 33.91400146484375, 325 | "seed": 1891020285, 326 | "groupIds": [ 327 | "4D1ojplACrlVIaNZ7P0FH" 328 | ], 329 | "roundness": { 330 | "type": 2 331 | }, 332 | "boundElements": [], 333 | "updated": 1679413025878, 334 | "link": null, 335 | "locked": false, 336 | "startBinding": null, 337 | "endBinding": null, 338 | "lastCommittedPoint": null, 339 | "startArrowhead": null, 340 | "endArrowhead": null, 341 | "points": [ 342 | [ 343 | 0, 344 | 0 345 | ], 346 | [ 347 | -17.21380615234375, 348 | 33.91400146484375 349 | ] 350 | ] 351 | }, 352 | { 353 | "type": "line", 354 | "version": 1090, 355 | "versionNonce": 2107359010, 356 | "isDeleted": false, 357 | "id": "hMliKg9HCYu-lEplUg1Y6", 358 | "fillStyle": "hachure", 359 | "strokeWidth": 2, 360 | "strokeStyle": "solid", 361 | "roughness": 1, 362 | "opacity": 100, 363 | "angle": 0, 364 | "x": 230.28854590888392, 365 | "y": 55.03281381542172, 366 | "strokeColor": "#000000", 367 | "backgroundColor": "transparent", 368 | "width": 12.9422607421875, 369 | "height": 35.16510009765625, 370 | "seed": 554296925, 371 | "groupIds": [ 372 | "4D1ojplACrlVIaNZ7P0FH" 373 | ], 374 | "roundness": { 375 | "type": 2 376 | }, 377 | "boundElements": [], 378 | "updated": 1679413025878, 379 | "link": null, 380 | "locked": false, 381 | "startBinding": null, 382 | "endBinding": null, 383 | "lastCommittedPoint": null, 384 | "startArrowhead": null, 385 | "endArrowhead": null, 386 | "points": [ 387 | [ 388 | 0, 389 | 0 390 | ], 391 | [ 392 | 12.9422607421875, 393 | 35.16510009765625 394 | ] 395 | ] 396 | }, 397 | { 398 | "type": "line", 399 | "version": 1106, 400 | "versionNonce": 173849598, 401 | "isDeleted": false, 402 | "id": "pnA0DA25tJxDKgaSQnlkY", 403 | "fillStyle": "hachure", 404 | "strokeWidth": 2, 405 | "strokeStyle": "solid", 406 | "roughness": 1, 407 | "opacity": 100, 408 | "angle": 0, 409 | "x": 231.48437720771204, 410 | "y": 30.60697153026547, 411 | "strokeColor": "#000000", 412 | "backgroundColor": "transparent", 413 | "width": 29.445220947265625, 414 | "height": 20.990234375, 415 | "seed": 1729565373, 416 | "groupIds": [ 417 | "4D1ojplACrlVIaNZ7P0FH" 418 | ], 419 | "roundness": { 420 | "type": 2 421 | }, 422 | "boundElements": [], 423 | "updated": 1679413025878, 424 | "link": null, 425 | "locked": false, 426 | "startBinding": null, 427 | "endBinding": null, 428 | "lastCommittedPoint": null, 429 | "startArrowhead": null, 430 | "endArrowhead": null, 431 | "points": [ 432 | [ 433 | 0, 434 | 0 435 | ], 436 | [ 437 | 29.445220947265625, 438 | -20.990234375 439 | ] 440 | ] 441 | }, 442 | { 443 | "type": "line", 444 | "version": 1145, 445 | "versionNonce": 1675332322, 446 | "isDeleted": false, 447 | "id": "HhvpXqS4JXS1iu_1aiVep", 448 | "fillStyle": "hachure", 449 | "strokeWidth": 2, 450 | "strokeStyle": "solid", 451 | "roughness": 1, 452 | "opacity": 100, 453 | "angle": 0, 454 | "x": 230.59347754950892, 455 | "y": 29.87680795604672, 456 | "strokeColor": "#000000", 457 | "backgroundColor": "transparent", 458 | "width": 25.4169921875, 459 | "height": 9.85821533203125, 460 | "seed": 1602204445, 461 | "groupIds": [ 462 | "4D1ojplACrlVIaNZ7P0FH" 463 | ], 464 | "roundness": { 465 | "type": 2 466 | }, 467 | "boundElements": [], 468 | "updated": 1679413025878, 469 | "link": null, 470 | "locked": false, 471 | "startBinding": null, 472 | "endBinding": null, 473 | "lastCommittedPoint": null, 474 | "startArrowhead": null, 475 | "endArrowhead": null, 476 | "points": [ 477 | [ 478 | 0, 479 | 0 480 | ], 481 | [ 482 | -25.4169921875, 483 | -9.85821533203125 484 | ] 485 | ] 486 | }, 487 | { 488 | "type": "text", 489 | "version": 1171, 490 | "versionNonce": 27298878, 491 | "isDeleted": false, 492 | "id": "T0m-48lm7wA_Uw99BXSmk", 493 | "fillStyle": "hachure", 494 | "strokeWidth": 2, 495 | "strokeStyle": "solid", 496 | "roughness": 1, 497 | "opacity": 100, 498 | "angle": 0, 499 | "x": 208.38458472724375, 500 | "y": -46.837089749031406, 501 | "strokeColor": "#000000", 502 | "backgroundColor": "transparent", 503 | "width": 44.679962158203125, 504 | "height": 24, 505 | "seed": 380979069, 506 | "groupIds": [ 507 | "4D1ojplACrlVIaNZ7P0FH" 508 | ], 509 | "roundness": null, 510 | "boundElements": [], 511 | "updated": 1679413025878, 512 | "link": null, 513 | "locked": false, 514 | "fontSize": 20, 515 | "fontFamily": 1, 516 | "text": "User", 517 | "textAlign": "left", 518 | "verticalAlign": "top", 519 | "containerId": null, 520 | "originalText": "User" 521 | }, 522 | { 523 | "type": "arrow", 524 | "version": 1049, 525 | "versionNonce": 1336663714, 526 | "isDeleted": false, 527 | "id": "gxDaAxiK9vlXNqvOjW4hR", 528 | "fillStyle": "hachure", 529 | "strokeWidth": 1, 530 | "strokeStyle": "solid", 531 | "roughness": 1, 532 | "opacity": 100, 533 | "angle": 0, 534 | "x": 519.0704978131809, 535 | "y": 169.12250952760584, 536 | "strokeColor": "#000000", 537 | "backgroundColor": "transparent", 538 | "width": 115.5447998046875, 539 | "height": 1.9716219740382144, 540 | "seed": 192154430, 541 | "groupIds": [], 542 | "roundness": { 543 | "type": 2 544 | }, 545 | "boundElements": [], 546 | "updated": 1679413025878, 547 | "link": null, 548 | "locked": false, 549 | "startBinding": { 550 | "elementId": "htH4DvpAlw_lK0WCfkn_y", 551 | "focus": 0.05538097063906322, 552 | "gap": 6.961016001657413 553 | }, 554 | "endBinding": { 555 | "elementId": "LF8GW9IPL_GFv-IztvS9e", 556 | "focus": -0.14363249406295067, 557 | "gap": 4.995106536240087 558 | }, 559 | "lastCommittedPoint": null, 560 | "startArrowhead": "arrow", 561 | "endArrowhead": "arrow", 562 | "points": [ 563 | [ 564 | 0, 565 | 0 566 | ], 567 | [ 568 | 115.5447998046875, 569 | 1.9716219740382144 570 | ] 571 | ] 572 | }, 573 | { 574 | "type": "arrow", 575 | "version": 408, 576 | "versionNonce": 947934334, 577 | "isDeleted": false, 578 | "id": "S2vEJVRV77fKJD57e9pov", 579 | "fillStyle": "hachure", 580 | "strokeWidth": 1, 581 | "strokeStyle": "solid", 582 | "roughness": 1, 583 | "opacity": 100, 584 | "angle": 0, 585 | "x": 260.2475628190264, 586 | "y": 26.546714572257653, 587 | "strokeColor": "#000000", 588 | "backgroundColor": "transparent", 589 | "width": 137.51619678929893, 590 | "height": 72.78610229492188, 591 | "seed": 1983245747, 592 | "groupIds": [], 593 | "roundness": { 594 | "type": 2 595 | }, 596 | "boundElements": [], 597 | "updated": 1679413025878, 598 | "link": null, 599 | "locked": false, 600 | "startBinding": null, 601 | "endBinding": { 602 | "elementId": "htH4DvpAlw_lK0WCfkn_y", 603 | "focus": 0.05632092297363595, 604 | "gap": 20.912880154304844 605 | }, 606 | "lastCommittedPoint": null, 607 | "startArrowhead": "arrow", 608 | "endArrowhead": "arrow", 609 | "points": [ 610 | [ 611 | 0, 612 | 0 613 | ], 614 | [ 615 | 122.75413261533386, 616 | 8.677509370562575 617 | ], 618 | [ 619 | 137.51619678929893, 620 | 72.78610229492188 621 | ] 622 | ] 623 | }, 624 | { 625 | "type": "arrow", 626 | "version": 460, 627 | "versionNonce": 1304148578, 628 | "isDeleted": false, 629 | "id": "LC_fwtJR91uw8tsfA6VGV", 630 | "fillStyle": "hachure", 631 | "strokeWidth": 1, 632 | "strokeStyle": "solid", 633 | "roughness": 1, 634 | "opacity": 100, 635 | "angle": 0, 636 | "x": 194.06096337378136, 637 | "y": 23.636009005851406, 638 | "strokeColor": "#000000", 639 | "backgroundColor": "transparent", 640 | "width": 113.85602790549177, 641 | "height": 83.36630249023438, 642 | "seed": 255049555, 643 | "groupIds": [], 644 | "roundness": { 645 | "type": 2 646 | }, 647 | "boundElements": [], 648 | "updated": 1679413025878, 649 | "link": null, 650 | "locked": false, 651 | "startBinding": null, 652 | "endBinding": { 653 | "elementId": "NKmNZxYxWMCKh3prRiPwX", 654 | "focus": 0.013634127401409455, 655 | "gap": 13.243385525398594 656 | }, 657 | "lastCommittedPoint": null, 658 | "startArrowhead": "arrow", 659 | "endArrowhead": "arrow", 660 | "points": [ 661 | [ 662 | 0, 663 | 0 664 | ], 665 | [ 666 | -105.46894620306404, 667 | 12.23523978147166 668 | ], 669 | [ 670 | -113.85602790549177, 671 | 83.36630249023438 672 | ] 673 | ] 674 | }, 675 | { 676 | "type": "arrow", 677 | "version": 124, 678 | "versionNonce": 1220351166, 679 | "isDeleted": false, 680 | "id": "oy43fHR6VZhZNW2ajzclU", 681 | "fillStyle": "hachure", 682 | "strokeWidth": 1, 683 | "strokeStyle": "solid", 684 | "roughness": 1, 685 | "opacity": 100, 686 | "angle": 0, 687 | "x": 177.97221825659386, 688 | "y": 172.8113041923967, 689 | "strokeColor": "#000000", 690 | "backgroundColor": "#fd7e14", 691 | "width": 126.33859252929688, 692 | "height": 5.325196330049778, 693 | "seed": 463204386, 694 | "groupIds": [], 695 | "roundness": { 696 | "type": 2 697 | }, 698 | "boundElements": [], 699 | "updated": 1679413025878, 700 | "link": null, 701 | "locked": false, 702 | "startBinding": { 703 | "elementId": "NKmNZxYxWMCKh3prRiPwX", 704 | "focus": 0.15752488434768247, 705 | "gap": 2.1192649990770747 706 | }, 707 | "endBinding": null, 708 | "lastCommittedPoint": null, 709 | "startArrowhead": "arrow", 710 | "endArrowhead": "arrow", 711 | "points": [ 712 | [ 713 | 0, 714 | 0 715 | ], 716 | [ 717 | 126.33859252929688, 718 | -5.325196330049778 719 | ] 720 | ] 721 | } 722 | ], 723 | "appState": { 724 | "gridSize": null, 725 | "viewBackgroundColor": "#ffffff" 726 | }, 727 | "files": {} 728 | } -------------------------------------------------------------------------------- /documentation/project-diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/springboot-elasticsearch-thymeleaf/ff0f4a56a88f68dfde0bd1471fd4e96462bb9dcf/documentation/project-diagram.jpeg -------------------------------------------------------------------------------- /elasticsearch/mapping-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "analyzer": { 5 | "my_analyzer": { 6 | "tokenizer": "standard", 7 | "filter": [ 8 | "lowercase", 9 | "edge_ngram_filter" 10 | ] 11 | }, 12 | "my_search_analyzer": { 13 | "tokenizer": "standard", 14 | "filter": [ 15 | "lowercase" 16 | ] 17 | } 18 | }, 19 | "filter": { 20 | "edge_ngram_filter": { 21 | "type": "edge_ngram", 22 | "min_gram": 1, 23 | "max_gram": 20 24 | } 25 | } 26 | } 27 | }, 28 | "mappings": { 29 | "properties": { 30 | "categories": { 31 | "type": "keyword" 32 | }, 33 | "created": { 34 | "type": "date", 35 | "format": "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 36 | }, 37 | "description": { 38 | "type": "text", 39 | "analyzer": "my_analyzer", 40 | "search_analyzer": "my_search_analyzer" 41 | }, 42 | "name": { 43 | "type": "text", 44 | "analyzer": "my_analyzer", 45 | "search_analyzer": "my_search_analyzer" 46 | }, 47 | "price": { 48 | "type": "float" 49 | }, 50 | "reference": { 51 | "type": "text" 52 | }, 53 | "reviews": { 54 | "properties": { 55 | "comment": { 56 | "type": "text" 57 | }, 58 | "created": { 59 | "type": "date", 60 | "format": "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 61 | }, 62 | "stars": { 63 | "type": "short" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /elasticsearch/mapping-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "analyzer": { 5 | "my_analyzer": { 6 | "tokenizer": "standard", 7 | "filter": [ 8 | "lowercase", 9 | "edge_ngram_filter" 10 | ] 11 | }, 12 | "my_search_analyzer": { 13 | "tokenizer": "standard", 14 | "filter": [ 15 | "lowercase" 16 | ] 17 | } 18 | }, 19 | "filter": { 20 | "edge_ngram_filter": { 21 | "type": "edge_ngram", 22 | "min_gram": 1, 23 | "max_gram": 20 24 | } 25 | } 26 | } 27 | }, 28 | "mappings": { 29 | "properties": { 30 | "categories": { 31 | "type": "keyword" 32 | }, 33 | "created": { 34 | "type": "date", 35 | "format": "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 36 | }, 37 | "description": { 38 | "type": "text", 39 | "analyzer": "my_analyzer", 40 | "search_analyzer": "my_search_analyzer" 41 | }, 42 | "name": { 43 | "type": "text", 44 | "analyzer": "my_analyzer", 45 | "search_analyzer": "my_search_analyzer" 46 | }, 47 | "price": { 48 | "type": "float" 49 | }, 50 | "reference": { 51 | "type": "keyword" 52 | }, 53 | "reviews": { 54 | "properties": { 55 | "comment": { 56 | "type": "text" 57 | }, 58 | "created": { 59 | "type": "date", 60 | "format": "strict_date_time_no_millis||yyyy-MM-dd'T'HH:mmZZ" 61 | }, 62 | "stars": { 63 | "type": "short" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /elasticsearch/products.json: -------------------------------------------------------------------------------- 1 | {"index":{}} 2 | { "reference": "SBES@TBA1-10000", "name": "Acer Aspire 3 A315-51-356P", "description": "This laptop from Acer is a stylish and powerful Notebook Laptop and is powered by Intel Core i3 (6th Gen) processor clocked at a speed of 2 GHz and sports a 15.6 inchOn the memory front, the laptop is equipped with a hard drive of 1 TB and a 4 DDR4 RAM, thereby making it possible to store ample amount of data.", "price": 599.99, "categories": ["laptops", "acer"], "reviews": [ {"comment": "I didn't like the keyboard.", "stars": 3, "created": "2018-09-20T10:00:00Z"} ], "created": "2018-09-19T10:00:00Z" } 3 | {"index":{}} 4 | { "reference": "SBES@DDR4-10100", "name": "Dell XPS 13", "description": "This laptop from Dell is probably the smallest 13-inch ultrabook out there. And despite its slim-trim dimensions, Dell packs the XPS 13 with a lot of hardware firepower and features an excellent look, feel and design to propel it amongst the best ultrabooks available in the market. Dell XPS 13 is a stylish and powerfulthereby making it possible to store ample amount of data.", "price": 799.00, "categories": ["laptops", "dell"], "reviews": [ {"comment": "It's a very good laptop. I recommend!", "stars": 4, "created": "2018-09-20T11:00:00Z"}, {"comment": "The screen is too small!", "stars": 2, "created": "2018-09-20T12:00:00Z"} ], "created": "2018-09-19T11:00:00Z" } 5 | {"index":{}} 6 | { "reference": "SBES@DDR4-10200", "name": "Dell Inspiron 15 3000", "description": "This 15-inch laptop from Dell is dressed all in black, through and through. While that may be considered boring in comparison to a few colourful laptops mentioned above, the fine textured finish on the Dell Inspiron 15 3000’s screen cover gives it an air of premiumness. It doesn’t have an optical drive, which is a bit of a shame, but on every other front it matches toe to toe with laptops ranked above it. In fact, it has a 720p HD webcam and very good onboard sound thanks to its Waves Maxxaudio speakers.", "price": 890.00, "categories": ["laptops", "dell"], "reviews": [ {"comment": "It has a very good webcam!", "stars": 4, "created": "2018-09-20T13:00:00Z"} ], "created": "2018-09-19T12:00:00Z" } 7 | {"index":{}} 8 | { "reference": "SBES@DDR4-10300", "name": "Microsoft Surface Laptop", "description": "Microsoft has launched a durable, portable, lightweight and premium looking laptop called the Surface Laptop, that weights merely 1.25 Kgs and thickness ranges from 9.9mm to 14.47mm. According to the company, it is thinner and lighter than any MacBook in the market, and it is faster than the current MacBook Air. It features a 13.5-inch PixelSense touchscreen display with a screen resolution of 2256 x 1504 pixels, aspect ratio of 3:2 and Corning Gorilla Glass for protection. It is powered by Intel core i5 or i7 processor and supports upto 16 GB RAM and 512 GB SSD. The base model comes with 4 GB RAM and 128 GB SSD, while other memory variants are also available. The base model supports Intel HD 620 graphics engine, while the Intel Core i7 model supports Intel Iris Plus Graphics 640.", "price": 1590.90, "categories": ["laptops", "microsoft"], "reviews": [ {"comment": "It is very light and has 16 GB RAM. Amazing laptop!", "stars": 5, "created": "2018-09-20T14:00:00Z"} ], "created": "2018-09-19T13:00:00Z" } 9 | {"index":{}} 10 | { "reference": "SBES@DDR4-20100", "name": "Apple MacBook Pro 2018 15-inch", "description": "This 15-inch laptop from Apple is a stylish and powerful Ultraportable Laptop and is powered by Dual core and sports a 13.3 inch Retina that comes with a resolution of 2560x1600 pixels, so the picture quality is crisp and detailed.The screen size is big enough for an enriching gaming, surfing and video watching experience.On the memory front, the laptop is equipped with a hard drive of 1 TB and a 8 DDR3 RAM,thereby making it possible to store ample amount of data. All the above features ensure that you breeze through all your tasks throughout the day. The connectivity options available on the device are Yes, Bluetooth 4.2 wireless technology. It is backed up by a lithium-polymer battery that keeps the device running for up to up to 10 hours hours or a considerable amount of time", "price": 1785.98, "categories": ["laptops", "apple"], "reviews": [ {"comment": "It's very expensive, but it is worth to pay this price! Nice laptop!", "stars": 4, "created": "2018-09-20T15:00:00Z"}, {"comment": "Too expensive! No way! :P!", "stars": 1, "created": "2018-09-20T16:00:00Z"}, {"comment": "1 TB of HHD! Wow!", "stars": 4, "created": "2018-09-20T17:00:00Z"} ], "created": "2018-09-19T14:00:00Z" } 11 | {"index":{}} 12 | { "reference": "SBES@DDR4-20200", "name": "Apple MacBook Air MQD32HN/A", "description": "This laptop from Apple is a stylish and powerful Notebook Laptop and is powered by Dual core clocked at a speed of 1.8 GHz and sports a 13.3 inch LED-backlit glossy widescreen that comes with a resolution of 1366 x 768 pixels, so the picture quality is crisp and detailed. The screen size is big enough for an enriching gaming, surfing and video watching experience. On the memory front, the laptop is equipped with a hard drive of 128 GB and a 8 LPDDR3 RAM, thereby making it possible to store ample amount of data. All the above features ensure that you breeze through all your tasks throughout the day. It is backed up by a Li-ion battery that keeps the device running for Up to 10 hours hours or a considerable amount of time.", "price": 1999.99, "categories": ["laptops", "apple"], "reviews": [ {"comment": "Hard drive of just 128 GB", "stars": 2, "created": "2018-09-20T18:00:00Z"}, {"comment": "It's very light but too expensive!", "stars": 2, "created": "2018-09-20T19:00:00Z"} ], "created": "2018-09-19T15:00:00Z" } 13 | {"index":{}} 14 | { "reference": "SBES@CBA2-20400", "name": "Asus VivoBook S15", "description": "This laptop from Asus is a stylish and powerful Ultraportable Laptop and is powered by Intel 8th generation Core i5-8250U processor and sports a 15.6 inch FHD NanoEdge that comes with a resolution of 1920 x 1080 pixels,so the picture quality is crisp and detailed. The screen size is big enough for an enriching gaming, surfing and video watching experience.On the memory front, the laptop is equipped with a hard drive of 1 TB and a 16 DDR4 RAM,thereby making it possible to store ample amount of data. All the above features ensure that you breeze through all your tasks throughout the day. The connectivity options available on the device are Yes, Bluetooth v4.2.", "price": 1450.00, "categories": ["laptops", "asus"], "reviews": [], "created": "2018-09-19T16:00:00Z" } 15 | {"index":{}} 16 | { "reference": "SBES@1C4A-20500", "name": "Asus VivoBook S14 (S410UA)", "description": "This laptop from Asus is a stylish and powerful and is powered by 8th Generation Intel Core processor and sports a 14 inchOn the memory front, the laptop is equipped with a hard drive of 256 GB and a 8 DDR4 RAM, thereby making it possible to store ample amount of data. All the above features ensure that you breeze through all your tasks throughout the day. The connectivity options available on the device are Yes, WiFi, Bluetooth. It supports an optical Drive, comes with USB 3.1 Type-C port, one USB 3.1 port and USB 2.0 porT. It is backed up by a Li-Polymer battery that keeps the device running for upto 8 hours or a considerable amount of time.", "price": 1250.00, "categories": ["laptops", "asus"], "reviews": [ {"comment": "Good processor and memory", "stars": 3, "created": "2018-09-20T20:00:00Z"} ], "created": "2018-09-19T17:00:00Z" } 17 | -------------------------------------------------------------------------------- /insert-products.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "------------------" 4 | echo "Inserting products" 5 | echo "------------------" 6 | 7 | read -p "Type index name or alias (ecommerce.products): " index_name_alias 8 | index_name_alias=${index_name_alias:-ecommerce.products} 9 | 10 | read -p "Type path to products file (elasticsearch/products.json): " products_file 11 | products_file=${products_file:-elasticsearch/products.json} 12 | 13 | curl -X POST localhost:9200/${index_name_alias}/_bulk -H "Content-Type: application/json" --data-binary @${products_file} 14 | echo -------------------------------------------------------------------------------- /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.4 9 | 10 | 11 | com.ivanfranchin 12 | springboot-elasticsearch-thymeleaf 13 | 1.0.0 14 | pom 15 | springboot-elasticsearch-thymeleaf 16 | Demo project for Spring Boot 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 21 32 | 33 | 34 | product-api 35 | product-ui 36 | 37 | 38 | -------------------------------------------------------------------------------- /product-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | springboot-elasticsearch-thymeleaf 8 | 1.0.0 9 | ../pom.xml 10 | 11 | product-api 12 | product-api 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 2.8.6 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-elasticsearch 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-validation 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | 45 | 46 | org.springdoc 47 | springdoc-openapi-starter-webmvc-ui 48 | ${springdoc-openapi.version} 49 | 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | true 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.apache.maven.plugins 67 | maven-compiler-plugin 68 | 69 | 70 | 71 | org.projectlombok 72 | lombok 73 | 74 | 75 | 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-maven-plugin 80 | 81 | 82 | 83 | org.projectlombok 84 | lombok 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/ProductApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ProductApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ProductApiApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.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, 22 | options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 23 | } 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/config/SpringDataWebSupportConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 5 | 6 | import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO; 7 | 8 | /* 9 | The reason this configuration class was added is to prevent the following WARN message from occurring. 10 | 11 | WARN Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure! 12 | For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) 13 | or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables. 14 | */ 15 | @Configuration 16 | @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) 17 | public class SpringDataWebSupportConfig { 18 | } -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.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 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product; 2 | 3 | import com.ivanfranchin.productapi.product.model.Product; 4 | import com.ivanfranchin.productapi.product.dto.CreateProductRequest; 5 | import com.ivanfranchin.productapi.product.dto.SearchRequest; 6 | import com.ivanfranchin.productapi.product.dto.UpdateProductRequest; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import jakarta.validation.Valid; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springdoc.core.annotations.ParameterObject; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.Pageable; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.DeleteMapping; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.PostMapping; 18 | import org.springframework.web.bind.annotation.PutMapping; 19 | import org.springframework.web.bind.annotation.RequestBody; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.ResponseStatus; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | @RequiredArgsConstructor 25 | @RestController 26 | @RequestMapping("/api/products") 27 | public class ProductController { 28 | 29 | private final ProductService productService; 30 | 31 | @Operation(summary = "Get Products") 32 | @GetMapping 33 | public Page getProducts(@ParameterObject Pageable pageable) { 34 | return productService.listProductsByPage(pageable); 35 | } 36 | 37 | @Operation(summary = "Get Product") 38 | @GetMapping("/{id}") 39 | public Product getProduct(@PathVariable String id) { 40 | return productService.validateAndGetProductById(id); 41 | } 42 | 43 | @Operation(summary = "Create Product") 44 | @ResponseStatus(HttpStatus.CREATED) 45 | @PostMapping 46 | public Product createProduct(@Valid @RequestBody CreateProductRequest createProductRequest) { 47 | Product product = Product.from(createProductRequest); 48 | return productService.saveProduct(product); 49 | } 50 | 51 | @Operation(summary = "Update Product") 52 | @PutMapping("/{id}") 53 | public Product updateProduct(@PathVariable String id, @Valid @RequestBody UpdateProductRequest updateProductRequest) { 54 | Product product = productService.validateAndGetProductById(id); 55 | Product.updateFrom(updateProductRequest, product); 56 | return productService.saveProduct(product); 57 | } 58 | 59 | @Operation(summary = "Delete Product") 60 | @DeleteMapping("/{id}") 61 | public String deleteProduct(@PathVariable String id) { 62 | Product product = productService.validateAndGetProductById(id); 63 | productService.deleteProduct(product); 64 | return id; 65 | } 66 | 67 | @Operation( 68 | summary = "Search for Products", 69 | description = "This endpoint queries for a 'text' informed in the following fields: 'reference', 'name' and 'description'") 70 | @PutMapping("/search") 71 | public Page searchProducts(@Valid @RequestBody SearchRequest searchRequest, @ParameterObject Pageable pageable) { 72 | return productService.search(searchRequest.text(), pageable); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product; 2 | 3 | import com.ivanfranchin.productapi.product.model.Product; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface ProductRepository extends ElasticsearchRepository { 11 | 12 | Page findByReferenceOrNameOrDescription(String reference, String name, String description, Pageable pageable); 13 | } 14 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product; 2 | 3 | import com.ivanfranchin.productapi.product.exception.ProductNotFoundException; 4 | import com.ivanfranchin.productapi.product.model.Product; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Service; 9 | 10 | import static org.apache.commons.lang3.RandomStringUtils.secure; 11 | 12 | @RequiredArgsConstructor 13 | @Service 14 | public class ProductService { 15 | 16 | private final ProductRepository productRepository; 17 | 18 | public Page listProductsByPage(Pageable pageable) { 19 | return productRepository.findAll(pageable); 20 | } 21 | 22 | public Product validateAndGetProductById(String id) { 23 | return productRepository.findById(id).orElseThrow(() -> new ProductNotFoundException(id)); 24 | } 25 | 26 | public Product saveProduct(Product product) { 27 | product.setReference("SBES@%s-%s" 28 | .formatted(secure().nextAlphanumeric(4), secure().nextNumeric(5))); 29 | return productRepository.save(product); 30 | } 31 | 32 | public void deleteProduct(Product product) { 33 | productRepository.delete(product); 34 | } 35 | 36 | public Page search(String text, Pageable pageable) { 37 | return productRepository.findByReferenceOrNameOrDescription(text, text, text, pageable); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/dto/CreateProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.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 | 7 | import java.math.BigDecimal; 8 | import java.util.Set; 9 | 10 | public record CreateProductRequest( 11 | @Schema(title = "product name", example = "Apple 13\" MacBook Pro") @NotBlank String name, 12 | @Schema(title = "product description", example = "Apple 13\" MacBook Pro, Retina Display, 2.3GHz Intel Core i5 Dual Core, 8GB RAM, 128GB SSD, Space Gray, MPXQ2LL/A ") @NotBlank String description, 13 | @Schema(title = "product price", example = "1099.90") @NotNull BigDecimal price, 14 | @Schema(title = "product categories", example = "[\"laptops\", \"apple\"]") Set categories) { 15 | } 16 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/dto/SearchRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record SearchRequest(@Schema(title = "text to be searched", example = "DDR4") @NotBlank String text) { 7 | } 8 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/dto/UpdateProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Set; 7 | 8 | public record UpdateProductRequest( 9 | @Schema(title = "product name", example = "Apple 15\" MacBook Pro") String name, 10 | @Schema(title = "product description", example = "Apple 15\" MacBook Pro, Retina Display, 2.3GHz Intel Core i5 Dual Core, 8GB RAM, 128GB SSD, Space Gray, MPXQ2LL/A ") String description, 11 | @Schema(title = "product price", example = "1599.90") BigDecimal price, 12 | @Schema(title = "product categories", example = "[\"laptops\", \"apple\"]") Set categories) { 13 | } 14 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/exception/ProductNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.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(String id) { 10 | super(String.format("Product id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.model; 2 | 3 | import com.ivanfranchin.productapi.product.dto.CreateProductRequest; 4 | import com.ivanfranchin.productapi.product.dto.UpdateProductRequest; 5 | import com.ivanfranchin.productapi.product.review.Review; 6 | import com.ivanfranchin.productapi.util.DateTimeUtil; 7 | import lombok.Data; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.elasticsearch.annotations.Document; 10 | 11 | import java.math.BigDecimal; 12 | import java.util.ArrayList; 13 | import java.util.LinkedHashSet; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | @Data 18 | @Document(indexName = "ecommerce.products", createIndex = false) 19 | public class Product { 20 | 21 | public Product() { 22 | this.created = DateTimeUtil.createCurrentDateAsString(); 23 | } 24 | 25 | @Id 26 | private String id; 27 | private String reference; 28 | private String name; 29 | private String description; 30 | private BigDecimal price; 31 | private Set categories; 32 | private List reviews = new ArrayList<>(); 33 | private String created; 34 | 35 | public static Product from(CreateProductRequest createProductRequest) { 36 | Product product = new Product(); 37 | product.setName(createProductRequest.name()); 38 | product.setDescription(createProductRequest.description()); 39 | product.setPrice(createProductRequest.price()); 40 | Set set = createProductRequest.categories(); 41 | if (set != null) { 42 | product.setCategories(new LinkedHashSet<>(set)); 43 | } 44 | return product; 45 | } 46 | 47 | public static void updateFrom(UpdateProductRequest updateProductRequest, Product product) { 48 | if (updateProductRequest.name() != null) { 49 | product.setName(updateProductRequest.name()); 50 | } 51 | if (updateProductRequest.description() != null) { 52 | product.setDescription(updateProductRequest.description()); 53 | } 54 | if (updateProductRequest.price() != null) { 55 | product.setPrice(updateProductRequest.price()); 56 | } 57 | if (product.getCategories() != null) { 58 | Set set = updateProductRequest.categories(); 59 | if (set != null) { 60 | product.getCategories().clear(); 61 | product.getCategories().addAll(set); 62 | } 63 | } else { 64 | Set set = updateProductRequest.categories(); 65 | if (set != null) { 66 | product.setCategories(new LinkedHashSet<>(set)); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/review/AddReviewRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.review; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Max; 5 | import jakarta.validation.constraints.Min; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.NotNull; 8 | 9 | public record AddReviewRequest( 10 | @Schema(title = "comment about product", example = "This product is very good!") @NotBlank String comment, 11 | @Schema(title = "product evaluation (from 0 to 5)", example = "5") @NotNull @Min(0) @Max(5) Integer stars) { 12 | } 13 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/review/ProductReviewController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.review; 2 | 3 | import com.ivanfranchin.productapi.product.ProductService; 4 | import com.ivanfranchin.productapi.product.model.Product; 5 | import io.swagger.v3.oas.annotations.Operation; 6 | import jakarta.validation.Valid; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.ResponseStatus; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import java.util.List; 18 | 19 | @RequiredArgsConstructor 20 | @RestController 21 | @RequestMapping("/api/products/{id}/reviews") 22 | public class ProductReviewController { 23 | 24 | private final ProductService productService; 25 | 26 | @Operation(summary = "Get reviews about product") 27 | @GetMapping 28 | public List getProductReviews(@PathVariable String id) { 29 | Product product = productService.validateAndGetProductById(id); 30 | return product.getReviews(); 31 | } 32 | 33 | @Operation(summary = "Add review about product") 34 | @ResponseStatus(HttpStatus.CREATED) 35 | @PostMapping 36 | public Review addProductReview(@PathVariable String id, @Valid @RequestBody AddReviewRequest addReviewRequest) { 37 | Product product = productService.validateAndGetProductById(id); 38 | 39 | Review review = Review.from(addReviewRequest); 40 | product.getReviews().add(review); 41 | productService.saveProduct(product); 42 | 43 | return review; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/product/review/Review.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.product.review; 2 | 3 | import com.ivanfranchin.productapi.util.DateTimeUtil; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class Review { 8 | 9 | public Review() { 10 | this.created = DateTimeUtil.createCurrentDateAsString(); 11 | } 12 | 13 | private String comment; 14 | private Short stars; 15 | private String created; 16 | 17 | public static Review from(AddReviewRequest addReviewRequest) { 18 | Review review = new Review(); 19 | review.setComment(addReviewRequest.comment()); 20 | if (addReviewRequest.stars() != null) { 21 | review.setStars(addReviewRequest.stars().shortValue()); 22 | } 23 | return review; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /product-api/src/main/java/com/ivanfranchin/productapi/util/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi.util; 2 | 3 | import java.time.Instant; 4 | 5 | public class DateTimeUtil { 6 | 7 | private DateTimeUtil() { 8 | } 9 | 10 | public static String createCurrentDateAsString() { 11 | return Instant.ofEpochSecond(Instant.now().getEpochSecond()).toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /product-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=product-api 2 | spring.elasticsearch.uris=${ELASTICSEARCH_URIS:localhost:9200} 3 | springdoc.swagger-ui.disable-swagger-default-url=true 4 | -------------------------------------------------------------------------------- /product-api/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | _ __ _ __ ___ __| |_ _ ___| |_ __ _ _ __ (_) 3 | | '_ \| '__/ _ \ / _` | | | |/ __| __|____ / _` | '_ \| | 4 | | |_) | | | (_) | (_| | |_| | (__| ||_____| (_| | |_) | | 5 | | .__/|_| \___/ \__,_|\__,_|\___|\__| \__,_| .__/|_| 6 | |_| |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /product-api/src/test/java/com/ivanfranchin/productapi/ProductApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productapi; 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 ProductApiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /product-ui/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | springboot-elasticsearch-thymeleaf 8 | 1.0.0 9 | ../pom.xml 10 | 11 | product-ui 12 | product-ui 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-thymeleaf 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | true 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-compiler-plugin 54 | 55 | 56 | 57 | org.projectlombok 58 | lombok 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | 67 | 68 | 69 | org.projectlombok 70 | lombok 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/ProductUiApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ProductUiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ProductUiApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/ProductApiClient.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client; 2 | 3 | import com.ivanfranchin.productui.client.dto.MyPage; 4 | import com.ivanfranchin.productui.client.dto.Product; 5 | import com.ivanfranchin.productui.client.dto.ProductDto; 6 | import com.ivanfranchin.productui.client.dto.Review; 7 | import com.ivanfranchin.productui.client.dto.SearchDto; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.service.annotation.GetExchange; 12 | import org.springframework.web.service.annotation.HttpExchange; 13 | import org.springframework.web.service.annotation.PostExchange; 14 | import org.springframework.web.service.annotation.PutExchange; 15 | 16 | @HttpExchange("/api/products") 17 | public interface ProductApiClient { 18 | 19 | @GetExchange 20 | MyPage listProductsByPage(@RequestParam(required = false) Integer page, 21 | @RequestParam(required = false) Integer size, 22 | @RequestParam(required = false) String sort); 23 | 24 | @PutExchange("/search") 25 | MyPage searchProductsByPage(@RequestBody SearchDto searchDto, 26 | @RequestParam(required = false) Integer page, 27 | @RequestParam(required = false) Integer size, 28 | @RequestParam(required = false) String sort); 29 | 30 | @GetExchange("/{id}") 31 | Product getProduct(@PathVariable String id); 32 | 33 | @PostExchange 34 | Product createProduct(@RequestBody ProductDto productDto); 35 | 36 | @PutExchange("/{id}") 37 | Product updateProduct(@PathVariable String id, @RequestBody ProductDto productDto); 38 | 39 | @PostExchange("/{id}/reviews") 40 | Product addProductReview(@PathVariable String id, @RequestBody Review review); 41 | } 42 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/ProductApiClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.client.RestClient; 7 | import org.springframework.web.client.support.RestClientAdapter; 8 | import org.springframework.web.service.invoker.HttpServiceProxyFactory; 9 | 10 | @Configuration 11 | public class ProductApiClientConfig { 12 | 13 | @Value("${product-api.url}") 14 | private String productApiUrl; 15 | 16 | @Bean 17 | ProductApiClient productApiClient(RestClient.Builder builder) { 18 | RestClient restClient = builder.baseUrl(productApiUrl).build(); 19 | RestClientAdapter adapter = RestClientAdapter.create(restClient); 20 | HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); 21 | return factory.createClient(ProductApiClient.class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/dto/MyPage.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client.dto; 2 | 3 | import java.util.List; 4 | 5 | public record MyPage(List content, Integer totalElements, Integer totalPages, Integer size, 6 | Integer numberOfElements, Boolean first, Boolean last) { 7 | } 8 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/dto/Product.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client.dto; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Date; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | public record Product(String id, String reference, String name, String description, BigDecimal price, 9 | Set categories, List reviews, Date created) { 10 | } 11 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/dto/ProductDto.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Set; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class ProductDto { 14 | 15 | private String name; 16 | private String description; 17 | private BigDecimal price; 18 | private Set categories; 19 | } 20 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/dto/Review.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Date; 6 | 7 | @Data 8 | public class Review { 9 | 10 | private String comment; 11 | private Short stars; 12 | private Date created; 13 | } 14 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/client/dto/SearchDto.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.client.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class SearchDto { 7 | 8 | private String text; 9 | } 10 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.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, 22 | options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 23 | 24 | } 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.controller; 2 | 3 | import com.ivanfranchin.productui.client.ProductApiClient; 4 | import com.ivanfranchin.productui.client.dto.MyPage; 5 | import com.ivanfranchin.productui.client.dto.Product; 6 | import com.ivanfranchin.productui.client.dto.ProductDto; 7 | import com.ivanfranchin.productui.client.dto.Review; 8 | import com.ivanfranchin.productui.client.dto.SearchDto; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.ModelAttribute; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.PostMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | 18 | @RequiredArgsConstructor 19 | @Controller 20 | public class ProductController { 21 | 22 | private final ProductApiClient productApiClient; 23 | 24 | @GetMapping(value = {"/products", "/"}) 25 | public String getProducts(@RequestParam(required = false) Integer page, 26 | @RequestParam(required = false) Integer size, 27 | @RequestParam(required = false, defaultValue = "created,desc") String sort, 28 | Model model) { 29 | model.addAttribute("searchDto", new SearchDto()); 30 | model.addAttribute("products", productApiClient.listProductsByPage(page, size, sort)); 31 | return "products"; 32 | } 33 | 34 | @PostMapping("/products/search") 35 | public String searchProducts(@RequestParam(required = false) Integer page, 36 | @RequestParam(required = false) Integer size, 37 | @RequestParam(required = false, defaultValue = "created,desc") String sort, 38 | @ModelAttribute SearchDto searchDto, 39 | Model model) { 40 | MyPage result = searchDto.getText().trim().isEmpty() ? 41 | productApiClient.listProductsByPage(page, size, sort) : 42 | productApiClient.searchProductsByPage(searchDto, page, size, sort); 43 | model.addAttribute("products", result); 44 | return "products"; 45 | } 46 | 47 | @GetMapping("/products/{id}/edit") 48 | public String editProductForm(@PathVariable String id, Model model) { 49 | Product product = productApiClient.getProduct(id); 50 | ProductDto productDto = new ProductDto(product.name(), product.description(), product.price(), product.categories()); 51 | 52 | model.addAttribute("productDto", productDto); 53 | model.addAttribute("product", product); 54 | return "productEdit"; 55 | } 56 | 57 | @GetMapping("/products/{id}/view") 58 | public String viewProductForm(@PathVariable String id, Model model) { 59 | model.addAttribute("review", new Review()); 60 | model.addAttribute("product", productApiClient.getProduct(id)); 61 | return "productView"; 62 | } 63 | 64 | @GetMapping("/products/create") 65 | public String createProductForm(Model model) { 66 | model.addAttribute("productDto", new ProductDto()); 67 | return "productCreate"; 68 | } 69 | 70 | @PostMapping("/products") 71 | public String createProduct(@ModelAttribute ProductDto productDto) { 72 | productApiClient.createProduct(productDto); 73 | return "redirect:/"; 74 | } 75 | 76 | @PostMapping("/products/{id}") 77 | public String updateProduct(@PathVariable String id, @ModelAttribute ProductDto productDto) { 78 | productApiClient.updateProduct(id, productDto); 79 | return "redirect:/"; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /product-ui/src/main/java/com/ivanfranchin/productui/controller/ProductReviewController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui.controller; 2 | 3 | import com.ivanfranchin.productui.client.dto.Review; 4 | import com.ivanfranchin.productui.client.ProductApiClient; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.ModelAttribute; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | 11 | @RequiredArgsConstructor 12 | @Controller 13 | public class ProductReviewController { 14 | 15 | private final ProductApiClient productApiClient; 16 | 17 | @PostMapping("/products/{id}/review") 18 | public String addReview(@PathVariable String id, @ModelAttribute Review review) { 19 | productApiClient.addProductReview(id, review); 20 | return String.format("redirect:/products/%s/view", id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=product-ui 2 | product-api.url=http://localhost:8080 3 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | _ __ _ __ ___ __| |_ _ ___| |_ _ _(_) 3 | | '_ \| '__/ _ \ / _` | | | |/ __| __|____| | | | | 4 | | |_) | | | (_) | (_| | |_| | (__| ||_____| |_| | | 5 | | .__/|_| \___/ \__,_|\__,_|\___|\__| \__,_|_| 6 | |_| 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-UI 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
Something went wrong!
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Date
Status
Error
Message
43 |
44 |
45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/fragments/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/fragments/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/productCreate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-UI 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
Details
23 |
24 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | 40 | 41 |
42 |
43 |
44 |
45 | 47 | 48 |
49 |
50 |
51 |
52 | Cancel 54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 |
64 | 65 | 66 | 67 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/productEdit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-UI 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
Details
23 |
24 |
26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 |
42 | 44 | 45 |
46 |
47 | 49 | 50 |
51 |
52 |
53 |
54 | 56 | 57 |
58 |
59 |
60 |
61 | Cancel 63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/productView.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-UI 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
Details
23 |
24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 |
51 | 53 | 54 |
55 |
56 |
57 |
58 | 59 |
60 |
Reviews
61 |
62 |
64 |
65 |
66 | comment 67 | 68 | 69 |
70 |
71 | star 72 | 74 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 |
CommentStarsCreated
No Reviews
CommentStarsCreated 101 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | 112 |
113 | 114 | 115 | 116 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /product-ui/src/main/resources/templates/products.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product-UI 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 73 | 74 | 75 |
NameDescriptionPrice (€)Actions
No Products Available
Name Description Price 56 | 57 | menu 58 | 59 | 72 |
76 |
77 |
78 | 79 |
80 | 81 | add 82 | 83 |
84 |
85 |
86 | 87 |
88 | 89 | 90 | 91 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /product-ui/src/test/java/com/ivanfranchin/productui/ProductUiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.productui; 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 ProductUiApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /reindex.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-------------------" 4 | echo "Applying reindexing" 5 | echo "-------------------" 6 | 7 | read -p "Type the index alias (ecommerce.products): " index_alias 8 | index_alias=${index_alias:-ecommerce.products} 9 | 10 | read -p "Type the old index name (ecommerce.products.v1): " old_index_name 11 | old_index_name=${old_index_name:-ecommerce.products.v1} 12 | 13 | read -p "Type new index name (ecommerce.products.v2): " new_index_name 14 | new_index_name=${new_index_name:-ecommerce.products.v2} 15 | 16 | read -p "Inform the path to the new mapping json file (elasticsearch/mapping-v2.json): " new_mapping_file 17 | new_mapping_file=${new_mapping_file:-elasticsearch/mapping-v2.json} 18 | 19 | echo "----------------" 20 | echo "Create new index" 21 | echo "----------------" 22 | curl -X PUT localhost:9200/${new_index_name} -H "Content-Type: application/json" -d @${new_mapping_file} 23 | 24 | echo 25 | echo "--------" 26 | echo "Re-index" 27 | echo "--------" 28 | curl -X POST localhost:9200/_reindex -H 'Content-Type: application/json' \ 29 | -d '{ "source": { "index": "'${old_index_name}'" }, "dest": { "index": "'${new_index_name}'" }}' 30 | 31 | echo 32 | echo "------------" 33 | echo "Update alias" 34 | echo "------------" 35 | curl -X POST localhost:9200/_aliases -H 'Content-Type: application/json' \ 36 | -d '{ "actions": [{ "remove": {"alias": "'${index_alias}'", "index": "'${old_index_name}'" }}, { "add": {"alias": "'${index_alias}'", "index": "'${new_index_name}'" }}]}' 37 | echo 38 | 39 | echo "----------------" 40 | echo "Delete old index" 41 | echo "----------------" 42 | curl -X DELETE localhost:9200/${old_index_name} -------------------------------------------------------------------------------- /remove-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker rmi ivanfranchin/product-api:1.0.0 4 | docker rmi ivanfranchin/product-ui:1.0.0 5 | -------------------------------------------------------------------------------- /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 product-api..." 7 | 8 | docker run -d --rm --name product-api \ 9 | -p 8080:8080 \ 10 | -e ELASTICSEARCH_URIS=elasticsearch:9200 \ 11 | --network springboot-elasticsearch-thymeleaf_default \ 12 | ivanfranchin/product-api:1.0.0 13 | 14 | wait_for_container_log "product-api" "Started" 15 | 16 | echo 17 | echo "Starting product-ui..." 18 | 19 | docker run -d --rm --name product-ui \ 20 | -p 9080:8080 \ 21 | -e PRODUCT_API_URL=http://product-api:8080 \ 22 | --network springboot-elasticsearch-thymeleaf_default \ 23 | ivanfranchin/product-ui:1.0.0 24 | 25 | wait_for_container_log "product-ui" "Started" 26 | 27 | printf "\n" 28 | printf "%12s | %37s |\n" "Application" "URL" 29 | printf "%12s + %37s |\n" "------------" "-------------------------------------" 30 | printf "%12s | %37s |\n" "product-api" "http://localhost:8080/swagger-ui.html" 31 | printf "%12s | %37s |\n" "product-ui" "http://localhost:9080" 32 | printf "\n" -------------------------------------------------------------------------------- /stop-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker stop product-api product-ui --------------------------------------------------------------------------------