diff --git a/.github/workflows/mvn-release-prepare-perform.yaml b/.github/workflows/mvn-release-prepare-perform.yaml index c858555..d12a45b 100644 --- a/.github/workflows/mvn-release-prepare-perform.yaml +++ b/.github/workflows/mvn-release-prepare-perform.yaml @@ -36,12 +36,12 @@ jobs: distribution: 'temurin' gpg-passphrase: 'GPG_PASSPHRASE' gpg-private-key: '${{ secrets.GPG_PRIVATE_KEY }}' - java-version: '23' - mvn-toolchain-id: 'Temurin 23' + java-version: '24' + mvn-toolchain-id: 'Temurin 24' 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..a9a759e 100644 --- a/.github/workflows/mvn-verify.yaml +++ b/.github/workflows/mvn-verify.yaml @@ -22,8 +22,8 @@ jobs: with: cache: 'maven' distribution: 'temurin' - java-version: '23' - mvn-toolchain-id: 'Temurin 23' + java-version: '24' + mvn-toolchain-id: 'Temurin 24' 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 d58dfb7..2079886 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +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 e9aed19..b7f98cc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Maven dependency: org.microbean microbean-reference - 0.0.4 + 0.0.5 ``` 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 88d8845..2bdfe22 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™ Bean Site https://microbean.github.io/microbean-reference/ - - sonatype-oss-repository-hosting - https://oss.sonatype.org/content/repositories/snapshots - @@ -89,7 +80,6 @@ deployment [maven-release-plugin] [skip ci] v@{project.version} - false @@ -101,12 +91,8 @@ true false - - - true - - https://oss.sonatype.org/ - 10 + + ${project.name} v${project.version} UTF8 @@ -125,7 +111,7 @@ org.junit junit-bom - 5.13.0 + 6.0.1 pom import @@ -141,28 +127,22 @@ - - org.microbean - microbean-attributes - 0.0.2 - - org.microbean microbean-bean - 0.0.18 + 0.0.22 org.microbean microbean-construct - 0.0.10 + 0.0.18 org.microbean microbean-proxy - 0.0.2 + 0.0.5 @@ -170,12 +150,6 @@ - - org.microbean - microbean-attributes - compile - - org.microbean microbean-bean @@ -221,11 +195,11 @@ maven-antrun-plugin - 3.1.0 + 3.2.0 maven-assembly-plugin - 3.7.1 + 3.8.0 maven-checkstyle-plugin @@ -335,7 +309,7 @@ com.puppycrawl.tools checkstyle - 10.12.6 + 12.3.0 @@ -356,7 +330,7 @@ maven-compiler-plugin - 3.14.0 + 3.14.1 -Xlint:all @@ -366,7 +340,7 @@ maven-dependency-plugin - 3.8.1 + 3.9.0 maven-deploy-plugin @@ -374,12 +348,11 @@ maven-enforcer-plugin - 3.5.0 + 3.6.2 maven-gpg-plugin - - 3.2.7 + 3.2.8 maven-install-plugin @@ -387,11 +360,11 @@ maven-jar-plugin - 3.4.2 + 3.5.0 maven-javadoc-plugin - 3.11.2 + 3.12.0 true @@ -423,16 +396,15 @@ 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 @@ -444,7 +416,7 @@ maven-source-plugin - 3.3.1 + 3.4.0 attach-sources @@ -456,7 +428,7 @@ maven-surefire-plugin - 3.5.3 + 3.5.4 maven-toolchains-plugin @@ -465,35 +437,25 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.0 + 4.9.8.2 org.codehaus.mojo versions-maven-plugin - 2.18.0 + 2.20.1 io.smallrye jandex-maven-plugin - 3.3.1 + 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 @@ -514,7 +476,7 @@ 23 - 3.9.9 + 3.9.12 @@ -539,8 +501,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 d67aab8..9b6a0e0 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -21,9 +21,8 @@ exports org.microbean.reference; - requires org.microbean.attributes; requires transitive org.microbean.bean; - requires transitive org.microbean.construct; + requires org.microbean.construct; requires transitive org.microbean.proxy; } diff --git a/src/main/java/org/microbean/reference/DefaultDestructorTree.java b/src/main/java/org/microbean/reference/DefaultDestructorTree.java new file mode 100644 index 0000000..7b47ced --- /dev/null +++ b/src/main/java/org/microbean/reference/DefaultDestructorTree.java @@ -0,0 +1,207 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023–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.reference; + +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.microbean.reference.DestructorRegistry.Destructor; + +/** + * A straightforward {@link DestructorTree} implementation. + * + * @author Laird Nelson + * + * @see DestructorTree + */ +public class DefaultDestructorTree implements DestructorTree { + + + /* + * Instance fields. + */ + + + private final Lock lock; + + // @GuardedBy("lock") + private Map destructors; // identity hashmap when open for business + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link DefaultDestructorTree}. + */ + public DefaultDestructorTree() { + super(); + this.lock = new ReentrantLock(); + } + + + /* + * Instance methods. + */ + + + /* + * Returns a new {@link DefaultDestructorTree} instance that is not {@linkplain #close() closed}, has no {@linkplain + * #register(Object, Destructor) registrations} yet, and is itself {@linkplain #register(Object, Destructor) registered} + * as a destructor with this {@link DefaultDestructorTree}. + * + * @return a new, {@linkplain #close() unclosed} {@link DefaultDestructorTree} {@linkplain #register(Object, Destructor) + * registered} as a destructor with this {@link DefaultDestructorTree} + * + * @exception IllegalStateException if this {@link DefaultDestructorTree} is {@linkplain #close() closed} + * + * @microbean.nullability This method does not, and its overrides must not, return {@code null}. + * + * @microbean.idempotency Overrides of this method must return new, distinct {@link DefaultDestructorTree} instances. + * + * @microbean.threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. + * + * @see #register(Object, Destructor) + * + * @see #close() + */ + @Override // DestructorTree + public DefaultDestructorTree newChild() { + final DefaultDestructorTree child = new DefaultDestructorTree(); + if (!this.register(child, child::close)) { // CRITICAL + throw new IllegalStateException(); + } + return child; + } + + /** + * Closes this {@link DefaultDestructorTree} and destroys its {@linkplain #register(Object, Destructor) registrants} + * by {@linkplain Destructor#destroy() running} their destructors {@linkplain #register(Object, Destructor) supplied + * at registration time}. + * + *

{@link Destructor#destroy()} is called on all {@linkplain #register(Object, Destructor) registrants}, even in the + * presence of exceptions. {@link RuntimeException}s consequently thrown may {@linkplain Throwable#getSuppressed() + * contain suppressed exceptions}.

+ * + *

Overrides of this method wishing to add semantics to this behavior should perform that work before calling + * {@link #close() super.close()}.

+ * + *

Overrides of this method must call {@link #close() super.close()} or undefined behavior may result.

+ * + *

After any successful invocation of this method, this {@link DefaultDestructorTree} is deemed to be + * irrevocably closed. Invoking this method again will have no effect.

+ * + * @microbean.idempotency This method is, and its overrides must be, idempotent. + * + * @microbean.threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads. + */ + @Override // DestructorTree + public void close() { + Map destructors; + lock.lock(); + try { + destructors = this.destructors; + if (destructors == Map.of()) { + // Already closed + return; + } + this.destructors = Map.of(); + } finally { + lock.unlock(); + } + + if (destructors == null) { + // nothing to do + return; + } + + RuntimeException re = null; + for (final Destructor d : destructors.values()) { + try { + d.destroy(); + } catch (final RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } + } + + if (re != null) { + throw re; + } + } + + /** + * If this {@link DefaultDestructorTree} is not closed, and if the supplied {@code reference} has not yet been + * registered, registers it such that it will be destroyed by the supplied {@code destructor} when this {@link + * DefaultDestructorTree} is {@linkplain #close() closed}, and returns {@code true}. + * + *

This method takes no action and returns {@code false} in all other cases.

+ * + * @param reference a contextual reference that will be destroyed later; if {@code null} then no action will be taken + * and {@code false} will be returned + * + * @param destructor a {@link Destructor} that, when {@linkplain Destructor#destroy() run}, will destroy the supplied + * {@code reference} in some way; if {@code null} then no action will be taken and {@code false} will be returned; if + * non-{@code null} must be idempotent and safe for concurrent use by multiple threads + * + * @return {@code true} if and only if this {@link DefaultDestructorTree} is not closed, and the supplied {@code + * reference} is not already registered and registration completed successfully; {@code false} in all other cases + * + * @microbean.idempotency This method is idempotent. + * + * @microbean.threadsafety This method is safe for concurrent use by multiple threads. + */ + @Override // DestructorRegistry + public final boolean register(final Object reference, final Destructor destructor) { + if (reference == null || destructor == null) { + return false; + } + lock.lock(); + try { + if (this.destructors == null) { + this.destructors = new IdentityHashMap<>(); // critical that this is an IdentityHashMap + } else if (this.destructors == Map.of()) { + // Already closed; register must therefore be a no-op. + return false; + } + return this.destructors.putIfAbsent(reference, destructor) == null; + } finally { + lock.unlock(); + } + } + + @Override // DestructorTree + public final Destructor remove(final Object reference) { + if (reference == null) { + return null; + } + lock.lock(); + try { + return this.destructors == null ? null : this.destructors.remove(reference); + } finally { + lock.unlock(); + } + } + +} diff --git a/src/main/java/org/microbean/reference/DestructorRegistry.java b/src/main/java/org/microbean/reference/DestructorRegistry.java new file mode 100644 index 0000000..6518898 --- /dev/null +++ b/src/main/java/org/microbean/reference/DestructorRegistry.java @@ -0,0 +1,80 @@ +/* -*- 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.reference; + +/** + * An interface whose implementations can register contextual instances for idempotent destruction at some later point. + * + *

This interface is often used by implementors of systems of dependent object destruction, and normally by no other + * kinds of users.

+ * + * Laird Nelson + * + * @see #register(Object, Destructor) + */ +// Needed by "dependent"/"none" scopes/lifecycle managers. +// Not used by "normal" factories etc. +// +// TODO: the package feels wrong, but stashing it in microbean-scopelet, alongside NoneScopelet, doesn't let +// microbean-reference's Request implement it +// +// TODO: maybe could move it to microbean-reference? microbean-scopelet depends on microbean-reference already? +public interface DestructorRegistry { + + /** + * Registers the supplied contextual instance such that at some future moment, or perhaps not at all, the + * supplied {@link Destructor} will be {@linkplain Destructor#destroy() run} to destroy it idempotently, and returns + * {@code true} if and only if the registration was successful. + * + * @param instance a contextual instance; may be {@code null} in which case no action will be taken and {@code false} + * will be returned + * + * @param destructor a {@link Destructor}; may be {@code null} in which case no action will be taken and {@code false} + * will be returned + * + * @return {@code true} if and only if registration was successful; {@code false} otherwise + * + * @see Destructor + */ + public boolean register(final Object instance, final Destructor destructor); + + /** + * An interface indicating that an implementation is capable of destroying an object that it opaquely + * references such that the destroyed object will no longer be suitable for use. + * + * @author Laird Nelson + * + * @see #destroy() + * + * @see DestructorRegistry#register(Object, Destructor) + */ + @FunctionalInterface + public static interface Destructor { + + /** + * Destroys an object that this implementation opaquely references such that the destroyed object will no longer be + * suitable for use. + * + *

Implementations of this method must be safe for concurrent use by multiple threads.

+ * + *

Implementations of this method must be idempotent, performing no action if destruction of the implicit object + * has already taken place.

+ * + * @see DestructorRegistry#register(Object, Destructor) + */ + public void destroy(); + + } + +} diff --git a/src/main/java/org/microbean/reference/DestructorTree.java b/src/main/java/org/microbean/reference/DestructorTree.java new file mode 100644 index 0000000..b34d91a --- /dev/null +++ b/src/main/java/org/microbean/reference/DestructorTree.java @@ -0,0 +1,71 @@ +/* -*- 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.reference; + +/** + * A hierarchical {@link DestructorRegistry} that is {@link AutoCloseable}. + * + *

This interface is often used by {@link org.microbean.bean.ReferencesSelector} implementors, and normally by no + * other kinds of users.

+ * + * @author Laird Nelson + * + * @see #close() + * + * @see DestructorRegistry + */ +// Needed and used only by ReferencesSelector implementations. +// +// TODO: maybe could move it to microbean-reference? microbean-scopelet depends on microbean-reference already? +public interface DestructorTree extends AutoCloseable, DestructorRegistry { + + /** + * Creates a new child instance of this implementation, or a subtype, {@linkplain #register(Object, + * Destructor) registers it} with this implementation, using a method reference to its {@link #close()} method as the + * {@link Destructor}, and returns it. + * + * @return a new (non-{@code null}) child instance of this implementation, or a subtype, {@linkplain #register(Object, + * Destructor) registered} with this implementation such that {@link #close() closing} this implementation will also + * {@linkplain #close() close} the child instance + * + * @see #close() + * + * @see #register(Object, Destructor) + */ + public DestructorTree newChild(); + + /** + * Closes this {@link DestructorTree} implementation by effectively {@linkplain #remove(Object) removing} all + * {@linkplain #register(Object, Destructor) registered} contextual instances and {@linkplain Destructor#destroy() + * running their affiliated Destructors}. + * + * @see #remove(Object) + */ + @Override // AutoCloseable + public void close(); + + /** + * Removes the supplied contextual instance and the {@link Destructor} that was {@linkplain #register(Object, + * Destructor) registered with it}. + * + *

The {@link Destructor#destroy()} will not be invoked by implementations of this method.

+ * + * @param instance the contextual instance to remove; may be {@code null} in which case {@code null} will be returned + * + * @return a {@link Destructor} {@linkplain #register(Object, Destructor) registered} with the supplied instance, or + * {@code null} if no such {@link Destructor} exists + */ + public Destructor remove(final Object instance); + +} diff --git a/src/main/java/org/microbean/reference/Instances.java b/src/main/java/org/microbean/reference/Instances.java index 736a591..eccc692 100644 --- a/src/main/java/org/microbean/reference/Instances.java +++ b/src/main/java/org/microbean/reference/Instances.java @@ -13,25 +13,26 @@ */ package org.microbean.reference; +import java.util.function.BooleanSupplier; import java.util.function.Supplier; import org.microbean.bean.Bean; import org.microbean.bean.Id; import org.microbean.bean.Creation; +import org.microbean.bean.ReferencesSelector; /** * A factory for {@link Supplier}s of contextual instances. * + *

{@link Instances} instances are used by {@link Request} instances.

+ * * @author Laird Nelson * * @see #supplier(Bean, Creation) + * + * @see Request */ -public interface Instances extends AutoCloseable { - - @Override // AutoCloseable - public default void close() { - - } +public interface Instances { /** * Returns {@code true} if and only if it is possible for a client proxy to be created for contextual instances diff --git a/src/main/java/org/microbean/reference/ReflectiveClientProxier.java b/src/main/java/org/microbean/reference/ReflectiveClientProxier.java index 314df2e..9fe83e1 100644 --- a/src/main/java/org/microbean/reference/ReflectiveClientProxier.java +++ b/src/main/java/org/microbean/reference/ReflectiveClientProxier.java @@ -101,6 +101,7 @@ protected final Proxy proxy(final ProxySpecification ps, .computeIfAbsent(ps, ps0 -> newProxyInstance(this.classLoader(), interfaces, + // Anonymous class, not lambda, due to Throwable in the signature new InvocationHandler() { @Override // InvocationHandler public final Object invoke(final Object p, diff --git a/src/main/java/org/microbean/reference/Request.java b/src/main/java/org/microbean/reference/Request.java index 8a5214a..b4e224a 100644 --- a/src/main/java/org/microbean/reference/Request.java +++ b/src/main/java/org/microbean/reference/Request.java @@ -14,39 +14,37 @@ package org.microbean.reference; import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import javax.lang.model.type.TypeMirror; +import java.util.function.Supplier; + +import org.microbean.assign.AttributedType; +import org.microbean.assign.Selectable; -import org.microbean.bean.AmbiguousReductionException; -import org.microbean.bean.AttributedType; -import org.microbean.bean.AutoCloseableRegistry; import org.microbean.bean.Bean; import org.microbean.bean.BeanException; import org.microbean.bean.Creation; -import org.microbean.bean.DefaultAutoCloseableRegistry; import org.microbean.bean.Destruction; -import org.microbean.bean.Factory; import org.microbean.bean.Id; -import org.microbean.bean.Reducible; import org.microbean.bean.References; import org.microbean.bean.ReferencesSelector; -import org.microbean.bean.Selectable; -import org.microbean.bean.UnsatisfiedReductionException; import org.microbean.construct.Domain; +import org.microbean.proxy.Proxy; + +import static java.util.Collections.emptyIterator; + +import static java.util.Objects.requireNonNull; + /** * A central object representing a request for dependencies that is a {@link Creation} (and therefore also a {@link - * Destruction}) and a {@link References}. + * Destruction}), a {@link DestructorRegistry}, and a {@link References}. * *

Instances of this class are the heart and soul of a dependency injection and acquisition system.

* - * @param the contextual instance type (see for example {@link Creation}) + * @param the contextual instance type being instantiated (see for example {@link Creation}) * - * @param the contextual reference type (see {@link References}) + * @param the contextual reference type being sought (see {@link References}) * * @author Laird Nelson * @@ -54,13 +52,15 @@ * * @see Destruction * + * @see DestructorRegistry + * * @see References * - * @see Factory#create(Creation) + * @see org.microbean.bean.Factory#create(Creation) * - * @see Factory#destroy(Object, Destruction) + * @see org.microbean.bean.Factory#destroy(Object, Destruction) */ -public final class Request implements Creation, Destruction, References { +public final class Request implements Creation, Destruction, DestructorRegistry, References { /* @@ -68,17 +68,19 @@ public final class Request implements Creation, Destruction, References */ - private final Selectable> s; + private final Domain domain; + + private final Selectable> beans; private final Instances instances; - private final AutoCloseableRegistry acr; + private final DestructorTree destructorTree; private final ClientProxier cp; - private final Bean b; + private final Bean b; // nullable; B and R must then be (effectively) Void - private final AttributedType rType; + private final AttributedType rType; // nullable; R must then be Void /* @@ -89,37 +91,67 @@ public final class Request implements Creation, Destruction, References /** * Creates a new {@link Request}. * + * @param d a {@link Domain}; must not be {@code null} + * + * @param s a {@link Selectable} providing access to {@link Bean}s by {@link AttributedType}; must not be {@code + * null}; must be safe for concurrent use by multiple threads; often assembled out of methods present in the {@link + * org.microbean.bean.Selectables} and {@link org.microbean.bean.Beans} classes, among other such utility classes + * + * @param instances an {@link Instances} responsible for using a {@link Bean} to acquire an appropriate {@link + * Supplier} of contextual instances; must not be {@code null} + * + * @param cp a {@link ClientProxier}; must not be {@code null} + * + * @exception NullPointerException if any argument is {@code null} + * + * @see #Request(Domain, Selectable, Instances, DestructorTree, ClientProxier) + */ + public Request(final Domain d, + final Selectable> s, + final Instances instances, + final ClientProxier cp) { + this(d, s, instances, null, cp, null, null); + } + + /** + * Creates a new {@link Request}. + * + * @param d a {@link Domain}; must not be {@code null} + * * @param s a {@link Selectable} providing access to {@link Bean}s by {@link AttributedType}; must not be {@code * null}; must be safe for concurrent use by multiple threads; often assembled out of methods present in the {@link * org.microbean.bean.Selectables} and {@link org.microbean.bean.Beans} classes, among other such utility classes * * @param instances an {@link Instances} responsible for using a {@link Bean} to acquire an appropriate {@link - * java.util.function.Supplier} of contextual instances; must not be {@code null} + * Supplier} of contextual instances; must not be {@code null} * - * @param acr an {@link AutoCloseableRegistry}; may be {@code null} in which case a default implementation will be - * used instead + * @param destructorTree a {@link DestructorTree}; may be {@code null} in which case a default implementation will be used + * instead * * @param cp a {@link ClientProxier}; must not be {@code null} * * @exception NullPointerException if {@code s}, {@code instances}, or {@code cp} is {@code null} */ - public Request(final Selectable> s, + public Request(final Domain d, + final Selectable> s, final Instances instances, - final AutoCloseableRegistry acr, + final DestructorTree destructorTree, // nullable final ClientProxier cp) { - this(s, instances, acr, cp, null, null); + this(d, s, instances, destructorTree, cp, null, null); } - private Request(final Selectable> s, + private Request(final Domain d, + final Selectable> s, final Instances instances, - final AutoCloseableRegistry acr, // nullable + final DestructorTree destructorTree, // nullable final ClientProxier cp, - final Bean b, - final AttributedType rType) { // the type of the references returned; nullable - this.s = Objects.requireNonNull(s, "s"); - this.instances = Objects.requireNonNull(instances, "instances"); - this.cp = Objects.requireNonNull(cp, "cp"); - this.acr = acr == null ? new DefaultAutoCloseableRegistry() : acr; + final Bean b, // nullable + final AttributedType rType) { // the type of the references returned (); nullable + this.domain = requireNonNull(d, "d"); + this.beans = requireNonNull(s, "s"); + this.instances = requireNonNull(instances, "instances"); + this.cp = requireNonNull(cp, "cp"); + this.destructorTree = destructorTree == null ? new DefaultDestructorTree() : destructorTree; this.b = b; this.rType = rType; } @@ -132,7 +164,22 @@ private Request(final Selectable> s, @Override // Destruction public final void close() { - try (this.instances; this.acr) {} + this.destructorTree.close(); + } + + @Override // ReferencesSelector + public final boolean destroy(final Object r) { + final Destructor destructor = this.destructorTree.remove(r instanceof Proxy p ? p.$proxied() : r); + if (destructor != null) { + destructor.destroy(); // I keep going back and forth on whether this should be under some kind of lock, or whether the Destructor contract covers it + return true; + } + return false; + } + + @Override // ReferencesSelector + public final Domain domain() { + return this.domain; } @Override // Creation @@ -142,40 +189,65 @@ public final Id id() { @Override // References (Iterable) public final Iterator iterator() { - return new ReferencesIterator(); + return new ReferencesIterator(); // inner class; see below } - // Called by ReferencesIterator below - private final R get(final Request r) { - final Bean bean = r.b; + @Override // ReferencesSelector + public final R reference(final Bean bean) { + final Supplier supplier = this.instances.supplier(bean, this.newChild(bean)); // newChild is critical final Id id = bean.id(); - if (this.instances.proxiable(id)) { - final R ref = this.cp.clientProxy(id, instances.supplier(bean, r)); - // TODO: we know that ref can be destroyed because it's from a normal scope, i.e. it is not itself a contextual - // instance, so save it in a collection somewhere so it can be destroyed via the #destroy(R) method (see CDI's - // Instances#destroy(Object)) - return ref; - } - // TODO: ask instances if ref is destroyable and save it off - return instances.supplier(bean, r).get(); + return this.instances.proxiable(id) ? this.cp.clientProxy(id, supplier) : supplier.get(); } @Override // ReferencesSelector - public final References references(final AttributedType t) { - return new Request<>(this.s, this.instances, this.acr, this.cp, this.b, t); + @SuppressWarnings("unchecked") + public final References references(final AttributedType rType) { + return this.rType == rType ? (References)this : + // This basically returns "this" but with a new rType. But Request is immutable so we make a copy. + new Request<>(this.domain, + this.beans, + this.instances, + this.destructorTree, // deliberately NO this.destructorTree.newChild() call + this.cp, + this.b, // nullable; will then be (effectively) Void + rType); // nullable; will then be Void + } + + @Override // DestructorTree (DestructorRegistry) + public final boolean register(final Object reference, final Destructor destructor) { + return this.destructorTree.register(reference, destructor); } @Override // References public final int size() { - return this.s.select(this.rType).size(); + return this.rType == null ? 0 : this.beans.select(this.rType).size(); } - @Override // References - public final boolean destroy(final R r) { - if (r != null) { // and is in dependent scope; we'll deal with that later - // TODO: remove it from a collection of dependent refs returned by get(Request) above + /* + * Private instance methods. + */ + + private final Iterator> beanIterator() { + return this.rType == null ? emptyIterator() : this.beans.select(this.rType).iterator(); + } + + @SuppressWarnings("unchecked") + private final Request newChild(final Bean b) { + if (b == null) { + if (this.b == null) { + return (Request)this; // both and are effectively Void + } + } else if (b.equals(this.b)) { + return (Request)this; } - return false; + return + new Request(this.domain, + this.beans, + this.instances, + this.destructorTree.newChild(), // critical; !b.equals(this.b) + this.cp, + b, // nullable; if so, better resolve to Void + null); // rType; resolves to Void } @@ -184,6 +256,7 @@ public final boolean destroy(final R r) { */ + // NOT thread-safe. private final class ReferencesIterator implements Iterator { private Iterator> i; @@ -192,38 +265,36 @@ private final class ReferencesIterator implements Iterator { private ReferencesIterator() { super(); + if (rType == null) { + this.i = emptyIterator(); + } } @Override // Iterator public final boolean hasNext() { - if (rType == null) { - return false; - } if (this.i == null) { - this.i = s.select(rType).iterator(); + this.i = beanIterator(); } return this.i.hasNext(); } @Override // Iterator + @SuppressWarnings("unchecked") public final R next() { - if (rType == null) { - throw new NoSuchElementException(); - } if (this.i == null) { - this.i = s.select(rType).iterator(); + this.i = beanIterator(); } - @SuppressWarnings("unchecked") - final R ref = get(new Request<>(s, instances, acr.newChild(), cp, (Bean)this.i.next(), null)); - this.ref = ref; - return ref; + return this.ref = reference((Bean)this.i.next()); } @Override // Iterator public final void remove() { final R ref = this.ref; + if (ref == null) { + throw new IllegalStateException(); // per Iterator#remove() contract + } this.ref = null; - Request.this.destroy(ref); + destroy(ref); } }