diff --git a/.github/workflows/update-since-tags.yml b/.github/workflows/update-since-tags.yml index f819d0e275c..36a8c7d7e73 100644 --- a/.github/workflows/update-since-tags.yml +++ b/.github/workflows/update-since-tags.yml @@ -78,6 +78,21 @@ jobs: artifacts: | vaadin-spring + # vaadin-cdi needs its own isolated index for the same reason vaadin-spring + # does, and the reasoning above applies unchanged: under the same + # com.vaadin:vaadin-cdi GAV it kept its own scheme -- 46 releases from + # 1.0.0.alpha1 to 16.1.2 -- before the module was imported into this + # repository and started releasing with Flow's version number. Pooling + # those onto the shared axis would drop 16.x into the gap between Flow 9.2 + # and 23.0 and rewrite @since across most of the framework. + - uses: vaadin/github-actions/reconcile-since-tags@2a6fabc668c09424c29ca789789e9bf0720de8ee + with: + group-id: com.vaadin + version: ${{ inputs.dev }} + index-dir: .since-index-cdi + artifacts: | + vaadin-cdi + - name: Open pull request uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 5c5a5a66fee..c887b909ae2 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -283,6 +283,172 @@ jobs: with: name: tests-output-it-${{ matrix.current }} path: tests-report-*.tgz + cdi-tests: + name: CDI Tests (${{ matrix.profile }}) + # The CDI integration tests deploy to a real application server, which the + # generic it-tests matrix cannot express, so computeMatrix.js excludes the + # module and it runs here instead. Every change runs the tomee profile as + # a smoke test; the other four containers only run when the CDI sources + # themselves change, since that is when a container specific regression + # gets introduced. Needs the TestBench licence, so not for forks. + needs: build + if: ${{ !github.event.pull_request.head.repo.fork }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + profile: [tomee, wildfly, payara, liberty, tomcat-weld] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + ref: ${{env._HEAD_SHA}} + # Full history only where the step below actually diffs. tomee is + # ungated, so cloning the whole repository for it buys nothing. + fetch-depth: ${{ matrix.profile == 'tomee' && 1 || 0 }} + - name: Decide whether to run + # Same gating as gradle-tests below: a push or a manual run has nothing + # to compare against, and a gate that cannot tell what changed has to + # run the job rather than report green. + # + # The root pom counts as a CDI source: it manages weld.version, which + # the tomcat-weld profile resolves weld-servlet-shaded from, so a bump + # there is exactly the kind of change only the gated containers see. + id: decide + run: | + set -euo pipefail + run=true + if [ "${{ matrix.profile }}" != "tomee" ]; then + DIFF_BASE= + case "$GITHUB_EVENT_NAME" in + pull_request) + if git fetch --no-tags origin \ + "+refs/heads/$_BASE_REF:refs/remotes/origin/$_BASE_REF"; then + DIFF_BASE=$(git merge-base HEAD "refs/remotes/origin/$_BASE_REF" || true) + fi + ;; + merge_group) + DIFF_BASE=$_MERGE_GROUP_BASE_SHA + ;; + esac + if [ -z "$DIFF_BASE" ]; then + echo "No base to compare against, running every container." + elif ! CHANGED=$(git diff --name-only "$DIFF_BASE" HEAD); then + echo "Could not diff against $DIFF_BASE, running every container." + else + RELEVANT=$(printf '%s\n' "$CHANGED" | grep -E \ + '^(vaadin-cdi/|flow-tests/vaadin-cdi-tests/|pom\.xml$|\.github/workflows/validation\.yml$)' || true) + if [ -z "$RELEVANT" ]; then + echo "No changes to the CDI modules, tomee alone covers this." + run=false + else + echo "Changes to the CDI modules:" + printf '%s\n' "$RELEVANT" + fi + fi + fi + echo "run=$run" >> "$GITHUB_OUTPUT" + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + if: ${{ steps.decide.outputs.run == 'true' }} + with: + node-version: '24.21.0' + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + if: ${{ steps.decide.outputs.run == 'true' }} + with: + version: '11.26.0' + - name: Set up JDK + if: ${{ steps.decide.outputs.run == 'true' }} + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: "${{ env.JAVA_VERSION }}" + distribution: 'temurin' + - name: Set flow version to 999.99-SNAPSHOT + if: ${{ steps.decide.outputs.run == 'true' }} + run: | + ./scripts/computeMatrix.js set-version --version=999.99-SNAPSHOT + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + if: ${{ steps.decide.outputs.run == 'true' }} + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + if: ${{ steps.decide.outputs.run == 'true' && github.run_attempt == 1 }} + with: + name: saved-workspace + - name: Restore Workspace + if: ${{ steps.decide.outputs.run == 'true' && github.run_attempt == 1 }} + run: | + tar xf workspace.tar + tar cf - .m2 | (cd ~ && tar xf -) + - name: Compile and Install Flow + if: ${{ steps.decide.outputs.run == 'true' && github.run_attempt > 1 }} + run: | + ./scripts/computeMatrix.js set-version --version=999.99-SNAPSHOT + cmd="mvn install -B -ntp -DskipTests -pl \!flow-plugins/flow-gradle-plugin" + eval $cmd -T 2C -q || eval $cmd + - name: Set TB License + if: ${{ steps.decide.outputs.run == 'true' }} + env: + TB_LICENSE: ${{ secrets.TB_LICENSE }} + run: | + mkdir -p ~/.vaadin/ + echo '{"username":"'`echo $TB_LICENSE | cut -d / -f1`'","proKey":"'`echo $TB_LICENSE | cut -d / -f2`'"}' > ~/.vaadin/proKey + - name: Verify the no-container build skips its tests + # cdi.container.tests.skip only takes effect when no container profile + # is active, which no other job exercises: the module lives in the + # it-test-modules profile, so `mvn install -DskipTests` leaves it out, + # and every leg below passes -P. Assert it here, once, so a + # regression surfaces in CI rather than in someone's `mvn verify` over + # flow-tests. + if: ${{ steps.decide.outputs.run == 'true' && matrix.profile == 'tomee' }} + run: | + set -e + LOG=mvn-cdi-no-profile.out + mvn -B -ntp -e -pl flow-tests/vaadin-cdi-tests verify >"$LOG" 2>&1 || { + cat "$LOG"; exit 1; } + cat "$LOG" + if grep -qE '^\[INFO\] Tests run:' "$LOG"; then + echo "::error::CDI container tests ran without a container profile" + exit 1 + fi + grep -q 'Tests are skipped' "$LOG" || { + echo "::error::surefire did not report the tests as skipped"; exit 1; } + - name: Run CDI ITs + if: ${{ steps.decide.outputs.run == 'true' }} + timeout-minutes: 35 + run: | + # enforcer:enforce has to be named explicitly: the Flow parent keeps + # the dependencyConvergence rule in the default-cli execution, so no + # lifecycle phase evaluates it. Each container profile brings its own + # Arquillian adapter, so this is where a divergence would appear. + cmd="mvn -V -B -ntp -e -pl flow-tests/vaadin-cdi-tests -P${{ matrix.profile }} -Dcom.vaadin.testbench.Parameters.maxAttempts=2" + set -x -e + LOG=mvn-cdi-tests-${{ matrix.profile }}.out + : > "$LOG" + # Detached the same way the it-tests job detaches its Maven, and for + # the same reason twice over: these tests fork application server + # JVMs that can outlive Maven and pin the step open by holding a pipe + # to tee, and piping would also make the step exit with tee's status + # instead of Maven's, so a failing container would report success. + $cmd enforcer:enforce verify "$LOG" 2>&1 & + MVN_PID=$! + tail -f --pid=$MVN_PID "$LOG" & + wait $MVN_PID + - name: Kill leftover Java processes + if: ${{ always() && steps.decide.outputs.run == 'true' }} + run: | + pkill -TERM java || true + sleep 2 + pkill -KILL java || true + - name: Package test-report files + if: ${{ always() && steps.decide.outputs.run == 'true' }} + run: find . -name surefire-reports -o -name failsafe-reports -o -name error-screenshots -o -name "mvn-*.out" | tar -czf tests-report-cdi-${{ matrix.profile }}.tgz -T - + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ always() && steps.decide.outputs.run == 'true' }} + with: + name: tests-output-cdi-${{ matrix.profile }} + path: tests-report-*.tgz gradle-tests: name: Gradle Tests # Ports the TeamCity 'Gradle Tests' build. Always runs so it can be marked @@ -556,7 +722,7 @@ jobs: # was cancelled, and a skipped required check satisfies branch # protection — letting auto-merge proceed past a cancelled IT. if: ${{ always() && !github.event.pull_request.head.repo.fork }} - needs: [unit-tests, it-tests] + needs: [unit-tests, it-tests, cdi-tests] runs-on: ubuntu-24.04 steps: - name: Merge Artifacts @@ -579,7 +745,7 @@ jobs: run: | ./scripts/computeMatrix.js test-results >> $GITHUB_STEP_SUMMARY - name: Set Failure Status - if: ${{ always() && (needs.unit-tests.result != 'success' || needs.it-tests.result != 'success') }} + if: ${{ always() && (needs.unit-tests.result != 'success' || needs.it-tests.result != 'success' || needs.cdi-tests.result != 'success') }} run: | echo "🚫 THERE ARE TEST MODULES WITH FAILURES or BEEN CANCELLED" | tee -a $GITHUB_STEP_SUMMARY exit 1 diff --git a/CLAUDE.md b/CLAUDE.md index 2fd6c16aed5..17f1e07c270 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,7 @@ React support). - `flow-devloop-daemon`: daemon for the `vaadin-dev` dev loop - `flow-tests/`: integration test suite - `vaadin-spring`: Spring Framework integration +- `vaadin-cdi`: CDI integration See `guidelines/repository.md` for the full module map and `guidelines/architecture.md` for how the pieces fit together. diff --git a/flow-bom/pom.xml b/flow-bom/pom.xml index fc0b68875e6..cb78ba6e71c 100644 --- a/flow-bom/pom.xml +++ b/flow-bom/pom.xml @@ -126,6 +126,11 @@ vaadin-spring ${project.version} + + com.vaadin + vaadin-cdi + ${project.version} + com.vaadin flow-webpush diff --git a/flow-tests/README.md b/flow-tests/README.md index 699cdd0e0c1..e2d6e07fc61 100644 --- a/flow-tests/README.md +++ b/flow-tests/README.md @@ -34,7 +34,7 @@ copying the test. Never add the same test in two places. 1. Needs a non-Spring-Boot container? → plain `VaadinServlet` / custom servlet service: **test-plain-servlet**; Spring MVC without Boot: **test-plain-spring**; - CDI: **test-cdi**. + CDI: **vaadin-cdi-tests**. 2. Needs Spring Security? → **test-spring-security** (its url-mapping / context-path / method-security / route-path variants live here as profiles). 3. Needs a specific theme setting? → custom application theme: **test-themes**; @@ -73,7 +73,7 @@ copying the test. Never add the same test in two places. | **test-spring-security** | Spring Security (auth, url-mapping, method / route-path checks) | | **test-livereload** | live reload | | **test-redeployment** | Spring DevTools restart | -| **test-cdi** (future) | the CDI container | +| **vaadin-cdi-tests** | the CDI container (deploys to a real application server, so it runs only under a container profile) | Some tests need irreducible special infrastructure and keep their own modules: `servlet-containers` (non-Jetty containers), `test-multi-war`, @@ -178,6 +178,7 @@ Active when `-DskipTests` is **not** set (the default). | Module | Description | |--------|-------------| | **servlet-containers** | Aggregator that runs integration tests on servlet containers other than the default Jetty. Contains a `tomcat10` submodule deploying the root-context WAR onto Apache Tomcat 10 via the Cargo plugin. | +| **vaadin-cdi-tests** | Arquillian suite for the `vaadin-cdi` integration, deploying to TomEE, WildFly, Payara, Open Liberty and Tomcat+Weld: CDI scopes and contexts, the instantiator, push, i18n, templates and deployment validation. Every test needs an application server, so its tests are skipped unless one of the container profiles is active — see [vaadin-cdi-tests/README.md](vaadin-cdi-tests/README.md). | ## Nightly Profile diff --git a/flow-tests/pom.xml b/flow-tests/pom.xml index 009f79d4d79..80231b760e8 100644 --- a/flow-tests/pom.xml +++ b/flow-tests/pom.xml @@ -384,6 +384,7 @@ test-custom-frontend-directory vaadin-spring-tests + vaadin-cdi-tests test-vaadin-router test-react-adapter test-react-adapter/pom-production.xml diff --git a/flow-tests/vaadin-cdi-tests/.dtrackignore b/flow-tests/vaadin-cdi-tests/.dtrackignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/flow-tests/vaadin-cdi-tests/README.md b/flow-tests/vaadin-cdi-tests/README.md new file mode 100644 index 00000000000..7c783be447e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/README.md @@ -0,0 +1,41 @@ +# Vaadin CDI tests + +Arquillian tests for `vaadin-cdi`. Each test deploys a WAR, assembled by +ShrinkWrap from this module's effective POM, to a real application server, so +one of the container profiles has to be selected — without a profile the tests +are skipped rather than failed: + +```sh +mvn verify -pl flow-tests/vaadin-cdi-tests -Ptomee +``` + +The available profiles are `tomee`, `wildfly`, `payara`, `liberty` and +`tomcat-weld`. Validation runs `tomee` for every change and the rest when the +CDI sources themselves change; see the `cdi-tests` job in +`.github/workflows/validation.yml`. + +Run these on **JDK 21**, the version validation uses. On JDK 24 and later +`java.security.Policy.setPolicy` throws `UnsupportedOperationException` (the +Security Manager is gone, JEP 486), and TomEE calls it while installing its +JACC policy provider, so the container never starts and every test fails with +`Could not install JACC Policy Provider` rather than anything about CDI. + +The tests drive a real Chrome through TestBench, so a `~/.vaadin/proKey` is +required and browser windows open while they run. + +The tests carrying the `SlowTests` category are excluded by default. They +still need a container, so enable them alongside one: + +```sh +mvn verify -pl flow-tests/vaadin-cdi-tests -Ptomee,slow-tests +``` + +### Dependencies for tests + +If there is a need to add or remove a dependency from the test module +the library should be also be added/removed from the `base(String warName)` +method in `ArchiveProvider`. + +If the tomee execution doesn't return any clear indication on a failure +except 404 for a module then running with the wildfly profile will often +yield better exception information. diff --git a/flow-tests/vaadin-cdi-tests/pom.xml b/flow-tests/vaadin-cdi-tests/pom.xml new file mode 100644 index 00000000000..41fa3f568c8 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/pom.xml @@ -0,0 +1,586 @@ + + + 4.0.0 + + com.vaadin + flow-tests + 25.4-SNAPSHOT + + + vaadin-cdi-tests + Vaadin CDI tests + Test modules for CDI integration + + + + com.vaadin.cdi.itest.SlowTests + 11.0.8 + 10.1.0 + 40.0.1.Final + 25.0.0.6 + jakartaee10 + io.openliberty + 3.8.1 + + true + 1.9.5.Final + 3.3.4 + 2.0.0 + + + + + + + org.jboss.arquillian + arquillian-bom + ${arquillian.version} + pom + import + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + ${shrinkwrap.resolver.version} + + + org.jboss.shrinkwrap.descriptors + shrinkwrap-descriptors-impl-javaee + ${shrinkwrap.descriptors.version} + + + + + + + com.vaadin + vaadin-cdi + ${project.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.enterprise.concurrent + jakarta.enterprise.concurrent-api + provided + + + + + com.vaadin + flow-server + ${project.version} + + + com.vaadin + flow-polymer-template + ${project.version} + + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + test + + + org.jboss.shrinkwrap.descriptors + shrinkwrap-descriptors-impl-javaee + test + + + + + + + com.vaadin + flow-maven-plugin + + + + prepare-frontend + build-frontend + + compile + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${cdi.container.tests.skip} + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + parse-version + none + + + + + + org.apache.maven.plugins + maven-help-plugin + 3.5.1 + + ${project.build.directory}/effective-pom.xml + + + + + effective-pom + + generate-test-resources + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + + delete-lib-folder + none + + + sanitize-effective-pom + + run + + generate-test-resources + + + true + + + + + + + + + + + + + + + tomee + + false + + + + org.apache.tomee + arquillian-tomee-remote + ${apache-tomee.version} + test + + + org.apache.tomee + apache-tomee + ${apache-tomee.version} + webprofile + zip + test + + + + + + maven-surefire-plugin + ${maven.surefire.plugin.version} + + ${surefire.excludedGroups} + ${surefire.groups} + + 0.0.0.0 + + + + + + + + wildfly + + false + + + + org.wildfly.arquillian + wildfly-arquillian-container-managed + 5.1.0.Final + test + + + org.slf4j + slf4j-simple + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + unpack + + unpack + + process-test-classes + + + + org.wildfly + wildfly-dist + ${wildfly.version} + zip + target + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + ${surefire.excludedGroups} + ${surefire.groups} + + org.jboss.logmanager.LogManager + ${project.build.directory}/wildfly-${wildfly.version} + wildfly + + false + + + + + + + payara + + false + + + + fish.payara.arquillian + arquillian-payara-server-managed + 3.1 + test + + + org.slf4j + slf4j-simple + runtime + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + unpack + + unpack + + process-test-classes + + + + fish.payara.distributions + payara + 6.2025.6 + zip + false + target + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + ${surefire.excludedGroups} + ${surefire.groups} + + ${project.build.directory}/payara6 + payara + true + + false + + + + + + + liberty + + false + + + + io.openliberty.arquillian + arquillian-liberty-managed-jakarta-junit + 2.1.4 + pom + test + + + + + + io.openliberty.tools + liberty-maven-plugin + 3.11.4 + + + create-server + + create + install-feature + configure-arquillian + + process-test-classes + + + ${openliberty.runtimeArtifactGroupId} + openliberty-runtime + ${openliberty.version} + zip + + true + + + jakartaee-10.0 + localConnector-1.0 + + + + + + + + maven-antrun-plugin + 3.1.0 + + + + run + + process-test-classes + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + ${surefire.excludedGroups} + ${surefire.groups} + + liberty_managed + + false + + + + + + + tomcat-weld + + false + + + + org.jboss.arquillian.container + arquillian-tomcat-managed-10 + 1.2.3.Final + test + + + + org.jboss.weld.servlet + weld-servlet-shaded + ${weld.version} + test + + + + jakarta.enterprise.concurrent + jakarta.enterprise.concurrent-api + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + unpack + + unpack + + process-test-classes + + + + org.apache.tomcat + tomcat + ${apache-tomcat.version} + zip + target + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + setup-tomcat-users + + copy-resources + + process-test-classes + + + + ${project.basedir}/src/test/resources/tomcat + + tomcat-users.xml + + + + ${project.build.directory}/apache-tomcat-${apache-tomcat.version}/conf + + + + + + maven-surefire-plugin + ${maven.surefire.plugin.version} + + com.vaadin.cdi.itest.NeedsCDICompliantContainer,${surefire.excludedGroups} + ${surefire.groups} + + tomcat-weld + ${project.build.directory}/apache-tomcat-${apache-tomcat.version} + + + + + + + + slow-tests + + + com.vaadin.cdi.itest.SlowTests + + + + diff --git a/flow-tests/vaadin-cdi-tests/src/main/frontend/test-template.js b/flow-tests/vaadin-cdi-tests/src/main/frontend/test-template.js new file mode 100644 index 00000000000..570c0b84cd6 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/frontend/test-template.js @@ -0,0 +1,20 @@ +import { html } from '@polymer/polymer/lib/utils/html-tag.js'; +import { PolymerElement } from '@polymer/polymer/polymer-element.js'; + +class TestTemplate extends PolymerElement { + static get template() { + return html` +
+ AAAAAAAAAAAAAAAAAAAAAAAAA + +
+ `; + } + + static get is() { + return 'test-template'; + } +} + +window.customElements.define(TestTemplate.is, TestTemplate); diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/Counter.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/Counter.java new file mode 100644 index 00000000000..ebfecb75d92 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/Counter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@ApplicationScoped +public class Counter { + private ConcurrentHashMap map = new ConcurrentHashMap<>(); + + public int increment(String key) { + map.putIfAbsent(key, new AtomicInteger(0)); + return map.get(key).incrementAndGet(); + } + + public int get(String key) { + map.putIfAbsent(key, new AtomicInteger(0)); + return map.get(key).get(); + } + + public void reset() { + map.clear(); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/CounterFilter.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/CounterFilter.java new file mode 100644 index 00000000000..c8e636dbd54 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/CounterFilter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import jakarta.inject.Inject; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; + +import java.io.IOException; + +@WebFilter(urlPatterns = "/*", asyncSupported = true) +public class CounterFilter implements jakarta.servlet.Filter { + + @Inject + private Counter counter; + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (request.getParameter("resetCounts") != null) { + counter.reset(); + } else { + String key = request.getParameter("getCount"); + if (key != null) { + response.getWriter().println(counter.get(key)); + } else { + chain.doFilter(request, response); + } + } + } + + @Override + public void destroy() { + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/BeanDiscoveryModeView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/BeanDiscoveryModeView.java new file mode 100644 index 00000000000..270d2c49a0c --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/BeanDiscoveryModeView.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.beandiscoverymode; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route("") +@RouteScoped +@CdiComponent +public class BeanDiscoveryModeView extends Div { + + public static final String SET_NAME_BTN_ID = "SET_NAME_BTN_ID"; + public static final String CLEAR_NAME_BTN_ID = "CLEAR_NAME_BTN_ID"; + public static final String RESULT_SPAN_ID = "RESULT_SPAN_ID"; + public static final String NORMAL_SRV_BTN_ID = "NORMAL_SRV_BTN_ID"; + public static final String CDI_SRV_BTN_ID = "CDI_SRV_BTN_ID"; + + @Inject + private NormalScopedGreetService normalScopedGreetService; + @Inject + private CdiComponentGreetService cdiComponentGreetService; + + @PostConstruct + public void init() { + Input nameField = new Input(); + NativeButton setNameButton = new NativeButton("Set the Name in input", + e -> nameField.setValue("MyName")); + setNameButton.setId(SET_NAME_BTN_ID); + NativeButton clearNameButton = new NativeButton("Clear the Name input", + e -> nameField.setValue("")); + clearNameButton.setId(CLEAR_NAME_BTN_ID); + + Span resultSpan = new Span(); + resultSpan.setId(RESULT_SPAN_ID); + + NativeButton normalServiceButton = new NativeButton( + "Normal Scoped Say hello", e -> resultSpan.setText( + normalScopedGreetService.greet(nameField.getValue()))); + normalServiceButton.setId(NORMAL_SRV_BTN_ID); + + NativeButton cdiCompServiceButton = new NativeButton( + "CDI Component Say hello", e -> resultSpan.setText( + cdiComponentGreetService.greet(nameField.getValue()))); + cdiCompServiceButton.setId(CDI_SRV_BTN_ID); + + add(setNameButton, clearNameButton, nameField, resultSpan, + normalServiceButton, cdiCompServiceButton); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/CdiComponentGreetService.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/CdiComponentGreetService.java new file mode 100644 index 00000000000..8bf930f4663 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/CdiComponentGreetService.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.beandiscoverymode; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; + +@CdiComponent +@UIScoped +public class CdiComponentGreetService { + + public String greet(String name) { + String greeting = "Hello %s from CDIComponent service."; + if (name == null || name.isEmpty()) { + return String.format(greeting, "anonymous user"); + } else { + return String.format(greeting, name); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/NormalScopedGreetService.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/NormalScopedGreetService.java new file mode 100644 index 00000000000..9e2b2bb8492 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/beandiscoverymode/NormalScopedGreetService.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.beandiscoverymode; + +import com.vaadin.cdi.annotation.NormalUIScoped; + +@NormalUIScoped +public class NormalScopedGreetService { + + public String greet(String name) { + String greeting = "Hello %s from NormalScoped service."; + if (name == null || name.isEmpty()) { + return String.format(greeting, "anonymous user"); + } else { + return String.format(greeting, name); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/i18n/TranslationView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/i18n/TranslationView.java new file mode 100644 index 00000000000..6cc25441c2f --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/i18n/TranslationView.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.i18n; + +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.i18n.I18NProvider; +import com.vaadin.flow.internal.LocaleUtil; +import com.vaadin.flow.router.Route; + +@Route(value = "translations") +public class TranslationView extends Div { + + public static final String TEST_VIEW_ID = "TranslationView"; + public static final String LOCALES_ID = "available-locales"; + + private Span dynamic; + + public TranslationView() { + setId(TEST_VIEW_ID); + + Span defaultLang = new Span(getTranslation(Locale.ENGLISH, "label")); + defaultLang.setId("english"); + Span german = new Span(getTranslation(Locale.GERMAN, "label")); + german.setId("german"); + Span germany = new Span(getTranslation(Locale.GERMANY, "label")); + germany.setId("germany"); + Span finnish = new Span( + getTranslation(new Locale("fi", "FI"), "label")); + finnish.setId("finnish"); + Span french = new Span(getTranslation(Locale.FRANCE, "label")); + french.setId("french"); + Span japanese = new Span(getTranslation(Locale.JAPAN, "label")); + japanese.setId("japanese"); + + Optional i18NProvider = LocaleUtil.getI18NProvider(); + if (i18NProvider.isPresent()) { + add(new Span("Available translation locales:")); + StringBuilder locales = new StringBuilder(); + for (Locale locale : i18NProvider.get().getProvidedLocales()) { + if (locales.length() > 0) { + locales.append(", "); + } + locales.append(locale.toString()); + } + Span localeSpan = new Span(locales.toString()); + localeSpan.setId(LOCALES_ID); + add(localeSpan, new Div()); + } + dynamic = new Span("waiting"); + dynamic.setId("dynamic"); + + add(defaultLang, new Div(), german, new Div(), germany, new Div(), + finnish, new Div(), french, new Div(), japanese, new Div(), + dynamic); + } + + @Override + protected void onAttach(AttachEvent event) { + UI ui = event.getUI(); + ui.setPollInterval(100); + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(50); + } catch (Exception e) { + e.printStackTrace(); + } finally { + ui.access(() -> { + dynamic.setText(getTranslation(Locale.FRANCE, "label")); + ui.setPollInterval(-1); + }); + + } + }); + + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorAlternative.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorAlternative.java new file mode 100644 index 00000000000..f41dccf6297 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorAlternative.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.instantiatorcustomize; + +import jakarta.enterprise.inject.spi.Unmanaged; + +import java.util.stream.Stream; + +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.server.VaadinServiceInitListener; + +public class InstantiatorAlternative implements Instantiator { + + @Override + public Stream getServiceInitListeners() { + return Stream.of(); + } + + @Override + public T getOrCreate(Class type) { + T instance = BeanProvider.getContextualReference(type, true); + if (InstantiatorCustomizeView.class.equals(type)) { + ((InstantiatorCustomizeView) instance).customize(); + } + return instance; + } + + @Override + public T createComponent(Class componentClass) { + Unmanaged unmanagedClass = new Unmanaged(componentClass); + Unmanaged.UnmanagedInstance instance = unmanagedClass.newInstance(); + instance.produce().inject().postConstruct(); + return instance.get(); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorCustomizeView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorCustomizeView.java new file mode 100644 index 00000000000..97933e01d3a --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorCustomizeView.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.instantiatorcustomize; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("") +@CdiComponent +public class InstantiatorCustomizeView extends Div { + public static final String VIEW = "VIEW"; + public static final String CUSTOMIZED = "CUSTOMIZED"; + + @PostConstruct + private void init() { + setId(VIEW); + } + + public void customize() { + setText(CUSTOMIZED); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryAlternative.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryAlternative.java new file mode 100644 index 00000000000..4f02b519b39 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryAlternative.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.instantiatorcustomize; + +import jakarta.annotation.Priority; +import jakarta.enterprise.inject.Alternative; +import jakarta.interceptor.Interceptor; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.server.VaadinService; + +@Priority(Interceptor.Priority.APPLICATION) +@Alternative +@VaadinServiceEnabled +@VaadinServiceScoped +public class InstantiatorFactoryAlternative implements InstantiatorFactory { + + @Override + public Instantiator createInstantitor(VaadinService service) { + return new InstantiatorAlternative(); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryDecorator.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryDecorator.java new file mode 100644 index 00000000000..1274dcb2034 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/instantiatorcustomize/InstantiatorFactoryDecorator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.instantiatorcustomize; + +import jakarta.annotation.Priority; +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.inject.Inject; +import jakarta.interceptor.Interceptor; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.server.VaadinService; + +@Priority(Interceptor.Priority.APPLICATION) +@Decorator +public abstract class InstantiatorFactoryDecorator + implements InstantiatorFactory { + @Inject + @Delegate + @VaadinServiceEnabled + private InstantiatorFactory delegate; + + @Override + public Instantiator createInstantitor(VaadinService service) { + return new InstantiatorAlternative(); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/DeploymentView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/DeploymentView.java new file mode 100644 index 00000000000..1e37d275c08 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/DeploymentView.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.invaliddeployment; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("") +@CdiComponent +public class DeploymentView extends Div { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/NormalScopedLabel.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/NormalScopedLabel.java new file mode 100644 index 00000000000..475e03b3f39 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/invaliddeployment/NormalScopedLabel.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.invaliddeployment; + +import com.vaadin.cdi.annotation.NormalUIScoped; +import com.vaadin.flow.component.html.NativeLabel; + +@NormalUIScoped +public class NormalScopedLabel extends NativeLabel { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/CustomSchedulerPushComponent.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/CustomSchedulerPushComponent.java new file mode 100644 index 00000000000..ef074b65bb4 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/CustomSchedulerPushComponent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.push; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import com.vaadin.cdi.annotation.CdiComponent; + +@CdiComponent +public class CustomSchedulerPushComponent extends PushComponent { + + private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors + .newScheduledThreadPool(4); + + @Override + public ScheduledExecutorService getExecutorService() { + return EXECUTOR_SERVICE; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/ManagedExecutorPushComponent.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/ManagedExecutorPushComponent.java new file mode 100644 index 00000000000..5d8462d09a9 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/ManagedExecutorPushComponent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.push; + +import jakarta.annotation.Resource; +import jakarta.enterprise.concurrent.ManagedScheduledExecutorService; + +import java.util.concurrent.ScheduledExecutorService; + +import com.vaadin.cdi.annotation.CdiComponent; + +@CdiComponent +public class ManagedExecutorPushComponent extends PushComponent { + + @Resource + private ManagedScheduledExecutorService executorService; + + @Override + public ScheduledExecutorService getExecutorService() { + return executorService; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/PushComponent.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/PushComponent.java new file mode 100644 index 00000000000..633dd372663 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/PushComponent.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.push; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.SessionScoped; + +import java.lang.annotation.Annotation; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.util.ContextUtils; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; + +public abstract class PushComponent extends Div { + public static final String RUN_BACKGROUND = "RUN_BACKGROUND"; + public static final String RUN_FOREGROUND = "RUN_FOREGROUND"; + + private class ContextCheckTask implements Runnable { + + private final UI ui; + private ClassLoader classLoader; + + private ContextCheckTask(UI ui) { + this.ui = ui; + this.classLoader = Thread.currentThread().getContextClassLoader(); + } + + @Override + public void run() { + ClassLoader contextClassLoader = Thread.currentThread() + .getContextClassLoader(); + Thread.currentThread().setContextClassLoader(this.classLoader); + try { + // We can acquire the lock after the request started this thread + // is processed + // Needed to make sure that this is sent as a push message + Lock lockInstance = ui.getSession().getLockInstance(); + lockInstance.lock(); + lockInstance.unlock(); + + ui.access(PushComponent.this::print); + } finally { + Thread.currentThread() + .setContextClassLoader(contextClassLoader); + } + } + } + + private void print() { + printContextIsActive(RequestScoped.class); + printContextIsActive(SessionScoped.class); + printContextIsActive(ApplicationScoped.class); + printContextIsActive(UIScoped.class); + printContextIsActive(RouteScoped.class); + printContextIsActive(VaadinServiceScoped.class); + printContextIsActive(VaadinSessionScoped.class); + } + + private void printContextIsActive(Class scope) { + NativeLabel label = new NativeLabel( + ContextUtils.isContextActive(scope) + ""); + label.setId(scope.getName()); + add(new Div(new NativeLabel(scope.getSimpleName() + ": "), label)); + } + + @PostConstruct + private void init() { + NativeButton bgButton = new NativeButton("background", event -> { + ContextCheckTask task = new ContextCheckTask(UI.getCurrent()); + // Delay execution to prevent UIDL being pushed by the thread + // serving + // the current HTTP request instead of the background thread + getExecutorService().schedule(task, 10, TimeUnit.MILLISECONDS); + }); + bgButton.setId(RUN_BACKGROUND); + + NativeButton fgButton = new NativeButton("foreground", + event -> print()); + fgButton.setId(RUN_FOREGROUND); + + add(bgButton, fgButton); + } + + public abstract ScheduledExecutorService getExecutorService(); + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketPushView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketPushView.java new file mode 100644 index 00000000000..95b57ca5f88 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketPushView.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.push; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.shared.communication.PushMode; +import com.vaadin.flow.shared.ui.Transport; + +@Route("websocket") +@CdiComponent +public class WebsocketPushView extends Div { + + @Inject + private PushComponent pushComponent; + + @Override + protected void onAttach(AttachEvent attachEvent) { + UI ui = attachEvent.getUI(); + ui.getPushConfiguration().setPushMode(PushMode.AUTOMATIC); + ui.getPushConfiguration().setTransport(Transport.WEBSOCKET); + } + + @PostConstruct + private void init() { + add(pushComponent); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketXhrPushView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketXhrPushView.java new file mode 100644 index 00000000000..5dc6df97637 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/push/WebsocketXhrPushView.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.push; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.shared.communication.PushMode; +import com.vaadin.flow.shared.ui.Transport; + +@Route("websocket-xhr") +@CdiComponent +public class WebsocketXhrPushView extends Div { + + @Inject + private PushComponent pushComponent; + + @Override + protected void onAttach(AttachEvent attachEvent) { + UI ui = attachEvent.getUI(); + ui.getPushConfiguration().setPushMode(PushMode.AUTOMATIC); + ui.getPushConfiguration().setTransport(Transport.WEBSOCKET_XHR); + } + + @PostConstruct + private void init() { + add(pushComponent); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/regression/RemoveOldContentView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/regression/RemoveOldContentView.java new file mode 100644 index 00000000000..7be9aaf07d6 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/regression/RemoveOldContentView.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.regression; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.ParentLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +@UIScoped +@Route("") +@CdiComponent +public class RemoveOldContentView extends Div implements RouterLayout { + + public static String NAVIGATE_TO_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID = "navigate-to-another-route-inside-main-layout-button-id"; + public static String NAVIGATE_TO_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID = "navigate-to-another-layout-button-id"; + public static String NAVIGATE_BACK_FROM_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID = "navigate-back-from-another-route-inside-main-layout-button-id"; + public static String NAVIGATE_BACK_FROM_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID = "navigate-back-from-another-route-outside-main-layout-button-id"; + public static String SUB_LAYOUT_ID = "sub-layout-id"; + + public RemoveOldContentView() { + add(new Span("This is a topmost parent router layout")); + } + + @UIScoped + @ParentLayout(RemoveOldContentView.class) + public static class SubLayout extends Div implements RouterLayout { + public SubLayout() { + setId(SUB_LAYOUT_ID); + add(new Span("This is a sub router layout")); + } + } + + @Route(value = "first-child-route", layout = SubLayout.class) + public static class FirstView extends Div { + + public FirstView() { + add(new Span("This is a child route inside main layout")); + NativeButton navigateToAnotherViewButton = new NativeButton( + "Navigate to another route inside Main Layout", + click -> UI.getCurrent().navigate(SecondView.class)); + navigateToAnotherViewButton.setId( + NAVIGATE_TO_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID); + add(navigateToAnotherViewButton); + NativeButton navigateToAnotherLayoutButton = new NativeButton( + "Navigate to another route outside Main Layout", + click -> UI.getCurrent() + .navigate(AnotherParentLayout.ThirdView.class)); + navigateToAnotherLayoutButton.setId( + NAVIGATE_TO_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID); + add(navigateToAnotherLayoutButton); + } + } + + @Route(value = "second-child-route", layout = RemoveOldContentView.class) + public static class SecondView extends Div { + + public SecondView() { + add(new Span("This is another route inside Main Layout")); + NativeButton navigateToChildView = new NativeButton( + "Navigate to first route", + click -> UI.getCurrent().navigate(FirstView.class)); + navigateToChildView.setId( + NAVIGATE_BACK_FROM_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID); + add(navigateToChildView); + } + } + + @Route("secondary") + public static class AnotherParentLayout extends Div + implements RouterLayout { + + public AnotherParentLayout() { + add(new Span("This is an another topmost parent router layout")); + } + + @Route(value = "third-child-route", layout = AnotherParentLayout.class) + public static class ThirdView extends Div { + + public ThirdView() { + add(new Span("This is another route outside of Main Layout")); + NativeButton navigateToChildView = new NativeButton( + "Navigate to first view", + click -> UI.getCurrent().navigate( + RemoveOldContentView.FirstView.class)); + navigateToChildView.setId( + NAVIGATE_BACK_FROM_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID); + add(navigateToChildView); + } + } + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedBean.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedBean.java new file mode 100644 index 00000000000..a4751af377d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedBean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +public class AbstractCountedBean implements CountedPerUI { + + private String data = ""; + + @PostConstruct + private void construct() { + countConstruct(); + } + + @PreDestroy + private void destroy() { + countDestroy(); + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java new file mode 100644 index 00000000000..06c77436731 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AbstractCountedView.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +import com.vaadin.flow.component.html.Div; + +public abstract class AbstractCountedView extends Div implements CountedPerUI { + + @PostConstruct + private void construct() { + countConstruct(); + } + + @PreDestroy + private void destroy() { + countDestroy(); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ApartBean.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ApartBean.java new file mode 100644 index 00000000000..9f6ebad5065 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ApartBean.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.annotation.RouteScopeOwner; + +@NormalRouteScoped +@RouteScopeOwner(DetailApartView.class) +public class ApartBean extends AbstractCountedBean { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AssignedBean.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AssignedBean.java new file mode 100644 index 00000000000..53c02c97304 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/AssignedBean.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.annotation.RouteScopeOwner; + +@NormalRouteScoped +@RouteScopeOwner(MasterView.class) +public class AssignedBean extends AbstractCountedBean { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java new file mode 100644 index 00000000000..15bb86666a3 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/BeanNoOwner.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +@CdiComponent +public class BeanNoOwner extends AbstractCountedBean { + + public BeanNoOwner() { + setData(UUID.randomUUID().toString()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java new file mode 100644 index 00000000000..3903b6717fd --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ChildNoOwnerView.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route(value = "child-no-owner", layout = ParentNoOwnerView.class) +@CdiComponent +public class ChildNoOwnerView extends Div { + + @Inject + private Instance instance; + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (attachEvent.isInitialAttach()) { + RouterLink link = new RouterLink("parent", ParentNoOwnerView.class); + link.setId("to-parent"); + add(link); + + Div div = new Div(); + div.setId("child-info"); + div.getElement().getStyle().set("display", "block"); + div.setText(instance.get().getData()); + add(div); + + NativeButton button = new NativeButton("Reset bean instance", + event -> div.setText(instance.get().getData())); + add(button); + button.setId("reset"); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CountedPerUI.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CountedPerUI.java new file mode 100644 index 00000000000..fc8e12af693 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CountedPerUI.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.cdi.itest.Counter; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.component.UI; + +public interface CountedPerUI { + + default int getUiId() { + return UI.getCurrent().getUIId(); + } + + default Counter getCounter() { + return BeanProvider.getContextualReference(Counter.class); + } + + default void countConstruct() { + getCounter().increment(getClass().getSimpleName() + "C" + getUiId()); + } + + default void countDestroy() { + if (UI.getCurrent() != null) { + getCounter() + .increment(getClass().getSimpleName() + "D" + getUiId()); + } + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java new file mode 100644 index 00000000000..88093b93d9e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +public class CustomException extends RuntimeException { + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java new file mode 100644 index 00000000000..da99b62a7f2 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubButton.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.NativeButton; + +@RouteScoped +@RouteScopeOwner(ErrorHandlerView.class) +@CdiComponent +public class CustomExceptionSubButton extends AbstractCountedView { + + public CustomExceptionSubButton() { + NativeButton button = new NativeButton(); + button.setId("custom-exception-button"); + button.setText(UUID.randomUUID().toString()); + add(button); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java new file mode 100644 index 00000000000..046283af5b8 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/CustomExceptionSubDiv.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +@RouteScopeOwner(ErrorHandlerView.class) +@CdiComponent +public class CustomExceptionSubDiv extends AbstractCountedView { + + public CustomExceptionSubDiv() { + setId("custom-exception-div"); + setText(UUID.randomUUID().toString()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailApartView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailApartView.java new file mode 100644 index 00000000000..ed08ae94d55 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailApartView.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@RouteScoped +@Route(value = "apart", layout = MasterView.class) +@CdiComponent +public class DetailApartView extends AbstractCountedView + implements AfterNavigationObserver { + + public static final String MASTER = "master"; + public static final String BEAN_LABEL = "BEAN_LABEL"; + + @Inject + @RouteScopeOwner(DetailApartView.class) + private ApartBean apartBean; + private NativeLabel apartLabel; + + @PostConstruct + private void init() { + apartLabel = new NativeLabel(); + apartLabel.setId(BEAN_LABEL); + apartBean.setData("APART"); + add(apartLabel, new Div(new RouterLink(MASTER, MasterView.class))); + } + + @Override + public void afterNavigation(AfterNavigationEvent event) { + apartLabel.setText(apartBean.getData()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailAssignedView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailAssignedView.java new file mode 100644 index 00000000000..16906d60e0f --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/DetailAssignedView.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@RouteScoped +@RouteScopeOwner(MasterView.class) +@Route(value = "assigned", layout = MasterView.class) +@CdiComponent +public class DetailAssignedView extends AbstractCountedView + implements AfterNavigationObserver { + + public static final String MASTER = "master"; + public static final String BEAN_LABEL = "BEAN_LABEL"; + + @Inject + @RouteScopeOwner(MasterView.class) + private AssignedBean assignedBean; + private NativeLabel assignedLabel; + + @PostConstruct + private void init() { + assignedLabel = new NativeLabel(); + assignedLabel.setId(BEAN_LABEL); + assignedBean.setData("ASSIGNED"); + add(assignedLabel, new Div(new RouterLink(MASTER, MasterView.class))); + } + + @Override + public void afterNavigation(AfterNavigationEvent event) { + assignedLabel.setText(assignedBean.getData()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java new file mode 100644 index 00000000000..90ba65cc39a --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorHandlerView.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletResponse; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.ParentLayout; +import com.vaadin.flow.router.RouterLink; + +@RouteScoped +@RouteScopeOwner(ErrorParentView.class) +@ParentLayout(ErrorParentView.class) +@CdiComponent +public class ErrorHandlerView extends AbstractCountedView + implements HasErrorParameter { + + public static final String PARENT = "parent"; + + @Inject + @RouteScopeOwner(ErrorHandlerView.class) + private Instance buttonInjection; + + @Inject + @RouteScopeOwner(ErrorHandlerView.class) + private Instance divInjection; + + private boolean isSubDiv; + + private Component current; + + @Override + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter parameter) { + add(new RouterLink(PARENT, ErrorParentView.class)); + + NativeButton button = new NativeButton("switch content", ev -> { + remove(current); + if (isSubDiv) { + current = buttonInjection.get(); + } else { + current = divInjection.get(); + } + add(current); + isSubDiv = !isSubDiv; + }); + button.setId("switch-content"); + add(button); + current = buttonInjection.get(); + add(current); + + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java new file mode 100644 index 00000000000..66c1a3e3aec --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorParentView.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +@RouteScoped +@RouteScopeOwner(ErrorParentView.class) +@Route("error-layout") +@CdiComponent +public class ErrorParentView extends AbstractCountedView + implements RouterLayout { + + public static final String ROOT = "root"; + + @PostConstruct + private void init() { + add(new RouterLink(ROOT, RootView.class)); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java new file mode 100644 index 00000000000..afbf087d2b2 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ErrorView.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; + +@RouteScoped +@Route("error") +@CdiComponent +public class ErrorView extends AbstractCountedView + implements BeforeEnterObserver { + + @Override + public void beforeEnter(BeforeEnterEvent event) { + if (true) { + throw new CustomException(); + } + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventObserverLayout.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventObserverLayout.java new file mode 100644 index 00000000000..7632a33e3dc --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventObserverLayout.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.Reception; + +import java.time.LocalDateTime; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.RouterLayout; + +@RouteScoped +@CdiComponent +public class EventObserverLayout extends Div implements RouterLayout { + + public static final String EVENTS_COUNTER_LABEL = "eventsCounter"; + private final NativeLabel receivedEvents; + private int counter = 0; + + public EventObserverLayout() { + receivedEvents = new NativeLabel(); + receivedEvents.setId(EVENTS_COUNTER_LABEL); + add(receivedEvents); + } + + public void onEvent( + @Observes(notifyObserver = Reception.IF_EXISTS) CustomEvent event) { + receivedEvents.setText("EVENTS COUNT: " + ++counter); + add(new Span("EVENT AT " + LocalDateTime.now())); + } + + public enum CustomEvent { + INSTANCE + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventView.java new file mode 100644 index 00000000000..02f454a0656 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/EventView.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; + +@Route("event") +@RouteScoped +@CdiComponent +public class EventView extends Div { + + public static final String FIRE = "FIRE"; + public static final String OBSERVER_LABEL = "OBSERVER_LABEL"; + + @RouteScoped + @RouteScopeOwner(EventView.class) + @CdiComponent + public static class ObserverLabel extends NativeLabel { + private void onPrintEvent(@Observes PrintEvent printEvent) { + setText(printEvent.getMessage()); + } + } + + public static class PrintEvent { + private final String message; + + public PrintEvent(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + @Inject + @RouteScopeOwner(EventView.class) + private NativeLabel label; + @Inject + private Event printEventTrigger; + + @PostConstruct + private void init() { + label.setId(OBSERVER_LABEL); + NativeButton fireBtn = new NativeButton("fire event", + clickEvent -> printEventTrigger.fire(new PrintEvent("HELLO"))); + fireBtn.setId(FIRE); + + add(fireBtn, label); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java new file mode 100644 index 00000000000..2839fdffb8d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/InvalidView.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("invalid-injection") +@CdiComponent +public class InvalidView extends Div { + + @Inject + @RouteScopeOwner(ErrorParentView.class) + /* + * There is no a ErrorParentView in navigation: this injection has no scope. + * Pseudo-scope @RouteScoped is used here with the component to immediately + * get an exception, for normal scope proxy is created and the exception + * won't be thrown immediately. + */ + private ErrorParentView bean; + + public InvalidView() { + setId("invalid-injection"); + setText("This view should not be shown since the " + + "injection has no scope and an exception should be thrown"); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView.java new file mode 100644 index 00000000000..5a67ee3048a --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route(value = "layout-event", layout = EventObserverLayout.class) +@RouteScoped +@CdiComponent +public class LayoutEventView extends Div { + + public static final String FIRE = "FIRE"; + public static final String CHANGE_VIEW = "CHANGE_VIEW"; + public static final String VIEW_NAME = "VIEW_NAME"; + @Inject + private Event eventTrigger; + + @PostConstruct + private void init() { + NativeButton fireBtn = new NativeButton("fire event", + clickEvent -> eventTrigger + .fire(EventObserverLayout.CustomEvent.INSTANCE)); + fireBtn.setId(FIRE); + NativeButton navigateBtn = new NativeButton("change view", + clickEvent -> UI.getCurrent().navigate(LayoutEventView2.class)); + navigateBtn.setId(CHANGE_VIEW); + + Span viewName = new Span("Layout Event View 1"); + viewName.setId(VIEW_NAME); + add(viewName, fireBtn, navigateBtn); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView2.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView2.java new file mode 100644 index 00000000000..3c408cec5ff --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutEventView2.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route(value = "layout-event-2", layout = EventObserverLayout.class) +@RouteScoped +@CdiComponent +public class LayoutEventView2 extends Div { + + public static final String FIRE = "FIRE"; + public static final String CHANGE_VIEW = "CHANGE_VIEW"; + public static final String VIEW_NAME = "VIEW_NAME"; + @Inject + private Event eventTrigger; + + @PostConstruct + private void init() { + NativeButton fireBtn = new NativeButton("fire event", + clickEvent -> eventTrigger + .fire(EventObserverLayout.CustomEvent.INSTANCE)); + fireBtn.setId(FIRE); + NativeButton navigateBtn = new NativeButton("change view", + clickEvent -> UI.getCurrent().navigate(LayoutEventView.class)); + navigateBtn.setId(CHANGE_VIEW); + Span viewName = new Span("Layout Event View 2"); + viewName.setId(VIEW_NAME); + add(viewName, fireBtn, navigateBtn); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPlainView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPlainView.java new file mode 100644 index 00000000000..aadaaee932e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPlainView.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; + +/** + * A plain sibling of {@link LayoutScopedPreservedView} under the same layout. + * The layout instance is reused when navigating between the two, so the beans + * it owns must not be recreated. + */ +@Route(value = "layout-scoped-plain", layout = MainLayout.class) +@CdiComponent +public class LayoutScopedPlainView extends Div { + + public static final String LAYOUT_BEAN_LABEL = "plain-layout-bean"; + + @Inject + @RouteScopeOwner(MainLayout.class) + private Instance injection; + + @PostConstruct + private void init() { + NativeLabel beanLabel = new NativeLabel(injection.get().getData()); + beanLabel.setId(LAYOUT_BEAN_LABEL); + add(beanLabel); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPreservedView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPreservedView.java new file mode 100644 index 00000000000..b8a65edaa71 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/LayoutScopedPreservedView.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.PreserveOnRefresh; +import com.vaadin.flow.router.Route; + +/** + * A preserved child of {@link MainLayout}, which is not preserved itself. Flow + * preserves the whole chain, so the beans owned by the layout are shared with + * the UI created by a page refresh. + */ +@PreserveOnRefresh +@Route(value = "layout-scoped-preserved", layout = MainLayout.class) +@CdiComponent +public class LayoutScopedPreservedView extends Div { + + public static final String LAYOUT_BEAN_LABEL = "preserved-layout-bean"; + + @Inject + @RouteScopeOwner(MainLayout.class) + private Instance injection; + + private NativeLabel beanLabel; + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (beanLabel != null) { + remove(beanLabel); + } + beanLabel = new NativeLabel(injection.get().getData()); + beanLabel.setId(LAYOUT_BEAN_LABEL); + add(beanLabel); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java new file mode 100644 index 00000000000..946c7c0bf3d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayout.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +public class MainLayout extends Div implements RouterLayout { + + public static final String UIID = "UIID"; + + public static final String PRESERVE = "preserve"; + public static final String INVALID = "invalid"; + public static final String PARENT_NO_OWNER = "parent-no-owner"; + public static final String CHILD_NO_OWNER = "child-no-owner"; + public static final String LAYOUT_PRESERVED = "layout-preserved"; + public static final String LAYOUT_PLAIN = "layout-plain"; + + private NativeLabel uiIdLabel; + + public MainLayout() { + add(new RouterLink(PRESERVE, PreserveOnRefreshView.class), + new RouterLink(INVALID, InvalidView.class), + new RouterLink(PARENT_NO_OWNER, ParentNoOwnerView.class), + new RouterLink(CHILD_NO_OWNER, ChildNoOwnerView.class), + new RouterLink(LAYOUT_PRESERVED, + LayoutScopedPreservedView.class), + new RouterLink(LAYOUT_PLAIN, LayoutScopedPlainView.class)); + ; + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (uiIdLabel != null) { + remove(uiIdLabel); + } + uiIdLabel = new NativeLabel(attachEvent.getUI().getUIId() + ""); + uiIdLabel.setId(UIID); + add(uiIdLabel); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayoutBean.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayoutBean.java new file mode 100644 index 00000000000..1f827d22e7d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MainLayoutBean.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; + +/** + * A bean owned by the layout shared by a preserved and a plain child view. + */ +@RouteScoped +@RouteScopeOwner(MainLayout.class) +@CdiComponent +public class MainLayoutBean extends AbstractCountedBean { + + @PostConstruct + private void init() { + setData(UUID.randomUUID().toString()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java new file mode 100644 index 00000000000..7384e3e836c --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/MasterView.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RoutePrefix; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +@RouteScoped +@Route("") +@RoutePrefix("master") +@CdiComponent +public class MasterView extends AbstractCountedView + implements RouterLayout, AfterNavigationObserver { + + public static final String ASSIGNED = "assigned"; + public static final String ASSIGNED_BEAN_LABEL = "ASSIGNED"; + public static final String APART = "apart"; + public static final String APART_BEAN_LABEL = "APART"; + + @Inject + @RouteScopeOwner(MasterView.class) + private AssignedBean assignedBean; + private NativeLabel assignedLabel; + + @PostConstruct + private void init() { + assignedLabel = new NativeLabel(); + assignedLabel.setId(ASSIGNED_BEAN_LABEL); + add(new NativeLabel("MASTER"), new Div(assignedLabel), + new Div(new RouterLink(ASSIGNED, DetailAssignedView.class)), + new Div(new RouterLink(APART, DetailApartView.class))); + } + + @Override + public void afterNavigation(AfterNavigationEvent event) { + assignedLabel.setText(assignedBean.getData()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java new file mode 100644 index 00000000000..3264dd9a064 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/ParentNoOwnerView.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.router.RouterLink; + +@Route("parent-no-owner") +@CdiComponent +public class ParentNoOwnerView extends Div implements RouterLayout { + + @Inject + private BeanNoOwner bean; + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (attachEvent.isInitialAttach()) { + RouterLink link = new RouterLink("child", ChildNoOwnerView.class); + link.setId("to-child"); + add(link); + + Div div = new Div(); + div.setId("parent-info"); + div.getElement().getStyle().set("display", "block"); + div.setText(bean.getData()); + add(div); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PostponeView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PostponeView.java new file mode 100644 index 00000000000..15e6e385278 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PostponeView.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.BeforeLeaveEvent; +import com.vaadin.flow.router.BeforeLeaveObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route("postpone") +@RouteScoped +@CdiComponent +public class PostponeView extends AbstractCountedView + implements BeforeLeaveObserver { + + public static final String NAVIGATE = "NAVIGATE"; + public static final String POSTPONED_ROOT = "postpone"; + + private BeforeLeaveEvent.ContinueNavigationAction navigationAction; + + @PostConstruct + private void init() { + NativeButton navBtn = new NativeButton("navigate", + clickEvent -> navigationAction.proceed()); + navBtn.setId(NAVIGATE); + + add(new Div(new RouterLink(POSTPONED_ROOT, RootView.class)), + new Div(navBtn)); + } + + @Override + public void beforeLeave(BeforeLeaveEvent beforeLeaveEvent) { + navigationAction = beforeLeaveEvent.postpone(); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java new file mode 100644 index 00000000000..279420a9ca7 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; + +import java.util.UUID; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; + +@RouteScoped +@RouteScopeOwner(PreserveOnRefreshView.class) +@CdiComponent +public class PreserveOnRefreshBean extends AbstractCountedBean { + + @PostConstruct + private void init() { + setData(UUID.randomUUID().toString()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java new file mode 100644 index 00000000000..a3b1f168467 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/PreserveOnRefreshView.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.PreserveOnRefresh; +import com.vaadin.flow.router.Route; + +@PreserveOnRefresh +@Route(value = "preserve-on-refresh", layout = MainLayout.class) +@CdiComponent +public class PreserveOnRefreshView extends Div { + + @Inject + @RouteScopeOwner(PreserveOnRefreshView.class) + private Instance injection; + + public PreserveOnRefreshView() { + setId("preserve-on-refresh"); + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + setText(injection.get().getData()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RerouteView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RerouteView.java new file mode 100644 index 00000000000..4b010275d03 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RerouteView.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.Route; + +@Route("reroute") +@RouteScoped +@CdiComponent +public class RerouteView extends AbstractCountedView + implements BeforeEnterObserver { + + @Override + public void beforeEnter(BeforeEnterEvent event) { + event.rerouteTo(RootView.class); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java new file mode 100644 index 00000000000..ede322cd43a --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/routecontext/RootView.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.routecontext; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route(value = "", layout = MainLayout.class) +@RouteScoped +@CdiComponent +public class RootView extends AbstractCountedView { + + public static final String MASTER = "master"; + public static final String REROUTE = "reroute"; + public static final String POSTPONE = "postpone"; + public static final String EVENT = "event"; + public static final String LAYOUT_EVENT = "layout-event"; + public static final String ERROR = "ERROR"; + + @PostConstruct + private void init() { + add(new Div(new NativeLabel("ROOT")), + new Div(new RouterLink(MASTER, MasterView.class)), + new Div(new RouterLink(REROUTE, RerouteView.class)), + new Div(new RouterLink(POSTPONE, PostponeView.class)), + new Div(new RouterLink(EVENT, EventView.class)), + new Div(new RouterLink(LAYOUT_EVENT, LayoutEventView.class)), + new Div(new RouterLink(ERROR, ErrorView.class))); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizeView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizeView.java new file mode 100644 index 00000000000..d4b91a632e6 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizeView.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("bootstrap") +@CdiComponent +public class BootstrapCustomizeView extends Div { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizer.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizer.java new file mode 100644 index 00000000000..7038d4938fc --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/BootstrapCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import jakarta.enterprise.event.Observes; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.communication.IndexHtmlResponse; + +@CdiComponent +public class BootstrapCustomizer { + + public static final String APPENDED_ID = "TEST_ID"; + public static final String APPENDED_TXT = "By Test"; + + private void onServiceInit(@Observes ServiceInitEvent serviceInitEvent) { + serviceInitEvent.addIndexHtmlRequestListener(this::modifyBootstrapPage); + } + + private void modifyBootstrapPage(IndexHtmlResponse response) { + response.getDocument().body() + .append("

" + APPENDED_TXT + "

"); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/EventObserver.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/EventObserver.java new file mode 100644 index 00000000000..1b9476cd98d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/EventObserver.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.itest.Counter; +import com.vaadin.flow.server.SessionDestroyEvent; +import com.vaadin.flow.server.SessionInitEvent; +import com.vaadin.flow.server.UIInitEvent; + +@CdiComponent +public class EventObserver { + @Inject + private Counter counter; + + private void onSessionInit(@Observes SessionInitEvent sessionInitEvent) { + counter.increment(SessionInitEvent.class.getSimpleName()); + } + + private void onSessionDestroy( + @Observes SessionDestroyEvent sessionDestroyEvent) { + counter.increment(SessionDestroyEvent.class.getSimpleName()); + } + + private void onUIInit(@Observes UIInitEvent uiInitEvent) { + counter.increment(UIInitEvent.class.getSimpleName()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/ServiceView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/ServiceView.java new file mode 100644 index 00000000000..44196649d18 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/ServiceView.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; + +@Route("") +@CdiComponent +public class ServiceView extends Div { + + public static final String EXPIRE = "EXPIRE"; + public static final String ACTION = "ACTION"; + public static final String FAIL = "FAIL"; + + @PostConstruct + private void init() { + NativeButton expireBtn = new NativeButton("expire", + event -> VaadinSession.getCurrent().getSession().invalidate()); + expireBtn.setId(EXPIRE); + + NativeButton actionButton = new NativeButton("an action", event -> { + }); + actionButton.setId(ACTION); + + NativeButton failBtn = new NativeButton("fail", event -> { + if (true) { + throw new NullPointerException(); + } + }); + failBtn.setId(FAIL); + + add(expireBtn, actionButton, failBtn); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestErrorHandler.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestErrorHandler.java new file mode 100644 index 00000000000..6f38189dc28 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestErrorHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.itest.Counter; +import com.vaadin.flow.server.ErrorEvent; +import com.vaadin.flow.server.ErrorHandler; + +@VaadinServiceEnabled +@VaadinServiceScoped +public class TestErrorHandler implements ErrorHandler { + + @Inject + private Counter counter; + + @Override + public void error(ErrorEvent event) { + counter.increment(TestErrorHandler.class.getSimpleName()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestSystemMessagesProvider.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestSystemMessagesProvider.java new file mode 100644 index 00000000000..e19a493c7aa --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/service/TestSystemMessagesProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.service; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.flow.server.CustomizedSystemMessages; +import com.vaadin.flow.server.SystemMessages; +import com.vaadin.flow.server.SystemMessagesInfo; +import com.vaadin.flow.server.SystemMessagesProvider; + +@VaadinServiceEnabled +@VaadinServiceScoped +public class TestSystemMessagesProvider implements SystemMessagesProvider { + + public static final String EXPIRED_BY_TEST = "EXPIRED BY TEST"; + + @Override + public SystemMessages getSystemMessages( + SystemMessagesInfo systemMessagesInfo) { + CustomizedSystemMessages messages = new CustomizedSystemMessages(); + messages.setSessionExpiredNotificationEnabled(true); + messages.setSessionExpiredMessage(EXPIRED_BY_TEST); + // On Wildfy this message pops up after session invalidate button. + messages.setCookiesDisabledMessage(EXPIRED_BY_TEST); + return messages; + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontext/SessionContextView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontext/SessionContextView.java new file mode 100644 index 00000000000..0b2ef8a7d34 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontext/SessionContextView.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.sessioncontext; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.itest.Counter; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; + +@Route("") +@CdiComponent +public class SessionContextView extends Div { + + public static final String SETVALUEBTN_ID = "setvalbtn"; + public static final String VALUELABEL_ID = "label"; + public static final String VALUE = "session"; + public static final String INVALIDATEBTN_ID = "invalidatebtn"; + public static final String HTTP_INVALIDATEBTN_ID = "httpinvalidatebtn"; + public static final String EXPIREBTN_ID = "expirebtn"; + + @Inject + private SessionScopedBean sessionScopedBean; + + @PostConstruct + private void init() { + NativeButton setBtn = new NativeButton("set"); + setBtn.addClickListener(event -> sessionScopedBean.setValue(VALUE)); + setBtn.setId(SETVALUEBTN_ID); + add(setBtn); + + NativeButton invalidateBtn = new NativeButton("invalidate"); + invalidateBtn + .addClickListener(event -> VaadinSession.getCurrent().close()); + invalidateBtn.setId(INVALIDATEBTN_ID); + add(invalidateBtn); + + NativeButton httpInvalidateBtn = new NativeButton("httpinvalidate"); + httpInvalidateBtn.addClickListener( + event -> VaadinSession.getCurrent().getSession().invalidate()); + httpInvalidateBtn.setId(HTTP_INVALIDATEBTN_ID); + add(httpInvalidateBtn); + + NativeButton expireBtn = new NativeButton("httpexpire"); + expireBtn.addClickListener(event -> VaadinSession.getCurrent() + .getSession().setMaxInactiveInterval(1)); + expireBtn.setId(EXPIREBTN_ID); + add(expireBtn); + + NativeLabel label = new NativeLabel(); + label.setText(sessionScopedBean.getValue()); // bean instantiated here + label.setId(VALUELABEL_ID); + add(label); + } + + @VaadinSessionScoped + // Like other vaadin scopes, + // Serializable is mandatory only if you want a working session + // serialization + public static class SessionScopedBean { + public static final String DESTROY_COUNT = "SessionScopedBeanDestroy"; + + @Inject + private Counter counter; + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @PreDestroy + private void preDestroy() { + counter.increment(DESTROY_COUNT); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/LenientSessionContextManager.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/LenientSessionContextManager.java new file mode 100644 index 00000000000..ac14dd93134 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/LenientSessionContextManager.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.sessioncontextspecializes; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.inject.Specializes; + +import java.util.concurrent.atomic.AtomicReference; + +import com.vaadin.cdi.context.VaadinSessionScopedContext; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.server.VaadinSession; + +/** + * Replaces the framework-default + * {@link VaadinSessionScopedContext.ContextualStorageManager} via + * {@code @Specializes} to activate the {@code @VaadinSessionScoped} context + * whenever a {@link VaadinSession} is set on the current thread, and to acquire + * the session lock around storage access when the calling thread does not + * already hold it. + */ +@ApplicationScoped +@Specializes +public class LenientSessionContextManager + extends VaadinSessionScopedContext.ContextualStorageManager { + + @Override + protected boolean isActive() { + return VaadinSession.getCurrent() != null; + } + + @Override + protected ContextualStorage getContextualStorage(Contextual contextual, + boolean createIfNotExist) { + VaadinSession session = VaadinSession.getCurrent(); + if (session.hasLock()) { + return super.getContextualStorage(contextual, createIfNotExist); + } + AtomicReference ref = new AtomicReference<>(); + session.accessSynchronously(() -> ref + .set(super.getContextualStorage(contextual, createIfNotExist))); + return ref.get(); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/SessionContextSpecializesView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/SessionContextSpecializesView.java new file mode 100644 index 00000000000..7bdf8827e3e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/sessioncontextspecializes/SessionContextSpecializesView.java @@ -0,0 +1,126 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.sessioncontextspecializes; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.itest.Counter; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; + +/** + * Background-thread probe used by both strict-default and {@code @Specializes} + * integration tests. From a thread that has only set the {@link VaadinSession} + * thread-local — without acquiring the session lock — the view calls a method + * on a {@code @VaadinSessionScoped} bean and fires a CDI event observed by the + * same bean. + *

+ * Outcomes are recorded via the application-scoped {@link Counter} so that + * failures on the session-scoped proxy don't lose information: + * {@link #DIRECT_CALL_COUNT} is incremented if the proxy call succeeds, + * {@link #OBSERVED_COUNT} if the observer is invoked, and {@link #ERROR_COUNT} + * for each {@link RuntimeException} caught by the background thread. + * {@link #DONE_COUNT} is incremented when the background thread finishes, + * regardless of outcome — tests synchronize on it before asserting the other + * counters. + */ +@Route("") +@CdiComponent +public class SessionContextSpecializesView extends Div { + + public static final String FIREBTN_ID = "firebtn"; + public static final String OBSERVED_COUNT = "specializesObserved"; + public static final String DIRECT_CALL_COUNT = "specializesDirectCall"; + public static final String ERROR_COUNT = "specializesError"; + public static final String UNEXPECTED_ERROR_COUNT = "specializesUnexpectedError"; + public static final String DONE_COUNT = "specializesBackgroundDone"; + + @Inject + private SessionScopedObserver observer; + + @Inject + private Event eventBus; + + @Inject + private Counter counter; + + @PostConstruct + private void init() { + NativeButton fireBtn = new NativeButton("fire-background"); + fireBtn.addClickListener(click -> { + VaadinSession session = VaadinSession.getCurrent(); + Thread thread = new Thread(() -> { + VaadinSession previous = VaadinSession.getCurrent(); + VaadinSession.setCurrent(session); + try { + try { + // Touch a @VaadinSessionScoped bean directly from a + // background thread that does NOT hold the session + // lock. + observer.recordDirectCall(); + } catch (ContextNotActiveException e) { + counter.increment(ERROR_COUNT); + } catch (RuntimeException e) { + counter.increment(UNEXPECTED_ERROR_COUNT); + } + try { + // Fire a CDI event; the observer is on a + // @VaadinSessionScoped bean. + eventBus.fire(new BackgroundEvent()); + } catch (ContextNotActiveException e) { + counter.increment(ERROR_COUNT); + } catch (RuntimeException e) { + counter.increment(UNEXPECTED_ERROR_COUNT); + } + } finally { + VaadinSession.setCurrent(previous); + // Completion signal: tests wait for this counter before + // asserting the other ones. + counter.increment(DONE_COUNT); + } + }, "background-event-fire"); + thread.start(); + }); + fireBtn.setId(FIREBTN_ID); + add(fireBtn); + } + + public static class BackgroundEvent { + } + + @VaadinSessionScoped + public static class SessionScopedObserver { + + @Inject + private Counter counter; + + public void recordDirectCall() { + counter.increment(DIRECT_CALL_COUNT); + } + + private void onBackgroundEvent(@Observes BackgroundEvent event) { + counter.increment(OBSERVED_COUNT); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/CdiView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/CdiView.java new file mode 100644 index 00000000000..d354b036cb9 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/CdiView.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.smoke; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Inject; + +import java.io.Serializable; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; + +@Route("") +@CdiComponent +public class CdiView extends Div { + @Inject + private HelloProvider helloProvider; + + public CdiView() { + add(new NativeButton("Click me", + event -> add(new NativeLabel(getLabelText())))); + } + + private String getLabelText() { + if (helloProvider != null) { + return helloProvider.getHello(); + } else { + return "no CDI"; + } + } + + @PostConstruct + private void init() { + helloProvider.setHello("hello CDI"); + } + + @SessionScoped + public static class HelloProvider implements Serializable { + private String hello; + + public String getHello() { + return hello; + } + + public void setHello(String hello) { + this.hello = hello; + } + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/ProxiedNavigationTargetView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/ProxiedNavigationTargetView.java new file mode 100644 index 00000000000..07ae6b7ef80 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/ProxiedNavigationTargetView.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.smoke; + +import jakarta.interceptor.Interceptors; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.OptionalParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route("proxied") +@CdiComponent +// @Interceptors annotation should cause CDI to create +// a proxy class for the bean instance +@Interceptors(UselessInterceptor.class) +public class ProxiedNavigationTargetView extends Div + implements HasUrlParameter { + + private final String uuid = UUID.randomUUID().toString(); + private final AtomicInteger counter = new AtomicInteger(); + private final RouterLink routerLink; + private final Div clickCounter; + + public ProxiedNavigationTargetView() { + Div uuid = new Div(this.uuid); + uuid.setId("COMPONENT_ID"); + add(uuid); + + clickCounter = new Div("P:0, C:0"); + clickCounter.setId("CLICK_COUNTER"); + add(clickCounter); + + // Self navigation should use the same view instance + routerLink = new RouterLink("Self Link", + ProxiedNavigationTargetView.class, counter.incrementAndGet()); + add(routerLink); + } + + @Override + public void setParameter(BeforeEvent event, + @OptionalParameter Integer parameter) { + if (parameter != null) { + clickCounter.setText("P:" + parameter + ", C:" + counter.get()); + routerLink.setRoute(ProxiedNavigationTargetView.class, + counter.incrementAndGet()); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/UselessInterceptor.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/UselessInterceptor.java new file mode 100644 index 00000000000..620fa95751e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/smoke/UselessInterceptor.java @@ -0,0 +1,19 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.smoke; + +public class UselessInterceptor { +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/template/TestTemplate.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/template/TestTemplate.java new file mode 100644 index 00000000000..f7d2b1e9787 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/template/TestTemplate.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.template; + +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.component.polymertemplate.Id; +import com.vaadin.flow.component.polymertemplate.PolymerTemplate; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.templatemodel.TemplateModel; + +@Tag("test-template") +@JsModule("./test-template.js") +@UIScoped +@Route("") +@CdiComponent +public class TestTemplate extends PolymerTemplate { + private @Id("input") Input input; + + private @Id("label") NativeLabel label; + + private @Inject Event setTextEventTrigger; + + public TestTemplate() { + input.addValueChangeListener(event -> { + setTextEventTrigger.fire(new InputChangeEvent(input.getValue())); + }); + } + + private void onSetText(@Observes InputChangeEvent event) { + label.setText(event.getText()); + } + + public static class InputChangeEvent { + private final String text; + + public InputChangeEvent(String text) { + this.text = text; + } + + public String getText() { + return text; + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIContextRootView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIContextRootView.java new file mode 100644 index 00000000000..8096dfa16dc --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIContextRootView.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uicontext; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Event; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.itest.uicontext.UIScopedLabel.SetTextEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route("") +@CdiComponent +public class UIContextRootView extends Div { + + public static final String CLOSE_UI_BTN = "CLOSE_UI_BTN"; + public static final String CLOSE_SESSION_BTN = "CLOSE_SESSION_BTN"; + public static final String TRIGGER_EVENT_BTN = "TRIGGER_EVENT_BTN"; + public static final String INJECTER_LINK = "injecter view"; + public static final String UISCOPED_LINK = "uiscoped view"; + public static final String UIID_LABEL = "UIID_LABEL"; + public static final String NORMALSCOPED_LINK = "normalscoped bean view"; + public static final String EVENT_PAYLOAD = "EVENT_PAYLOAD"; + + @Inject + private UIScopedLabel label; + + @Inject + private Event setTextEventTrigger; + + @PostConstruct + private void init() { + final String uiIdStr = UI.getCurrent().getUIId() + ""; + label.setText(uiIdStr); + + final NativeLabel uiId = new NativeLabel(uiIdStr); + uiId.setId(UIID_LABEL); + + final NativeButton closeUI = new NativeButton("close UI", + event -> getUI().ifPresent(UI::close)); + closeUI.setId(CLOSE_UI_BTN); + + final NativeButton closeSession = new NativeButton("close session", + event -> getUI().ifPresent(ui -> ui.getSession().close())); + closeSession.setId(CLOSE_SESSION_BTN); + + final NativeButton triggerEvent = new NativeButton("event trigger", + event -> setTextEventTrigger + .fire(new SetTextEvent(EVENT_PAYLOAD))); + triggerEvent.setId(TRIGGER_EVENT_BTN); + + final Div navDiv = new Div( + new RouterLink(INJECTER_LINK, UIScopeInjecterView.class), + new RouterLink(UISCOPED_LINK, UIScopedView.class), + new RouterLink(NORMALSCOPED_LINK, + UINormalScopedBeanView.class)); + + add(new Div(uiId), new Div(closeUI, closeSession), + new Div(triggerEvent), new Div(this.label), navDiv); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UINormalScopedBeanView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UINormalScopedBeanView.java new file mode 100644 index 00000000000..967d4f3d059 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UINormalScopedBeanView.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uicontext; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.NormalUIScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; + +@Route("normalscopedbean") +@CdiComponent +public class UINormalScopedBeanView extends Div { + + public static final String UIID_LABEL = "UIID_LABEL"; + + @Inject + private SessionScopedUIidService sessionScopedUIidService; + + @PostConstruct + private void init() { + final NativeLabel label = new NativeLabel( + sessionScopedUIidService.getUiIdStr()); + label.setId(UIID_LABEL); + add(label); + } + + @NormalUIScoped + public static class NormalUIScopedUIidService { + private String uiIdStr; + + @PostConstruct + public void init() { + uiIdStr = UI.getCurrent().getUIId() + ""; + } + + public String getUiIdStr() { + return uiIdStr; + } + } + + @VaadinSessionScoped + public static class SessionScopedUIidService { + @Inject + private NormalUIScopedUIidService normalUIScopedUIidService; + + public String getUiIdStr() { + return normalUIScopedUIidService.getUiIdStr(); + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopeInjecterView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopeInjecterView.java new file mode 100644 index 00000000000..6acb9788dda --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopeInjecterView.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uicontext; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.router.Route; + +@Route("injecter") +@CdiComponent +public class UIScopeInjecterView extends Div { + @Inject + private UIScopedLabel label; + + @PostConstruct + private void init() { + add(label); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedLabel.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedLabel.java new file mode 100644 index 00000000000..4e04ddb9572 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedLabel.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uicontext; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.itest.Counter; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.NativeLabel; + +@UIScoped +@CdiComponent +public class UIScopedLabel extends NativeLabel { + + public static final String DESTROY_COUNT = "UIScopedLabelDestroy"; + + @Inject + private Counter counter; + + private int uiId; + + public static final String ID = "UISCOPED_LABEL"; + + public UIScopedLabel() { + setId(ID); + uiId = UI.getCurrent().getUIId(); + } + + @PreDestroy + private void destroy() { + counter.increment(DESTROY_COUNT + uiId); + } + + private void onSetText(@Observes SetTextEvent event) { + setText(event.getText()); + } + + public static class SetTextEvent { + private final String text; + + public SetTextEvent(String text) { + this.text = text; + } + + public String getText() { + return text; + } + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedView.java new file mode 100644 index 00000000000..e7315768f15 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uicontext/UIScopedView.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uicontext; + +import jakarta.annotation.PostConstruct; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLink; + +@Route("uiscoped") +@UIScoped +@CdiComponent +public class UIScopedView extends Div { + + public static final String VIEWSTATE_LABEL = "VIEWSTATE_LABEL"; + public static final String SETSTATE_BTN = "SETSTATE_BTN"; + public static final String ROOT_LINK = "root view"; + public static final String UISCOPED_STATE = "UISCOPED_STATE"; + + @PostConstruct + private void init() { + final NativeLabel state = new NativeLabel(""); + state.setId(VIEWSTATE_LABEL); + + final NativeButton button = new NativeButton("set state", + event -> state.setText(UISCOPED_STATE)); + button.setId(SETSTATE_BTN); + + add(button, state, new RouterLink(ROOT_LINK, UIContextRootView.class)); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/NavigationObserver.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/NavigationObserver.java new file mode 100644 index 00000000000..5880c5acc43 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/NavigationObserver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uievents; + +import jakarta.enterprise.event.Observes; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; + +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeLeaveEvent; + +@VaadinSessionScoped +public class NavigationObserver implements Serializable { + + private List navigationEvents = new ArrayList<>(); + + private void onBeforeLeave(@Observes BeforeLeaveEvent event) { + navigationEvents.add(event); + } + + private void onBeforeEnter(@Observes BeforeEnterEvent event) { + navigationEvents.add(event); + } + + private void onAfterNavigation(@Observes AfterNavigationEvent event) { + navigationEvents.add(event); + } + + public List getNavigationEvents() { + return navigationEvents; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/UIEventsView.java b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/UIEventsView.java new file mode 100644 index 00000000000..6d53869482c --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/java/com/vaadin/cdi/itest/uievents/UIEventsView.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest.uievents; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import java.util.EventObject; +import java.util.List; + +import com.vaadin.cdi.annotation.CdiComponent; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.flow.component.PollEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeLabel; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationObserver; +import com.vaadin.flow.router.Route; + +@Route("uievents") +@UIScoped +@CdiComponent +public class UIEventsView extends Div implements AfterNavigationObserver { + + public static final String POLL_FROM_CLIENT = "POLL_FROM_CLIENT"; + public static final String NAVIGATION_EVENTS = "NAVIGATION_EVENTS"; + + @Inject + private NavigationObserver navigationObserver; + + @PostConstruct + private void init() { + UI.getCurrent().setPollInterval(500); + } + + @Override + public void afterNavigation(AfterNavigationEvent afterNavigationEvent) { + showNavigationEvents(); + } + + private void showNavigationEvents() { + Div events = new Div(); + events.setId(NAVIGATION_EVENTS); + List navigationEvents = navigationObserver + .getNavigationEvents(); + navigationEvents.stream() + .map(event -> new NativeLabel(event.getClass().getSimpleName())) + .forEach(events::add); + add(events); + } + + private void showPollEvent(@Observes PollEvent pollEvent) { + final NativeLabel poll = new NativeLabel(pollEvent.isFromClient() + ""); + poll.setId(POLL_FROM_CLIENT); + add(new Div(poll)); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/beans.xml b/flow-tests/vaadin-cdi-tests/src/main/resources/beans.xml new file mode 100644 index 00000000000..48f4180aec4 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/disablecdi-web.xml b/flow-tests/vaadin-cdi-tests/src/main/resources/disablecdi-web.xml new file mode 100644 index 00000000000..7afbe0008fa --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/disablecdi-web.xml @@ -0,0 +1,34 @@ + + + + + + Vaadin Servlet + com.vaadin.flow.server.VaadinServlet + true + + + + Vaadin Servlet + /* + + + diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations.properties b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations.properties new file mode 100644 index 00000000000..d8e29a944ad --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations.properties @@ -0,0 +1 @@ +label=Default diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_de.properties b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_de.properties new file mode 100644 index 00000000000..5afe465e628 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_de.properties @@ -0,0 +1 @@ +label=Deutsch diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fi_FI.properties b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fi_FI.properties new file mode 100644 index 00000000000..38e68f8f24e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fi_FI.properties @@ -0,0 +1 @@ +label=Suomi diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fr_FR.properties b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fr_FR.properties new file mode 100644 index 00000000000..ef14ff62a17 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_fr_FR.properties @@ -0,0 +1 @@ +label=français diff --git a/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_ja_JP.properties b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_ja_JP.properties new file mode 100644 index 00000000000..16d1921d5e0 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/main/resources/vaadin-i18n/translations_ja_JP.properties @@ -0,0 +1 @@ +label=日本語 diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/AbstractCdiTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/AbstractCdiTest.java new file mode 100644 index 00000000000..43b997842fd --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/AbstractCdiTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.URL; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; + +import com.vaadin.flow.testutil.ChromeBrowserTest; +import com.vaadin.testbench.parallel.Browser; + +@RunWith(Arquillian.class) +@RunAsClient +abstract public class AbstractCdiTest extends ChromeBrowserTest { + + @ArquillianResource + protected URL deploymentUrl; + + @Override + protected String getRootURL() { + return super.getRootURL() + deploymentUrl.getPath(); + } + + @Override + protected int getDeploymentPort() { + return deploymentUrl.getPort(); + } + + @Override + protected String getTestPath() { + return "/"; + } + + @Override + public void setup() throws Exception { + setDesiredCapabilities(Browser.CHROME.getDesiredCapabilities()); + super.setup(); + } + + protected void click(String elementId) { + findElement(By.id(elementId)).click(); + } + + protected void follow(String linkText) { + findElement(By.linkText(linkText)).click(); + } + + protected String getText(String id) { + return findElement(By.id(id)).getText(); + } + + protected void assertCountEquals(int expectedCount, String counter) + throws IOException { + Assert.assertEquals(expectedCount, getCount(counter)); + } + + protected void waitForCount(int expectedCount, String counter) { + waitUntil(driver -> { + try { + return getCount(counter) == expectedCount; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }, 10); + } + + protected void assertTextEquals(String expectedText, String elementId) { + Assert.assertEquals(expectedText, getText(elementId)); + } + + protected void resetCounts() throws IOException { + slurp("?resetCounts"); + } + + protected int getCount(String id) throws IOException { + getCommandExecutor().waitForVaadin(); + String line = slurp("?getCount=" + id); + return Integer.parseInt(line); + } + + private String slurp(String uri) throws IOException { + URL url = new URL(getRootURL() + uri); + InputStream is = url.openConnection().getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line = reader.readLine(); + reader.close(); + return line; + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ArchiveProvider.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ArchiveProvider.java new file mode 100644 index 00000000000..394dd643c8b --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ArchiveProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.File; +import java.util.function.Consumer; + +import org.jboss.shrinkwrap.api.ArchivePaths; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.PomEquippedResolveStage; + +public class ArchiveProvider { + + public static WebArchive createWebArchive(String warName, + Consumer customizer) { + WebArchive archive = base(warName); + customizer.accept(archive); + return archive; + } + + public static WebArchive createWebArchive(String warName, + Class... classes) { + return createWebArchive(warName, archive -> archive.addClasses(classes) + .addAsResource(new File("target/classes/META-INF"))); + } + + private static WebArchive base(String warName) { + PomEquippedResolveStage pom = Maven.configureResolver().workOffline() + .loadPomFromFile("target/effective-pom.xml"); + WebArchive archive = ShrinkWrap + .create(WebArchive.class, warName + ".war") + .addAsLibraries(pom.resolve("com.vaadin:vaadin-cdi", + "com.vaadin:flow-server", "com.vaadin:flow-client", + "com.vaadin:flow-html-components", + "com.vaadin:flow-polymer-template").withTransitivity() + .asFile()) + .addAsWebInfResource(EmptyAsset.INSTANCE, + ArchivePaths.create("beans.xml")) + .addClasses(Counter.class, CounterFilter.class); + return applyContainerConfigurations(archive, pom); + } + + private static WebArchive applyContainerConfigurations(WebArchive archive, + PomEquippedResolveStage pom) { + // Testing Tomcat + Weld + String container = System.getProperty("arquillian.launch"); + if ("tomcat-weld".equals(container)) { + archive.addAsLibraries(pom + .resolve("org.slf4j:slf4j-simple", + "org.jboss.weld.servlet:weld-servlet-shaded") + .withoutTransitivity().asFile()); + + } + // Workaround for https://github.com/payara/Payara/issues/5898 + // Slf4J implementation lookup error in Payara 6 + else if ("payara".equals(container)) { + archive.addAsWebInfResource(AbstractCdiTest.class.getClassLoader() + .getResource("payara/glassfish-web.xml"), + "glassfish-web.xml") + .addAsLibraries(pom.resolve("org.slf4j:slf4j-simple") + .withoutTransitivity().asFile()); + + } else if ("wildfly".equals(container)) { + archive.addAsWebInfResource( + AbstractCdiTest.class.getClassLoader().getResource( + "wildfly/jboss-deployment-structure.xml"), + "jboss-deployment-structure.xml"); + } + + return archive; + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/BeanDiscoveryModeTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/BeanDiscoveryModeTest.java new file mode 100644 index 00000000000..195d94c4b6d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/BeanDiscoveryModeTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView; +import com.vaadin.cdi.itest.beandiscoverymode.CdiComponentGreetService; +import com.vaadin.cdi.itest.beandiscoverymode.NormalScopedGreetService; + +import static com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView.CDI_SRV_BTN_ID; +import static com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView.CLEAR_NAME_BTN_ID; +import static com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView.NORMAL_SRV_BTN_ID; +import static com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView.RESULT_SPAN_ID; +import static com.vaadin.cdi.itest.beandiscoverymode.BeanDiscoveryModeView.SET_NAME_BTN_ID; + +public class BeanDiscoveryModeTest extends AbstractCdiTest { + + private static final Class[] CLASSES = { BeanDiscoveryModeView.class, + CdiComponentGreetService.class, NormalScopedGreetService.class }; + + @Deployment(name = "bean-discovery-mode-annotated", testable = false) + public static WebArchive createImplicitBeanArchiveDeployment() { + return ArchiveProvider + .createWebArchive("bean-discovery-mode-annotated-test", CLASSES) + .addAsWebInfResource(ArchiveProvider.class.getClassLoader() + .getResource("beans.xml"), "beans.xml"); + } + + @Deployment(name = "bean-discovery-mode-all", testable = false) + public static WebArchive createCdiServletEnabledDeployment() { + return ArchiveProvider.createWebArchive("bean-discovery-mode-all-test", + CLASSES); + } + + @Before + public void setUp() { + open(); + } + + @After + public void cleanUp() { + click(CLEAR_NAME_BTN_ID); + } + + @Test + @OperateOnDeployment("bean-discovery-mode-annotated") + public void beanDiscoveryWorks_when_beanDiscoveryModeIsAnnotated() { + validateBeanDiscoveryAndInjectionWorks(); + } + + @Test + @OperateOnDeployment("bean-discovery-mode-all") + public void beanDiscoveryWorks_when_beanDiscoveryModeIsAll() { + validateBeanDiscoveryAndInjectionWorks(); + } + + private void validateBeanDiscoveryAndInjectionWorks() { + click(SET_NAME_BTN_ID); + click(NORMAL_SRV_BTN_ID); + waitUntilNot(driver -> getText(RESULT_SPAN_ID) == null); + Assert.assertEquals("Hello MyName from NormalScoped service.", + getText(RESULT_SPAN_ID)); + + cleanUp(); + + click(SET_NAME_BTN_ID); + click(CDI_SRV_BTN_ID); + waitUntilNot(driver -> getText(RESULT_SPAN_ID) == null); + Assert.assertEquals("Hello MyName from CDIComponent service.", + getText(RESULT_SPAN_ID)); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InstantiatorFactoryCustomizeTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InstantiatorFactoryCustomizeTest.java new file mode 100644 index 00000000000..db94d9c8191 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InstantiatorFactoryCustomizeTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorAlternative; +import com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorCustomizeView; +import com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorFactoryAlternative; +import com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorFactoryDecorator; + +import static com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorCustomizeView.CUSTOMIZED; +import static com.vaadin.cdi.itest.instantiatorcustomize.InstantiatorCustomizeView.VIEW; + +@SuppressWarnings("ArquillianTooManyDeployment") +public class InstantiatorFactoryCustomizeTest extends AbstractCdiTest { + + @Deployment(name = "decorator", testable = false) + public static WebArchive decoratorDeployment() { + return ArchiveProvider.createWebArchive("instantiator-decorator-test", + InstantiatorAlternative.class, + InstantiatorFactoryDecorator.class, + InstantiatorCustomizeView.class); + } + + @Deployment(name = "alternative", testable = false) + public static WebArchive alternativeDeployment() { + return ArchiveProvider.createWebArchive("instantiator-alternative-test", + InstantiatorAlternative.class, + InstantiatorFactoryAlternative.class, + InstantiatorCustomizeView.class); + } + + @Before + public void setUp() { + open(); + } + + @Test + @OperateOnDeployment("decorator") + public void instantiatorCustomizedByDecorator() { + assertInstantiatorCustomized(); + } + + @Test + @OperateOnDeployment("alternative") + public void instantiatorCustomizedByAlternative() { + assertInstantiatorCustomized(); + } + + private void assertInstantiatorCustomized() { + Assert.assertEquals(CUSTOMIZED, $("div").id(VIEW).getText()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InvalidDeploymentTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InvalidDeploymentTest.java new file mode 100644 index 00000000000..6993f8ce9a3 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/InvalidDeploymentTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.vaadin.cdi.itest.invaliddeployment.DeploymentView; +import com.vaadin.cdi.itest.invaliddeployment.NormalScopedLabel; + +@RunWith(Arquillian.class) +@RunAsClient +public class InvalidDeploymentTest { + + @ArquillianResource + private Deployer deployer; + + @Deployment(name = "invalid-deployment", testable = false, managed = false) + public static WebArchive invalidDeployment() { + return ArchiveProvider.createWebArchive("invalid-deployment", + DeploymentView.class, NormalScopedLabel.class); + } + + @Test(expected = Exception.class) + public void invalidDeploymentShouldBreakDeploy() { + deployer.deploy("invalid-deployment"); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/NeedsCDICompliantContainer.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/NeedsCDICompliantContainer.java new file mode 100644 index 00000000000..e4995c5f4e7 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/NeedsCDICompliantContainer.java @@ -0,0 +1,19 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +public interface NeedsCDICompliantContainer { +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushTest.java new file mode 100644 index 00000000000..0bd3938ac2f --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.SessionScoped; + +import java.io.File; +import java.lang.annotation.Annotation; + +import net.jcip.annotations.NotThreadSafe; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.openqa.selenium.By; + +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.itest.push.ManagedExecutorPushComponent; +import com.vaadin.cdi.itest.push.PushComponent; +import com.vaadin.cdi.itest.push.WebsocketPushView; +import com.vaadin.cdi.itest.push.WebsocketXhrPushView; + +@NotThreadSafe +@Category(NeedsCDICompliantContainer.class) +public class PushTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("push-managed", + webArchive -> webArchive + .addClasses(WebsocketPushView.class, + WebsocketXhrPushView.class, PushComponent.class, + ManagedExecutorPushComponent.class) + .addAsResource(new File("target/classes/META-INF"))); + } + + @Test + public void wsWithXhrBackgroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket-xhr"); + click(PushComponent.RUN_BACKGROUND); + waitForPush(); + assertAllExceptRequestAndSessionActive(); + } + + @Test + public void wsWithXhrForegroundAllContextsActive() { + getDriver().get(getTestURL() + "websocket-xhr"); + click(PushComponent.RUN_FOREGROUND); + assertContextActive(RequestScoped.class, true); + assertContextActive(SessionScoped.class, true); + assertContextActive(ApplicationScoped.class, true); + assertVaadinContextsActive(); + } + + @Test + public void wsNoXhrBackgroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket"); + click(PushComponent.RUN_BACKGROUND); + waitForPush(); + assertAllExceptRequestAndSessionActive(); + } + + @Test + public void wsNoXhrForegroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket"); + click(PushComponent.RUN_FOREGROUND); + assertAllExceptRequestAndSessionActive(); + } + + private void assertAllExceptRequestAndSessionActive() { + assertContextActive(RequestScoped.class, false); + assertContextActive(SessionScoped.class, false); + assertContextActive(ApplicationScoped.class, true); + assertVaadinContextsActive(); + } + + private void assertVaadinContextsActive() { + assertContextActive(RouteScoped.class, true); + assertContextActive(UIScoped.class, true); + assertContextActive(VaadinSessionScoped.class, true); + assertContextActive(VaadinServiceScoped.class, true); + } + + private void assertContextActive(Class scope, + boolean active) { + assertTextEquals(active + "", scope.getName()); + } + + private void waitForPush() { + waitForElementPresent(By.id(RequestScoped.class.getName())); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushWithoutManagedExecutorTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushWithoutManagedExecutorTest.java new file mode 100644 index 00000000000..415c1eccfb6 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/PushWithoutManagedExecutorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.SessionScoped; + +import java.io.File; +import java.lang.annotation.Annotation; + +import net.jcip.annotations.NotThreadSafe; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.itest.push.CustomSchedulerPushComponent; +import com.vaadin.cdi.itest.push.PushComponent; +import com.vaadin.cdi.itest.push.WebsocketPushView; +import com.vaadin.cdi.itest.push.WebsocketXhrPushView; + +@NotThreadSafe +public class PushWithoutManagedExecutorTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deploymentTomcatWeld() { + return ArchiveProvider.createWebArchive("push", webArchive -> webArchive + .addClasses(WebsocketPushView.class, WebsocketXhrPushView.class, + PushComponent.class, CustomSchedulerPushComponent.class) + .addAsResource(new File("target/classes/META-INF"))); + } + + @Test + public void wsWithXhrBackgroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket-xhr"); + click(PushComponent.RUN_BACKGROUND); + waitForPush(); + assertAllExceptRequestAndSessionActive(); + } + + @Test + public void wsWithXhrForegroundAllContextsActive() { + getDriver().get(getTestURL() + "websocket-xhr"); + click(PushComponent.RUN_FOREGROUND); + assertContextActive(RequestScoped.class, true); + assertContextActive(SessionScoped.class, true); + assertContextActive(ApplicationScoped.class, true); + assertVaadinContextsActive(); + } + + @Test + public void wsNoXhrBackgroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket"); + click(PushComponent.RUN_BACKGROUND); + waitForPush(); + assertAllExceptRequestAndSessionActive(); + } + + @Test + public void wsNoXhrForegroundRequestAndSessionDoesNotActive() { + getDriver().get(getTestURL() + "websocket"); + click(PushComponent.RUN_FOREGROUND); + assertAllExceptRequestAndSessionActive(); + } + + private void assertAllExceptRequestAndSessionActive() { + assertContextActive(RequestScoped.class, false); + assertContextActive(SessionScoped.class, false); + assertContextActive(ApplicationScoped.class, true); + assertVaadinContextsActive(); + } + + private void assertVaadinContextsActive() { + assertContextActive(RouteScoped.class, true); + assertContextActive(UIScoped.class, true); + assertContextActive(VaadinSessionScoped.class, true); + assertContextActive(VaadinServiceScoped.class, true); + } + + private void assertContextActive(Class scope, + boolean active) { + assertTextEquals(active + "", scope.getName()); + } + + private void waitForPush() { + waitForElementPresent(By.id(RequestScoped.class.getName())); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RemoveOldContentTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RemoveOldContentTest.java new file mode 100644 index 00000000000..0bd158d041f --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RemoveOldContentTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.File; +import java.util.List; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.cdi.itest.regression.RemoveOldContentView; +import com.vaadin.testbench.TestBenchElement; + +import static com.vaadin.cdi.itest.regression.RemoveOldContentView.NAVIGATE_BACK_FROM_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID; +import static com.vaadin.cdi.itest.regression.RemoveOldContentView.NAVIGATE_BACK_FROM_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID; +import static com.vaadin.cdi.itest.regression.RemoveOldContentView.NAVIGATE_TO_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID; +import static com.vaadin.cdi.itest.regression.RemoveOldContentView.NAVIGATE_TO_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID; +import static com.vaadin.cdi.itest.regression.RemoveOldContentView.SUB_LAYOUT_ID; + +public class RemoveOldContentTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("remove-old-content", + webArchive -> webArchive + .addPackage(RemoveOldContentView.class.getPackage()) + .addAsResource(new File("target/classes/META-INF"))); + } + + @Override + protected String getTestPath() { + return "/first-child-route"; + } + + @Test + public void removeUIScopedRouterLayoutContent_navigateToAnotherRouteInsideMainLayoutAndBack_subLayoutOldContentRemoved() { + open(); + waitForElementPresent( + By.id(NAVIGATE_TO_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID)); + click(NAVIGATE_TO_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID); + waitForElementPresent(By.id( + NAVIGATE_BACK_FROM_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID)); + click(NAVIGATE_BACK_FROM_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID); + waitForElementPresent(By.id(SUB_LAYOUT_ID)); + + assertSubLayoutHasNoOldContent(); + } + + @Test + public void removeUIScopedRouterLayoutContent_navigateToAnotherRouteOutsideMainLayoutAndBack_mainLayoutOldContentRemoved() { + open(); + waitForElementPresent( + By.id(NAVIGATE_TO_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID)); + click(NAVIGATE_TO_ANOTHER_ROUTE_OUTSIDE_MAIN_LAYOUT_BUTTON_ID); + waitForElementPresent(By.id( + NAVIGATE_BACK_FROM_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID)); + click(NAVIGATE_BACK_FROM_ANOTHER_ROUTE_INSIDE_MAIN_LAYOUT_BUTTON_ID); + waitForElementPresent(By.id(SUB_LAYOUT_ID)); + + assertSubLayoutHasNoOldContent(); + } + + private void assertSubLayoutHasNoOldContent() { + TestBenchElement subLayout = $("div").id(SUB_LAYOUT_ID); + List subLayoutChildren = subLayout + .findElements(By.tagName("div")); + Assert.assertEquals(1, subLayoutChildren.size()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java new file mode 100644 index 00000000000..57df6325819 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/RouteContextTest.java @@ -0,0 +1,416 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.File; +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WindowType; + +import com.vaadin.cdi.itest.routecontext.ApartBean; +import com.vaadin.cdi.itest.routecontext.AssignedBean; +import com.vaadin.cdi.itest.routecontext.BeanNoOwner; +import com.vaadin.cdi.itest.routecontext.CustomExceptionSubButton; +import com.vaadin.cdi.itest.routecontext.CustomExceptionSubDiv; +import com.vaadin.cdi.itest.routecontext.DetailApartView; +import com.vaadin.cdi.itest.routecontext.DetailAssignedView; +import com.vaadin.cdi.itest.routecontext.ErrorHandlerView; +import com.vaadin.cdi.itest.routecontext.ErrorParentView; +import com.vaadin.cdi.itest.routecontext.ErrorView; +import com.vaadin.cdi.itest.routecontext.EventObserverLayout; +import com.vaadin.cdi.itest.routecontext.EventView; +import com.vaadin.cdi.itest.routecontext.LayoutEventView; +import com.vaadin.cdi.itest.routecontext.LayoutEventView2; +import com.vaadin.cdi.itest.routecontext.LayoutScopedPlainView; +import com.vaadin.cdi.itest.routecontext.LayoutScopedPreservedView; +import com.vaadin.cdi.itest.routecontext.MainLayout; +import com.vaadin.cdi.itest.routecontext.MainLayoutBean; +import com.vaadin.cdi.itest.routecontext.MasterView; +import com.vaadin.cdi.itest.routecontext.PostponeView; +import com.vaadin.cdi.itest.routecontext.PreserveOnRefreshBean; +import com.vaadin.cdi.itest.routecontext.RerouteView; +import com.vaadin.cdi.itest.routecontext.RootView; + +public class RouteContextTest extends AbstractCdiTest { + + private String uiId; + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("route-context", + webArchive -> webArchive + .addPackage(MasterView.class.getPackage()) + .addAsResource(new File("target/classes/META-INF"))); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + open(""); + uiId = getText(MainLayout.UIID); + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 0); + assertConstructed(RerouteView.class, 0); + assertConstructed(MasterView.class, 0); + assertConstructed(AssignedBean.class, 0); + assertConstructed(ApartBean.class, 0); + assertConstructed(DetailApartView.class, 0); + assertConstructed(DetailAssignedView.class, 0); + assertConstructed(ErrorParentView.class, 0); + assertConstructed(ErrorHandlerView.class, 0); + } + + @Test + public void navigateFromRootToMasterReleasesRootInjectsEmptyBeans() + throws IOException { + follow(RootView.MASTER); + assertTextEquals("", MasterView.ASSIGNED_BEAN_LABEL); + + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 1); + assertConstructed(MasterView.class, 1); + assertDestroyed(MasterView.class, 0); + assertConstructed(AssignedBean.class, 1); + assertDestroyed(AssignedBean.class, 0); + assertConstructed(ApartBean.class, 0); + assertDestroyed(ApartBean.class, 0); + assertConstructed(DetailApartView.class, 0); + assertConstructed(DetailAssignedView.class, 0); + } + + @Test + public void navigationFromAssignedToMasterHoldsGroup() throws IOException { + follow(RootView.MASTER); + follow(MasterView.ASSIGNED); + assertTextEquals("ASSIGNED", DetailAssignedView.BEAN_LABEL); + + follow(DetailAssignedView.MASTER); + assertConstructed(MasterView.class, 1); + assertDestroyed(MasterView.class, 0); + assertConstructed(DetailAssignedView.class, 1); + assertDestroyed(DetailAssignedView.class, 0); + assertConstructed(DetailApartView.class, 0); + + assertTextEquals("ASSIGNED", MasterView.ASSIGNED_BEAN_LABEL); + } + + @Test + public void navigationFromApartToMasterReleasesGroup() throws IOException { + follow(RootView.MASTER); + follow(MasterView.APART); + assertTextEquals("", MasterView.ASSIGNED_BEAN_LABEL); + assertTextEquals("APART", DetailApartView.BEAN_LABEL); + + follow(DetailApartView.MASTER); + assertConstructed(MasterView.class, 1); + assertDestroyed(MasterView.class, 0); + assertConstructed(DetailAssignedView.class, 0); + assertDestroyed(DetailAssignedView.class, 0); + assertConstructed(DetailApartView.class, 1); + assertDestroyed(DetailApartView.class, 1); + + assertTextEquals("", MasterView.ASSIGNED_BEAN_LABEL); + } + + @Test + public void rerouteReleasesSource() throws IOException { + follow(RootView.REROUTE); + assertConstructed(RerouteView.class, 1); + assertDestroyed(RerouteView.class, 1); + + assertRootViewIsDisplayed(); + } + + @Test + public void postponedNavigationDoesNotCreateTarget() throws IOException { + follow(RootView.POSTPONE); + assertConstructed(RootView.class, 1); + + follow(PostponeView.POSTPONED_ROOT); + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 1); + + click(PostponeView.NAVIGATE); + assertConstructed(RootView.class, 2); + assertDestroyed(RootView.class, 1); + assertRootViewIsDisplayed(); + } + + @Test + public void eventObserved() { + follow(RootView.EVENT); + assertTextEquals("", EventView.OBSERVER_LABEL); + + click(EventView.FIRE); + assertTextEquals("HELLO", EventView.OBSERVER_LABEL); + } + + @Test + public void eventObservedByLayout() { + follow(RootView.LAYOUT_EVENT); + assertTextEquals("", EventObserverLayout.EVENTS_COUNTER_LABEL); + + click(LayoutEventView.FIRE); + assertTextEquals("EVENTS COUNT: 1", + EventObserverLayout.EVENTS_COUNTER_LABEL); + + click(LayoutEventView.CHANGE_VIEW); + assertTextEquals("Layout Event View 2", LayoutEventView2.VIEW_NAME); + + click(LayoutEventView2.FIRE); + assertTextEquals("EVENTS COUNT: 2", + EventObserverLayout.EVENTS_COUNTER_LABEL); + + click(LayoutEventView2.CHANGE_VIEW); + assertTextEquals("Layout Event View 1", LayoutEventView.VIEW_NAME); + + click(LayoutEventView.FIRE); + assertTextEquals("EVENTS COUNT: 3", + EventObserverLayout.EVENTS_COUNTER_LABEL); + } + + @Test + public void errorHandlerIsScoped() throws IOException { + follow(RootView.ERROR); + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 1); + assertConstructed(ErrorView.class, 1); + assertDestroyed(ErrorView.class, 1); + assertConstructed(ErrorParentView.class, 1); + assertDestroyed(ErrorParentView.class, 0); + assertConstructed(ErrorHandlerView.class, 1); + assertDestroyed(ErrorHandlerView.class, 0); + + follow(ErrorHandlerView.PARENT); + assertConstructed(ErrorParentView.class, 1); + assertDestroyed(ErrorParentView.class, 0); + assertConstructed(ErrorHandlerView.class, 1); + assertDestroyed(ErrorHandlerView.class, 0); + + follow(ErrorParentView.ROOT); + assertConstructed(RootView.class, 2); + assertDestroyed(RootView.class, 1); + assertConstructed(ErrorParentView.class, 1); + assertDestroyed(ErrorParentView.class, 1); + assertConstructed(ErrorHandlerView.class, 1); + assertDestroyed(ErrorHandlerView.class, 1); + + assertRootViewIsDisplayed(); + } + + @Test + public void routeScopeDoesNotExist_injectionWithOwnerOutOfNavigationThrows_invalidViewIsNotRendered() { + follow(MainLayout.INVALID); + + Assert.assertFalse(isElementPresent(By.id("invalid-injection"))); + } + + @Test + public void beansWithNoOwner_preservedWithinTheSameRouteTarget_notPreservedAfterNavigation() + throws IOException { + follow(MainLayout.PARENT_NO_OWNER); + + assertConstructed(BeanNoOwner.class, 1); + assertDestroyed(BeanNoOwner.class, 0); + + follow("child"); + + assertDestroyed(BeanNoOwner.class, 0); + + follow("parent"); + + assertConstructed(BeanNoOwner.class, 2); + assertDestroyed(BeanNoOwner.class, 1); + } + + @Test + public void beanWithNoOwner_preservedWithinTheSameRoutingChain() + throws IOException { + follow(MainLayout.CHILD_NO_OWNER); + + assertConstructed(BeanNoOwner.class, 1); + assertDestroyed(BeanNoOwner.class, 0); + + findElement(By.id("reset")).click(); + + assertDestroyed(BeanNoOwner.class, 0); + } + + @Test + public void navigateToViewWhichThrows_beansInsideErrorViewArePreservedinScope() + throws IOException { + follow(RootView.ERROR); + + assertConstructed(CustomExceptionSubButton.class, 1); + assertDestroyed(CustomExceptionSubButton.class, 0); + + assertConstructed(CustomExceptionSubDiv.class, 0); + assertDestroyed(CustomExceptionSubDiv.class, 0); + + findElement(By.id("switch-content")).click(); + + assertDestroyed(CustomExceptionSubButton.class, 0); + assertConstructed(CustomExceptionSubDiv.class, 1); + assertDestroyed(CustomExceptionSubDiv.class, 0); + + findElement(By.id("switch-content")).click(); + + assertConstructed(CustomExceptionSubButton.class, 1); + assertConstructed(CustomExceptionSubDiv.class, 1); + assertDestroyed(CustomExceptionSubButton.class, 0); + assertDestroyed(CustomExceptionSubDiv.class, 0); + } + + @Test + public void routeScopedBeanIsDestroyedOnNavigationOutOfViewAfterPreserveOnRefresh() + throws IOException { + follow(MainLayout.PRESERVE); + + assertConstructed(PreserveOnRefreshBean.class, 1); + assertDestroyed(PreserveOnRefreshBean.class, 0); + + // refresh + getDriver().get(getDriver().getCurrentUrl()); + + // UI ID has to be updated: all bean creations/removals will be done + // now within the new UI + uiId = getText(MainLayout.UIID); + + // navigate out of the preserved view + follow(MainLayout.PARENT_NO_OWNER); + + assertDestroyed(PreserveOnRefreshBean.class, 1); + } + + @Test + public void noPreserveOnRefresh_routeTargetIsRecreatedOnRefresh() + throws IOException { + getDriver().get(getDriver().getCurrentUrl()); + + // UI ID has to be updated: all bean creations/removals will be done + // now within the new UI + uiId = getText(MainLayout.UIID); + + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 0); + assertRootViewIsRendered(); + } + + @Test + public void noPreserveOnRefresh_otherUIWithSameWindowName_routeTargetIsNotShared() + throws IOException { + String url = getDriver().getCurrentUrl(); + String windowName = (String) executeScript("return window.name;"); + + // Open a second tab and make it report the window name of the first + // one, as a duplicated tab or a restored browser session does. Both + // UIs are alive at the same time, so a route scope keyed by window + // name would be shared between them. + getDriver().switchTo().newWindow(WindowType.TAB); + getDriver().get(url); + executeScript("window.name = arguments[0];", windowName); + // same origin reload, so the window name is retained + getDriver().navigate().refresh(); + + uiId = getText(MainLayout.UIID); + + assertConstructed(RootView.class, 1); + assertDestroyed(RootView.class, 0); + assertRootViewIsRendered(); + } + + @Test + public void sharedLayout_navigateBetweenPreservedAndPlainChild_layoutBeanIsKept() + throws IOException { + follow(MainLayout.LAYOUT_PRESERVED); + String beanData = getText(LayoutScopedPreservedView.LAYOUT_BEAN_LABEL); + + assertConstructed(MainLayoutBean.class, 1); + assertDestroyed(MainLayoutBean.class, 0); + + // the layout instance is reused, so the beans it owns must not be + // recreated even if the navigation chain is not preserved anymore + follow(MainLayout.LAYOUT_PLAIN); + + Assert.assertEquals(beanData, + getText(LayoutScopedPlainView.LAYOUT_BEAN_LABEL)); + assertConstructed(MainLayoutBean.class, 1); + assertDestroyed(MainLayoutBean.class, 0); + + // ... and the other way around + follow(MainLayout.LAYOUT_PRESERVED); + + Assert.assertEquals(beanData, + getText(LayoutScopedPreservedView.LAYOUT_BEAN_LABEL)); + assertConstructed(MainLayoutBean.class, 1); + assertDestroyed(MainLayoutBean.class, 0); + } + + private void assertRootViewIsRendered() { + Assert.assertNotNull( + "The root view is expected to be rendered in the current UI", + findElement(By.linkText(RootView.MASTER))); + } + + @Test + public void preserveOnRefresh_beanIsNotDestroyed() throws IOException { + follow(MainLayout.PRESERVE); + + assertConstructed(PreserveOnRefreshBean.class, 1); + assertDestroyed(PreserveOnRefreshBean.class, 0); + + String beanData = findElement(By.id("preserve-on-refresh")).getText(); + + // refresh + getDriver().get(getDriver().getCurrentUrl()); + + // check that the bean has not been removed in the previous UI + assertDestroyed(PreserveOnRefreshBean.class, 0); + + // UI ID has to be updated: all bean creations/removals will be done + // now within the new UI + uiId = getText(MainLayout.UIID); + + // the bean should not be destroyed with the new UI as well + assertDestroyed(PreserveOnRefreshBean.class, 0); + + Assert.assertEquals(beanData, + findElement(By.id("preserve-on-refresh")).getText()); + } + + private void assertRootViewIsDisplayed() { + assertTextEquals(uiId, MainLayout.UIID); + } + + private void assertConstructed(Class beanClass, int count) + throws IOException { + Assert.assertEquals(count, + getCount(beanClass.getSimpleName() + "C" + uiId)); + } + + private void assertDestroyed(Class beanClass, int count) + throws IOException { + Assert.assertEquals(count, + getCount(beanClass.getSimpleName() + "D" + uiId)); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ServiceTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ServiceTest.java new file mode 100644 index 00000000000..43ef11f9bb0 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/ServiceTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.cdi.itest.service.BootstrapCustomizeView; +import com.vaadin.cdi.itest.service.BootstrapCustomizer; +import com.vaadin.cdi.itest.service.EventObserver; +import com.vaadin.cdi.itest.service.ServiceView; +import com.vaadin.cdi.itest.service.TestErrorHandler; +import com.vaadin.cdi.itest.service.TestSystemMessagesProvider; +import com.vaadin.flow.server.SessionDestroyEvent; +import com.vaadin.flow.server.SessionInitEvent; +import com.vaadin.flow.server.UIInitEvent; + +import static com.vaadin.cdi.itest.service.ServiceView.ACTION; +import static com.vaadin.cdi.itest.service.ServiceView.EXPIRE; +import static com.vaadin.cdi.itest.service.ServiceView.FAIL; + +public class ServiceTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("services", + BootstrapCustomizer.class, BootstrapCustomizeView.class, + ServiceView.class, EventObserver.class, TestErrorHandler.class, + TestSystemMessagesProvider.class); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + } + + @Test + public void bootstrapCustomizedByServiceInitEventObserver() { + getDriver().get(getTestURL() + "bootstrap"); + assertTextEquals(BootstrapCustomizer.APPENDED_TXT, + BootstrapCustomizer.APPENDED_ID); + } + + @Test + public void sessionExpiredMessageCustomized() { + open(); + click(EXPIRE); + click(ACTION); + assertSystemMessageEquals(TestSystemMessagesProvider.EXPIRED_BY_TEST); + } + + @Test + public void errorHandlerCustomized() throws IOException { + String counter = TestErrorHandler.class.getSimpleName(); + assertCountEquals(0, counter); + open(); + click(FAIL); + assertCountEquals(1, counter); + } + + @Test + public void sessionInitEventObserved() throws IOException { + String initCounter = SessionInitEvent.class.getSimpleName(); + assertCountEquals(0, initCounter); + getDriver().manage().deleteAllCookies(); + open(); + assertCountEquals(1, initCounter); + } + + @Test + public void sessionDestroyEventObserved() throws IOException { + String destroyCounter = SessionDestroyEvent.class.getSimpleName(); + assertCountEquals(0, destroyCounter); + open(); + assertCountEquals(0, destroyCounter); + click(EXPIRE); + assertCountEquals(1, destroyCounter); + } + + @Test + public void uiInitEventObserved() throws IOException { + String uiInitCounter = UIInitEvent.class.getSimpleName(); + assertCountEquals(0, uiInitCounter); + open(); + assertCountEquals(1, uiInitCounter); + } + + private void assertSystemMessageEquals(String expected) { + WebElement message = waitUntil(d -> findElement( + By.cssSelector("div.v-system-error div.message"))); + Assert.assertEquals(expected, message.getText()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextSpecializesTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextSpecializesTest.java new file mode 100644 index 00000000000..1edc705367e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextSpecializesTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.cdi.itest.sessioncontextspecializes.LenientSessionContextManager; +import com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView; + +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DIRECT_CALL_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DONE_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.ERROR_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.FIREBTN_ID; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.OBSERVED_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.UNEXPECTED_ERROR_COUNT; + +/** + * Verifies that an application can take {@code @VaadinSessionScoped} beans into + * use from a background thread that only sets the + * {@link com.vaadin.flow.server.VaadinSession} thread-local — without holding + * the session lock — by providing a {@code @Specializes} lenient + * {@link com.vaadin.cdi.context.VaadinSessionScopedContext.ContextualStorageManager}. + *

+ * Reproduces the user-facing scenarios from issues #495 and #506. + */ +public class SessionContextSpecializesTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("session-context-specializes", + SessionContextSpecializesView.class, + SessionContextSpecializesView.BackgroundEvent.class, + SessionContextSpecializesView.SessionScopedObserver.class, + LenientSessionContextManager.class); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + open(); + } + + @Test + public void backgroundThread_firesEventAndCallsBean_specializesAllowsIt() + throws IOException { + assertCountEquals(0, OBSERVED_COUNT); + assertCountEquals(0, DIRECT_CALL_COUNT); + assertCountEquals(0, ERROR_COUNT); + assertCountEquals(0, UNEXPECTED_ERROR_COUNT); + + click(FIREBTN_ID); + + // The background thread updates the counters asynchronously; wait + // for its completion signal before asserting. + waitForCount(1, DONE_COUNT); + + assertCountEquals(1, DIRECT_CALL_COUNT); + assertCountEquals(1, OBSERVED_COUNT); + assertCountEquals(0, ERROR_COUNT); + assertCountEquals(0, UNEXPECTED_ERROR_COUNT); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextStrictTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextStrictTest.java new file mode 100644 index 00000000000..b672b626a6e --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextStrictTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView; + +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DIRECT_CALL_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.DONE_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.ERROR_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.FIREBTN_ID; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.OBSERVED_COUNT; +import static com.vaadin.cdi.itest.sessioncontextspecializes.SessionContextSpecializesView.UNEXPECTED_ERROR_COUNT; +import static org.junit.Assert.assertTrue; + +/** + * Verifies the framework's supported activation condition for + * {@code @VaadinSessionScoped}: when the current thread has set the + * {@link com.vaadin.flow.server.VaadinSession} thread-local but does not hold + * the session lock, the context is inactive — bean lookups fail and observer + * methods on session-scoped beans are not invoked. Regression guard for the + * lock check introduced in {@code VaadinSessionScopedContext.isActive()}. + */ +public class SessionContextStrictTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("session-context-strict", + SessionContextSpecializesView.class, + SessionContextSpecializesView.BackgroundEvent.class, + SessionContextSpecializesView.SessionScopedObserver.class); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + open(); + } + + @Test + public void backgroundThread_withoutSpecializes_contextInactive() + throws IOException { + assertCountEquals(0, OBSERVED_COUNT); + assertCountEquals(0, DIRECT_CALL_COUNT); + assertCountEquals(0, ERROR_COUNT); + assertCountEquals(0, UNEXPECTED_ERROR_COUNT); + + click(FIREBTN_ID); + + // The background thread updates the counters asynchronously; wait + // for its completion signal so the zero-assertions below are + // meaningful instead of passing on a not-yet-run thread. + waitForCount(1, DONE_COUNT); + + // Neither the proxy call nor the observer dispatch succeeded: + assertCountEquals(0, DIRECT_CALL_COUNT); + assertCountEquals(0, OBSERVED_COUNT); + // At least one of the two attempts threw ContextNotActiveException + // because the context is inactive on an unlocked thread. + assertTrue("Expected at least one ContextNotActiveException, got " + + getCount(ERROR_COUNT), getCount(ERROR_COUNT) >= 1); + // Any other RuntimeException would indicate something went wrong + // unrelated to the inactive context — guard against false positives. + assertCountEquals(0, UNEXPECTED_ERROR_COUNT); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextTest.java new file mode 100644 index 00000000000..4640c1efa03 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SessionContextTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.itest.sessioncontext.SessionContextView; + +import static com.vaadin.cdi.itest.sessioncontext.SessionContextView.SessionScopedBean.DESTROY_COUNT; +import static org.junit.Assert.assertTrue; + +public class SessionContextTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("session-context", + SessionContextView.class); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + open(); + } + + @Test + public void sameSessionIsAccessibleFromUIs() { + assertLabelEquals(""); + click(SessionContextView.SETVALUEBTN_ID); + getDriver().navigate().refresh();// creates new UI + assertLabelEquals(SessionContextView.VALUE); + } + + @Test + public void vaadinSessionCloseDestroysSessionContext() throws Exception { + assertDestroyCountEquals(0); + click(SessionContextView.INVALIDATEBTN_ID); + assertDestroyCountEquals(1); + } + + @Test + public void httpSessionCloseDestroysSessionContext() throws Exception { + assertDestroyCountEquals(0); + click(SessionContextView.HTTP_INVALIDATEBTN_ID); + assertDestroyCountEquals(1); + } + + @Test + @Category(SlowTests.class) + public void httpSessionExpirationDestroysSessionContext() throws Exception { + assertDestroyCountEquals(0); + click(SessionContextView.EXPIREBTN_ID); + boolean destroyed = false; + getLogger().info("Waiting for session expiration..."); + for (int i = 0; i < 60; i++) { + Thread.sleep(1000); + if (getCount(DESTROY_COUNT) > 0) { + getLogger().info("session expired after {} seconds", i); + destroyed = true; + break; + } + } + assertTrue(destroyed); + } + + private void assertLabelEquals(String expected) { + assertTextEquals(expected, SessionContextView.VALUELABEL_ID); + } + + private void assertDestroyCountEquals(int expectedCount) + throws IOException { + assertCountEquals(expectedCount, DESTROY_COUNT); + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(SessionContextTest.class); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SlowTests.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SlowTests.java new file mode 100644 index 00000000000..df2db24f5f1 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SlowTests.java @@ -0,0 +1,19 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +public interface SlowTests { +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SmokeTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SmokeTest.java new file mode 100644 index 00000000000..85b996d1905 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/SmokeTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.util.concurrent.atomic.AtomicReference; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.cdi.itest.smoke.CdiView; +import com.vaadin.cdi.itest.smoke.ProxiedNavigationTargetView; +import com.vaadin.cdi.itest.smoke.UselessInterceptor; + +@SuppressWarnings("ArquillianTooManyDeployment") +public class SmokeTest extends AbstractCdiTest { + + @Deployment(name = "noncdi", testable = false) + public static WebArchive createCdiServletDisabledDeployment() { + return ArchiveProvider.createWebArchive("noncdi-test", CdiView.class) + .addAsWebInfResource(ArchiveProvider.class.getClassLoader() + .getResource("disablecdi-web.xml"), "web.xml"); + } + + @Deployment(name = "cdi", testable = false) + public static WebArchive createCdiServletEnabledDeployment() { + return ArchiveProvider.createWebArchive("cdi-test", CdiView.class, + ProxiedNavigationTargetView.class, UselessInterceptor.class); + } + + @Before + public void setUp() { + open(); + $("button").first().click(); + } + + @Test + @OperateOnDeployment("noncdi") + public void injectionDoesNotHappenWithDisabledCdiServlet() { + assertLabelEquals("no CDI"); + } + + @Test + @OperateOnDeployment("cdi") + public void injectionHappensWithEnabledCdiServlet() { + assertLabelEquals("hello CDI"); + } + + @Test + @OperateOnDeployment("cdi") + public void navigationCorrectlyHandlesProxiedViews() { + getDriver().get(getTestURL() + "proxied"); + waitForDevServer(); + + String prevUuid = null; + AtomicReference prevCounter = new AtomicReference<>(""); + for (int i = 0; i < 5; i++) { + String uuid = waitUntil(d -> d.findElement(By.id("COMPONENT_ID"))) + .getText(); + + waitUntil(d -> !prevCounter.get() + .equals(d.findElement(By.id("CLICK_COUNTER")).getText())); + + if (prevUuid != null) { + Assert.assertEquals("UUID should not have been changed", + prevUuid, uuid); + } else { + prevUuid = uuid; + } + String counter = findElement(By.id("CLICK_COUNTER")).getText(); + Assert.assertEquals( + "Parameter and counter should have the same value", + "P:" + i + ", C:" + i, counter); + + prevCounter.set(counter); + + $("a").first().click(); + } + } + + private void assertLabelEquals(String expected) { + Assert.assertEquals(expected, $("label").first().getText()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TemplateTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TemplateTest.java new file mode 100644 index 00000000000..d8d543f5cf5 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TemplateTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; + +import com.vaadin.cdi.itest.template.TestTemplate; +import com.vaadin.testbench.TestBenchElement; + +public class TemplateTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("templates", + TestTemplate.class); + } + + @Before + public void setUp() { + open(); + } + + @Test + public void scopedComponentInjectedToTemplate() { + checkLogsForErrors(); + TestBenchElement template = $("test-template").first(); + TestBenchElement label = template.$(TestBenchElement.class).id("label"); + Assert.assertEquals("", label.getText()); + TestBenchElement input = template.$(TestBenchElement.class).id("input"); + input.sendKeys("CDI"); + input.sendKeys(Keys.ENTER); + Assert.assertEquals("CDI", label.getText()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TranslationTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TranslationTest.java new file mode 100644 index 00000000000..f055d0f768d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/TranslationTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.File; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.cdi.itest.i18n.TranslationView; + +public class TranslationTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive createCdiServletEnabledDeployment() { + return ArchiveProvider + .createWebArchive("translations", TranslationView.class) + .addAsResource(new File("src/main/resources/vaadin-i18n")); + + } + + @Override + protected String getTestPath() { + return "/translations"; + } + + @Test + public void translationFilesExist_defaultI18NInstantiated_languagesWork() { + open(); + + String locales = $("span").id(TranslationView.LOCALES_ID).getText(); + Assert.assertTrue("Couldn't verify German locale", + locales.contains("de")); + Assert.assertTrue("Couldn't verify Finnish locale", + locales.contains("fi_FI")); + Assert.assertTrue("Couldn't verify French locale", + locales.contains("fr_FR")); + Assert.assertTrue("Couldn't verify Japanese locale", + locales.contains("ja_JP")); + + Assert.assertEquals("Default", $("span").id("english").getText()); + Assert.assertEquals("Deutsch", $("span").id("german").getText()); + Assert.assertEquals("Deutsch", $("span").id("germany").getText()); + Assert.assertEquals("Suomi", $("span").id("finnish").getText()); + Assert.assertEquals("français", $("span").id("french").getText()); + Assert.assertEquals("日本語", $("span").id("japanese").getText()); + } + + @Test + public void translationFilesExist_defaultI18NInstantiated_updateFromExternalThreadWorks() { + open(); + + waitUntilNot( + driver -> $("span").id("dynamic").getText().equals("waiting")); + + Assert.assertEquals( + "Dynamic update from thread should have used correct bundle.", + "français", $("span").id("dynamic").getText()); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIContextTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIContextTest.java new file mode 100644 index 00000000000..c2d1434676c --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIContextTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.io.File; +import java.io.IOException; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.cdi.itest.uicontext.UIContextRootView; +import com.vaadin.cdi.itest.uicontext.UINormalScopedBeanView; +import com.vaadin.cdi.itest.uicontext.UIScopedLabel; +import com.vaadin.cdi.itest.uicontext.UIScopedView; + +public class UIContextTest extends AbstractCdiTest { + + private String uiId; + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("ui-context", + webArchive -> webArchive + .addPackage(UIContextRootView.class.getPackage()) + .addAsResource(new File("target/classes/META-INF"))); + } + + @Before + public void setUp() throws Exception { + resetCounts(); + open(); + uiId = getText(UIContextRootView.UIID_LABEL); + } + + @Test + public void beanDestroyedOnUIClose() throws IOException { + assertDestroyCountEquals(0); + click(UIContextRootView.CLOSE_UI_BTN); + assertDestroyCountEquals(1); + } + + @Test + public void beanDestroyedOnSessionClose() throws IOException { + assertDestroyCountEquals(0); + click(UIContextRootView.CLOSE_SESSION_BTN); + assertDestroyCountEquals(1); + } + + @Test + public void viewSurvivesNavigation() { + follow(UIContextRootView.UISCOPED_LINK); + assertTextEquals("", UIScopedView.VIEWSTATE_LABEL); + click(UIScopedView.SETSTATE_BTN); + assertTextEquals(UIScopedView.UISCOPED_STATE, + UIScopedView.VIEWSTATE_LABEL); + follow(UIScopedView.ROOT_LINK); + follow(UIContextRootView.UISCOPED_LINK); + assertTextEquals(UIScopedView.UISCOPED_STATE, + UIScopedView.VIEWSTATE_LABEL); + } + + @Test + public void sameScopedComponentInjectedInOtherView() { + assertTextEquals(uiId, UIScopedLabel.ID); + follow(UIContextRootView.INJECTER_LINK); + assertTextEquals(uiId, UIScopedLabel.ID); + } + + @Test + public void observerCalledOnInstanceAttachedToUI() { + click(UIContextRootView.TRIGGER_EVENT_BTN); + assertTextEquals(UIContextRootView.EVENT_PAYLOAD, UIScopedLabel.ID); + } + + @Test + public void normalScopedBeanInjectedToLargerScopeChangesWithActiveUI() { + follow(UIContextRootView.NORMALSCOPED_LINK); + assertTextEquals(uiId, UINormalScopedBeanView.UIID_LABEL); + open(); + uiId = getText(UIContextRootView.UIID_LABEL); + follow(UIContextRootView.NORMALSCOPED_LINK); + assertTextEquals(uiId, UINormalScopedBeanView.UIID_LABEL); + } + + private void assertDestroyCountEquals(int expectedCount) + throws IOException { + assertCountEquals(expectedCount, UIScopedLabel.DESTROY_COUNT + uiId); + } +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIEventsTest.java b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIEventsTest.java new file mode 100644 index 00000000000..728ec53c588 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/java/com/vaadin/cdi/itest/UIEventsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.itest; + +import java.util.List; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.cdi.itest.uievents.NavigationObserver; +import com.vaadin.cdi.itest.uievents.UIEventsView; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeLeaveEvent; +import com.vaadin.testbench.TestBenchElement; + +public class UIEventsTest extends AbstractCdiTest { + + @Deployment(testable = false) + public static WebArchive deployment() { + return ArchiveProvider.createWebArchive("uievents", UIEventsView.class, + NavigationObserver.class); + } + + @Before + public void setUp() { + getDriver().get(getRootURL() + "uievents"); + } + + @Test + public void navigationEventsObserved() { + List events = $("div") + .id(UIEventsView.NAVIGATION_EVENTS).$("label").all(); + Assert.assertEquals(3, events.size()); + assertEventIs(events.get(0), BeforeLeaveEvent.class); + assertEventIs(events.get(1), BeforeEnterEvent.class); + assertEventIs(events.get(2), AfterNavigationEvent.class); + } + + @Test + public void pollEventObserved() { + waitForElementPresent(By.id(UIEventsView.POLL_FROM_CLIENT)); + assertTextEquals("true", UIEventsView.POLL_FROM_CLIENT); + } + + private void assertEventIs(TestBenchElement eventElem, + Class eventClass) { + Assert.assertEquals(eventClass.getSimpleName(), eventElem.getText()); + } + +} diff --git a/flow-tests/vaadin-cdi-tests/src/test/resources/arquillian.xml b/flow-tests/vaadin-cdi-tests/src/test/resources/arquillian.xml new file mode 100644 index 00000000000..333a1c87574 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/resources/arquillian.xml @@ -0,0 +1,69 @@ + + + + + + target/deployment + + + + + + ${catalina.home} + deployer + deployer + + --add-opens=java.base/java.io=ALL-UNNAMED + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.util.concurrent=ALL-UNNAMED + --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED + --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED + --add-opens=java.base/java.net=ALL-UNNAMED + + + + + + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + + + + + + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Djboss.bind.address=0.0.0.0 + + + + + + -Djboss.bind.address=0.0.0.0 + + + + + + 4848 + ${my.admin.password} + + + diff --git a/flow-tests/vaadin-cdi-tests/src/test/resources/liberty/server.xml b/flow-tests/vaadin-cdi-tests/src/test/resources/liberty/server.xml new file mode 100644 index 00000000000..1fbd0682c59 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/resources/liberty/server.xml @@ -0,0 +1,16 @@ + + + + + + jakartaee-10.0 + localConnector-1.0 + + + + + + diff --git a/flow-tests/vaadin-cdi-tests/src/test/resources/payara/glassfish-web.xml b/flow-tests/vaadin-cdi-tests/src/test/resources/payara/glassfish-web.xml new file mode 100644 index 00000000000..ac9a5d3b975 --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/resources/payara/glassfish-web.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/flow-tests/vaadin-cdi-tests/src/test/resources/tomcat/tomcat-users.xml b/flow-tests/vaadin-cdi-tests/src/test/resources/tomcat/tomcat-users.xml new file mode 100644 index 00000000000..31dd796991d --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/resources/tomcat/tomcat-users.xml @@ -0,0 +1,57 @@ + + + + + + + + + + diff --git a/flow-tests/vaadin-cdi-tests/src/test/resources/wildfly/jboss-deployment-structure.xml b/flow-tests/vaadin-cdi-tests/src/test/resources/wildfly/jboss-deployment-structure.xml new file mode 100644 index 00000000000..afcb1bcbb7c --- /dev/null +++ b/flow-tests/vaadin-cdi-tests/src/test/resources/wildfly/jboss-deployment-structure.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/guidelines/repository.md b/guidelines/repository.md index f8eb2498488..c1e2f5f08d5 100644 --- a/guidelines/repository.md +++ b/guidelines/repository.md @@ -33,6 +33,7 @@ Every top-level Maven module of the repository: | `flow-devloop-daemon` | Daemon for the `vaadin-dev` dev loop. | | `vaadin-dev-server` | Development tooling served to the browser. | | `vaadin-spring` | Spring Framework integration. | +| `vaadin-cdi` | CDI integration, for Jakarta EE application servers. | | `flow-server-production-mode` | Wrapper artifact whose `web-fragment.xml` turns on production mode. | | `flow-jandex` | Jandex index of the Flow packages, for use outside Vaadin Platform. | | `flow` | Aggregate POM that pulls in the modules an application needs. | diff --git a/pom.xml b/pom.xml index 6ecafb3d0f2..01076f3d242 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,7 @@ vaadin-dev-server flow-jandex vaadin-spring + vaadin-cdi flow-polymer2lit @@ -85,9 +86,20 @@ Scoped to this one rule, so the lock files keep being analyzed for everything else. --> - agenticInstallScripts + + agenticInstallScripts,cdiDependentProviderSerialization githubactions:S6505 **/.github/workflows/*.lock.yml + java:S2118 + **/com/vaadin/cdi/util/DependentProvider.java true false @@ -111,6 +123,8 @@ 33.7.1-jre 3.30.2-GA 6.1.3 + 6.0.3.Final + 5.0.1.Final 1.5 @@ -295,6 +309,16 @@ jcip-annotations 1.0.0 + + org.jboss.weld.se + weld-se-core + ${weld.version} + + + org.jboss.weld + weld-junit5 + ${weld.junit.version} + diff --git a/scripts/computeMatrix.js b/scripts/computeMatrix.js index 288d14334f3..e7622867ed7 100755 --- a/scripts/computeMatrix.js +++ b/scripts/computeMatrix.js @@ -8,6 +8,9 @@ const globalExclusions = [ 'flow-tests/test-multi-war/test-war1', 'flow-tests/test-multi-war/test-war2', 'flow-tests/test-webpush', + // Deploys to a real application server, so it needs one of the container + // profiles and has its own job in validation.yml + 'flow-tests/vaadin-cdi-tests', 'flow-tests/vaadin-spring-tests/test-plain-spring-boot-reload-time', 'flow-tests/vaadin-spring-tests/test-spring-boot-reload-time', 'flow-tests/vaadin-spring-tests/test-spring-boot-multimodule-reload-time', diff --git a/vaadin-cdi/README.md b/vaadin-cdi/README.md new file mode 100644 index 00000000000..9c0521769d6 --- /dev/null +++ b/vaadin-cdi/README.md @@ -0,0 +1,156 @@ +# vaadin-cdi + +Vaadin-CDI is the official CDI integration for Vaadin Flow. It is built and +released as part of Flow and shares its version number. + +The integration tests live in `flow-tests/vaadin-cdi-tests`. + +### Startup + +If you do not customize Vaadin Servlet in your web.xml, +a CDI enabled Vaadin servlet is deployed automatically. + +Otherwise you can customize +[CdiVaadinServlet](src/main/java/com/vaadin/cdi/CdiVaadinServlet.java) +just like VaadinServlet. + +### Component instantiation and CDI + +Vaadin triggered instantiation happens in a +[CDI aware Vaadin Instantiator](src/main/java/com/vaadin/cdi/CdiInstantiator.java) +implementation. +These components are created by the instantiator: + +- `@Route`, RouteLayout, HasErrorParameter components +- components injected by `@Id` into polymer templates + +By default instantiator looks up the CDI bean by type ( component class ), +and gets a contextual reference from BeanManager. +All the CDI features are usable like observer, interceptor, decorator. + +When type is not found as a CDI bean +( for example ambiguous, or does not have a no-arg public constructor ), +instantiation falls back to the default Vaadin behavior. +After the instantiation, dependency injection is performed. +Injects work, but other CDI features are not, because instantiated component is not a contextual instance. + +### Vaadin Contexts + +#### VaadinServiceScoped + +[@VaadinServiceScoped](src/main/java/com/vaadin/cdi/annotation/VaadinServiceScoped.java) +is a normal ( proxied ) scope. Its purpose to define a scope for the beans used by VaadinService. Like an Instantiator, or an I18NProvider. + +#### VaadinSessionScoped + +[@VaadinSessionScoped](src/main/java/com/vaadin/cdi/annotation/VaadinSessionScoped.java) +is a normal ( proxied ) scope. Every VaadinSession have a separate Context. + +#### UIScoped, NormalUIScoped + +Every UI has a separate Context. +Practically it means there is just one instance per UI for the scoped class. + +For components, use [@UIScoped](src/main/java/com/vaadin/cdi/annotation/UIScoped.java). +It is a pseudo scope, so gives a direct reference. +Vaadin component tree does not work properly with CDI client proxies. + +For other beans you can use +[@NormalUIScoped](src/main/java/com/vaadin/cdi/annotation/NormalUIScoped.java). +Given it is normal scoped, have some benefit. +For example can handle cyclic dependency. + +#### RouteScoped, NormalRouteScoped + +[@RouteScoped](src/main/java/com/vaadin/cdi/annotation/RouteScoped.java) context lifecycle on its own is same as UI context's. +Together with the concept of [@RouteScopeOwner](src/main/java/com/vaadin/cdi/annotation/RouteScopeOwner.java) it can be used to bind beans to router components (target/layout/exceptionhandler). +Until owner remains in the route, all beans owned by it remain in the scope. + +Same as before, for vaadin components use `@RouteScoped`, it is a pseudo scope. +For other beans you can use `@NormalRouteScoped`, it is a normal scope. + +### Services + +Some Vaadin service interfaces can be implemented as a CDI bean. + +- I18NProvider +- Instantiator +- SystemMessagesProvider +- ErrorHandler + +Beans have to be qualifed by +[@VaadinServiceEnabled](src/main/java/com/vaadin/cdi/annotation/VaadinServiceEnabled.java) +to be picked up automatically. + +### Vaadin Events + +The following events are fired as a CDI event: + +- ServiceInitEvent +- PollEvent +- BeforeEnterEvent +- BeforeLeaveEvent +- AfterNavigationEvent +- UIInitEvent +- SessionInitEvent +- SessionDestroyEvent +- ServiceDestroyEvent + +You just need a CDI observer to handle them. + +### Known issues and limitations + +#### Custom UI + +As of V10 no custom UI subclass is needed for the application. +You can define one by the corresponding servlet parameter, +but it is instantiated by the framework as a POJO. + +You should not need a custom UI subclass. Though dependency injection can be achieved, just in case. +Use CDI BeanManager in `UI.init`. Through Deltaspike's `BeanProvider.injectFields(this)` for example. + +#### ServiceDestroyEvent + +During application shutdown it is implementation specific, +whether it works with CDI or not. +But according to servlet specs, +a servlet destroy ( it means a service destroy too ) can happen in +other circumstances too. + +#### Push with CDI + +Vaadin contexts are usable inside `UI.access` with any push transport. + +But an incoming websocket message does not count as a request in CDI. +Need a http request to have request, session, and conversation context. + +So you should use WEBSOCKET_XHR (it is the default), or LONG_POLLING +transport, otherwise you lost these standard contexts. + +In background threads these contexts are not active regardless of push. + +#### Router and CDI + +Vaadin scans router classes (targets, layouts) without any clue about CDI beans. +Using producers, or excluding the bean class from types with `@Typed` causes issues with these kind of beans. + +#### Instantiator and CDI Qualifiers + +As you can see at component instantiation, beans looked up by bean type. +The API can not provide qualifiers, so lookup is done with `@Any`. + +--- + +Copyright 2012-2018 Vaadin Ltd. + +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/vaadin-cdi/pom.xml b/vaadin-cdi/pom.xml new file mode 100644 index 00000000000..4ccd438dd3a --- /dev/null +++ b/vaadin-cdi/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + + com.vaadin + flow-project + 25.4-SNAPSHOT + + + vaadin-cdi + jar + vaadin-cdi + Provides CDI integration for Vaadin applications. + + + + src/main/java/com/vaadin/cdi/util/** + + + + + + com.vaadin + flow-server + ${project.version} + + + com.vaadin + flow-client + ${project.version} + + + jakarta.servlet + jakarta.servlet-api + provided + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + jakarta.enterprise.concurrent + jakarta.enterprise.concurrent-api + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + + + com.vaadin + flow-polymer-template + ${project.version} + test + + + com.vaadin + flow-html-components + ${project.version} + test + + + org.jboss.weld.se + weld-se-core + test + + + org.jboss.weld + weld-junit5 + test + + + org.slf4j + slf4j-simple + test + + + diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/AbstractCdiInstantiator.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/AbstractCdiInstantiator.java new file mode 100644 index 00000000000..108e3cc93aa --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/AbstractCdiInstantiator.java @@ -0,0 +1,108 @@ +/* + * Copyright 2000-2019 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.di.DefaultInstantiator; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.i18n.I18NProvider; +import com.vaadin.flow.router.PageTitleGenerator; +import com.vaadin.flow.server.VaadinServiceInitListener; +import com.vaadin.flow.server.auth.MenuAccessControl; + +abstract public class AbstractCdiInstantiator implements Instantiator { + + private static final String CANNOT_USE_CDI_BEANS_FOR_I18N = "Cannot use CDI beans for I18N, falling back to the default behavior."; + private static final String FALLING_BACK_TO_DEFAULT_INSTANTIATION = "Falling back to default instantiation."; + + private AtomicBoolean i18NLoggingEnabled = new AtomicBoolean(true); + + protected abstract DefaultInstantiator getDelegate(); + + public abstract BeanManager getBeanManager(); + + @Override + public T getOrCreate(Class type) { + return new BeanLookup<>(getBeanManager(), type) + .setUnsatisfiedHandler(() -> getLogger().debug( + "'{}' is not a CDI bean. " + + FALLING_BACK_TO_DEFAULT_INSTANTIATION, + type.getName())) + .setAmbiguousHandler( + e -> getLogger().debug( + "Multiple CDI beans found. " + + FALLING_BACK_TO_DEFAULT_INSTANTIATION, + e)) + .lookupOrElseGet(() -> { + final T instance = getDelegate().getOrCreate(type); + BeanProvider.injectFields(instance); + return instance; + }); + } + + @Override + public I18NProvider getI18NProvider() { + final BeanLookup lookup = new BeanLookup<>( + getBeanManager(), I18NProvider.class, BeanLookup.SERVICE); + if (i18NLoggingEnabled.compareAndSet(true, false)) { + lookup.setUnsatisfiedHandler(() -> getLogger().info( + "Can't find any @VaadinServiceScoped bean implementing '{}'. " + + CANNOT_USE_CDI_BEANS_FOR_I18N, + I18NProvider.class.getSimpleName())).setAmbiguousHandler( + e -> getLogger().warn( + "Found more beans for I18N. " + + CANNOT_USE_CDI_BEANS_FOR_I18N, + e)); + } else { + lookup.setAmbiguousHandler(e -> { + }); + } + return lookup.lookupOrElseGet(getDelegate()::getI18NProvider); + } + + @Override + public MenuAccessControl getMenuAccessControl() { + final BeanLookup lookup = new BeanLookup<>( + getBeanManager(), MenuAccessControl.class, BeanLookup.SERVICE); + return lookup.lookupOrElseGet(getDelegate()::getMenuAccessControl); + } + + @Override + public PageTitleGenerator getPageTitleGenerator() { + final BeanLookup lookup = new BeanLookup<>( + getBeanManager(), PageTitleGenerator.class, BeanLookup.SERVICE); + return lookup.lookupOrElseGet(getDelegate()::getPageTitleGenerator); + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(CdiInstantiator.class); + } + + @Override + public Stream getServiceInitListeners() { + return Stream.concat(getDelegate().getServiceInitListeners(), + Stream.of(serviceInitEvent -> getBeanManager().getEvent() + .fire(serviceInitEvent))); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/BeanLookup.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/BeanLookup.java new file mode 100644 index 00000000000..0940e7c0529 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/BeanLookup.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.AmbiguousResolutionException; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.util.AnnotationLiteral; + +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.util.AnyLiteral; + +/** + * Utility class for CDI lookup, and instantiation. + *

+ * Dependent beans are instantiated without any warning, but do not get + * destroyed properly. {@link jakarta.annotation.PreDestroy} won't run. + * + * @param + * Bean Type + */ +class BeanLookup { + private final BeanManager beanManager; + private final Class type; + private final Annotation[] qualifiers; + private UnsatisfiedHandler unsatisfiedHandler = () -> { + }; + private Consumer ambiguousHandler = e -> { + throw e; + }; + + final static Annotation SERVICE = new ServiceLiteral(); + private final static Annotation[] ANY = new Annotation[] { + new AnyLiteral() }; + + private static class ServiceLiteral + extends AnnotationLiteral + implements VaadinServiceEnabled { + + } + + @FunctionalInterface + public interface UnsatisfiedHandler { + void handle(); + } + + BeanLookup(BeanManager beanManager, Class type, + Annotation... qualifiers) { + this.beanManager = beanManager; + this.type = type; + if (qualifiers.length > 0) { + this.qualifiers = qualifiers; + } else { + this.qualifiers = ANY; + } + } + + BeanLookup setUnsatisfiedHandler(UnsatisfiedHandler unsatisfiedHandler) { + this.unsatisfiedHandler = unsatisfiedHandler; + return this; + } + + BeanLookup setAmbiguousHandler( + Consumer ambiguousHandler) { + this.ambiguousHandler = ambiguousHandler; + return this; + } + + T lookupOrElseGet(Supplier fallback) { + final Set> beans = beanManager.getBeans(type, qualifiers); + if (beans == null || beans.isEmpty()) { + unsatisfiedHandler.handle(); + return fallback.get(); + } + final Bean bean; + try { + bean = beanManager.resolve(beans); + } catch (AmbiguousResolutionException e) { + ambiguousHandler.accept(e); + return fallback.get(); + } + final CreationalContext ctx = beanManager + .createCreationalContext(bean); + // noinspection unchecked + return (T) beanManager.getReference(bean, type, ctx); + } + + T lookup() { + return lookupOrElseGet(() -> null); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiator.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiator.java new file mode 100644 index 00000000000..19089f8f322 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Unmanaged; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.di.DefaultInstantiator; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.internal.UsageStatistics; +import com.vaadin.flow.server.VaadinService; + +/** + * Default CDI instantiator. + * + * @see Instantiator + */ +public class CdiInstantiator extends AbstractCdiInstantiator { + + private final BeanManager beanManager; + + private final DefaultInstantiator delegate; + + public CdiInstantiator(BeanManager beanManager, VaadinService service) { + this.beanManager = beanManager; + this.delegate = new DefaultInstantiator(service); + UsageStatistics.markAsUsed("flow/CdiInstantiator", null); + } + + @Override + protected DefaultInstantiator getDelegate() { + return delegate; + } + + @Override + public BeanManager getBeanManager() { + return beanManager; + } + + @Override + public T createComponent(Class componentClass) { + Unmanaged unmanagedClass = new Unmanaged(componentClass); + Unmanaged.UnmanagedInstance instance = unmanagedClass.newInstance(); + instance.produce().inject().postConstruct(); + return instance.get(); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiatorFactory.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiatorFactory.java new file mode 100644 index 00000000000..ff217e62b02 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiInstantiatorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.server.VaadinService; + +/** + * Default CDI instantiator factory. + *

+ * Can be overridden by a @{@link VaadinServiceEnabled} CDI + * Alternative/Specializes, or can be customized with a Decorator. + * + * @see Instantiator + */ +@VaadinServiceScoped +@VaadinServiceEnabled +public class CdiInstantiatorFactory implements InstantiatorFactory { + + @Inject + private BeanManager beanManager; + + @Override + public Instantiator createInstantitor(VaadinService service) { + if (!getServiceClass().isAssignableFrom(service.getClass())) { + return null; + } + + return new CdiInstantiator(beanManager, service); + } + + public Class getServiceClass() { + return CdiVaadinServletService.class; + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiServletDeployer.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiServletDeployer.java new file mode 100644 index 00000000000..7d2a707ceb9 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiServletDeployer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.annotation.HandlesTypes; +import jakarta.servlet.annotation.WebListener; + +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.startup.ServletDeployer; + +/** + * Container initializer that automatically registers a CDI Vaadin servlet. + * + * The servlet is only registered if all of the following conditions apply: + *

    + *
  • At least one class annotated with {@link Route @Route} is found on the + * classpath + *
  • No servlet is registered for /* + *
  • No Vaadin servlet is registered + *
+ *

+ * Vaadin ships with {@link ServletDeployer}, which is a {@link WebListener} to + * register the default {@link VaadinServlet}. We can't override it from java, + * but since this class is a container initializer, we run first. After + * registration of the CDI Vaadin Servlet, the original deployer won't register + * the default one. + *

+ * The rest of this class is copied from {@link ServletDeployer}. + */ +@HandlesTypes(Route.class) +public class CdiServletDeployer implements ServletContainerInitializer { + + @Override + public void onStartup(Set> classSet, ServletContext ctx) { + if (classSet == null) { + // ServletDeployer will log routes are missing. + return; + } + + ServletRegistration rootServlet = findRootServlet(ctx); + if (rootServlet != null) { + // ServletDeployer will log automatic servlet registration skipped. + // Even if we registered successfully the CDI servlet. + return; + } + + ServletRegistration vaadinServlet = findVaadinServlet(ctx); + if (vaadinServlet != null) { + // ServletDeployer will log it. + return; + } + + String servletName = getClass().getName(); + ServletRegistration.Dynamic registration = ctx.addServlet(servletName, + CdiVaadinServlet.class); + if (registration == null) { + // Not expected to ever happen + // ServletDeployer have to fail too, and will log it + return; + } + + getLogger().info("Automatically deploying CDI Vaadin servlet to /*"); + + registration.setAsyncSupported(true); + registration.addMapping("/*"); + registration.setLoadOnStartup(1); + } + + private static ServletRegistration findRootServlet(ServletContext context) { + return context.getServletRegistrations().values().stream().filter( + registration -> registration.getMappings().contains("/*")) + .findAny().orElse(null); + } + + private static ServletRegistration findVaadinServlet( + ServletContext context) { + return context.getServletRegistrations().values().stream() + .filter(registration -> isVaadinServlet( + context.getClassLoader(), registration)) + .findAny().orElse(null); + } + + private static boolean isVaadinServlet(ClassLoader classLoader, + ServletRegistration registration) { + String className = registration.getClassName(); + try { + return VaadinServlet.class + .isAssignableFrom(classLoader.loadClass(className)); + } catch (ClassNotFoundException e) { + getLogger().info("Assuming {} is not a Vaadin servlet", className, + e); + return false; + } + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(CdiServletDeployer.class); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServlet.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServlet.java new file mode 100644 index 00000000000..9d93602263a --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServlet.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.server.ServiceException; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.VaadinServletService; + +/** + * Servlet to create {@link CdiVaadinServletService}. + *

+ * An instance of this servlet is automatically deployed by + * {@link CdiServletDeployer} if no VaadinServlet is deployed based on web.xml + * or Servlet 3.0 annotations. A subclass of this servlet and of + * {@link CdiVaadinServletService} can be used and explicitly deployed to + * customize it, in which case + * {@link #createServletService(DeploymentConfiguration)} must call + * service.init() . + */ + +public class CdiVaadinServlet extends VaadinServlet { + @Inject + private BeanManager beanManager; + + private static final ThreadLocal servletName = new ThreadLocal<>(); + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + try { + servletName.set(servletConfig.getServletName()); + super.init(servletConfig); + } finally { + servletName.remove(); + } + } + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + try { + servletName.set(getServletName()); + super.service(request, response); + } finally { + servletName.remove(); + } + } + + /** + * Name of the Vaadin servlet for the current thread. + *

+ * Until VaadinService appears in CurrentInstance, it have to be used to get + * the servlet name. + *

+ * This method is meant for internal use only. + * + * @see VaadinServlet#getCurrent() + * @return currently processing vaadin servlet name + */ + public static String getCurrentServletName() { + return servletName.get(); + } + + @Override + protected VaadinServletService createServletService( + DeploymentConfiguration configuration) throws ServiceException { + + final CdiVaadinServletService service = new CdiVaadinServletService( + this, configuration, beanManager); + service.init(); + return service; + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServletService.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServletService.java new file mode 100644 index 00000000000..5ef3f8913e1 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/CdiVaadinServletService.java @@ -0,0 +1,303 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.AmbiguousResolutionException; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.io.Serializable; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Executor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.context.VaadinSessionScopedContext; +import com.vaadin.cdi.util.BeanManagerProvider; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.PollEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.AfterNavigationListener; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterListener; +import com.vaadin.flow.router.BeforeLeaveEvent; +import com.vaadin.flow.router.BeforeLeaveListener; +import com.vaadin.flow.router.ListenerPriority; +import com.vaadin.flow.server.ErrorHandler; +import com.vaadin.flow.server.ServiceDestroyEvent; +import com.vaadin.flow.server.ServiceException; +import com.vaadin.flow.server.SessionDestroyEvent; +import com.vaadin.flow.server.SessionInitEvent; +import com.vaadin.flow.server.SystemMessagesProvider; +import com.vaadin.flow.server.UIInitEvent; +import com.vaadin.flow.server.UIInitListener; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinServletService; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.WrappedSession; + +import static com.vaadin.cdi.BeanLookup.SERVICE; + +/** + * Servlet service implementation for Vaadin CDI. + *

+ * This class creates and initializes a @{@link VaadinServiceEnabled} + * {@link Instantiator}. + *

+ * Some @{@link VaadinServiceEnabled} beans can be used to customize Vaadin, + * they are also created, and bound if found. + *

    + *
  • {@link SystemMessagesProvider} is bound to service by + * {@link VaadinService#setSystemMessagesProvider(SystemMessagesProvider)}. + *
  • {@link ErrorHandler} is bound to created sessions by + * {@link VaadinSession#setErrorHandler(ErrorHandler)}. + *
+ * + * @see CdiVaadinServlet + */ +public class CdiVaadinServletService extends VaadinServletService { + + private final CdiVaadinServiceDelegate delegate; + + public CdiVaadinServletService(CdiVaadinServlet servlet, + DeploymentConfiguration configuration, BeanManager beanManager) { + super(servlet, configuration); + this.delegate = new CdiVaadinServiceDelegate(beanManager); + } + + @Override + public void init() throws ServiceException { + delegate.init(this); + super.init(); + } + + @Override + protected VaadinSession loadSession(WrappedSession wrappedSession) { + return super.loadSession(wrappedSession); + } + + @Override + protected void storeSession(VaadinSession session, + WrappedSession wrappedSession) { + super.storeSession(session, wrappedSession); + } + + private void restoreDelegate(VaadinSession session) { + + } + + @Override + protected Executor createDefaultExecutor() { + Instance instance = delegate + .getBeanManager().createInstance() + .select(VaadinTaskExecutorSelector.class); + VaadinTaskExecutorSelector selector = instance.get(); + Executor executor = selector.getExecutor() + .orElseGet(super::createDefaultExecutor); + instance.destroy(selector); + return executor; + } + + public Optional loadInstantiators() throws ServiceException { + BeanManager beanManager = delegate.getBeanManager(); + + final Set> beans = beanManager + .getBeans(InstantiatorFactory.class, SERVICE); + if (beans == null || beans.isEmpty()) { + throw new ServiceException("Cannot init VaadinService " + + "because no CDI instantiator factory bean found."); + } + final Bean bean; + try { + // noinspection unchecked + bean = (Bean) beanManager.resolve(beans); + } catch (AmbiguousResolutionException e) { + throw new ServiceException("There are multiple eligible CDI " + + InstantiatorFactory.class.getSimpleName() + " beans.", e); + } + + // Return the contextual instance (rather than CDI proxy) as it will be + // stored inside VaadinService. Not relying on the proxy allows + // accessing VaadinService::getInstantiator even when + // VaadinServiceScopedContext is not active + final CreationalContext creationalContext = beanManager + .createCreationalContext(bean); + final Context context = beanManager + .getContext(VaadinServiceScoped.class); + final InstantiatorFactory instantiatorFactory = context.get(bean, + creationalContext); + Instantiator instantiator = instantiatorFactory.createInstantitor(this); + + if (instantiator == null) { + throw new ServiceException("Cannot init VaadinService because " + + " no Instantiator is available. Please check your " + + " InstatiatorFactory CDI bean implementation."); + } + return Optional.of(instantiator); + } + + public CdiVaadinServlet getServlet() { + return (CdiVaadinServlet) super.getServlet(); + } + + /** + * This class implements the actual instantiation and event brokering + * functionality of {@link CdiVaadinServletService}. + */ + public static class CdiVaadinServiceDelegate implements Serializable { + + private transient BeanManager beanManager; + + private final UIEventListener uiEventListener; + + public CdiVaadinServiceDelegate(BeanManager beanManager) { + this.beanManager = beanManager; + uiEventListener = new UIEventListener(this); + } + + public void init(VaadinService vaadinService) throws ServiceException { + lookup(SystemMessagesProvider.class) + .ifPresent(vaadinService::setSystemMessagesProvider); + vaadinService.addUIInitListener(uiEventListener); + vaadinService.addSessionInitListener(this::sessionInit); + vaadinService.addSessionDestroyListener(this::sessionDestroy); + vaadinService.addServiceDestroyListener(this::fireCdiDestroyEvent); + } + + public void addUIListeners(UI ui) { + ui.addAfterNavigationListener(uiEventListener); + ui.addBeforeLeaveListener(uiEventListener); + ui.addBeforeEnterListener(uiEventListener); + ui.addPollListener(uiEventListener); + ui.addDetachListener(e -> getBeanManager().getEvent() + .fire(new UIDetachEvent(e))); + } + + public Optional lookup(Class type) throws ServiceException { + try { + T instance = new BeanLookup<>(getBeanManager(), type, SERVICE) + .lookup(); + return Optional.ofNullable(instance); + } catch (AmbiguousResolutionException e) { + throw new ServiceException("There are multiple eligible CDI " + + type.getSimpleName() + " beans.", e); + } + } + + public BeanManager getBeanManager() { + if (beanManager == null) { + beanManager = BeanManagerProvider.getInstance() + .getBeanManager(); + } + return beanManager; + } + + private void sessionInit(SessionInitEvent sessionInitEvent) + throws ServiceException { + VaadinSession session = sessionInitEvent.getSession(); + lookup(ErrorHandler.class).ifPresent(session::setErrorHandler); + getBeanManager().getEvent().fire(sessionInitEvent); + } + + private void sessionDestroy(SessionDestroyEvent sessionDestroyEvent) { + getBeanManager().getEvent().fire(sessionDestroyEvent); + if (VaadinSessionScopedContext.guessContextIsUndeployed()) { + // Happens on tomcat when it expires sessions upon undeploy. + // beanManager.getPassivationCapableBean returns null for + // passivation id, + // so we would get an NPE from AbstractContext.destroyAllActive + getLogger().warn("VaadinSessionScoped context does not exist. " + + "Maybe application is undeployed." + + " Can't destroy VaadinSessionScopedContext."); + return; + } + getLogger().debug("VaadinSessionScopedContext destroy"); + VaadinSessionScopedContext + .destroy(sessionDestroyEvent.getSession()); + } + + private void fireCdiDestroyEvent(ServiceDestroyEvent event) { + try { + getBeanManager().getEvent().fire(event); + } catch (Exception e) { + // During application shutdown on TomEE 7, + // beans are lost at this point. + // Does not throw an exception, but catch anything just to be + // sure. + getLogger().warn( + "Error at destroy event distribution with CDI.", e); + } + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(CdiVaadinServiceDelegate.class); + } + } + + /** + * Static listener class, to avoid registering the whole service instance. + */ + @ListenerPriority(-100) // navigation event listeners are last by default + private static class UIEventListener implements UIInitListener, + AfterNavigationListener, BeforeEnterListener, BeforeLeaveListener, + ComponentEventListener { + + private final CdiVaadinServiceDelegate delegate; + + private UIEventListener(CdiVaadinServiceDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void uiInit(UIInitEvent event) { + // UI listeners must be attached before observers are notified of + // the UI initialization + delegate.addUIListeners(event.getUI()); + delegate.getBeanManager().getEvent().fire(event); + } + + @Override + public void afterNavigation(AfterNavigationEvent event) { + delegate.getBeanManager().getEvent().fire(event); + } + + @Override + public void beforeEnter(BeforeEnterEvent event) { + delegate.getBeanManager().getEvent().fire(event); + } + + @Override + public void beforeLeave(BeforeLeaveEvent event) { + delegate.getBeanManager().getEvent().fire(event); + } + + @Override + public void onComponentEvent(PollEvent event) { + delegate.getBeanManager().getEvent().fire(event); + } + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java new file mode 100644 index 00000000000..f238fe1ae5b --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/DeploymentValidator.java @@ -0,0 +1,250 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NON_ROUTE_SCOPED_HAVE_OWNER; +import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NORMAL_SCOPED_COMPONENT; +import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.OWNER_IS_NOT_ROUTE_COMPONENT; + +@Dependent +class DeploymentValidator { + + static class BeanInfo { + + private final Bean bean; + private final Annotated annotated; + + BeanInfo(Bean bean, Annotated annotated) { + this.bean = bean; + this.annotated = annotated; + } + + Type getBaseType() { + return annotated.getBaseType(); + } + + private boolean isNormalScoped(BeanManager bm) { + return bm.isNormalScope(bean.getScope()); + } + + private boolean isRouteScoped() { + Class scope = bean.getScope(); + return scope.equals(RouteScoped.class) + || scope.equals(NormalRouteScoped.class); + } + + private boolean isComponent() { + for (Type type : bean.getTypes()) { + if (type instanceof Class + && Component.class.isAssignableFrom((Class) type)) { + return true; + } + } + return false; + } + + private Optional getRouteScopeOwner() { + for (Annotation ann : bean.getQualifiers()) { + if (ann instanceof RouteScopeOwner) { + return Optional.of((RouteScopeOwner) ann); + } + } + return Optional.empty(); + } + + } + + /** + * Represents a deployment problem to be passed to the container. Message + * and stacktrace will appear in server log. It is not thrown, or caught. + */ + static class DeploymentProblem extends Throwable { + + enum ErrorCode { + NORMAL_SCOPED_COMPONENT, + NON_ROUTE_SCOPED_HAVE_OWNER, + OWNER_IS_NOT_ROUTE_COMPONENT + } + + private final Type baseType; + private final ErrorCode errorCode; + + private DeploymentProblem(String message, Type baseType, + ErrorCode errorCode) { + super(message); + this.baseType = baseType; + this.errorCode = errorCode; + } + + /** + * For testing purposes only. + * + * @return annotated base type of the invalid bean + */ + Type getBaseType() { + return baseType; + } + + /** + * For testing purposes only. + * + * @return errorCode of the invalid bean + */ + ErrorCode getErrorCode() { + return errorCode; + } + } + + private interface BeanValidator { + + boolean isInvalid(BeanInfo beanInfo); + + ErrorCode getErrorCode(); + + String getErrorMessage(BeanInfo beanInfo); + + } + + private class NormalScopedComponentValidator implements BeanValidator { + + @Override + public boolean isInvalid(BeanInfo beanInfo) { + return beanInfo.isComponent() + && beanInfo.isNormalScoped(beanManager); + } + + @Override + public ErrorCode getErrorCode() { + return NORMAL_SCOPED_COMPONENT; + } + + @Override + public String getErrorMessage(BeanInfo beanInfo) { + return String.format( + "Normal scoped Vaadin components are not supported. " + + "'%s' should not belong to a normal scope.", + beanInfo.getBaseType().getTypeName()); + } + + } + + private class OwnerIsNotRouteComponentValidator implements BeanValidator { + + @Override + public boolean isInvalid(BeanInfo beanInfo) { + return beanInfo.isRouteScoped() + && beanInfo.getRouteScopeOwner().map(RouteScopeOwner::value) + .filter(DeploymentValidator::isNonRouteComponent) + .isPresent(); + } + + @Override + public ErrorCode getErrorCode() { + return OWNER_IS_NOT_ROUTE_COMPONENT; + } + + @Override + public String getErrorMessage(BeanInfo beanInfo) { + return String.format( + "'@%s' should define a route component on '%s'.", + RouteScopeOwner.class.getSimpleName(), + beanInfo.getBaseType().getTypeName()); + } + + } + + private class NonRouteScopedHaveOwnerValidator implements BeanValidator { + + @Override + public boolean isInvalid(BeanInfo beanInfo) { + return !beanInfo.isRouteScoped() + && beanInfo.getRouteScopeOwner().isPresent(); + } + + @Override + public ErrorCode getErrorCode() { + return NON_ROUTE_SCOPED_HAVE_OWNER; + } + + @Override + public String getErrorMessage(BeanInfo beanInfo) { + return String.format( + "'%s' should be '@%s' or '@%s' to have a '@%s'.", + beanInfo.getBaseType().getTypeName(), + RouteScoped.class.getSimpleName(), + NormalRouteScoped.class.getSimpleName(), + RouteScopeOwner.class.getSimpleName()); + } + + } + + @Inject + private BeanManager beanManager; + + private final List validators = Arrays.asList( + new NormalScopedComponentValidator(), + new OwnerIsNotRouteComponentValidator(), + new NonRouteScopedHaveOwnerValidator()); + + void validate(Set infoSet, Consumer problemConsumer) { + infoSet.forEach(info -> validateBean(info, problemConsumer)); + } + + private void validateBean(BeanInfo beanInfo, + Consumer problemConsumer) { + validators.stream().filter(validator -> validator.isInvalid(beanInfo)) + .map(validator -> new DeploymentProblem( + validator.getErrorMessage(beanInfo), + beanInfo.getBaseType(), validator.getErrorCode())) + .forEach(problemConsumer); + } + + private static boolean isNonRouteComponent(Type type) { + if (!(type instanceof Class)) { + return true; + } + Class clazz = (Class) type; + // RouteRegistry contains filtered route targets, + // but we want to ignore filters here. + return !clazz.isAnnotationPresent(Route.class) + && !HasErrorParameter.class.isAssignableFrom(clazz) + && !RouterLayout.class.isAssignableFrom(clazz); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/UIDetachEvent.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/UIDetachEvent.java new file mode 100644 index 00000000000..14cbca88463 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/UIDetachEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import com.vaadin.flow.component.DetachEvent; +import com.vaadin.flow.component.UI; + +/** + * Event fired before a {@link UI} is detached. + * + * @since 16.1 + */ +public class UIDetachEvent extends DetachEvent { + + /** + * Creates a new UI detach event with given DetachEvent. + * + * @param event + * the detach event that triggered this UI detach event + */ + public UIDetachEvent(DetachEvent event) { + super(event.getSource()); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinExtension.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinExtension.java new file mode 100644 index 00000000000..a9e591799ac --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinExtension.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AfterDeploymentValidation; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessBean; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Executor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.DeploymentValidator.BeanInfo; +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.annotation.NormalUIScoped; +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.context.ContextWrapper; +import com.vaadin.cdi.context.RouteScopedContext; +import com.vaadin.cdi.context.UIScopedContext; +import com.vaadin.cdi.context.VaadinServiceScopedContext; +import com.vaadin.cdi.context.VaadinSessionScopedContext; +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.DependentProvider; + +/** + * CDI Extension needed to register Vaadin scopes to the runtime. + */ +public class VaadinExtension implements Extension { + + private VaadinServiceScopedContext serviceScopedContext; + private VaadinSessionScopedContext sessionScopedContext; + private UIScopedContext uiScopedContext; + private RouteScopedContext routeScopedContext; + private Set beanInfoSet = new HashSet<>(); + + private void storeBeanValidationInfo(@Observes ProcessBean processBean) { + beanInfoSet.add(new BeanInfo(processBean.getBean(), + processBean.getAnnotated())); + } + + private void addContexts(@Observes AfterBeanDiscovery afterBeanDiscovery, + BeanManager beanManager) { + serviceScopedContext = new VaadinServiceScopedContext(beanManager); + sessionScopedContext = new VaadinSessionScopedContext(beanManager); + uiScopedContext = new UIScopedContext(beanManager); + routeScopedContext = new RouteScopedContext(beanManager); + addContext(afterBeanDiscovery, serviceScopedContext, null); + addContext(afterBeanDiscovery, sessionScopedContext, null); + addContext(afterBeanDiscovery, uiScopedContext, NormalUIScoped.class); + addContext(afterBeanDiscovery, routeScopedContext, + NormalRouteScoped.class); + } + + // Validate annotated executor + + private void initializeContexts(@Observes AfterDeploymentValidation adv, + BeanManager beanManager) { + serviceScopedContext.init(beanManager); + sessionScopedContext.init(beanManager); + uiScopedContext.init(beanManager); + routeScopedContext.init(beanManager, uiScopedContext::isActive); + } + + private void validateDeployment(@Observes AfterDeploymentValidation adv, + BeanManager beanManager) { + DependentProvider validatorProvider = BeanProvider + .getDependent(beanManager, DeploymentValidator.class); + DeploymentValidator validator = validatorProvider.get(); + validator.validate(beanInfoSet, adv::addDeploymentProblem); + validatorProvider.destroy(); + beanInfoSet = null; + } + + private void ensureAtMostOneVaadinTaskExecutor( + @Observes AfterDeploymentValidation event, + BeanManager beanManager) { + Set> candidates = beanManager.getBeans(Executor.class, + BeanLookup.SERVICE); + if (candidates.size() > 1) { + event.addDeploymentProblem(new IllegalStateException( + "There must be at most one Executor bean annotated with @" + + VaadinServiceEnabled.class.getSimpleName() + + " in the application. " + "Found " + + candidates.size() + ": " + candidates)); + } + } + + private void addContext(AfterBeanDiscovery afterBeanDiscovery, + AbstractContext context, + Class additionalScope) { + afterBeanDiscovery + .addContext(new ContextWrapper(context, context.getScope())); + if (additionalScope != null) { + afterBeanDiscovery + .addContext(new ContextWrapper(context, additionalScope)); + } + getLogger().info("{} registered for Vaadin CDI", + context.getClass().getSimpleName()); + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(VaadinExtension.class); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinTaskExecutorSelector.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinTaskExecutorSelector.java new file mode 100644 index 00000000000..43174ca39d8 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/VaadinTaskExecutorSelector.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.annotation.Resource; +import jakarta.enterprise.concurrent.ManagedExecutorService; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; + +/** + * Utility class responsible for selecting an {@link Executor} for task + * execution within a Vaadin-based application. + *

+ * The class attempts to provide a custom {@link Executor} that is marked with + * the {@link VaadinServiceEnabled} qualifier, leveraging the CDI (Contexts and + * Dependency Injection) mechanism for bean resolution. If such a custom + * executor is resolvable, it is returned. Otherwise, a potential container + * provided {@link ManagedExecutorService} is used as a fallback executor. + */ +class VaadinTaskExecutorSelector { + + /* + * Workaround for environments that aren't fully Jakarta EE compliant (e.g., + * Tomcat with Weld). + * + * This inner class allows us to check at runtime whether + * ManagedExecutorService is available without causing deployment failures. + * By programmatically resolving this bean through CDI, we can gracefully + * handle two potential issues: 1. Missing ManagedExecutorService in the + * classpath 2. javax.naming.NameNotFoundException when JNDI lookup fails + * + * This approach prevents hard deployment failures and allows the + * application to fall back to alternative execution strategies when + * necessary. + * + * See https://github.com/vaadin/cdi/issues/476 for more details. + */ + static class FromResource { + @Resource + ManagedExecutorService managedExecutor; + } + + @Inject + BeanManager beanManager; + + @Inject + @VaadinServiceEnabled + Instance customExecutor; + + /** + * Provides an {@link Optional} containing an {@link Executor}, based on the + * resolvability of a custom {@link Executor} annotated with + * {@code @VaadinServiceEnabled}. If a custom executor is not resolvable, + * the method falls back to a default {@link ManagedExecutorService}. + * + * @return an {@link Optional} containing the selected {@link Executor}, or + * an empty {@link Optional} if no executor is available. + */ + Optional getExecutor() { + if (customExecutor.isResolvable()) { + LoggerFactory.getLogger(VaadinTaskExecutorSelector.class).debug( + "Using custom Vaadin Executor {}", + customExecutor.getHandle().getBean()); + return Optional.of(customExecutor.get()); + } else if (customExecutor.isAmbiguous()) { + String candidates = customExecutor.handlesStream() + .map(handle -> handle.getBean().toString()) + .collect(Collectors.joining(", ", "[", "]")); + String message = String.format( + "Multiple Executor beans annotated with @%1$s found: %2$s. " + + "Please make sure a single instance is resolvable.", + VaadinServiceEnabled.class.getSimpleName(), candidates); + throw new IllegalStateException(message); + } + Instance managerFromResource = beanManager + .createInstance().select(FromResource.class); + + ExecutorService resolvedManagedExecutor = null; + if (managerFromResource.isResolvable()) { + resolvedManagedExecutor = managerFromResource.get().managedExecutor; + } + if (resolvedManagedExecutor != null) { + LoggerFactory.getLogger(VaadinTaskExecutorSelector.class) + .debug("Using container Managed Executor"); + } + return Optional.ofNullable(resolvedManagedExecutor); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/CdiComponent.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/CdiComponent.java new file mode 100644 index 00000000000..c69316ffa54 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/CdiComponent.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2022 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.enterprise.inject.Stereotype; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This is a {@code @Stereotype} to let the container scan and manage the + * instances of the annotated types. + *

+ * Annotating the beans with this annotation is necessary when the + * {@code bean-discovery-mode="annotated"} is set in the beans.xml file + * (implicit bean archive), and in this case CDI can only manage and inject + * beans annotated with bean defining annotations. + *

+ * Although, this can be applied to any type of beans such as service objects, + * it is originally designed to be used on Vaadin components or views that may + * or may not already bean annotated by any other pseudo-scope annotations e.g. + * {@code @RouteScoped} or {@code @UIScoped}. See + * https://docs.jboss.org/cdi/spec/1.2/cdi-spec.html#normal_scope for more + * information about scopes and pseudo-scopes. + *

+ * If the explicit bean archive strategy is in use (via an empty beans.xml or + * the one containing bean-discovery-mode="all"), CDI can manage and inject any + * bean, except those annotated with @Vetoed. + */ +@Stereotype +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CdiComponent { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalRouteScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalRouteScoped.java new file mode 100644 index 00000000000..2b3cc02adc0 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalRouteScoped.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.enterprise.context.NormalScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +/** + * The lifecycle of a NormalRouteScoped bean is controlled by route navigation. + *

+ * Every NormalRouteScoped bean belongs to one router component owner. It can be + * a {@link Route @Route}, or a {@link RouterLayout}, or a + * {@link HasErrorParameter HasErrorParameter}. Beans are qualified by + * {@link RouteScopeOwner @RouteScopeOwner} to link with their owner. + *

+ * Until owner remains active, all beans owned by it remain in the scope. + *

+ * You cannot use this scope with Vaadin Components. Proxy Components do not + * work correctly within the Vaadin framework, so as a precaution the Vaadin CDI + * plugin will not deploy if any such beans are discovered. + *

+ * The sister annotation to this is the {@link RouteScoped}. Both annotations + * reference the same underlying scope, so it is possible to get both a proxy + * and a direct reference to the same object by using different annotations. + */ +@NormalScope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NormalRouteScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalUIScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalUIScoped.java new file mode 100644 index 00000000000..8c44e74792c --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/NormalUIScoped.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.enterprise.context.NormalScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The lifecycle of a UIScoped component is bound to a browser tab. + *

+ * Injecting with this annotation will create a proxy for the contextual + * instance rather than provide the contextual instance itself. + *

+ * When using proxies, be aware that it's not guaranteed that the hashcode or + * equals will match when comparing a proxy to it's underlying instance. It's + * imperative to be aware of this when (for example) adding proxies to a + * Collection. + *

+ * You cannot use this scope with Vaadin Components. Proxy Components do not + * work correctly within the Vaadin framework, so as a precaution the Vaadin CDI + * plugin will not deploy if any such beans are discovered. + *

+ * The sister annotation to this is the {@link UIScoped}. Both annotations + * reference the same underlying scope, so it is possible to get both a proxy + * and a direct reference to the same object by using different annotations. + */ +@NormalScope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NormalUIScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScopeOwner.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScopeOwner.java new file mode 100644 index 00000000000..aa753326641 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScopeOwner.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Link a {@link RouteScoped @RouteScoped}, or + * {@link NormalRouteScoped @NormalRouteScoped} bean to its owner. + *

+ * Owner is a router component. A {@link Route @Route}, or a + * {@link RouterLayout}, or a {@link HasErrorParameter}. + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface RouteScopeOwner { + /** + * Owner class of the qualified {@link RouteScoped @RouteScoped}, or + * {@link NormalRouteScoped @NormalRouteScoped} bean. + *

+ * A {@link Route @Route}, or a {@link RouterLayout}, or a + * {@link HasErrorParameter} + * + * @return owner class + */ + Class value(); +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java new file mode 100644 index 00000000000..0f97f3dddb3 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/RouteScoped.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.inject.Scope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +/** + * The lifecycle of a RouteScoped component is controlled by route navigation. + *

+ * Every RouteScoped bean belongs to one router component owner. It can be a + * {@link Route @Route} component, or a {@link RouterLayout}, or a + * {@link HasErrorParameter HasErrorParameter}. Beans are qualified by + * {@link RouteScopeOwner @RouteScopeOwner} to link with their owner. + *

+ * Until owner remains active, all beans owned by it remain in the scope. + *

+ * Without the {@link RouteScopeOwner} annotation the owner is the current route + * target component (dynamically calculated). With nested routing hierarchies, + * the target is the "leaf" or "bottom most" routing component. The beans are + * preserved as long as the owner component remains in the navigation chain. It + * means that the bean may be preserved even if the navigation target is changed + * (but the "initial" calculated owner is still in the navigation chain). + *

+ * Injection with this annotation will create a direct reference to the object + * rather than a proxy. + *

+ * There are some limitations when not using proxies. Circular referencing (that + * is, injecting A to B and B to A) will not work. Injecting into a larger scope + * will bind the instance from the currently active smaller scope, and will + * ignore smaller scope change. For example after being injected into session + * scope it will point to the same RouteScoped bean instance ( even it is + * destroyed ) regardless of UI, or any navigation change. + *

+ * The sister annotation to this is the {@link NormalRouteScoped}. Both + * annotations reference the same underlying scope, so it is possible to get + * both a proxy and a direct reference to the same object by using different + * annotations. + */ +@Scope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface RouteScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/UIScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/UIScoped.java new file mode 100644 index 00000000000..a269816d24f --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/UIScoped.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.inject.Scope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.flow.component.UI; + +/** + * The lifecycle of a UIScoped component is bound to a browser tab. + *

+ * Injection with this annotation will create a direct reference to the object + * rather than a proxy. + *

+ * There are some limitations when not using proxies. Circular referencing (that + * is, injecting A to B and B to A) will not work. Injecting into a larger scope + * will bind the instance from the currently active smaller scope, and will + * ignore smaller scope change. For example after being injected into session + * scope it will point to the same {@link UIScoped} bean instance ( even its + * {@link UI} is closed ) regardless of {@link UI} change. + *

+ * The sister annotation to this is the {@link NormalUIScoped}. Both annotations + * reference the same underlying scope, so it is possible to get both a proxy + * and a direct reference to the same object by using different annotations. + */ +@Scope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface UIScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceEnabled.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceEnabled.java new file mode 100644 index 00000000000..85323385ea0 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceEnabled.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Qualifier to mark Vaadin service implementations. + * + * Qualified CDI beans implementing {@link com.vaadin.flow.i18n.I18NProvider}, + * and {@link com.vaadin.flow.di.Instantiator} interfaces are loaded. + */ +@Qualifier +@Retention(RUNTIME) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +public @interface VaadinServiceEnabled { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceScoped.java new file mode 100644 index 00000000000..9e93ed72356 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinServiceScoped.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.enterprise.context.NormalScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The lifecycle of a VaadinServiceScoped bean is bound to a + * {@link com.vaadin.flow.server.VaadinService}. + *

+ * Injecting with this annotation will create a proxy for the contextual + * instance rather than provide the contextual instance itself. + */ +@NormalScope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface VaadinServiceScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinSessionScoped.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinSessionScoped.java new file mode 100644 index 00000000000..554505671c0 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/annotation/VaadinSessionScoped.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.annotation; + +import jakarta.enterprise.context.NormalScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.flow.server.VaadinSession; + +/** + * The lifecycle of a VaadinSessionScoped bean is bound to a + * {@link VaadinSession}. + *

+ * Injecting with this annotation will create a proxy for the contextual + * instance rather than provide the contextual instance itself. + *

+ * Contextual instances stored in {@link VaadinSession}, so indirectly stored in + * HTTP session. {@link jakarta.annotation.PreDestroy} called after + * {@link com.vaadin.flow.server.SessionDestroyEvent} fired. + * + * @since 3.0 + */ +@NormalScope +@Inherited +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD, + ElementType.METHOD, ElementType.CONSTRUCTOR }) +@Retention(RetentionPolicy.RUNTIME) +public @interface VaadinSessionScoped { +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java new file mode 100644 index 00000000000..8926a825b3c --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/AbstractContextualStorageManager.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.ContextualStorage; + +/** + * Base class for manage and store ContextualStorages. + * + * This class is responsible for - creating, and providing the ContextualStorage + * for a context key - destroying ContextualStorages + */ +@SuppressWarnings("CdiManagedBeanInconsistencyInspection") +abstract class AbstractContextualStorageManager implements Serializable { + @Inject + private BeanManager beanManager; + private final boolean concurrent; + private final Map storageMap; + + protected AbstractContextualStorageManager(boolean concurrent) { + if (concurrent) { + this.storageMap = new ConcurrentHashMap<>(); + } else { + this.storageMap = new HashMap<>(); + } + this.concurrent = concurrent; + } + + protected ContextualStorage getContextualStorage(K key, + boolean createIfNotExist) { + if (createIfNotExist) { + return storageMap.computeIfAbsent(key, this::newContextualStorage); + } else { + return storageMap.get(key); + } + } + + protected void relocate(K from, K to) { + ContextualStorage storage = storageMap.remove(from); + if (storage != null) { + storageMap.put(to, storage); + } + } + + protected ContextualStorage newContextualStorage(K key) { + // Not required by the spec, but in reality beans are + // PassivationCapable. + // Even for non serializable bean classes. + // CDI implementations use PassivationCapable beans, + // because injecting non serializable proxies might block serialization + // of bean instances in a passivation capable context. + return new ContextualStorage(beanManager, concurrent, true); + } + + @PreDestroy + protected void destroyAll() { + Collection storages = storageMap.values(); + for (ContextualStorage storage : storages) { + AbstractContext.destroyAllActive(storage); + } + storageMap.clear(); + } + + protected void destroy(K key) { + ContextualStorage storage = storageMap.remove(key); + if (storage != null) { + AbstractContext.destroyAllActive(storage); + } + } + + protected Set getKeySet() { + return Collections.unmodifiableSet(storageMap.keySet()); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/ContextWrapper.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/ContextWrapper.java new file mode 100644 index 00000000000..e8facddf9bb --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/ContextWrapper.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.spi.AlterableContext; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; + +import java.lang.annotation.Annotation; + +import com.vaadin.cdi.util.AbstractContext; + +/** + * Used to bind multiple scope annotations to a single context. Will delegate + * all context-related operations to it's underlying instance, apart from + * getting the scope of the context. + * + */ +public class ContextWrapper implements AlterableContext { + + private final AbstractContext context; + private final Class scope; + + public ContextWrapper(AbstractContext context, + Class scope) { + this.context = context; + this.scope = scope; + } + + @Override + public Class getScope() { + return scope; + } + + @Override + public T get(Contextual component, + CreationalContext creationalContext) { + return context.get(component, creationalContext); + } + + @Override + public T get(Contextual component) { + return context.get(component); + } + + @Override + public boolean isActive() { + return context.isActive(); + } + + @Override + public void destroy(Contextual contextual) { + context.destroy(contextual); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java new file mode 100644 index 00000000000..0a0776b59c6 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/RouteScopedContext.java @@ -0,0 +1,422 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.PassivationCapable; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.page.ExtendedClientDetails; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.PreserveOnRefresh; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.server.VaadinSession; + +import static jakarta.enterprise.event.Reception.IF_EXISTS; + +/** + * Context for {@link RouteScoped @RouteScoped} beans. + */ +public class RouteScopedContext extends AbstractContext { + + @VaadinSessionScoped + public static class ContextualStorageManager + extends AbstractContextualStorageManager { + + /** + * Prefix of storage identifiers bound to a single UI instance. + */ + private static final String UI_STORE_ID_PREFIX = "uid-"; + + /** + * Prefix of storage identifiers bound to a browser window, and thus + * shared by all the UIs of that window. + */ + private static final String WINDOW_STORE_ID_PREFIX = "win-"; + + public ContextualStorageManager() { + // Session lock checked in VaadinSessionScopedContext while + // getting the session attribute. + super(false); + } + + @Override + protected ContextualStorage newContextualStorage(RouteStorageKey key) { + UI.getCurrent().addDetachListener( + event -> handleUIDetach(event.getUI(), key)); + return super.newContextualStorage(key); + } + + private void onAfterNavigation( + @Observes(notifyObserver = IF_EXISTS) AfterNavigationEvent event) { + Set> activeChain = event.getActiveChain().stream() + .map(Object::getClass).collect(Collectors.toSet()); + + destroyDescopedBeans(event.getLocationChangeEvent().getUI(), + activeChain); + + } + + private void onBeforeEnter(@Observes BeforeEnterEvent event) { + UI ui = event.getUI(); + ComponentUtil.setData(ui, NavigationData.class, new NavigationData( + event.getNavigationTarget(), event.getLayouts())); + + Set> activeChain = new HashSet<>(); + activeChain.add(event.getNavigationTarget()); + activeChain.addAll(event.getLayouts()); + + destroyDescopedBeans(ui, activeChain); + } + + private void destroyDescopedBeans(UI ui, + Set> navigationChain) { + Set uiStoreIds = getUIStoreIds(ui); + + Set missingKeys = getKeySet().stream() + .filter(key -> uiStoreIds.contains(key.getUIId())) + .filter(key -> !navigationChain.contains(key.getOwner())) + .collect(Collectors.toSet()); + + missingKeys.forEach(this::destroy); + } + + private void handleUIDetach(UI ui, RouteStorageKey key) { + if (getContextualStorage(key, false) == null) { + // The storage has been relocated to another key because the + // scope of its owner changed, or it is already destroyed. + return; + } + if (!key.isWindowScoped()) { + // The storage belongs to this UI only, so there is nothing to + // preserve for a potential UI created by a page refresh. + destroy(key); + return; + } + UI uiAfterRefresh = findPreservingUI(ui); + if (uiAfterRefresh == null) { + destroy(key); + } else { + uiAfterRefresh.addDetachListener( + event -> handleUIDetach(event.getUI(), key)); + } + } + + private UI findPreservingUI(UI ui) { + VaadinSession session = ui.getSession(); + String windowName = getWindowName(ui); + for (UI sessionUi : session.getUIs()) { + if (sessionUi != ui && windowName != null + && windowName.equals(getWindowName(sessionUi))) { + return sessionUi; + } + } + return null; + } + + private static String getWindowName(UI ui) { + ExtendedClientDetails details = ui.getInternals() + .getExtendedClientDetails(); + return details.getWindowName(); + } + + private RouteStorageKey getKey(UI ui, Class owner) { + RouteStorageKey uiKey = new RouteStorageKey(owner, getUIStoreId(ui), + false); + String windowName = getWindowName(ui); + if (windowName == null) { + return uiKey; + } + RouteStorageKey windowKey = new RouteStorageKey(owner, + WINDOW_STORE_ID_PREFIX + windowName, true); + // Beans are shared with the UI created by a page refresh only if + // the navigation chain is preserved by Flow. In that case the + // storage is bound to the browser window, exactly like Flow binds + // the preserved component chain. + if (isPreserveOnRefreshChain(ui)) { + return rescope(uiKey, windowKey, ui); + } + // The owner may stay in the navigation chain while the chain stops + // being preserved, for example navigating from a preserved view to + // a plain sibling of the same layout. Its beans are then bound back + // to this UI, unless another UI of the same browser window is still + // alive and may be holding the preserved chain. + if (getContextualStorage(windowKey, false) != null + && findPreservingUI(ui) == null) { + return rescope(windowKey, uiKey, ui); + } + return uiKey; + } + + /** + * Moves the storage of an owner whose scope changed, so that its beans + * are not recreated while the owner stays in the navigation chain. + * + * @return the key the storage of the owner is bound to, always + * {@code to} + */ + private RouteStorageKey rescope(RouteStorageKey from, + RouteStorageKey to, UI ui) { + if (getContextualStorage(to, false) == null + && getContextualStorage(from, false) != null) { + relocate(from, to); + // The listener registered for the previous key does not find + // any storage anymore, so the new key needs its own. + ui.addDetachListener( + event -> handleUIDetach(event.getUI(), to)); + } + return to; + } + + private List getActiveContextualStorages() { + Set uiStoreIds = getUIStoreIds(UI.getCurrent()); + return getKeySet().stream() + .filter(key -> uiStoreIds.contains(key.getUIId())) + .map(key -> getContextualStorage(key, false)) + .collect(Collectors.toList()); + } + + /** + * Gets all the storage identifiers a UI can hold beans for: its own + * one, plus the one shared by all the UIs of the same browser window. A + * single UI may own both kinds of storage, for example after navigating + * from a regular view to a {@link PreserveOnRefresh} one. + */ + private Set getUIStoreIds(UI ui) { + Set ids = new HashSet<>(); + ids.add(getUIStoreId(ui)); + String windowName = getWindowName(ui); + if (windowName != null) { + ids.add(WINDOW_STORE_ID_PREFIX + windowName); + } + return ids; + } + + private String getUIStoreId(UI ui) { + return UI_STORE_ID_PREFIX + ui.getUIId(); + } + + private static boolean isPreserveOnRefreshChain(UI ui) { + NavigationData data = ComponentUtil.getData(ui, + NavigationData.class); + if (data == null) { + return false; + } + return isPreserveOnRefresh(data.getNavigationTarget()) + || data.getLayouts().stream().anyMatch( + ContextualStorageManager::isPreserveOnRefresh); + } + + private static boolean isPreserveOnRefresh(Class clazz) { + return clazz != null + && clazz.isAnnotationPresent(PreserveOnRefresh.class); + } + + } + + private static class RouteStorageKey implements Serializable { + private final Class owner; + private final String uiId; + // Derived from uiId, so it is not part of equals/hashCode + private final boolean windowScoped; + + private RouteStorageKey(Class owner, String uiId, + boolean windowScoped) { + this.owner = owner; + this.uiId = uiId; + this.windowScoped = windowScoped; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RouteStorageKey)) { + return false; + } + if (obj == this) { + return true; + } + RouteStorageKey key = (RouteStorageKey) obj; + return owner.equals(key.owner) && uiId.equals(key.uiId); + } + + @Override + public int hashCode() { + return Objects.hash(owner, uiId); + } + + @Override + public String toString() { + return "[ ui-key='" + getUIId() + "', owner='" + getOwner() + "' ]"; + } + + Class getOwner() { + return owner; + } + + String getUIId() { + return uiId; + } + + /** + * Whether the storage is shared by all the UIs of the same browser + * window, as required to preserve beans of a {@link PreserveOnRefresh} + * navigation chain. + */ + boolean isWindowScoped() { + return windowScoped; + } + + } + + static class NavigationData implements Serializable { + private final Class navigationTarget; + private final List> layouts; + + NavigationData(Class navigationTarget, + List> layouts) { + this.navigationTarget = navigationTarget; + this.layouts = layouts; + } + + Class getNavigationTarget() { + return navigationTarget; + } + + List> getLayouts() { + return layouts; + } + } + + private ContextualStorageManager contextManager; + private Supplier isUIContextActive; + private BeanManager beanManager; + + public RouteScopedContext(BeanManager beanManager) { + super(beanManager); + } + + public void init(BeanManager beanManager, + Supplier isUIContextActive) { + contextManager = BeanProvider.getContextualReference(beanManager, + ContextualStorageManager.class, false); + this.beanManager = beanManager; + this.isUIContextActive = isUIContextActive; + } + + @Override + public Class getScope() { + return RouteScoped.class; + } + + @Override + public boolean isActive() { + return isUIContextActive.get(); + } + + @Override + protected List getActiveContextualStorages() { + return contextManager.getActiveContextualStorages(); + } + + @Override + protected ContextualStorage getContextualStorage(Contextual contextual, + boolean createIfNotExist) { + Bean bean = getBean(contextual); + UI ui = UI.getCurrent(); + Class owner = getOwner(ui, bean); + if (!navigationChainHasOwner(ui, owner) && createIfNotExist) { + throw new IllegalStateException(String.format( + "Route owner '%s' instance is not available in the " + + "active navigation components chain: the scope defined by the bean '%s' doesn't exist.", + owner, bean.getBeanClass().getName())); + } + RouteStorageKey key = contextManager.getKey(ui, owner); + return contextManager.getContextualStorage(key, createIfNotExist); + } + + private boolean navigationChainHasOwner(UI ui, Class owner) { + NavigationData data = ComponentUtil.getData(ui, NavigationData.class); + if (data == null) { + // No navigation has happened on this UI yet, so there is no chain + // for the owner to be part of. Reporting that as "not in the chain" + // lets the caller raise its own IllegalStateException, which names + // the owner and the bean, instead of failing with a bare NPE. + return false; + } + if (owner.equals(data.getNavigationTarget())) { + return true; + } + return data.getLayouts().stream() + .anyMatch(clazz -> clazz.equals(owner)); + } + + @SuppressWarnings("unchecked") + private Class getOwner(UI ui, Bean bean) { + return bean.getQualifiers().stream() + .filter(annotation -> annotation instanceof RouteScopeOwner) + .map(annotation -> (Class) (((RouteScopeOwner) annotation) + .value())) + .findFirst() + .orElseGet(() -> getCurrentNavigationTarget(ui, bean)); + } + + @SuppressWarnings("rawtypes") + private Class getCurrentNavigationTarget(UI ui, Bean bean) { + NavigationData data = ComponentUtil.getData(ui, NavigationData.class); + if (data == null) { + throw new IllegalStateException(String.format( + "There is no yet any navigation chain available, " + + "so bean '%s' has no scope and may not be injected", + bean.getBeanClass().getName())); + } + if (data.getLayouts().contains(bean.getBeanClass())) + return bean.getBeanClass(); + return data.getNavigationTarget(); + } + + private Bean getBean(Contextual contextual) { + if (contextual instanceof Bean) { + return (Bean) contextual; + } + if (contextual instanceof PassivationCapable) { + String id = ((PassivationCapable) contextual).getId(); + return beanManager.getPassivationCapableBean(id); + } else { + throw new IllegalArgumentException(contextual.getClass().getName() + + " is not of type " + Bean.class.getName()); + } + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/UIScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/UIScopedContext.java new file mode 100644 index 00000000000..db3cb66e7d1 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/UIScopedContext.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.lang.annotation.Annotation; + +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.component.DetachEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinSession; + +/** + * UIScopedContext is the context for {@link UIScoped @UIScoped} beans. + */ +public class UIScopedContext extends AbstractContext { + + private ContextualStorageManager contextualStorageManager; + + public UIScopedContext(final BeanManager beanManager) { + super(beanManager); + } + + @Override + protected ContextualStorage getContextualStorage(Contextual contextual, + boolean createIfNotExist) { + return contextualStorageManager.getContextualStorage(createIfNotExist); + } + + public void init(BeanManager beanManager) { + contextualStorageManager = BeanProvider.getContextualReference( + beanManager, ContextualStorageManager.class, false); + } + + @Override + public Class getScope() { + return UIScoped.class; + } + + @Override + public boolean isActive() { + return VaadinSession.getCurrent() != null && UI.getCurrent() != null + && contextualStorageManager != null; + } + + @VaadinSessionScoped + public static class ContextualStorageManager + extends AbstractContextualStorageManager { + + public ContextualStorageManager() { + // Session lock checked in VaadinSessionScopedContext while + // getting the session attribute of this beans context. + super(false); + } + + public ContextualStorage getContextualStorage( + boolean createIfNotExist) { + final Integer uiId = UI.getCurrent().getUIId(); + return super.getContextualStorage(uiId, createIfNotExist); + } + + @Override + protected ContextualStorage newContextualStorage(Integer uiId) { + UI.getCurrent().addDetachListener(this::destroy); + return super.newContextualStorage(uiId); + } + + private void destroy(DetachEvent event) { + final int uiId = event.getUI().getUIId(); + super.destroy(uiId); + } + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinServiceScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinServiceScopedContext.java new file mode 100644 index 00000000000..2143108085c --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinServiceScopedContext.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.lang.annotation.Annotation; + +import com.vaadin.cdi.CdiVaadinServlet; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.server.ServiceDestroyEvent; +import com.vaadin.flow.server.VaadinServlet; +import com.vaadin.flow.server.VaadinServletService; + +import static jakarta.enterprise.event.Reception.IF_EXISTS; + +/** + * Context for {@link VaadinServiceScoped @VaadinServiceScoped} beans. + */ +public class VaadinServiceScopedContext extends AbstractContext { + + private ContextualStorageManager contextManager; + + public VaadinServiceScopedContext(BeanManager beanManager) { + super(beanManager); + } + + public void init(BeanManager beanManager) { + contextManager = BeanProvider.getContextualReference(beanManager, + ContextualStorageManager.class, false); + } + + @Override + protected ContextualStorage getContextualStorage(Contextual contextual, + boolean createIfNotExist) { + CdiVaadinServlet servlet = (CdiVaadinServlet) VaadinServlet + .getCurrent(); + String servletName; + if (servlet != null) { + servletName = servlet.getServletName(); + } else { + servletName = CdiVaadinServlet.getCurrentServletName(); + } + return contextManager.getContextualStorage(servletName, + createIfNotExist); + } + + @Override + public Class getScope() { + return VaadinServiceScoped.class; + } + + @Override + public boolean isActive() { + VaadinServlet servlet = VaadinServlet.getCurrent(); + return servlet instanceof CdiVaadinServlet || (servlet == null + && CdiVaadinServlet.getCurrentServletName() != null); + } + + @ApplicationScoped + public static class ContextualStorageManager + extends AbstractContextualStorageManager { + + public ContextualStorageManager() { + super(true); + } + + /** + * Service destroy event observer. + * + * During application shutdown it is container specific whether this + * observer being called, or not. Application context destroy may happen + * earlier, and cleanup done by {@link #destroyAll()}. + * + * @param event + * service destroy event + */ + private void onServiceDestroy( + @Observes(notifyObserver = IF_EXISTS) ServiceDestroyEvent event) { + if (!(event.getSource() instanceof VaadinServletService)) { + return; + } + VaadinServletService service = (VaadinServletService) event + .getSource(); + String servletName = service.getServlet().getServletName(); + destroy(servletName); + } + + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinSessionScopedContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinSessionScopedContext.java new file mode 100644 index 00000000000..ee154d3ab2e --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/context/VaadinSessionScopedContext.java @@ -0,0 +1,142 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import java.lang.annotation.Annotation; + +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.util.AbstractContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.ContextUtils; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.server.VaadinSession; + +/** + * Context for {@link VaadinSessionScoped @VaadinSessionScoped} beans. + *

+ * Stores contextuals in {@link VaadinSession}. Other Vaadin CDI contexts are + * stored in the corresponding {@link VaadinSessionScoped} context. + *

+ * The context is active when the current {@link VaadinSession} is bound to the + * calling thread and that thread holds the session lock — i.e. when the code + * runs inside an active Vaadin request, a {@code UI#access} block, or a + * {@code VaadinSession#access} block. Code running on a background thread that + * has only set {@link VaadinSession#setCurrent(VaadinSession)} without + * acquiring the session lock is operating outside the supported usage of the + * framework; behavior in that case is not guaranteed. + * + * @since 3.0 + */ +public class VaadinSessionScopedContext extends AbstractContext { + + private ContextualStorageManager contextManager; + + public VaadinSessionScopedContext(BeanManager beanManager) { + super(beanManager); + } + + public void init(BeanManager beanManager) { + contextManager = BeanProvider.getContextualReference(beanManager, + ContextualStorageManager.class, false); + } + + @Override + protected ContextualStorage getContextualStorage(Contextual contextual, + boolean createIfNotExist) { + return contextManager.getContextualStorage(contextual, + createIfNotExist); + } + + @Override + public Class getScope() { + return VaadinSessionScoped.class; + } + + @Override + public boolean isActive() { + return contextManager != null && contextManager.isActive(); + } + + public static void destroy(VaadinSession session) { + ContextualStorageManager.destroy(session); + } + + /** + * Guess whether this context is undeployed. + * + * Tomcat expires sessions after contexts are undeployed. Need this guess to + * prevent exceptions when try to properly destroy contexts on session + * expiration. + * + * @return true when context is not active, but sure it should + */ + public static boolean guessContextIsUndeployed() { + // Given there is a current VaadinSession, we should have an active + // context, + // except we get here after the application is undeployed. + return (VaadinSession.getCurrent() != null + && !ContextUtils.isContextActive(VaadinSessionScoped.class)); + } + + /** + * For internal use only. + */ + @ApplicationScoped + public static class ContextualStorageManager { + + protected static final String ATTRIBUTE_NAME = VaadinSessionScopedContext.class + .getName(); + + @Inject + private BeanManager beanManager; + + protected boolean isActive() { + VaadinSession session = VaadinSession.getCurrent(); + return session != null && session.hasLock(); + } + + protected ContextualStorage getContextualStorage( + Contextual contextual, boolean createIfNotExist) { + VaadinSession session = VaadinSession.getCurrent(); + ContextualStorage storage = findContextualStorage(session); + if (storage == null && createIfNotExist) { + storage = new ContextualStorage(beanManager, false, true); + session.setAttribute(ATTRIBUTE_NAME, storage); + } + return storage; + } + + private static ContextualStorage findContextualStorage( + VaadinSession session) { + // session lock is checked inside + return (ContextualStorage) session.getAttribute(ATTRIBUTE_NAME); + } + + private static void destroy(VaadinSession session) { + ContextualStorage storage = findContextualStorage(session); + if (storage != null) { + AbstractContext.destroyAllActive(storage); + } + } + + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AbstractContext.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AbstractContext.java new file mode 100644 index 00000000000..7191eb4f486 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AbstractContext.java @@ -0,0 +1,214 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.PassivationCapable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A skeleton containing the most important parts of a custom CDI Context. An + * implementing Context needs to implement the missing methods from the + * {@link Context} interface and + * {@link #getContextualStorage(Contextual, boolean)}. + */ +public abstract class AbstractContext implements Context { + /** + * Whether the Context is for a passivating scope. + */ + private final boolean passivatingScope; + + protected AbstractContext(BeanManager beanManager) { + passivatingScope = beanManager.isPassivatingScope(getScope()); + } + + /** + * An implementation has to return the underlying storage which contains the + * items held in the Context. + * + * @param createIfNotExist + * whether a ContextualStorage shall get created if it doesn't + * yet exist. + * @return the underlying storage + */ + protected abstract ContextualStorage getContextualStorage( + Contextual contextual, boolean createIfNotExist); + + protected List getActiveContextualStorages() { + List result = new ArrayList(); + result.add(getContextualStorage(null, false)); + return result; + } + + /** + * @return whether the served scope is a passivating scope + */ + public boolean isPassivatingScope() { + return passivatingScope; + } + + @Override + public T get(Contextual bean) { + checkActive(); + + ContextualStorage storage = getContextualStorage(bean, false); + if (storage == null) { + return null; + } + + Map> contextMap = storage + .getStorage(); + ContextualInstanceInfo contextualInstanceInfo = contextMap + .get(storage.getBeanKey(bean)); + if (contextualInstanceInfo == null) { + return null; + } + + return (T) contextualInstanceInfo.getContextualInstance(); + } + + @Override + public T get(Contextual bean, + CreationalContext creationalContext) { + if (creationalContext == null) { + return get(bean); + } + + checkActive(); + + if (passivatingScope) { + if (!(bean instanceof PassivationCapable)) { + throw new IllegalStateException( + bean.toString() + " doesn't implement " + + PassivationCapable.class.getName()); + } + } + + ContextualStorage storage = getContextualStorage(bean, true); + + Map> contextMap = storage + .getStorage(); + ContextualInstanceInfo contextualInstanceInfo = contextMap + .get(storage.getBeanKey(bean)); + + if (contextualInstanceInfo != null) { + @SuppressWarnings("unchecked") + final T instance = (T) contextualInstanceInfo + .getContextualInstance(); + + if (instance != null) { + return instance; + } + } + + return storage.createContextualInstance(bean, creationalContext); + } + + /** + * Destroy the Contextual Instance of the given Bean. + * + * @param bean + * dictates which bean shall get cleaned up + * @return true if the bean was destroyed, false + * if there was no such bean. + */ + public boolean destroy(Contextual bean) { + ContextualStorage storage = getContextualStorage(bean, false); + if (storage == null) { + return false; + } + + ContextualInstanceInfo contextualInstanceInfo = storage.getStorage() + .remove(storage.getBeanKey(bean)); + + if (contextualInstanceInfo == null) { + return false; + } + + destroyBean(bean, contextualInstanceInfo); + + return true; + } + + /** + * destroys all the Contextual Instances in the Storage returned by + * {@link #getContextualStorage(Contextual, boolean)}. + */ + public void destroyAllActive() { + List storages = getActiveContextualStorages(); + if (storages == null) { + return; + } + + for (ContextualStorage storage : storages) { + if (storage != null) { + destroyAllActive(storage); + } + } + } + + /** + * Destroys all the Contextual Instances in the specified ContextualStorage. + * This is a static method to allow various holder objects to cleanup + * properly in @PreDestroy. + */ + public static Map> destroyAllActive( + ContextualStorage storage) { + // drop all entries in the storage before starting with destroying the + // original entries + Map> contextMap = new HashMap>( + storage.getStorage()); + storage.getStorage().clear(); + + for (Map.Entry> entry : contextMap + .entrySet()) { + Contextual bean = storage.getBean(entry.getKey()); + + ContextualInstanceInfo contextualInstanceInfo = entry.getValue(); + destroyBean(bean, contextualInstanceInfo); + } + return contextMap; + } + + public static void destroyBean(Contextual bean, + ContextualInstanceInfo contextualInstanceInfo) { + bean.destroy(contextualInstanceInfo.getContextualInstance(), + contextualInstanceInfo.getCreationalContext()); + } + + /** + * Make sure that the Context is really active. + * + * @throws ContextNotActiveException + * if there is no active Context for the current Thread. + */ + protected void checkActive() { + if (!isActive()) { + throw new ContextNotActiveException( + "CDI context with scope annotation @" + getScope().getName() + + " is not active with respect to the current thread"); + } + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AnyLiteral.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AnyLiteral.java new file mode 100644 index 00000000000..4b82353cfa1 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/AnyLiteral.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.util.AnnotationLiteral; + +public class AnyLiteral extends AnnotationLiteral implements Any { + private static final long serialVersionUID = -8623640277155878657L; +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanManagerProvider.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanManagerProvider.java new file mode 100644 index 00000000000..765a23f5038 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanManagerProvider.java @@ -0,0 +1,446 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AfterDeploymentValidation; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeShutdown; +import jakarta.enterprise.inject.spi.Extension; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class provides access to the {@link BeanManager} by registering the + * current {@link BeanManager} in an extension and making it available via a + * singleton factory for the current application. + * + *

+ * This is really handy when you need to access CDI functionality from places + * where no injection is available. + *

+ * + *

+ * If a simple but manual bean lookup is needed, it's easier to use the + * {@link BeanProvider}. + *

+ * + *

+ * As soon as an application shuts down, the reference to the + * {@link BeanManager} is removed. + *

+ * + *

+ * Usage: + * + *

+ * BeanManager bm = BeanManagerProvider.getInstance().getBeanManager();
+ * 
+ *

+ * Attention: This approach is intended for use in user code at runtime. + * If BeanManagerProvider is used during Container boot (in an Extension), + * non-portable behaviour results. During bootstrapping, an Extension shall + * @Inject BeanManager to get access to the underlying BeanManager (see + * e.g. {@link #cleanupFinalBeanManagers}). This is the only way to guarantee + * that the right BeanManager is obtained in more complex Container scenarios. + *

+ */ +public class BeanManagerProvider implements Extension { + private static final Logger LOG = LoggerFactory + .getLogger(BeanManagerProvider.class); + + // for CDI 1.1+ delegation + private static final Method CDI_CURRENT_METHOD; + private static final Method CDI_CURRENT_BEAN_MANAGER_METHOD; + + private static BeanManagerProvider bmpSingleton; + + static { + Class cdiClass = ClassUtils + .tryToLoadClassForName("jakarta.enterprise.inject.spi.CDI"); + + Method resolvedCdiCurrentMethod = null; + Method resolvedCdiBeanManagerMethod = null; + // only init methods if a cdi 1.1+ container is available and the + // delegation-mode isn't deactivated. + // deactivation is e.g. useful if owb is used in "parallel mode" in a + // weld-based server. + // if (cdiClass != null && + // CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP) + // { + // try + // { + // resolvedCdiCurrentMethod = cdiClass.getDeclaredMethod("current"); + // resolvedCdiBeanManagerMethod = + // cdiClass.getDeclaredMethod("getBeanManager"); + // } + // catch (Exception e) + // { + // LOG.log(Level.SEVERE, "Couldn't get method from " + + // cdiClass.getName(), e); + // } + // } + + // null if no init happened e.g. due to CDI 1.0 or deactivated + // delegation-mode + CDI_CURRENT_METHOD = resolvedCdiCurrentMethod; + CDI_CURRENT_BEAN_MANAGER_METHOD = resolvedCdiBeanManagerMethod; + } + + /** + * This data container is used for storing the BeanManager for each web + * application. This is needed in EAR or other multi-webapp scenarios when + * the DeltaSpike classes (jars) are provided in a shared ClassLoader. + */ + private static class BeanManagerInfo { + /** + * The BeanManager picked up via Extension loading. + */ + private BeanManager loadTimeBm; + + /** + * The final BeanManager. After the container did finally boot, we first + * try to resolve them from JNDI, and only if we don't find any BM there + * we take the ones picked up at startup. + */ + private BeanManager finalBm; + + /** + * Whether the CDI Application has finally booted. Please note that this + * is only a nearby value as there is no reliable event for this status + * in EE6. + */ + private boolean booted; + } + + /** + * The BeanManagerInfo for the current ClassLoader. + * + *

+ * Attention: This instance must only be used through the + * {@link #bmpSingleton} singleton! + *

+ */ + private final Map bmInfos = new ConcurrentHashMap<>(); + + /** + * Indicates whether the {@link BeanManagerProvider} has been initialized. + * Usually it's not necessary to call this method in application code. It's + * useful e.g. for other frameworks to check if DeltaSpike and the CDI + * container in general have been started. + * + * @return true if the BeanManagerProvider is ready to be used + */ + public static boolean isActive() { + // CDI#current delegation enabled, skip everything + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null) { + return bmpSingleton != null; + } + + return bmpSingleton != null && bmpSingleton.bmInfos + .containsKey(ClassUtils.getClassLoader(null)); + } + + /** + * Returns the current provider instance which provides access to the + * current {@link BeanManager}. + * + * @throws IllegalStateException + * if the {@link BeanManagerProvider} isn't ready to be used. + * That's the case if the environment isn't configured properly + * and therefore the {@link AfterBeanDiscovery} hasn't been + * called before this method gets called. + * @return the singleton BeanManagerProvider + */ + public static BeanManagerProvider getInstance() { + /* + * X TODO Java-EE5 support needs to be discussed if (bmpSingleton == + * null) { // workaround for some Java-EE5 environments in combination + * with a special // StartupBroadcaster for bootstrapping CDI + * + * // CodiStartupBroadcaster.broadcastStartup(); // here bmp might not + * be null (depends on the broadcasters) } + */ + + if (bmpSingleton == null) { + throw new IllegalStateException("No " + + BeanManagerProvider.class.getName() + " in place! " + + "Please ensure that you configured the CDI implementation of your choice properly. " + + "If your setup is correct, please clear all caches and compiled artifacts."); + } + return bmpSingleton; + } + + /** + * It doesn't really matter which of the system events is used to obtain the + * BeanManager, but {@link AfterBeanDiscovery} has been chosen since it + * allows all events which occur after the {@link AfterBeanDiscovery} to use + * the {@link BeanManagerProvider}. + * + * @param afterBeanDiscovery + * event which we don't actually use ;) + * @param beanManager + * the BeanManager we store and make available. + */ + public void setBeanManager(@Observes AfterBeanDiscovery afterBeanDiscovery, + BeanManager beanManager) { + setBeanManagerProvider(this); + + // CDI#current delegation enabled, skip everything + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null + && resolveBeanManagerViaStaticHelper() != null) { + return; + } + + BeanManagerInfo bmi = getBeanManagerInfo( + ClassUtils.getClassLoader(null)); + bmi.loadTimeBm = beanManager; + } + + /** + * The active {@link BeanManager} for the current application (current + * {@link ClassLoader}). This method will throw an + * {@link IllegalStateException} if the BeanManager cannot be found. + * + * @return the current BeanManager, never null + * + * @throws IllegalStateException + * if the BeanManager cannot be found + */ + public BeanManager getBeanManager() { + // CDI#current delegation enabled, skip everything + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null) { + BeanManager bm = resolveBeanManagerViaStaticHelper(); + if (bm != null) { + return bm; + } + } + + BeanManagerInfo bmi = getBeanManagerInfo( + ClassUtils.getClassLoader(null)); + + if (!bmi.booted) { + // This is a workaround for some containers with messed up EAR + // handling. + // Those containers might boot up with the shared ear ClassLoader + // and later run the WARs with their own child ClassLoaders. + if (bmi.loadTimeBm == null) { + BeanManagerInfo parentBmi = getParentBeanManagerInfo( + ClassUtils.getClassLoader(null)); + if (parentBmi != null) { + bmi.loadTimeBm = parentBmi.loadTimeBm; + } + } + } + + BeanManager result = bmi.finalBm; + + if (result == null) { + synchronized (bmi) { + result = bmi.finalBm; + if (result == null) { + // first we look for a BeanManager from JNDI + result = resolveBeanManagerViaJndi(); + + if (result == null) { + // if none found, we take the one we got from the + // Extension loading + result = bmi.loadTimeBm; + } + + if (result == null) { + throw new IllegalStateException( + "Unable to find BeanManager. " + + "Please ensure that you configured the CDI implementation of your choice properly."); + } + + // store the resolved BeanManager in the result cache until + // #cleanupFinalBeanManagers gets called + // -> afterwards the next call of #getBeanManager will + // trigger the final lookup + bmi.finalBm = result; + } + } + } + + return result; + } + + /** + * By cleaning the final BeanManager map after the deployment gets + * validated, premature loading of information from JNDI is prevented in + * cases where the container might not be fully setup yet. + * + * This might happen if the BeanManagerProvider is used in an extension + * during CDI bootstrap. This should be generally avoided. Instead, an + * injected BeanManager should be used in Extensions and propagated using + * setters. + * + * In EARs with multiple webapps, each WAR might get a different Extension. + * This depends on the container used. + */ + public void cleanupFinalBeanManagers( + @Observes AfterDeploymentValidation adv) { + // CDI#current delegation enabled, skip everything + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null + && resolveBeanManagerViaStaticHelper() != null) { + return; + } + + for (BeanManagerInfo bmi : bmpSingleton.bmInfos.values()) { + bmi.finalBm = null; + bmi.booted = true; + + /* + * possible issue with >weld< based servers: if #getBeanManager gets + * called in a custom AfterDeploymentValidation observer >after< + * this observer, the wrong bean-manager might get stored (not + * deterministic due to the unspecified order of observers). finally + * a bean-manager for a single bda will be stored and returned + * (which isn't the bm exposed via jndi). + */ + } + } + + /** + * Cleanup on container shutdown. + * + * @param beforeShutdown + * CDI shutdown event + */ + public void cleanupStoredBeanManagerOnShutdown( + @Observes BeforeShutdown beforeShutdown) { + // CDI#current delegation enabled, skip everything + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null) { + return; + } + + if (bmpSingleton == null) { + // this happens if there has been a failure at startup + return; + } + + bmpSingleton.bmInfos.remove(ClassUtils.getClassLoader(null)); + } + + /** + * Get the BeanManager from the JNDI registry. + * + * @return current {@link BeanManager} which is provided via JNDI + */ + private BeanManager resolveBeanManagerViaJndi() { + try { + // this location is specified in JSR-299 and must be + // supported in all certified EE environments + return (BeanManager) new InitialContext() + .lookup("java:comp/BeanManager"); + } catch (NamingException e) { + // workaround didn't work -> return null + return null; + } + } + + private BeanManager resolveBeanManagerViaStaticHelper() { + if (CDI_CURRENT_METHOD != null + && CDI_CURRENT_BEAN_MANAGER_METHOD != null) { + try { + Object cdiCurrentObject = CDI_CURRENT_METHOD.invoke(null); + return (BeanManager) CDI_CURRENT_BEAN_MANAGER_METHOD + .invoke(cdiCurrentObject); + } catch (Throwable t) { + LOG.trace( + "failed to delegate bean-manager lookup -> fallback to default.", + t); + } + } + return null; + } + + /** + * Get or create the BeanManagerInfo for the given ClassLoader. + */ + private BeanManagerInfo getBeanManagerInfo(ClassLoader cl) { + BeanManagerInfo bmi = bmpSingleton.bmInfos.get(cl); + + if (bmi == null) { + synchronized (this) { + bmi = bmpSingleton.bmInfos.get(cl); + if (bmi == null) { + bmi = new BeanManagerInfo(); + bmpSingleton.bmInfos.put(cl, bmi); + if (cl.getParent() != null && !bmpSingleton.bmInfos + .containsKey(cl.getParent())) { + bmpSingleton.bmInfos.put(cl.getParent(), bmi); + } + } + } + } + + return bmi; + } + + /** + * This function exists to prevent findbugs from complaining about setting a + * static member from a non-static function. + * + * @param beanManagerProvider + * the bean-manager-provider which should be used if there isn't + * an existing provider + * + * @return the first BeanManagerProvider + */ + private static BeanManagerProvider setBeanManagerProvider( + BeanManagerProvider beanManagerProvider) { + if (bmpSingleton == null) { + bmpSingleton = beanManagerProvider; + } + + return bmpSingleton; + } + + /** + * Returns the {@link BeanManagerInfo} of the given ClassLoader's immediate + * parent, creating and storing an empty one if that ClassLoader has none + * yet. + * + * @return the parent ClassLoader's BeanManagerInfo, or null + * only when the given ClassLoader has no parent - that is, when it + * is the bootstrap ClassLoader. + */ + private BeanManagerInfo getParentBeanManagerInfo(ClassLoader classLoader) { + ClassLoader parentClassLoader = classLoader.getParent(); + if (parentClassLoader == null) { + return null; + } + + return getBeanManagerInfo(parentClassLoader); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanProvider.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanProvider.java new file mode 100644 index 00000000000..718864e3d95 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/BeanProvider.java @@ -0,0 +1,596 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Typed; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionTarget; +import jakarta.enterprise.inject.spi.InjectionTargetFactory; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class contains utility methods for resolution of contextual references + * in situations where no injection is available because the current class is + * not managed by the CDI Container. This can happen in e.g. a JPA 2.0 + * EntityListener, a ServletFilter, a Spring managed Bean, etc. + * + *

+ * Attention: This approach is intended for use in user code at runtime. + * If BeanProvider is used during Container boot (in an Extension), non-portable + * behaviour results. The CDI specification only allows injection of the + * BeanManager during CDI container boot time. + *

+ * + * @see DependentProvider + * + * Changes made by Vaadin: removed logging warning if bean is Dependent + * scope + */ +@Typed() +public final class BeanProvider { + + private BeanProvider() { + // this is a utility class which doesn't get instantiated. + } + + /** + * Get a Contextual Reference by its type and qualifiers. You can use this + * method to get contextual references of a given type. A "Contextual + * Reference" is a proxy which will automatically resolve the correct + * contextual instance when you access any method. + * + *

+ * Attention: You shall not use this method to manually resolve a + * @Dependent bean! The reason is that contextual instances usually + * live in the well-defined lifecycle of their injection point (the bean + * they got injected into). But if we manually resolve a @Dependent + * bean, then it does not belong to such well defined lifecycle + * (because @Dependent is not @NormalScoped) and thus will not be + * automatically destroyed at the end of the lifecycle. You need to manually + * destroy this contextual instance via + * {@link jakarta.enterprise.context.spi.Contextual#destroy(Object, jakarta.enterprise.context.spi.CreationalContext)}. + * Thus you also need to manually store the CreationalContext and the Bean + * you used to create the contextual instance. + *

+ * + * @param type + * the type of the bean in question + * @param qualifiers + * additional qualifiers which further distinct the resolved bean + * @param + * target type + * + * @return the resolved Contextual Reference + * + * @throws IllegalStateException + * if the bean could not be found. + * @see #getContextualReference(Class, boolean, Annotation...) + */ + public static T getContextualReference(Class type, + Annotation... qualifiers) { + return getContextualReference(type, false, qualifiers); + } + + /** + * {@link #getContextualReference(Class, Annotation...)} which returns + * null if the 'optional' parameter is set to + * true. + * + * @param type + * the type of the bean in question + * @param optional + * if true it will return null if no + * bean could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param qualifiers + * additional qualifiers which distinguish the resolved bean + * @param + * target type + * + * @return the resolved Contextual Reference + * + * @see #getContextualReference(Class, Annotation...) + */ + public static T getContextualReference(Class type, boolean optional, + Annotation... qualifiers) { + BeanManager beanManager = getBeanManager(); + + return getContextualReference(beanManager, type, optional, qualifiers); + } + + /** + * {@link #getContextualReference(Class, Annotation...)} which returns + * null if the 'optional' parameter is set to + * true. This method is intended for usage where the BeanManger + * is known, e.g. in Extensions. + * + * @param beanManager + * the BeanManager to use + * @param type + * the type of the bean in question + * @param optional + * if true it will return null if no + * bean could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param qualifiers + * additional qualifiers which further distinct the resolved bean + * @param + * target type + * + * @return the resolved Contextual Reference + * + * @see #getContextualReference(Class, Annotation...) + */ + public static T getContextualReference(BeanManager beanManager, + Class type, boolean optional, Annotation... qualifiers) { + Set> beans = beanManager.getBeans(type, qualifiers); + + if (beans == null || beans.isEmpty()) { + if (optional) { + return null; + } + + throw new IllegalStateException("Could not find beans for Type=" + + type + " and qualifiers:" + Arrays.toString(qualifiers)); + } + + return getContextualReference(type, beanManager, beans); + } + + /** + * Get a Contextual Reference by its EL Name. This only works for beans with + * the @Named annotation. + * + *

+ * Attention: please see the notes on manually resolving + * @Dependent beans in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ * + * @param name + * the EL name of the bean + * + * @return the resolved Contextual Reference + * + * @throws IllegalStateException + * if the bean could not be found. + * @see #getContextualReference(String, boolean) + */ + public static Object getContextualReference(String name) { + return getContextualReference(name, false); + } + + /** + * Get a Contextual Reference by its EL Name. This only works for beans with + * the @Named annotation. + * + *

+ * Attention: please see the notes on manually resolving + * @Dependent beans in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ * + * @param name + * the EL name of the bean + * @param optional + * if true it will return null if no + * bean could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * + * @return the resolved Contextual Reference + */ + public static Object getContextualReference(String name, boolean optional) { + return getContextualReference(name, optional, Object.class); + } + + /** + * Get a Contextual Reference by its EL Name. This only works for beans with + * the @Named annotation. + * + *

+ * Attention: please see the notes on manually resolving + * @Dependent beans in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ * + * @param name + * the EL name of the bean + * @param optional + * if true it will return null if no + * bean could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param type + * the type of the bean in question - use + * {@link #getContextualReference(String, boolean)} if the type + * is unknown e.g. in dyn. use-cases + * @param + * target type + * + * @return the resolved Contextual Reference + */ + public static T getContextualReference(String name, boolean optional, + Class type) { + return getContextualReference(getBeanManager(), name, optional, type); + } + + /** + *

+ * Get a Contextual Reference by its EL Name. This only works for beans with + * the @Named annotation. + *

+ * + *

+ * Attention: please see the notes on manually resolving + * @Dependent bean in + * {@link #getContextualReference(Class, boolean, java.lang.annotation.Annotation...)}! + *

+ * + * + * @param beanManager + * the BeanManager to use + * @param name + * the EL name of the bean + * @param optional + * if true it will return null if no + * bean could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param type + * the type of the bean in question - use + * {@link #getContextualReference(String, boolean)} if the type + * is unknown e.g. in dyn. use-cases + * @param + * target type + * @return the resolved Contextual Reference + */ + public static T getContextualReference(BeanManager beanManager, + String name, boolean optional, Class type) { + Set> beans = beanManager.getBeans(name); + + if (beans == null || beans.isEmpty()) { + if (optional) { + return null; + } + + throw new IllegalStateException("Could not find beans for Type=" + + type + " and name:" + name); + } + + return getContextualReference(type, beanManager, beans); + } + + /** + * Get the Contextual Reference for the given bean. + * + *

+ * Attention: please see the notes on manually resolving + * @Dependent beans in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ * + * @param type + * the type of the bean in question + * @param bean + * bean definition for the contextual reference + * @param + * target type + * + * @return the resolved Contextual Reference + */ + public static T getContextualReference(Class type, Bean bean) { + return getContextualReference(type, getBeanManager(), bean); + } + + private static T getContextualReference(Class type, + BeanManager beanManager, Bean bean) { + // noinspection unchecked + return getContextualReference(type, beanManager, + Collections.> singleton(bean)); + } + + /** + * Get a list of Contextual References by type, regardless of qualifiers + * (including dependent scoped beans). + * + * You can use this method to get all contextual references of a given type. + * A 'Contextual Reference' is a proxy which will automatically resolve the + * correct contextual instance when you access any method. + * + *

+ * Attention: please see the notes on manually resolving + * @Dependent beans in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ *

+ * Attention: This will also return instances of beans for which an + * Alternative exists! The @Alternative resolving is only done via + * {@link BeanManager#resolve(java.util.Set)} which we cannot use in this + * case! + *

+ * + * @param type + * the type of the bean in question + * @param optional + * if true it will return an empty list if no bean + * could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param + * target type + * + * @return the resolved list of Contextual Reference or an empty-list if + * optional is true + */ + public static List getContextualReferences(Class type, + boolean optional) { + return getContextualReferences(type, optional, true); + } + + /** + * Get a list of Contextual References by type, regardless of the qualifier. + * + * Further details are available at + * {@link #getContextualReferences(Class, boolean)}. + *

+ * Attention: please see the notes on manually resolving + * @Dependent bean in + * {@link #getContextualReference(Class, java.lang.annotation.Annotation...)}! + *

+ *

+ * Attention: This will also return instances of beans for which an + * Alternative exists! The @Alternative resolving is only done via + * {@link BeanManager#resolve(java.util.Set)} which we cannot use in this + * case! + *

+ * + * @param type + * the type of the bean in question + * @param optional + * if true it will return an empty list if no bean + * could be found or created. Otherwise it will throw an + * {@code IllegalStateException} + * @param includeDefaultScopedBeans + * specifies if dependent scoped beans should be included in the + * result + * @param + * target type + * + * @return the resolved list of Contextual Reference or an empty-list if + * optional is true + */ + public static List getContextualReferences(Class type, + boolean optional, boolean includeDefaultScopedBeans) { + BeanManager beanManager = getBeanManager(); + + Set> beans = getBeanDefinitions(type, optional, + includeDefaultScopedBeans, beanManager); + + List result = new ArrayList(beans.size()); + + for (Bean bean : beans) { + // noinspection unchecked + result.add(getContextualReference(type, beanManager, bean)); + } + return result; + } + + public static DependentProvider getDependent(Class type, + Annotation... qualifiers) { + BeanManager beanManager = getBeanManager(); + return getDependent(beanManager, type, qualifiers); + } + + public static DependentProvider getDependent(BeanManager beanManager, + Class type, Annotation... qualifiers) { + Set> beans = beanManager.getBeans(type, qualifiers); + @SuppressWarnings("unchecked") + Bean bean = (Bean) beanManager.resolve(beans); + return createDependentProvider(beanManager, type, bean); + } + + public static DependentProvider getDependent(String name) { + BeanManager beanManager = getBeanManager(); + return getDependent(beanManager, name); + } + + public static DependentProvider getDependent(BeanManager beanManager, + String name) { + Set> beans = beanManager.getBeans(name); + @SuppressWarnings("unchecked") + Bean bean = (Bean) beanManager.resolve(beans); + @SuppressWarnings("unchecked") + Class beanClass = (Class) bean.getBeanClass(); + + return createDependentProvider(beanManager, beanClass, bean); + } + + private static DependentProvider createDependentProvider( + BeanManager beanManager, Class type, Bean bean) { + CreationalContext cc = beanManager.createCreationalContext(bean); + @SuppressWarnings("unchecked") + T instance = (T) beanManager.getReference(bean, type, cc); + + return new DependentProvider(bean, cc, instance); + } + + /** + * Get a set of {@link Bean} definitions by type, regardless of qualifiers. + * + * @param type + * the type of the bean in question + * @param optional + * if true it will return an empty set if no bean + * could be found. Otherwise it will throw an + * {@code IllegalStateException} + * @param includeDefaultScopedBeans + * specifies whether dependent scoped beans should be included in + * the result + * @param + * target type + * + * @return the resolved set of {@link Bean} definitions or an empty set if + * optional is true + */ + public static Set> getBeanDefinitions(Class type, + boolean optional, boolean includeDefaultScopedBeans) { + BeanManager beanManager = getBeanManager(); + + return getBeanDefinitions(type, optional, includeDefaultScopedBeans, + beanManager); + } + + /** + * Get a set of {@link Bean} definitions by type, regardless of qualifiers. + * + * @param type + * the type of the bean in question + * @param optional + * if true it will return an empty set if no bean + * could be found. Otherwise it will throw an + * {@code IllegalStateException} + * @param includeDefaultScopedBeans + * specifies whether dependent scoped beans should be included in + * the result + * @param + * target type + * @param beanManager + * the {@link BeanManager} to use + * + * @return the resolved set of {@link Bean} definitions or an empty set if + * optional is true + */ + public static Set> getBeanDefinitions(Class type, + boolean optional, boolean includeDefaultScopedBeans, + BeanManager beanManager) { + Set> beans = beanManager.getBeans(type, new AnyLiteral()); + + if (beans == null || beans.isEmpty()) { + if (optional) { + return Collections.emptySet(); + } + + throw new IllegalStateException( + "Could not find beans for Type=" + type); + } + + if (!includeDefaultScopedBeans) { + beans = filterDefaultScopedBeans(beans); + } + + Set> result = new HashSet>(); + + for (Bean bean : beans) { + // noinspection unchecked + @SuppressWarnings("unchecked") + Bean beanT = (Bean) bean; + result.add(beanT); + } + + return result; + } + + /** + * Performs dependency injection on an instance. Useful for instances which + * aren't managed by CDI. Attention:
+ * The resulting instance isn't managed by CDI; only fields annotated + * with @Inject get initialized. + * + * @param instance + * current instance + * @param + * current type + * + * @return instance with injected fields (if possible - or null if the given + * instance is null) + */ + @SuppressWarnings("unchecked") + public static T injectFields(T instance) { + if (instance == null) { + return null; + } + + BeanManager beanManager = getBeanManager(); + + CreationalContext creationalContext = beanManager + .createCreationalContext(null); + + AnnotatedType annotatedType = beanManager + .createAnnotatedType((Class) instance.getClass()); + + InjectionTargetFactory injectionTargetFactory = beanManager + .getInjectionTargetFactory(annotatedType); + InjectionTarget injectionTarget = injectionTargetFactory + .createInjectionTarget(null); + injectionTarget.inject(instance, creationalContext); + return instance; + } + + private static Set> filterDefaultScopedBeans(Set> beans) { + Set> result = new HashSet>(beans.size()); + + for (Bean currentBean : beans) { + if (!Dependent.class.isAssignableFrom(currentBean.getScope())) { + result.add(currentBean); + } + } + return result; + } + + /** + * Internal helper method to resolve the right bean and resolve the + * contextual reference. + * + * @param type + * the type of the bean in question + * @param beanManager + * current bean-manager + * @param beans + * beans in question + * @param + * target type + * @return the contextual reference + */ + private static T getContextualReference(Class type, + BeanManager beanManager, Set> beans) { + Bean bean = beanManager.resolve(beans); + + CreationalContext creationalContext = beanManager + .createCreationalContext(bean); + + @SuppressWarnings({ "unchecked", "UnnecessaryLocalVariable" }) + T result = (T) beanManager.getReference(bean, type, creationalContext); + return result; + } + + /** + * Internal method to resolve the BeanManager via the + * {@link org.apache.deltaspike.core.api.provider.BeanManagerProvider}. + * + * @return current BeanManager + */ + private static BeanManager getBeanManager() { + return BeanManagerProvider.getInstance().getBeanManager(); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ClassUtils.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ClassUtils.java new file mode 100644 index 00000000000..0531bb268dc --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ClassUtils.java @@ -0,0 +1,518 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.inject.Typed; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +@Typed() +public abstract class ClassUtils { + /** + * Constructor which prevents the instantiation of this class + */ + private ClassUtils() { + // prevent instantiation + } + + /** + * Detect the right ClassLoader. The lookup order is determined by: + *
    + *
  1. ContextClassLoader of the current Thread
  2. + *
  3. ClassLoader of the given Object 'o'
  4. + *
  5. ClassLoader of this very ClassUtils class
  6. + *
+ * + * @param o + * if not null it may get used to detect the + * classloader. + * @return The {@link ClassLoader} which should get used to create new + * instances + */ + public static ClassLoader getClassLoader(Object o) { + if (System.getSecurityManager() != null) { + return AccessController + .doPrivileged(new ClassUtils.GetClassLoaderAction(o)); + } else { + return getClassLoaderInternal(o); + } + } + + static class GetClassLoaderAction implements PrivilegedAction { + private Object object; + + GetClassLoaderAction(Object object) { + this.object = object; + } + + @Override + public ClassLoader run() { + try { + return getClassLoaderInternal(object); + } catch (Exception e) { + return null; + } + } + } + + private static ClassLoader getClassLoaderInternal(Object o) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + if (loader == null && o != null) { + loader = o.getClass().getClassLoader(); + } + + if (loader == null) { + loader = ClassUtils.class.getClassLoader(); + } + + return loader; + } + + /** + * Checks whether the CDI rules for proxyable beans are met. See + * CDI spec unproxyable bean types + * + * @param type + * @return {@code true} if all proxy conditions are met, {@code false} + * otherwise + */ + public static boolean isProxyableClass(Type type) { + Class clazz = null; + if (type instanceof Class) { + clazz = (Class) type; + } + if (type instanceof ParameterizedType + && ((ParameterizedType) type).getRawType() instanceof Class) { + clazz = (Class) ((ParameterizedType) type).getRawType(); + } + if (clazz == null) { + return false; + } + + // classes which don’t have a non-private constructor with no parameters + try { + Constructor constructor = clazz.getConstructor(); + if (Modifier.isPrivate(constructor.getModifiers())) { + return false; + } + } catch (NoSuchMethodException e) { + return false; + } + + // classes which are declared final + if (Modifier.isFinal(clazz.getModifiers())) { + return false; + } + + // classes which have non-static, final methods with public, protected + // or default visibility, + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == Object.class) { + continue; + } + + if (!method.isBridge() && !method.isSynthetic() + && !Modifier.isStatic(method.getModifiers()) + && !Modifier.isPrivate(method.getModifiers()) + && Modifier.isFinal(method.getModifiers())) { + return false; + } + } + + // primitive types, + // and array types. + if (clazz.isPrimitive() || clazz.isArray()) { + return false; + + } + return true; + } + + /** + * Tries to load a class based on the given name and interface or abstract + * class. + * + * @param name + * name of the concrete class + * @param targetType + * target type (interface or abstract class) + * @param + * current type + * @return loaded class or null if it isn't in the classpath + */ + public static Class tryToLoadClassForName(String name, + Class targetType) { + return (Class) tryToLoadClassForName(name); + } + + /** + * Tries to load a class based on the given name and interface or abstract + * class. + * + * @param name + * name of the concrete class + * @param targetType + * target type (interface or abstract class) + * @param classLoader + * The {@link ClassLoader}. + * @param + * current type + * @return loaded class or null if it isn't in the classpath + */ + public static Class tryToLoadClassForName(String name, + Class targetType, ClassLoader classLoader) { + return (Class) tryToLoadClassForName(name, classLoader); + } + + /** + * Tries to load a class based on the given name + * + * @param name + * name of the class + * @return loaded class or null if it isn't in the classpath + */ + public static Class tryToLoadClassForName(String name) { + try { + return loadClassForName(name); + } catch (ClassNotFoundException e) { + // do nothing - it's just a try + return null; + } + } + + /** + * Tries to load a class based on the given name + * + * @param name + * name of the class + * @param classLoader + * The {@link ClassLoader}. + * @return loaded class or null if it isn't in the classpath + */ + public static Class tryToLoadClassForName(String name, + ClassLoader classLoader) { + try { + return classLoader.loadClass(name); + } catch (ClassNotFoundException e) { + // do nothing - it's just a try + return null; + } + } + + /** + * Loads class for the given name + * + * @param name + * name of the class + * @return loaded class + * @throws ClassNotFoundException + * if the class can't be loaded + */ + public static Class loadClassForName(String name) + throws ClassNotFoundException { + try { + // Try WebApp ClassLoader first + return Class.forName(name, false, // do not initialize for faster + // startup + getClassLoader(null)); + } catch (ClassNotFoundException ignore) { + // fallback: Try ClassLoader for ClassUtils (i.e. the myfaces.jar + // lib) + return Class.forName(name, false, // do not initialize for faster + // startup + ClassUtils.class.getClassLoader()); + } + } + + /** + * Instantiates a given class via the default constructor + * + * @param targetClass + * class which should be instantiated + * @param + * current type + * @return created instance or null if the instantiation failed + */ + public static T tryToInstantiateClass(Class targetClass) { + try { + return targetClass.newInstance(); + } catch (InstantiationException e) { + // do nothing - it was just a try + } catch (IllegalAccessException e) { + // do nothing - it was just a try + } + return null; + } + + /** + * Tries to instantiate a class for the given name and type via the default + * constructor + * + * @param className + * name of the class + * @param targetType + * target type + * @param + * current type + * @return created instance or null if the instantiation failed + */ + public static T tryToInstantiateClassForName(String className, + Class targetType) { + Object result = tryToInstantiateClassForName(className); + + // noinspection unchecked + return result != null ? (T) result : null; + } + + /** + * Tries to instantiate a class for the given name via the default + * constructor + * + * @param className + * name of the class + * @return created instance or null if the instantiation failed + */ + public static Object tryToInstantiateClassForName(String className) { + try { + return instantiateClassForName(className); + } catch (Exception e) { + // do nothing - it was just a try + } + return null; + } + + /** + * Creates an instance for the given class-name + * + * @param className + * name of the class which should be instantiated + * @return created instance + * @throws ClassNotFoundException + * if the instantiation failed + * @throws IllegalAccessException + * if the instantiation failed + * @throws InstantiationException + * if the instantiation failed + */ + public static Object instantiateClassForName(String className) + throws ClassNotFoundException, IllegalAccessException, + InstantiationException { + return loadClassForName(className).newInstance(); + } + + /** + * Reads the version of the jar which contains the given class + * + * @param targetClass + * class within the jar + * @return version-string which has been found in the manifest or null if + * there is no version information available + */ + public static String getJarVersion(Class targetClass) { + String manifestFileLocation = getManifestFileLocationOfClass( + targetClass); + + try { + return new Manifest(new URL(manifestFileLocation).openStream()) + .getMainAttributes() + .getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } catch (Exception e) { + return null; + } + } + + /** + * Reads the VCS revision which was used for creating the jar + * + * @param targetClass + * class within the jar + * @return revision-string which has been found in the manifest or null if + * there is no information available + */ + public static String getRevision(Class targetClass) { + String manifestFileLocation = getManifestFileLocationOfClass( + targetClass); + + try { + return new Manifest(new URL(manifestFileLocation).openStream()) + .getMainAttributes().getValue("Revision"); + } catch (Exception e) { + return null; + } + } + + private static String getManifestFileLocationOfClass(Class targetClass) { + String manifestFileLocation; + + try { + manifestFileLocation = getManifestLocation(targetClass); + } catch (Exception e) { + // in this case we have a proxy + manifestFileLocation = getManifestLocation( + targetClass.getSuperclass()); + } + return manifestFileLocation; + } + + private static String getManifestLocation(Class targetClass) { + String classFilePath = targetClass.getCanonicalName().replace('.', '/') + + ".class"; + String manifestFilePath = "/META-INF/MANIFEST.MF"; + + String classLocation = targetClass + .getResource(targetClass.getSimpleName() + ".class").toString(); + return classLocation.substring(0, + classLocation.indexOf(classFilePath) - 1) + manifestFilePath; + } + + /** + * Checks if the given class contains a method with the same signature. + * + * @param targetClass + * The class to check + * @param method + * The source method + * @return if it contains a method with the same signature. + */ + public static boolean containsMethod(Class targetClass, Method method) { + return extractMethod(targetClass, method) != null; + } + + /** + * Extracts a method with same signature as the source method. + * + * @param clazz + * The target class + * @param sourceMethod + * The source method. + * @return the extracted method or null + */ + public static Method extractMethod(Class clazz, Method sourceMethod) { + try { + String name = sourceMethod.getName(); + return clazz != null + ? clazz.getMethod(name, sourceMethod.getParameterTypes()) + : null; + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Extract a method with the given name and parameterTypes. Return + * {@code null} if no such visible method exists on the given class. + * + * @param clazz + * @param methodName + * @param parameterTypes + * @return + */ + public static Method extractMethod(Class clazz, String methodName, + Class... parameterTypes) { + try { + return clazz != null ? clazz.getMethod(methodName, parameterTypes) + : null; + } catch (NoSuchMethodException e) { + return null; + } + } + + /** + * Checks if the given class has a method with the same signature, taking in + * to account generic types + * + * @param targetClass + * @param method + * @return if it contains a method with the same signature. + */ + public static boolean containsPossiblyGenericMethod(Class targetClass, + Method method) { + return extractPossiblyGenericMethod(targetClass, method) != null; + } + + /** + * Extracts a method matching the source method, allowing generic type + * parameters to be substituted as long as they are properly castable. + * + * @param clazz + * The target class + * @param sourceMethod + * The source method. + * @return the extracted method or null + */ + public static Method extractPossiblyGenericMethod(Class clazz, + Method sourceMethod) { + // extractMethod tolerates a null class and answers null for it, so + // the generic fallback below has to as well rather than dereferencing + // it. + if (clazz == null) { + return null; + } + + Method exactMethod = extractMethod(clazz, sourceMethod); + if (exactMethod == null) { + String methodName = sourceMethod.getName(); + Class[] parameterTypes = sourceMethod.getParameterTypes(); + for (Method method : clazz.getMethods()) { + if (method.getName().equals(methodName) && allSameType( + method.getParameterTypes(), parameterTypes)) { + return method; + } + } + return null; + } else { + return exactMethod; + } + } + + /** + * Whether all of the parameters from left to right are equivalent. In order + * to support generics, it takes the form of left.isAssignableFrom(right) + * + * @param left + * left hand side to check + * @param right + * right hand side to check + * @return whether all of the left classes can be assigned to the right hand + * side types + */ + private static boolean allSameType(Class[] left, Class[] right) { + if (left.length != right.length) { + return false; + } + for (int p = 0; p < left.length; p++) { + if (!left[p].isAssignableFrom(right[p])) { + return false; + } + } + return true; + } + + public static boolean returns(Method method, Class clazz) { + return method.getReturnType().isAssignableFrom(clazz); + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextUtils.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextUtils.java new file mode 100644 index 00000000000..0c170c42179 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.inject.Typed; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.lang.annotation.Annotation; + +/** + * A set of utility methods for working with contexts. + */ +@Typed() +public abstract class ContextUtils { + private ContextUtils() { + // prevent instantiation + } + + /** + * Checks if the context for the given scope annotation is active. + * + * @param scopeAnnotationClass + * The scope annotation (e.g. @RequestScoped.class) + * @return If the context is active. + */ + public static boolean isContextActive( + Class scopeAnnotationClass) { + return isContextActive(scopeAnnotationClass, + BeanManagerProvider.getInstance().getBeanManager()); + } + + /** + * Checks if the context for the given scope annotation is active. + * + * @param scopeAnnotationClass + * The scope annotation (e.g. @RequestScoped.class) + * @param beanManager + * The {@link BeanManager} + * @return If the context is active. + */ + public static boolean isContextActive( + Class scopeAnnotationClass, + BeanManager beanManager) { + try { + if (beanManager.getContext(scopeAnnotationClass) == null + || !beanManager.getContext(scopeAnnotationClass) + .isActive()) { + return false; + } + } catch (ContextNotActiveException e) { + return false; + } + + return true; + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualInstanceInfo.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualInstanceInfo.java new file mode 100644 index 00000000000..40ac213e8f5 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualInstanceInfo.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.spi.CreationalContext; + +import java.io.Serializable; + +/** + * This data holder contains all necessary data you need to store a Contextual + * Instance in a CDI Context. + */ +public class ContextualInstanceInfo implements Serializable { + private static final long serialVersionUID = 6384932199958645324L; + + /** + * The actual Contextual Instance in the context + */ + private T contextualInstance; + + /** + * We need to store the CreationalContext as we need it for properly + * destroying the contextual instance via + * {@link jakarta.enterprise.context.spi.Contextual#destroy(Object, jakarta.enterprise.context.spi.CreationalContext)} + */ + private CreationalContext creationalContext; + + /** + * @return the CreationalContext of the bean + */ + public CreationalContext getCreationalContext() { + return creationalContext; + } + + /** + * @param creationalContext + * the CreationalContext of the bean + */ + public void setCreationalContext(CreationalContext creationalContext) { + this.creationalContext = creationalContext; + } + + /** + * @return the contextual instance itself + */ + public T getContextualInstance() { + return contextualInstance; + } + + /** + * @param contextualInstance + * the contextual instance itself + */ + public void setContextualInstance(T contextualInstance) { + this.contextualInstance = contextualInstance; + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualStorage.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualStorage.java new file mode 100644 index 00000000000..289e569f3fe --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ContextualStorage.java @@ -0,0 +1,154 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.PassivationCapable; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * This Storage holds all information needed for storing Contextual Instances in + * a Context. + * + * It also addresses Serialisation in case of passivating scopes. + */ +public class ContextualStorage implements Serializable { + private static final long serialVersionUID = 1L; + + private final Map> contextualInstances; + + private final BeanManager beanManager; + + private final boolean concurrent; + + private final boolean passivationCapable; + + /** + * @param beanManager + * is needed for serialisation + * @param concurrent + * whether the ContextualStorage might get accessed concurrently + * by different threads + * @param passivationCapable + * whether the storage is for passivation capable Scopes + */ + public ContextualStorage(BeanManager beanManager, boolean concurrent, + boolean passivationCapable) { + this.beanManager = beanManager; + this.concurrent = concurrent; + this.passivationCapable = passivationCapable; + if (concurrent) { + contextualInstances = new ConcurrentHashMap>(); + } else { + contextualInstances = new HashMap>(); + } + } + + /** + * @return the underlying storage map. + */ + public Map> getStorage() { + return contextualInstances; + } + + /** + * @return whether the ContextualStorage might get accessed concurrently by + * different threads. + */ + public boolean isConcurrent() { + return concurrent; + } + + /** + * + * @param bean + * @param creationalContext + * @param + * @return + */ + public T createContextualInstance(Contextual bean, + CreationalContext creationalContext) { + Object beanKey = getBeanKey(bean); + if (isConcurrent()) { + // locked approach + ContextualInstanceInfo instanceInfo = new ContextualInstanceInfo(); + + ConcurrentMap> concurrentMap = (ConcurrentHashMap>) contextualInstances; + + ContextualInstanceInfo oldInstanceInfo = (ContextualInstanceInfo) concurrentMap + .putIfAbsent(beanKey, instanceInfo); + + if (oldInstanceInfo != null) { + instanceInfo = oldInstanceInfo; + } + synchronized (instanceInfo) { + T instance = instanceInfo.getContextualInstance(); + if (instance == null) { + instance = bean.create(creationalContext); + instanceInfo.setContextualInstance(instance); + instanceInfo.setCreationalContext(creationalContext); + } + + return instance; + } + + } else { + // simply create the contextual instance + ContextualInstanceInfo instanceInfo = new ContextualInstanceInfo(); + instanceInfo.setCreationalContext(creationalContext); + instanceInfo.setContextualInstance(bean.create(creationalContext)); + + contextualInstances.put(beanKey, instanceInfo); + + return instanceInfo.getContextualInstance(); + } + } + + /** + * If the context is a passivating scope then we return the passivationId of + * the Bean. Otherwise we use the Bean directly. + * + * @return the key to use in the context map + */ + public Object getBeanKey(Contextual bean) { + if (passivationCapable) { + // if the + return ((PassivationCapable) bean).getId(); + } + + return bean; + } + + /** + * Restores the Bean from its beanKey. + * + * @see #getBeanKey(jakarta.enterprise.context.spi.Contextual) + */ + public Contextual getBean(Object beanKey) { + if (passivationCapable) { + return beanManager.getPassivationCapableBean((String) beanKey); + } else { + return (Contextual) beanKey; + } + } +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/DependentProvider.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/DependentProvider.java new file mode 100644 index 00000000000..767fc6ba049 --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/DependentProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.PassivationCapable; +import jakarta.inject.Provider; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * A {@link Provider} for @Dependent scoped contextual instances. We need + * this to be able to properly clean them up when they are not needed anymore + * via the {@link #destroy()} method. + * + * Instances of this class can be retrieved using the {@link BeanProvider}. + * + * Instances of this class are Serializable if the wrapped contextual instance + * is Serializable. + * + * @see BeanProvider#getDependent(java.lang.Class, + * java.lang.annotation.Annotation...) + */ +public class DependentProvider implements Provider, Serializable { + private static final long serialVersionUID = 23423413412001L; + + private T instance; + private CreationalContext creationalContext; + private transient Bean bean; + + DependentProvider(Bean bean, CreationalContext creationalContext, + T instance) { + this.bean = bean; + this.creationalContext = creationalContext; + this.instance = instance; + } + + @Override + public T get() { + return instance; + } + + /** + * This method will properly destroy the @Dependent scoped instance. It + * will have no effect if the bean is NormalScoped as those have their own + * lifecycle which we must not disrupt. + */ + public void destroy() { + if (!BeanManagerProvider.getInstance().getBeanManager() + .isNormalScope(bean.getScope())) { + bean.destroy(instance, creationalContext); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + if (!(bean instanceof PassivationCapable)) { + throw new NotSerializableException( + "Bean is not PassivationCapable: " + bean.toString()); + } + String passivationId = ((PassivationCapable) bean).getId(); + if (passivationId == null) { + throw new NotSerializableException(bean.toString()); + } + + out.writeLong(serialVersionUID); + out.writeObject(passivationId); + out.writeObject(instance); + out.writeObject(creationalContext); + } + + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + long oldSerialId = in.readLong(); + if (oldSerialId != serialVersionUID) { + throw new NotSerializableException( + getClass().getName() + " serialVersion does not match"); + } + String passivationId = (String) in.readObject(); + bean = (Bean) BeanManagerProvider.getInstance().getBeanManager() + .getPassivationCapableBean(passivationId); + instance = (T) in.readObject(); + creationalContext = (CreationalContext) in.readObject(); + } + +} diff --git a/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ProxyUtils.java b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ProxyUtils.java new file mode 100644 index 00000000000..d1b6b735b1f --- /dev/null +++ b/vaadin-cdi/src/main/java/com/vaadin/cdi/util/ProxyUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.util; + +import jakarta.enterprise.inject.Typed; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper for proxies + */ +@Typed() +public abstract class ProxyUtils { + private ProxyUtils() { + // prevent instantiation + } + + /** + * @param currentClass + * current class + * @return class of the real implementation + */ + public static Class getUnproxiedClass(Class currentClass) { + Class unproxiedClass = currentClass; + + while (isProxiedClass(unproxiedClass)) { + unproxiedClass = unproxiedClass.getSuperclass(); + } + + return unproxiedClass; + } + + /** + * Analyses if the given class is a generated proxy class + * + * @param currentClass + * current class + * @return true if the given class is a known proxy class, false otherwise + */ + public static boolean isProxiedClass(Class currentClass) { + if (currentClass == null || currentClass.getSuperclass() == null) { + return false; + } + + String name = currentClass.getName(); + return name.startsWith(currentClass.getSuperclass().getName()) + && (name.contains("$$") // CDI + || name.contains("_ClientProxy") // Quarkus + || name.contains("$HibernateProxy$")); // Hibernate + } + + public static List> getProxyAndBaseTypes(Class proxyClass) { + List> result = new ArrayList>(); + result.add(proxyClass); + + if (isInterfaceProxy(proxyClass)) { + for (Class currentInterface : proxyClass.getInterfaces()) { + if (proxyClass.getName() + .startsWith(currentInterface.getName())) { + result.add(currentInterface); + } + } + } else { + Class unproxiedClass = proxyClass.getSuperclass(); + result.add(unproxiedClass); + + while (isProxiedClass(unproxiedClass)) { + unproxiedClass = unproxiedClass.getSuperclass(); + result.add(unproxiedClass); + } + } + return result; + } + + public static boolean isInterfaceProxy(Class proxyClass) { + Class[] interfaces = proxyClass.getInterfaces(); + if (Proxy.class.equals(proxyClass.getSuperclass()) && interfaces != null + && interfaces.length > 0) { + return true; + } + + if (proxyClass.getSuperclass() != null + && !proxyClass.getSuperclass().equals(Object.class)) { + return false; + } + + if (proxyClass.getName().contains("$$")) { + for (Class currentInterface : interfaces) { + if (proxyClass.getName() + .startsWith(currentInterface.getName())) { + return true; + } + } + } + + return false; + } +} diff --git a/vaadin-cdi/src/main/resources/META-INF/beans.xml b/vaadin-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..1f8521eadbe --- /dev/null +++ b/vaadin-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/vaadin-cdi/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension b/vaadin-cdi/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..503adf310ea --- /dev/null +++ b/vaadin-cdi/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension @@ -0,0 +1,2 @@ +com.vaadin.cdi.util.BeanManagerProvider +com.vaadin.cdi.VaadinExtension diff --git a/vaadin-cdi/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/vaadin-cdi/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 00000000000..f2226c3a739 --- /dev/null +++ b/vaadin-cdi/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +com.vaadin.cdi.CdiServletDeployer diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/AbstractWeldTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/AbstractWeldTest.java new file mode 100644 index 00000000000..776b0782525 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/AbstractWeldTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import org.jboss.weld.junit5.EnableWeld; +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldSetup; + +@EnableWeld +public class AbstractWeldTest { + + @WeldSetup + public WeldInitiator weld = WeldInitiator.performDefaultDiscovery(); + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorDefaultsTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorDefaultsTest.java new file mode 100644 index 00000000000..114c3e1a347 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorDefaultsTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import org.jboss.weld.junit5.EnableWeld; +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldSetup; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.context.ServiceUnderTestContext; +import com.vaadin.cdi.context.VaadinServiceScopedContext; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.router.PageTitleGenerator; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.auth.DefaultMenuAccessControl; +import com.vaadin.flow.server.auth.MenuAccessControl; + +@EnableWeld +public class CdiInstantiatorDefaultsTest { + + @WeldSetup + public WeldInitiator weld = WeldInitiator + .from(CdiInstantiatorFactory.class, CdiInstantiator.class, + VaadinServiceScopedContext.ContextualStorageManager.class) + .activate(VaadinServiceScoped.class).build(); + + @Inject + private BeanManager beanManager; + + @Inject + @VaadinServiceEnabled + private CdiInstantiatorFactory instantiatorFactory; + + private Instantiator instantiator; + + private ServiceUnderTestContext serviceUnderTestContext; + + @BeforeEach + public void setUp() { + serviceUnderTestContext = new ServiceUnderTestContext(beanManager); + serviceUnderTestContext.activate(); + instantiator = instantiatorFactory + .createInstantitor(VaadinService.getCurrent()); + } + + @AfterEach + public void tearDown() { + serviceUnderTestContext.tearDownAll(); + } + + @Test + public void getMenuAccessControl_beanNotProvided_instanceReturned() { + MenuAccessControl menuAccessControl = instantiator + .getMenuAccessControl(); + Assertions.assertNotNull(menuAccessControl); + Assertions.assertInstanceOf(DefaultMenuAccessControl.class, + menuAccessControl); + } + + @Test + public void getPageTitleGenerator_beanNotProvided_fallsBackToDelegate() { + // No bean and no "pageTitle.generator" init parameter is configured, + // so the delegate's default (null) is returned. + PageTitleGenerator pageTitleGenerator = instantiator + .getPageTitleGenerator(); + Assertions.assertNull(pageTitleGenerator); + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorTest.java new file mode 100644 index 00000000000..3bd3c808b87 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiInstantiatorTest.java @@ -0,0 +1,314 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Vetoed; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptors; +import jakarta.interceptor.InvocationContext; +import jakarta.servlet.ServletException; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.context.ServiceUnderTestContext; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.i18n.I18NProvider; +import com.vaadin.flow.internal.UsageStatistics; +import com.vaadin.flow.router.PageTitleContext; +import com.vaadin.flow.router.PageTitleGenerator; +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.auth.MenuAccessControl; + +public class CdiInstantiatorTest extends AbstractWeldTest { + + @Singleton + public static class SomeCdiBean { + } + + @ApplicationScoped + public static class ScopedCdiBean { + } + + public static class MyInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext ctx) throws Exception { + return ctx.proceed(); + } + } + + @Interceptors(MyInterceptor.class) + public static class InterceptedCdiBean { + + public void exec() { + + } + } + + public static class ParentBean { + + @Inject + private BeanManager bm; + + public BeanManager getBm() { + return bm; + } + + } + + @Singleton + public static class SingletonComponent extends Div { + @Inject + BeanManager beanManager; + } + + @Vetoed + public static class NotACdiBean extends ParentBean { + } + + public static class Ambiguous extends ParentBean { + } + + @VaadinServiceEnabled + public static class I18NTestProvider implements I18NProvider { + + @Override + public List getProvidedLocales() { + return null; + } + + @Override + public String getTranslation(String key, Locale locale, + Object... params) { + return null; + } + + } + + @VaadinServiceEnabled + public static class TestMenuAccessControl implements MenuAccessControl { + + @Override + public void setPopulateClientSideMenu( + PopulateClientMenu populateClientSideMenu) { + + } + + @Override + public PopulateClientMenu getPopulateClientSideMenu() { + return null; + } + } + + @VaadinServiceEnabled + public static class TestPageTitleGenerator implements PageTitleGenerator { + + @Override + public String generatePageTitle(PageTitleContext context) { + return null; + } + } + + @Singleton + public static class ServiceInitObserver { + + ServiceInitEvent event; + + public void serviceInit(@Observes ServiceInitEvent event) { + this.event = event; + } + + public ServiceInitEvent getEvent() { + return event; + } + + } + + @Inject + private BeanManager beanManager; + + @Inject + @VaadinServiceEnabled + private CdiInstantiatorFactory instantiatorFactory; + + private Instantiator instantiator; + + @Inject + private SomeCdiBean singleton; + + @Inject + private ServiceInitObserver serviceInitObserver; + + private ServiceUnderTestContext serviceUnderTestContext; + + @BeforeEach + public void setUp() { + serviceUnderTestContext = new ServiceUnderTestContext(beanManager); + serviceUnderTestContext.activate(); + instantiator = instantiatorFactory + .createInstantitor(VaadinService.getCurrent()); + } + + @AfterEach + public void tearDown() { + serviceUnderTestContext.tearDownAll(); + } + + @Test + public void getI18NProvider_beanEnabled_instanceReturned() { + I18NProvider i18NProvider = instantiator.getI18NProvider(); + Assertions.assertNotNull(i18NProvider); + Assertions.assertTrue((i18NProvider instanceof I18NTestProvider)); + } + + @Test + public void getMenuAccessControl_beanEnabled_instanceReturned() { + MenuAccessControl menuAccessControl = instantiator + .getMenuAccessControl(); + Assertions.assertNotNull(menuAccessControl); + Assertions.assertInstanceOf(TestMenuAccessControl.class, + menuAccessControl); + } + + @Test + public void getPageTitleGenerator_beanEnabled_instanceReturned() { + PageTitleGenerator pageTitleGenerator = instantiator + .getPageTitleGenerator(); + Assertions.assertNotNull(pageTitleGenerator); + Assertions.assertInstanceOf(TestPageTitleGenerator.class, + pageTitleGenerator); + } + + @Test + public void getServiceInitListeners_javaSPIListenerExists_containsJavaSPIListener() { + Assertions.assertTrue(instantiator.getServiceInitListeners().anyMatch( + listener -> listener instanceof JavaSPIVaadinServiceInitListener)); + } + + @Test + public void getServiceInitListeners_eventFired_cdiObserverCalled() { + VaadinService service = Mockito.mock(VaadinService.class); + ServiceInitEvent event = new ServiceInitEvent(service); + instantiator.getServiceInitListeners() + .filter(listener -> listener.getClass().getPackage() + .equals(CdiInstantiator.class.getPackage())) + .forEach(listener -> listener.serviceInit(event)); + Assertions.assertSame(event, serviceInitObserver.getEvent()); + } + + @Test + public void getOrCreate_beanSingleton_sameInstanceReturned() { + Assertions.assertSame(singleton, + instantiator.getOrCreate(SomeCdiBean.class)); + } + + @Test + public void getOrCreate_beanUnsatisfied_instantiatedAndInjectionOccurred() { + NotACdiBean instance = instantiator.getOrCreate(NotACdiBean.class); + Assertions.assertNotNull(instance); + Assertions.assertNotNull(instance.getBm()); + } + + @Test + public void getOrCreate_beanAmbiguous_instantiatedAndInjectionOccurred() { + ParentBean instance = instantiator.getOrCreate(ParentBean.class); + Assertions.assertNotNull(instance); + Assertions.assertNotNull(instance.getBm()); + } + + @Test + public void createComponent_componentIsCreated() { + SingletonComponent component = instantiator + .createComponent(SingletonComponent.class); + Assertions.assertNotNull(component); + Assertions.assertNotNull(component.beanManager); + } + + @Test + public void createComponent_componentIsCreatedOnEveryCall() + throws ServletException { + SingletonComponent component = instantiator + .createComponent(SingletonComponent.class); + Assertions.assertNotNull(component); + + SingletonComponent anotherComponent = instantiator + .createComponent(SingletonComponent.class); + Assertions.assertNotEquals(component, anotherComponent); + } + + @Test + public void init_callsUsageStatistics() { + // @Before does instantiator.init() + // There will be other entries too to filter out + List entries = UsageStatistics.getEntries() + .filter(entry -> entry.getName().contains("Cdi")) + .collect(Collectors.toList()); + Assertions.assertEquals(1, entries.size()); + + UsageStatistics.UsageEntry entry = entries.get(0); + Assertions.assertEquals("flow/CdiInstantiator", entry.getName()); + } + + @Test + public void getApplicationClass_regularClass_getsSameClass() + throws ServletException { + SomeCdiBean instance = instantiator.getOrCreate(SomeCdiBean.class); + Assertions.assertSame(SomeCdiBean.class, + instantiator.getApplicationClass(instance)); + Assertions.assertSame(SomeCdiBean.class, + instantiator.getApplicationClass(instance.getClass())); + } + + @Test + public void getApplicationClass_scopedBean_getsApplicationClass() + throws ServletException { + ScopedCdiBean instance = instantiator.getOrCreate(ScopedCdiBean.class); + Assertions.assertSame(ScopedCdiBean.class, + instantiator.getApplicationClass(instance)); + Assertions.assertSame(ScopedCdiBean.class, + instantiator.getApplicationClass(instance.getClass())); + } + + @Test + public void getApplicationClass_proxiedBean_getsApplicationClass() + throws ServletException { + InterceptedCdiBean instance = instantiator + .getOrCreate(InterceptedCdiBean.class); + instance.exec(); + Assertions.assertNotSame(InterceptedCdiBean.class, instance.getClass()); + Assertions.assertSame(InterceptedCdiBean.class, + instantiator.getApplicationClass(instance)); + Assertions.assertSame(InterceptedCdiBean.class, + instantiator.getApplicationClass(instance.getClass())); + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceExecutorTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceExecutorTest.java new file mode 100644 index 00000000000..0d09101c898 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceExecutorTest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2000-2025 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.concurrent.ManagedExecutorService; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.junit.MockBean; +import org.jboss.weld.junit5.EnableWeld; +import org.jboss.weld.junit5.WeldInitiator; +import org.jboss.weld.junit5.WeldSetup; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.vaadin.cdi.context.ServiceUnderTestContext; +import com.vaadin.flow.internal.ReflectTools; +import com.vaadin.flow.server.ServiceException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnableWeld +public class CdiVaadinServletServiceExecutorTest { + + @Nested + class UseDefaultManagedExecutor extends BaseTest { + + @WeldSetup + public WeldInitiator weld = withManagedExecutorService().build(); + + @Test + void getExecutor_serviceInitialized_cdiManagedExecutorReturned() + throws ServiceException { + initService(beanManager); + Executor executor = service.getExecutor(); + Assertions.assertSame(managedExecutorService, executor); + } + } + + @Nested + class UseVaadinServiceEnabledExecutor extends BaseTest { + + @WeldSetup + public WeldInitiator weld = withManagedExecutorService() + .addBeans(customTaskExecutorBean()).build(); + + @Test + void getExecutor_serviceInitialized_cdiManagedExecutorReturned() + throws Exception { + initService(beanManager); + Executor executor = service.getExecutor(); + Assertions.assertSame(customExecutor, executor); + } + } + + @Nested + class UseDefaultVaadinExecutor extends BaseTest { + + @WeldSetup + public WeldInitiator weld = WeldInitiator.performDefaultDiscovery(); + + @Test + void getExecutor_serviceInitialized_cdiManagedExecutorReturned() + throws ServiceException { + initService(beanManager); + Executor executor = service.getExecutor(); + Assertions.assertNotNull(executor); + Assertions.assertNotSame(managedExecutorService, executor); + Assertions.assertNotSame(customExecutor, executor); + } + } + + @Nested + class AmbiguousVaadinServiceEnabledExecutor extends BaseTest { + + public WeldInitiator weld = withManagedExecutorService() + .addBeans( + customTaskExecutorBean( + b -> b.name("customVaadinExecutor")), + customTaskExecutorBean(b -> b.name("otherExecutor"))) + .build(); + + @Test + void deploymentValidation_throwsException() throws Exception { + var initWeldMethod = ReflectTools.findMethod(WeldInitiator.class, + "initWeld", Object.class); + initWeldMethod.setAccessible(true); + Throwable error = Assertions.assertThrows( + InvocationTargetException.class, + () -> initWeldMethod.invoke(weld, this)); + error = error.getCause(); + while (error != null && !(error instanceof IllegalStateException)) { + error = error.getCause(); + } + assertNotNull(error); + assertTrue( + error.getMessage().contains("at most one Executor bean")); + assertTrue(error.getMessage().contains("@VaadinServiceEnabled")); + } + } + + // Multiple @VaadinServiceEnabled beans, alternatives are ignored + @Nested + class AmbiguousVaadinServiceEnabledWithAlternativeExecutor + extends BaseTest { + + Executor expectedExecutor = Mockito.mock(Executor.class); + + @WeldSetup + public WeldInitiator weld = withManagedExecutorService().addBeans( + customTaskExecutorBean( + b -> b.name("customVaadinExecutor").alternative(true)), + customTaskExecutorBean(b -> b.name("alternativeVaadinExecutor") + .creating(expectedExecutor))) + .build(); + + @Test + void getExecutor_serviceInitialized_cdiManagedExecutorReturned() + throws ServiceException { + initService(beanManager); + Executor executor = service.getExecutor(); + Assertions.assertSame(expectedExecutor, executor); + } + } + + private static abstract class BaseTest { + + ManagedExecutorService managedExecutorService = Mockito + .mock(ManagedExecutorService.class); + + Executor customExecutor = Mockito.mock(Executor.class); + + @Inject + BeanManager beanManager; + + CdiVaadinServletService service; + + void initService(BeanManager beanManager) throws ServiceException { + ServiceUnderTestContext serviceUnderTestContext = new ServiceUnderTestContext( + beanManager); + serviceUnderTestContext.activate(); + service = serviceUnderTestContext.getService(); + service.init(); + } + + WeldInitiator.Builder withManagedExecutorService() { + return WeldInitiator.from(new Weld()) + // Simulate @Resource injection without name or lookup into + // VaadinTaskExecutorSelector bean in weld mock environment + .bindResource( + "java:comp/env/com.vaadin.cdi.VaadinTaskExecutorSelector$FromResource/managedExecutor", + managedExecutorService); + } + + Bean customTaskExecutorBean() { + return customTaskExecutorBean(unused -> { + }); + } + + Bean customTaskExecutorBean( + Consumer> customizer) { + MockBean.Builder builder = MockBean. builder() + .types(Executor.class).scope(Singleton.class) + .addQualifier(BeanLookup.SERVICE).creating(customExecutor); + customizer.accept(builder); + return builder.build(); + } + + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceTest.java new file mode 100644 index 00000000000..b8d2cd5661e --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletServiceTest.java @@ -0,0 +1,394 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.AmbiguousResolutionException; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.vaadin.cdi.annotation.VaadinServiceEnabled; +import com.vaadin.cdi.annotation.VaadinServiceScoped; +import com.vaadin.cdi.context.ServiceUnderTestContext; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.PollEvent; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.di.InstantiatorFactory; +import com.vaadin.flow.router.internal.AfterNavigationHandler; +import com.vaadin.flow.router.internal.BeforeEnterHandler; +import com.vaadin.flow.router.internal.BeforeLeaveHandler; +import com.vaadin.flow.server.CustomizedSystemMessages; +import com.vaadin.flow.server.DefaultSystemMessagesProvider; +import com.vaadin.flow.server.ServiceException; +import com.vaadin.flow.server.SystemMessages; +import com.vaadin.flow.server.SystemMessagesInfo; +import com.vaadin.flow.server.SystemMessagesProvider; +import com.vaadin.flow.server.UIInitEvent; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.VaadinSession; + +import static com.vaadin.cdi.SerializationUtils.serializeAndDeserialize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CdiVaadinServletServiceTest extends AbstractWeldTest { + + @VaadinServiceEnabled + @VaadinServiceScoped + public static class CdiSystemMessagesProvider + implements SystemMessagesProvider { + + @Override + public SystemMessages getSystemMessages( + SystemMessagesInfo systemMessagesInfo) { + return new CustomizedSystemMessages(); + } + + } + + @Singleton + private static class UIListenerEventReceiver { + + private UI pollEventUI; + + void onPollEvent(@Observes PollEvent pollEvent) { + pollEventUI = pollEvent.getSource(); + } + } + + @Singleton + private static class UIDetachEventReceiver { + + private UI detachEventUI; + + void onUIDetach(@Observes UIDetachEvent uiDetachEvent) { + detachEventUI = (UI) uiDetachEvent.getSource(); + } + } + + @Singleton + private static class UIInitEventReceiver { + + private UI initEventUI; + + private int navigationListenersOnInit; + + void onUIInit(@Observes UIInitEvent uiInitEvent) { + initEventUI = (UI) uiInitEvent.getUI(); + navigationListenersOnInit = initEventUI.getInternals() + .getListeners(BeforeEnterHandler.class).size(); + } + } + + @Inject + private BeanManager beanManager; + + private CdiVaadinServletService service; + + @AfterEach + public void tearDown() { + new ServiceUnderTestContext(beanManager).tearDownAll(); + } + + @Test + public void getInstantiator_serviceInitialized_cdiInstantiatorReturned() + throws ServiceException { + initService(beanManager); + final Instantiator instantiator = service.getInstantiator(); + Assertions.assertTrue(CdiInstantiator.class + .isAssignableFrom(instantiator.getClass())); + } + + @Test + public void init_instantiatorAmbiguous_ExceptionThrown() + throws ServiceException { + assertAmbiguousThrowsException(InstantiatorFactory.class); + } + + @Test + public void init_instantiatorUnsatisfied_ExceptionThrown() { + Assertions.assertThrows(ServiceException.class, + () -> initServiceWithoutBeanFor(InstantiatorFactory.class)); + } + + @Test + public void init_instantiatorInitReturnsFalse_ExceptionThrown() { + BeanManager mockBm = mock(BeanManager.class); + Bean mockBean = mock(Bean.class); + Set> beans = Collections.singleton(mockBean); + when(mockBm.getBeans(eq(InstantiatorFactory.class), + same(BeanLookup.SERVICE))).thenReturn(beans); + // noinspection unchecked + when(mockBm.resolve(same(beans))).thenReturn((Bean) mockBean); + InstantiatorFactory mockInstantiatorFactory = mock( + InstantiatorFactory.class); + Context mockContext = mock(Context.class); + when(mockBm.getContext(VaadinServiceScoped.class)) + .thenReturn(mockContext); + when(mockContext.get(same(mockBean), any())) + .thenReturn(mockInstantiatorFactory); + when(mockInstantiatorFactory.createInstantitor(any())).thenReturn(null); + Assertions.assertThrows(ServiceException.class, + () -> initService(mockBm)); + verify(mockInstantiatorFactory, times(1)) + .createInstantitor(same(service)); + } + + @Test + public void init_SystemMessagesProviderExists_configured() + throws ServiceException { + initService(beanManager); + SystemMessagesProvider systemMessagesProvider = service + .getSystemMessagesProvider(); + Assertions.assertTrue(CdiSystemMessagesProvider.class + .isAssignableFrom(systemMessagesProvider.getClass())); + } + + @Test + public void init_SystemMessagesProviderAmbiguous_ExceptionThrown() { + assertAmbiguousThrowsException(SystemMessagesProvider.class); + } + + @Test + public void init_SystemMessagesProviderMissing_defaultConfigured() { + Assertions.assertThrows(ServiceException.class, () -> { + initServiceWithoutBeanFor(SystemMessagesProvider.class); + Assertions.assertTrue( + DefaultSystemMessagesProvider.class.isAssignableFrom( + service.getSystemMessagesProvider().getClass())); + }); + } + + @Test + public void loadInstantiators_serviceInitialized_instantiatorInstanceCreated() + throws ServiceException { + // #346 + BeanManager mockBm = mock(BeanManager.class); + + ServiceUnderTestContext serviceUnderTestContext = new ServiceUnderTestContext( + mockBm); + serviceUnderTestContext.activate(); + CdiVaadinServletService service = serviceUnderTestContext.getService(); + + Bean mockBean = mock(Bean.class); + Set> beans = Collections.singleton(mockBean); + when(mockBm.getBeans(eq(InstantiatorFactory.class), + same(BeanLookup.SERVICE))).thenReturn(beans); + when(mockBm.resolve(same(beans))).thenReturn((Bean) mockBean); + + CreationalContext mockCreationalContext = mock( + CreationalContext.class); + when(mockBm.createCreationalContext(same(mockBean))) + .thenReturn(mockCreationalContext); + + Context mockContext = mock(Context.class); + when(mockBm.getContext(VaadinServiceScoped.class)) + .thenReturn(mockContext); + + InstantiatorFactory mockInstantiatorFactory = mock( + InstantiatorFactory.class); + when(mockContext.get(same(mockBean), same(mockCreationalContext))) + .thenReturn(mockInstantiatorFactory); + + Instantiator mockInstantiator = mock(Instantiator.class); + when(mockInstantiatorFactory.createInstantitor(same(service))) + .thenReturn(mockInstantiator); + + Optional maybeInstantiator = service.loadInstantiators(); + Assertions.assertTrue(maybeInstantiator.isPresent()); + Assertions.assertEquals(mockInstantiator, maybeInstantiator.get()); + } + + @Test + void uiInit_serialization_UIserializableAndListenersWork() + throws Exception { + initService(beanManager); + + UIListenerEventReceiver uiListenerEventReceiver = service + .getInstantiator().getOrCreate(UIListenerEventReceiver.class); + UI ui = new UI(); + VaadinSession session = new MockVaadinSession(service); + initUI(ui, session); + + ComponentUtil.fireEvent(ui, new PollEvent(ui, false)); + Assertions.assertEquals(ui, uiListenerEventReceiver.pollEventUI); + + UI ui2 = serializeAndDeserialize(ui); + Assertions.assertNotNull(ui2); + + ComponentUtil.fireEvent(ui2, new PollEvent(ui2, false)); + Assertions.assertEquals(ui2, uiListenerEventReceiver.pollEventUI); + } + + @Test + void uiInit_uiAttachedAndDetached_UIeventsCanBeObserved() throws Exception { + initService(beanManager); + + UIDetachEventReceiver uiDetachEventReceiver = service.getInstantiator() + .getOrCreate(UIDetachEventReceiver.class); + UIInitEventReceiver uiInitEventReceiver = service.getInstantiator() + .getOrCreate(UIInitEventReceiver.class); + UI ui = new UI(); + VaadinSession session = new MockVaadinSession(service); + initUI(ui, session); + + Assertions.assertEquals(ui, uiInitEventReceiver.initEventUI); + + session.getLockInstance().lock(); + try { + ui.getInternals().setSession(null); + } finally { + session.getLockInstance().unlock(); + } + + Assertions.assertEquals(ui, uiDetachEventReceiver.detachEventUI); + } + + @Test + void uiInit_uiEventListenersAttachedBeforeCdiEventIsFired() + throws Exception { + initService(beanManager); + + UIInitEventReceiver uiInitEventReceiver = service.getInstantiator() + .getOrCreate(UIInitEventReceiver.class); + UI ui = new UI(); + initUI(ui, new MockVaadinSession(service)); + + Object listener = getSingleListener(ui, AfterNavigationHandler.class); + Assertions.assertSame(listener, + getSingleListener(ui, BeforeEnterHandler.class), + "The same listener should observe all navigation events"); + Assertions.assertSame(listener, + getSingleListener(ui, BeforeLeaveHandler.class), + "The same listener should observe all navigation events"); + + Assertions.assertEquals(1, + uiInitEventReceiver.navigationListenersOnInit, + "UI listeners should be attached before the CDI event"); + } + + private Object getSingleListener(UI ui, Class handlerType) { + List listeners = ui.getInternals().getListeners(handlerType); + Assertions.assertEquals(1, listeners.size(), "Expecting a single " + + handlerType.getSimpleName() + " registered on the UI"); + return listeners.get(0); + } + + private void initUI(UI ui, VaadinSession session) { + session.getLockInstance().lock(); + try { + ui.getInternals().setSession(session); + fireUIInitEvent(ui); + } finally { + session.getLockInstance().unlock(); + } + } + + /** + * Mimics what Flow does once the UI has been created. + *

+ * Since Flow 25.3 the event is fired through the service event bus and + * {@link VaadinService#fireUIInitListeners(UI)} is deprecated for removal + * and no longer called by Flow. The event bus is used reflectively so that + * the test exercises the real code path also when compiled against Flow + * 25.2, where the event bus does not exist yet. + */ + @SuppressWarnings({ "deprecation", "removal" }) + private void fireUIInitEvent(UI ui) { + try { + Object eventBus = VaadinService.class.getMethod("getEventBus") + .invoke(service); + eventBus.getClass().getMethod("fireEvent", EventObject.class) + .invoke(eventBus, new UIInitEvent(ui, service)); + } catch (NoSuchMethodException e) { + // Flow 25.2 and older + service.fireUIInitListeners(ui); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Cannot fire the UI init event", e); + } + } + + private void initService(BeanManager beanManager) throws ServiceException { + ServiceUnderTestContext serviceUnderTestContext = new ServiceUnderTestContext( + beanManager); + serviceUnderTestContext.activate(); + service = serviceUnderTestContext.getService(); + service.init(); + } + + private void initServiceWithoutBeanFor(Class type) + throws ServiceException { + BeanManager mockBm = mock(BeanManager.class); + when(mockBm.getBeans(eq(type), same(BeanLookup.SERVICE))) + .thenReturn(Collections.emptySet()); + initService(mockBm); + } + + private void assertAmbiguousThrowsException(Class type) { + BeanManager mockBm = mock(BeanManager.class); + Bean mockBean = mock(Bean.class); + Set> beans = new HashSet<>(Arrays.asList(mockBean, mockBean)); + when(mockBm.getBeans(eq(type), same(BeanLookup.SERVICE))) + .thenReturn(beans); + // noinspection unchecked + when(mockBm.resolve(same(beans))) + .thenThrow(AmbiguousResolutionException.class); + Assertions.assertThrows(ServiceException.class, + () -> initService(mockBm)); + + verify(mockBm, times(1)).resolve(same(beans)); + } + + private static class MockVaadinSession extends VaadinSession { + + ReentrantLock lock = new ReentrantLock(); + + public MockVaadinSession(VaadinService service) { + super(service); + } + + @Override + public Lock getLockInstance() { + return lock; + } + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletTest.java new file mode 100644 index 00000000000..1657aeca5be --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/CdiVaadinServletTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +import com.vaadin.cdi.context.ServiceUnderTestContext; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.di.Lookup; +import com.vaadin.flow.di.ResourceProvider; +import com.vaadin.flow.server.StaticFileHandlerFactory; +import com.vaadin.flow.server.StaticFileServer; +import com.vaadin.flow.server.VaadinContext; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.server.startup.ApplicationConfiguration; +import com.vaadin.flow.server.startup.ApplicationConfigurationFactory; +import com.vaadin.flow.server.startup.DefaultApplicationConfigurationFactory; + +public class CdiVaadinServletTest extends AbstractWeldTest { + + @Inject + private BeanManager beanManager; + + private CdiVaadinServlet servlet; + + @BeforeEach + public void setUp() throws ServletException { + final ServletConfig servletConfig = Mockito.mock(ServletConfig.class); + final ServletContext servletContext = Mockito + .mock(ServletContext.class); + ClassLoader loader = CdiVaadinServletServiceTest.class.getClassLoader(); + Mockito.when(servletContext.getClassLoader()).thenReturn(loader); + + Lookup lookup = Mockito.mock(Lookup.class); + ResourceProvider provider = Mockito.mock(ResourceProvider.class); + Mockito.when(lookup.lookup(ResourceProvider.class)) + .thenReturn(provider); + Mockito.when(servletContext.getAttribute(Lookup.class.getName())) + .thenReturn(lookup); + + final DefaultApplicationConfigurationFactory applicationConfigurationFactory = Mockito + .mock(DefaultApplicationConfigurationFactory.class); + final ApplicationConfiguration applicationConfiguration = Mockito + .mock(ApplicationConfiguration.class); + Mockito.when(applicationConfiguration.getPropertyNames()) + .thenReturn(Collections.emptyEnumeration()); + final VaadinContext context = Mockito.mock(VaadinContext.class); + Mockito.when(applicationConfiguration.getContext()).thenReturn(context); + Mockito.when(context.getAttribute(Lookup.class)).thenReturn(lookup); + Mockito.when(context.getAttribute(ArgumentMatchers.any(Class.class), + ArgumentMatchers.any(Supplier.class))) + .then(i -> i.getArgument(1, Supplier.class).get()); + + Mockito.when(lookup.lookup(ApplicationConfigurationFactory.class)) + .thenReturn(applicationConfigurationFactory); + Mockito.when(applicationConfigurationFactory.create(Mockito.any())) + .thenReturn(applicationConfiguration); + + StaticFileHandlerFactory staticFileHandlerFactory = vaadinService -> new StaticFileServer( + vaadinService); + Mockito.when(lookup.lookup(StaticFileHandlerFactory.class)) + .thenReturn(staticFileHandlerFactory); + + Mockito.when(servletConfig.getInitParameterNames()) + .thenReturn(Collections.emptyEnumeration()); + Mockito.when(servletConfig.getServletContext()) + .thenReturn(servletContext); + Mockito.when(servletConfig.getServletName()).thenReturn("test"); + Mockito.when(servletContext.getInitParameterNames()) + .thenReturn(Collections.emptyEnumeration()); + + final ServletRegistration servletRegistration = Mockito + .mock(ServletRegistration.class); + final Map servletRegistrationMap = Collections.singletonMap("test", + servletRegistration); + Mockito.when(servletContext.getServletRegistrations()) + .thenReturn(servletRegistrationMap); + Mockito.when(servletRegistration.getMappings()) + .thenReturn(Collections.emptyList()); + + servlet = new CdiVaadinServlet(); + BeanProvider.injectFields(servlet); + servlet.init(servletConfig); + } + + @AfterEach + public void tearDown() { + new ServiceUnderTestContext(beanManager).tearDownAll(); + servlet.destroy(); + } + + @Test + public void getService_servletInitialized_cdiVaadinServletServiceReturned() { + final VaadinService service = servlet.getService(); + Assertions.assertTrue(CdiVaadinServletService.class + .isAssignableFrom(service.getClass())); + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java new file mode 100644 index 00000000000..ad32d015b7a --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/DeploymentValidatorTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Vetoed; +import jakarta.inject.Inject; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.cdi.DeploymentValidator.BeanInfo; +import com.vaadin.cdi.DeploymentValidator.DeploymentProblem; +import com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode; +import com.vaadin.cdi.annotation.NormalUIScoped; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; + +import static com.vaadin.cdi.DeploymentValidator.DeploymentProblem.ErrorCode.NORMAL_SCOPED_COMPONENT; + +public class DeploymentValidatorTest extends AbstractWeldTest { + + @NormalUIScoped + public static class NormalScopedLabel extends Span { + } + + @UIScoped + public static class PseudoScopedLabel extends Span { + } + + @NormalUIScoped + public static class NormalScopedBean { + } + + @Vetoed + public static class ProducedNormalScopedComponent extends Component { + } + + @Route + @RouteScoped + @RouteScopeOwner(RouteTargetOfSelf.class) + public static class RouteTargetOfSelf extends Span { + } + + @Route + @RouteScoped + public static class TestRouteScopedTarget extends Span { + } + + @RouteScoped + public static class TestRouterLayout extends Div implements RouterLayout { + } + + @RouteScoped + public static class TestHasErrorParameter extends Span + implements HasErrorParameter { + @Override + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter parameter) { + return 0; + } + } + + @RouteScopeOwner(RouteTargetOfSelf.class) + @RouteScoped + public static class BeanOfRouteTarget { + } + + @RouteScopeOwner(TestRouterLayout.class) + @RouteScoped + public static class BeanOfRouterLayout { + } + + @RouteScopeOwner(TestRouterLayout.class) + @RouteScoped + @Route(layout = TestRouterLayout.class) + public static class RouteTargetOfRouterLayout { + } + + @RouteScopeOwner(TestHasErrorParameter.class) + @RouteScoped + public static class BeanOfHasErrorParameter { + } + + private static class ProblemId { + private final ErrorCode errorCode; + private final Type baseType; + + private ProblemId(DeploymentProblem problem) { + this.errorCode = problem.getErrorCode(); + this.baseType = problem.getBaseType(); + } + + private ProblemId(ErrorCode errorCode, Type baseType) { + this.errorCode = errorCode; + this.baseType = baseType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProblemId problemId = (ProblemId) o; + return errorCode == problemId.errorCode + && Objects.equals(baseType, problemId.baseType); + } + + @Override + public int hashCode() { + return Objects.hash(errorCode, baseType); + } + + @Override + public String toString() { + return "ProblemId{" + "errorCode=" + errorCode + ", baseType=" + + baseType + '}'; + } + } + + @Inject + private TestDeploymentValidator validator; + + @Inject + private TestDeploymentValidator.BeanInfoSetHolder beanInfoSetHolder; + + private List problems; + + @BeforeEach + public void setUp() { + problems = new ArrayList<>(); + } + + @AfterEach + public void tearDown() { + problems.forEach(problem -> getLogger().error(problem.getMessage())); + } + + @Test + public void validate_normalScopedProblems_collected() { + Set infoSet = createBeanSet(PseudoScopedLabel.class, + NormalScopedBean.class, NormalScopedLabel.class, + ProducedNormalScopedComponent.class); + + validator.validateForTest(infoSet, problems::add); + + Assertions.assertEquals(2, problems.size()); + assertProblemExists(NORMAL_SCOPED_COMPONENT, NormalScopedLabel.class); + assertProblemExists(NORMAL_SCOPED_COMPONENT, + ProducedNormalScopedComponent.class); + } + + @Test + public void validate_routeScopedProblems_collected() { + Set infoSet = createBeanSet(RouteTargetOfSelf.class, + TestRouteScopedTarget.class, TestRouterLayout.class, + TestHasErrorParameter.class, BeanOfHasErrorParameter.class, + BeanOfRouterLayout.class, BeanOfRouteTarget.class, + RouteTargetOfRouterLayout.class); + + validator.validateForTest(infoSet, problems::add); + + Assertions.assertEquals(0, problems.size()); + } + + private void assertProblemExists(ErrorCode errorCode, Type baseType) { + ProblemId expected = new ProblemId(errorCode, baseType); + boolean match = problems.stream() + .map(info -> new ProblemId((DeploymentProblem) info)) + .anyMatch(Predicate.isEqual(expected)); + if (!match) { + Assertions.fail("Problem does not exist: " + expected); + } + } + + private Set createBeanSet(Type... clazz) { + Map map = getBeanInfoSetAsMap(); + return Arrays.stream(clazz).map(map::get).collect(Collectors.toSet()); + } + + private Map getBeanInfoSetAsMap() { + return beanInfoSetHolder.getInfoSet().stream() + // Weld causes duplicate key because of exposing weird things as + // Object. + // Tests are not interested in it. + .filter(beanInfo -> !beanInfo.getBaseType() + .equals(Object.class)) + .collect(Collectors.toMap(BeanInfo::getBaseType, + Function.identity())); + } + + @Produces + @NormalUIScoped + private ProducedNormalScopedComponent getProducedComponent() { + return new ProducedNormalScopedComponent(); + } + + private static Logger getLogger() { + return LoggerFactory.getLogger(DeploymentValidatorTest.class); + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/JavaSPIVaadinServiceInitListener.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/JavaSPIVaadinServiceInitListener.java new file mode 100644 index 00000000000..d26b22b69d4 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/JavaSPIVaadinServiceInitListener.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import com.vaadin.flow.server.ServiceInitEvent; +import com.vaadin.flow.server.VaadinServiceInitListener; + +public class JavaSPIVaadinServiceInitListener + implements VaadinServiceInitListener { + + @Override + public void serviceInit(ServiceInitEvent event) { + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/PolymerTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/PolymerTest.java new file mode 100644 index 00000000000..d92dba5b449 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/PolymerTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +import org.jsoup.Jsoup; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.cdi.annotation.UIScoped; +import com.vaadin.cdi.context.UIUnderTestContext; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.polymertemplate.Id; +import com.vaadin.flow.component.polymertemplate.PolymerTemplate; +import com.vaadin.flow.component.polymertemplate.TemplateParser.TemplateData; +import com.vaadin.flow.di.Instantiator; +import com.vaadin.flow.internal.CurrentInstance; +import com.vaadin.flow.server.VaadinService; +import com.vaadin.flow.templatemodel.TemplateModel; + +public class PolymerTest extends AbstractWeldTest { + + @UIScoped + @Tag("uiscoped-label") + public static class PseudoScopedLabel extends Span { + } + + @Tag("test-template") + public static class TestTemplate extends PolymerTemplate { + + @Id("pseudo") + private PseudoScopedLabel pseudo; + + public TestTemplate() { + super((clazz, tag, service) -> new TemplateData("", + Jsoup.parse(getTemplateContent()))); + } + + } + + @Inject + private Provider pseudoScopedLabelProvider; + private TestTemplate template; + private UIUnderTestContext uiUnderTestContext; + private Instantiator instantiator; + + @BeforeEach + public void setUp() throws Exception { + uiUnderTestContext = new UIUnderTestContext(); + uiUnderTestContext.activate(); + UI ui = uiUnderTestContext.getUi(); + VaadinService service = ui.getSession().getService(); + service.init(); + VaadinService.setCurrent(service); + instantiator = service.getInstantiator(); + template = instantiator.getOrCreate(TestTemplate.class); + } + + @AfterEach + public void tearDown() { + uiUnderTestContext.tearDownAll(); + CurrentInstance.clearAll(); + } + + @Test + public void injectField_componentHasScope_scopeIsIgnored() { + final PseudoScopedLabel label = pseudoScopedLabelProvider.get(); + Assertions.assertNotSame(label, template.pseudo); + } + + @Test + public void injectField_componentHasScope_elementBindingSuccess() { + Assertions.assertNotNull( + template.pseudo.getElement().getNode().getParent()); + } + + private static String getTemplateContent() { + return "\n" + " \n" + "\n"; + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/SerializationUtils.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/SerializationUtils.java new file mode 100644 index 00000000000..968ed5d1e42 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/SerializationUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2023 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +public final class SerializationUtils { + + private SerializationUtils() { + } + + @SuppressWarnings("unchecked") + public static S serializeAndDeserialize(S obj) + throws Exception { + // Serialize and deserialize + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + try (ObjectOutputStream out = new ObjectOutputStream(bs)) { + out.writeObject(obj); + } + byte[] data = bs.toByteArray(); + try (ObjectInputStream in = new ObjectInputStream( + new ByteArrayInputStream(data))) { + return (S) in.readObject(); + } + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/TestCdiVaadinServletService.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/TestCdiVaadinServletService.java new file mode 100644 index 00000000000..dec1630171a --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/TestCdiVaadinServletService.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.servlet.ServletContext; + +import java.util.Properties; + +import com.vaadin.flow.di.Lookup; +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.startup.ApplicationConfiguration; +import com.vaadin.flow.server.startup.ApplicationConfigurationFactory; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestCdiVaadinServletService extends CdiVaadinServletService { + + public TestCdiVaadinServletService(BeanManager beanManager, + String servletName) { + super(mock(CdiVaadinServlet.class), mock(DeploymentConfiguration.class), + beanManager); + when(getServlet().getServletName()).thenReturn(servletName); + when(getServlet().getService()).thenReturn(this); + final ServletContext servletcontext = mock(ServletContext.class); + when(getServlet().getServletContext()).thenReturn(servletcontext); + Lookup lookup = mock(Lookup.class); + when(servletcontext.getAttribute(Lookup.class.getName())) + .thenReturn(lookup); + when(lookup.lookup(ApplicationConfigurationFactory.class)) + .thenReturn(context -> mock(ApplicationConfiguration.class)); + + DeploymentConfiguration config = getDeploymentConfiguration(); + Properties properties = new Properties(); + when(config.getInitParameters()).thenReturn(properties); + } + + @Override + public String getMainDivId(VaadinSession session, VaadinRequest request) { + return "test-1"; + } + + // We have nothing to do with atmosphere, + // and mocking is much easier without it. + @Override + protected boolean isAtmosphereAvailable() { + return false; + } + + @Override + public void setClassLoader(ClassLoader classLoader) { + if (classLoader != null) { + super.setClassLoader(classLoader); + } + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/TestDeploymentValidator.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/TestDeploymentValidator.java new file mode 100644 index 00000000000..81ce9262ab1 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/TestDeploymentValidator.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Alternative; +import jakarta.inject.Inject; + +import java.util.Collections; +import java.util.Set; +import java.util.function.Consumer; + +@Alternative +@Dependent +@Priority(100) +class TestDeploymentValidator extends DeploymentValidator { + + @ApplicationScoped + static class BeanInfoSetHolder { + + private Set infoSet; + + Set getInfoSet() { + return Collections.unmodifiableSet(infoSet); + } + + void setInfoSet(Set infoSet) { + this.infoSet = infoSet; + } + + } + + @Inject + private BeanInfoSetHolder beanInfoSetHolder; + + @Override + void validate(Set infoSet, Consumer problemConsumer) { + // No-op. + // We need CDI to startup in Unit tests even with + // intentionally misconfigured beans. + beanInfoSetHolder.setInfoSet(infoSet); + } + + void validateForTest(Set infoSet, + Consumer problemConsumer) { + super.validate(infoSet, problemConsumer); + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java new file mode 100644 index 00000000000..24b59a327a9 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/AbstractContextTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.ContextNotActiveException; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.vaadin.cdi.AbstractWeldTest; +import com.vaadin.cdi.util.BeanProvider; + +public abstract class AbstractContextTest + extends AbstractWeldTest { + + private List contexts; + + @BeforeEach + public void setUp() { + TestBean.resetCount(); + contexts = new ArrayList<>(); + } + + @AfterEach + public void tearDown() { + newContextUnderTest().tearDownAll(); + contexts = null; + } + + @Test + public void get_contextNotActive_ExceptionThrown() { + Assertions.assertThrows(ContextNotActiveException.class, () -> { + final T reference = BeanProvider + .getContextualReference(getBeanType()); + reference.getState(); + }); + } + + @Test + public void get_sameContextActive_beanCreatedOnce() { + createContext().activate(); + T referenceA = BeanProvider.getContextualReference(getBeanType()); + referenceA.setState("hello"); + Assertions.assertEquals("hello", referenceA.getState()); + T referenceB = BeanProvider.getContextualReference(getBeanType()); + Assertions.assertEquals("hello", referenceB.getState()); + Assertions.assertEquals(1, TestBean.getBeanCount()); + } + + @Test + public void get_newContextActive_newBeanCreated() { + createContext().activate(); + final T referenceA = BeanProvider.getContextualReference(getBeanType()); + referenceA.setState("hello"); + createContext().activate(); + if (isNormalScoped()) { + // proxy delegates to the active context automatically + Assertions.assertEquals("", referenceA.getState()); + } else { + // pseudo scoped bean ignores active context after creation + Assertions.assertEquals("hello", referenceA.getState()); + } + final T referenceB = BeanProvider.getContextualReference(getBeanType()); + Assertions.assertEquals("", referenceB.getState()); + Assertions.assertEquals(2, TestBean.getBeanCount()); + } + + @Test + public void destroy_beanExistsInContext_beanDestroyed() { + final UnderTestContext contextUnderTestA = createContext(); + contextUnderTestA.activate(); + final T referenceA = BeanProvider.getContextualReference(getBeanType()); + referenceA.setState("hello"); + final UnderTestContext contextUnderTestB = createContext(); + contextUnderTestB.activate(); + final T referenceB = BeanProvider.getContextualReference(getBeanType()); + referenceB.setState("hello"); + Assertions.assertEquals(2, TestBean.getBeanCount()); + contextUnderTestA.destroy(); + Assertions.assertEquals(1, TestBean.getBeanCount()); + contextUnderTestB.destroy(); + Assertions.assertEquals(0, TestBean.getBeanCount()); + } + + protected UnderTestContext createContext() { + UnderTestContext underTestContext = newContextUnderTest(); + /* + * UnderTestContext implementations set fields to Vaadin + * CurrentInstance. Need to hold a hard reference to prevent possible + * GC, because CurrentInstance works with weak reference. + */ + contexts.add(underTestContext); + return underTestContext; + } + + protected abstract UnderTestContext newContextUnderTest(); + + protected abstract boolean isNormalScoped(); + + protected abstract Class getBeanType(); + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java new file mode 100644 index 00000000000..820922661df --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextNormalTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import java.io.Serializable; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.vaadin.cdi.annotation.NormalRouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.startup.ApplicationConfiguration; + +import static com.vaadin.cdi.SerializationUtils.serializeAndDeserialize; + +public class RouteContextNormalTest extends + AbstractContextTest { + + @NormalRouteScoped + @Route("") + public static class RouteScopedTestBean extends TestBean + implements Serializable { + } + + @Override + protected Class getBeanType() { + return RouteScopedTestBean.class; + } + + @Override + protected UnderTestContext newContextUnderTest() { + // Intentionally UI Under Test Context. Nothing else needed. + UIUnderTestContext context = new UIUnderTestContext() { + + @Override + public void activate() { + super.activate(); + + NavigationData data = new NavigationData( + TestNavigationTarget.class, Collections.emptyList()); + ComponentUtil.setData(getUi(), NavigationData.class, data); + } + }; + + return context; + } + + @Override + protected boolean isNormalScoped() { + return true; + } + + @Test + void activeContext_UISerializable() throws Exception { + UIUnderTestContext context = (UIUnderTestContext) createContext(); + context.activate(); + BeanProvider.getContextualReference(getBeanType()); + UI ui = context.getUi(); + try (MockedStatic appCfg = Mockito + .mockStatic(ApplicationConfiguration.class)) { + appCfg.when( + () -> ApplicationConfiguration.get(ArgumentMatchers.any())) + .thenReturn(Mockito.mock(ApplicationConfiguration.class)); + UI ui2 = serializeAndDeserialize(ui); + Assertions.assertNotNull(ui2); + } + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java new file mode 100644 index 00000000000..473cea0f104 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextPseudoTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import java.io.Serializable; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.startup.ApplicationConfiguration; + +import static com.vaadin.cdi.SerializationUtils.serializeAndDeserialize; + +public class RouteContextPseudoTest extends + AbstractContextTest { + + @RouteScoped + @Route("") + public static class RouteScopedTestBean extends TestBean + implements Serializable { + } + + @Override + protected Class getBeanType() { + return RouteScopedTestBean.class; + } + + @Override + protected UnderTestContext newContextUnderTest() { + // Intentionally UI Under Test Context. Nothing else needed. + UIUnderTestContext context = new UIUnderTestContext() { + + @Override + public void activate() { + super.activate(); + + NavigationData data = new NavigationData( + TestNavigationTarget.class, Collections.emptyList()); + ComponentUtil.setData(getUi(), NavigationData.class, data); + } + }; + + return context; + } + + @Override + protected boolean isNormalScoped() { + return false; + } + + @Test + void activeContext_UISerializable() throws Exception { + UIUnderTestContext context = (UIUnderTestContext) createContext(); + context.activate(); + BeanProvider.getContextualReference(getBeanType()); + UI ui = context.getUi(); + try (MockedStatic appCfg = Mockito + .mockStatic(ApplicationConfiguration.class)) { + appCfg.when( + () -> ApplicationConfiguration.get(ArgumentMatchers.any())) + .thenReturn(Mockito.mock(ApplicationConfiguration.class)); + UI ui2 = serializeAndDeserialize(ui); + Assertions.assertNotNull(ui2); + } + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java new file mode 100644 index 00000000000..79a9ce4c53c --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/RouteContextualStorageManagerTest.java @@ -0,0 +1,609 @@ +/* + * Copyright 2000-2026 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.Reception; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.inject.Provider; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.vaadin.cdi.AbstractWeldTest; +import com.vaadin.cdi.annotation.RouteScopeOwner; +import com.vaadin.cdi.annotation.RouteScoped; +import com.vaadin.cdi.context.RouteScopedContext.NavigationData; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.page.ExtendedClientDetails; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.router.AfterNavigationEvent; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.LocationChangeEvent; +import com.vaadin.flow.router.PreserveOnRefresh; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; +import com.vaadin.flow.server.VaadinSession; + +public class RouteContextualStorageManagerTest extends AbstractWeldTest { + + private static final String STATE = "hello"; + + private abstract static class HasElementTestBean extends TestBean + implements HasElement { + @Override + public Element getElement() { + return null; + } + } + + @Route("group1") + public static class Group1 extends HasElementTestBean { + } + + @RouteScoped + @RouteScopeOwner(Group1.class) + public static class MemberOfGroup1 extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } + } + + @RouteScoped + public static class NoOwnerBean extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } + + } + + @Route("group2") + public static class Group2 extends HasElementTestBean { + } + + @Route("preserved") + @PreserveOnRefresh + public static class PreservedGroup extends HasElementTestBean { + } + + @PreserveOnRefresh + public static class PreservedLayout extends HasElementTestBean + implements RouterLayout { + } + + @RouteScoped + @RouteScopeOwner(PreservedGroup.class) + public static class MemberOfPreservedGroup extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } + } + + /** + * A layout without {@link PreserveOnRefresh}, shared by a preserved and a + * plain child. Flow reuses the same layout instance when navigating between + * the two, and preserves it on refresh together with the preserved child. + */ + public static class SharedLayout extends HasElementTestBean + implements RouterLayout { + } + + @RouteScoped + @RouteScopeOwner(SharedLayout.class) + public static class MemberOfSharedLayout extends HasElementTestBean { + + boolean isDestroyed; + + @PreDestroy + private void onDestroy() { + isDestroyed = true; + } + } + + @Route("") + public static class InitialRoute extends HasElementTestBean { + + } + + @Route("") + public static class ConditionalComponentRoute extends HasElementTestBean { + + } + + @RouteScoped + @RouteScopeOwner(ConditionalComponentRoute.class) + public static class ConditionalComponent extends HasElementTestBean { + + private void onEvent( + @Observes(notifyObserver = Reception.IF_EXISTS) CustomEvent event) { + } + } + + private static class CustomEvent { + } + + private UIUnderTestContext uiUnderTestContext; + + @Inject + @RouteScopeOwner(Group1.class) + private Provider memberOfGroup1; + + @Inject + @RouteScopeOwner(PreservedGroup.class) + private Provider memberOfPreservedGroup; + + @Inject + private Provider noOwnerBean; + + @Inject + @RouteScopeOwner(SharedLayout.class) + private Provider memberOfSharedLayout; + + @Inject + private Event beforeNavigationTrigger; + + @Inject + private Event afterNavigationTrigger; + + @Inject + private Event customEventEventTrigger; + + private BeforeEnterEvent event; + private AfterNavigationEvent afterEvent; + private LocationChangeEvent changeEvent; + + private NavigationData data = Mockito.mock(NavigationData.class); + + @Inject + private BeanManager manager; + + @BeforeEach + public void setUp() { + doSetUp(null, null); + } + + @AfterEach + public void tearDown() { + uiUnderTestContext.tearDownAll(); + } + + @Test + public void onBeforeEnter_initialNavigationTarget_scopeDoesNotExist_Throws() { + Assertions.assertThrows(IllegalStateException.class, () -> { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) InitialRoute.class); + beforeNavigationTrigger.fire(event); + + memberOfGroup1.get(); + }); + } + + @Test + public void get_noNavigationDataYet_ownedBean_scopeDoesNotExist_Throws() { + // A bean that names its owner resolves it from the qualifier without + // consulting the navigation data, so it reaches the chain check even + // when no navigation has happened on this UI at all. That has to be + // reported as a missing scope rather than failing on the absent data. + ComponentUtil.setData(uiUnderTestContext.getUi(), NavigationData.class, + null); + + Assertions.assertThrows(IllegalStateException.class, + () -> memberOfGroup1.get()); + } + + @Test + public void afterNavigation_initialNavigationTarget_scopeDoesNotExist_Throws() { + Assertions.assertThrows(IllegalStateException.class, () -> { + Mockito.when(afterEvent.getActiveChain()) + .thenReturn(Collections.singletonList(new InitialRoute())); + afterNavigationTrigger.fire(afterEvent); + + memberOfGroup1.get(); + }); + } + + @Test + public void onBeforeEnter_group1Navigation_beansAreScoped() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean = memberOfGroup1.get(); + bean.setState(STATE); + Assertions.assertEquals(STATE, memberOfGroup1.get().getState()); + + noOwnerBean.get().setState(STATE); + Assertions.assertEquals(STATE, noOwnerBean.get().getState()); + } + + @Test + public void onBeforeEnter_group2NavigationAfterGroup1_beansAreDestroyed() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + NoOwnerBean bean2 = noOwnerBean.get(); + bean2.setState(STATE); + + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group2.class); + beforeNavigationTrigger.fire(event); + + Assertions.assertTrue(bean1.isDestroyed); + Assertions.assertTrue(bean2.isDestroyed); + + // no owner bean is not preserved: the new one is created + Assertions.assertNotEquals(STATE, noOwnerBean.get().getState()); + } + + @Test + public void afterNavigation_group2NavigationAftergroup1_beansAreDestroyed() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + NoOwnerBean bean2 = noOwnerBean.get(); + bean2.setState(STATE); + + Mockito.when(afterEvent.getActiveChain()) + .thenReturn(Collections.singletonList(new Group2())); + afterNavigationTrigger.fire(afterEvent); + + Assertions.assertTrue(bean1.isDestroyed); + Assertions.assertTrue(bean2.isDestroyed); + } + + @Test + public void preserveOnRefresh_anotherUIHasSameWindowName_beanIsPreserved() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) PreservedGroup.class); + beforeNavigationTrigger.fire(event); + + MemberOfPreservedGroup bean1 = memberOfPreservedGroup.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context + doSetUp("foo", ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) PreservedGroup.class); + beforeNavigationTrigger.fire(event); + + ComponentUtil.onComponentDetach(ui); + + Assertions.assertFalse(bean1.isDestroyed); + Assertions.assertSame(bean1, memberOfPreservedGroup.get()); + Assertions.assertEquals(STATE, memberOfPreservedGroup.get().getState()); + } + + @Test + public void noPreserveOnRefresh_anotherUIHasSameWindowName_beanIsNotShared() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context, + // simulating e.g. a page refresh or a duplicated browser tab + doSetUp("foo", ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean2 = memberOfGroup1.get(); + Assertions.assertNotSame(bean1, bean2, + "Beans of a not preserved navigation chain must not be shared " + + "between UIs of the same browser window"); + Assertions.assertNotEquals(STATE, bean2.getState()); + + // the bean of the first UI is still bound to that UI only, and is + // destroyed together with it + Assertions.assertFalse(bean1.isDestroyed); + ComponentUtil.onComponentDetach(ui); + Assertions.assertTrue(bean1.isDestroyed); + Assertions.assertFalse(bean2.isDestroyed); + } + + @Test + public void preserveOnRefreshLayout_anotherUIHasSameWindowName_beanIsPreserved() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + Mockito.when(event.getLayouts()).thenReturn( + Collections.> singletonList( + PreservedLayout.class)); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context + doSetUp("foo", ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + Mockito.when(event.getLayouts()).thenReturn( + Collections.> singletonList( + PreservedLayout.class)); + beforeNavigationTrigger.fire(event); + + ComponentUtil.onComponentDetach(ui); + + Assertions.assertFalse(bean1.isDestroyed); + Assertions.assertSame(bean1, memberOfGroup1.get()); + Assertions.assertEquals(STATE, memberOfGroup1.get().getState()); + } + + @Test + public void onBeforeEnter_anotherUIHasNoWindowName_beanIsDestroyedOnUiDestroy() { + UI ui = doSetUp("foo", null); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context + doSetUp(null, ui.getSession()); + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + ComponentUtil.onComponentDetach(ui); + + Assertions.assertTrue(bean1.isDestroyed); + Assertions.assertNotEquals(STATE, memberOfGroup1.get().getState()); + } + + @Test + public void getActiveContextualStorages_getAllStoragesForGivenUI() { + RouteScopedContext context = new RouteScopedContext(manager); + context.init(manager, null); + + UI ui = doSetUp("foo", null); + + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean = memberOfGroup1.get(); + + List storages = context + .getActiveContextualStorages(); + Assertions.assertEquals(1, storages.size()); + ContextualStorage storage = storages.get(0); + + doSetUp(null, ui.getSession()); + + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) Group1.class); + beforeNavigationTrigger.fire(event); + + MemberOfGroup1 bean1 = memberOfGroup1.get(); + + storages = context.getActiveContextualStorages(); + Assertions.assertEquals(1, storages.size()); + // Now the second UI is active and its storage is a different one + Assertions.assertNotSame(storage, storages.get(0)); + } + + @Test + public void sharedLayout_navigateFromPreservedChildToPlainChild_layoutBeanIsKept() { + doSetUp("foo", null); + + navigateTo(PreservedGroup.class, SharedLayout.class); + MemberOfSharedLayout bean1 = memberOfSharedLayout.get(); + bean1.setState(STATE); + + // the layout instance is reused by Flow, so its beans must survive + navigateTo(Group1.class, SharedLayout.class); + + MemberOfSharedLayout bean2 = memberOfSharedLayout.get(); + Assertions.assertFalse(bean1.isDestroyed, + "The layout owned bean must not be destroyed while the layout " + + "stays in the navigation chain"); + Assertions.assertSame(bean1, bean2, + "The layout owned bean must stay the same instance while the " + + "layout is reused across navigations"); + Assertions.assertEquals(STATE, bean2.getState()); + } + + @Test + public void sharedLayout_navigateFromPlainChildToPreservedChild_layoutBeanIsKept() { + doSetUp("foo", null); + + navigateTo(Group1.class, SharedLayout.class); + MemberOfSharedLayout bean1 = memberOfSharedLayout.get(); + bean1.setState(STATE); + + navigateTo(PreservedGroup.class, SharedLayout.class); + + MemberOfSharedLayout bean2 = memberOfSharedLayout.get(); + Assertions.assertFalse(bean1.isDestroyed, + "The layout owned bean must not be destroyed while the layout " + + "stays in the navigation chain"); + Assertions.assertSame(bean1, bean2, + "The layout owned bean must stay the same instance when the " + + "navigation chain becomes preserved"); + Assertions.assertEquals(STATE, bean2.getState()); + } + + @Test + public void sharedLayout_navigateBackAndForth_layoutBeanIsKept() { + doSetUp("foo", null); + + navigateTo(PreservedGroup.class, SharedLayout.class); + MemberOfSharedLayout bean = memberOfSharedLayout.get(); + bean.setState(STATE); + + navigateTo(Group1.class, SharedLayout.class); + MemberOfSharedLayout beanOnPlainChild = memberOfSharedLayout.get(); + navigateTo(PreservedGroup.class, SharedLayout.class); + + Assertions.assertFalse(bean.isDestroyed); + Assertions.assertSame(bean, beanOnPlainChild, + "The layout owned bean must stay the same instance when the " + + "navigation chain is not preserved anymore"); + Assertions.assertSame(bean, memberOfSharedLayout.get(), + "The layout owned bean must stay the same instance when the " + + "navigation chain is preserved again"); + Assertions.assertEquals(STATE, memberOfSharedLayout.get().getState()); + } + + @Test + public void sharedLayout_beanReboundToUI_isDestroyedOnUIDetach() { + UI ui = doSetUp("foo", null); + + navigateTo(PreservedGroup.class, SharedLayout.class); + MemberOfSharedLayout bean = memberOfSharedLayout.get(); + + // the chain is not preserved anymore, so the bean is bound to this UI + navigateTo(Group1.class, SharedLayout.class); + Assertions.assertSame(bean, memberOfSharedLayout.get()); + + // set another UI instance with the same window name into the context, + // simulating e.g. a page refresh + doSetUp("foo", ui.getSession()); + ComponentUtil.onComponentDetach(ui); + + Assertions.assertTrue(bean.isDestroyed, + "The layout owned bean is not preserved anymore, so it must be " + + "destroyed together with its UI"); + } + + @Test + public void sharedLayout_anotherUIHoldsPreservedChain_beanIsNotStolen() { + UI ui = doSetUp("foo", null); + + navigateTo(PreservedGroup.class, SharedLayout.class); + MemberOfSharedLayout bean1 = memberOfSharedLayout.get(); + bean1.setState(STATE); + + // set another UI instance with the same window name into the context, + // both UIs being alive at the same time + doSetUp("foo", ui.getSession()); + navigateTo(Group1.class, SharedLayout.class); + + MemberOfSharedLayout bean2 = memberOfSharedLayout.get(); + Assertions.assertNotSame(bean1, bean2, + "The beans of a not preserved navigation chain must not be " + + "taken from a UI that may still hold the preserved " + + "chain"); + Assertions.assertNotEquals(STATE, bean2.getState()); + Assertions.assertFalse(bean1.isDestroyed); + } + + @Test + public void onBeforeEnter_conditionalBean_doesNotThrow() { + Mockito.when(event.getNavigationTarget()) + .thenReturn((Class) InitialRoute.class); + beforeNavigationTrigger.fire(event); + customEventEventTrigger.fire(new CustomEvent()); + } + + @Test + public void customEvent_noNavigationDataYet_conditionalBean_doesNotThrow() { + // An IF_EXISTS observer has the container look the bean up without + // creating it, which reaches the navigation chain check with + // createIfNotExist false - the other side of the branch covered by + // get_noNavigationDataYet_ownedBean_scopeDoesNotExist_Throws. That + // path has to answer "no storage" rather than fail on the absent + // navigation data, so firing the event before any navigation must + // simply notify nobody. + ComponentUtil.setData(uiUnderTestContext.getUi(), NavigationData.class, + null); + + customEventEventTrigger.fire(new CustomEvent()); + } + + private void navigateTo(Class navigationTarget, + Class layout) { + Mockito.when(event.getNavigationTarget()).thenReturn(navigationTarget); + Mockito.when(event.getLayouts()).thenReturn(Collections + .> singletonList(layout)); + beforeNavigationTrigger.fire(event); + } + + private UI doSetUp(String windowName, VaadinSession session) { + changeEvent = Mockito.mock(LocationChangeEvent.class); + + Mockito.when(data.getNavigationTarget()) + .thenReturn((Class) InitialRoute.class); + event = Mockito.mock(BeforeEnterEvent.class); + uiUnderTestContext = new UIUnderTestContext(session) { + + @Override + public void activate() { + super.activate(); + + if (windowName != null) { + ExtendedClientDetails details = Mockito + .mock(ExtendedClientDetails.class); + Mockito.when(details.getWindowName()) + .thenReturn(windowName); + getUi().getInternals().setExtendedClientDetails(details); + } + + ComponentUtil.setData(getUi(), NavigationData.class, data); + Mockito.when(changeEvent.getUI()).thenReturn(getUi()); + Mockito.when(event.getUI()) + .thenReturn(uiUnderTestContext.getUi()); + } + }; + uiUnderTestContext.activate(); + + UI ui = uiUnderTestContext.getUi(); + + uiUnderTestContext.getUi().getSession().addUI(ui); + + afterEvent = Mockito.mock(AfterNavigationEvent.class); + Mockito.when(afterEvent.getLocationChangeEvent()) + .thenReturn(changeEvent); + + return uiUnderTestContext.getUi(); + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceContextTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceContextTest.java new file mode 100644 index 00000000000..470981cd6a7 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceContextTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; + +import com.vaadin.cdi.annotation.VaadinServiceScoped; + +public class ServiceContextTest + extends AbstractContextTest { + @Inject + private BeanManager beanManager; + + @Override + protected Class getBeanType() { + return ServiceScopedTestBean.class; + } + + @Override + protected UnderTestContext newContextUnderTest() { + return new ServiceUnderTestContext(beanManager); + } + + @Override + protected boolean isNormalScoped() { + return true; + } + + @VaadinServiceScoped + public static class ServiceScopedTestBean extends TestBean { + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceUnderTestContext.java new file mode 100644 index 00000000000..43b8fa1bcd7 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/ServiceUnderTestContext.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.spi.AlterableContext; +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.util.Set; + +import com.vaadin.cdi.CdiVaadinServletService; +import com.vaadin.cdi.TestCdiVaadinServletService; +import com.vaadin.cdi.context.VaadinServiceScopedContext.ContextualStorageManager; +import com.vaadin.flow.server.ServiceDestroyEvent; +import com.vaadin.flow.server.VaadinService; + +public class ServiceUnderTestContext implements UnderTestContext { + private CdiVaadinServletService service; + private static int NDX; + private final BeanManager beanManager; + + public ServiceUnderTestContext(BeanManager beanManager) { + this.beanManager = beanManager; + } + + @Override + public void activate() { + NDX++; + service = new TestCdiVaadinServletService(beanManager, NDX + ""); + VaadinService.setCurrent(service); + } + + @Override + public void tearDownAll() { + VaadinService.setCurrent(null); + Context appContext = beanManager.getContext(ApplicationScoped.class); + Set> beans = beanManager + .getBeans(ContextualStorageManager.class); + ((AlterableContext) appContext).destroy(beanManager.resolve(beans)); + } + + @Override + public void destroy() { + if (service != null) { + beanManager.getEvent().fire(new ServiceDestroyEvent(service)); + } + } + + public CdiVaadinServletService getService() { + return service; + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionContextTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionContextTest.java new file mode 100644 index 00000000000..0ecf176ce59 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionContextTest.java @@ -0,0 +1,271 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.inject.spi.BeanManager; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.vaadin.cdi.annotation.VaadinSessionScoped; +import com.vaadin.cdi.context.VaadinSessionScopedContext.ContextualStorageManager; +import com.vaadin.cdi.util.BeanProvider; +import com.vaadin.cdi.util.ContextualStorage; +import com.vaadin.flow.server.Command; +import com.vaadin.flow.server.VaadinSession; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SessionContextTest + extends AbstractContextTest { + + @Override + protected UnderTestContext newContextUnderTest() { + return new SessionUnderTestContext(); + } + + @Override + protected boolean isNormalScoped() { + return true; + } + + @Override + protected Class getBeanType() { + return SessionScopedTestBean.class; + } + + @Test + public void get_sessionExistsButNotLocked_contextNotActive() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(false); + + VaadinSessionScopedContext sessionContext = newContext(); + + assertFalse(sessionContext.isActive()); + + assertThrows(ContextNotActiveException.class, () -> { + SessionScopedTestBean ref = BeanProvider + .getContextualReference(SessionScopedTestBean.class); + ref.getState(); + }); + } + + @Test + public void defaultManager_sessionLocked_isActive() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(true); + + ContextualStorageManager manager = lookupManager(); + assertTrue(manager.isActive()); + } + + @Test + public void context_nullSession_notActive() { + VaadinSession.setCurrent(null); + VaadinSessionScopedContext sessionContext = newContext(); + assertFalse(sessionContext.isActive()); + } + + @Test + public void defaultManager_storageAccess_returnsStorage() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(true); + + VaadinSessionScopedContext sessionContext = newContext(); + ContextualStorage storage = sessionContext.getContextualStorage(null, + true); + assertNotNull(storage); + verify(session, never()).accessSynchronously(any(Command.class)); + } + + @Test + public void lenientSubclass_isActiveWithoutLock() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(false); + + ContextualStorageManager lenient = injectBeanManager( + new ContextualStorageManager() { + @Override + protected boolean isActive() { + return VaadinSession.getCurrent() != null; + } + }); + + VaadinSessionScopedContext sessionContext = newContext(lenient); + assertTrue(sessionContext.isActive()); + } + + @Test + public void lenientSubclass_storageAccess_wrappedInAccessSynchronously() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(false); + doAnswer(inv -> { + ((Command) inv.getArgument(0)).execute(); + return null; + }).when(session).accessSynchronously(any(Command.class)); + + ContextualStorageManager lenient = injectBeanManager( + new ContextualStorageManager() { + @Override + protected boolean isActive() { + return VaadinSession.getCurrent() != null; + } + + @Override + protected ContextualStorage getContextualStorage( + Contextual c, boolean create) { + VaadinSession s = VaadinSession.getCurrent(); + if (s.hasLock()) { + return super.getContextualStorage(c, create); + } + AtomicReference ref = new AtomicReference<>(); + s.accessSynchronously(() -> ref + .set(super.getContextualStorage(c, create))); + return ref.get(); + } + }); + + VaadinSessionScopedContext sessionContext = newContext(lenient); + ContextualStorage storage = sessionContext.getContextualStorage(null, + true); + + assertNotNull(storage); + verify(session).accessSynchronously(any(Command.class)); + } + + @Test + public void lenientSubclass_storageAccess_lockHeldFastPath() { + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + VaadinSession session = context.getSession(); + when(session.hasLock()).thenReturn(true); + + ContextualStorageManager lenient = injectBeanManager( + new ContextualStorageManager() { + @Override + protected boolean isActive() { + return VaadinSession.getCurrent() != null; + } + + @Override + protected ContextualStorage getContextualStorage( + Contextual c, boolean create) { + VaadinSession s = VaadinSession.getCurrent(); + if (s.hasLock()) { + return super.getContextualStorage(c, create); + } + AtomicReference ref = new AtomicReference<>(); + s.accessSynchronously(() -> ref + .set(super.getContextualStorage(c, create))); + return ref.get(); + } + }); + + VaadinSessionScopedContext sessionContext = newContext(lenient); + ContextualStorage storage = sessionContext.getContextualStorage(null, + true); + + assertNotNull(storage); + // With the lock held, lenient strategy avoids the extra wrap. + verify(session, never()).accessSynchronously(any(Command.class)); + // A second lookup must return the same storage instance. + assertSame(storage, sessionContext.getContextualStorage(null, false)); + } + + @Test + public void contextIsActive_whenInitNotCalled_returnsFalse() { + // Guards against NPE if isActive() is somehow invoked before + // VaadinExtension.initializeContexts has run. + SessionUnderTestContext context = new SessionUnderTestContext(); + context.activate(); + BeanManager beanManager = weld.select().select(BeanManager.class).get(); + VaadinSessionScopedContext bare = new VaadinSessionScopedContext( + beanManager); + assertFalse(bare.isActive()); + } + + private VaadinSessionScopedContext newContext() { + BeanManager beanManager = weld.select().select(BeanManager.class).get(); + VaadinSessionScopedContext sessionContext = new VaadinSessionScopedContext( + beanManager); + sessionContext.init(beanManager); + return sessionContext; + } + + private VaadinSessionScopedContext newContext( + ContextualStorageManager manager) { + BeanManager beanManager = weld.select().select(BeanManager.class).get(); + VaadinSessionScopedContext sessionContext = new VaadinSessionScopedContext( + beanManager); + try { + Field f = VaadinSessionScopedContext.class + .getDeclaredField("contextManager"); + f.setAccessible(true); + f.set(sessionContext, manager); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to install test manager", e); + } + return sessionContext; + } + + private ContextualStorageManager lookupManager() { + return BeanProvider.getContextualReference( + weld.select().select(BeanManager.class).get(), + ContextualStorageManager.class, false); + } + + private static M injectBeanManager( + M target) { + try { + Field f = ContextualStorageManager.class + .getDeclaredField("beanManager"); + f.setAccessible(true); + f.set(target, Mockito.mock(BeanManager.class)); + return target; + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to inject BeanManager", e); + } + } + + @VaadinSessionScoped + public static class SessionScopedTestBean extends TestBean { + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java new file mode 100644 index 00000000000..b0fb1d143cb --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/SessionUnderTestContext.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.enterprise.inject.spi.CDI; + +import java.util.Properties; + +import org.mockito.Mockito; + +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.VaadinSessionState; + +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.when; + +public class SessionUnderTestContext implements UnderTestContext { + + private VaadinSession session; + private static ServiceUnderTestContext serviceUnderTestContext; + + private void mockSession() { + if (serviceUnderTestContext == null) { + serviceUnderTestContext = new ServiceUnderTestContext( + CDI.current().getBeanManager()); + serviceUnderTestContext.activate(); + } + session = Mockito.mock(TestSession.class, + Mockito.withSettings().useConstructor()); + doCallRealMethod().when(session).setAttribute(Mockito.any(String.class), + Mockito.any()); + doCallRealMethod().when(session) + .getAttribute(Mockito.any(String.class)); + doCallRealMethod().when(session).getService(); + + when(session.getState()).thenReturn(VaadinSessionState.OPEN); + + when(session.hasLock()).thenReturn(true); + DeploymentConfiguration configuration = Mockito + .mock(DeploymentConfiguration.class); + when(session.getConfiguration()).thenReturn(configuration); + Properties props = new Properties(); + when(configuration.getInitParameters()).thenReturn(props); + + doCallRealMethod().when(session).addUI(Mockito.any()); + doCallRealMethod().when(session).getUIs(); + } + + @Override + public void activate() { + if (session == null) { + mockSession(); + } + VaadinSession.setCurrent(session); + } + + @Override + public void tearDownAll() { + VaadinSession.setCurrent(null); + if (serviceUnderTestContext != null) { + serviceUnderTestContext.tearDownAll(); + serviceUnderTestContext = null; + } + } + + @Override + public void destroy() { + VaadinSessionScopedContext.destroy(session); + } + + public VaadinSession getSession() { + return session; + } + + public static class TestSession extends VaadinSession { + + public TestSession() { + super(serviceUnderTestContext.getService()); + } + + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestBean.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestBean.java new file mode 100644 index 00000000000..6aaffa4d83b --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestBean.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +import java.util.concurrent.atomic.AtomicInteger; + +public class TestBean { + private static AtomicInteger beanCount = new AtomicInteger(); + private String state = ""; + + @PostConstruct + private void construct() { + beanCount.incrementAndGet(); + } + + @PreDestroy + private void destruct() { + beanCount.decrementAndGet(); + } + + public static int getBeanCount() { + return beanCount.get(); + } + + public static void resetCount() { + beanCount.set(0); + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java new file mode 100644 index 00000000000..28dc486e4bd --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/TestNavigationTarget.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; + +@Tag(Tag.A) +class TestNavigationTarget extends Component { + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java new file mode 100644 index 00000000000..68acf06e932 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UIUnderTestContext.java @@ -0,0 +1,83 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinSession; + +public class UIUnderTestContext implements UnderTestContext { + + private VaadinSession session; + private UI ui; + private static int uiIdNdx = 0; + private static SessionUnderTestContext sessionContextUnderTest; + + public UIUnderTestContext() { + this(null); + } + + protected UIUnderTestContext(VaadinSession session) { + this.session = session; + } + + private void mockUI() { + if (session == null) { + mockSession(); + } + + ui = new UI(); + ui.getInternals().setSession(session); + uiIdNdx++; + ui.doInit(null, uiIdNdx, "appId"); + } + + private void mockSession() { + if (sessionContextUnderTest == null) { + sessionContextUnderTest = new SessionUnderTestContext(); + sessionContextUnderTest.activate(); + } + session = sessionContextUnderTest.getSession(); + } + + @Override + public void activate() { + if (ui == null) { + mockUI(); + } + UI.setCurrent(ui); + } + + @Override + public void tearDownAll() { + UI.setCurrent(null); + uiIdNdx = 0; + if (sessionContextUnderTest != null) { + sessionContextUnderTest.tearDownAll(); + sessionContextUnderTest = null; + } + } + + @Override + public void destroy() { + ComponentUtil.onComponentDetach(ui); + } + + public UI getUi() { + return ui; + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextNormalTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextNormalTest.java new file mode 100644 index 00000000000..3f310701387 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextNormalTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import com.vaadin.cdi.annotation.NormalUIScoped; + +public class UiContextNormalTest extends + AbstractContextTest { + + @Override + protected Class getBeanType() { + return NormalUIScopedTestBean.class; + } + + @Override + protected UnderTestContext newContextUnderTest() { + return new UIUnderTestContext(); + } + + @Override + protected boolean isNormalScoped() { + return true; + } + + @NormalUIScoped + public static class NormalUIScopedTestBean extends TestBean { + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextPseudoTest.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextPseudoTest.java new file mode 100644 index 00000000000..726ad171c53 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UiContextPseudoTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +import com.vaadin.cdi.annotation.UIScoped; + +public class UiContextPseudoTest + extends AbstractContextTest { + + @Override + protected Class getBeanType() { + return UIScopedTestBean.class; + } + + @Override + protected UnderTestContext newContextUnderTest() { + return new UIUnderTestContext(); + } + + @Override + protected boolean isNormalScoped() { + return false; + } + + @UIScoped + public static class UIScopedTestBean extends TestBean { + } + +} diff --git a/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UnderTestContext.java b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UnderTestContext.java new file mode 100644 index 00000000000..f0678acd1d3 --- /dev/null +++ b/vaadin-cdi/src/test/java/com/vaadin/cdi/context/UnderTestContext.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2018 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.cdi.context; + +public interface UnderTestContext { + void activate(); + + void tearDownAll(); + + void destroy(); +} diff --git a/vaadin-cdi/src/test/resources/META-INF/VAADIN/config/flow-build-info.json b/vaadin-cdi/src/test/resources/META-INF/VAADIN/config/flow-build-info.json new file mode 100644 index 00000000000..fdc33d7c846 --- /dev/null +++ b/vaadin-cdi/src/test/resources/META-INF/VAADIN/config/flow-build-info.json @@ -0,0 +1,5 @@ +{ + "compatibilityMode": false, + "productionMode": false, + "enableDevServer": false +} diff --git a/vaadin-cdi/src/test/resources/META-INF/beans.xml b/vaadin-cdi/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000000..629a073c7df --- /dev/null +++ b/vaadin-cdi/src/test/resources/META-INF/beans.xml @@ -0,0 +1,12 @@ + + + + + com.vaadin.cdi.TestDeploymentValidator + + diff --git a/vaadin-cdi/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener b/vaadin-cdi/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener new file mode 100644 index 00000000000..8496e93f649 --- /dev/null +++ b/vaadin-cdi/src/test/resources/META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener @@ -0,0 +1 @@ +com.vaadin.cdi.JavaSPIVaadinServiceInitListener