From b55407dc8b0fbec3da110464ffb5b30caffcdea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Mon, 13 Dec 2021 10:57:15 +0100 Subject: [PATCH] Initial import --- .github/dependabot.yml | 13 + .github/project.yml | 4 + .github/release/maven-settings.xml.gpg | Bin 0 -> 856 bytes .github/workflows/build.yml | 53 + .github/workflows/pre-release.yml | 25 + .github/workflows/quarkus-snapshot.yaml | 52 + .github/workflows/release.yml | 71 + .gitignore | 64 + LICENSE | 201 +++ README.md | 23 + deployment/pom.xml | 80 ++ .../renarde/deployment/ControllerVisitor.java | 375 +++++ .../renarde/deployment/RenardeProcessor.java | 505 +++++++ .../deployment/RouterMethodVisitor.java | 43 + .../renarde/deployment/RouterUserVisitor.java | 28 + .../renarde/test/RenardeDevModeTest.java | 23 + .../quarkiverse/renarde/test/RenardeTest.java | 23 + docs/antora.yml | 5 + docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/config.adoc | 112 ++ docs/modules/ROOT/pages/index.adoc | 1259 +++++++++++++++++ integration-tests/pom.xml | 89 ++ .../renarde/it/RenardeResource.java | 32 + .../src/main/resources/application.properties | 0 .../renarde/it/NativeRenardeResourceIT.java | 7 + .../renarde/it/RenardeResourceTest.java | 21 + pom.xml | 71 + runtime/pom.xml | 90 ++ .../io/quarkiverse/renarde/Controller.java | 109 ++ .../quarkiverse/renarde/router/Method0.java | 6 + .../quarkiverse/renarde/router/Method0V.java | 6 + .../quarkiverse/renarde/router/Method1.java | 6 + .../quarkiverse/renarde/router/Method10.java | 7 + .../quarkiverse/renarde/router/Method10V.java | 7 + .../quarkiverse/renarde/router/Method11.java | 7 + .../quarkiverse/renarde/router/Method11V.java | 7 + .../quarkiverse/renarde/router/Method12.java | 7 + .../quarkiverse/renarde/router/Method12V.java | 7 + .../quarkiverse/renarde/router/Method13.java | 7 + .../quarkiverse/renarde/router/Method13V.java | 7 + .../quarkiverse/renarde/router/Method1V.java | 6 + .../quarkiverse/renarde/router/Method2.java | 6 + .../quarkiverse/renarde/router/Method2V.java | 6 + .../quarkiverse/renarde/router/Method3.java | 6 + .../quarkiverse/renarde/router/Method3V.java | 6 + .../quarkiverse/renarde/router/Method4.java | 6 + .../quarkiverse/renarde/router/Method4V.java | 6 + .../quarkiverse/renarde/router/Method5.java | 6 + .../quarkiverse/renarde/router/Method5V.java | 6 + .../quarkiverse/renarde/router/Method6.java | 6 + .../quarkiverse/renarde/router/Method6V.java | 6 + .../quarkiverse/renarde/router/Method7.java | 6 + .../quarkiverse/renarde/router/Method7V.java | 6 + .../quarkiverse/renarde/router/Method8.java | 6 + .../quarkiverse/renarde/router/Method8V.java | 6 + .../quarkiverse/renarde/router/Method9.java | 7 + .../quarkiverse/renarde/router/Method9V.java | 7 + .../io/quarkiverse/renarde/router/Router.java | 171 +++ .../renarde/router/RouterMethod.java | 8 + .../AuthenticationFailedExceptionMapper.java | 61 + .../io/quarkiverse/renarde/util/CRSF.java | 87 ++ .../io/quarkiverse/renarde/util/Filters.java | 53 + .../io/quarkiverse/renarde/util/Flash.java | 118 ++ .../renarde/util/JavaExtensions.java | 96 ++ .../renarde/util/MyParamConverters.java | 46 + .../renarde/util/MyValidationInterceptor.java | 58 + .../renarde/util/QuteResolvers.java | 77 + .../renarde/util/RedirectException.java | 17 + .../renarde/util/RedirectExceptionMapper.java | 12 + .../quarkiverse/renarde/util/RenderArgs.java | 26 + .../quarkiverse/renarde/util/StringUtils.java | 7 + .../io/quarkiverse/renarde/util/UriUtils.java | 104 ++ .../quarkiverse/renarde/util/Validation.java | 110 ++ .../resources/META-INF/quarkus-extension.yaml | 9 + .../templates/tags/authenticityToken.html | 1 + .../main/resources/templates/tags/error.html | 1 + .../main/resources/templates/tags/form.html | 4 + .../resources/templates/tags/gravatar.html | 1 + .../resources/templates/tags/ifError.html | 1 + 79 files changed, 4625 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/project.yml create mode 100644 .github/release/maven-settings.xml.gpg create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/pre-release.yml create mode 100644 .github/workflows/quarkus-snapshot.yaml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deployment/pom.xml create mode 100644 deployment/src/main/java/io/quarkiverse/renarde/deployment/ControllerVisitor.java create mode 100644 deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java create mode 100644 deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterMethodVisitor.java create mode 100644 deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterUserVisitor.java create mode 100644 deployment/src/test/java/io/quarkiverse/renarde/test/RenardeDevModeTest.java create mode 100644 deployment/src/test/java/io/quarkiverse/renarde/test/RenardeTest.java create mode 100644 docs/antora.yml create mode 100644 docs/modules/ROOT/nav.adoc create mode 100644 docs/modules/ROOT/pages/config.adoc create mode 100644 docs/modules/ROOT/pages/index.adoc create mode 100644 integration-tests/pom.xml create mode 100644 integration-tests/src/main/java/io/quarkiverse/renarde/it/RenardeResource.java create mode 100644 integration-tests/src/main/resources/application.properties create mode 100644 integration-tests/src/test/java/io/quarkiverse/renarde/it/NativeRenardeResourceIT.java create mode 100644 integration-tests/src/test/java/io/quarkiverse/renarde/it/RenardeResourceTest.java create mode 100644 pom.xml create mode 100644 runtime/pom.xml create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/Controller.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method0.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method0V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method1.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method10.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method10V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method11.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method11V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method12.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method12V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method13.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method13V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method1V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method2.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method2V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method3.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method3V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method4.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method4V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method5.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method5V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method6.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method6V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method7.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method7V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method8.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method8V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method9.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Method9V.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/Router.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/router/RouterMethod.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/AuthenticationFailedExceptionMapper.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/CRSF.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/Flash.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/JavaExtensions.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/MyParamConverters.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/MyValidationInterceptor.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/RedirectException.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/RedirectExceptionMapper.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/RenderArgs.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/StringUtils.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/UriUtils.java create mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/Validation.java create mode 100644 runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 runtime/src/main/resources/templates/tags/authenticityToken.html create mode 100644 runtime/src/main/resources/templates/tags/error.html create mode 100644 runtime/src/main/resources/templates/tags/form.html create mode 100644 runtime/src/main/resources/templates/tags/gravatar.html create mode 100644 runtime/src/main/resources/templates/tags/ifError.html diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..4b11d34c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "org.apache.maven.plugins:maven-compiler-plugin" diff --git a/.github/project.yml b/.github/project.yml new file mode 100644 index 00000000..9afd242a --- /dev/null +++ b/.github/project.yml @@ -0,0 +1,4 @@ +release: + current-version: 0 + next-version: 1.0.0-SNAPSHOT + diff --git a/.github/release/maven-settings.xml.gpg b/.github/release/maven-settings.xml.gpg new file mode 100644 index 0000000000000000000000000000000000000000..b342f7c1f62cd53c74527af166e36c3b5b45e701 GIT binary patch literal 856 zcmV-e1E>6j0t^F0JI%MV2--pc5CF7mLz1Q7ICpW1>rN1SZ93bnmX$kRj7+ymY|l~M zOfF{8Ib7y8Q5zngSD0_9X+rR zc``gl)ud618Zp}e;4U@9lLqZ_fhr#mw5Ru)HY-1v0nZ2OEC~wk&jY5Pd%x$B^9I!4z?zBXg@pHP|3lkb30WIU3o5XDbh6t7$2h=RxS zn|03P=#^E>+XI~<;JU}oRxET~k%b&XTX_yvW97@sl*GH+6_I3@f7HojrCJ7zM=5QG z+m3d#ft?Y8B4+pAWSgL&sR^}asK=6s93y^uwJ?cr*2Qbr8i7PMQ2rxOpp to manually launch the ecosystem CI in addition to the bots + if: github.actor == 'quarkusbot' || github.actor == 'quarkiversebot' || github.actor == '' + + steps: + - name: Install yq + run: sudo add-apt-repository ppa:rmescandon/yq && sudo apt update && sudo apt install yq -y + + - name: Set up Java + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + - name: Checkout repo + uses: actions/checkout@v2 + with: + path: current-repo + + - name: Checkout Ecosystem + uses: actions/checkout@v2 + with: + repository: ${{ env.ECOSYSTEM_CI_REPO }} + path: ecosystem-ci + + - name: Setup and Run Tests + run: ./ecosystem-ci/setup-and-test + env: + ECOSYSTEM_CI_TOKEN: ${{ secrets.ECOSYSTEM_CI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..7bf1c6ba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Quarkiverse Release + +on: + pull_request: + types: [closed] + paths: + - '.github/project.yml' + +jobs: + release: + runs-on: ubuntu-latest + name: release + if: ${{github.event.pull_request.merged == true}} + + steps: + - uses: radcortez/project-metadata-action@main + name: Retrieve project metadata + id: metadata + with: + github-token: ${{secrets.GITHUB_TOKEN}} + metadata-file-path: '.github/project.yml' + + - uses: actions/checkout@v2 + + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v3 + with: + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 11 + + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Configure Git author + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Maven release ${{steps.metadata.outputs.current-version}} + run: | + gpg --quiet --batch --yes --decrypt --passphrase="${{secrets.GPG_PASSPHRASE}}" --output /tmp/maven-settings.xml .github/release/maven-settings.xml.gpg + git checkout -b release + mvn -B release:prepare -Prelease -DreleaseVersion=${{steps.metadata.outputs.current-version}} -DdevelopmentVersion=${{steps.metadata.outputs.next-version}} -s /tmp/maven-settings.xml + git checkout ${{github.base_ref}} + git rebase release + mvn -B release:perform -Darguments=-DperformRelease -DperformRelease -Prelease -s /tmp/maven-settings.xml + + - name: Push changes to ${{github.base_ref}} + uses: ad-m/github-push-action@v0.6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{github.base_ref}} + + - name: Push tags + uses: ad-m/github-push-action@v0.6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tags: true + branch: ${{github.base_ref}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a556d947 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Gradle +.gradle/ +build/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..fdbcc161 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Quarkus - Renarde + +## Welcome to Quarkiverse! + +Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse! + +Feel free to replace this content with the proper description of your new project and necessary instructions how to use and contribute to it. + +You can find the basic info, Quarkiverse policies and conventions in [the Quarkiverse wiki](https://github.com/quarkiverse/quarkiverse/wiki). + +In case you are creating a Quarkus extension project for the first time, please follow [Building My First Extension](https://quarkus.io/guides/building-my-first-extension) guide. + +Other useful articles related to Quarkus extension development can be found under the [Writing Extensions](https://quarkus.io/guides/#writing-extensions) guide category on the [Quarkus.io](http://quarkus.io) website. + +Thanks again, good luck and have fun! + +## Documentation + +The documentation for this extension should be maintained as part of this repository and it is stored in the `docs/` directory. + +The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). + +Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). \ No newline at end of file diff --git a/deployment/pom.xml b/deployment/pom.xml new file mode 100644 index 00000000..495cabfa --- /dev/null +++ b/deployment/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + io.quarkiverse.renarde + quarkus-renarde-parent + 1.0.0-SNAPSHOT + + quarkus-renarde-deployment + Quarkus - Renarde - Deployment + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-narayana-jta-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-security-deployment + + + io.quarkus + quarkus-resteasy-reactive-deployment + + + io.quarkus + quarkus-resteasy-reactive-qute-deployment + + + io.quarkus + quarkus-reactive-routes-deployment + + + io.quarkus + quarkus-smallrye-jwt-deployment + + + io.quarkus + quarkus-hibernate-validator-deployment + + + io.quarkus + quarkus-qute-deployment + + + io.quarkiverse.renarde + quarkus-renarde + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/deployment/src/main/java/io/quarkiverse/renarde/deployment/ControllerVisitor.java b/deployment/src/main/java/io/quarkiverse/renarde/deployment/ControllerVisitor.java new file mode 100644 index 00000000..ca1ce573 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/renarde/deployment/ControllerVisitor.java @@ -0,0 +1,375 @@ +package io.quarkiverse.renarde.deployment; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.deployment.util.AsmUtil; +import io.quarkus.runtime.util.HashUtil; + +public class ControllerVisitor implements BiFunction { + + static abstract class UriPart { + } + + static class StaticUriPart extends UriPart { + public final String part; + + public StaticUriPart(String part) { + this.part = part; + } + } + + static class PathParamUriPart extends UriPart { + public final String name; + public final int asmParamIndex; + public final int paramIndex; + public final boolean declared; + + public PathParamUriPart(String name, int paramIndex, int asmParamIndex, boolean declared) { + this.name = name; + this.asmParamIndex = asmParamIndex; + this.paramIndex = paramIndex; + this.declared = declared; + } + } + + static class QueryParamUriPart extends UriPart { + public final String name; + public final int asmParamIndex; + public final int paramIndex; + + public QueryParamUriPart(String name, int paramIndex, int asmParamIndex) { + this.name = name; + this.paramIndex = paramIndex; + this.asmParamIndex = asmParamIndex; + } + } + + static class ControllerMethod { + public final String name; + public final String descriptor; + public final List parts; + public final List parameters; + + public ControllerMethod(String name, String descriptor, List parts, List parameters) { + this.name = name; + this.descriptor = descriptor; + this.parts = parts; + this.parameters = parameters; + } + } + + static class ControllerClass { + public final String className; + public final Map methods; + + public ControllerClass(String className, Map methods) { + this.className = className; + this.methods = methods; + } + } + + Map controllers; + + public ControllerVisitor(Map controllers) { + this.controllers = controllers; + } + + @Override + public ClassVisitor apply(String className, ClassVisitor visitor) { + return new ControllerClassVisitor(controllers.get(className), controllers, visitor); + } + + public static class ControllerClassVisitor extends ClassVisitor { + + private String className; + private ControllerClass controller; + private Map controllers; + + public ControllerClassVisitor(ControllerClass controller, Map controllers, + ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + this.controller = controller; + this.controllers = controllers; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.className = name; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions); + return new RouterMethodVisitor(Opcodes.ASM9, visitor) { + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + boolean cleanupTarget = false; + if (opcode == Opcodes.INVOKEVIRTUAL) { + String ownerClass = owner.replace('/', '.'); + ControllerClass ownerController = controllers.get(ownerClass); + String key = name + "/" + descriptor; + if (ownerController != null && ownerController.methods.get(key) != null) { + /* + * We turn this.method(…) calls into (static) __redirect$method(…) calls + */ + name = "__redirect$" + name; + // turn it into a static call and ignore the target until after the call + opcode = Opcodes.INVOKESTATIC; + cleanupTarget = true; + } else if (owner.equals(className) + && name.equals("redirect") + && descriptor.equals("(Ljava/lang/Class;)Lio/quarkus/vixen/Controller;")) { + /* + * We replace this.redirect(Class) calls with null on the stack + */ + super.visitInsn(Opcodes.POP); // get rid of Class param + super.visitInsn(Opcodes.POP); // get rid of this target + super.visitInsn(Opcodes.ACONST_NULL); // insert null target for next static call + // do not call the method + return; + } + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + if (cleanupTarget) { + /* + * If we've replaced a virtual call with a static call, we need to remove the extraneous target + * instance from the stack, which is sitting behind the call's return value (if any) + */ + if (!descriptor.endsWith(")V")) { + // we have a return value to get around + super.visitInsn(Opcodes.SWAP); // [null, ret] -> [ret, null] + } + super.visitInsn(Opcodes.POP); // get rid of null target + } + } + }; + } + + @Override + public void visitEnd() { + for (ControllerMethod method : controller.methods.values()) { + makeUriMethod(method); + makeUriVarargsMethod(method); + makeRedirectMethod(method); + } + super.visitEnd(); + } + + private void makeUriVarargsMethod(ControllerMethod method) { + // add descriptor hash to method name since signature doesn't include parameters + MethodVisitor visitor = super.visitMethod( + Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_VARARGS, + uriVarargsName(method.name, method.descriptor), "(Z[Ljava/lang/Object;)Ljava/net/URI;", null, null); + + visitor.visitVarInsn(Opcodes.ILOAD, 0); + + // FIXME: ignore non-path/query params + int index = 0; + for (Type parameterType : method.parameters) { + // varargs.length > paramIndex ? (cast)varargs[paramIndex] : null-value + // load the varargs + visitor.visitVarInsn(Opcodes.ALOAD, 1); + // check the varargs length + visitor.visitInsn(Opcodes.ARRAYLENGTH); + Label end = new Label(); + Label elseBranch = new Label(); + // if we don't have enough varargs, jump to else + visitor.visitIntInsn(Opcodes.BIPUSH, index); + visitor.visitJumpInsn(Opcodes.IF_ICMPLE, elseBranch); + // load the varargs value + visitor.visitVarInsn(Opcodes.ALOAD, 1); + visitor.visitIntInsn(Opcodes.BIPUSH, index); + visitor.visitInsn(Opcodes.AALOAD); + if (parameterType.kind() == Kind.PRIMITIVE) { + // this produces the CHECKCAST and unbox call + AsmUtil.unboxIfRequired(visitor, parameterType); + } else { + visitor.visitTypeInsn(Opcodes.CHECKCAST, parameterType.name().toString('/')); + } + visitor.visitJumpInsn(Opcodes.GOTO, end); + visitor.visitLabel(elseBranch); + // default value + if (parameterType.kind() == Kind.PRIMITIVE) { + switch (parameterType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + visitor.visitInsn(Opcodes.ICONST_0); + break; + case DOUBLE: + visitor.visitInsn(Opcodes.DCONST_0); + break; + case FLOAT: + visitor.visitInsn(Opcodes.FCONST_0); + break; + case LONG: + visitor.visitInsn(Opcodes.LCONST_0); + break; + } + } else { + visitor.visitInsn(Opcodes.ACONST_NULL); + } + visitor.visitLabel(end); + // this is a varargs index, not a parameter index + index++; + } + + visitor.visitMethodInsn(Opcodes.INVOKESTATIC, className, "__uri$" + method.name, uriDescriptor(method), false); + visitor.visitInsn(Opcodes.ARETURN); + visitor.visitMaxs(0, 0); + visitor.visitEnd(); + } + + static String uriVarargsName(String name, String descriptor) { + return "__urivarargs$" + name + "$" + HashUtil.sha1(descriptor); + } + + private void makeRedirectMethod(ControllerMethod method) { + // same signature makes it easier for callers + MethodVisitor visitor = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, + "__redirect$" + method.name, method.descriptor, null, null); + // redirect(uri(false, param1, param2)) + visitor.visitInsn(Opcodes.ICONST_0); + int index = 0; + for (Type parameterType : method.parameters) { + visitor.visitVarInsn(AsmUtil.getLoadOpcode(parameterType), index); + index += AsmUtil.getParameterSize(parameterType); + } + visitor.visitMethodInsn(Opcodes.INVOKESTATIC, className, "__uri$" + method.name, uriDescriptor(method), false); + visitor.visitMethodInsn(Opcodes.INVOKESTATIC, "io/quarkus/vixen/Controller", "seeOther", + "(Ljava/net/URI;)Ljavax/ws/rs/core/Response;", false); + visitor.visitInsn(Opcodes.POP); + + int lastParen = method.descriptor.lastIndexOf(')'); + String returnDescriptor = method.descriptor.substring(lastParen + 1); + int returnInstruction = AsmUtil.getReturnInstruction(returnDescriptor); + switch (returnInstruction) { + case Opcodes.RETURN: + break; + case Opcodes.ARETURN: + visitor.visitInsn(Opcodes.ACONST_NULL); + break; + case Opcodes.DRETURN: + visitor.visitInsn(Opcodes.DCONST_0); + break; + case Opcodes.FRETURN: + visitor.visitInsn(Opcodes.FCONST_0); + break; + case Opcodes.IRETURN: + visitor.visitInsn(Opcodes.ICONST_0); + break; + case Opcodes.LRETURN: + visitor.visitInsn(Opcodes.LCONST_0); + break; + } + visitor.visitInsn(returnInstruction); + visitor.visitMaxs(0, 0); + visitor.visitEnd(); + } + + private String uriDescriptor(ControllerMethod method) { + // same signature but takes extra boolean and returns a String + int lastParen = method.descriptor.lastIndexOf(')'); + return "(Z" + method.descriptor.substring(1, lastParen + 1) + "Ljava/net/URI;"; + } + + private void makeUriMethod(ControllerMethod method) { + String descriptor = uriDescriptor(method); + // Figure out the uriBuilder var index + int uriBuilderIndex = 1; + for (Type parameterType : method.parameters) { + uriBuilderIndex += AsmUtil.getParameterSize(parameterType); + } + MethodVisitor visitor = super.visitMethod(Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, + "__uri$" + method.name, descriptor, null, null); + // UriBuilder uri = Router.getUriBuilder(absolute) + visitor.visitVarInsn(Opcodes.ILOAD, 0); + visitor.visitMethodInsn(Opcodes.INVOKESTATIC, "io/quarkus/vixen/router/Router", "getUriBuilder", + "(Z)Ljavax/ws/rs/core/UriBuilder;", false); + visitor.visitVarInsn(Opcodes.ASTORE, uriBuilderIndex); + for (UriPart part : method.parts) { + if (part instanceof StaticUriPart) { + // uri.path("Users"); + visitor.visitVarInsn(Opcodes.ALOAD, uriBuilderIndex); + visitor.visitLdcInsn(((StaticUriPart) part).part); + visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/ws/rs/core/UriBuilder", "path", + "(Ljava/lang/String;)Ljavax/ws/rs/core/UriBuilder;", false); + visitor.visitInsn(Opcodes.POP); + } else if (part instanceof PathParamUriPart) { + PathParamUriPart pathPart = (PathParamUriPart) part; + if (!pathPart.declared) { + // uri.path("{param}"); + visitor.visitVarInsn(Opcodes.ALOAD, uriBuilderIndex); + visitor.visitLdcInsn("{" + pathPart.name + "}"); + visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/ws/rs/core/UriBuilder", "path", + "(Ljava/lang/String;)Ljavax/ws/rs/core/UriBuilder;", false); + visitor.visitInsn(Opcodes.POP); + } + // uri.resolveTemplate("userName", userName); + visitor.visitVarInsn(Opcodes.ALOAD, uriBuilderIndex); + visitor.visitLdcInsn(pathPart.name); + Type paramType = method.parameters.get(pathPart.paramIndex); + visitor.visitVarInsn(AsmUtil.getLoadOpcode(paramType), pathPart.asmParamIndex); + AsmUtil.boxIfRequired(visitor, paramType); + visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/ws/rs/core/UriBuilder", "resolveTemplate", + "(Ljava/lang/String;Ljava/lang/Object;)Ljavax/ws/rs/core/UriBuilder;", false); + visitor.visitInsn(Opcodes.POP); + } else if (part instanceof QueryParamUriPart) { + /* + * if(queryParam != null){ + * Object[] params = new Object[1]; + * params[0] = queryParam; + * uri.queryParam("queryParam", params); + * } + */ + QueryParamUriPart queryPart = (QueryParamUriPart) part; + Type paramType = method.parameters.get(queryPart.paramIndex); + Label end = new Label(); + if (paramType.kind() != Kind.PRIMITIVE) { + visitor.visitVarInsn(Opcodes.ALOAD, queryPart.asmParamIndex); + visitor.visitJumpInsn(Opcodes.IFNULL, end); + } + visitor.visitVarInsn(Opcodes.ALOAD, uriBuilderIndex); + visitor.visitLdcInsn(queryPart.name); + visitor.visitInsn(Opcodes.ICONST_1); + visitor.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + visitor.visitInsn(Opcodes.DUP); + visitor.visitInsn(Opcodes.ICONST_0); + visitor.visitVarInsn(AsmUtil.getLoadOpcode(paramType), queryPart.asmParamIndex); + AsmUtil.boxIfRequired(visitor, paramType); + visitor.visitInsn(Opcodes.AASTORE); + visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/ws/rs/core/UriBuilder", "queryParam", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljavax/ws/rs/core/UriBuilder;", false); + visitor.visitInsn(Opcodes.POP); + if (paramType.kind() != Kind.PRIMITIVE) { + visitor.visitLabel(end); + } + } + } + /* + * return uri.build(); + */ + visitor.visitVarInsn(Opcodes.ALOAD, uriBuilderIndex); + visitor.visitInsn(Opcodes.ICONST_0); + visitor.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); + visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "javax/ws/rs/core/UriBuilder", "build", + "([Ljava/lang/Object;)Ljava/net/URI;", false); + visitor.visitInsn(Opcodes.ARETURN); + visitor.visitMaxs(0, 0); + visitor.visitEnd(); + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java new file mode 100644 index 00000000..f2cd593a --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java @@ -0,0 +1,505 @@ +package io.quarkiverse.renarde.deployment; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.enterprise.event.Observes; +import javax.inject.Singleton; +import javax.transaction.Transactional; +import javax.ws.rs.Priorities; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.MethodInfo; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; +import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer.TransformationContext; +import org.jboss.resteasy.reactive.common.processor.transformation.Transformation; +import org.jboss.resteasy.reactive.common.util.URLUtils; + +import io.quarkiverse.renarde.Controller; +import io.quarkiverse.renarde.deployment.ControllerVisitor.ControllerClass; +import io.quarkiverse.renarde.deployment.ControllerVisitor.ControllerMethod; +import io.quarkiverse.renarde.deployment.ControllerVisitor.UriPart; +import io.quarkiverse.renarde.router.Router; +import io.quarkiverse.renarde.router.RouterMethod; +import io.quarkiverse.renarde.util.AuthenticationFailedExceptionMapper; +import io.quarkiverse.renarde.util.CRSF; +import io.quarkiverse.renarde.util.Filters; +import io.quarkiverse.renarde.util.Flash; +import io.quarkiverse.renarde.util.JavaExtensions; +import io.quarkiverse.renarde.util.MyParamConverters; +import io.quarkiverse.renarde.util.MyValidationInterceptor; +import io.quarkiverse.renarde.util.QuteResolvers; +import io.quarkiverse.renarde.util.RedirectExceptionMapper; +import io.quarkiverse.renarde.util.RenderArgs; +import io.quarkiverse.renarde.util.Validation; +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.ExcludedTypeBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; +import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.util.AsmUtil; +import io.quarkus.deployment.util.IoUtil; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FunctionCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyReactiveEndPointValidationInterceptor; +import io.quarkus.qute.Variant; +import io.quarkus.qute.deployment.TemplateTagBuildItem; +import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem; +import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem; +import io.quarkus.resteasy.reactive.spi.ParamConverterBuildItem; +import io.quarkus.runtime.StartupEvent; + +public class RenardeProcessor { + + private static final Logger logger = Logger.getLogger(RenardeProcessor.class); + + public static final DotName DOTNAME_CONTROLLER = DotName.createSimple(Controller.class.getName()); + public static final DotName DOTNAME_ROUTER = DotName.createSimple(Router.class.getName()); + public static final DotName DOTNAME_UNREMOVABLE = DotName.createSimple(Unremovable.class.getName()); + public static final DotName DOTNAME_TRANSACTIONAL = DotName.createSimple(Transactional.class.getName()); + + private static final String FEATURE = "renarde"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + void removeHibernateLogging(LaunchModeBuildItem launchMode, + BuildProducer logFilters) { + if (launchMode.getLaunchMode().isDevOrTest()) { + // FIXME: this is too broad, but waits for https://github.com/quarkusio/quarkus/issues/16204 to be fixed + logFilters + .produce(new LogCleanupFilterBuildItem("org.hibernate.engine.jdbc.spi.SqlExceptionHelper", + "SQL Warning Code: 0, SQLState: 00000", + "relation \"", + "table \"", + "sequence \"")); + } + } + + @BuildStep + void setupJWT(LaunchModeBuildItem launchMode, Capabilities capabilities, + BuildProducer runtimeConfigurationBuildItem) + throws IOException, NoSuchAlgorithmException { + if (launchMode.getLaunchMode().isDevOrTest() + && capabilities.isPresent(Capability.JWT)) { + // make sure we have minimal config + final Config config = ConfigProvider.getConfig(); + + // PRIVATE + Optional decryptKeyLocationOpt = config.getOptionalValue("mp.jwt.decrypt.key.location", String.class); + Optional signKeyLocationOpt = config.getOptionalValue("smallrye.jwt.sign.key.location", String.class); + // PUBLIC + Optional verifyKeyLocationOpt = config.getOptionalValue("mp.jwt.verify.publickey.location", String.class); + Optional encryptKeyLocationOpt = config.getOptionalValue("smallrye.jwt.encrypt.key.location", String.class); + if (!decryptKeyLocationOpt.isPresent() + && !signKeyLocationOpt.isPresent() + && !verifyKeyLocationOpt.isPresent() + && !encryptKeyLocationOpt.isPresent()) { + // FIXME: folder + File privateKey = new File("target/classes/dev.privateKey.pem"); + File publicKey = new File("target/classes/dev.publicKey.pem"); + if (!privateKey.exists() && !publicKey.exists()) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + + logger.infof("Generating private/public keys for DEV/TEST in %s and %s", privateKey, publicKey); + try (FileWriter fw = new FileWriter(privateKey)) { + fw.append("-----BEGIN PRIVATE KEY-----\n"); + fw.append(Base64.getMimeEncoder().encodeToString(kp.getPrivate().getEncoded())); + fw.append("\n"); + fw.append("-----END PRIVATE KEY-----\n"); + } + try (FileWriter fw = new FileWriter(publicKey)) { + fw.append("-----BEGIN PUBLIC KEY-----\n"); + fw.append(Base64.getMimeEncoder().encodeToString(kp.getPublic().getEncoded())); + fw.append("\n"); + fw.append("-----END PUBLIC KEY-----\n"); + } + } + runtimeConfigurationBuildItem + .produce(new RunTimeConfigurationDefaultBuildItem("mp.jwt.decrypt.key.location", privateKey.getName())); + runtimeConfigurationBuildItem.produce( + new RunTimeConfigurationDefaultBuildItem("smallrye.jwt.sign.key.location", privateKey.getName())); + runtimeConfigurationBuildItem.produce( + new RunTimeConfigurationDefaultBuildItem("mp.jwt.verify.publickey.location", publicKey.getName())); + runtimeConfigurationBuildItem.produce( + new RunTimeConfigurationDefaultBuildItem("smallrye.jwt.encrypt.key.location", publicKey.getName())); + } + } + } + + @BuildStep + void produceBeans(BuildProducer additionalBeanBuildItems, + BuildProducer paramConverterBuildItems, + BuildProducer additionalIndexedClassesBuildItems) { + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(Filters.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(QuteResolvers.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(CRSF.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(Flash.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(RenderArgs.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(Validation.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(JavaExtensions.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(MyValidationInterceptor.class)); + additionalBeanBuildItems.produce(AdditionalBeanBuildItem.unremovableOf(AuthenticationFailedExceptionMapper.class)); + + paramConverterBuildItems.produce(new ParamConverterBuildItem(MyParamConverters.class.getName(), Priorities.USER, true)); + + additionalIndexedClassesBuildItems.produce( + new AdditionalIndexedClassesBuildItem(Filters.class.getName(), RedirectExceptionMapper.class.getName())); + + } + + @BuildStep + ExcludedTypeBuildItem removeOriginalValidatorInterceptor() { + return new ExcludedTypeBuildItem(ResteasyReactiveEndPointValidationInterceptor.class.getName()); + } + + @BuildStep + void produceTags(BuildProducer tags) { + tags.produce(addTag("authenticityToken")); + tags.produce(addTag("error")); + tags.produce(addTag("form")); + tags.produce(addTag("gravatar")); + tags.produce(addTag("ifError")); + } + + private TemplateTagBuildItem addTag(String tagName) { + URL resource = QuteResolvers.class.getClassLoader().getResource("/templates/tags/" + tagName + ".html"); + byte[] bytes; + try { + bytes = IoUtil.readBytes(resource.openStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + return new TemplateTagBuildItem(tagName, new String(bytes, StandardCharsets.UTF_8), + Variant.forContentType(Variant.TEXT_HTML)); + } + + @BuildStep + void collectControllers(ApplicationIndexBuildItem indexBuildItem, + BuildProducer additionalResourceClassBuildItems, + BuildProducer annotationTransformerBuildItems, + BuildProducer arcTransformers, + BuildProducer unremovableBeans, + BuildProducer bytecodeTransformers, + BuildProducer generatedBeans) { + Set controllers = new HashSet<>(); + Map methodsByClass = new HashMap<>(); + for (ClassInfo controllerInfo : indexBuildItem.getIndex().getAllKnownSubclasses(DOTNAME_CONTROLLER)) { + // skip abstract classes + if (Modifier.isAbstract(controllerInfo.flags())) + continue; + additionalResourceClassBuildItems.produce(new AdditionalResourceClassBuildItem(controllerInfo, "")); + controllers.add(controllerInfo.name()); + unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(controllerInfo.name())); + methodsByClass.put(controllerInfo.name().toString(), scanController(controllerInfo)); + } + for (DotName controller : controllers) { + bytecodeTransformers + .produce(new BytecodeTransformerBuildItem(controller.toString(), new ControllerVisitor(methodsByClass))); + } + for (ClassInfo routerUserInfo : indexBuildItem.getIndex().getKnownUsers(DOTNAME_ROUTER)) { + if (!controllers.contains(routerUserInfo.name())) { + bytecodeTransformers + .produce(new BytecodeTransformerBuildItem(routerUserInfo.name().toString(), new RouterUserVisitor())); + } + } + generateRouterInit(generatedBeans, methodsByClass); + annotationTransformerBuildItems.produce(new AnnotationsTransformerBuildItem( + AnnotationsTransformer.builder().appliesTo(Kind.METHOD) + .transform(ti -> transformControllerMethod(ti, controllers)))); + annotationTransformerBuildItems.produce(new AnnotationsTransformerBuildItem( + AnnotationsTransformer.builder().appliesTo(Kind.CLASS).transform(ti -> transformController(ti, controllers)))); + + arcTransformers.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem( + new io.quarkus.arc.processor.AnnotationsTransformer() { + @Override + public void transform(TransformationContext transformationContext) { + if (transformationContext.isClass() + && controllers.contains(transformationContext.getTarget().asClass().name())) { + // FIXME: probably don't add a scope annotation if it has one already? + transformationContext.transform().add(ResteasyReactiveDotNames.REQUEST_SCOPED) + .done(); + } + if (transformationContext.isMethod()) { + MethodInfo method = transformationContext.getTarget().asMethod(); + if (controllers.contains(method.declaringClass().name()) + && !method.hasAnnotation(DOTNAME_TRANSACTIONAL) + && (method.hasAnnotation(ResteasyReactiveDotNames.POST) + || method.hasAnnotation(ResteasyReactiveDotNames.PUT) + || method.hasAnnotation(ResteasyReactiveDotNames.DELETE))) { + transformationContext.transform().add(DOTNAME_TRANSACTIONAL) + .done(); + } + } + } + })); + + } + + private void generateRouterInit(BuildProducer generatedBeans, + Map methodsByClass) { + ClassOutput beansClassOutput = new GeneratedBeanGizmoAdaptor(generatedBeans); + try (ClassCreator beanClassCreator = ClassCreator.builder().classOutput(beansClassOutput) + .className("__VixenInit") + .build()) { + beanClassCreator.addAnnotation(Singleton.class); + + try (MethodCreator methodCreator = beanClassCreator.getMethodCreator("init", void.class, StartupEvent.class)) { + methodCreator.getParameterAnnotations(0).addAnnotation(Observes.class); + for (ControllerClass controllerClass : methodsByClass.values()) { + String simpleControllerName = controllerClass.className; + int lastDot = simpleControllerName.lastIndexOf('.'); + if (lastDot != -1) { + simpleControllerName = simpleControllerName.substring(lastDot + 1); + } + for (ControllerMethod method : controllerClass.methods.values()) { + FunctionCreator function = methodCreator.createFunction(RouterMethod.class); + String uriMethodName = ControllerVisitor.ControllerClassVisitor.uriVarargsName(method.name, + method.descriptor); + try (BytecodeCreator functionBytecode = function.getBytecode()) { + functionBytecode.returnValue(functionBytecode.invokeStaticMethod( + MethodDescriptor.ofMethod(controllerClass.className, uriMethodName, + URI.class, boolean.class, Object[].class), + functionBytecode.getMethodParam(0), + functionBytecode.getMethodParam(1))); + } + methodCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(Router.class, "registerRoute", void.class, String.class, + RouterMethod.class), + methodCreator.load(simpleControllerName + "." + method.name), + function.getInstance()); + } + } + methodCreator.returnValue(null); + } + } + } + + private ControllerVisitor.ControllerClass scanController(ClassInfo controllerInfo) { + Map methods = new HashMap<>(); + for (MethodInfo method : controllerInfo.methods()) { + if (!isControllerMethod(method)) + continue; + List parts = new ArrayList<>(); + + AnnotationInstance classPath = method.declaringClass().classAnnotation(ResteasyReactiveDotNames.PATH); + String className = method.declaringClass().simpleName(); + String classPathValue = classPath != null ? classPath.value().value().toString() : className; + + AnnotationInstance methodPath = method.annotation(ResteasyReactiveDotNames.PATH); + String methodPathValue = methodPath != null ? methodPath.value().value().toString() : method.name(); + + String path = classPathValue + (methodPathValue.startsWith("/") ? "" : "/") + methodPathValue; + + // path annotations + if (!methodPathValue.startsWith("/")) { + parts.add(new ControllerVisitor.StaticUriPart(classPathValue)); + } + parts.add(new ControllerVisitor.StaticUriPart(methodPathValue)); + + // collect declared path params + Set pathParameters = new HashSet<>(); + URLUtils.parsePathParameters(path, pathParameters); + + // collect param annotations + Map[] parameterAnnotations = getParameterAnnotations(method); + + // look for undeclared path params + for (int paramIndex = 0, asmParamIndex = 1; paramIndex < method.parameters().size(); ++paramIndex) { + String paramName = method.parameterName(paramIndex); + AnnotationInstance pathParam = parameterAnnotations[paramIndex].get(ResteasyReactiveDotNames.PATH_PARAM); + if (pathParam != null) { + String name = (String) pathParam.value().value(); + parts.add(new ControllerVisitor.PathParamUriPart(name, paramIndex, asmParamIndex, + pathParameters.contains(paramName))); + } + AnnotationInstance restPathParam = parameterAnnotations[paramIndex] + .get(ResteasyReactiveDotNames.REST_PATH_PARAM); + if (restPathParam != null) { + String name = restPathParam.value() != null ? (String) restPathParam.value().value() : ""; + if (name != null) + name = paramName; + parts.add(new ControllerVisitor.PathParamUriPart(name, paramIndex, asmParamIndex, + pathParameters.contains(paramName))); + } + if (pathParameters.contains(paramName)) { + parts.add(new ControllerVisitor.PathParamUriPart(paramName, paramIndex, asmParamIndex, true)); + } + AnnotationInstance queryParam = parameterAnnotations[paramIndex].get(ResteasyReactiveDotNames.QUERY_PARAM); + if (queryParam != null) { + String name = (String) queryParam.value().value(); + parts.add(new ControllerVisitor.QueryParamUriPart(name, paramIndex, asmParamIndex)); + } + AnnotationInstance restQueryParam = parameterAnnotations[paramIndex] + .get(ResteasyReactiveDotNames.REST_QUERY_PARAM); + if (restQueryParam != null) { + String name = restQueryParam.value() != null ? (String) restQueryParam.value().value() : ""; + if (name.isEmpty()) + name = paramName; + parts.add(new ControllerVisitor.QueryParamUriPart(name, paramIndex, asmParamIndex)); + } + asmParamIndex += AsmUtil.getParameterSize(method.parameters().get(paramIndex)); + } + + String descriptor = AsmUtil.getDescriptor(method, v -> v); + String key = method.name() + "/" + descriptor; + methods.put(key, new ControllerMethod(method.name(), descriptor, parts, + method.parameters())); + } + return new ControllerVisitor.ControllerClass(controllerInfo.name().toString(), methods); + } + + private boolean isControllerMethod(MethodInfo method) { + return !Modifier.isAbstract(method.flags()) + && Modifier.isPublic(method.flags()) + && !Modifier.isNative(method.flags()) + && !Modifier.isStatic(method.flags()) + && !method.name().equals("") + && !method.name().equals(""); + } + + private void transformController(TransformationContext ti, Set controllers) { + ClassInfo klass = ti.getTarget().asClass(); + if (controllers.contains(klass.name())) { + if (klass.classAnnotation(ResteasyReactiveDotNames.PATH) == null) { + ti.transform().add(ResteasyReactiveDotNames.PATH, AnnotationValue.createStringValue("value", "")).done(); + } + } + } + + private void transformControllerMethod(TransformationContext ti, Set controllers) { + MethodInfo method = ti.getTarget().asMethod(); + if (!isControllerMethod(method)) { + return; + } + if (controllers.contains(method.declaringClass().name())) { + /* + * @Path("foo") class Class { @Path("bar") method(); } -> no change + * + * @Path("foo") class Class { method(); } -> @Path("foo") class Class { @Path("method") method(); } + * class Class { @Path("/bar") method(); } -> @Path("") class Class { @Path("/bar") method(); } + * class Class { @Path("bar") method(); } -> @Path("") class Class { @Path("Class/bar") method(); } + * class Class { method(); } -> @Path("") class Class { @Path("Class/method") method(); } + */ + // class @Path or class name first + AnnotationInstance classPath = method.declaringClass().classAnnotation(ResteasyReactiveDotNames.PATH); + String className = method.declaringClass().simpleName(); + String classPathValue = classPath != null ? classPath.value().value().toString() : className; + + AnnotationInstance methodPath = method.annotation(ResteasyReactiveDotNames.PATH); + String methodPathValue = methodPath != null ? methodPath.value().value().toString() : method.name(); + + String path = classPathValue + (methodPathValue.startsWith("/") ? "" : "/") + methodPathValue; + + String newMethodPathValue = methodPathValue; + boolean setMethodPath = false; + + if (classPath != null && methodPath == null) { + // can remain the method name + setMethodPath = true; + } else if (classPath == null) { + if (methodPath == null || !methodPathValue.startsWith("/")) { + // prepend the class name + setMethodPath = true; + newMethodPathValue = className + "/" + methodPathValue; + } + } + + // collect declared path params + Set pathParameters = new HashSet<>(); + URLUtils.parsePathParameters(path, pathParameters); + + // collect param annotations + Map[] parameterAnnotations = getParameterAnnotations(method); + + // look for undeclared path params + for (int paramPos = 0; paramPos < method.parameters().size(); ++paramPos) { + if ((parameterAnnotations[paramPos].get(ResteasyReactiveDotNames.PATH_PARAM) != null + || parameterAnnotations[paramPos].get(ResteasyReactiveDotNames.REST_PATH_PARAM) != null) + && !pathParameters.contains(method.parameterName(paramPos))) { + // add them to the method path + setMethodPath = true; + newMethodPathValue += "/{" + method.parameterName(paramPos) + "}"; + } + } + + if (setMethodPath) { + Transformation transform = ti.transform(); + if (methodPathValue != null) + transform.remove(ai -> ai.name().equals(ResteasyReactiveDotNames.PATH)); + transform.add(ResteasyReactiveDotNames.PATH, AnnotationValue.createStringValue("value", newMethodPathValue)) + .done(); + } + + // FIXME: doesn't work for custom Http methods, whatever + if (!method.hasAnnotation(ResteasyReactiveDotNames.GET) + && !method.hasAnnotation(ResteasyReactiveDotNames.PUT) + && !method.hasAnnotation(ResteasyReactiveDotNames.POST) + && !method.hasAnnotation(ResteasyReactiveDotNames.HEAD) + && !method.hasAnnotation(ResteasyReactiveDotNames.OPTIONS) + && !method.hasAnnotation(ResteasyReactiveDotNames.DELETE)) { + ti.transform().add(ResteasyReactiveDotNames.GET).done(); + } + } + } + + private Map[] getParameterAnnotations(MethodInfo method) { + // collect param annotations + Map[] parameterAnnotations = new Map[method.parameters().size()]; + for (int paramPos = 0; paramPos < method.parameters().size(); ++paramPos) { + parameterAnnotations[paramPos] = new HashMap<>(); + } + for (AnnotationInstance i : method.annotations()) { + if (i.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + parameterAnnotations[i.target().asMethodParameter().position()].put(i.name(), i); + } + } + return parameterAnnotations; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterMethodVisitor.java b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterMethodVisitor.java new file mode 100644 index 00000000..fb54f5ce --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterMethodVisitor.java @@ -0,0 +1,43 @@ +package io.quarkiverse.renarde.deployment; + +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class RouterMethodVisitor extends MethodVisitor { + Handle targetIndyDescriptor; + + public RouterMethodVisitor(int api, MethodVisitor methodVisitor) { + super(api, methodVisitor); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, + Object... bootstrapMethodArguments) { + if (name.equals("method") + && descriptor.startsWith("()Lio/quarkus/vixen/router/Method") + && bootstrapMethodArguments.length > 2) { + Handle targetDescriptor = (Handle) bootstrapMethodArguments[1]; + // FIXME: extract to all classes using Router, not just controllers + targetIndyDescriptor = targetDescriptor; + // replace by the first boolean param to the uri method: false + super.visitInsn(Opcodes.ICONST_0); + return; + } + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (opcode == Opcodes.INVOKESTATIC) { + if (owner.equals("io/quarkus/vixen/router/Router") && name.equals("getURI")) { + // replace by a call to the uri/varargs method + owner = targetIndyDescriptor.getOwner(); + name = ControllerVisitor.ControllerClassVisitor.uriVarargsName(targetIndyDescriptor.getName(), + targetIndyDescriptor.getDesc()); + descriptor = "(Z[Ljava/lang/Object;)Ljava/net/URI;"; + } + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } +} diff --git a/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterUserVisitor.java b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterUserVisitor.java new file mode 100644 index 00000000..4339e7bf --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RouterUserVisitor.java @@ -0,0 +1,28 @@ +package io.quarkiverse.renarde.deployment; + +import java.util.function.BiFunction; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class RouterUserVisitor implements BiFunction { + + @Override + public ClassVisitor apply(String className, ClassVisitor visitor) { + return new RouterUserClassVisitor(visitor); + } + + public static class RouterUserClassVisitor extends ClassVisitor { + + public RouterUserClassVisitor(ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions); + return new RouterMethodVisitor(Opcodes.ASM9, visitor); + } + } +} diff --git a/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeDevModeTest.java b/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeDevModeTest.java new file mode 100644 index 00000000..4b7c4a3e --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeDevModeTest.java @@ -0,0 +1,23 @@ +package io.quarkiverse.renarde.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class RenardeDevModeTest { + + // Start hot reload (DevMode) test with your extension loaded + @RegisterExtension + static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnDevModeTest() { + // Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information + Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName()); + } +} diff --git a/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeTest.java b/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeTest.java new file mode 100644 index 00000000..482a25aa --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/renarde/test/RenardeTest.java @@ -0,0 +1,23 @@ +package io.quarkiverse.renarde.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RenardeTest { + + // Start unit test with your extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnUnitTest() { + // Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information + Assertions.assertTrue(true, "Add some assertions to " + getClass().getName()); + } +} diff --git a/docs/antora.yml b/docs/antora.yml new file mode 100644 index 00000000..d2e5399b --- /dev/null +++ b/docs/antora.yml @@ -0,0 +1,5 @@ +name: quarkus-renarde +title: Quarkus - Renarde +version: dev +nav: + - modules/ROOT/nav.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc new file mode 100644 index 00000000..71f9640f --- /dev/null +++ b/docs/modules/ROOT/nav.adoc @@ -0,0 +1 @@ +* xref:index.adoc[Quarkus - Renarde] diff --git a/docs/modules/ROOT/pages/config.adoc b/docs/modules/ROOT/pages/config.adoc new file mode 100644 index 00000000..926cca39 --- /dev/null +++ b/docs/modules/ROOT/pages/config.adoc @@ -0,0 +1,112 @@ +// +// This content is generated using mvn compile and copied manually to here +// +[.configuration-legend] +icon:lock[title=Fixed at build time] Configuration property fixed at build time - All other configuration properties are overridable at runtime +[.configuration-reference.searchable, cols="80,.^10,.^10"] +|=== + +h|[[quarkus-freemarker_configuration]]link:#quarkus-freemarker_configuration[Configuration property] + +h|Type +h|Default + +a|icon:lock[title=Fixed at build time] [[quarkus-freemarker_quarkus.freemarker.resource-paths]]`link:#quarkus-freemarker_quarkus.freemarker.resource-paths[quarkus.freemarker.resource-paths]` + +[.description] +-- +Comma-separated list of absolute resource paths to scan recursively for templates. All tree folder from 'resource-paths' will be added as a resource. Unprefixed locations or locations starting with classpath will be processed in the same way. +--|list of string +|`freemarker/templates` + + +a| [[quarkus-freemarker_quarkus.freemarker.file-paths]]`link:#quarkus-freemarker_quarkus.freemarker.file-paths[quarkus.freemarker.file-paths]` + +[.description] +-- +Comma-separated of file system paths where freemarker templates are located +--|list of string +| + + +a| [[quarkus-freemarker_quarkus.freemarker.default-encoding]]`link:#quarkus-freemarker_quarkus.freemarker.default-encoding[quarkus.freemarker.default-encoding]` + +[.description] +-- +Set the preferred charset template files are stored in. +--|string +| + + +a| [[quarkus-freemarker_quarkus.freemarker.template-exception-handler]]`link:#quarkus-freemarker_quarkus.freemarker.template-exception-handler[quarkus.freemarker.template-exception-handler]` + +[.description] +-- +Sets how errors will appear. rethrow, debug, html-debug, ignore. +--|string +| + + +a| [[quarkus-freemarker_quarkus.freemarker.log-template-exceptions]]`link:#quarkus-freemarker_quarkus.freemarker.log-template-exceptions[quarkus.freemarker.log-template-exceptions]` + +[.description] +-- +If false, don't log exceptions inside FreeMarker that it will be thrown at you anyway. +--|boolean +| + + +a| [[quarkus-freemarker_quarkus.freemarker.wrap-unchecked-exceptions]]`link:#quarkus-freemarker_quarkus.freemarker.wrap-unchecked-exceptions[quarkus.freemarker.wrap-unchecked-exceptions]` + +[.description] +-- +Wrap unchecked exceptions thrown during template processing into TemplateException-s. +--|boolean +| + + +a| [[quarkus-freemarker_quarkus.freemarker.fallback-on-null-loop-variable]]`link:#quarkus-freemarker_quarkus.freemarker.fallback-on-null-loop-variable[quarkus.freemarker.fallback-on-null-loop-variable]` + +[.description] +-- +If false, do not fall back to higher scopes when reading a null loop variable. +--|boolean +| + + +a| [[quarkus-freemarker_quarkus.freemarker.boolean-format]]`link:#quarkus-freemarker_quarkus.freemarker.boolean-format[quarkus.freemarker.boolean-format]` + +[.description] +-- +The string value for the boolean `true` and `false` values, usually intended for human consumption (not for a computer language), separated with comma. +--|string +| + + +a| [[quarkus-freemarker_quarkus.freemarker.number-format]]`link:#quarkus-freemarker_quarkus.freemarker.number-format[quarkus.freemarker.number-format]` + +[.description] +-- +Sets the default number format used to convert numbers to strings. +--|string +| + + +a| [[quarkus-freemarker_quarkus.freemarker.object-wrapper-expose-fields]]`link:#quarkus-freemarker_quarkus.freemarker.object-wrapper-expose-fields[quarkus.freemarker.object-wrapper-expose-fields]` + +[.description] +-- +If true, the object wrapper will be configured to expose fields. +--|boolean +| + + +a|icon:lock[title=Fixed at build time] [[quarkus-freemarker_quarkus.freemarker.directive-directive]]`link:#quarkus-freemarker_quarkus.freemarker.directive-directive[quarkus.freemarker.directive]` + +[.description] +-- +List of directives to register with format name=classname +--|`Map` +| + +|=== diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 00000000..8cf5cff8 --- /dev/null +++ b/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1,1259 @@ += Quarkus - Renarde +:extension-status: preview + +Describe what the extension does here. + +== Installation + +If you want to use this extension, you need to add the `io.quarkiverse.renarde:quarkus-renarde` extension first. +In your `pom.xml` file, add: + +[source,xml] +---- + + io.quarkiverse.renarde + quarkus-renarde + +---- + +[[extension-configuration-reference]] +== Extension Configuration Reference + +include::config.adoc[leveloffset=+1, opts=optional] +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Vixen Web Framework + +include::./attributes.adoc[] +:config-file: application.properties + +Vixen is a server-side Web Framework based on Quarkus, xref:qute-reference.adoc[Qute], +xref:hibernate-orm-panache.adoc[Hibernate] and xref:resteasy-reactive.adoc[RESTEasy Reactive]. + +[source,xml] +---- + + io.quarkus + quarkus-vixen + +---- + +== First: an example + +Let's see how you can quickly build a Web Application with Vixen. Let's start with a _Controller_: + +[source,java] +---- +package rest; + +import javax.ws.rs.Path; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.vixen.Controller; + +public class Application extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(); + } + + @Path("/") + public TemplateInstance index() { + return Templates.index(); + } +} +---- + +A _Controller_ is the logic class that binds URIs to actions and views. They are almost like regular +xref:resteasy-reactive.adoc#declaring-endpoints-uri-mapping[JAX-RS endpoints], +but you opt-in to special magic by extending the `Controller` class, which gives you nice methods, +but also super friendly behaviour. + +In this Controller we declare a Qute template, and map the `/` to it. + +We can then define the main page in `src/main/resources/templates/Application/index.html`: + +[source,html] +---- + + + + Hello, World! + + +---- + +Now if you navigate to your application at http://localhost:8080 you will see `Hello, World!` rendered. + +== Models + +By convention, you can place your model classes in the `model` package, but anywhere else works just as well. We +recommend using xref:hibernate-orm-panache.adoc[Hibernate ORM with Panache]. Here's an example entity for our sample Todo application: + +[source,java] +---- +package model; + +import java.util.Date; +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.ManyToOne; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +public class Todo extends PanacheEntity { + + @ManyToOne + public User owner; + + public String task; + + public boolean done; + + public Date doneDate; + + public static List findByOwner(User user) { + return find("owner = ?1 ORDER BY id", user).list(); + } +} +---- + +== Controllers + +By convention, you can place your controllers in the `rest` package, but anywhere else works just as well. You +have to extend the `Controller` class in order to benefit from extra easy endpoint declarations and reverse-routing, +but that superclass also gives you useful methods. We usually have one controller per model class, so we tend to use +the plural entity name for the corresponding controller: + +[source,java] +---- +package rest; + +import java.util.Date; +import java.util.List; + +import javax.validation.constraints.NotBlank; +import javax.ws.rs.POST; + +import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.RestPath; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import model.Todo; + +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } + + @POST + public void delete(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // delete it + todo.delete(); + // send loving message + flash("message", "Task deleted"); + // redirect to index page + index(); + } + + @POST + public void done(@RestPath Long id) { + // find the Todo + Todo todo = Todo.findById(id); + notFoundIfNull(todo); + // switch its done state + todo.done = !todo.done; + if(todo.done) + todo.doneDate = new Date(); + // send loving message + flash("message", "Task updated"); + // redirect to index page + index(); + } + + @POST + public void add(@NotBlank @RestForm String task) { + // check if there are validation issues + if(validationFailed()) { + // go back to the index page + index(); + } + // create a new Todo + Todo todo = new Todo(); + todo.task = task; + todo.persist(); + // send loving message + flash("message", "Task added"); + // redirect to index page + index(); + } +} +---- + +=== Methods + +Every public method is a valid endpoint. If it has no HTTP method annotation (`@GET`, `@HEAD`, `@POST`, `@PUT`, `@DELETE`) then +it is assumed to be a `@GET` method. + +Most `@GET` methods will typically return a `TemplateInstance` for rendering an HTML server-side template, and should not +modify application state. + +Controller methods annotated with `@POST`, `@PUT` and `@DELETE` will typically return `void` and trigger a redirect to a `@GET` +method after they do their action. This is not mandatory, you can also return a `TemplateInstance` if you want, but it is good form +to use a redirect to avoid involuntary actions when browsers reload the page. Those methods also get an implicit `@Transactional` +annotation so you don't need to add it. + +If your controller is not annotated with `@Path` it will default to a path using the class name. If your controller method is not +annotated with `@Path` it will default to a path using the method name. The exception is if you have a `@Path` annotation on the +method with an absolute path, in which case the class path part will be ignored. Here's a list of example annotations and how they +result: + +[cols="1,1,1"] +|=== +|Class declaration|Method declaration|URI + +|`class Foo` +|`public TemplateInstance bar()` +|`Foo/bar` + +|`@Path("f") class Foo` +|`public TemplateInstance bar()` +|`f/bar` + +|`class Foo` +|`@Path("b") public TemplateInstance bar()` +|`Foo/b` + + +|`@Path("f") class Foo` +|`@Path("b") public TemplateInstance bar()` +|`f/b` + +|`class Foo` +|`@Path("/bar") public TemplateInstance bar()` +|`bar` + +|`@Path("f") class Foo` +|`@Path("/bar") public TemplateInstance bar()` +|`f/bar` + +|=== + +Furthermore, if you specify path parameters that are not present in your path annotations, they will be automatically +appended to your path: + +[source,java] +---- +public class Orders extends Controller { + + // The URI will be Orders/get/{owner}/{id} + public TemplateInstance get(@RestPath String owner, @RestPath Long id) { + } + + // The URI will be /orders/{owner}/{id} + @Path("/orders") + public TemplateInstance otherGet(@RestPath String owner, @RestPath Long id) { + } +} +---- + +== Views + +You can place your xref:qute-reference.adoc[Qute views] in the `src/main/resources/templates` folder, +using the `{className}/{methodName}.html` naming convention. + +Every controller that has views should declare them with a nested static class annotated with `@CheckedTemplate`: + +[source,java] +---- +public class Todos extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(List todos); + } + + public TemplateInstance index() { + // list every todo + List todos = Todo.listAll(); + // render the index template + return Templates.index(todos); + } +} +---- + +Here we're declaring the `Todos/index.html` template, specifying that it takes a `todos` parameter of type +`List` which allows us to validate the template at build-time. + +Templates are written in Qute, and you can also declare imported templates in order to validate them using a +toplevel class, such as the `main.html` template: + +[source,java] +---- +package rest; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; + +@CheckedTemplate +public class Templates { + public static native TemplateInstance main(); +} +---- + +=== Template composition + +Typical web applications will have a main template for their layout and use composition in every method. For example, we +can declare the following main template in `main.html`: + +[source,html] +---- + + + + {#insert title /} + + + {#insert moreStyles /} + + {#insert moreScripts /} + + + {#insert /} + + +---- + +And then use it in our `Todos/index.html` template to list the todo items: + +[source,html] +---- +{#include main.html } +{#title}Todos{/title} + + + + + + + + + + {#for todo in todos} + + + + + {/for} + +
#Task
{todo.id}{todo.task}
+ +{/include} +---- + +=== Standard tags + +[cols="1,1"] +|=== +|Tag|Description + +|xref:qute-reference.adoc#loop_section[for/each] +|Iterate over collections + +|xref:qute-reference.adoc#if_section[if/else] +|Conditional statement + +|xref:qute-reference.adoc#when_section[switch/case] +|Switch statement + +|xref:qute-reference.adoc#with_section[with] +|Adds value members to the local scope + +|xref:qute-reference.adoc#letset_section[let] +|Declare local variables + +|xref:qute-reference.adoc#include_helper[include/insert] +|Template composition + +|=== + +=== User tags + +If you want to declare additional tags in order to be able to repeat them in your templates, simply place them in the +`templates/tags` folder. For example, here is our `user.html` tag: + +[source,html] +---- + +{#if img??} +{#gravatar it.email size=size.or(20) default='mm' /} +{/if} +{it.userName} +---- + +Which allows us to use it in every template: + +[source,html] +---- +{#if inject:user} + {#if inject:user.isAdmin}{/if} + {#user inject:user img=true size=20/} +{/if} +---- + +You can pass parameters to your template with `name=value` pairs, and the first unnamed parameter value becomes available +as the `it` parameter. + +See the xref:qute-reference.adoc#user_tags[Qute documentation] for more information. + +=== Vixen tags + +Vixen comes with a few extra tags to make your life easier: + +[cols="1,1"] +|=== +|Tag|Description + +|`{#authenticityToken/}` +|Generate a hidden HTML form element containing a CRSF token to be matched in the next request. + +|`{#error 'field'/}` +|Inserts the error message for the given field name + +|`{#form uri method='POST' klass='css'}...{/form}` +|Generates an HTML form for the given `URI`, `method` (defaults to `POST`) and optional CSS classes. Includes a CRSF token. + +|`{#gravatar email size='mm'/}` +|Inserts a gravatar image for the given `email`, with optional `size` (defaults to `mm`) + +|`{#ifError 'field'}...{/ifError}` +|Conditional statement executed if there is an error for the given field + +|=== + +=== Extension methods + +If you need additional methods to be registered to be used on your template expressions, you can declare static methods in +a class annotated with `@TemplateExtension`: + +[source,java] +---- +package util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class JavaExtensions { + + public static boolean isRecent(Date date){ + Date now = new Date(); + Calendar cal = new GregorianCalendar(); + cal.add(Calendar.MONTH, -6); + Date sixMonthsAgo = cal.getTime(); + return date.before(now) && date.after(sixMonthsAgo); + } + +} +---- + +This one declares an additional method on the `Date` type, allowing you to test whether a date is recent or not: + +[source,html] +---- +{#if todo.done && todo.doneDate.isRecent()} + This was done recently! +{/if} +---- + +=== Vixen extension methods + +[cols="1,1,1"] +|=== +|Target type|Method|Description + +|`Date` +|`format()` +|Formats the date to the `dd/MM/yyyy` format + +|`Date` +|`internetFormat()` +|Formats the date to the `yyyy-MM-dd` format + +|`Date` +|`future()` +|Returns `true` if the date is in the future + +|`Date` +|`since()` +|Formats the date in terms of `X seconds/minutes/hours/days/months/years ago` + +|`String` +|`md5()` +|Returns an MD5 hash of the given string + +|`Object` +|`instanceOf(className)` +|Returns true if the given object is exactly of the specified class name + +|=== + + +=== External CSS, JavaScript libraries + +You can use webjars to provide third-party JavaScript or CSS. For example, here is how you can import Bootstrap +and Bootstrap-icons in your `pom.xml`: + +[source,xml] +---- + + org.webjars + bootstrap + 5.1.3 + + + org.webjars.npm + bootstrap-icons + 1.7.0 + + + io.quarkus + quarkus-webjars-locator + +---- + +After that, you can include them in your Qute templates with: + +[source,html] +---- + + + + + +---- + +Look at https://mvnrepository.com/artifact/org.webjars for the list of available options. + +== Forms + +A lot of the time, you need to send data from the browser to your endpoints, which is often done with forms. + +=== The HTML form + +Creating forms in Vixen is easy: let's see an example of how to do it in Qute: + +[source,html] +---- +{#form uri:Register.complete(newUser.confirmationCode)} + +
+ Complete registration for {newUser.email} + {#formElement name="userName" label="User Name"} + {#input name="userName"/} + {/formElement} + {#formElement name="password" label="Password"} + {#input name="password" type="password"/} + {/formElement} + {#formElement name="password2" label="Password Confirmation"} + {#input name="password2" type="password"/} + {/formElement} + {#formElement name="firstName" label="First Name"} + {#input name="firstName"/} + {/formElement} + {#formElement name="lastName" label="Last Name"} + {#input name="lastName"/} + {/formElement} + +
+ +{/form} +---- + +Here we're defining a form whose action will go to `Register.complete(newUser.confirmationCode)` and +which contains several form elements, which are just tags to make composition easier. For example `formElement` is +a custom Qute tag for Bootstrap which defines layout for the form element and displays any associated error: + +[source,html] +---- +
+ + {nested-content} + {#ifError name} + ​{#error name/}​ + {/ifError} +
+---- + +The `input` user tag is also designed for Bootstrap as an abstraction: + +[source,html] +---- + +---- + +As you can see, we have default values for certain attributes, a special error class if there is a validation +error, and we default the value to the one preserved in the flash scope, which is filled whenever validation +fails, so that the user can see the validation error without losing their form values. + +As for the `form` Vixen tag, it is also fairly simple, and only includes an authenticity token for CRSF protection. + +[source,html] +---- +
+ {#authenticityToken/} + {nested-content} +
+---- + +=== The endpoint + +Most forms will be a `@POST` endpoint, with each form element having a corresponding parameter annotated with `@RestForm`. + +[source,java] +---- +@POST +public void complete(@RestQuery String confirmationCode, + @RestForm String userName, + @RestForm String password, + @RestForm String password2, + @RestForm String firstName, + @RestForm String lastName) { + // do something with the form parameters +} +---- + +You can also group parameters in a POJO, but for now you have to add a special +`@Consumes(MediaType.MULTIPART_FORM_DATA)` annotation: + +[source,java] +---- +@Consumes(MediaType.MULTIPART_FORM_DATA) +@POST +public void complete(@RestQuery String confirmationCode, + FormData form) { + // do something with the form parameters +} + +public static class FormData { + @RestForm String userName; + @RestForm String password; + @RestForm String password2; + @RestForm String firstName; + @RestForm String lastName; +} +---- + +Check out the xref:resteasy-reactive.adoc#handling-multipart-form-data[RESTEasy Reactive documentation] +for more information about form parameters and multi-part. + +=== Validation + +You can place your usual xref:validation.adoc[Hibernate Validation] annotations on the controller methods that receive user data, but +keep in mind that you have to check for validation errors in your method before you do any action that modifies your state. +This allows you to check more things than you can do with just annotations, with richer logic: + +[source,java] +---- +@POST +public Response complete(@RestQuery String confirmationCode, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String userName, + @RestForm @NotBlank @Length(min = 8, max = Util.VARCHAR_SIZE) String password, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String password2, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String firstName, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String lastName) { + // Find the user for this confirmation code + User user = User.findForContirmation(confirmationCode); + if(user == null){ + validation.addError("confirmationCode", "Invalid confirmation code"); + } + + // Make sure the passwords match + validation.equals("password", password, password2); + + // Make sure the username is free + if(User.findByUserName(userName) != null){ + validation.addError("userName", "User name already taken"); + } + + // If validation failed, redirect to the confirm page + if(validationFailed()){ + confirm(confirmationCode); + } + + // Now proceed to complete user registration + ... +} +---- + +You can use the `validation` object to trigger additional validation logic and collect errors. + +Those errors are then placed in the _flash_ scope by a call to `validationFailed()` if there +are any errors, and thus preserved when you redirect from your action method to the `@GET` method +that holds the submitted form, which you can then access in your views using the `{#ifError field}{/ifError}` +conditional tag, or the `{#error field/}` tag which accesses the error message for the given field. + +== Routing, URI mapping, redirects + +We have seen how to declare endpoints and how URIs map to them, but very often we need to map from endpoints to +URIs, which Vixen makes easy. + +=== Redirects after POST + +When handling a `@POST`, `@PUT` or `@DELETE` endpoint, it's good form to redirect to a `@GET` endpoint after +the action has been done, in order to allow the user to reload the page without triggering the action a second +time, and such redirects are simply done by calling the corresponding `@GET` endpoint. In reality, the endpoint +will not be called and will be replaced by a redirect that points to the endpoint in question. + +[source,java] +---- +package rest; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.vixen.Controller; + +public class Application extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(); + } + + @Path("/") + public TemplateInstance index() { + return Templates.index(); + } + + @POST + public void someAction() { + // do something + ... + // redirect to the index page + index(); + } +} +---- + +If there are any parameters that form the URI, you must pass them along: + +[source,java] +---- +package rest; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.vixen.Controller; + +public class Application extends Controller { + + @CheckedTemplate + static class Templates { + public static native TemplateInstance index(); + } + + @Path("/") + public TemplateInstance index() { + return Templates.index(); + } + + public TemplateInstance somePage(@RestPath String id, @RestQuery String q) { + // do something with the id and q + return Templates.index(); + } + + @POST + public void someAction() { + // do something + ... + // redirect to the somePage page + somePage("foo", "bar"); + } +} +---- + +If you want to redirect to another controller, you can use the `redirect(Class)` method: + +[source,java] +---- +package rest; + +import javax.ws.rs.POST; + +import io.quarkus.vixen.Controller; + +public class Application extends Controller { + + @POST + public void someAction() { + // do something + ... + // redirect to the Todos.index() endpoint + redirect(Todos.class).index(); + } +} +---- + +=== Obtaining a URI in endpoints + +If you don't want a redirect but need a URI to a given endpoint, you can use the `Router.getURI` methods, by +passing them a method reference to the endpoint you want and the required parameters: + +[source,java] +---- +package rest; + +import java.net.URI; + +import org.jboss.resteasy.reactive.RestPath; +import org.jboss.resteasy.reactive.RestQuery; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.vixen.Controller; +import io.quarkus.vixen.router.Router; + +public class Application extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance somePage(); + public static native TemplateInstance otherPage(URI uri); + } + + public TemplateInstance somePage(@RestPath String foo, @RestQuery Long bar) { + return Templates.somePage(); + } + + public TemplateInstance otherPage() { + // Obtain a URI to somePage + URI uri = Router.getURI(Login::somePage, "something", 23l); + // pass it on to our view + return Templates.otherPage(uri); + } +} +---- + +=== Obtaining a URI in Qute views + +If you want a URI to an endpoint in a Qute view, you can use the `uri` and `uriabs` namespace with a +call to the endpoint you want to point to: + +[source,html] +---- +Todo +---- + +Naturally, you can also pass any required parameters. + +== Emails + +[source,xml] +---- + + io.quarkus + quarkus-mailer + +---- + +Often you will need your actions to send email notifications. You can use Qute for this too, by declaring your +emails in an `Emails` class: + +[source,java] +---- +package email; + +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.qute.CheckedTemplate; +import model.User; + +public class Emails { + + private static final String FROM = "Todos "; + private static final String SUBJECT_PREFIX = "[Todos] "; + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance confirm(User user); + } + + public static void confirm(User user) { + Templates.confirm(user) + .subject(SUBJECT_PREFIX + "Please confirm your email address") + .to(user.email) + .from(FROM) + .send().await().indefinitely(); + } +} +---- + +You can then send the email from your endpoint by calling `Emails.confirm(user)`. + +You can use composition for emails too, by having a pair of base templates for HTML in +`src/main/resources/templates/email.html`: + +[source,html] +---- + + + + + + + + {#insert /} +

+ This is an automated email, you should not reply to it: your mail will be ignored. +

+ + +---- + +And for text in `src/main/resources/templates/email.txt`: + +[source,txt] +---- +{#insert /} + +This is an automated email, you should not reply to it: your mail will be ignored. +---- + +You can then use those templates in your emails in `src/main/resources/templates/Emails/confirm.html`: + +[source,html] +---- +{#include email.html } + +

+ Welcome to Todos. +

+ +

+ You received this email because someone (hopefully you) wants to register on Todos. +

+ +

+ If you don't want to register, you can safely ignore this email. +

+ +

+ If you want to register, complete your registration. +

+{/include} +---- + +And for text in `src/main/resources/templates/Emails/confirm.txt`: + +[source,txt] +---- +{#include email.txt} + +Welcome to Todos. + +You received this email because someone (hopefully you) wants to register on Todos. + +If you don't want to register, you can safely ignore this email. + +If you want to register, complete your registration by going to the following address: + +{uriabs:Register.confirm(user.confirmationCode)} +{/include} +---- + +Note that in emails you will want to use the `uriabs` namespace for absolute URIs and not relative ones, +otherwise the links won't work for your email recipients. + +You can find more information in the xref:mailer.adoc[Quarkus mailer documentation]. + +== Authentication + +=== Custom authentication with JWT + +In order to handle your own authentication by storing users in your database, you can use xref:security-jwt.adoc[JWT tokens]. +Start with importing those modules: + +[source,xml] +---- + + io.quarkus + quarkus-elytron-security-common + + + io.quarkus + quarkus-smallrye-jwt-build + +---- + +And set those configuration values: + +[source,properties] +---- +mp.jwt.verify.issuer=https://example.com/issuer +mp.jwt.token.header=Cookie +mp.jwt.token.cookie=QuarkusUser + +quarkus.http.auth.proactive=false +---- + +Your entity can look like this: + +[source,java] +---- +@Entity +@Table(name = "user_table") +public class User extends PanacheEntity { + + @Column(unique = true) + public String userName; + public String password; + public boolean isAdmin; + + public static User findByUserName(String username) { + return find("LOWER(userName) = ?1", username.toLowerCase()).firstResult(); + } +} +---- + +And your login endpoint can look like this to build the JWT token: + +[source,java] +---- +package rest; + +import javax.validation.constraints.NotBlank; +import javax.ws.rs.POST; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestForm; + +import io.quarkus.elytron.security.common.BcryptUtil; +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.vixen.Controller; +import io.quarkus.vixen.router.Router; +import model.User; + +public class Login extends Controller { + + @CheckedTemplate + public static class Templates { + public static native TemplateInstance login(); + } + + public TemplateInstance loginForm() { + return Templates.login(); + } + + @POST + public Response login(@NotBlank @RestForm String userName, + @NotBlank @RestForm String password) { + // validation check first + if(validationFailed()) { + loginForm(); + } + // look for the user + User user = User.findByUserName(userName); + // make sure user is found and password matches + if(user == null + || !BcryptUtil.matches(password, user.password)) { + validation.addError("userName", "Invalid username/pasword"); + prepareForErrorRedirect(); + loginForm(); + } + // make a JWT cookie + NewCookie cookie = Register.makeUserCookie(user); + // redirect to the index page with the new cookie + return Response.seeOther(Router.getURI(Application::index)).cookie(cookie).build(); + } + + public Response logout() { + // build a clearing cookie + NewCookie cookie = new NewCookie("QuarkusUser", null, "/", null, null, 0, false, true); + // redirect to the index page with the new cookie + return Response.seeOther(Router.getURI(Application::index)).cookie(cookie).build(); + } +} +---- + +The `Register` controller will handle creating new users and contains the code required to create a valid JWT +token: + +[source,java] +---- +@POST +public Response complete(@RestQuery String confirmationCode, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String userName, + @RestForm @NotBlank @Length(min = 8, max = Util.VARCHAR_SIZE) String password, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String password2, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String firstName, + @RestForm @NotBlank @Length(max = Util.VARCHAR_SIZE) String lastName) { + // do the validation + ... + + // create the user + User user = new User(); + user.userName = userName; + user.password = BcryptUtil.bcryptHash(password); + user.firstName = firstName; + user.lastName = lastName; + user.persist(); + + NewCookie cookie = makeUserCookie(user); + // make sure we set the user on the request scope + security.setUser(user); + // send the cookie to the user + return Response.ok(Templates.complete(user)).cookie(cookie).build(); +} + +static NewCookie makeUserCookie(User user) { + Set roles = new HashSet<>(); + if(user.isAdmin) { + roles.add("admin"); + } + String token = + Jwt.issuer("https://example.com/issuer") + .upn(user.userName) + .groups(roles) + .expiresIn(Duration.ofDays(10)) + .innerSign().encrypt(); + return new NewCookie("QuarkusUser", token, "/", null, Cookie.DEFAULT_VERSION, null, NewCookie.DEFAULT_MAX_AGE, null, false, false); +} +---- + +We've seen how to login, logout and create a new user, so all that is left is how to obtain the current user. +Quarkus handles the part that associates the `QuarkusUser` JWT cookie to a `SecurityIdentity` but that is +not very useful, so we often want to create a request bean that will load the corresponding `User` from the +database: + +[source,java] +---- +package util; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Named; + +import io.quarkus.security.identity.SecurityIdentity; +import model.User; + +@RequestScoped +public class Security { + + private final static User NO_USER = new User(); + + @Inject + SecurityIdentity identity; + + User user; + + @Named("user") + @Produces + public User getUser() { + if(user != null) { + // turn our null marker into null + return user == NO_USER ? null : user; + } + if(!identity.isAnonymous()) { + user = User.findByUserName(identity.getPrincipal().getName()); + if(user == null) { + // FIXME: error for invalid user? + // avoid looking it up again + user = NO_USER; + } + } + return user; + } + + // for initial login, since we can't arbitrarily set request-scoped values + public void setUser(User user) { + this.user = user; + } +} +---- + +This will make sure that we produce a `user` named bean containing the current user if there is a +logged-in user. You can then use it in your views with `inject:user` or by injecting it in +your endpoints. + +You can also use the `@Authenticated` and `@RolesAllowed("admin")` annotations on your endpoints. + +==== Private/public keys + +In DEV mode, your private/public keys will be generated for you at startup if they do not exist and +are not specified. Cleaning your project will remove those keys, so previous JWT tokens won't be valid +anymore on restart. + +In production environments you will need to generate and specify your private and public keys using the following +commands: + +[source,shell] +---- +$ openssl genrsa -out rsaPrivateKey.pem 2048 +$ openssl rsa -pubout -in rsaPrivateKey.pem -out src/main/resources/publicKey.pem +$ openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out src/main/resources/privateKey.pem +---- + +You can then point to those files in your `application.properties` Quarkus configuration: + +[source,properties] +---- +mp.jwt.verify.publickey.location=publicKey.pem +mp.jwt.decrypt.key.location=privateKey.pem +smallrye.jwt.sign.key.location=privateKey.pem +smallrye.jwt.encrypt.key.location=publicKey.pem + +quarkus.native.resources.includes=publicKey.pem +quarkus.native.resources.includes=privateKey.pem +---- + +== Flash scope + +If you need to pass values from one endpoint to another request after a redirect, you can use the Flash scope. +Usually this is done in a `@POST` endpoint, by filling the Flash scope with either errors or messages, +before trigerring a redirect to the right `@GET` endpoint. + +You can push values in the Flash scope in an endpoint using the `flash(name, value)` method, or using the +`Flash` injectable component. + +You can read values from the Flash scope in your Qute views using the `{flash:name}` namespace. + +The Flash scope only lasts from one request to the next and is cleared at each request. diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml new file mode 100644 index 00000000..f221b4a1 --- /dev/null +++ b/integration-tests/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + io.quarkiverse.renarde + quarkus-renarde-parent + 1.0.0-SNAPSHOT + + quarkus-renarde-integration-tests + Quarkus - Renarde - Integration Tests + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkiverse.renarde + quarkus-renarde + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + diff --git a/integration-tests/src/main/java/io/quarkiverse/renarde/it/RenardeResource.java b/integration-tests/src/main/java/io/quarkiverse/renarde/it/RenardeResource.java new file mode 100644 index 00000000..3ce6b7ef --- /dev/null +++ b/integration-tests/src/main/java/io/quarkiverse/renarde/it/RenardeResource.java @@ -0,0 +1,32 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.quarkiverse.renarde.it; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/renarde") +@ApplicationScoped +public class RenardeResource { + // add some rest methods here + + @GET + public String hello() { + return "Hello renarde"; + } +} diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/integration-tests/src/test/java/io/quarkiverse/renarde/it/NativeRenardeResourceIT.java b/integration-tests/src/test/java/io/quarkiverse/renarde/it/NativeRenardeResourceIT.java new file mode 100644 index 00000000..fbcab09b --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/renarde/it/NativeRenardeResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.it; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class NativeRenardeResourceIT extends RenardeResourceTest { +} diff --git a/integration-tests/src/test/java/io/quarkiverse/renarde/it/RenardeResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/renarde/it/RenardeResourceTest.java new file mode 100644 index 00000000..51cb47d0 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/renarde/it/RenardeResourceTest.java @@ -0,0 +1,21 @@ +package io.quarkiverse.renarde.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RenardeResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/renarde") + .then() + .statusCode(200) + .body(is("Hello renarde")); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..5d151d05 --- /dev/null +++ b/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + io.quarkiverse + quarkiverse-parent + 8 + + io.quarkiverse.renarde + quarkus-renarde-parent + 1.0.0-SNAPSHOT + pom + Quarkus - Renarde - Parent + + deployment + runtime + + + 3.8.1 + 11 + UTF-8 + UTF-8 + 999-SNAPSHOT + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + + + + + it + + + performRelease + !true + + + + integration-tests + + + + diff --git a/runtime/pom.xml b/runtime/pom.xml new file mode 100644 index 00000000..ef7b7c7f --- /dev/null +++ b/runtime/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + io.quarkiverse.renarde + quarkus-renarde-parent + 1.0.0-SNAPSHOT + + quarkus-renarde + Quarkus - Renarde - Runtime + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-narayana-jta + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-qute + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-resteasy-reactive-qute + + + io.quarkus + quarkus-reactive-routes + + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus.qute + qute-core + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/runtime/src/main/java/io/quarkiverse/renarde/Controller.java b/runtime/src/main/java/io/quarkiverse/renarde/Controller.java new file mode 100644 index 00000000..49890e1c --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/Controller.java @@ -0,0 +1,109 @@ +package io.quarkiverse.renarde; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; + +import io.quarkiverse.renarde.util.Flash; +import io.quarkiverse.renarde.util.RedirectException; +import io.quarkiverse.renarde.util.RenderArgs; +import io.quarkiverse.renarde.util.Validation; +import io.quarkus.security.identity.SecurityIdentity; + +public class Controller { + + @Inject + protected SecurityIdentity identity; + + @Inject + protected RenderArgs renderArgs; + + @Inject + protected Validation validation; + + @Inject + protected Flash flash; + + // FIXME: force injecting it so that it exists in the request context because we use it in the templates + // via the injection API + @Inject + protected UriInfo uriInfo; + + protected boolean validationFailed() { + if (validation.hasErrors()) { + prepareForErrorRedirect(); + return true; + } + return false; + } + + protected void prepareForErrorRedirect() { + flash.flashParams(); // add http parameters to the flash scope + validation.keep(); // keep the errors for the next request + } + + protected void flash(String key, Object value) { + flash.flash(key, value); + } + + protected static String emptyAsNull(String val) { + if (val == null || val.isEmpty()) + return null; + return val; + } + + protected Response forbidden() { + throw new WebApplicationException(RestResponse.StatusCode.FORBIDDEN); + } + + protected Response forbidden(String message) { + throw new WebApplicationException( + RestResponse.ResponseBuilder.create(RestResponse.Status.FORBIDDEN, message).build().toResponse()); + } + + protected Response badRequest() { + throw new WebApplicationException(RestResponse.StatusCode.BAD_REQUEST); + } + + protected void notFoundIfNull(Object obj) { + if (obj == null) + throw new WebApplicationException(RestResponse.notFound().toResponse()); + } + + protected Response notFound(String message) { + throw new WebApplicationException( + RestResponse.ResponseBuilder.create(RestResponse.Status.NOT_FOUND, message).build().toResponse()); + } + + protected Response notFound() { + throw new WebApplicationException(RestResponse.StatusCode.NOT_FOUND); + } + + protected Response seeOther(String uri) { + try { + return seeOther(new URI(uri)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + // static to allow usage in redirect(FooController.class).method() usage without actually having an instance of FooController + protected static Response seeOther(URI uri) { + throw new RedirectException(Response.seeOther(uri).build()); + } + + protected Response temporaryRedirect(URI uri) { + throw new WebApplicationException(Response.temporaryRedirect(uri).build()); + } + + protected T redirect(Class target) { + throw new RuntimeException( + "This method can only be called when instrumented together with a view call to the result directly: redirect(FooController.class).method()"); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method0.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method0.java new file mode 100644 index 00000000..6ec6f042 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method0.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method0 { + Object method(Target target); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method0V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method0V.java new file mode 100644 index 00000000..4507d93f --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method0V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method0V { + void method(Target target); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method1.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method1.java new file mode 100644 index 00000000..563d9c5a --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method1.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method1 { + Object method(Target target, P1 param1); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method10.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method10.java new file mode 100644 index 00000000..347e865e --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method10.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method10 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method10V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method10V.java new file mode 100644 index 00000000..d420cbe0 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method10V.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method10V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method11.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method11.java new file mode 100644 index 00000000..b3d8b078 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method11.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method11 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method11V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method11V.java new file mode 100644 index 00000000..89d65be7 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method11V.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method11V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method12.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method12.java new file mode 100644 index 00000000..afc240db --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method12.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method12 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11, P12 param12); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method12V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method12V.java new file mode 100644 index 00000000..343e3042 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method12V.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method12V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11, P12 param12); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method13.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method13.java new file mode 100644 index 00000000..6af87386 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method13.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method13 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11, P12 param12, P13 param13); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method13V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method13V.java new file mode 100644 index 00000000..b84b39d0 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method13V.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method13V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9, P10 param10, P11 param11, P12 param12, P13 param13); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method1V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method1V.java new file mode 100644 index 00000000..613b0db4 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method1V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method1V { + void method(Target target, P1 param1); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method2.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method2.java new file mode 100644 index 00000000..b5a51db5 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method2.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method2 { + Object method(Target target, P1 param1, P2 param2); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method2V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method2V.java new file mode 100644 index 00000000..aa7827ca --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method2V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method2V { + void method(Target target, P1 param1, P2 param2); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method3.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method3.java new file mode 100644 index 00000000..561871c4 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method3.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method3 { + Object method(Target target, P1 param1, P2 param2, P3 param3); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method3V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method3V.java new file mode 100644 index 00000000..9638b498 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method3V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method3V { + void method(Target target, P1 param1, P2 param2, P3 param3); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method4.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method4.java new file mode 100644 index 00000000..bbfe3aa1 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method4.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method4 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method4V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method4V.java new file mode 100644 index 00000000..ba19ca2c --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method4V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method4V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method5.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method5.java new file mode 100644 index 00000000..aed97dd8 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method5.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method5 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method5V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method5V.java new file mode 100644 index 00000000..d71f7fdb --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method5V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method5V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method6.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method6.java new file mode 100644 index 00000000..953db426 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method6.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method6 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method6V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method6V.java new file mode 100644 index 00000000..f376b2dd --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method6V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method6V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method7.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method7.java new file mode 100644 index 00000000..99b2777a --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method7.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method7 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method7V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method7V.java new file mode 100644 index 00000000..b336be4d --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method7V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method7V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method8.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method8.java new file mode 100644 index 00000000..39f52e40 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method8.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method8 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method8V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method8V.java new file mode 100644 index 00000000..586d601b --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method8V.java @@ -0,0 +1,6 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method8V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method9.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method9.java new file mode 100644 index 00000000..e40fb77c --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method9.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method9 { + Object method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Method9V.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Method9V.java new file mode 100644 index 00000000..28fdded6 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Method9V.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.router; + +@FunctionalInterface +public interface Method9V { + void method(Target target, P1 param1, P2 param2, P3 param3, P4 param4, P5 param5, P6 param6, P7 param7, P8 param8, + P9 param9); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/Router.java b/runtime/src/main/java/io/quarkiverse/renarde/router/Router.java new file mode 100644 index 00000000..b3c8651b --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/Router.java @@ -0,0 +1,171 @@ +package io.quarkiverse.renarde.router; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import io.quarkus.arc.Arc; + +public class Router { + + public static URI getURI(Method0 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method0V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method1 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method1V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method2 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method2V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method3 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method3V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method4 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method4V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method5 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method5V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method6 method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method6V method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method7 method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method7V method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method8 method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI(Method8V method, + Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method9 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method9V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method10 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method10V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method11 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method11V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method12 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method12V method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method13 method, Object... params) { + return findURI(method, params); + } + + public static URI getURI( + Method13V method, Object... params) { + return findURI(method, params); + } + + private static URI findURI(Object method, Object... params) { + // make sure all the calls are instrumented + throw new RuntimeException("This call should have been instrumented away"); + } + + public static URI findURI(String route, boolean absolute, Object... params) { + // This is only used by the views + RouterMethod routerMethod = routerMethods.get(route); + if (routerMethod == null) + throw new RuntimeException("No route defined for " + route); + return routerMethod.getRoute(absolute, params); + } + + private static Map routerMethods = new HashMap<>(); + + // Called by generated class __VixenInit for each controller route + public static void registerRoute(String route, RouterMethod method) { + if (routerMethods.containsKey(route)) { + System.err.println("WARNING: duplicate route registered for " + route); + } + routerMethods.put(route, method); + } + + // Used by generated bytecode for each controller method, which has a corresponding method to build a URI to it + public static UriBuilder getUriBuilder(boolean absolute) { + UriInfo uriInfo = Arc.container().instance(UriInfo.class).get(); + UriBuilder ret = absolute ? uriInfo.getAbsolutePathBuilder().replacePath("") : uriInfo.getBaseUriBuilder(); + return ret; + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/router/RouterMethod.java b/runtime/src/main/java/io/quarkiverse/renarde/router/RouterMethod.java new file mode 100644 index 00000000..0709bae8 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/router/RouterMethod.java @@ -0,0 +1,8 @@ +package io.quarkiverse.renarde.router; + +import java.net.URI; + +@FunctionalInterface +public interface RouterMethod { + URI getRoute(boolean absolute, Object... args); +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/AuthenticationFailedExceptionMapper.java b/runtime/src/main/java/io/quarkiverse/renarde/util/AuthenticationFailedExceptionMapper.java new file mode 100644 index 00000000..9bd31606 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/AuthenticationFailedExceptionMapper.java @@ -0,0 +1,61 @@ +package io.quarkiverse.renarde.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import javax.crypto.AEADBadTagException; + +import org.jose4j.jwt.consumer.ErrorCodes; +import org.jose4j.jwt.consumer.InvalidJwtException; + +import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.web.RouteFilter; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailedExceptionMapper { + + // FIXME: we can do better than this filter + @RouteFilter(100) + void myFilter(RoutingContext rc) { + rc.put(QuarkusHttpUser.AUTH_FAILURE_HANDLER, new BiConsumer() { + @Override + public void accept(RoutingContext routingContext, Throwable throwable) { + while (throwable.getCause() != null) + throwable = throwable.getCause(); + if (throwable instanceof InvalidJwtException) { + InvalidJwtException x = (InvalidJwtException) throwable; + if (x.hasErrorCode(ErrorCodes.EXPIRED)) { + redirectToRoot(routingContext, "Login expired, you've been logged out"); + return; + } + } + // This happens when the private/public keys change, like in DEV mode + if (throwable instanceof AEADBadTagException) { + redirectToRoot(routingContext, "Something is rotten about your JWT, clearing it"); + return; + } + if (throwable instanceof AuthenticationRedirectException) { + // handled upstream + return; + } + // FIXME: what now? + routingContext.end(); + } + + private void redirectToRoot(RoutingContext routingContext, String message) { + // FIXME: constant + routingContext.removeCookie("QuarkusUser").setPath("/"); + Map map = new HashMap<>(); + // FIXME: format? + map.put("message", message); + Flash.setFlashCookie(routingContext.response(), map); + // FIXME: URI, perhaps redirect to login page? + // Note that this calls end() + routingContext.redirect("/"); + } + }); + rc.next(); + } +} \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/CRSF.java b/runtime/src/main/java/io/quarkiverse/renarde/util/CRSF.java new file mode 100644 index 00000000..ac694e98 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/CRSF.java @@ -0,0 +1,87 @@ +package io.quarkiverse.renarde.util; + +import java.security.SecureRandom; +import java.util.Base64; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.spi.CDI; +import javax.inject.Inject; +import javax.inject.Named; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.FormData; +import org.jboss.resteasy.reactive.server.core.multipart.FormData.FormValue; + +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpServerRequest; + +@Named("CRSF") +@RequestScoped +public class CRSF { + + @Inject + HttpServerRequest request; + + private String crsfToken; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private final static int CRSF_SIZE = 16; + private final static String CRSF_COOKIE_NAME = "_aviouf_crsf"; + private final static String CRSF_FORM_NAME = "_aviouf_crsf_token"; + + public void setCRSFCookie() { + // FIXME: expiry, others? + // in some cases with exception mappers, it appears the filters get invoked twice + // FIXME: sometimes we seem to lose the flow and request scope, leading to calls like: + /* + * Reading CRSF cookie + * Existing cookie: MEJBRQDw9Y8FGOmEG1vItA== + * Saving CRSF cookie: MEJBRQDw9Y8FGOmEG1vItA== + * Saving CRSF cookie: null + */ + if (!request.response().headWritten() && crsfToken != null) + request.response().addCookie( + Cookie.cookie(CRSF_COOKIE_NAME, crsfToken).setPath("/")); + } + + public void readCRSFCookie() { + Cookie cookie = request.getCookie(CRSF_COOKIE_NAME); + if (cookie != null) { + crsfToken = cookie.getValue(); + } else { + byte[] bytes = new byte[CRSF_SIZE]; + SECURE_RANDOM.nextBytes(bytes); + crsfToken = Base64.getEncoder().encodeToString(bytes); + } + } + + public void checkCRSFToken() { + CurrentVertxRequest currentVertxRequest = CDI.current().select(CurrentVertxRequest.class).get(); + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) currentVertxRequest + .getOtherHttpContextObject(); + FormData formData = rrContext.getFormData(); + String formToken = null; + // FIXME: we could allow checks for query params + if (formData != null) { + FormValue value = formData.getFirst(CRSF_FORM_NAME); + formToken = value != null ? value.getValue() : null; + } + if (formToken == null || !formToken.equals(crsfToken)) { + throw new WebApplicationException( + Response.status(Response.Status.BAD_REQUEST).entity("Invalid or missing CRSF Token").build()); + } + } + + // For views + public String formName() { + return CRSF_FORM_NAME; + } + + // For views + public String token() { + return crsfToken; + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java b/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java new file mode 100644 index 00000000..1949029d --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java @@ -0,0 +1,53 @@ +package io.quarkiverse.renarde.util; + +import java.util.Map.Entry; + +import javax.inject.Inject; +import javax.ws.rs.container.ContainerResponseContext; + +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; + +import io.quarkus.qute.TemplateInstance; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; + +public class Filters { + + @Inject + RenderArgs renderArgs; + + @Inject + Flash flash; + + @Inject + CRSF crsf; + + @ServerRequestFilter + public void filterRequest(HttpServerRequest req) { + flash.handleFlashCookie(); + crsf.readCRSFCookie(); + // check CRSF param for every method except the three safe ones + if (req.method() != HttpMethod.GET + && req.method() != HttpMethod.HEAD + && req.method() != HttpMethod.OPTIONS) { + //FIXME: can't do this for now because form values are not read when filter is invoked + // crsf.checkCRSFToken(); + } + } + + @ServerResponseFilter + public void filterResponse(ContainerResponseContext responseContext, HttpServerResponse resp) { + Object entity = responseContext.getEntity(); + if (entity instanceof TemplateInstance) { + TemplateInstance template = (TemplateInstance) entity; + for (Entry entry : renderArgs.entrySet()) { + template.data(entry.getKey(), entry.getValue()); + } + } + flash.setFlashCookie(); + crsf.setCRSFCookie(); + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/Flash.java b/runtime/src/main/java/io/quarkiverse/renarde/util/Flash.java new file mode 100644 index 00000000..0df0164f --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/Flash.java @@ -0,0 +1,118 @@ +package io.quarkiverse.renarde.util; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.spi.CDI; +import javax.inject.Inject; +import javax.inject.Named; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.core.multipart.FormData; +import org.jboss.resteasy.reactive.server.core.multipart.FormData.FormValue; + +import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.vertx.core.http.Cookie; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; + +@Named("flash") +@RequestScoped +public class Flash { + + @Inject + HttpServerRequest request; + + @Inject + Validation validation; + + private Map values = new HashMap<>(); + private Map futureValues = new HashMap<>(); + + private final static String FLASH_COOKIE_NAME = "_aviouf_flash"; + + public void setFlashCookie() { + setFlashCookie(request.response(), futureValues); + } + + public static void setFlashCookie(HttpServerResponse response, Map values) { + // FIXME: expiry, others? + // in some cases with exception mappers, it appears the filters get invoked twice + if (!response.headWritten()) + response.addCookie( + Cookie.cookie(FLASH_COOKIE_NAME, Base64.getEncoder().encodeToString(marshallMap(values))) + .setPath("/")); + } + + public void handleFlashCookie() { + Cookie cookie = request.getCookie(FLASH_COOKIE_NAME); + if (cookie != null) { + byte[] bytes = cookie.getValue().getBytes(); + if (bytes != null && bytes.length != 0) { + byte[] decoded = Base64.getDecoder().decode(bytes); + // API says it can't be null + if (decoded.length > 0) { + Map data = unmarshallMap(decoded); + values.putAll(data); + validation.loadErrorsFromFlash(); + } + } + } + // must do this after we've read the value, otherwise we can't read it, for some reason + request.response().removeCookie(FLASH_COOKIE_NAME); + } + + private static byte[] marshallMap(Map data) { + String json = Json.encode(data); + // FIXME: this is optimistic + return json.getBytes(StandardCharsets.UTF_8); + } + + private static Map unmarshallMap(byte[] data) { + String json = new String(data, StandardCharsets.UTF_8); + JsonObject obj = (JsonObject) Json.decodeValue(json); + return obj.getMap(); + } + + public void flashParams() { + // FIXME: different for GET? + // FIXME: multiple values? + CurrentVertxRequest currentVertxRequest = CDI.current().select(CurrentVertxRequest.class).get(); + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) currentVertxRequest + .getOtherHttpContextObject(); + FormData formData = rrContext.getFormData(); + if (formData != null) { + for (String key : formData) { + // FIXME: more than first value? + FormValue firstValue = formData.getFirst(key); + // skip files, since we can't set them in error forms anyway + if (!firstValue.isFileItem()) { + futureValues.put(key, firstValue.getValue()); + } + } + } + } + + public void flash(String key, Object value) { + futureValues.put(key, value); + } + + public T get(String key) { + // FIXME: is this really about the previous values or the future ones? + return (T) values.get(key); + } + + // FIXME: this is just to get around not being able to prefix error. in Qute + public T getError(String key) { + return (T) values.get("error." + key); + } + + public Map values() { + return values; + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/JavaExtensions.java b/runtime/src/main/java/io/quarkiverse/renarde/util/JavaExtensions.java new file mode 100644 index 00000000..bcd12689 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/JavaExtensions.java @@ -0,0 +1,96 @@ +package io.quarkiverse.renarde.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.Period; +import java.time.ZoneId; +import java.util.Date; + +import io.quarkus.arc.Arc; +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class JavaExtensions { + + public static String format(Date date) { + // FIXME: L10N + return new SimpleDateFormat("dd/MM/yyyy").format(date); + } + + public static String internetDateFormat(Date date) { + return new SimpleDateFormat("yyyy-MM-dd").format(date); + } + + public static boolean isFuture(Date date) { + return date.after(new Date()); + } + + public static String since(Date date) { + LocalDateTime t1 = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + LocalDateTime t2 = LocalDateTime.now(); + Period period = Period.between(t1.toLocalDate(), t2.toLocalDate()); + Duration duration = Duration.between(t1, t2); + + if (period.getYears() > 1) + return period.getYears() + " years ago"; + if (period.getYears() == 1) + return period.getYears() + " year ago"; + if (period.getMonths() > 1) + return period.getMonths() + " months ago"; + if (period.getMonths() == 1) + return period.getMonths() + " month ago"; + if (period.getDays() > 1) + return period.getDays() + " days ago"; + if (period.getDays() == 1) + return period.getDays() + " day ago"; + if (duration.toHours() > 1) + return duration.toHours() + " hours ago"; + if (duration.toHours() == 1) + return duration.toHours() + " hour ago"; + if (duration.toMinutes() > 1) + return duration.toMinutes() + " minutes ago"; + if (duration.toMinutes() == 1) + return duration.toMinutes() + " minute ago"; + return "moments ago"; + } + + public static String gravatarHash(String str) { + if (str == null) + return null; + return md5(str.trim().toLowerCase()); + } + + public static String md5(String value) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(40); + for (int i = 0; i < digest.length; ++i) { + sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + // workaround a boxing bug? + public static int minus(int a, int b) { + return a - b; + } + + public static boolean instanceOf(Object val, String type) { + if (val == null) + return false; + return val.getClass().getName().equals(type); + } + + @TemplateExtension(namespace = "flash", matchName = "*") + static Object flash(String value) { + return Arc.container().instance(Flash.class).get().get(value); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/MyParamConverters.java b/runtime/src/main/java/io/quarkiverse/renarde/util/MyParamConverters.java new file mode 100644 index 00000000..5b40c9c4 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/MyParamConverters.java @@ -0,0 +1,46 @@ +package io.quarkiverse.renarde.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import javax.ws.rs.ext.Provider; + +@Provider +public class MyParamConverters implements ParamConverterProvider { + + public static class DateParamConverter implements ParamConverter { + + @Override + public Date fromString(String value) { + if (StringUtils.isEmpty(value)) + return null; + if (value.matches("\\d{4}-\\d{2}-\\d{2}")) { + try { + return new SimpleDateFormat("yyyy-MM-dd").parse(value); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + throw new RuntimeException("Don't know how to deserialise " + value + " as a Date"); + } + + @Override + public String toString(Date value) { + // FIXME: is this used? + return JavaExtensions.internetDateFormat(value); + } + } + + @Override + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (rawType == Date.class) + return (ParamConverter) new DateParamConverter(); + return null; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/MyValidationInterceptor.java b/runtime/src/main/java/io/quarkiverse/renarde/util/MyValidationInterceptor.java new file mode 100644 index 00000000..6e645452 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/MyValidationInterceptor.java @@ -0,0 +1,58 @@ +package io.quarkiverse.renarde.util; + +import java.util.Set; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.interceptor.AroundConstruct; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; + +import io.quarkus.hibernate.validator.runtime.interceptor.AbstractMethodValidationInterceptor; +import io.quarkus.hibernate.validator.runtime.jaxrs.JaxrsEndPointValidated; +import io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyReactiveViolationException; + +@JaxrsEndPointValidated +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_AFTER + 700) +public class MyValidationInterceptor extends AbstractMethodValidationInterceptor { + + @Inject + Validator validator; + + @Inject + Validation validation; + + @AroundInvoke + @Override + public Object validateMethodInvocation(InvocationContext ctx) throws Exception { + ExecutableValidator executableValidator = validator.forExecutables(); + Set> violations = executableValidator.validateParameters(ctx.getTarget(), + ctx.getMethod(), ctx.getParameters()); + + if (!violations.isEmpty()) { + // just collect them and go on + validation.addErrors(violations); + } + + Object result = ctx.proceed(); + + violations = executableValidator.validateReturnValue(ctx.getTarget(), ctx.getMethod(), result); + + if (!violations.isEmpty()) { + throw new ResteasyReactiveViolationException(violations); + } + + return result; + } + + @AroundConstruct + @Override + public void validateConstructorInvocation(InvocationContext ctx) throws Exception { + super.validateConstructorInvocation(ctx); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java b/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java new file mode 100644 index 00000000..37791606 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java @@ -0,0 +1,77 @@ +package io.quarkiverse.renarde.util; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import javax.enterprise.event.Observes; + +import io.quarkiverse.renarde.router.Router; +import io.quarkus.qute.CompletedStage; +import io.quarkus.qute.EngineBuilder; +import io.quarkus.qute.EvalContext; +import io.quarkus.qute.Expression; +import io.quarkus.qute.NamespaceResolver; +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.ValueResolver; +import io.smallrye.mutiny.Uni; + +public class QuteResolvers { + + static class BoundRouter { + public final String target; + public final boolean absolute; + + public BoundRouter(String target, boolean absolute) { + this.target = target; + this.absolute = absolute; + } + } + + void configureEngine(@Observes EngineBuilder builder) { + builder.addValueResolver(ValueResolver.builder() + .appliesTo(ctx -> ctx.getBase() instanceof BoundRouter) + .resolveSync(ctx -> { + List params = ctx.getParams(); + if (params.isEmpty()) { + return CompletedStage.of(findURI(ctx, Collections.emptyList())); + } else { + List> unis = new ArrayList<>(params.size()); + for (int i = 0; i < params.size(); i++) { + CompletionStage val = ctx.evaluate(params.get(i)); + Uni uni = Uni.createFrom().completionStage(val); + unis.add(uni); + } + return Uni.combine().all().unis(unis) + .collectFailures().combinedWith(paramValues -> findURI(ctx, paramValues)) + .convert().toCompletionStage(); + } + }) + .build()); + builder.addNamespaceResolver(NamespaceResolver.builder("uri") + .resolve(ctx -> new BoundRouter(ctx.getName(), false)) + .build()); + builder.addNamespaceResolver(NamespaceResolver.builder("uriabs") + .resolve(ctx -> new BoundRouter(ctx.getName(), true)) + .build()); + } + + private URI findURI(EvalContext ctx, List paramValues) { + BoundRouter boundRouter = (BoundRouter) ctx.getBase(); + Class[] paramClasses = new Class[paramValues.size()]; + int i = 0; + for (Object val : paramValues) { + if (val == null) { + throw new TemplateException("Failed to find route to " + boundRouter.target + "." + ctx.getName() + + " due to a null parameter: " + paramValues); + } + paramClasses[i++] = val.getClass(); + } + // FIXME: make it work for multiple sets of parameters? with optional query params that's a bit harder + // but probably GET/PUT/POST/DELETE will all have the same required set and URI? + String route = boundRouter.target + "." + ctx.getName(); + return Router.findURI(route, boundRouter.absolute, paramValues.toArray()); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectException.java b/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectException.java new file mode 100644 index 00000000..a096c773 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectException.java @@ -0,0 +1,17 @@ +package io.quarkiverse.renarde.util; + +import javax.ws.rs.core.Response; + +import io.quarkus.narayana.jta.DontRollback; + +@DontRollback +@SuppressWarnings("serial") +public class RedirectException extends RuntimeException { + + public final Response response; + + public RedirectException(Response response) { + this.response = response; + } + +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectExceptionMapper.java b/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectExceptionMapper.java new file mode 100644 index 00000000..37679604 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/RedirectExceptionMapper.java @@ -0,0 +1,12 @@ +package io.quarkiverse.renarde.util; + +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +public class RedirectExceptionMapper { + @ServerExceptionMapper + public Response toResponse(RedirectException e) { + return e.response; + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/RenderArgs.java b/runtime/src/main/java/io/quarkiverse/renarde/util/RenderArgs.java new file mode 100644 index 00000000..13a40106 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/RenderArgs.java @@ -0,0 +1,26 @@ +package io.quarkiverse.renarde.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.enterprise.context.RequestScoped; + +@RequestScoped +public class RenderArgs { + + Map args = new HashMap<>(); + + public void put(String key, Object value) { + args.put(key, value); + } + + public Set> entrySet() { + return args.entrySet(); + } + + public T get(String key) { + return (T) args.get(key); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/StringUtils.java b/runtime/src/main/java/io/quarkiverse/renarde/util/StringUtils.java new file mode 100644 index 00000000..39830316 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/StringUtils.java @@ -0,0 +1,7 @@ +package io.quarkiverse.renarde.util; + +public class StringUtils { + public static boolean isEmpty(String str) { + return str == null || str.isEmpty(); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/UriUtils.java b/runtime/src/main/java/io/quarkiverse/renarde/util/UriUtils.java new file mode 100644 index 00000000..816eefe7 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/UriUtils.java @@ -0,0 +1,104 @@ +package io.quarkiverse.renarde.util; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.BitSet; + +public class UriUtils { + + private static BitSet ALPHA = new BitSet(); + static { + for (char c = 'a'; c <= 'z'; c++) + ALPHA.set(c); + for (char c = 'A'; c <= 'Z'; c++) + ALPHA.set(c); + } + + private static BitSet DIGIT = new BitSet(); + static { + for (char c = '0'; c <= '9'; c++) + DIGIT.set(c); + } + + private static BitSet UNRESERVED = new BitSet(); + static { + UNRESERVED.or(ALPHA); + UNRESERVED.or(DIGIT); + UNRESERVED.set('-'); + UNRESERVED.set('.'); + UNRESERVED.set('_'); + UNRESERVED.set('~'); + } + + private static BitSet SUB_DELIMS = new BitSet(); + static { + SUB_DELIMS.set('!'); + SUB_DELIMS.set('$'); + SUB_DELIMS.set('&'); + SUB_DELIMS.set('\''); + SUB_DELIMS.set('('); + SUB_DELIMS.set(')'); + SUB_DELIMS.set('*'); + SUB_DELIMS.set('+'); + SUB_DELIMS.set(','); + SUB_DELIMS.set(';'); + SUB_DELIMS.set('='); + } + + private static BitSet PCHAR = new BitSet(); + static { + PCHAR.or(UNRESERVED); + PCHAR.or(SUB_DELIMS); + PCHAR.set(':'); + PCHAR.set('@'); + } + + public static String encodeSegment(String segment) { + int length = segment.codePointCount(0, segment.length()); + StringBuffer sb = new StringBuffer(length); + for (int i = 0; i < length; i++) { + int c = segment.codePointAt(i); + if (PCHAR.get(c)) + sb.append((char) c); + else + percentEncode(c, sb); + } + return sb.toString(); + } + + private static void percentEncode(int c, StringBuffer sb) { + CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); + ByteBuffer out = ByteBuffer.allocate(4); + CharBuffer in = CharBuffer.allocate(2); + if (Character.isSupplementaryCodePoint(c)) { + in.append(Character.highSurrogate(c)); + in.append(Character.lowSurrogate(c)); + } else + in.append((char) c); + in.flip(); + if (encoder.encode(in, out, true) != CoderResult.UNDERFLOW) + throw new RuntimeException("Illegal UTF-8 encoding for codepoint " + c); + out.flip(); + while (out.hasRemaining()) { + sb.append("%"); + toHexa(out.get(), sb); + } + } + + private static void toHexa(byte b, StringBuffer sb) { + byte h = (byte) ((b & 0b11110000) >> 4); + byte l = (byte) (b & 0b1111); + toHexa2(h, sb); + toHexa2(l, sb); + } + + private static void toHexa2(byte l, StringBuffer sb) { + if (l < 10) + sb.append(l); + else + sb.append((char) ((l - 10) + 'A')); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/Validation.java b/runtime/src/main/java/io/quarkiverse/renarde/util/Validation.java new file mode 100644 index 00000000..54d46605 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/Validation.java @@ -0,0 +1,110 @@ +package io.quarkiverse.renarde.util; + +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.validation.ConstraintViolation; +import javax.validation.Path.Node; +import javax.validation.Validator; + +@Named("validation") +@RequestScoped +public class Validation { + @Inject + Validator validator; + @Inject + Flash flash; + + private List errors = new ArrayList<>(); + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + public void keep() { + for (Error error : errors) { + flash.flash("error." + error.field, error.message); + } + } + + public void required(String field, Object value) { + if (value == null || (value instanceof String && ((String) value).isEmpty())) + addError(field, "Required"); + } + + public void addError(String field, String message) { + errors.add(new Error(field, message)); + } + + public boolean hasError(String field) { + for (Error error : errors) { + if (error.field.equals(field)) + return true; + } + return false; + } + + public static class Error { + + public final String field; + public final String message; + + public Error(String field, String message) { + this.field = field; + this.message = message; + } + + @Override + public String toString() { + return "[Error field=" + field + ", message=" + message + "]"; + } + } + + public void minSize(String field, String value, int size) { + if (value == null || value.length() < size) + addError(field, "Must be at least " + size + " characters long"); + } + + public void maxSize(String field, String value, int size) { + if (value == null || value.length() > size) + addError(field, "Must be at most " + size + " characters long"); + } + + public void equals(String field, Object a, Object b) { + if (!Objects.equals(a, b)) + addError(field, "Must be equal"); + } + + public void future(String field, Date date) { + if (!date.after(new Date())) + addError(field, "Must be in the future"); + } + + public void addErrors(Set> violations) { + for (ConstraintViolation violation : violations) { + Iterator iterator = violation.getPropertyPath().iterator(); + String lastNode = null; + while (iterator.hasNext()) { + lastNode = iterator.next().getName(); + } + addError(lastNode, violation.getMessage()); + } + } + + public void loadErrorsFromFlash() { + for (Entry entry : flash.values().entrySet()) { + if (entry.getKey().startsWith("error.")) { + String field = entry.getKey().substring(6); + addError(field, (String) entry.getValue()); + } + } + } +} diff --git a/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000..56f8bed8 --- /dev/null +++ b/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Renarde +#description: Renarde ... +metadata: +# keywords: +# - renarde +# guide: ... +# categories: +# - "miscellaneous" +# status: "preview" \ No newline at end of file diff --git a/runtime/src/main/resources/templates/tags/authenticityToken.html b/runtime/src/main/resources/templates/tags/authenticityToken.html new file mode 100644 index 00000000..63d5fa79 --- /dev/null +++ b/runtime/src/main/resources/templates/tags/authenticityToken.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/runtime/src/main/resources/templates/tags/error.html b/runtime/src/main/resources/templates/tags/error.html new file mode 100644 index 00000000..bda7bc69 --- /dev/null +++ b/runtime/src/main/resources/templates/tags/error.html @@ -0,0 +1 @@ +{inject:flash.getError(it)} diff --git a/runtime/src/main/resources/templates/tags/form.html b/runtime/src/main/resources/templates/tags/form.html new file mode 100644 index 00000000..8111295e --- /dev/null +++ b/runtime/src/main/resources/templates/tags/form.html @@ -0,0 +1,4 @@ +
+ {#authenticityToken/} + {nested-content} +
\ No newline at end of file diff --git a/runtime/src/main/resources/templates/tags/gravatar.html b/runtime/src/main/resources/templates/tags/gravatar.html new file mode 100644 index 00000000..f0a4c781 --- /dev/null +++ b/runtime/src/main/resources/templates/tags/gravatar.html @@ -0,0 +1 @@ + diff --git a/runtime/src/main/resources/templates/tags/ifError.html b/runtime/src/main/resources/templates/tags/ifError.html new file mode 100644 index 00000000..85fced4e --- /dev/null +++ b/runtime/src/main/resources/templates/tags/ifError.html @@ -0,0 +1 @@ +{#if inject:validation.hasError(it)}{nested-content}{/if}