├── .devfile.yaml ├── .dockerignore ├── .gitignore ├── LICENSE ├── README.md ├── decks ├── README.md ├── deck-assets │ ├── npm.png │ ├── quarkus.png │ └── quarkus_logo_vertical_1280px_reverse.png ├── web-dev.md └── web-dev.sh ├── justfile ├── pom.xml └── src ├── main ├── docker │ ├── Dockerfile.jvm │ └── Dockerfile.native ├── java │ └── io │ │ └── quarkus │ │ └── sample │ │ ├── Todo.java │ │ ├── TodoGraphQLEndpoint.java │ │ ├── TodoResource.java │ │ ├── ai │ │ ├── TodoAiService.java │ │ ├── TodoDatabaseContentRetriever.java │ │ └── TodoRetrievalAugmentor.java │ │ └── audit │ │ ├── AuditLogEncoder.java │ │ ├── AuditLogSocket.java │ │ └── AuditType.java └── resources │ ├── application.properties │ ├── import.sql │ └── web │ ├── app │ ├── todos-app.js │ ├── todos-audit-log.js │ ├── todos-cards.js │ ├── todos-footer.js │ ├── todos-header.js │ ├── todos-task.js │ └── todos.css │ ├── audit.html │ ├── index.html │ └── static │ ├── favicon.ico │ ├── quarkus_icon_dark.png │ └── quarkus_icon_light.png └── test └── java └── io └── quarkus └── sample ├── NativeTodoResourceIT.java └── TodoResourceTest.java /.devfile.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: 2.2.0 2 | metadata: 3 | name: quarkus-todo 4 | components: 5 | - name: tooling-container 6 | container: 7 | env: 8 | - name: KUBEDOCK_ENABLED 9 | value: 'true' 10 | - name: DOCKER_HOST 11 | value: 'tcp://127.0.0.1:2475' 12 | - name: TESTCONTAINERS_RYUK_DISABLED 13 | value: 'true' 14 | - name: QUARKUS_DATASOURCE_DEVSERVICES_VOLUMES_ # <-- https://quarkus.io/guides/databases-dev-services#quarkus-datasource-config-group-dev-services-build-time-config_quarkus.datasource.devservices.volumes-volumes 15 | value: '/var/lib/postgresql/' 16 | image: quay.io/devfile/universal-developer-image:latest 17 | memoryRequest: 2Gi 18 | memoryLimit: 8Gi 19 | cpuRequest: 1000m 20 | cpuLimit: 4000m 21 | endpoints: 22 | - exposure: none 23 | name: kubedock 24 | protocol: tcp 25 | targetPort: 2475 26 | commands: 27 | - id: run 28 | exec: 29 | label: '1. Quarkus Dev' 30 | component: tooling-container 31 | commandLine: mvn quarkus:dev 32 | - id: podman-run 33 | exec: 34 | label: '2. Podman Run Sample' 35 | component: tooling-container 36 | commandLine: podman run --name httpd -d -p 8080:8080 python python -m http.server 8080 37 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !target/*-runner 3 | !target/*-runner.jar 4 | !target/lib/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | 14 | ObjectStore 15 | 16 | /nbactions.xml 17 | 18 | # Eclipse 19 | .project 20 | .classpath 21 | .settings/ 22 | 23 | # IntelliJ 24 | *.iml 25 | *.ipr 26 | *.iws 27 | .idea 28 | 29 | # VS Code 30 | .vscode 31 | /transcript.txt 32 | /openai.sh 33 | /podman.sh 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TODO Application with Quarkus 2 | 3 | This is an example application based on a Todo list where the different tasks are created, read, updated, or deleted from the database. This application uses `postgresql` as a database and that is provided with Quarkus Dev Services. When running in a 4 | non-dev mode you will have to provide the database yourself. 5 | 6 | ## Development mode 7 | 8 | ```bash 9 | mvn compile quarkus:dev 10 | ``` 11 | Then, open: http://localhost:8080/ 12 | 13 | ## Compile and run on a JVM with PostgresSQL ( in a container ) 14 | 15 | ```bash 16 | mvn package 17 | ``` 18 | Run: 19 | ```bash 20 | docker run --ulimit memlock=-1:-1 -it --rm=true \ 21 | --name postgres-quarkus-rest-http-crud \ 22 | -e POSTGRES_USER=restcrud \ 23 | -e POSTGRES_PASSWORD=restcrud \ 24 | -e POSTGRES_DB=rest-crud \ 25 | -p 5432:5432 postgres:14 26 | java -jar target/quarkus-app/quarkus-run.jar 27 | ``` 28 | 29 | Then, open: http://localhost:8080/ 30 | 31 | ## Compile to Native and run with PostgresSQL ( in a container ) 32 | 33 | Compile: 34 | ```bash 35 | mvn clean package -Pnative 36 | ``` 37 | Run: 38 | ```bash 39 | docker run --ulimit memlock=-1:-1 -it --rm=true \ 40 | --name postgres-quarkus-rest-http-crud \ 41 | -e POSTGRES_USER=restcrud \ 42 | -e POSTGRES_PASSWORD=restcrud \ 43 | -e POSTGRES_DB=rest-crud \ 44 | -p 5432:5432 postgres:14 45 | ./target/todo-backend-1.0-SNAPSHOT-runner 46 | ``` 47 | ## Other links 48 | 49 | - http://localhost:8080/q/health (Show the build in Health check for the datasource) 50 | - http://localhost:8080/q/openapi (The OpenAPI Schema document in yaml format) 51 | - http://localhost:8080/q/swagger-ui (The Swagger UI to test out the REST Endpoints) 52 | - http://localhost:8080/graphql/schema.graphql (The GraphQL Schema document) 53 | - http://localhost:8080/q/graphql-ui/ (The GraphiQL UI to test out the GraphQL Endpoint) 54 | - http://localhost:8080/q/dev-ui/ (Show dev ui) 55 | -------------------------------------------------------------------------------- /decks/README.md: -------------------------------------------------------------------------------- 1 | Deck are made for Quarkus Reveal, to install `quarkus-reveal`: 2 | https://github.com/ia3andy/quarkus-reveal 3 | -------------------------------------------------------------------------------- /decks/deck-assets/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/decks/deck-assets/npm.png -------------------------------------------------------------------------------- /decks/deck-assets/quarkus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/decks/deck-assets/quarkus.png -------------------------------------------------------------------------------- /decks/deck-assets/quarkus_logo_vertical_1280px_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/decks/deck-assets/quarkus_logo_vertical_1280px_reverse.png -------------------------------------------------------------------------------- /decks/web-dev.md: -------------------------------------------------------------------------------- 1 | ![Quarkus](deck-assets/quarkus_logo_vertical_1280px_reverse.png) 2 | 3 | ## Web dev with Quarkus 4 | 5 | --- 6 | 7 | # 8 | 9 | Phillip Kruger `@phillipkruger` 10 | - Principal Software Engineer at Red Hat 11 | - Quarkus, Smallrye, Microprofile 12 | 13 | --- 14 | 15 | ### What is Quarkus? 16 | 17 | - Open Source framework for… 18 | - Cloud-Native, Serverless, Micro-Services, Lambdas, Command-Lines… 19 | - And Web Applications! 20 | 21 | --- 22 | 23 | ### Why Quarkus? 24 | 25 | - Developer Joy 26 | - Container First 27 | - Community and Standards 28 | - Imperative and Reactive 29 | 30 | --- 31 | 32 | ### What is an extension? 33 | 34 | - Adds `functionality` to a Quarkus application 35 | - Not just a `plain` dependency 36 | - Consists of two parts: 37 | - `build-time` module (hidden) 38 | - `runtime` module 39 | 40 | -- 41 | 42 | ### Where can I find extensions? 43 | 44 |
45 | 46 | ### `extensions.quarkus.io` 47 | 48 | -- 49 | 50 | ### Can I contribute a new extension? 51 | 52 | Yes, meet the [Quarkiverse Hub](https://github.com/quarkiverse)! 53 | 54 | A GitHub organization that provides repository hosting (including build, CI and release publishing setup) for Quarkus extensions contributed by the community. 55 | 56 | --- 57 | 58 | ### Nothing 59 | 60 | - /src/main/resources/META-INF/resources 61 | 62 | --- 63 | 64 | ## Web Dependency Locator 65 | 66 | Bundle your: 67 | 68 | - web dependencies in `pom.xml` (Lit, Htmx, Bootstrap, ...) 69 | - scripts (js) 70 | - and styles (css) 71 | - importmap support 72 | 73 | ### => ZERO CONFIGURATION 74 | 75 | --- 76 | 77 | ## Web Bundler 78 | 79 | Bundle your: 80 | 81 | - web dependencies in `pom.xml` (Lit, Htmx, Bootstrap, React, ...) 82 | - scripts (js, ts, jsx, tsx) 83 | - and styles (css, scss, sass). 84 | 85 | ### => ZERO CONFIGURATION 86 | 87 | --- 88 | 89 | ## Quinoa 90 |
91 |
92 | 93 |
94 |
95 | 96 | - with `package.json` 97 | - integrated proxy for `dev` 98 | - framework detection (React, Angular, Vue, ...) 99 | 100 | --- 101 | 102 | ## Qute 103 | 104 | Qute is a _templating engine_ designed to meet Quarkus needs. 105 | 106 | -- 107 | 108 | ### Qute - goals 109 | 110 | - Simple but powerful syntax 111 | - Easily extensible API 112 | - Type-safe templates to enable build-time validation (optional) 113 | - Minimize reflection usage 114 | - Support async data types (`Uni`, `CompletionStage`) out-of-the-box 115 | - ... a first-class Quarkus citizen! 116 | 117 | -- 118 | 119 | ### Qute - simple syntax 120 | 121 | The dynamic parts of a template include _comments_, _output expressions_, _sections_ and _unparsed character data_. 122 | 123 | ```html 124 | {! A simple comment !} 125 |

{item.name ?: 'Dummy'}

126 | {#if item.active} 127 |

{item.description}

128 | {/if} 129 | {| |} 130 | ``` 131 | 132 | -- 133 | 134 | ### Qute - extensible API example 135 | 136 | Template extension methods can be used to extend the data classes with new functionality. 137 | 138 | For example, it is possible to add _computed properties_ and _virtual methods_ to existing Java types. 139 | 140 | ```java 141 | @TemplateExtension 142 | public static String toMonthStr(LocalDate date) { 143 | return date.getMonth().getDisplayName(TextStyle.SHORT, Locale.getDefault()); 144 | } 145 | ``` 146 | 147 | -- 148 | 149 | ### Qute - type-safe templates 150 | 151 | The goal is to catch user errors during the build and fail fast. 152 | 153 | -- 154 | 155 | ### Qute - type-safe templates 156 | 157 | #1 - directly in the template. 158 | 159 | ```html 160 | {@java.lang.String name} 161 | 162 | 163 | Hello {name.toLowerCase}! 164 | 165 | 166 | ``` 167 | 168 | -- 169 | 170 | ### Qute - type-safe templates 171 | 172 | #2 - directly in the code. 173 | 174 | ```java 175 | @CheckedTemplate 176 | class Templates { 177 | static native TemplateInstance hello(String name); 178 | } 179 | ``` 180 | 181 | -- 182 | 183 | ### Qute - type-safe templates 184 | 185 | #3 - records (Java 14+) 186 | 187 | ```java 188 | record Hello(String name) implements TemplateInstance {} 189 | ``` 190 | 191 | -- 192 | 193 | ### Qute - type-safe templates 194 | 195 | #4 - `@Named` CDI beans 196 | 197 | ```html 198 | 199 | 200 | Hello {cdi:bean.name.toLowerCase}! 201 | 202 | 203 | ``` 204 | 205 | -- 206 | 207 | ### Qute - async data resolution API 208 | 209 | Allows for better resource utilization and fits the Quarkus reactive model. 210 | 211 | For example, it’s possible to use _non-blocking clients_ directly from a template. 212 | 213 | -- 214 | 215 | ### Qute - tight Quarkus integration 216 | 217 | - Dev mode and UI 218 | - CDI integration (`{cdi:myBean.foo}`, `@Inject Template`, ...) 219 | - `quarkus-rest-qute` 220 | - `quarkus-mailer` 221 | - ... 222 | 223 | --- 224 | 225 | ## Qute Web 226 | 227 | It's a Quarkiverse extension. The goal is to expose the Qute templates located in the `src/main/resource/templates/pub` directory via HTTP. Automatically, no controllers needed. 228 | 229 | -- 230 | 231 | ### Qute Web - accesible data 232 | 233 | - `@Named` CDI beans; `{cdi:myBean.name}` 234 | - static members of a class annotated with `@TemplateData` 235 | - enums annotated with `@TemplateEnum` 236 | - the current HTTP request and query parameters; `{http:request.path}` and `{http:param('name')}` 237 | - global variables 238 | - ... 239 | 240 | --- 241 | 242 | ## Quarkus Freemarker 243 | 244 | Freemarker is very popular and mature templating engine. 245 | 246 | --- 247 | 248 | ## Renarde 249 | 250 | - An old-school Web Framework 251 | - Server-side rendering for views (Qute) 252 | - Model with Hibernate with Panache 253 | - Controllers with RESTEasy Reactive and magic 254 | 255 | -- 256 | 257 | ### Your first controller 258 | 259 | ```java 260 | public class Application extends Controller { 261 | 262 | @CheckedTemplate 263 | public static class Templates { 264 | public static native TemplateInstance index(String message); 265 | } 266 | 267 | @Path("/") 268 | public TemplateInstance index(){ 269 | return Templates.index("Hello Slovenia"); 270 | } 271 | } 272 | ``` 273 | 274 | -- 275 | 276 | ### Your first view 277 | 278 | ```html 279 | {#include main.html} 280 | {#title}Index page{/title} 281 | 282 |

We have a message for you: {message}

283 | ``` 284 | 285 | -- 286 | 287 | ### Your first entity 288 | 289 | ```java 290 | @Entity 291 | public class Todo extends PanacheEntity { 292 | public String task; 293 | public LocalDate done; 294 | 295 | public static List listTodos(){ 296 | return listAll(Sort.by("id")); 297 | } 298 | 299 | public static List listDone(){ 300 | return list("done is not null", Sort.by("done")); 301 | } 302 | } 303 | ``` 304 | 305 | -- 306 | 307 | ### Using the model, controller side 308 | 309 | ```java 310 | public class Todos extends Controller { 311 | 312 | @CheckedTemplate 313 | public static class Templates { 314 | public static native TemplateInstance index(List todos); 315 | } 316 | 317 | public TemplateInstance index(){ 318 | return Templates.index(Todo.listTodos()); 319 | } 320 | } 321 | ``` 322 | 323 | -- 324 | 325 | ### Using the model, view side 326 | 327 | ```html 328 | {#include main.html} 329 | {#title}List of tasks{/title} 330 | 331 |
    332 | {#for todo in todos} 333 |
  • {todo.task} done: {todo.done}
  • 334 | {/for} 335 |
336 | ``` 337 | 338 | -- 339 | 340 | ### Need an controller action? 1/2 341 | 342 | ```java 343 | public class Todos extends Controller { 344 | 345 | @CheckedTemplate 346 | public static class Templates { 347 | public static native TemplateInstance edit(Todo todo); 348 | } 349 | 350 | public TemplateInstance edit(@RestPath String id){ 351 | Todo todo = Todo.findById(id); 352 | notFoundIfNull(id); 353 | return Templates.edit(todo); 354 | } 355 | 356 | … 357 | ``` 358 | -- 359 | 360 | ### Need an controller action? 2/2 361 | 362 | ```java 363 | … 364 | 365 | @POST 366 | public void save(@RestPath String id, 367 | @RestForm task, 368 | @RestForm LocalDate done){ 369 | Todo todo = Todo.findById(id); 370 | notFoundIfNull(id); 371 | todo.task = task; 372 | todo.done = done; 373 | index(); 374 | } 375 | } 376 | ``` 377 | -- 378 | 379 | ### The view side 380 | 381 | ```html 382 | {#include main.html} 383 | {#title}Edit task{/title} 384 | 385 |
386 | {#authenticityToken/} 387 | 388 | 389 | 390 |
391 | ``` 392 | 393 | -- 394 | 395 | ### Validation, in your controller 396 | 397 | ```java 398 | @POST 399 | public void save(@RestPath String id, 400 | @RestForm @NotBlank task, 401 | @RestForm LocalDate done){ 402 | Todo todo = Todo.findById(id); 403 | notFoundIfNull(id); 404 | if(validationFailed()) { 405 | edit(id); 406 | } 407 | todo.task = task; 408 | todo.done = done; 409 | index(); 410 | } 411 | ``` 412 | 413 | -- 414 | 415 | ### Validation, in your view 416 | 417 | ```html 418 | {#ifError 'task'}Error: {#error 'task'/}{/ifError} 419 | 420 | ``` 421 | 422 | -- 423 | 424 | ### Localisation: configuration 425 | 426 | Configure it in your `application.properties`: 427 | 428 | ```properties 429 | # This is the default locale for your application 430 | quarkus.default-locale=en 431 | # These are the supported locales (should include the default locale, 432 | # but order is not important) 433 | quarkus.locales=en,fr 434 | ``` 435 | -- 436 | 437 | ### Localisation: default messages 438 | 439 | Set your messages in `messages.properties`: 440 | 441 | ```properties 442 | # A simple message 443 | hello=Hello World 444 | # A parameterised message for your view 445 | views_Application_index_greet=Hello %s 446 | ``` 447 | 448 | -- 449 | 450 | ### Localisation: localised messages 451 | 452 | Set your messages in `messages_fr.properties`: 453 | 454 | ```properties 455 | hello=Bonjour Monde 456 | views_Application_index_greet=Salut %s 457 | ``` 458 | 459 | -- 460 | 461 | ### Localisation: use it from your controller 462 | 463 | ```java 464 | public String hello() { 465 | return i18n.formatMessage("hello"); 466 | } 467 | ``` 468 | -- 469 | 470 | ### Localisation: use it from your view 471 | 472 | ```html 473 | With no parameter: 474 | {m:hello} 475 | 476 | With parameters: 477 | {m:views_Application_index_greet(name)} 478 | ``` 479 | 480 | -- 481 | 482 | ### Emails: declaring them 483 | 484 | ```java 485 | public class Emails { 486 | 487 | @CheckedTemplate 488 | static class Templates { 489 | public static native MailTemplateInstance notify(User user); 490 | } 491 | 492 | public static void notify(Todo todo) { 493 | Templates.notify(todo) 494 | .subject("[Todos] We wanted to let you know") 495 | .to(todo.owner.email) 496 | .from("Todos ") 497 | .send().await().indefinitely(); 498 | } 499 | } 500 | ``` 501 | -- 502 | ### Emails: the controller 503 | 504 | ```java 505 | @POST 506 | public void saveTodo(…){ 507 | … 508 | Emails.notify(todo); 509 | index(); 510 | } 511 | ``` 512 | -- 513 | 514 | ### Emails: the view 515 | 516 | ```html 517 | {#include email.html} 518 | 519 |

520 | We got a notification about {todo.task} 521 |

522 | 523 |

524 | View it online. 525 |

526 | ``` 527 | 528 | \* Also supports the plain text variant 529 | 530 | -- 531 | 532 | ### Other features 533 | 534 | - Generating PDFs from views 535 | - Generating barcodes 536 | - A generated backoffice for your entities 537 | - A Database transporter 538 | - Helper methods for password or webauthn authentication, OIDC 539 | 540 | -- 541 | 542 | ## HTMX 543 | 544 | All this is fine, and old-school, but if what if you want to do partial page updates, get some 545 | of this AJAX action going? 546 | 547 | -- 548 | 549 | HTMX allows you to turn your pages into AJAX pages without writing JavaScript, by declaring AJAX 550 | actions and consequences as custom HTML attributes. 551 | 552 | -- 553 | 554 | ```html 555 | Click me 556 | ``` 557 | 558 | This will do an AJAX `GET` of that 559 | controller and replace the contents with what it returns. 560 | 561 | 562 | You can use other HTMX attributes to define what to do with the results. 563 | 564 | -- 565 | 566 | ## HTMX fragments 567 | 568 | You can declare fragments of your template: 569 | 570 | ```html 571 | {#fragment id="entries"} 572 |
    573 | {#for entry in entries} 574 |
  • {entry.published}: {entry.title}
  • 575 | {/for} 576 |
577 | {/fragment} 578 | ``` 579 | 580 | -- 581 | 582 | ```java 583 | public class Cms extends HxController { 584 | @CheckedTemplate 585 | public static class Templates { 586 | static native TemplateInstance index(List entries); 587 | 588 | static native TemplateInstance 589 | index$entries(List entries); 590 | } 591 | public TemplateInstance index() { 592 | if (isHxRequest()) 593 | return Templates.index$entries(BlogEntry.listAll()); 594 | return Templates.index(BlogEntry.listAll()); 595 | } 596 | } 597 | ``` 598 | 599 | 600 | 601 | ## Turning HTML into HTMX 602 | 603 | - Add `hx-get`, `hx-post` and other attributes to your views 604 | - Add `#fragment` to your views 605 | - Make your controller extend `HxController` 606 | - Declare the fragments in your controller 607 | - Define partial-rendering outcomes from your endpoints 608 | - Profit! 609 | 610 | -- 611 | 612 | ## Conclusion 613 | 614 | 1. Use Quarkus 615 | 2. Add Web 616 | 3. ?!?! 617 | 4. Profit 618 | 619 | --- 620 | 621 | ## Vaadin Flow 622 | 623 | Vaadin Flow is a unique framework that lets you build web apps without writing HTML or JavaScript 624 | 625 | --- 626 | 627 | ## JSF 628 | 629 | Developed through the Java Community Process under JSR - 314, JSF technology establishes the standard for building server-side user interfaces 630 | 631 | - PrimeFaces 632 | - Apache MyFaces 633 | 634 | --- 635 | 636 | ## Quarkus Playwright 637 | 638 | Playwright is an open-source automation library designed for browser testing 639 | 640 | --- 641 | 642 | Any questions? 643 | -------------------------------------------------------------------------------- /decks/web-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | quarkus-reveal web-dev.md -t quarkus 3 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # Handy sets of commands to run side operations usually described in the doc otherwise 2 | # expects podman and quarkus CLI 3 | 4 | postgres := "postgres:15-bullseye" 5 | 6 | # Start the database using podman 7 | start-infra: 8 | podman run --ulimit memlock=-1:-1 -it --rm=true \ 9 | --name postgres-quarkus-rest-http-crud \ 10 | -e POSTGRES_USER=restcrud \ 11 | -e POSTGRES_PASSWORD=restcrud \ 12 | -e POSTGRES_DB=rest-crud \ 13 | -p 5432:5432 {{postgres}} 14 | 15 | # Stop the database using podman 16 | stop-infra: 17 | podman stop $(podman ps -q --filter ancestor={{postgres}}) 18 | 19 | #Using quarkus CLI, build in native 20 | native: 21 | quarkus build --no-tests --native 22 | 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | io.quarkus.sample 5 | todo-backend 6 | 1.0-SNAPSHOT 7 | TODOS Application 8 | 9 | 3.11.0 10 | 17 11 | 17 12 | 17 13 | UTF-8 14 | UTF-8 15 | quarkus-bom 16 | io.quarkus 17 | 3.28.1 18 | 1.2.0 19 | true 20 | 3.1.2 21 | 2.22.2 22 | 23 | 24 | 25 | 26 | 27 | ${quarkus.platform.group-id} 28 | ${quarkus.platform.artifact-id} 29 | ${quarkus.platform.version} 30 | pom 31 | import 32 | 33 | 34 | 35 | 36 | 37 | io.quarkus 38 | quarkus-rest 39 | 40 | 41 | io.quarkus 42 | quarkus-rest-jsonb 43 | 44 | 45 | io.quarkus 46 | quarkus-smallrye-openapi 47 | 48 | 49 | io.quarkus 50 | quarkus-smallrye-health 51 | 52 | 53 | io.quarkus 54 | quarkus-smallrye-metrics 55 | 56 | 57 | io.quarkus 58 | quarkus-smallrye-fault-tolerance 59 | 60 | 61 | io.quarkus 62 | quarkus-smallrye-graphql 63 | 64 | 65 | io.quarkus 66 | quarkus-hibernate-validator 67 | 68 | 69 | io.quarkus 70 | quarkus-jdbc-postgresql 71 | 72 | 73 | io.quarkus 74 | quarkus-hibernate-orm-panache 75 | 76 | 77 | io.quarkus 78 | quarkus-info 79 | 80 | 81 | io.quarkus 82 | quarkus-websockets 83 | 84 | 85 | io.quarkiverse.langchain4j 86 | quarkus-langchain4j-openai 87 | ${quarkus-langchain4j.version} 88 | 89 | 90 | 91 | 92 | 98 | 99 | io.quarkus 100 | quarkus-web-dependency-locator 101 | 102 | 107 | 108 | org.mvnpm.at.mvnpm 109 | vaadin-webcomponents 110 | 111 | runtime 112 | 113 | 114 | 115 | 116 | io.quarkus 117 | quarkus-junit5 118 | test 119 | 120 | 121 | io.rest-assured 122 | rest-assured 123 | test 124 | 125 | 126 | 127 | 128 | 129 | ${quarkus.platform.group-id} 130 | quarkus-maven-plugin 131 | ${quarkus.platform.version} 132 | true 133 | 134 | 135 | 136 | build 137 | generate-code 138 | generate-code-tests 139 | 140 | 141 | 142 | 143 | 144 | maven-compiler-plugin 145 | ${compiler-plugin.version} 146 | 147 | 148 | -parameters 149 | 150 | 151 | 152 | 153 | maven-surefire-plugin 154 | ${surefire-plugin.version} 155 | 156 | 157 | org.jboss.logmanager.LogManager 158 | ${maven.home} 159 | 160 | 161 | 162 | 163 | maven-failsafe-plugin 164 | ${surefire-plugin.version} 165 | 166 | 167 | 168 | integration-test 169 | verify 170 | 171 | 172 | 173 | ${project.build.directory}/${project.build.finalName}-runner 174 | org.jboss.logmanager.LogManager 175 | ${maven.home} 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | native 186 | 187 | 188 | native 189 | 190 | 191 | 192 | false 193 | true 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.jvm: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/my-artifactId-jvm . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/my-artifactId-jvm 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 18 | # 19 | # Then run the container using : 20 | # 21 | # docker run -i --rm -p 8080:8080 quarkus/my-artifactId-jvm 22 | # 23 | # This image uses the `run-java.sh` script to run the application. 24 | # This scripts computes the command line to execute your Java application, and 25 | # includes memory/GC tuning. 26 | # You can configure the behavior using the following environment properties: 27 | # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") 28 | # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options 29 | # in JAVA_OPTS (example: "-Dsome.property=foo") 30 | # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is 31 | # used to calculate a default maximal heap memory based on a containers restriction. 32 | # If used in a container without any memory constraints for the container then this 33 | # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio 34 | # of the container available memory as set here. The default is `50` which means 50% 35 | # of the available memory is used as an upper boundary. You can skip this mechanism by 36 | # setting this value to `0` in which case no `-Xmx` option is added. 37 | # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This 38 | # is used to calculate a default initial heap memory based on the maximum heap memory. 39 | # If used in a container without any memory constraints for the container then this 40 | # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio 41 | # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` 42 | # is used as the initial heap size. You can skip this mechanism by setting this value 43 | # to `0` in which case no `-Xms` option is added (example: "25") 44 | # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. 45 | # This is used to calculate the maximum value of the initial heap memory. If used in 46 | # a container without any memory constraints for the container then this option has 47 | # no effect. If there is a memory constraint then `-Xms` is limited to the value set 48 | # here. The default is 4096MB which means the calculated value of `-Xms` never will 49 | # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") 50 | # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output 51 | # when things are happening. This option, if set to true, will set 52 | # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). 53 | # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: 54 | # true"). 55 | # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). 56 | # - CONTAINER_CORE_LIMIT: A calculated core limit as described in 57 | # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") 58 | # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). 59 | # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. 60 | # (example: "20") 61 | # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. 62 | # (example: "40") 63 | # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. 64 | # (example: "4") 65 | # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus 66 | # previous GC times. (example: "90") 67 | # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") 68 | # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") 69 | # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should 70 | # contain the necessary JRE command-line options to specify the required GC, which 71 | # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). 72 | # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") 73 | # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") 74 | # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be 75 | # accessed directly. (example: "foo.example.com,bar.example.com") 76 | # 77 | ### 78 | FROM registry.access.redhat.com/ubi8/openjdk-17:1.11 79 | 80 | ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' 81 | 82 | 83 | # We make four distinct layers so if there are application changes the library layers can be re-used 84 | COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ 85 | COPY --chown=185 target/quarkus-app/*.jar /deployments/ 86 | COPY --chown=185 target/quarkus-app/app/ /deployments/app/ 87 | COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ 88 | 89 | EXPOSE 8080 90 | USER 185 91 | ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 92 | ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" 93 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.native: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Pnative 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.native -t quarkus/my-artifactId . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 quarkus/my-artifactId 15 | # 16 | ### 17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5 18 | WORKDIR /work/ 19 | RUN chown 1001 /work \ 20 | && chmod "g+rwX" /work \ 21 | && chown 1001:root /work 22 | COPY --chown=1001:root target/*-runner /work/application 23 | 24 | EXPOSE 8080 25 | USER 1001 26 | 27 | CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/Todo.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample; 2 | 3 | import io.quarkus.hibernate.orm.panache.PanacheEntity; 4 | 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.validation.constraints.NotBlank; 8 | import java.util.List; 9 | 10 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 11 | 12 | @Entity 13 | public class Todo extends PanacheEntity { 14 | 15 | @NotBlank 16 | @Column(unique = true) 17 | public String title; 18 | 19 | public boolean completed; 20 | 21 | @Column(name = "ordering") 22 | public int order; 23 | 24 | @Schema(examples = "https://github.com/quarkusio/todo-demo-app") 25 | public String url; 26 | 27 | public static List findNotCompleted() { 28 | return list("completed", false); 29 | } 30 | 31 | public static List findCompleted() { 32 | return list("completed", true); 33 | } 34 | 35 | public static long deleteCompleted() { 36 | return delete("completed", true); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/TodoGraphQLEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample; 2 | 3 | import io.quarkus.panache.common.Sort; 4 | 5 | import jakarta.validation.Valid; 6 | import java.util.List; 7 | import org.eclipse.microprofile.graphql.Description; 8 | import org.eclipse.microprofile.graphql.GraphQLApi; 9 | import org.eclipse.microprofile.graphql.Mutation; 10 | import org.eclipse.microprofile.graphql.Query; 11 | 12 | @GraphQLApi 13 | public class TodoGraphQLEndpoint { 14 | 15 | @Query 16 | @Description("Get all the todos") 17 | public List getAllTodos() { 18 | return Todo.listAll(Sort.by("order")); 19 | } 20 | 21 | @Query 22 | @Description("Get a specific todo by id") 23 | public Todo getTodo(Long id) { 24 | return Todo.findById(id); 25 | } 26 | 27 | @Mutation 28 | @Description("Create a new todo") 29 | public Todo create(@Valid Todo item) { 30 | item.persist(); 31 | return item; 32 | } 33 | 34 | @Mutation 35 | @Description("Update an exiting todo") 36 | public Todo update(@Valid Todo todo, Long id) { 37 | Todo entity = Todo.findById(id); 38 | entity.id = id; 39 | entity.completed = todo.completed; 40 | entity.order = todo.order; 41 | entity.title = todo.title; 42 | entity.url = todo.url; 43 | return entity; 44 | } 45 | 46 | @Mutation 47 | @Description("Remove all completed todos") 48 | public List deleteCompleted() { 49 | List completed = Todo.findCompleted(); 50 | Todo.deleteCompleted(); 51 | return completed; 52 | } 53 | 54 | @Mutation 55 | @Description("Delete a specific todo") 56 | public Todo deleteTodo(Long id) { 57 | Todo entity = Todo.findById(id); 58 | if (entity == null) { 59 | return null; 60 | } 61 | entity.delete(); 62 | return entity; 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/TodoResource.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample; 2 | 3 | import io.quarkus.sample.audit.AuditType; 4 | import io.quarkus.panache.common.Sort; 5 | import io.quarkus.sample.ai.TodoAiService; 6 | import io.vertx.core.eventbus.EventBus; 7 | import jakarta.inject.Inject; 8 | 9 | import jakarta.transaction.Transactional; 10 | import jakarta.validation.Valid; 11 | import jakarta.ws.rs.DELETE; 12 | import jakarta.ws.rs.GET; 13 | import jakarta.ws.rs.OPTIONS; 14 | import jakarta.ws.rs.PATCH; 15 | import jakarta.ws.rs.POST; 16 | import jakarta.ws.rs.Path; 17 | import jakarta.ws.rs.PathParam; 18 | import jakarta.ws.rs.WebApplicationException; 19 | import jakarta.ws.rs.core.Response; 20 | import jakarta.ws.rs.core.Response.Status; 21 | import java.util.List; 22 | import org.eclipse.microprofile.openapi.annotations.Operation; 23 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 24 | 25 | @Path("/api") 26 | @Tag(name = "Todo Resource", description = "All Todo Operations") 27 | public class TodoResource { 28 | 29 | @Inject 30 | TodoAiService ai; 31 | 32 | @Inject 33 | EventBus bus; 34 | 35 | @OPTIONS 36 | @Operation(hidden = true) 37 | public Response opt() { 38 | return Response.ok().build(); 39 | } 40 | 41 | @GET 42 | @Operation(description = "Get all the todos") 43 | public List getAll() { 44 | return Todo.listAll(Sort.by("order")); 45 | } 46 | 47 | @GET 48 | @Path("/{id}") 49 | @Operation(description = "Get a specific todo by id") 50 | public Todo getOne(@PathParam("id") Long id) { 51 | Todo entity = Todo.findById(id); 52 | if (entity == null) { 53 | throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND); 54 | } 55 | return entity; 56 | } 57 | 58 | @POST 59 | @Transactional 60 | @Operation(description = "Create a new todo") 61 | public Response create(@Valid Todo item) { 62 | item.persist(); 63 | bus.publish(AuditType.TODO_ADDED.name(), item); 64 | return Response.status(Status.CREATED).entity(item).build(); 65 | } 66 | 67 | @PATCH 68 | @Path("/{id}") 69 | @Transactional 70 | @Operation(description = "Update an exiting todo") 71 | public Response update(@Valid Todo todo, @PathParam("id") Long id) { 72 | Todo entity = Todo.findById(id); 73 | if(entity.completed!=todo.completed && todo.completed){ 74 | bus.publish(AuditType.TODO_CHECKED.name(), todo); 75 | }else if(entity.completed!=todo.completed && !todo.completed){ 76 | bus.publish(AuditType.TODO_UNCHECKED.name(), todo); 77 | } 78 | 79 | entity.id = id; 80 | entity.completed = todo.completed; 81 | entity.order = todo.order; 82 | entity.title = todo.title; 83 | entity.url = todo.url; 84 | 85 | return Response.ok(entity).build(); 86 | } 87 | 88 | @DELETE 89 | @Transactional 90 | @Operation(description = "Remove all completed todos") 91 | public Response deleteCompleted() { 92 | Todo.deleteCompleted(); 93 | return Response.noContent().build(); 94 | } 95 | 96 | @DELETE 97 | @Transactional 98 | @Path("/{id}") 99 | @Operation(description = "Delete a specific todo") 100 | public Response deleteOne(@PathParam("id") Long id) { 101 | Todo entity = Todo.findById(id); 102 | if (entity == null) { 103 | throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND); 104 | } 105 | entity.delete(); 106 | bus.publish(AuditType.TODO_REMOVED.name(), entity); 107 | return Response.noContent().build(); 108 | } 109 | 110 | @GET 111 | @Path("/suggest") 112 | @Operation(description = "Suggest something to do") 113 | @Transactional 114 | public Todo suggest() { 115 | Todo suggestion = new Todo(); 116 | 117 | String title = ai.suggestSomethingTodo(1,"Features of my TODO list application"); 118 | title = title.trim(); 119 | suggestion.title = title; 120 | suggestion.persistAndFlush(); 121 | return suggestion; 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/ai/TodoAiService.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.ai; 2 | 3 | import dev.langchain4j.service.MemoryId; 4 | import dev.langchain4j.service.SystemMessage; 5 | import dev.langchain4j.service.UserMessage; 6 | import io.quarkiverse.langchain4j.RegisterAiService; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.enterprise.context.SessionScoped; 9 | import java.time.temporal.ChronoUnit; 10 | import org.eclipse.microprofile.faulttolerance.Fallback; 11 | import org.eclipse.microprofile.faulttolerance.Timeout; 12 | 13 | @RegisterAiService(retrievalAugmentor = TodoRetrievalAugmentor.class) 14 | @ApplicationScoped 15 | public interface TodoAiService { 16 | 17 | @SystemMessage("You are a helpful assistant in a TODO app") 18 | @UserMessage(""" 19 | I am bust learning {subject}. I need things to add to my TODO list to learn {subject}. 20 | What can I add to my TODO list ? 21 | 22 | Answer with only one item for the list, and only that item, no other text. 23 | The item on the list needs to be related to {subject} topics to learn. 24 | Do NOT add things like 'You can add' or 'Add a' or 'Learn about' at the start of the response. 25 | Do NOT add things like 'to the list' to the end of the response, 26 | just reply with the list item, that can be directly added to the list. 27 | 28 | Do NOT repeat any previous suggestions. 29 | Do NOT put the response in quotes (") or escaped quotes (\"). 30 | Remove all the extra spaces from the start and end of the response (trim). 31 | If the response contains multiple words, keep the spaces between those words of the response. 32 | Do NOT add a new line (\\n). 33 | 34 | Use the `current todos` as input of things currently on the list, and do NOT repeat any of them. 35 | Use the `current todos` as context on the things being learned and suggest something that would make sense to learn next. 36 | """) 37 | @Timeout(value = 60, unit = ChronoUnit.SECONDS) 38 | @Fallback(fallbackMethod = "fallback") 39 | String suggestSomethingTodo(@MemoryId int memoryId, String subject); 40 | 41 | default String fallback(int memoryId,String subject) { 42 | return "Fix AI integration"; 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/ai/TodoDatabaseContentRetriever.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.ai; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import jakarta.enterprise.context.ApplicationScoped; 7 | 8 | import dev.langchain4j.rag.content.Content; 9 | import dev.langchain4j.rag.content.retriever.ContentRetriever; 10 | import dev.langchain4j.rag.query.Query; 11 | import io.quarkus.sample.Todo; 12 | import io.vertx.core.json.JsonArray; 13 | import io.vertx.core.json.JsonObject; 14 | import java.util.stream.Collectors; 15 | 16 | @ApplicationScoped 17 | public class TodoDatabaseContentRetriever implements ContentRetriever { 18 | 19 | @Override 20 | public List retrieve(Query query) { 21 | List results = new ArrayList<>(); 22 | List all = Todo.listAll(); 23 | JsonObject json = new JsonObject(); 24 | List titles = all.stream().map((t) -> t.title).collect(Collectors.toList()); 25 | json.put("current todos", new JsonArray(titles)); 26 | results.add(Content.from(json.toString())); 27 | return results; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/ai/TodoRetrievalAugmentor.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.ai; 2 | 3 | import java.util.function.Supplier; 4 | 5 | import jakarta.enterprise.context.ApplicationScoped; 6 | import jakarta.inject.Inject; 7 | 8 | import dev.langchain4j.rag.DefaultRetrievalAugmentor; 9 | import dev.langchain4j.rag.RetrievalAugmentor; 10 | import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer; 11 | 12 | @ApplicationScoped 13 | public class TodoRetrievalAugmentor implements Supplier { 14 | 15 | @Inject 16 | TodoDatabaseContentRetriever contentRetriever; 17 | 18 | @Override 19 | public RetrievalAugmentor get() { 20 | return DefaultRetrievalAugmentor.builder() 21 | .contentRetriever(contentRetriever) 22 | .build(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/audit/AuditLogEncoder.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.audit; 2 | 3 | import jakarta.enterprise.inject.spi.CDI; 4 | import jakarta.json.bind.Jsonb; 5 | import jakarta.websocket.DecodeException; 6 | import jakarta.websocket.Decoder; 7 | import jakarta.websocket.EncodeException; 8 | import jakarta.websocket.Encoder; 9 | import jakarta.websocket.EndpointConfig; 10 | 11 | public class AuditLogEncoder implements Encoder.Text, Decoder.Text { 12 | 13 | private final Jsonb jsonb; 14 | 15 | public AuditLogEncoder() { 16 | this.jsonb = CDI.current().select(Jsonb.class).get(); 17 | } 18 | 19 | @Override 20 | public String encode(AuditLogSocket.AuditLogEntry object) throws EncodeException { 21 | return jsonb.toJson(object); 22 | } 23 | 24 | @Override 25 | public AuditLogSocket.AuditLogEntry decode(String s) throws DecodeException { 26 | return jsonb.fromJson(s, AuditLogSocket.AuditLogEntry.class); 27 | } 28 | 29 | @Override 30 | public boolean willDecode(String s) { 31 | return true; 32 | } 33 | 34 | @Override 35 | public void init(EndpointConfig config) { 36 | 37 | } 38 | 39 | @Override 40 | public void destroy() { 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/audit/AuditLogSocket.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.audit; 2 | 3 | import io.quarkus.logging.Log; 4 | import io.quarkus.sample.Todo; 5 | import io.quarkus.vertx.ConsumeEvent; 6 | import io.vertx.core.impl.ConcurrentHashSet; 7 | import jakarta.enterprise.context.ApplicationScoped; 8 | import jakarta.websocket.OnOpen; 9 | import jakarta.websocket.Session; 10 | import jakarta.websocket.server.ServerEndpoint; 11 | import java.util.Set; 12 | 13 | @ServerEndpoint(value = "/audit", encoders = AuditLogEncoder.class, decoders = AuditLogEncoder.class) 14 | @ApplicationScoped 15 | public class AuditLogSocket { 16 | 17 | Set sessions = new ConcurrentHashSet<>(); 18 | 19 | public record AuditLogEntry(AuditType type, Todo todo) { 20 | } 21 | 22 | @OnOpen 23 | public void onOpen(Session session) { 24 | sessions.add(session); 25 | } 26 | 27 | @ConsumeEvent("TODO_ADDED") 28 | public void add(Todo todo) { 29 | log(new AuditLogEntry(AuditType.TODO_ADDED, todo)); 30 | } 31 | 32 | @ConsumeEvent("TODO_CHECKED") 33 | public void check(Todo todo) { 34 | log(new AuditLogEntry(AuditType.TODO_CHECKED, todo)); 35 | } 36 | 37 | @ConsumeEvent("TODO_UNCHECKED") 38 | public void uncheck(Todo todo) { 39 | log(new AuditLogEntry(AuditType.TODO_UNCHECKED, todo)); 40 | } 41 | 42 | @ConsumeEvent("TODO_REMOVED") 43 | public void remove(Todo todo) { 44 | log(new AuditLogEntry(AuditType.TODO_REMOVED, todo)); 45 | } 46 | 47 | private void log(AuditLogEntry entry){ 48 | sessions.forEach(s -> { 49 | s.getAsyncRemote().sendObject(entry, result -> { 50 | if (result.getException() != null) { 51 | Log.error("Unable to send message: " + result.getException()); 52 | } 53 | }); 54 | }); 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/quarkus/sample/audit/AuditType.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample.audit; 2 | 3 | public enum AuditType { 4 | TODO_ADDED, TODO_CHECKED, TODO_UNCHECKED, TODO_REMOVED 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Hibernate 2 | quarkus.hibernate-orm.schema-management.strategy=drop-and-create 3 | #quarkus.hibernate-orm.log.sql=true 4 | 5 | # OpenAPI 6 | quarkus.smallrye-openapi.info-title=TODOS API 7 | %dev.quarkus.smallrye-openapi.info-title=TODOS API (development) 8 | %test.quarkus.smallrye-openapi.info-title=TODOS API (test) 9 | quarkus.smallrye-openapi.info-version=1.0.0 10 | quarkus.smallrye-openapi.info-description=Manage your todo list 11 | quarkus.smallrye-openapi.info-terms-of-service=This is for demo purpose only 12 | quarkus.smallrye-openapi.info-contact-email=techsupport@todos.com 13 | quarkus.smallrye-openapi.info-contact-name=TODOS API Support 14 | quarkus.smallrye-openapi.info-contact-url=http://todos.com/contact 15 | quarkus.smallrye-openapi.info-license-name=Apache 2.0 16 | quarkus.smallrye-openapi.info-license-url=https://www.apache.org/licenses/LICENSE-2.0.html 17 | quarkus.swagger-ui.always-include=true 18 | 19 | # DB (Prod mode) 20 | %prod.quarkus.datasource.db-kind=postgresql 21 | %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/rest-crud?loggerLevel=OFF 22 | %prod.quarkus.datasource.password=restcrud 23 | %prod.quarkus.datasource.username=restcrud 24 | 25 | # AI 26 | quarkus.langchain4j.openai.api-key=demo 27 | quarkus.langchain4j.openai.timeout=60s 28 | %dev.quarkus.datasource.dev-ui.allow-sql=true 29 | -------------------------------------------------------------------------------- /src/main/resources/import.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO todo(id, title, completed, ordering, url) VALUES (nextval('todo_seq'), 'Introduction to Quarkus', true, 0, null); 2 | INSERT INTO todo(id, title, completed, ordering, url) VALUES (nextval('todo_seq'), 'Hibernate with Panache', false, 1, null); 3 | INSERT INTO todo(id, title, completed, ordering, url) VALUES (nextval('todo_seq'), 'Visit Quarkus web site', false, 2, 'https://quarkus.io'); 4 | INSERT INTO todo(id, title, completed, ordering, url) VALUES (nextval('todo_seq'), 'Star Quarkus project', false, 3, 'https://github.com/quarkusio/quarkus/'); 5 | -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-app.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'lit'; 2 | 3 | import './todos-header.js'; 4 | import './todos-cards.js'; 5 | import './todos-footer.js'; 6 | 7 | class TodosApp extends LitElement { 8 | 9 | static styles = css` 10 | :host { 11 | display: flex; 12 | flex-direction: column; 13 | width: 100vw; 14 | height: 100vh; 15 | justify-content: space-between; 16 | overflow: hidden; 17 | } 18 | .center { 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: flex-start; 22 | height: 100%; 23 | } 24 | `; 25 | 26 | render() { 27 | return html`
28 | 29 | 30 |
31 | 32 | `; 33 | } 34 | } 35 | customElements.define('todos-app', TodosApp); -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-audit-log.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'lit'; 2 | import '@vaadin/grid'; 3 | import { columnBodyRenderer } from '@vaadin/grid/lit.js'; 4 | 5 | class TodosAuditLog extends LitElement { 6 | 7 | static webSocket; 8 | static serverUri; 9 | 10 | static styles = css` 11 | `; 12 | 13 | static properties = { 14 | _entries: {type: Array, state: true} 15 | }; 16 | 17 | constructor() { 18 | super(); 19 | this._entries = []; 20 | if (!TodosAuditLog.logWebSocket) { 21 | if (window.location.protocol === "https:") { 22 | TodosAuditLog.serverUri = "wss:"; 23 | } else { 24 | TodosAuditLog.serverUri = "ws:"; 25 | } 26 | TodosAuditLog.serverUri += "//" + window.location.host + "/audit"; 27 | TodosAuditLog.connect(); 28 | } 29 | this._eventAuditEntry = (event) => this._receiveAuditEntry(event.detail); 30 | } 31 | 32 | connectedCallback() { 33 | super.connectedCallback(); 34 | document.addEventListener('eventAuditEntryEvent', this._eventAuditEntry, false); 35 | } 36 | 37 | disconnectedCallback() { 38 | document.removeEventListener('eventAuditEntryEvent', this._eventAuditEntry, false); 39 | super.disconnectedCallback(); 40 | } 41 | 42 | render() { 43 | return html` 44 | 45 | 46 | 47 | 48 | `; 49 | } 50 | 51 | _typeRenderer(entry) { 52 | return html`${this._formatTodoType(entry.type)}`; 53 | } 54 | 55 | _formatTodoType(str) { 56 | return str.replace(/^TODO_(.*)$/, function(match, p1) { 57 | return p1.toLowerCase(); 58 | }); 59 | } 60 | 61 | _receiveAuditEntry(entry) { 62 | this._entries = [entry, ...this._entries]; 63 | } 64 | 65 | static connect() { 66 | TodosAuditLog.webSocket = new WebSocket(TodosAuditLog.serverUri); 67 | TodosAuditLog.webSocket.onmessage = function (event) { 68 | var auditentry = JSON.parse(event.data); 69 | const eventAuditEntryEvent = new CustomEvent('eventAuditEntryEvent', {detail: auditentry}); 70 | document.dispatchEvent(eventAuditEntryEvent); 71 | } 72 | TodosAuditLog.webSocket.onclose = function (event) { 73 | setTimeout(function () { 74 | TodosAuditLog.connect(); 75 | }, 100); 76 | }; 77 | } 78 | 79 | } 80 | customElements.define('todos-audit-log', TodosAuditLog); 81 | -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-cards.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'lit'; 2 | import '@vaadin/icon'; 3 | import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; 4 | import '@vaadin/icons'; 5 | import '@vaadin/progress-bar'; 6 | import './todos-task.js'; 7 | import { Notification } from '@vaadin/notification'; 8 | 9 | class TodosCards extends LitElement { 10 | static styles = css` 11 | :host { 12 | display: flex; 13 | gap: 20px; 14 | flex-direction: column; 15 | align-items: center; 16 | height: 100%; 17 | justify-content: space-between; 18 | } 19 | .inputBar { 20 | display: flex; 21 | align-items: center; 22 | font-size: 24px; 23 | } 24 | .input { 25 | padding: 12px 20px; 26 | margin: 8px 0; 27 | border: 0px solid white; 28 | font-size: 24px; 29 | color: var(--lumo-body-text-color); 30 | outline: none; 31 | background: var(--lumo-contrast-10pct); 32 | width: 450px; 33 | } 34 | input::placeholder { 35 | font-style: italic; 36 | color: var(--lumo-contrast-70pct); 37 | } 38 | 39 | .cards { 40 | display: flex; 41 | flex-direction: column; 42 | border: 1px solid var(--lumo-contrast-20pct); 43 | -webkit-box-shadow: 5px 5px 15px 5px var(--lumo-contrast-10pct); 44 | box-shadow: 5px 5px 15px 5px var(--lumo-contrast-10pct); 45 | min-width: 550px; 46 | } 47 | .items { 48 | display: flex; 49 | flex-direction: column; 50 | gap: 5px; 51 | padding-top: 10px; 52 | padding-bottom: 10px; 53 | overflow-y: scroll; 54 | overflow-x: hidden; 55 | max-height: 600px; 56 | } 57 | 58 | hr { 59 | border-bottom: none; 60 | width: 100%; 61 | color: var(--lumo-contrast-10pct); 62 | } 63 | 64 | .select-all-icon { 65 | color: var(--lumo-contrast-70pct); 66 | cursor: pointer; 67 | padding-left: 5px; 68 | padding-right: 5px; 69 | } 70 | 71 | .cards-footer { 72 | display: flex; 73 | justify-content: space-around; 74 | padding-bottom: 15px; 75 | font-size: 14px; 76 | text-align: center; 77 | color: var(--lumo-contrast-50pct); 78 | } 79 | 80 | .filter { 81 | padding: 3px; 82 | } 83 | .selected-filter { 84 | background: var(--lumo-contrast-10pct); 85 | padding: 3px; 86 | } 87 | 88 | .filter:hover { 89 | background: var(--lumo-contrast-10pct); 90 | cursor: pointer; 91 | } 92 | .clear-completed:hover { 93 | background: var(--lumo-contrast-10pct); 94 | cursor: pointer; 95 | } 96 | .hide { 97 | visibility:hidden; 98 | } 99 | .suggest { 100 | align-self: center; 101 | padding: 10px; 102 | margin-bottom: 5px; 103 | cursor: pointer; 104 | font-size: 24px; 105 | font-weight: 300; 106 | background: var(--lumo-primary-color-50pct); 107 | color: var(--lumo-base-color); 108 | } 109 | .suggest:hover { 110 | background: var(--lumo-primary-color); 111 | } 112 | .loading { 113 | padding: 15px; 114 | margin-bottom: 5px; 115 | } 116 | `; 117 | 118 | static properties = { 119 | _tasks: {type: Array, state: true}, 120 | _filteredTasks: {type: Array, state: true}, 121 | _filter: {type: String, state: true}, 122 | _isLoading: {type: Boolean, state: true} 123 | }; 124 | 125 | constructor() { 126 | super(); 127 | this._tasks = []; 128 | this._filteredTasks = []; 129 | this._filter = "all"; 130 | this._isLoading = false; 131 | } 132 | 133 | connectedCallback() { 134 | super.connectedCallback(); 135 | this._fetchAllTasks(); 136 | } 137 | 138 | render(){ 139 | return html`
140 | ${this._renderInput()} 141 | ${this._renderItems()} 142 | ${this._renderFooter()} 143 |
`; 144 | } 145 | 146 | _renderInput(){ 147 | return html`
148 | 149 | 150 |
`; 151 | 152 | } 153 | 154 | _renderItems(){ 155 | if(this._filteredTasks){ 156 | return html`
157 | ${this._filteredTasks.map((task) => 158 | this._renderItem(task) 159 | )} 160 | ${this._renderSuggestion()} 161 |
`; 162 | } 163 | } 164 | 165 | _renderItem(task){ 166 | return html`
`; 167 | } 168 | 169 | _renderSuggestion(){ 170 | if(this._isLoading){ 171 | return html``; 172 | }else{ 173 | return html`Suggest something to do`; 174 | } 175 | } 176 | 177 | _renderFooter(){ 178 | let outstandingTasksCount = this._tasks.filter(obj => obj.completed === false).length; 179 | let someCompleted = this._tasks.some(obj => obj.completed === true); 180 | 181 | let clearCompletedClass = "clear-completed"; 182 | if(!someCompleted){ 183 | clearCompletedClass = "hide"; 184 | } 185 | 186 | return html``; 195 | } 196 | 197 | _suggest(){ 198 | this._isLoading = true; 199 | fetch("/api/suggest") 200 | .then(response => response.json()) 201 | .then(response => this._addToTasks(response)); 202 | } 203 | 204 | _fetchAllTasks(){ 205 | fetch("/api") 206 | .then(response => response.json()) 207 | .then(response => this._setAll(response)); 208 | } 209 | 210 | _setAll(tasks){ 211 | this._tasks = tasks; 212 | this._filterTasks(); 213 | } 214 | 215 | _filterTasks(){ 216 | if(this._filter === "active"){ 217 | this._filteredTasks = this._tasks.filter(obj => obj.completed === false); 218 | }else if(this._filter === "completed") { 219 | this._filteredTasks = this._tasks.filter(obj => obj.completed === true); 220 | }else{ 221 | this._filteredTasks = this._tasks; 222 | } 223 | } 224 | 225 | _getFilterClass(forFilter){ 226 | if(this._filter === forFilter){ 227 | return "filter selected-filter"; 228 | } 229 | return "filter"; 230 | } 231 | 232 | _selectAll(event){ 233 | let allCompleted = this._tasks.every(obj => obj.completed === true); 234 | if(allCompleted){ 235 | this._markAll(false); 236 | }else{ 237 | this._markAll(true); 238 | } 239 | } 240 | 241 | _markAll(completed){ 242 | for (const task of this._tasks) { 243 | task.completed = completed; 244 | this._updateTask(task); 245 | } 246 | } 247 | 248 | _handleRequest(event) { 249 | if (event.keyCode == 13) { 250 | event.preventDefault(); 251 | let task = {title:event.target.value, completed: false}; 252 | 253 | const request = new Request('/api', { 254 | method: 'POST', 255 | body: JSON.stringify(task), 256 | headers: { 257 | 'Content-Type': 'application/json' 258 | } 259 | }); 260 | fetch(request) 261 | .then(r => r.json().then(data => ({status: r.status, body: data}))) 262 | .then(obj => this._handleResponse(obj)); 263 | event.target.value = ""; 264 | } 265 | } 266 | 267 | _handleResponse(statusAndBody) { 268 | if(statusAndBody.status === 201){ 269 | this._addToTasks(statusAndBody.body); 270 | }else { 271 | this._showErrorMessage(statusAndBody.body.details); 272 | } 273 | } 274 | 275 | _toggleSelect(e){ 276 | let task = this._getTaskById(e.detail); 277 | task.completed = !task.completed; 278 | this._updateTask(task); 279 | } 280 | 281 | _updateTask(task){ 282 | const request = new Request('/api/' + task.id, { 283 | method: 'PATCH', 284 | body: JSON.stringify(task), 285 | headers: { 286 | 'Content-Type': 'application/json' 287 | } 288 | }); 289 | fetch(request) 290 | .then(r => r.json().then(data => ({status: r.status, body: data}))) 291 | .then(obj => this._updateTaskInTasks(obj)); 292 | } 293 | 294 | _deleteItem(e) { 295 | const request = new Request('/api/' + e.detail, { 296 | method: 'DELETE', 297 | headers: { 298 | 'Content-Type': 'application/json' 299 | } 300 | }); 301 | fetch(request) 302 | .then(r => this._fetchAllTasks()); 303 | 304 | } 305 | 306 | _handleDeleteResponse(status) { 307 | if(status === 204){ 308 | this._fetchAllTasks(); 309 | }else { 310 | this._showErrorMessage("Delete failed with HTTP Response " + status); 311 | } 312 | } 313 | 314 | _filterAll(event){ 315 | this._filter = "all"; 316 | this._filterTasks(); 317 | } 318 | 319 | _filterActive(event){ 320 | this._filter = "active"; 321 | this._filterTasks(); 322 | } 323 | 324 | _filterCompleted(event){ 325 | this._filter = "completed"; 326 | this._filterTasks(); 327 | } 328 | 329 | _clearCompleted(event){ 330 | const request = new Request('/api', { 331 | method: 'DELETE', 332 | headers: { 333 | 'Content-Type': 'application/json' 334 | } 335 | }); 336 | fetch(request) 337 | .then(r => this._fetchAllTasks()); 338 | } 339 | 340 | _updateTaskInTasks(task) { 341 | this._tasks.map(obj => { 342 | if (obj.id === task.id) { 343 | return { ...obj, ...task }; 344 | } 345 | return obj; 346 | }); 347 | this._tasks = [ 348 | ...this._tasks]; 349 | 350 | this._filterTasks(); 351 | 352 | } 353 | 354 | _getTaskById(id){ 355 | return this._tasks.find(obj => obj.id === id); 356 | } 357 | 358 | _addToTasks(task){ 359 | this._isLoading = false; 360 | this._tasks = [ 361 | task, 362 | ...this._tasks 363 | ]; 364 | this._filterTasks(); 365 | } 366 | 367 | _showErrorMessage(message){ 368 | const notification = Notification.show(message, { 369 | position: 'bottom-stretch', 370 | duration: 10000, 371 | }); 372 | } 373 | } 374 | customElements.define('todos-cards', TodosCards); 375 | -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-footer.js: -------------------------------------------------------------------------------- 1 | import {LitElement, css, html} from 'lit'; 2 | import '@vaadin/horizontal-layout'; 3 | 4 | class TodosFooter extends LitElement { 5 | static styles = css` 6 | footer, footer a { 7 | color: var(--lumo-contrast-70pct); 8 | font-size: 10px; 9 | } 10 | `; 11 | 12 | render() { 13 | return html` 14 | 23 | `; 24 | } 25 | } 26 | customElements.define('todos-footer', TodosFooter); 27 | -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-header.js: -------------------------------------------------------------------------------- 1 | import {LitElement, css, html} from 'lit'; 2 | import '@vaadin/icon'; 3 | import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; 4 | import '@vaadin/icons'; 5 | import '@vaadin/tooltip'; 6 | 7 | class TodosHeader extends LitElement { 8 | static styles = css` 9 | :host { 10 | display: flex; 11 | justify-content: center; 12 | font-size: 100px; 13 | line-height: 100px; 14 | height: 100px; 15 | font-weight: 100; 16 | padding-top: 20px; 17 | padding-bottom: 20px; 18 | } 19 | 20 | .title { 21 | align-self: baseline; 22 | padding-left: 20px; 23 | } 24 | .logo { 25 | align-self: baseline; 26 | width: 64px; 27 | height: 64px; 28 | } 29 | .theme-switch { 30 | height: 25px; 31 | position: absolute; 32 | right: 30px; 33 | cursor: pointer; 34 | width: 25px; 35 | } 36 | `; 37 | 38 | static properties = { 39 | _nextTheme: {state: true}, 40 | _currentTheme: {state: true}, 41 | } 42 | 43 | constructor() { 44 | super(); 45 | } 46 | 47 | connectedCallback() { 48 | super.connectedCallback() 49 | this._currentTheme = this._retrieveTheme(); 50 | const body = document.body; 51 | body.setAttribute('theme', this._currentTheme); 52 | } 53 | 54 | 55 | 56 | render() { 57 | return html` todos 58 | 59 | 60 | `; 61 | } 62 | 63 | _switchTheme(){ 64 | const body = document.body; 65 | if (body.getAttribute('theme') === 'light') { 66 | this._currentTheme = "dark"; 67 | } else { 68 | this._currentTheme = "light"; 69 | } 70 | body.setAttribute('theme', this._currentTheme); 71 | this._storeTheme(this._currentTheme); 72 | } 73 | 74 | _flip(theme){ 75 | if (theme === 'light') { 76 | return "dark"; 77 | } else { 78 | return "light"; 79 | } 80 | } 81 | 82 | _storeTheme(theme){ 83 | localStorage.setItem("theme", theme); 84 | } 85 | 86 | _retrieveTheme(){ 87 | let theme = localStorage.getItem("theme"); 88 | if(theme === null){ 89 | return "light"; 90 | } 91 | return theme; 92 | } 93 | } 94 | customElements.define('todos-header', TodosHeader); -------------------------------------------------------------------------------- /src/main/resources/web/app/todos-task.js: -------------------------------------------------------------------------------- 1 | import {LitElement, html, css} from 'lit'; 2 | import '@vaadin/icon'; 3 | import '@vaadin/vaadin-lumo-styles/vaadin-iconset.js'; 4 | import '@vaadin/icons'; 5 | 6 | class TodosTask extends LitElement { 7 | static styles = css` 8 | .item { 9 | display: flex; 10 | justify-content:space-between; 11 | font-size: 24px; 12 | font-weight: 300; 13 | width: 100%; 14 | gap: 20px; 15 | } 16 | .done-icon { 17 | color: var(--lumo-success-color-50pct); 18 | cursor: pointer; 19 | padding-left: 5px; 20 | } 21 | .outstanding-icon { 22 | color: var(--lumo-contrast-30pct); 23 | cursor: pointer; 24 | padding-left: 5px; 25 | } 26 | .done-text { 27 | text-decoration: line-through; 28 | color: var(--lumo-contrast-50pct); 29 | } 30 | .delete-icon { 31 | color: var(--lumo-error-color); 32 | cursor: pointer; 33 | padding-right: 5px; 34 | } 35 | .hide { 36 | visibility:hidden; 37 | } 38 | `; 39 | 40 | static properties = { 41 | id: {type: Number}, 42 | task: {type: String}, 43 | done: {type: Boolean, reflect: true}, 44 | _deleteButtonClass: {type: Boolean, attribute: false}, 45 | }; 46 | 47 | constructor() { 48 | super(); 49 | this.id = -1; 50 | this.task = ""; 51 | this.done = false; 52 | this._deleteButtonClass = "hide"; 53 | } 54 | 55 | connectedCallback() { 56 | super.connectedCallback() 57 | this.addEventListener('mouseenter', this._handleMouseenter); 58 | this.addEventListener('mouseleave', this._handleMouseleave); 59 | } 60 | 61 | render() { 62 | if(this.task){ 63 | let icon = "vaadin:thin-square"; 64 | let iconClass = "outstanding-icon"; 65 | let textClass = "outstanding-text"; 66 | if(this.done){ 67 | icon = "vaadin:check-square-o"; 68 | iconClass = "done-icon"; 69 | textClass = "done-text"; 70 | } 71 | return html` 72 | 73 | ${this.task} 74 | ${this._renderDeleteButton()} 75 | `; 76 | } 77 | } 78 | 79 | _renderDeleteButton(){ 80 | return html``; 81 | } 82 | 83 | _handleMouseenter(){ 84 | this._deleteButtonClass = "delete-icon"; 85 | } 86 | 87 | _handleMouseleave(){ 88 | this._deleteButtonClass = "hide"; 89 | } 90 | 91 | _toggleSelect(event){ 92 | event = new CustomEvent('select', {detail: this.id, bubbles: true, composed: true}); 93 | this.dispatchEvent(event); 94 | } 95 | 96 | _delete(event){ 97 | event = new CustomEvent('delete', {detail: this.id, bubbles: true, composed: true}); 98 | this.dispatchEvent(event); 99 | } 100 | 101 | } 102 | customElements.define('todos-task', TodosTask); -------------------------------------------------------------------------------- /src/main/resources/web/app/todos.css: -------------------------------------------------------------------------------- 1 | body[theme~="dark"] { 2 | --lumo-primary-color-50pct: hsla(211, 63%, 54%, 0.5); 3 | --lumo-body-text-color: hsla(214, 96%, 96%, 0.9); 4 | --lumo-tertiary-text-color: hsla(214, 78%, 88%, 0.5); 5 | --lumo-base-color: hsla(210, 10%, 23%, 1.0); 6 | --lumo-error-contrast-color: hsla(0, 100%, 100%, 1.0); 7 | --lumo-contrast-30pct: hsla(214, 69%, 84%, 0.32); 8 | --lumo-contrast-80pct: hsla(214, 91%, 94%, 0.8); 9 | --lumo-contrast-70pct: hsla(214, 87%, 92%, 0.7); 10 | --lumo-contrast-20pct: hsla(214, 64%, 82%, 0.23); 11 | --lumo-primary-color: hsla(211, 63%, 54%, 1.0); 12 | --lumo-error-color: hsla(3, 90%, 63%, 1.0); 13 | --lumo-disabled-text-color: hsla(214, 69%, 84%, 0.32); 14 | --lumo-contrast-90pct: hsla(214, 96%, 96%, 0.9); 15 | --lumo-contrast-60pct: hsla(214, 82%, 90%, 0.6); 16 | --lumo-warning-text-color: hsla(30, 100%, 67%, 1.0); 17 | --lumo-contrast-10pct: hsla(214, 60%, 80%, 0.14); 18 | --lumo-primary-text-color: hsla(211, 63%, 54%, 1.0); 19 | --lumo-success-color-10pct: hsla(145, 65%, 42%, 0.1); 20 | --lumo-primary-color-10pct: hsla(214, 90%, 63%, 0.1); 21 | --lumo-primary-contrast-color: hsla(0, 100%, 100%, 1.0); 22 | --lumo-success-color-50pct: hsla(145, 65%, 42%, 0.5); 23 | --lumo-success-color: hsla(145, 65%, 42%, 1.0); 24 | --lumo-warning-color: hsla(30, 100%, 50%, 1.0); 25 | --lumo-success-contrast-color: hsla(0, 100%, 100%, 1.0); 26 | --lumo-contrast-50pct: hsla(214, 78%, 88%, 0.5); 27 | --lumo-warning-color-50pct: hsla(30, 100%, 50%, 0.5); 28 | --lumo-header-text-color: hsla(214, 100%, 98%, 1.0); 29 | --quarkus-blue: hsla(211, 63%, 54%, 1.0); 30 | --lumo-warning-color-10pct: hsla(30, 100%, 50%, 0.1); 31 | --lumo-contrast-40pct: hsla(214, 73%, 86%, 0.41); 32 | --lumo-error-color-50pct: hsla(3, 90%, 63%, 0.5); 33 | --lumo-warning-contrast-color: hsla(0, 100%, 100%, 1.0); 34 | --lumo-secondary-text-color: hsla(214, 87%, 92%, 0.7); 35 | --lumo-error-text-color: hsla(3, 100%, 67%, 1.0); 36 | --quarkus-red: hsla(343, 100%, 50%, 1.0); 37 | --quarkus-center: hsla(0, 0%, 90%, 1.0); 38 | --lumo-contrast-5pct: hsla(214, 65%, 85%, 0.06); 39 | --lumo-contrast: hsla(214, 100%, 98%, 1.0); 40 | --lumo-error-color-10pct: hsla(3, 90%, 63%, 0.1); 41 | --lumo-success-text-color: hsla(145, 85%, 47%, 1.0); 42 | } 43 | 44 | body[theme~="light"] { 45 | --lumo-primary-color-50pct: hsla(211, 63%, 54%, 0.76); 46 | --lumo-body-text-color: hsla(214, 40%, 16%, 0.94); 47 | --lumo-tertiary-text-color: hsla(214, 45%, 20%, 0.52); 48 | --lumo-base-color: hsla(0, 100%, 100%, 1.0); 49 | --lumo-error-contrast-color: hsla(0, 100%, 100%, 1.0); 50 | --lumo-contrast-30pct: hsla(214, 50%, 22%, 0.26); 51 | --lumo-contrast-80pct: hsla(214, 41%, 17%, 0.83); 52 | --lumo-contrast-70pct: hsla(214, 42%, 18%, 0.69); 53 | --lumo-contrast-20pct: hsla(214, 53%, 23%, 0.16); 54 | --lumo-primary-color: hsla(211, 63%, 54%, 1.0); 55 | --lumo-error-color: hsla(3, 85%, 48%, 1.0); 56 | --lumo-disabled-text-color: hsla(214, 50%, 22%, 0.26); 57 | --lumo-contrast-90pct: hsla(214, 40%, 16%, 0.94); 58 | --lumo-contrast-60pct: hsla(214, 43%, 19%, 0.6); 59 | --lumo-warning-text-color: hsla(30, 89%, 42%, 1.0); 60 | --lumo-contrast-10pct: hsla(214, 57%, 24%, 0.1); 61 | --lumo-primary-text-color: hsla(211, 63%, 54%, 1.0); 62 | --lumo-success-color-10pct: hsla(145, 72%, 31%, 0.1); 63 | --lumo-primary-color-10pct: hsla(214, 100%, 60%, 0.13); 64 | --lumo-primary-contrast-color: hsla(0, 100%, 100%, 1.0); 65 | --lumo-success-color-50pct: hsla(145, 72%, 31%, 0.5); 66 | --lumo-success-color: hsla(145, 72%, 30%, 1.0); 67 | --lumo-warning-color: hsla(30, 100%, 50%, 1.0); 68 | --lumo-success-contrast-color: hsla(0, 100%, 100%, 1.0); 69 | --lumo-contrast-50pct: hsla(214, 45%, 20%, 0.52); 70 | --lumo-warning-color-50pct: hsla(30, 100%, 50%, 0.5); 71 | --lumo-header-text-color: hsla(214, 35%, 15%, 1.0); 72 | --quarkus-blue: hsla(211, 63%, 54%, 1.0); 73 | --lumo-warning-color-10pct: hsla(30, 100%, 50%, 0.1); 74 | --lumo-contrast-40pct: hsla(214, 47%, 21%, 0.38); 75 | --lumo-error-color-50pct: hsla(3, 85%, 49%, 0.5); 76 | --lumo-warning-contrast-color: hsla(0, 100%, 100%, 1.0); 77 | --lumo-secondary-text-color: hsla(214, 42%, 18%, 0.69); 78 | --lumo-error-text-color: hsla(3, 89%, 42%, 1.0); 79 | --quarkus-red: hsla(343, 100%, 50%, 1.0); 80 | --quarkus-center: hsla(180, 36%, 5%, 1.0); 81 | --lumo-contrast-5pct: hsla(214, 61%, 25%, 0.05); 82 | --lumo-contrast: hsla(214, 35%, 15%, 1.0); 83 | --lumo-error-color-10pct: hsla(3, 85%, 49%, 0.1); 84 | --lumo-success-text-color: hsla(145, 85%, 25%, 1.0); 85 | } 86 | body { 87 | width: 100vw; 88 | height: 100vh; 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: space-between; 92 | overflow: hidden; 93 | 94 | background: var(--lumo-base-color); 95 | color: var(--lumo-body-text-color); 96 | } -------------------------------------------------------------------------------- /src/main/resources/web/audit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {#bundle /} 10 | 11 | Quarkus todos audit log 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {#bundle /} 10 | 11 | Quarkus todos 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/src/main/resources/web/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/web/static/quarkus_icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/src/main/resources/web/static/quarkus_icon_dark.png -------------------------------------------------------------------------------- /src/main/resources/web/static/quarkus_icon_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarkusio/todo-demo-app/a8e186b54ce6454ea10651a2a8e2b772cb48402d/src/main/resources/web/static/quarkus_icon_light.png -------------------------------------------------------------------------------- /src/test/java/io/quarkus/sample/NativeTodoResourceIT.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample; 2 | 3 | import io.quarkus.test.junit.QuarkusIntegrationTest; 4 | 5 | @QuarkusIntegrationTest 6 | public class NativeTodoResourceIT extends TodoResourceTest { 7 | 8 | // Execute the same tests but in native mode. 9 | } -------------------------------------------------------------------------------- /src/test/java/io/quarkus/sample/TodoResourceTest.java: -------------------------------------------------------------------------------- 1 | package io.quarkus.sample; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import io.quarkus.test.junit.QuarkusTest; 6 | import io.restassured.http.ContentType; 7 | import org.junit.jupiter.api.MethodOrderer; 8 | import org.junit.jupiter.api.Order; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.TestMethodOrder; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.Arguments; 13 | import org.junit.jupiter.params.provider.MethodSource; 14 | 15 | import static io.restassured.RestAssured.given; 16 | import static org.hamcrest.CoreMatchers.is; 17 | 18 | @QuarkusTest 19 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 20 | public class TodoResourceTest { 21 | 22 | @Test 23 | @Order(1) 24 | public void testGetAll() { 25 | given() 26 | .accept(ContentType.JSON) 27 | .when() 28 | .get("/api") 29 | .then() 30 | .statusCode(200) 31 | .body(is(ALL)); 32 | } 33 | 34 | @Test 35 | @Order(2) 36 | public void testGet() { 37 | given() 38 | .accept(ContentType.JSON) 39 | .when() 40 | .get("/api/1") 41 | .then() 42 | .statusCode(200) 43 | .body(is(ONE)); 44 | } 45 | 46 | @Test 47 | @Order(3) 48 | public void testCreateNew() { 49 | given() 50 | .contentType(ContentType.JSON) 51 | .when() 52 | .body(CREATE_NEW) 53 | .post("/api") 54 | .then() 55 | .statusCode(201) 56 | .body(is(NEW_CREATED)); 57 | } 58 | 59 | @Test 60 | @Order(4) 61 | public void testUpdate() { 62 | given() 63 | .contentType(ContentType.JSON) 64 | .when() 65 | .body(UPDATE) 66 | .patch("/api/201") 67 | .then() 68 | .statusCode(200) 69 | .body(is(UPDATED)); 70 | } 71 | 72 | @ParameterizedTest 73 | @Order(5) 74 | @MethodSource("todoItemsToDelete") 75 | public void testDelete(int id, int expectedStatus) { 76 | given() 77 | .pathParam("id", id) 78 | .when() 79 | .delete("/api/{id}") 80 | .then() 81 | .statusCode(expectedStatus); 82 | } 83 | 84 | private static Stream todoItemsToDelete() { 85 | return Stream.of( 86 | Arguments.of(201, 204), 87 | Arguments.of(15, 404)); 88 | } 89 | 90 | private static final String ALL = "[{\"id\":1,\"completed\":true,\"order\":0,\"title\":\"Introduction to Quarkus\"},{\"id\":51,\"completed\":false,\"order\":1,\"title\":\"Hibernate with Panache\"},{\"id\":101,\"completed\":false,\"order\":2,\"title\":\"Visit Quarkus web site\",\"url\":\"https://quarkus.io\"},{\"id\":151,\"completed\":false,\"order\":3,\"title\":\"Star Quarkus project\",\"url\":\"https://github.com/quarkusio/quarkus/\"}]"; 91 | 92 | private static final String ONE = "{\"id\":1,\"completed\":true,\"order\":0,\"title\":\"Introduction to Quarkus\"}"; 93 | 94 | private static final String CREATE_NEW = "{\"completed\":false,\"order\":0,\"title\":\"Use the REST Endpoint\"}"; 95 | 96 | private static final String NEW_CREATED = "{\"id\":201,\"completed\":false,\"order\":0,\"title\":\"Use the REST Endpoint\"}"; 97 | 98 | private static final String UPDATE = "{\"id\":201,\"completed\":false,\"order\":0,\"title\":\"Use the GraphQL Endpoint\"}"; 99 | 100 | private static final String UPDATED = "{\"id\":201,\"completed\":false,\"order\":0,\"title\":\"Use the GraphQL Endpoint\"}"; 101 | } --------------------------------------------------------------------------------