diff --git a/.github/workflows/mvn-release-prepare-perform.yaml b/.github/workflows/mvn-release-prepare-perform.yaml index c858555..3a93fc1 100644 --- a/.github/workflows/mvn-release-prepare-perform.yaml +++ b/.github/workflows/mvn-release-prepare-perform.yaml @@ -24,24 +24,24 @@ jobs: steps: - id: 'checkout' name: 'Step: Check Out Project' - uses: 'actions/checkout@v4' + uses: 'actions/checkout@v6' with: fetch-depth: 1 persist-credentials: false - id: 'setup-java' name: 'Step: Set Up Java and Maven' - uses: 'actions/setup-java@v4' + uses: 'actions/setup-java@v5' with: cache: 'maven' distribution: 'temurin' gpg-passphrase: 'GPG_PASSPHRASE' gpg-private-key: '${{ secrets.GPG_PRIVATE_KEY }}' - java-version: '23' - mvn-toolchain-id: 'Temurin 23' + java-version: '25' + mvn-toolchain-id: 'Temurin 25' mvn-toolchain-vendor: 'openjdk' # see ../../pom.xml - server-id: 'sonatype-oss-repository-hosting' # see https://github.com/microbean/microbean-parent/blob/master/pom.xml#L38 - server-password: 'SONATYPE_OSSRH_PASSWORD' - server-username: 'SONATYPE_OSSRH_USERNAME' + server-id: 'central.sonatype.com' + server-password: 'CENTRAL_SONATYPE_COM_PASSWORD' + server-username: 'CENTRAL_SONATYPE_COM_USERNAME' - id: 'setup-askpass' name: 'Step: Set Up GIT_ASKPASS' run: | @@ -58,6 +58,8 @@ jobs: - id: 'mvn-release-prepare' name: 'Step: Maven Release: Prepare, Perform and Publish Site' env: + CENTRAL_SONATYPE_COM_PASSWORD: '${{ secrets.CENTRAL_SONATYPE_COM_PASSWORD }}' + CENTRAL_SONATYPE_COM_USERNAME: '${{ secrets.CENTRAL_SONATYPE_COM_USERNAME }}' DRY_RUN: '${{ inputs.dryRun }}' GIT_ASKPASS: '${{ runner.temp }}/.askpass' GPG_PASSPHRASE: '${{ secrets.GPG_PASSPHRASE }}' @@ -65,9 +67,6 @@ jobs: MVN_TRANSFER_LOGGING: ${{ inputs.mvnTransferLogging && '' || '--no-transfer-progress' }} PUSH_TOKEN : '${{ secrets.PUSH_TOKEN }}' # critical; see ${GIT_ASKPASS} file SCM_GIT_HTTPS_URL: 'scm:git:${{ github.server_url }}/${{ github.repository }}.git' - SONATYPE_OSSRH_PASSWORD: '${{ secrets.SONATYPE_OSSRH_PASSWORD }}' - SONATYPE_OSSRH_STAGING_PROFILE_ID: '${{ vars.SONATYPE_OSSRH_STAGING_PROFILE_ID }}' - SONATYPE_OSSRH_USERNAME: '${{ secrets.SONATYPE_OSSRH_USERNAME }}' shell: 'bash -e {0}' run: > git config --global user.email 'ci@microbean.org' @@ -75,12 +74,12 @@ jobs: git config --global user.name 'microbean' echo "::group::Running mvn prepare" - + ./mvnw --batch-mode ${MVN_DEBUG} --errors ${MVN_TRANSFER_LOGGING} release:prepare -DdryRun="${DRY_RUN}" -Darguments="${MVN_TRANSFER_LOGGING}" -Dscm.url="${SCM_GIT_HTTPS_URL}" - + scm_tag="$(grep '^scm.tag=' release.properties | cut -f 2 -d =)" echo "Prepared ${scm_tag}" >> "${GITHUB_STEP_SUMMARY}" @@ -90,37 +89,13 @@ jobs: echo "::endgroup::" echo "::group::Running mvn perform" - - set +e - { ./mvnw --batch-mode ${MVN_DEBUG} --errors ${MVN_TRANSFER_LOGGING} release:perform - -Darguments="${MVN_TRANSFER_LOGGING} -Dscmpublish.dryRun=${DRY_RUN} -Dscmpublish.pubScmUrl=${SCM_GIT_HTTPS_URL} -DskipTests -DstagingProfileId=${SONATYPE_OSSRH_STAGING_PROFILE_ID}" + -Darguments="${MVN_TRANSFER_LOGGING} -Dscmpublish.dryRun=${DRY_RUN} -Dscmpublish.pubScmUrl=${SCM_GIT_HTTPS_URL} -DskipTests -DautoPublish=true -DwaitUntil=published -DwaitMaxTime=3600" -DdryRun="${DRY_RUN}" -Dgoals="process-classes,post-site,scm-publish:publish-scm,deploy" -Dscm.url="${SCM_GIT_HTTPS_URL}" - | - tee /dev/fd/3 - | - grep --invert-match --silent 'Java class com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO' || cat > /dev/null - ; - } - 3>&1 - exit_codes=(${PIPESTATUS[@]}) + echo "Released ${scm_tag} successfully" >> "${GITHUB_STEP_SUMMARY}"; echo "::endgroup::" - - set -e - - if [ "${exit_codes[2]}" -ne 0 ] ; then - # grep "failed" (found com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO) and mvn failed - echo "Released ${scm_tag} successfully, but verify that the staging repository was successfully released" >> "${GITHUB_STEP_SUMMARY}"; - # Treat this as a successful run - exit 0; - elif [ "${exit_codes[0]}" -eq 0 ] ; then - # mvn succeeded and grep "succeeded" (did not find com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO) - echo "Released ${scm_tag} successfully" >> "${GITHUB_STEP_SUMMARY}"; - fi - - exit "${exit_codes[0]}" diff --git a/.github/workflows/mvn-verify.yaml b/.github/workflows/mvn-verify.yaml index a414901..faefc80 100644 --- a/.github/workflows/mvn-verify.yaml +++ b/.github/workflows/mvn-verify.yaml @@ -12,18 +12,18 @@ jobs: steps: - id: 'checkout' name: 'Step: Checkout' - uses: 'actions/checkout@v4' + uses: 'actions/checkout@v6' with: fetch-depth: 1 persist-credentials: false - id: 'setup-java' name: 'Step: Set Up Java and Maven' - uses: 'actions/setup-java@v4' + uses: 'actions/setup-java@v5' with: cache: 'maven' distribution: 'temurin' - java-version: '23' - mvn-toolchain-id: 'Temurin 23' + java-version: '25' + mvn-toolchain-id: 'Temurin 25' mvn-toolchain-vendor: 'openjdk' # see ../../pom.xml - id: 'mvn-verify' name: 'Step: Maven Verify' diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index bca10a6..2079886 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,20 +1,13 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at +# Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE +# file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 +# 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. +wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip - +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/README.md b/README.md index e648921..e267152 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@ microBean™ Event requires a Java runtime of version 23 or higher. # Installation -microBean™ Event is available on [Maven Central](https://search.maven.org/). Include microBean™ Event as a Maven +microBean™ Event is available on [Maven Central](https://search.maven.org/). Include microBean™ Event as a Maven dependency: ```xml org.microbean microbean-event - - 0.0.1 + + 0.0.2 ``` diff --git a/mvnw b/mvnw index 19529dd..bd8896b 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.2 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -105,14 +105,17 @@ trim() { printf "%s" "${1}" | tr -d '[:space:]' } +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -130,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index b150b91..5761d94 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,16 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" + +$MAVEN_M2_PATH = "$HOME/.m2" if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" } -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -134,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { diff --git a/pom.xml b/pom.xml index 2dfdda5..b6e5c07 100644 --- a/pom.xml +++ b/pom.xml @@ -52,20 +52,11 @@ - - sonatype-oss-repository-hosting - - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - Github Pages microBean™ Event Site https://microbean.github.io/microbean-event/ - - sonatype-oss-repository-hosting - https://oss.sonatype.org/content/repositories/snapshots - @@ -80,7 +71,7 @@ ${project.organization.name}. All rights reserved.]]> <a href="${project.url}" target="_top"><span style="font-family:Lobster, cursive;">µb</span> ${project.artifactId}</a> ${project.version} - https://microbean.github.io/microbean-assign/apidocs/,https://microbean.github.io/microbean-construct/apidocs/,https://microbean.github.io/microbean-qualifier/apidocs/ + https://microbean.github.io/microbean-assign/apidocs/,https://microbean.github.io/microbean-attributes/apidocs/,https://microbean.github.io/microbean-bean/apidocs/,https://microbean.github.io/microbean-construct/apidocs/,https://microbean.github.io/microbean-qualifier/apidocs/ 2 @@ -89,7 +80,6 @@ deployment [maven-release-plugin] [skip ci] v@{project.version} - false @@ -101,13 +91,6 @@ true false - - - true - - https://oss.sonatype.org/ - 10 - UTF8 UTF8 @@ -125,7 +108,16 @@ org.junit junit-bom - 5.11.3 + 6.0.1 + pom + import + + + + + org.jboss.weld + weld-core-bom + 6.0.3.Final pom import @@ -135,19 +127,25 @@ org.microbean microbean-assign - 0.0.1 + 0.0.10 org.microbean - microbean-construct - 0.0.9 + microbean-attributes + 0.0.5 org.microbean - microbean-qualifier - 0.2.4 + microbean-bean + 0.0.21 + + + + org.microbean + microbean-construct + 0.0.18 @@ -163,18 +161,30 @@ org.microbean - microbean-construct + microbean-attributes compile org.microbean - microbean-qualifier + microbean-bean + compile + + + + org.microbean + microbean-construct compile + + org.jboss.weld.se + weld-se-core + test + + org.junit.jupiter junit-jupiter-api @@ -200,11 +210,11 @@ maven-antrun-plugin - 3.1.0 + 3.2.0 maven-assembly-plugin - 3.7.1 + 3.8.0 maven-checkstyle-plugin @@ -314,13 +324,13 @@ com.puppycrawl.tools checkstyle - 10.12.6 + 12.3.0 maven-clean-plugin - 3.4.0 + 3.5.0 @@ -335,49 +345,41 @@ maven-compiler-plugin - 3.13.0 + 3.14.1 -Xlint:all -parameters - - - org.codehaus.plexus - plexus-java - 1.3.0 - - maven-dependency-plugin - 3.8.1 + 3.9.0 maven-deploy-plugin - 3.1.3 + 3.1.4 maven-enforcer-plugin - 3.5.0 + 3.6.2 maven-gpg-plugin - - 3.2.7 + 3.2.8 maven-install-plugin - 3.1.3 + 3.1.4 maven-jar-plugin - 3.4.2 + 3.5.0 maven-javadoc-plugin - 3.11.2 + 3.12.0 true @@ -405,20 +407,19 @@ maven-project-info-reports-plugin - 3.8.0 + 3.9.0 maven-release-plugin - - 3.1.1 + 3.3.1 maven-resources-plugin - 3.3.1 + 3.4.0 maven-scm-plugin - 2.1.0 + 2.2.1 maven-scm-publish-plugin @@ -430,7 +431,7 @@ maven-source-plugin - 3.3.1 + 3.4.0 attach-sources @@ -442,14 +443,7 @@ maven-surefire-plugin - 3.5.2 - - - org.apache.maven.surefire - surefire-junit-platform - 3.5.2 - - + 3.5.4 maven-toolchains-plugin @@ -458,35 +452,25 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.6.6 + 4.9.8.2 org.codehaus.mojo versions-maven-plugin - 2.18.0 + 2.20.1 io.smallrye jandex-maven-plugin - 3.2.3 + 3.5.3 - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 true - - - - com.thoughtworks.xstream - xstream - 1.4.20 - - - sonatype-oss-repository-hosting - ${nexusUrl} - ${autoReleaseAfterClose} + central.sonatype.com @@ -507,7 +491,7 @@ 23 - 3.9.9 + 3.9.12 @@ -532,8 +516,8 @@ - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a5ef64a..9d65a61 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -23,7 +23,9 @@ requires transitive java.compiler; requires transitive org.microbean.assign; + requires transitive org.microbean.attributes; + requires transitive org.microbean.bean; + requires org.microbean.constant; requires transitive org.microbean.construct; - requires transitive org.microbean.qualifier; } diff --git a/src/main/java/org/microbean/event/EventListener.java b/src/main/java/org/microbean/event/EventListener.java new file mode 100644 index 0000000..95e4a71 --- /dev/null +++ b/src/main/java/org/microbean/event/EventListener.java @@ -0,0 +1,145 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.util.SequencedSet; + +import javax.lang.model.element.Element; + +import org.microbean.assign.Aggregate; +import org.microbean.assign.AttributedElement; +import org.microbean.assign.AttributedType; +import org.microbean.assign.AttributedTyped; + +import org.microbean.bean.ReferencesSelector; + +/** + * An {@link java.util.EventListener EventListener}, an {@link Aggregate}, and an {@link AttributedTyped} that + * {@linkplain #eventReceived(Object, ReferencesSelector) receives} events. + * + * @param the result of reception; almost always {@link Void} + * + * @param the event type + * + * @author Laird Nelson + * + * @see Events + */ +// Used for receiving events. +// +// TODO: IntEventListener, BooleanEventListener, etc.? +// +// is almost always Void. +// is the event type. +// +// I know this looks like a Bean, but it is not. Specifically, the dependencies here are not thsoe required to create +// the EventListener, but those required to receive the event. +// +// Given: +// +// private void onEvent(@Observes /* or similar */ String e, @Complicated Frob f) {} +// +// * R is Void +// * E is String +// * dependencies() includes VariableElement-representing-f and nothing else +// * eventDependency() includes VariableElement-representing-e and nothing else +// * receive() calls onEvent() supplying it with event and a @Complicated Frob acquired via r +// +// TODO: Does this actually need to be an AttributedTyped? +public interface EventListener extends AttributedTyped, Aggregate, java.util.EventListener { + + /** + * Returns a non-{@code null}, determinate {@link AttributedType} describing the kinds of events this {@link + * EventListener} is prepared to handle. + * + *

The default implementation of this method extracts this information from an invocation of the {@link + * #eventDependency()} method ({@linkplain #eventDependency() q.v.}).

+ * + * @return a non-{@code null}, determinate {@link AttributedType} + * + * @exception NullPointerException if the default implementation of this method receives a {@code null} return value + * from an invocation of the {@link #eventDependency()} method + * + * @see #eventDependency() + */ + @Override // AttributedTypedAggregate (AttributedTyped) + public default AttributedType attributedType() { + return this.eventDependency().attributedType(); + } + + /** + * Returns a non-{@code null}, determinate, immutable {@link SequencedSet} of {@link AttributedElement}s representing + * dependencies this {@link EventListener} has that must be resolved before any invocation of the {@link + * #eventReceived(Object, ReferencesSelector)} method may properly occur. + * + *

Implementations of this method must not include a result of any invocation of the {@link #eventDependency()} + * method as an element of the return value.

+ * + *

The default implementation of this method returns an {@linkplain SequencedSet#isEmpty() empty} {@link + * SequencedSet}. Overrides are expected.

+ * + * @return a non-{@code null}, determinate, immutable {@link SequencedSet} of {@link AttributedElement}s + * + * @see #eventReceived(Object, ReferencesSelector) + * + * @see #eventDependency() + */ + // Returns dependencies that are not the event dependency. These are resolved by the system. Think of an observer + // method with an observed parameter and other parameters. The other parameters are these dependencies. + @Override // AttributedTypedAggregate (Aggregate) + public default SequencedSet dependencies() { + return Aggregate.super.dependencies(); + } + + /** + * Returns a non-{@code null}, determinate {@link AttributedElement} representing the program element for which an + * event is destined. + * + *

The result of an invocation of this method must not appear as an element of the return value of an invocation of + * the {@link #dependencies()} method.

+ * + *

Note that the default implementation of the {@link #attributedType()} method calls this method and requires it + * to return a non-{@code null} value.

+ * + * @return a non-{@code null}, determinate {@link AttributedElement} + * + * @see #dependencies() + */ + // An AttributedElement describing the event event "slot" (the observed parameter). + // Conceptually just another dependency (see dependencies()) but it is supplied by the user, not the system. + // Normally a method parameter. + // T's argument (the event event type) must be among its types. + public AttributedElement eventDependency(); + + /** + * Receives and handles an event, normally as delivered by an invocation of the {@link Events#fire(TypeMirror, List, + * Object, ReferencesSelector)} method, returning the result (which is often {@link Void} ({@code null})). + * + * @param event the event; must not be {@code null} + * + * @param r a {@link ReferencesSelector} typically used to {@linkplain #assign(java.util.Function) assign} {@linkplain + * #dependencies() dependencies}; must not be {@code null} + * + * @return the result of reception, which may be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + */ + // R must be compatible with attributedType(). + // Implementation must arrange for all dependencies() to be assigned, probably using r. + // Implementation must arrange for event to be assigned to eventDependency(), whatever that might mean. (Normally a method call.) + // Implementation must return the result of reception (usually Void, so null). + // When invoked by the system, r should probably also be an AutoCloseableRegistry and an AutoCloseable. + public R eventReceived(final E event, final ReferencesSelector r); + +} diff --git a/src/main/java/org/microbean/event/EventQualifiersMatcher.java b/src/main/java/org/microbean/event/EventQualifiersMatcher.java index b1c1408..7e28042 100644 --- a/src/main/java/org/microbean/event/EventQualifiersMatcher.java +++ b/src/main/java/org/microbean/event/EventQualifiersMatcher.java @@ -17,11 +17,9 @@ import org.microbean.assign.Matcher; -import org.microbean.qualifier.NamedAttributeMap; +import org.microbean.attributes.Attributes; import static org.microbean.assign.Qualifiers.anyQualifier; -import static org.microbean.assign.Qualifiers.defaultQualifier; -import static org.microbean.assign.Qualifiers.defaultQualifiers; import static org.microbean.assign.Qualifiers.qualifiers; /** @@ -33,8 +31,7 @@ * * @see #test(Collection, Collection) */ -public final class EventQualifiersMatcher - implements Matcher>, Collection>> { +public final class EventQualifiersMatcher implements Matcher, Collection> { /* @@ -62,9 +59,9 @@ public EventQualifiersMatcher() { * payloadAttributes} {@linkplain Collection#containsAll(Collection) contains all} of the {@linkplain * org.microbean.assign.Qualifiers#qualifiers(Collection) qualifiers present} in {@code receiverAttributes}. * - * @param receiverAttributes a {@link Collection} of {@link NamedAttributeMap}s; must not be {@code null} + * @param receiverAttributes a {@link Collection} of {@link Attributes} instances; must not be {@code null} * - * @param payloadAttributes a {@link Collection} of {@link NamedAttributeMap}s; must not be {@code null} + * @param payloadAttributes a {@link Collection} of {@link Attributes} instances; must not be {@code null} * * @return {@code true} if and only if either the {@linkplain org.microbean.assign.Qualifiers#qualifiers(Collection) * qualifiers present} in {@code receiverAttributes} are {@linkplain Collection#isEmpty() empty}, or if the collection @@ -75,11 +72,11 @@ public EventQualifiersMatcher() { * @exception NullPointerException if either argument is {@code null} */ @Override // Matcher>, Collection>> - public final boolean test(final Collection> receiverAttributes, - final Collection> payloadAttributes) { + public final boolean test(final Collection receiverAttributes, + final Collection payloadAttributes) { // "An event is delivered to an observer method if...the observer method has no event qualifiers or has a subset of // the event qualifiers." - final Collection> receiverQualifiers = qualifiers(receiverAttributes); + final Collection receiverQualifiers = qualifiers(receiverAttributes); return receiverQualifiers.isEmpty() || qualifiers(payloadAttributes).containsAll(receiverQualifiers); } diff --git a/src/main/java/org/microbean/event/EventTypeList.java b/src/main/java/org/microbean/event/EventTypeList.java new file mode 100644 index 0000000..8538a21 --- /dev/null +++ b/src/main/java/org/microbean/event/EventTypeList.java @@ -0,0 +1,56 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.util.AbstractList; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.assign.SupertypeList; + +import static java.util.Objects.requireNonNull; + +/** + * An immutable {@link AbstractList} of {@link TypeMirror}s that contains only {@linkplain + * EventTypes#legalEventType(TypeMirror) legal event types}, sorted in a specific manner. + * + *

Note: Two {@link TypeMirror} instances may represent the {@linkplain + * org.microbean.construct.Domain#sameType(TypeMirror, TypeMirror) same type} while not being {@linkplain + * TypeMirror#equals(Object) equal to} one another. {@link java.util.List} implementations such as this one that contain + * {@link TypeMirror} elements may also represent the same types without being equal to each other.

+ * + * @author Laird Nelson + */ +public final class EventTypeList extends AbstractList { + + static final EventTypeList EMPTY_LIST = new EventTypeList(SupertypeList.of()); + + private final SupertypeList types; + + EventTypeList(final SupertypeList types) { + super(); + this.types = requireNonNull(types, "types"); + } + + @Override // AbstractList + public final TypeMirror get(final int index) { + return this.types.get(index); + } + + @Override // AbstractList + public final int size() { + return this.types.size(); + } + +} diff --git a/src/main/java/org/microbean/event/EventTypeMatcher.java b/src/main/java/org/microbean/event/EventTypeMatcher.java index 626d420..dc5487b 100644 --- a/src/main/java/org/microbean/event/EventTypeMatcher.java +++ b/src/main/java/org/microbean/event/EventTypeMatcher.java @@ -98,7 +98,7 @@ public final boolean test(final TypeMirror receiver, final TypeMirror payload) { // Interestingly array types are never discussed explicitly in the specification's sections on observer // resolution, but clearly they must be possible. case ARRAY -> switch (payload.getKind()) { - case ARRAY -> this.identical(this.domain().elementType(receiver), this.domain().elementType(payload)); // never spelled out in the spec but inferred + case ARRAY -> this.test(((ArrayType)receiver).getComponentType(), ((ArrayType)payload).getComponentType()); // never spelled out in the spec but inferred case DECLARED -> false; default -> throw illegalPayload(payload); }; @@ -262,7 +262,7 @@ private final boolean assignable(final DeclaredType receiver, final DeclaredType }; // end switch(receiver); end parameterizedPayload case case DeclaredType nonGenericOrRawPayload -> switch (receiver) { - // "A [non-generic or] raw event type [nonGenericOrRawayload] is considered assignable..." + // "A [non-generic or] raw event type [nonGenericOrRawPayload] is considered assignable..." case DeclaredType parameterizedReceiver when this.domain().parameterized(receiver) -> { // "...to a parameterized observed event type [parameterizedReceiver] if the[ir] [non-generic classes or] raw diff --git a/src/main/java/org/microbean/event/EventTypes.java b/src/main/java/org/microbean/event/EventTypes.java index ecafb3f..cee906e 100644 --- a/src/main/java/org/microbean/event/EventTypes.java +++ b/src/main/java/org/microbean/event/EventTypes.java @@ -15,31 +15,51 @@ import java.lang.System.Logger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; +import org.microbean.assign.SupertypeList; import org.microbean.assign.Types; import org.microbean.construct.Domain; +import static javax.lang.model.element.Modifier.ABSTRACT; + import static java.lang.System.Logger.Level.WARNING; +import static java.util.HashMap.newHashMap; + +import static java.util.Objects.requireNonNull; + +import static javax.lang.model.type.TypeKind.ARRAY; +import static javax.lang.model.type.TypeKind.BOOLEAN; +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.TYPEVAR; +import static javax.lang.model.type.TypeKind.VOID; +import static javax.lang.model.type.TypeKind.WILDCARD; + /** * A utility for working with event types. * * @author Laird Nelson * - * @see #eventTypes(TypeMirror) + * @see #eventTypes(TypeMirror, Object) * * @see #legalEventType(TypeMirror) */ @@ -77,39 +97,312 @@ public EventTypes(final Domain domain) { /** - * Returns an immutable {@link List} of {@linkplain #legalEventType(TypeMirror) legal event types} that the supplied + * Returns an {@link EventTypeList} of {@linkplain #legalEventType(TypeMirror) legal event types} that the supplied * {@link TypeMirror} bears. * - *

The returned {@link List} may be empty.

+ *

The returned {@link EventTypeList} may be {@linkplain EventTypeList#isEmpty() empty}.

* - * @param t a {@link TypeMirror}; must not be {@code null} - * - * @return an immutable {@link List} of {@linkplain #legalEventType(TypeMirror) legal event types} that the supplied - * {@link TypeMirror} bears; never {@code null} + * @param typeArgumentSource a {@link TypeMirror} used for {@index "event type argument inference"}; must + * not be {@code null}; must {@linkplain TypeMirror#getKind() have a kind} of either {@link TypeKind#ARRAY} or {@link + * TypeKind#DECLARED} * - * @exception NullPointerException if {@code t} is {@code null} + * @param event the event object whose event types should be returned; must not be {@code null} * - * @microbean.nullability This method never returns {@code null}. + * @return an {@link EventTypeList}; never {@code null} * - * @microbean.idempotency This method is idempotent and returns determinate values. + * @exception NullPointerException if any argument is {@code null} * - * @microbean.threadsafety This method is safe for concurrent use by multiple threads. + * @exception IllegalArgumentException if {@code typeArgumentSource} is not legal */ - public final List eventTypes(final TypeMirror t) { - // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#event_types_and_qualifier_types - if (t.getKind() == TypeKind.DECLARED) { - final Element e = ((DeclaredType)t).asElement(); - if (e.getKind().isInterface() || !e.getKind().isClass() || e.getModifiers().contains(Modifier.ABSTRACT)) { - // "An event object is an instance of a concrete Java class...." - if (LOGGER.isLoggable(WARNING)) { - LOGGER.log(WARNING, t + " is an illegal event type"); + public final EventTypeList eventTypes(final TypeMirror typeArgumentSource, final Object event) { + return new EventTypeList(this.supertypes(this.eventType(typeArgumentSource, event.getClass()), EventTypes::legalEventType)); + } + + // This MUST NOT become public. + final TypeMirror eventType(final TypeMirror typeArgumentSource, final Class eventClass) { + if (eventClass == void.class) { + // Optimization: void.class is never the answer. + throw new IllegalArgumentException("eventClass: " + eventClass); + } + if (eventClass.isPrimitive()) { + // Optimization: if it is primitive, there's no further work to be done. + return this.domain().primitiveType(eventClass.getCanonicalName()); + } + int dimensions = 0; + Class c = eventClass; + while (c.isArray()) { + c = c.getComponentType(); + ++dimensions; + } + final Domain d = this.domain(); + TypeMirror t = + this.eventType(typeArgumentSource, + c.isPrimitive() ? d.primitiveType(c.getCanonicalName()) : d.declaredType(d.typeElement(c.getCanonicalName()))); + for (int i = 0; i < dimensions; i++) { + t = d.arrayTypeOf(t); + } + return t; + } + + // This MUST NOT become public. + private final TypeMirror eventType(final TypeMirror typeArgumentSource, final TypeMirror eventType) { + // + // Given: + // + // class Sup {} + // class Sub extends Sup {} + // class Sub2 extends Sup {} + // + // +-----------------------+-------------+--------------------------+---------------------------------------------------+ + // | typeArgumentSource | eventType | return value/exception | notes | + // +-----+-----------------------+-------------+--------------------------+---------------------------------------------------+ + // | 1. | N/A | int | int | (typeArgumentSource not checked or used) | + // | 2. | N/A | int[] | int[] | (typeArgumentSource not checked or used) | + // | 3. | N/A | Object | Object | (typeArgumentSource not checked or used) | + // | 4. | N/A | Object[] | Object[] | (typeArgumentSource not checked or used) | + // | 5. | int | ArrayList | IllegalArgumentException | (typeArgumentSource cannot supply type arguments) | + // | 6. | int[] | ArrayList | IllegalArgumentException | (typeArgumentSource cannot supply type arguments) | + // | 7. | List | ArrayList | IllegalArgumentException | (typeArgumentSource cannot supply type arguments) | + // | 8. | List[] | ArrayList | IllegalArgumentException | (typeArgumentSource cannot supply type arguments) | + // | 9. | AbstractList | ArrayList | ArrayList | | + // | 10. | List[] | ArrayList[] | ArrayList[] | | + // | 11. | List[] | ArrayList | ArrayList | (typeArgumentSource dimensions don't matter) | + // | 12. | List | ArrayList | IllegalArgumentException | (type arguments must be declared or array types) | + // | 13. | List[] | ArrayList | IllegalArgumentException | (type arguments must be declared or array types) | + // | 14. | List | ArrayList | IllegalArgumentException | (type arguments must be declared or array types) | + // | 15. | List[] | ArrayList | IllegalArgumentException | (type arguments must be declared or array types) | + // | 16. | Map | ArrayList | IllegalArgumentException | (typeArgumentSource is not a supertype) | + // | 17. | Sup | Sub | Sub | | + // | 18. | Sup | Sub2 | Sub2 | | + // +-----+-----------------------+-------------+--------------------------+---------------------------------------------------+ + // + return switch (eventType.getKind()) { + case ARRAY -> { + final Domain d = this.domain(); + int dimensions = 1; + TypeMirror t = ((ArrayType)eventType).getComponentType(); + while (t.getKind() == ARRAY) { + t = ((ArrayType)t).getComponentType(); + ++dimensions; + } + yield switch (t.getKind()) { + case ARRAY -> throw new AssertionError(); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> eventType; // Case 2 above + case DECLARED -> { + t = this.eventType(typeArgumentSource, (DeclaredType)t); // see below + assert t.getKind() == DECLARED; + for (int i = 0; i < dimensions; i++) { + t = d.arrayTypeOf(t); } - return List.of(); + yield t; } + default -> throw new IllegalArgumentException("eventType: " + eventType); + }; } - // "The event types of the event include all superclasses and interfaces of the [concrete] runtime class of the - // event object." - return this.supertypes(t, EventTypes::legalEventType); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> eventType; // Case 1 above + case DECLARED -> this.eventType(typeArgumentSource, (DeclaredType)eventType); // see below + default -> throw new IllegalArgumentException("eventType: " + eventType); + }; + } + + private final TypeMirror eventType(TypeMirror typeArgumentSource, final DeclaredType eventType) { + if (eventType.getKind() != DECLARED) { + throw new IllegalArgumentException("eventType: " + eventType); + } + + final TypeElement eventTypeElement = (TypeElement)eventType.asElement(); + final int typeParametersCount = eventTypeElement.getTypeParameters().size(); + if (typeParametersCount <= 0) { + // Non-generic declared type. Case 3 above (and 4 by extension). + return eventType; + } + + // Optimization: if all the event type type arguments are already of kind ARRAY or DECLARED or WILDCARD, then we're + // done. Otherwise there is at least one type argument that has to be inferred. + final List eventTypeTypeArguments = eventType.getTypeArguments(); + OPTIMIZATION: + if (!eventTypeTypeArguments.isEmpty()) { + for (final TypeMirror ta : eventTypeTypeArguments) { + switch (ta.getKind()) { + case TYPEVAR: + // Optimization does not apply; break out of the OPTIMIZATION block entirely + break OPTIMIZATION; + case ARRAY: + case DECLARED: + case WILDCARD: + break; // go to the next for loop iteration + default: + throw new AssertionError("ta: " + ta); + } + } + return eventType; + } + + // "De-arrayize" the specified type; whether it is an array or not does not matter. We only care about its element + // type since that is the source of inferred/replacement type arguments. + final TypeMirror originalTypeArgumentSource = typeArgumentSource; + while (typeArgumentSource.getKind() == ARRAY) { + typeArgumentSource = ((ArrayType)typeArgumentSource).getComponentType(); + } + if (typeArgumentSource.getKind() != DECLARED) { + throw new IllegalArgumentException("typeArgumentSource: " + originalTypeArgumentSource + "; eventType: " + eventType); + } + + final TypeElement typeArgumentSourceElement = (TypeElement)((DeclaredType)typeArgumentSource).asElement(); + + // The event type is a generic declared type. As we begin, it is important in what follows to use the prototypical + // type, not a raw type. The prototypical type will ensure that type parameter arguments (type variables) will be + // "propagated" up the inheritance hierarchy. + // + // (The concept of a prototypical type is not defined in the Java Language Specification but only in the Java + // language model specification. See + // https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/lang/model/element/TypeElement.html#prototypicaltype.) + // + // For example, suppose the event type is ArrayList. We "go up" to its element via asElement(), yielding the + // TypeElement ArrayList. Then we "go down" using its asType() argument, which does _not_ yield ArrayList, + // but, rather, ArrayList. ArrayList is the prototypical type defined by the TypeElement ArrayList. The + // getSuperclass() method on the ArrayList TypeElement "propagates" E-defined-by-ArrayList "up" to AbstractList, + // such that the E in AbstractList simply is the E-defined-by-ArrayList. + final DeclaredType eventPrototypicalType = (DeclaredType)eventTypeElement.asType(); + assert !eventPrototypicalType.getTypeArguments().isEmpty(); + assert allAreTypeVariables(eventPrototypicalType.getTypeArguments()); + + // Given, e.g: + // + // class Sup {} + // class Sub extends Sup {} + // + // ...and: + // + // Sup // specified type + // + // ...we want to (ultimately) yield Sub. + // + // Visually, in what follows, the "right hand side" is the event type hierarchy starting from the prototypical event + // type. The "left hand side" is specified type usage. + // + // We need to substitute type arguments from the "left hand side" appropriately into the "right hand side". + + // First, get the "congruent supertype" (a term I made up). Given Sub (the prototypical event type), yield + // Sup (not Sup). It is congruent with Sup in some fashion. + final DeclaredType rhs = congruentSupertype(typeArgumentSourceElement, eventPrototypicalType); + if (rhs == null) { + // typeArgumentSource and eventType were distinct types. We COULD take type arguments from the specified type but that + // would ultimately be very confusing and almost certainly reflects programmer error. + throw new IllegalArgumentException("typeArgumentSource: " + originalTypeArgumentSource + "; eventType: " + eventType); + } else if (rhs == eventPrototypicalType) { + // Optimization: + // + // The contract of congruentSupertype() says that when the return value is identical to the second argument then + // no substitution was necessary. If no substitution was necessary, then just return what we already know. + return eventType; + } + + // B, A in Sup + final List rhsTypeArguments = rhs.getTypeArguments(); + assert allAreTypeVariables(rhsTypeArguments) : "rhsTypeArguments: " + rhsTypeArguments; + + // Sup in Sup + final TypeElement rhsTypeElement = (TypeElement)rhs.asElement(); + + // Now we have the proper left and right hand sides suitable for type argument substitution (Sup and + // Sup respectively). + + // String, Object in Sup + final List lhsTypeArguments = ((DeclaredType)typeArgumentSource).getTypeArguments(); + + int size = lhsTypeArguments.size(); + if (size <= 0) { + // lhs was non-generic or raw and hence is incapable of supplying (needed) type arguments. + throw new IllegalArgumentException("typeArgumentSource: " + originalTypeArgumentSource + "; eventType: " + eventType); + } + + // This Map will hold entries mapping, e.g., B to String and A to Object. For visualization purposes, this + // "connects" the left hand side to the right hand side. The map will have a size that is guaranteed to be exactly + // equal to the number of type parameters. + final Map m = newHashMap(size); + + for (int i = 0; i < size; i++) { + + // For every left hand side type argument... + + // On the left hand side: + // + // Sup + // + + // String, Object + final TypeMirror lhsTypeArgument = lhsTypeArguments.get(i); + switch (lhsTypeArgument.getKind()) { + case ARRAY: + case DECLARED: + case WILDCARD: + break; + default: + throw new IllegalArgumentException("typeArgumentSource: " + typeArgumentSource); + } + + // On the right hand side: + // + // Sup + + // B, A + final TypeMirror rhsTypeArgument = rhsTypeArguments.get(i); + switch (rhsTypeArgument.getKind()) { + case ARRAY: + case DECLARED: + // This argument is already taken care of. We're interested only in replacing type variables with + // non-type-variable reference types. + continue; + case TYPEVAR: + break; + case WILDCARD: + // (Don't have to handle wildcards because we're working with the prototypical type hierarchy on the right hand + // side which will never have them.) + // fall through + default: + // Other type kinds are not reference type kinds. + throw new AssertionError(); + } + final TypeVariable rhsTypeVariable = (TypeVariable)rhsTypeArgument; + + // Assert that B and A in Sup are type variables defined by Sub. (This is why using the prototypical + // type was important; see above.) + assert + ((TypeParameterElement)rhsTypeVariable.asElement()).getGenericElement().equals(eventPrototypicalType.asElement()) : + "rhsTypeVariable.asElement(): " + rhsTypeVariable.asElement() + + "; ((TypeParameterElement)rhsTypeVariable.asElement()).getGenericElement(): " + ((TypeParameterElement)rhsTypeVariable.asElement()).getGenericElement() + + "; eventPrototypicalElementType.asElement(): " + eventPrototypicalType.asElement(); + + // B in Sup := String + // A in Sup := Object + m.put(rhsTypeVariable, lhsTypeArgument); + + } + + if (m.isEmpty()) { + // No entries in the map means no substitution was performed for whatever reason, so return the event type unchanged. + return eventType; + } + + // Use the proper type arguments we stored in the Map; e.g. use Object for A (S) and String for B (T). + + // Use the type parameters from the left hand side type element, because that may represent a supertype. The + // substitutions we stored line up one for one with these type parameters. + // + // For example, consider a supertype (left hand side) of AbstractMap, a usage of it (AbstractMap), and a subtype of class SpecialMap extends AbstractMap. + + final List tpes = eventTypeElement.getTypeParameters(); + size = tpes.size(); + final List tas = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + final TypeMirror ta = m.get(tpes.get(i).asType()); + assert ta != null : "tpe: " + tpes.get(i) + "; tpe.asType(): " + tpes.get(i).asType() + "; tpe.getGenericElement(): " + tpes.get(i).getGenericElement() + "; m: " + m; + tas.add(ta); + } + assert !tas.isEmpty() : "tas: " + tas; + return this.domain().declaredType(eventTypeElement, tas.toArray(new TypeMirror[0])); } @@ -118,6 +411,15 @@ public final List eventTypes(final TypeMirror t) { */ + private static final boolean allAreTypeVariables(final Iterable i) { + for (final TypeMirror t : i) { + if (t.getKind() != TYPEVAR) { + return false; + } + } + return true; + } + /** * Returns {@code true} if and only if the supplied {@link TypeMirror} is a legal event type. * @@ -125,17 +427,16 @@ public final List eventTypes(final TypeMirror t) { * *
    * - *
  1. {@linkplain TypeKind#ARRAY Array} types whose {@linkplain ArrayType#getComponentType() component type}s are - * legal event types
  2. + *
  3. {@linkplain javax.lang.model.type.TypeKind#ARRAY Array} types whose {@linkplain ArrayType#getComponentType() + * component type}s are primitive types or legal event types
  4. * - *
  5. {@linkplain TypeKind#DECLARED Declared} types that contain no {@linkplain TypeKind#WILDCARD wildcard type}s for - * every level of containment
  6. + *
  7. {@linkplain javax.lang.model.type.TypeKind#DECLARED Declared} types that do not refer to type variables
  8. * *
* * @param t a {@link TypeMirror}; must not be {@code null} * - * @return {@code true} if and only if {@code t} is a legal bean type; {@code false} otherwise + * @return {@code true} if and only if {@code t} is a legal event type; {@code false} otherwise * * @exception NullPointerException if {@code t} is {@code null} * @@ -147,45 +448,69 @@ public final List eventTypes(final TypeMirror t) { public static final boolean legalEventType(final TypeMirror t) { // https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0#event_types_and_qualifier_types return switch (t.getKind()) { + case ARRAY -> { - // Recurse into the component type. - if (!legalEventType(((ArrayType)t).getComponentType())) { // note recursion + final TypeMirror componentType = ((ArrayType)t).getComponentType(); + if (!componentType.getKind().isPrimitive() && !legalEventType(componentType)) { if (LOGGER.isLoggable(WARNING)) { - LOGGER.log(WARNING, t + " has a component type that is an illegal event type (" + ((ArrayType)t).getComponentType()); + LOGGER.log(WARNING, t + " has a component type that is an illegal event type (" + componentType); } yield false; } yield true; } - // You can't fire a primitive event as of this writing, but there's nothing stopping a primitive event type from - // being legal otherwise. + // As of this writing, you can't fire a primitive event in CDI, because the fire() method takes an Object, but + // there's nothing stopping a primitive event type from being legal otherwise (you can envision a fire(int) method, + // for example). Consequently, this may end up being uncommented later. + // // case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> true; case DECLARED -> { - // "An event type may not contain an unresolvable type variable. A wildcard type is not considered an unresolvable - // type variable." + // "An event type may not contain an unresolvable [sic] type variable. A wildcard type is not considered an + // unresolvable type variable." // - // We interpret "contain" to mean "have as a type argument, recursively, anywhere". + // We interpret "contain" to mean "have as a type argument, or as a wildcard bound, recursively, anywhere". for (final TypeMirror ta : ((DeclaredType)t).getTypeArguments()) { - if (ta.getKind() != TypeKind.WILDCARD && !legalEventType(ta)) { // note recursion - if (LOGGER.isLoggable(WARNING)) { - LOGGER.log(WARNING, t + " has a type argument that is an illegal event type (" + ta + ")"); + switch (ta.getKind()) { + case WILDCARD: + final WildcardType wta = (WildcardType)ta; + TypeMirror b = wta.getExtendsBound(); + if (b != null && !legalEventType(b)) { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, t + " has a type argument that is an illegal event type (" + ta + ")"); + } + yield false; + } + b = wta.getSuperBound(); + if (b != null && !legalEventType(b)) { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, t + " has a type argument that is an illegal event type (" + ta + ")"); + } + yield false; + } + break; + default: + if (!legalEventType(ta)) { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, t + " has a type argument that is an illegal event type (" + ta + ")"); + } + yield false; } - yield false; + break; } } yield true; } default -> { + // (Including, of course, TYPEVAR.) if (LOGGER.isLoggable(WARNING)) { LOGGER.log(WARNING, t + " is an illegal event type"); } yield false; } }; - } /** @@ -215,4 +540,83 @@ public static final boolean legalObservedEventType(final TypeMirror t) { }; } + private static final DeclaredType congruentSupertype(final TypeElement lhsTypeElement, DeclaredType subtype) { + if (subtype.getKind() != DECLARED) { + throw new IllegalArgumentException("subtype: " + subtype); + } + if (lhsTypeElement == null) { + return subtype; + } + // Make sure the subtype is the prototypical subtype. Very important. + TypeElement subtypeTypeElement = (TypeElement)subtype.asElement(); + if (subtype.getTypeArguments().isEmpty()) { + subtype = (DeclaredType)subtypeTypeElement.asType(); + } + TypeElement rhsTypeElement = subtypeTypeElement; + DeclaredType rhs = subtype; + while (rhsTypeElement != null && !lhsTypeElement.equals(rhsTypeElement)) { + final TypeMirror rhsSuperclass = rhsTypeElement.getSuperclass(); + switch (rhsSuperclass.getKind()) { + case NONE: + rhs = null; + rhsTypeElement = null; // no superclass means it was Object + break; + case DECLARED: + rhs = (DeclaredType)rhsSuperclass; // extremely important + rhsTypeElement = (TypeElement)rhs.asElement(); + break; + } + } + if (rhsTypeElement == null) { + final Set seen = new HashSet<>(); + final List interfaces = new LinkedList<>(); + for (final TypeMirror iface : subtypeTypeElement.getInterfaces()) { + if (seen.add((TypeElement)((DeclaredType)iface).asElement())) { + interfaces.add(iface); + } + } + INTERFACES_LOOP: + while (!interfaces.isEmpty()) { + final DeclaredType iface = (DeclaredType)interfaces.removeFirst(); + final TypeElement ifaceTypeElement = (TypeElement)iface.asElement(); + if (lhsTypeElement.equals(ifaceTypeElement)) { + // we're done + rhs = iface; // extremely important + break INTERFACES_LOOP; + } + for (final TypeMirror superinterface : ifaceTypeElement.getInterfaces()) { + if (seen.add((TypeElement)((DeclaredType)superinterface).asElement())) { + interfaces.add(superinterface); + } + } + } + } + return rhs; + } + + /* + public static final class EventTypeList extends AbstractList { + + private static final EventTypeList EMPTY_LIST = new EventTypeList(SupertypeList.of()); + + private final SupertypeList types; + + private EventTypeList(final SupertypeList types) { + super(); + this.types = requireNonNull(types, "types"); + } + + @Override // AbstractList + public final TypeMirror get(final int index) { + return this.types.get(index); + } + + @Override // AbstractList + public final int size() { + return this.types.size(); + } + + } + */ + } diff --git a/src/main/java/org/microbean/event/Events.java b/src/main/java/org/microbean/event/Events.java new file mode 100644 index 0000000..3b313d5 --- /dev/null +++ b/src/main/java/org/microbean/event/Events.java @@ -0,0 +1,158 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.util.Iterator; +import java.util.List; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.assign.AttributedElement; +import org.microbean.assign.AttributedType; + +import org.microbean.attributes.Attributes; + +import org.microbean.bean.ReferencesSelector; + +import org.microbean.construct.Domain; + +import static java.util.Objects.requireNonNull; + +import static org.microbean.assign.Qualifiers.anyQualifiers; +import static org.microbean.assign.Qualifiers.qualifiers; + +/** + * A utility class for working with events. + * + * @author Laird Nelson + */ +// Deliberately not final. +public class Events { + + private final EventTypes eventTypes; + + private final EventTypeMatcher eventTypeMatcher; + + private final EventQualifiersMatcher eventQualifiersMatcher; + + private final AttributedType eventListenerAttributedType; + + /** + * Creates a new {@link Events}. + * + * @param eventTypes an {@link EventTypes}; must not be {@code null} + * + * @param eventTypeMatcher an {@link EventTypeMatcher}; must not be {@code null} + * + * @param eventQualifiersMatcher an {@link EventQualifiersMatcher}; must not be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + */ + public Events(final EventTypes eventTypes, + final EventTypeMatcher eventTypeMatcher, + final EventQualifiersMatcher eventQualifiersMatcher) { + super(); + this.eventTypes = requireNonNull(eventTypes, "eventTypes"); + this.eventTypeMatcher = requireNonNull(eventTypeMatcher, "eventTypeMatcher"); + this.eventQualifiersMatcher = requireNonNull(eventQualifiersMatcher, "eventQualifiersMatcher"); + final Domain d = eventTypes.domain(); + this.eventListenerAttributedType = + new AttributedType(d.declaredType(d.typeElement(EventListener.class.getCanonicalName()), + d.wildcardType(), + d.wildcardType(null, d.javaLangObjectType())), + anyQualifiers()); + } + + /** + * Delivers ("fires") the supplied {@code event} to suitable {@link EventListener}s. + * + *

A suitable {@link EventListener} is one whose {@link EventListener#attributedType()} method returns an {@link + * AttributedType} + * + * @param typeArgumentSource handwave here about the specified type and type argument substitution + * + * @param attributes a {@link List} of {@link Attributes} qualifying the event; must not be {@code null} + * + * @param event the event; must not be {@code null} + * + * @param rs a {@link ReferencesSelector}; used to find {@link EventListener EventListener<?, ?>} references; must not be + * {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @exception IllegalArgumentException if {@code typeArgumentSource} is unsuitable + * + * @see #fire(EventListener, Object, ReferencesSelector) + * + * @see EventTypes#eventTypes(TypeMirror, Object) + */ + // Deliberately final. + public final void fire(final TypeMirror typeArgumentSource, + final List attributes, + final Object event, + final ReferencesSelector rs) { + final EventTypeList eventTypes = this.eventTypes.eventTypes(typeArgumentSource, event); + final List eventQualifiers = qualifiers(attributes); + final Iterator> i = + rs.>references(this.eventListenerAttributedType).iterator(); + while (i.hasNext()) { + final EventListener el = i.next(); + try { + final AttributedType slot = el.attributedType(); + if (slot == null || !this.eventQualifiersMatcher.test(qualifiers(slot.attributes()), eventQualifiers)) { + continue; + } + final TypeMirror slotType = slot.type(); + for (final TypeMirror eventType : eventTypes) { + if (this.eventTypeMatcher.test(slotType, eventType)) { + // This level of indirection permits asynchronous notification. + this.fire(el, event, rs); + break; + } + } + } finally { + i.remove(); // if the EventListener is in a scope where it can be removed, do so, otherwise no-op + } + } + } + + /** + * Delivers ("Fires") the supplied {@code event} to the supplied {@link EventListener} via its {@link + * EventListener#eventReceived(Object, ReferencesSelector)} method. + * + *

The default implementation of this method behaves as if its body were exactly {@link EventListener + * el}.{@link EventListener#eventReceived(Object, ReferencesSelector) eventReceived(event, rs)}.

+ * + *

When this method is invoked by the {@link #fire(TypeMirror, List, Object, ReferencesSelector)} method, it is + * guaranteed that the supplied {@link EventListener} is a suitable one for the supplied {@code event}.

+ * + *

Overrides of this method must not call the {@link #fire(TypeMirror, List, Object, ReferencesSelector)} method, + * or an infinite loop may result.

+ * + * @param el an {@link EventListener} that has been determined to be suitable for the supplied {@code event}; must not + * be {@code null} + * + * @param event the event to deliver; must not be {@code null} + * + * @param rs a {@link ReferencesSelector}; must not be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + */ + protected void fire(final EventListener el, + final Object event, + final ReferencesSelector rs) { + el.eventReceived(event, rs); + } + +} diff --git a/src/site/site.xml b/src/site/site.xml index fb69fe9..b8967ea 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -13,7 +13,7 @@ org.apache.maven.skins maven-fluido-skin - 2.0.0 + 2.1.0 diff --git a/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java b/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java new file mode 100644 index 0000000..43c00bd --- /dev/null +++ b/src/test/java/org/microbean/event/TestCDIEventParameterizedType.java @@ -0,0 +1,158 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.event.Event; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; + +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; + +import jakarta.enterprise.util.TypeLiteral; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +final class TestCDIEventParameterizedType { + + private SeContainer c; + + private TestCDIEventParameterizedType() { + super(); + } + + @BeforeEach + final void setup() { + this.c = SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(DummyBean.class) + .initialize(); + } + + @AfterEach + final void teardown() { + if (this.c != null) { + this.c.close(); + } + } + + @Test + final void testFireParameterizedSubtype() { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + final ArrayList l = new ArrayListWithTwoTypeVariables(); + // l's raw type is ArrayListWithTwoTypeVariables. + // The specified type is ArrayList, so we can infer one of two type arguments. We can't infer the second + // one, so this fails. + assertThrows(IllegalArgumentException.class, () -> e.fire(l)); + } + + @Test + final void testFireArrayListString() { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + // The CDI specification says that the event types of an object serving as an event "include all superclasses and + // interfaces of the runtime class of the event object". Somewhat interestingly, the "runtime class" of the event + // being fired here is, of course, simply, java.util.ArrayList. Nevertheless, in Weld, one of the event types of the + // event is java.util.ArrayList (set a breakpoint on the fire() call below and find out for + // yourself). java.util.ArrayList is a ParameterizedType and not a "runtime class" so this is kind of + // interesting. See also https://github.com/jakartaee/cdi/issues/884. + e.fire(new ArrayList()); + } + + @Test + final void testFireSubtypeWithWildcards0() { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + e.fire(new ArrayList()); + } + + @Test + final void testFireSubtypeWithWildcards1() { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + // Weld really should permit this + assertThrows(IllegalArgumentException.class, () -> e.fire(new ArrayList())); + } + + @Test + final void testFireSubtypeWithWildcards2() { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + // Weld really should permit this + assertThrows(IllegalArgumentException.class, () -> e.fire(new ArrayList())); + } + + @Test + final void testFireOuterInner() { + final Event.Inner> e = this.c.select(new TypeLiteral.Inner>>() {}).get(); + e.fire(new Outer().new Inner()); + } + + @Test + final void testInstanceWithWildcards() { + // Obvious. + assertNotNull(this.c.select(DummyBean.class).get()); + // Same formulation, just using TypeLiteral. Obvious. + assertNotNull(this.c.select(new TypeLiteral() {}).get()); + // Maybe less obvious at first glance, but still obvious. + assertNotNull(this.c.select(new TypeLiteral>() {}).get().get()); + // Kind of weird until you remember that wildcards are illegal bean types. Still kind of weird. It is true that if + // this *did* return an unknown-type-that-extends-DummyBean it's not clear you could call destroy(Object) or + // remove(Object) on the iterator. + assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get); + assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get); + // See above. Here you wouldn't be able to iterate. + assertThrows(UnsatisfiedResolutionException.class, this.c.select(new TypeLiteral>() {}).get()::get); + } + + @Test + final void testInference() throws ReflectiveOperationException { + final Event> e = this.c.select(new TypeLiteral>>() {}).get(); + final Sub sub = new Sub<>(); + final Method m = e.getClass().getDeclaredMethod("getEventType", Class.class); + assertTrue(m.trySetAccessible()); + final ParameterizedType p = (ParameterizedType)m.invoke(e, sub.getClass()); + assertSame(p.getRawType(), Sub.class); + assertSame(Object.class, p.getActualTypeArguments()[0]); + assertSame(String.class, p.getActualTypeArguments()[1]); + } + + private static class Sup {} + + private static class Sub extends Sup {} + + private static final class DummyBean {} + + private static final class Outer { + private final class Inner extends ArrayList { + private static final long serialVersionUID = 1L; + } + } + + private static final class ArrayListWithTwoTypeVariables extends ArrayList { + private static final long serialVersionUID = 1L; + }; + +} diff --git a/src/test/java/org/microbean/event/TestEventTypes.java b/src/test/java/org/microbean/event/TestEventTypes.java new file mode 100644 index 0000000..d0b5ef5 --- /dev/null +++ b/src/test/java/org/microbean/event/TestEventTypes.java @@ -0,0 +1,140 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.construct.DefaultDomain; +import org.microbean.construct.Domain; + +import static javax.lang.model.type.TypeKind.ARRAY; +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.INT; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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; + +final class TestEventTypes { + + private Domain domain; + + private EventTypes eventTypes; + + private TestEventTypes() { + super(); + } + + @BeforeEach + final void setup() { + this.domain = new DefaultDomain(); + this.eventTypes = new EventTypes(this.domain); + } + + @Test + final void testEventTypeCase1() { + final TypeMirror na = domain.declaredType(domain.javaLangObject()); + final TypeMirror rv = eventTypes.eventType(na, int.class); + assertTrue(rv instanceof PrimitiveType); + assertSame(INT, rv.getKind()); + } + + @Test + final void testEventTypeCase2() { + final TypeMirror na = domain.declaredType(domain.javaLangObject()); + final TypeMirror rv = eventTypes.eventType(na, int[].class); + assertSame(ARRAY, rv.getKind()); + assertSame(INT, ((ArrayType)rv).getComponentType().getKind()); + } + + @Test + final void testEventTypeCase3() { + final TypeMirror na = domain.declaredType(domain.javaLangObject()); + final TypeMirror rv = eventTypes.eventType(na, Object.class); + assertSame(DECLARED, rv.getKind()); + assertTrue(domain.javaLangObject(rv)); + } + + @Test + final void testEventTypeCase4() { + final TypeMirror na = domain.declaredType(domain.javaLangObject()); + final TypeMirror rv = eventTypes.eventType(na, Object[].class); + assertSame(ARRAY, rv.getKind()); + assertTrue(domain.javaLangObject(((ArrayType)rv).getComponentType())); + } + + @Test + final void testEventTypeCase5() { + final TypeMirror intScalar = domain.primitiveType(INT); + assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(intScalar, ArrayList.class)); + } + + @Test + final void testEventTypeCase6() { + final TypeMirror intArray = domain.arrayTypeOf(domain.primitiveType(INT)); + assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(intArray, ArrayList.class)); + } + + @Test + final void testEventTypeCase7() { + final TypeMirror list = domain.declaredType(domain.typeElement(List.class.getCanonicalName())); + assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(list, ArrayList.class)); + } + + @Test + final void testEventTypeCase8() { + final TypeMirror listArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName()))); + assertThrows(IllegalArgumentException.class, () -> eventTypes.eventType(listArray, ArrayList.class)); + } + + @Test + final void testEventTypeCase9() { + final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName())); + final TypeMirror abstractListString = domain.declaredType(domain.typeElement(AbstractList.class.getCanonicalName()), string); + assertTrue(domain.sameType(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()), string), + eventTypes.eventType(abstractListString, ArrayList.class))); + } + + @Test + final void testEventTypeCase10() { + final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName())); + final TypeMirror listStringArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName()), + string)); + assertTrue(domain.sameType(domain.arrayTypeOf(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()), + string)), + eventTypes.eventType(listStringArray, ArrayList[].class))); + } + + @Test + final void testEventTypeCase11() { + final TypeMirror string = domain.declaredType(domain.typeElement(String.class.getCanonicalName())); + final TypeMirror listStringArray = domain.arrayTypeOf(domain.declaredType(domain.typeElement(List.class.getCanonicalName()), + string)); + assertTrue(domain.sameType(domain.declaredType(domain.typeElement(ArrayList.class.getCanonicalName()), string), + eventTypes.eventType(listStringArray, ArrayList.class))); + } + + +} diff --git a/src/test/java/org/microbean/event/TestJDKFacts.java b/src/test/java/org/microbean/event/TestJDKFacts.java new file mode 100644 index 0000000..4337014 --- /dev/null +++ b/src/test/java/org/microbean/event/TestJDKFacts.java @@ -0,0 +1,115 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestJDKFacts { + + private TestJDKFacts() { + super(); + } + + @Test + final void testCanonicalNames() { + final Outer.Inner0 i = new Outer().new Inner0(); + assertEquals(this.getClass().getName() + "." + + Outer.class.getSimpleName() + "." + + Outer.Inner0.class.getSimpleName(), + i.getClass().getCanonicalName()); + assertEquals("int", int.class.getCanonicalName()); + assertEquals("int[]", int[].class.getCanonicalName()); + } + + @Test + final void testClassKinds() { + assertTrue(Outer.Inner0.class.isMemberClass()); + assertTrue(Outer.InnerStatic.class.isMemberClass()); + } + + @Test + final void testReflectionAssumptionsAboutRawArrayList() { + // Generic superclass is a Class (specifically ArrayList.class); no type arguments + assertSame(ArrayList.class, RawArrayList.class.getGenericSuperclass()); + assertEquals(1, ArrayList.class.getTypeParameters().length); + } + + @Test + final void testReflectionAssumptionsAboutCookedArrayList() { + // Generic superclass is a ParameterizedType (specifically ArrayList); one type argument + final ParameterizedType pt = (ParameterizedType)CookedArrayList.class.getGenericSuperclass(); + assertSame(ArrayList.class, pt.getRawType()); + final Type[] tas = pt.getActualTypeArguments(); + assertEquals(1, tas.length); + assertSame(String.class, tas[0]); + } + + @Test + final void testOuterInner() { + final Outer.Inner0 i = new Outer().new Inner0(); + assertSame(Outer.Inner0.class, i.getClass()); + assertSame(Outer.class, Outer.Inner0.class.getEnclosingClass()); + assertSame(this.getClass(), Outer.class.getEnclosingClass()); + + // Get ArrayList. Can't get ArrayList. + final ParameterizedType inner0Supertype = (ParameterizedType)Outer.Inner0.class.getGenericSuperclass(); + assertSame(ArrayList.class, inner0Supertype.getRawType()); + final Type[] inner0SupertypeTypeArguments = inner0Supertype.getActualTypeArguments(); + assertEquals(1, inner0SupertypeTypeArguments.length); + assertTrue(inner0SupertypeTypeArguments[0] instanceof TypeVariable); + + final OuterSubclass os = new OuterSubclass(); + OuterSubclass.Inner1 x0 = os.new Inner1(); + Outer.Inner1 x1 = new OuterSubclass().new Inner1(); + OuterSubclass.Inner1 x2 = new OuterSubclass().new Inner1(); + + + + } + + @SuppressWarnings("rawtypes") + private static final class RawArrayList extends ArrayList { + private static final long serialVersionUID = 1L; + } + + private static final class CookedArrayList extends ArrayList { + private static final long serialVersionUID = 1L; + } + + private static class Outer { + private class Inner0 extends ArrayList { + private static final long serialVersionUID = 1L; + } + class Inner1 {} + private static class InnerStatic {} + } + + private static class OuterSubclass extends Outer {} + + private static class ParameterizedSubtype {} + +} diff --git a/src/test/java/org/microbean/event/TestLanguageModelFacts.java b/src/test/java/org/microbean/event/TestLanguageModelFacts.java new file mode 100644 index 0000000..d755e8e --- /dev/null +++ b/src/test/java/org/microbean/event/TestLanguageModelFacts.java @@ -0,0 +1,363 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import org.junit.jupiter.api.Test; + +import org.microbean.construct.DefaultDomain; +import org.microbean.construct.Domain; + +import org.microbean.construct.element.UniversalElement; + +import org.microbean.construct.type.UniversalType; + +import static javax.lang.model.element.ElementKind.ANNOTATION_TYPE; +import static javax.lang.model.element.ElementKind.CLASS; +import static javax.lang.model.element.ElementKind.METHOD; + +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.TYPEVAR; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestLanguageModelFacts { + + private static final Domain domain = new DefaultDomain(); + + private TestLanguageModelFacts() { + super(); + } + + @Test + final void testAnnotationMirrorEnclosingAndEnclosedElements() { + final TypeElement e = domain.typeElement("java.security.AccessControlException"); // has @Deprecated on it + final List as = e.getAnnotationMirrors(); + assertEquals(1, as.size()); + final AnnotationMirror deprecated = as.get(0); + final Map m = deprecated.getElementValues(); + assertEquals(2, m.size(), String.valueOf(m)); // since, forRemoval + // Just get whichever one happens to come up + final ExecutableElement element = m.keySet().iterator().next(); + final TypeElement deprecatedElement = (TypeElement)element.getEnclosingElement(); + assertSame(ANNOTATION_TYPE, deprecatedElement.getKind()); + final List enclosedElements = deprecatedElement.getEnclosedElements(); + assertEquals(2, enclosedElements.size()); + enclosedElements.forEach(ee -> assertSame(METHOD, ee.getKind())); + System.out.println("*** modifiers: " + deprecatedElement.getModifiers()); + enclosedElements.forEach(ee -> System.out.println(" modifiers for " + ee + ": " + ee.getModifiers())); + + } + + @Test + final void testPrototypicalTypeAndDeclaredType() { + final TypeElement string = domain.typeElement("java.lang.String"); + final DeclaredType prototypicalStringType = (DeclaredType)string.asType(); + final DeclaredType stringTypeUsage = domain.declaredType(string); + assertTrue(string instanceof UniversalElement); + assertTrue(prototypicalStringType instanceof UniversalType); + assertTrue(stringTypeUsage instanceof UniversalType); + assertNotSame(prototypicalStringType, stringTypeUsage); + // This surprises me, but it ends up being true because types in the compiler are always compared by identity, and + // UniversalType delegates to its delegate's equals() implementation. So the prototype type is indeed not the same + // as and therefore not equal to the type usage. + assertNotEquals(prototypicalStringType, stringTypeUsage); + assertTrue(domain.sameType(prototypicalStringType, stringTypeUsage)); + } + + @Test + final void testRawPropagation() { + final DeclaredType arrayListTU = domain.declaredType(domain.typeElement("java.util.ArrayList")); + assertTrue(arrayListTU.getTypeArguments().isEmpty()); + + // Hypothesis: if you use directSupertypes, it will sub in the type arguments. (For inferring event type arguments + // that's not what we want.) If you supply it a raw type, then all the supertypes will be raw as well. Again, not + // what we want. + assertEquals(List.of(), ((DeclaredType)domain.directSupertypes(arrayListTU).get(0)).getTypeArguments()); + } + + @Test + final void testNoTypeVariablePropagationInTypeUsages() { + final TypeElement arrayList = domain.typeElement("java.util.ArrayList"); + final DeclaredType abstractListSC = (DeclaredType)arrayList.getSuperclass(); + final TypeElement abstractList = (TypeElement)abstractListSC.asElement(); + assertEquals(domain.typeElement("java.util.AbstractList"), abstractList); + final TypeParameterElement abstractListTPE = abstractList.getTypeParameters().get(0); + final TypeVariable abstractListTV = (TypeVariable)abstractListTPE.asType(); + assertEquals(abstractListTPE, abstractListTV.asElement()); // note that the arraylist E died + } + + @Test + final void testJLSViolation() { + final TypeElement arrayList = domain.typeElement("java.util.ArrayList"); + final TypeElement string = domain.typeElement("java.lang.String"); + final DeclaredType arrayListTU = domain.declaredType(arrayList, string.asType()); + final List directSupertypes = domain.directSupertypes(arrayListTU); + // The first type in the list/set is AbstractList. + final DeclaredType abstractListST = (DeclaredType)directSupertypes.get(0); + assertEquals(1, abstractListST.getTypeArguments().size()); + // The next type in the list/set is List. Interface types are guaranteed to appear after non-interface types + // so this shows that the raw type represented by, simply, ArrayList does not appear in the set. The JLS says it + // should, among other types. See also https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.10.2 and + // https://bugs.openjdk.org/browse/JDK-8055219 and + // https://stackoverflow.com/questions/79817198/why-does-directsupertypes-not-return-a-raw-type-as-required-by-jls-4-10. + // + // My guess is that Types#directSupertypes(TypeMirror) should *really* be specified to return direct supertypes + // *that can be declared in the Java language*. This would make sense given that the annotation processing model was + // partially reverse engineered out of the existing guts of javac. So, for example, since you can write neither: + // + // // Invalid Java + // public class ArrayList extends ArrayList, AbstractList... + // + // ...nor: + // + // // Invalid Java + // public class ArrayLIst extends ArrayList... + // + // ...nor anything analogous, the types represented by ArrayList and ArrayList will not appear in the + // return value. + assertTrue(((DeclaredType)directSupertypes.get(1)).asElement().getKind().isInterface()); + } + + @Test + final void testPrototypicalTypeStructure() { + + // ArrayList in ArrayList. The declaring type element. + // + // <> + // (arrayList) + // +---------------+ + // | ArrayList | + // +---------------+ + // + final TypeElement arrayList = domain.typeElement("java.util.ArrayList"); + + // in ArrayList. The declaring type parameter element. + // + // <> <> + // (arrayList) (arrayListE) + // +---------------+ +------------------------+ + // | ArrayList <---> E | + // +---------------+ +------------------------+ + // + final TypeParameterElement arrayListE = (TypeParameterElement)arrayList.getTypeParameters().get(0); + assertEquals(arrayList, arrayListE.getGenericElement()); + + // The type variable declared by . + // + // <> <> + // (arrayList) (arrayListE) + // +---------------+ +------------------------+ + // | ArrayList <---> E | + // +---------------+ +------------^-----------+ + // | + // | + // V + // <> + // (arrayListETV) + // +----------------+ + // | (E) | + // +----------------+ + // + final TypeVariable arrayListETV = (TypeVariable)arrayListE.asType(); + assertEquals(arrayListE, arrayListETV.asElement()); + assertEquals(arrayListETV, arrayListE.asType()); + + // The prototypical type declared by ArrayList. + // + // <> <> + // (arrayList) (arrayListE) + // +---------------+ +------------------------+ + // | ArrayList <---> E | + // +--------^------+ +------------^-----------+ + // | | + // V | + // <> V + // <> <> + // (arrayListPT) (arrayListETV) + // +---------------------+ +----------------+ + // | (ArrayList) | | (E) | + // +---------------------+ +----------------+ + final DeclaredType arrayListPT = (DeclaredType)arrayList.asType(); + assertEquals(arrayList, arrayListPT.asElement()); + + // Its type argument is the type variable underlying . This completes a cycle: + // + // <> <> + // (arrayList) (arrayListE) + // +---------------+ +------------------------+ + // | ArrayList <---> E | + // +--------^------+ +------------^-----------+ + // | | + // V | + // <> V + // <> <> + // (arrayListPT) (arrayListETV) + // +---------------------+ +----------------+ + // | (ArrayList) +---> (E) | + // +---------------------+ +----------------+ + // + // The prototypical type is not raw, because it _does_ have type arguments. The type argument is a type variable, + // not nothing. + assertEquals(arrayListETV, arrayListPT.getTypeArguments().get(0)); + + } + + @Test + final void testAsMemberOf() { + final TypeElement arrayList = domain.typeElement("java.util.ArrayList"); + final TypeElement list = domain.typeElement("java.util.List"); + final TypeElement string = domain.typeElement("java.lang.String"); + + final DeclaredType stringTypeUsage = domain.declaredType(string); + final DeclaredType listStringTypeUsage = domain.declaredType(list, stringTypeUsage); + + final DeclaredType arrayListRawTypeUsage = domain.declaredType(arrayList); + + // Can we do this? No. + assertThrows(IllegalArgumentException.class, () -> domain.asMemberOf(listStringTypeUsage, arrayList)); + + // OK, can we do this? No. + assertThrows(IllegalArgumentException.class, () -> domain.asMemberOf(listStringTypeUsage, domain.typeParameterElement(arrayList, "E"))); + } + + @Test + final void testRawTypeRepresentation() { + final List l = new ArrayList<>(); + + // Here's a TypeElement representing ArrayList (not ArrayList, not ArrayList, not really ArrayList). + final TypeElement list = domain.typeElement(l.getClass().getCanonicalName()); + + // rawArrayListTypeUsage here is a *usage* of the type declared by the java.util.ArrayList TypeElement. This usage has no + // type arguments. This makes the declared type "in" this usage a raw type. + final DeclaredType rawArrayListTypeUsage = domain.declaredType(list); // note: no type arguments supplied + assertSame(DECLARED, rawArrayListTypeUsage.getKind()); + + // rawArrayListTypeUsage has no type arguments. (It can't; none were supplied. See above.) + List typeArguments = rawArrayListTypeUsage.getTypeArguments(); + assertEquals(0, typeArguments.size()); + + // The declared type's type element has one type parameter element (E). The fact that the declared type's type + // argument count is zero and its delcaring type element's type parameter count is one is what makes the declared + // type raw. + final TypeElement e = (TypeElement)rawArrayListTypeUsage.asElement(); + assertSame(CLASS, e.getKind()); + assertEquals(list, e); // basically identical, but always call equals() to allow delegates and wrappers to work + + // java.util.ArrayList's sole type parameter element is "E" (as in public class java.util.ArrayList...). + final List typeParameterElements = list.getTypeParameters(); + assertEquals(1, typeParameterElements.size()); + final TypeParameterElement typeParameterElement = typeParameterElements.get(0); + assertTrue(typeParameterElement.getSimpleName().contentEquals("E")); + + // This type parameter element declares a type variable (definitionally unnamed). + TypeVariable tv = (TypeVariable)typeParameterElement.asType(); + + // (The type variable can get its declaring type parameter element.) + assertEquals(typeParameterElement, (TypeParameterElement)tv.asElement()); + + // Here is the type as it was *declared* by the java.util.ArrayList TypeElement. Note that it *does* have type + // arguments. This is distinct from rawArrayListTypeUsage above, which represents a declared type *usage*. + final DeclaredType listDeclarationType = (DeclaredType)list.asType(); + assertSame(DECLARED, listDeclarationType.getKind()); + assertEquals(list, listDeclarationType.asElement()); + + // So what *are* the type arguments in a type declaration created/implied by a TypeElement? The TypeVariables + // declared by the TypeParameterElements. + typeArguments = listDeclarationType.getTypeArguments(); + assertEquals(1, typeArguments.size()); + assertEquals(tv, typeArguments.get(0)); + + // To recap so far: + // + // In this partial code snippet: + // + // public class ArrayList ... + // + // * The TypeElement is named java.util.ArrayList + // * It has one TypeParameterElement whose name is E + // * That TypeParameterElement's asType() method returns a TypeVariable "backing" E + // * The TypeVariable's asElement() method returns the TypeParameterElement (so mutual dependency) + // * The TypeElement's asType() method returns a DeclaredType representing the actual declaration type + // * This DeclaredType has the same number of type arguments as its declaring TypeElement has TypeParameterElements + // * The sole type argument is the TypeVariable mentioned above + // + // In this partial code snippet: + // + // DeclaredType rawArrayListTypeUsage = domain.declaredType(domain.typeElement("java.util.ArrayList")); + // + // * rawArrayListTypeUsage is a type *usage* + // * It has no type arguments at all + // * Therefore it is a raw type (a raw type usage) + // * Its asElement() returns the TypeElement declared by "public class ArrayList" + // * Note that that TypeElement's asType() method does NOT return this type usage, but returns the type + // declaration "backing" the TypeElement + // * As previously noted it DOES have type arguments + // + // +---------------------+ +---------------------------------+ + // | TypeElement (named) <1--declares--1> DeclaredType (type declaration) +-----+ + // +-----^------------^--+ +---------------------------------+ |type arguments + // 1 1 (equal to type parameter count) * + // ^ | | +------------------------------+ +-------v------+ + // uses | +-----------*> TypeParameterElement (named) <1---1> TypeVariable | + // | +------------------------------+ +--------------+ + // +-----+---------------------+ +------------------------------------------------+ + // | DeclaredType (type usage) +--*> TypeMirror (type argument) | + // +---------------------------+ | | + // | (one of DeclaredType, ArrayType, TypeVariable) | + // | (TypeVariable may be surprising, but consider | + // | a type usage in an "extends" or "implements" | + // | clause) | + // +------------------------------------------------+ + + } + + private static class Sup { + + private Sup() { + super(); + } + + } + + private static final class Sub extends Sup { + + private Sub() { + super(); + } + + } + +} diff --git a/src/test/java/org/microbean/event/TestRuntimeClassDivination.java b/src/test/java/org/microbean/event/TestRuntimeClassDivination.java new file mode 100644 index 0000000..7359e98 --- /dev/null +++ b/src/test/java/org/microbean/event/TestRuntimeClassDivination.java @@ -0,0 +1,84 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2025 microBean™. + * + * 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 org.microbean.event; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestRuntimeClassDivination { + + private TestRuntimeClassDivination() { + super(); + } + + @Test + final void testReflectionAssumptionsAboutRawArrayList() { + // Generic superclass is a Class (specifically ArrayList.class); no type arguments + assertSame(ArrayList.class, RawArrayList.class.getGenericSuperclass()); + assertEquals(1, ArrayList.class.getTypeParameters().length); + } + + @Test + final void testReflectionAssumptionsAboutCookedArrayList() { + // Generic superclass is a ParameterizedType (specifically ArrayList); one type argument + final ParameterizedType pt = (ParameterizedType)CookedArrayList.class.getGenericSuperclass(); + assertSame(ArrayList.class, pt.getRawType()); + final Type[] tas = pt.getActualTypeArguments(); + assertEquals(1, tas.length); + assertSame(String.class, tas[0]); + } + + @Test + final void testOuterInner() { + final Outer.Inner i = new Outer().new Inner(); + assertSame(Outer.Inner.class, i.getClass()); + assertSame(Outer.class, Outer.Inner.class.getEnclosingClass()); + assertSame(this.getClass(), Outer.class.getEnclosingClass()); + + // Get ArrayList. Can't get ArrayList. + final ParameterizedType innerSupertype = (ParameterizedType)Outer.Inner.class.getGenericSuperclass(); + assertSame(ArrayList.class, innerSupertype.getRawType()); + final Type[] innerSupertypeTypeArguments = innerSupertype.getActualTypeArguments(); + assertEquals(1, innerSupertypeTypeArguments.length); + assertTrue(innerSupertypeTypeArguments[0] instanceof TypeVariable); + + } + + @SuppressWarnings("rawtypes") + private static final class RawArrayList extends ArrayList { + private static final long serialVersionUID = 1L; + } + + private static final class CookedArrayList extends ArrayList { + private static final long serialVersionUID = 1L; + } + + private static final class Outer { + private final class Inner extends ArrayList { + private static final long serialVersionUID = 1L; + } + } + +}