From 6c47e66f591b24de519f8cf24a4bda5384707dcc Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Fri, 19 Dec 2025 23:29:37 -0800 Subject: [PATCH] Road grading. Refactors dependency tree. Updates dependencies and workflows. Signed-off-by: Laird Nelson --- .../mvn-release-prepare-perform.yaml | 8 +- .github/workflows/mvn-verify.yaml | 8 +- .mvn/wrapper/maven-wrapper.properties | 24 +- README.md | 2 +- mvnw | 50 ++- mvnw.cmd | 56 ++- pom.xml | 104 ++--- src/main/java/module-info.java | 1 + .../java/org/microbean/scopelet/Instance.java | 8 +- .../microbean/scopelet/MapBackedScopelet.java | 25 +- .../org/microbean/scopelet/NoneScopelet.java | 80 +++- .../microbean/scopelet/ScopedInstances.java | 396 ++++++++---------- .../java/org/microbean/scopelet/Scopelet.java | 150 +++++-- .../java/org/microbean/scopelet/Scopes.java | 218 ++++++++++ .../microbean/scopelet/SingletonScopelet.java | 7 +- src/site/site.xml | 2 +- .../TestDisposableReferenceStrategy.java | 54 --- .../scopelet/TestScopedInstances.java | 96 ----- .../org/microbean/scopelet/TestScopes.java | 86 ++++ 19 files changed, 824 insertions(+), 551 deletions(-) create mode 100644 src/main/java/org/microbean/scopelet/Scopes.java delete mode 100644 src/test/java/org/microbean/scopelet/TestDisposableReferenceStrategy.java delete mode 100644 src/test/java/org/microbean/scopelet/TestScopedInstances.java create mode 100644 src/test/java/org/microbean/scopelet/TestScopes.java diff --git a/.github/workflows/mvn-release-prepare-perform.yaml b/.github/workflows/mvn-release-prepare-perform.yaml index c858555..968b79f 100644 --- a/.github/workflows/mvn-release-prepare-perform.yaml +++ b/.github/workflows/mvn-release-prepare-perform.yaml @@ -24,20 +24,20 @@ 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' 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 d58dfb7..c652895 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 8ccd399..328d7b0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ dependency: org.microbean microbean-scopelet - 0.0.8 + 0.0.9 ``` 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 ae2e437..871454f 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™ Scopelet Site https://microbean.github.io/microbean-scopelet/ - - 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,13 +91,9 @@ true false - - - true - - https://oss.sonatype.org/ - 10 - + + ${project.name} v${project.version} + UTF8 UTF8 @@ -125,7 +111,7 @@ org.junit junit-bom - 5.12.2 + 6.0.1 pom import @@ -135,33 +121,39 @@ org.microbean microbean-assign - 0.0.5 + 0.0.11 org.microbean microbean-attributes - 0.0.2 + 0.0.5 org.microbean microbean-bean - 0.0.17 + 0.0.22 org.microbean microbean-construct - 0.0.10 + 0.0.18 org.microbean - microbean-reference + microbean-event 0.0.3 + + org.microbean + microbean-reference + 0.0.5 + + @@ -191,6 +183,12 @@ compile + + org.microbean + microbean-event + compile + + org.microbean microbean-reference @@ -218,11 +216,11 @@ maven-antrun-plugin - 3.1.0 + 3.2.0 maven-assembly-plugin - 3.7.1 + 3.8.0 maven-checkstyle-plugin @@ -332,13 +330,13 @@ com.puppycrawl.tools checkstyle - 10.12.6 + 12.3.0 maven-clean-plugin - 3.4.1 + 3.5.0 @@ -353,7 +351,7 @@ maven-compiler-plugin - 3.14.0 + 3.14.1 -Xlint:all @@ -363,7 +361,7 @@ maven-dependency-plugin - 3.8.1 + 3.9.0 maven-deploy-plugin @@ -371,12 +369,11 @@ maven-enforcer-plugin - 3.5.0 + 3.6.2 maven-gpg-plugin - - 3.2.7 + 3.2.8 maven-install-plugin @@ -384,11 +381,11 @@ maven-jar-plugin - 3.4.2 + 3.5.0 maven-javadoc-plugin - 3.11.2 + 3.12.0 true @@ -420,16 +417,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 @@ -441,7 +437,7 @@ maven-source-plugin - 3.3.1 + 3.4.0 attach-sources @@ -453,7 +449,7 @@ maven-surefire-plugin - 3.5.3 + 3.5.4 maven-toolchains-plugin @@ -462,35 +458,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.0 + 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 @@ -536,8 +522,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 2287955..b58b070 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -26,6 +26,7 @@ requires transitive org.microbean.bean; requires org.microbean.constant; requires org.microbean.construct; + requires org.microbean.event; requires transitive org.microbean.reference; } diff --git a/src/main/java/org/microbean/scopelet/Instance.java b/src/main/java/org/microbean/scopelet/Instance.java index b2b158b..6eb031a 100644 --- a/src/main/java/org/microbean/scopelet/Instance.java +++ b/src/main/java/org/microbean/scopelet/Instance.java @@ -35,7 +35,7 @@ * * @see Destruction */ -public final class Instance implements AutoCloseable, Supplier { +final class Instance implements AutoCloseable, Supplier { /* @@ -83,9 +83,9 @@ public final class Instance implements AutoCloseable, Supplier { * * @param destruction a {@link Destruction}; may be {@code null} */ - public Instance(final I contextualInstance, - final Destructor destructor, - final Destruction destruction) { + Instance(final I contextualInstance, + final Destructor destructor, + final Destruction destruction) { super(); this.destruction = destruction; this.object = contextualInstance; diff --git a/src/main/java/org/microbean/scopelet/MapBackedScopelet.java b/src/main/java/org/microbean/scopelet/MapBackedScopelet.java index 67b405b..088bb84 100644 --- a/src/main/java/org/microbean/scopelet/MapBackedScopelet.java +++ b/src/main/java/org/microbean/scopelet/MapBackedScopelet.java @@ -156,6 +156,8 @@ private final Supplier supplier(final Object id, if (creationLock == newLock) { + // The creation lock is our new lock, which means there wasn't a pre-existing creation lock. Create. + try { // (The finally block will unlock creationLock/newLock.) @@ -186,18 +188,19 @@ private final Supplier supplier(final Object id, } } - // There was a Lock in the creationLocks map already that was not the newLock we just created. That's either another - // thread performing creation (an OK situation) or we have re-entered this method on the current thread and have - // encountered a newLock ancestor (probably not such a great situation). In any event, "our" newLock was never - // inserted into the map. It will therefore be unlocked (it was never locked in the first place). Discard it in - // preparation for switching locks to creationLock instead. + // The creationLock was not our newLock. That means there was a Lock in the creationLocks map already that was not + // the newLock we just created. That's either another thread performing creation (an OK situation) or we have + // re-entered this method on the current thread and have encountered a newLock ancestor (probably not such a great + // situation). In any event, "our" newLock was never inserted into the map. It will therefore be unlocked (it was + // never locked in the first place). Discard it in preparation for switching locks to creationLock instead. assert !newLock.isLocked() : "newLock was locked: " + newLock; assert !this.creationLocks.containsValue(newLock) : "Creation locks contained " + newLock + "; creationLock: " + creationLock; // Lock and unlock in rapid succession. Why? lock() will block if another thread is currently creating, and will - // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). + // return immediately if it is not. This is kind of a cheap way of doing Object.wait(). (Probably could use a + // Condition or a Semaphore here too. This is simple.) try { - creationLock.lock(); // potentially blocks + creationLock.lock(); // potentially blocks if creationLock is locked by another thread } finally { creationLock.unlock(); } @@ -233,6 +236,14 @@ public boolean remove(final Object id) { return false; } + @Override // Scopelet + public boolean removes() { + if (!this.active()) { + throw new InactiveScopeletException(); + } + return true; + } + /* * Static methods. diff --git a/src/main/java/org/microbean/scopelet/NoneScopelet.java b/src/main/java/org/microbean/scopelet/NoneScopelet.java index b81e2d1..850b6ca 100644 --- a/src/main/java/org/microbean/scopelet/NoneScopelet.java +++ b/src/main/java/org/microbean/scopelet/NoneScopelet.java @@ -13,22 +13,29 @@ */ package org.microbean.scopelet; +import java.lang.System.Logger; + import java.lang.constant.ClassDesc; import java.lang.constant.Constable; import java.lang.constant.ConstantDesc; import java.lang.constant.DynamicConstantDesc; -import java.lang.constant.MethodHandleDesc; import java.util.Optional; -import org.microbean.bean.AutoCloseableRegistry; import org.microbean.bean.Creation; import org.microbean.bean.Destruction; -import org.microbean.bean.DisposableReference; import org.microbean.bean.Factory; +import org.microbean.reference.DestructorRegistry; + import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.MethodHandleDesc.ofConstructor; + +import static java.lang.System.getLogger; + +import static java.lang.System.Logger.Level.WARNING; + /** * A {@link Scopelet} implementation that does not cache objects at all. * @@ -36,8 +43,19 @@ */ public class NoneScopelet extends Scopelet implements Constable { - private static final boolean useDisposableReferences = - Boolean.parseBoolean(System.getProperty("useDisposableReferences", "false")); + + /* + * Static fields. + */ + + + private static final Logger LOGGER = getLogger(NoneScopelet.class.getName()); + + + /* + * Constructors. + */ + /** * Creates a new {@link NoneScopelet}. @@ -46,6 +64,42 @@ public NoneScopelet() { super(); } + + /* + * Instance methods. + */ + + + /** + * Checks to see if this {@link Scopelet} {@linkplain #active() is active} and then returns a contextual instance + * {@linkplain Factory#create(Creation) created by the supplied Factory}. + * + *

This method (and its overrides) may return {@code null}.

+ * + *

If the supplied {@link Factory} is {@code null}, this method will (and its overrides must) return {@code null}. + * + * @param ignoredBeanId an identifier; ignored by the default implementation; may be {@code null} + * + * @param factory a {@link Factory}; may be {@code null} in which case {@code null} will and must be returned + * + * @param creation a {@link Creation}, typically the one in effect that is causing this method to be invoked in the + * first place; may be {@code null}; most commonly also an instance of {@link DestructorRegistry} + * + * @return a contextual instance, or {@code null} + * + * @exception InactiveScopeletException if this {@link Scopelet} {@linkplain #active() is not active} + * + * @exception ClassCastException if destruction is called for, {@code creation} is non-{@code null}, and {@code + * creation} does not implement {@link org.microbean.bean.Destruction}, a requirement of its contract + * + * @see DestructorRegistry + * + * @see Creation + * + * @see Destruction + * + * @see Factory#destroys() + */ // All parameters are nullable. // Non-final to permit subclasses to, e.g., add logging. @Override // Scopelet @@ -57,14 +111,10 @@ public I instance(final Object ignoredBeanId, final Factory factory, fina } final I returnValue = factory.create(creation); if (factory.destroys()) { - if (useDisposableReferences) { - // Merely creating a DisposableReference will cause it to get disposed *IF* garbage collection runs (which is not - // guaranteed). - new DisposableReference<>(returnValue, referent -> factory.destroy(referent, (Destruction)creation)); - } else if (creation instanceof AutoCloseableRegistry acr) { - acr.register(new Instance(returnValue, factory::destroy, (Destruction)creation)); - } else { - // TODO: warn or otherwise point out that dependencies will not be destroyed + if (creation instanceof DestructorRegistry dr && creation instanceof Destruction d) { + dr.register(returnValue, () -> factory.destroy(returnValue, d)); + } else if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, "Dependent objects will not be destroyed"); } } return returnValue; @@ -72,9 +122,7 @@ public I instance(final Object ignoredBeanId, final Factory factory, fina @Override // Constable public Optional describeConstable() { - return - Optional.of(DynamicConstantDesc.of(BSM_INVOKE, - MethodHandleDesc.ofConstructor(ClassDesc.of(this.getClass().getName())))); + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, ofConstructor(ClassDesc.of(this.getClass().getName())))); } } diff --git a/src/main/java/org/microbean/scopelet/ScopedInstances.java b/src/main/java/org/microbean/scopelet/ScopedInstances.java index 0074ad2..05e2a04 100644 --- a/src/main/java/org/microbean/scopelet/ScopedInstances.java +++ b/src/main/java/org/microbean/scopelet/ScopedInstances.java @@ -17,35 +17,37 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Queue; +import java.util.Set; import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Supplier; import javax.lang.model.type.TypeMirror; +import org.microbean.assign.AttributedType; +import org.microbean.assign.Selectable; + import org.microbean.attributes.Attributed; import org.microbean.attributes.Attributes; import org.microbean.attributes.BooleanValue; -import org.microbean.bean.AmbiguousReductionException; -import org.microbean.bean.AttributedType; +import org.microbean.bean.AmbiguousResolutionException; import org.microbean.bean.Bean; import org.microbean.bean.Creation; import org.microbean.bean.Factory; import org.microbean.bean.Id; -import org.microbean.bean.RankedReducer; -import org.microbean.bean.Reducer; -import org.microbean.bean.Reducible; -import org.microbean.bean.Selectable; +import org.microbean.bean.Qualifiers; +import org.microbean.bean.ReferencesSelector; import org.microbean.construct.Domain; import org.microbean.reference.Instances; -import static org.microbean.assign.Qualifiers.anyQualifier; -import static org.microbean.assign.Qualifiers.primordialQualifier; -import static org.microbean.assign.Qualifiers.qualifier; +import static java.util.Objects.requireNonNull; /** * An {@link Instances} implementation that is based on scopes. @@ -64,14 +66,19 @@ public class ScopedInstances implements Instances { */ - private static final Attributes FOR_INSTANTIATION = Attributes.of("ForInstantiation"); - + // Note: deliberately not a scope or qualifier + private static final Attributes CONSIDER_ACTIVENESS = Attributes.of("ConsiderActiveness"); + /* * Instance fields. */ + private final Qualifiers qualifiers; + + private final Scopes scopes; + private final TypeMirror scopeletType; @@ -85,10 +92,16 @@ public class ScopedInstances implements Instances { * * @param domain a {@link Domain}; must not be {@code null} * - * @exception NullPointerException if {@code domain} is {@code null} + * @param qualifiers a {@link Qualifiers}; must not be {@code null} + * + * @param scopes a {@link Scopes}; must not be {@code null} + * + * @exception NullPointerException if any argument is {@code null} */ - public ScopedInstances(final Domain domain) { + public ScopedInstances(final Domain domain, final Qualifiers qualifiers, final Scopes scopes) { super(); + this.qualifiers = requireNonNull(qualifiers, "qualifiers"); + this.scopes = requireNonNull(scopes, "scopes"); this.scopeletType = scopeletType(domain); } @@ -99,69 +112,49 @@ public ScopedInstances(final Domain domain) { /** - * Calls the {@link #findScopeId(Collection)} method with the result of an invocation of the {@link - * Attributes#attributes()} method on the supplied {@link Attributes} and returns the result. - * - * @param a an {@link Attributes}; normally itself a scope; must not be {@code null} + * Returns {@code true} if and only if the supplied {@link Id} is proxiable. * - * @return the first {@link Attributes} found in the supplied {@link Attributes}' {@linkplain Attributes#attributes() - * attributes} that is a scope, or {@code null} + * @param id an {@link Id}; must not be {@code null} * - * @exception NullPointerException if {@code a} is {@code null} + * @return {@code true} if and only if the supplied {@link Id} is proxiable * - * @see #findScopeId(Collection) + * @exception NullPointerException if {@code id} is {@code null} */ - private final Attributes findScopeId(final Attributes a) { - return this.findScopeId(a.attributes()); - } - - private final Attributes findScopeId(final Id id) { - // Looks for an Any qualifier, which every bean must possess, and then looks on *it* for the scope. This allows us - // to "tunnel" scopes (which are Qualifiers in this implementation) without disrupting typesafe resolution, since - // meta-attributes are not part of an Attributes' equality computation. - final Object anyQualifier = anyQualifier(); - Attributes scopeId = null; - for (final Attributes a : id.attributes()) { - if (a.equals(anyQualifier)) { - scopeId = this.findScopeId(a); - break; - } - } - if (scopeId == null) { - throw new IllegalArgumentException("id: " + id); + @Override // Instances + public boolean proxiable(final Id id) { + if (!id.types().proxiable()) { + return false; } - return scopeId; + final Attributes scopeId = this.findScope(id); + return scopeId != null && this.scopes.normal(scopeId); } - /** - * Finds and returns the nearest scope identifier in the forest represented by the supplied {@link - * Attributes}. - * - * @param c a {@link Collection} of {@link Attributes}; must not be {@code null} - * - * @return the nearest scope identifier in the forest represented by the supplied {@link - * Attributes}, or {@code null} - * - * @exception NullPointerException if {@code c} is {@code null} - */ - protected Attributes findScopeId(final Collection c) { - if (c.isEmpty()) { - return null; + @Override // Instances + public final Supplier supplier(final Bean bean, final Creation request) { + final Id id = bean.id(); + final Attributes scopeId = this.findScope(id); + // In this implementation, all Ids must have scopes. + if (scopeId == null) { + throw new IllegalStateException(); } - // Breadth first on purpose. Scope Attributes closer to the Attributes they attribute win over Scope Attributes - // further away. - final Queue q = new ArrayDeque<>(c); - while (!q.isEmpty()) { - final Attributes a = q.poll(); - if (this.isScopeId(a)) { - return a; + final Factory factory = bean.factory(); + if (factory instanceof Scopelet && this.primordial(scopeId)) { + // This is a request for, e.g., the Singleton Scopelet, which backs the primordial (notional) singleton scope. + // Scopelets are always their own factories. The Scopelet implementing the primordial scope (normally Singleton) + // is not made or stored by any other Scopelet. + I scopelet = factory.singleton(); + if (scopelet == null) { + return () -> factory.create(request); } - q.addAll(a.attributes()); + assert scopelet == factory : "scopelet != factory: " + scopelet + " != " + factory; + return factory::singleton; } - return null; + final AttributedType st = this.scopeletAttributedType(scopeId); + // Get the Scopelet and have it provide the instance + return () -> request.>reference(st).instance(id, factory, request); // assumes Scopelet inactivity is handled } - /** + /* * Returns {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a * scope. * @@ -170,36 +163,15 @@ protected Attributes findScopeId(final Collection c) { * @return {@code true} if and only if the supplied {@link Attributes} is deemed to be an identifier of a scope * * @exception NullPointerException if {@code a} is {@code null} + * + * @see Scopes#scope(Attributes) + * + * @deprecated Use {@link Scopes#scope(Attributes)} instead. */ - protected boolean isScopeId(final Attributes a) { - boolean scopeFound = false; - boolean qualifierFound = false; - for (final Attributes a0 : a.attributes()) { - if (scopeFound) { - if (!qualifierFound && a0.equals(qualifier())) { - return true; - } - } else if (qualifierFound) { - if (a0.equals(Scopelet.SCOPE)) { - return true; - } - } else if (a0.equals(Scopelet.SCOPE)) { - scopeFound = true; - } else if (a0.equals(qualifier())) { - qualifierFound = true; - } - } - return false; - } - - private final boolean normal(final Attributes a) { - final BooleanValue v = a.value("normal"); - return v != null && v.value(); - } - - private final boolean primordial(final Attributed a) { - return this.primordial(a.attributes()); - } + // @Deprecated(forRemoval = true) + // protected boolean isScopeId(final Attributes a) { + // return this.scopes.scope(a); + // } /** * Returns {@code true} if and only if the supplied {@link Collection} of {@link Attributes} is deemed to designate @@ -207,7 +179,7 @@ private final boolean primordial(final Attributed a) { * *

The default implementation of this method returns {@code true} if and only if the supplied {@link Collection} * {@linkplain Collection#contains(Object) contains} the {@linkplain - * org.microbean.assign.Qualifiers#primordialQualifier() primordial qualifier}.

+ * org.microbean.bean.Qualifiers#primordialQualifier() primordial qualifier}.

* * @param c a {@link Collection}; must not be {@code null} * @@ -215,51 +187,57 @@ private final boolean primordial(final Attributed a) { * something as primordial * * @exception NullPointerException if {@code c} is {@code null} + * + * @see Qualifiers#primordialQualifier() */ protected boolean primordial(final Collection c) { - return c.contains(primordialQualifier()); + return c.contains(this.qualifiers.primordialQualifier()); } /** - * Returns {@code true} if and only if the supplied {@link Id} is proxiable. + * Finds and returns the nearest scope identifier in the forest represented by the supplied {@link + * Attributes}. * - * @param id an {@link Id}; must not be {@code null} + * @param c a {@link Collection} of {@link Attributes}; must not be {@code null} * - * @return {@code true} if and only if the supplied {@link Id} is proxiable + * @return the nearest scope identifier in the forest represented by the supplied {@link + * Attributes}, or {@code null} * - * @exception NullPointerException if {@code id} is {@code null} + * @exception NullPointerException if {@code c} is {@code null} + * + * @see Scopes#findScope(Collection) + * + * @deprecated Please use {@link Scopes#findScope(Collection)} instead. */ - @Override // Instances - public boolean proxiable(final Id id) { - if (!id.types().proxiable()) { - return false; - } - final Attributes scopeId = this.findScopeId(id); - return scopeId != null && this.normal(scopeId); + @Deprecated(forRemoval = true) + final Attributes findScopeId(final Collection c) { + return this.scopes.findScope(c); } - @Override // Instances - public final Supplier supplier(final Bean bean, final Creation request) { - final Id id = bean.id(); - final Attributes scopeId = this.findScopeId(id); - // In this implementation, all Ids must have scopes. - if (scopeId == null) { - throw new IllegalStateException(); - } - final Factory factory = bean.factory(); - if (factory instanceof Scopelet && this.primordial(scopeId)) { - // This is a request for, e.g., the Singleton Scopelet, which backs the primordial (notional) singleton scope. - // Scopelets are always their own factories. The Scopelet implementing the primordial scope (normally Singleton) - // is not made or stored by any other Scopelet. - final I scopelet = factory.singleton(); - if (scopelet == null) { - return () -> factory.create(request); + private final Attributes findScope(final Id id) { + // Looks for an Any qualifier, which every bean must possess, and then looks on *it* for the scope. This allows us + // to "tunnel" scopes (which are Qualifiers in this implementation) without disrupting typesafe resolution, since + // meta-attributes are not part of an Attributes' equality computation. + final Object anyQualifier = this.qualifiers.anyQualifier(); + Attributes scopeId = null; + for (final Attributes a : id.attributes()) { + if (a.equals(anyQualifier)) { + scopeId = this.scopes.findScope(a.attributes()); + break; } - assert scopelet == factory : "scopelet != factory: " + scopelet + " != " + factory; - return factory::singleton; } - final AttributedType t = AttributedType.of(this.scopeletType, findScopeId(scopeId), FOR_INSTANTIATION); - return () -> request.>references(t).get().instance(id, factory, request); // assumes a specific kind of reduction; see #reducible + if (scopeId == null) { + throw new IllegalArgumentException("id: " + id); + } + return scopeId; + } + + private final boolean primordial(final Attributed a) { + return this.primordial(a.attributes()); + } + + private final AttributedType scopeletAttributedType(final Attributes scopeId) { + return AttributedType.of(this.scopeletType, scopeId, CONSIDER_ACTIVENESS); } @@ -268,14 +246,56 @@ public final Supplier supplier(final Bean bean, final Creati */ + /** + * Returns a {@link Selectable Selectable<AttributedType, Bean<?>>} that properly considers the fact that + * a {@link Scopelet} may be {@linkplain Scopelet#active() active or inactive} at any point for any reason. + * + * @param domain a {@link Domain}; must not be {@code null} + * + * @param selectable a {@link Selectable} that will be used for all {@link AttributedType}s other than {@link + * Scopelet} types being sought for the purpose of instantiating or acquiring contextual instances; must not be {@code + * null} + * + * @return a non-{@code null} {@link Selectable} + * + * @exception NullPointerException if any argument is {@code null} + */ + public static final Selectable> selectableOf(final Domain domain, + final Selectable> selectable) { + Objects.requireNonNull(selectable, "selectable"); + final Selectable> scopeletSelectable = c -> { + Bean activeScopeletBean = null; + for (final Bean b : selectable.select(c)) { + if (((Scopelet)b.factory()).active()) { + if (activeScopeletBean == null) { + activeScopeletBean = b; + } else { + throw new TooManyActiveScopeletsException("scopelet1: " + activeScopeletBean + "; scopelet2: " + b); + } + } + } + return activeScopeletBean == null ? List.of() : List.of(activeScopeletBean); + }; + final TypeMirror scopeletType = scopeletType(domain); + return c -> + domain.sameType(scopeletType, c.type()) && c.attributes().contains(CONSIDER_ACTIVENESS) ? + // A ScopedInstances is requesting a Scopelet for the purposes of instantiating something else. Use the + // scopeletSelectable. + scopeletSelectable.select(c) : + // A ScopedInstances is requesting something "normal". Use the unadorned supplied Selectable. + selectable.select(c); + } + // Invoked by method reference only - static final Bean handleInactiveScopelets(final Collection> beans, final AttributedType attributedType) { + // (Actually, not used?) + @Deprecated(forRemoval = true) + private static final Bean handleInactiveScopelets(final Collection> beans, final AttributedType attributedType) { if (beans.size() < 2) { // 2 because we're disambiguating throw new IllegalArgumentException("beans: " + beans); } Bean b2 = null; Scopelet s2 = null; - final Iterator> i = beans.iterator(); // we use Iterator for good reasons + final Iterator> i = beans.iterator(); while (i.hasNext()) { final Bean b1 = i.next(); if (b1.factory() instanceof Scopelet s1) { @@ -297,26 +317,20 @@ static final Bean handleInactiveScopelets(final Collection> } } assert b2 != null; - // if (s2.scopeId().equals(s1.scopeId())) { // TODO: would like to make this go away - if (s2.active()) { - if (s1.active()) { - throw new TooManyActiveScopeletsException("scopelet1: " + s1 + "; scopelet2: " + s2); - } - // drop s1; keep s2 - } else if (s1.active()) { - // drop s2; keep s1 - s2 = s1; - b2 = b1; - } else { - // both are inactive; drop 'em both and keep going - s2 = null; - b2 = null; + if (s2.active()) { + if (s1.active()) { + throw new TooManyActiveScopeletsException("scopelet1: " + s1 + "; scopelet2: " + s2); } - // } else { - // s2 = null; - // b2 = null; - // break; - // } + // drop s1; keep s2 + } else if (s1.active()) { + // drop s2; keep s1 + s2 = s1; + b2 = b1; + } else { + // both are inactive; drop 'em both and keep going + s2 = null; + b2 = null; + } } else { s2 = null; b2 = null; @@ -324,96 +338,16 @@ static final Bean handleInactiveScopelets(final Collection> } } if (s2 == null) { - throw new AmbiguousReductionException(attributedType, - beans, - "TODO: this message needs to be better; can't resolve these alternates: " + beans); + throw new AmbiguousResolutionException(attributedType, + beans, + "TODO: this message needs to be better; can't resolve these alternates: " + beans); } assert b2 != null; return b2; } - static final TypeMirror scopeletType(final Domain domain) { + private static final TypeMirror scopeletType(final Domain domain) { return domain.declaredType(null, domain.typeElement(Scopelet.class.getCanonicalName()), domain.wildcardType()); } - /** - * Returns a {@link Reducible} suitable for use with {@link Scopelet}s. - * - * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null} - * - * @param selectable a {@link Selectable}; must not be {@code null} - * - * @return a non-{@code null} {@link Reducible} - * - * @exception NullPointerException if any argument is {@code null} - * - * @see #reducible(Domain, Selectable, Reducer) - * - * @see RankedReducer#of() - */ - public static final Reducible> reducible(final Domain domain, - final Selectable> selectable) { - return reducible(domain, selectable, RankedReducer.of()); - } - - /** - * Returns a {@link Reducible} suitable for use with {@link Scopelet}s. - * - * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null} - * - * @param selectable a {@link Selectable}; must not be {@code null} - * - * @param reducer a {@link Reducer}; must not be {@code null} - * - * @return a non-{@code null} {@link Reducible} - * - * @exception NullPointerException if any argument is {@code null} - * - * @see #reducible(Domain, Selectable, Reducer, BiFunction) - * - * @see Reducer#fail(List, Object) - */ - public static final Reducible> reducible(final Domain domain, - final Selectable> selectable, - final Reducer> reducer) { - return reducible(domain, selectable, reducer, Reducer::fail); - } - - /** - * Returns a {@link Reducible} suitable for use with {@link Scopelet}s. - * - * @param domain a {@link Domain} (that is normally shared among other cooperating components); must not be {@code null} - * - * @param selectable a {@link Selectable}; must not be {@code null} - * - * @param reducer a {@link Reducer}; must not be {@code null} - * - * @param failureHandler a {@link BiFunction} serving as the supplied {@link Reducer}'s failure handler; - * must not be {@code null} - * - * @return a non-{@code null} {@link Reducible} - * - * @exception NullPointerException if any argument is {@code null} - */ - public static final Reducible> - reducible(final Domain domain, - final Selectable> selectable, - final Reducer> reducer, - final BiFunction>, ? super AttributedType, ? extends Bean> failureHandler) { - // Normal reductions are cached. - final Reducible> cachingReducible = Reducible.ofCaching(selectable, reducer, failureHandler); - // Reductions of scopelets can't be cached because a Scopelet may be active or inactive at any point for any reason. - final Reducible> scopeletReducible = - Reducible.>of(selectable, reducer, ScopedInstances::handleInactiveScopelets); - final TypeMirror scopeletType = scopeletType(domain); - return c -> - (domain.sameType(scopeletType, c.type()) && c.attributes().contains(FOR_INSTANTIATION) ? - // A ScopedInstances is requesting a Scopelet for the purposes of instantiating something else. Use the - // scopeletReducible. - scopeletReducible : - // A ScopedInstances is requesting something "normal". Use the cachingReducible. - cachingReducible) - .reduce(c); - } - } diff --git a/src/main/java/org/microbean/scopelet/Scopelet.java b/src/main/java/org/microbean/scopelet/Scopelet.java index 40154d9..28741ba 100644 --- a/src/main/java/org/microbean/scopelet/Scopelet.java +++ b/src/main/java/org/microbean/scopelet/Scopelet.java @@ -19,18 +19,24 @@ import java.util.List; import java.util.Map; +import org.microbean.assign.AttributedType; +import org.microbean.assign.Qualifiers; + import org.microbean.attributes.Attributes; import org.microbean.attributes.BooleanValue; import org.microbean.attributes.Value; import org.microbean.bean.Bean; import org.microbean.bean.Creation; +import org.microbean.bean.Destruction; import org.microbean.bean.Factory; +import org.microbean.bean.ReferencesSelector; -import static java.lang.invoke.MethodHandles.lookup; +import org.microbean.construct.Domain; -import static org.microbean.assign.Qualifiers.primordialQualifier; -import static org.microbean.assign.Qualifiers.qualifier; +import org.microbean.event.Events; + +import static java.lang.invoke.MethodHandles.lookup; /** * A manager of object lifespans on behalf of one or more notional scopes. @@ -63,38 +69,6 @@ public abstract class Scopelet> implements AutoCloseable, } } - /** - * An {@link Attributes} identifying the scope designator. - */ - public static final Attributes SCOPE = Attributes.of("Scope"); - - private static final Map> normalScope = Map.of("normal", BooleanValue.of(true)); - - private static final Map> pseudoScope = Map.of("normal", BooleanValue.of(false)); - - /** - * An {@link Attributes} identifying the (well-known) singleton pseudo-scope. - * - *

The {@link Attributes} constituting the singleton pseudo-scope identifier is {@linkplain Attributes#attributes() - * attributed} with {@linkplain #SCOPE the scope designator}, {@linkplain org.microbean.assign.Qualifiers#qualifier() - * the qualifier designator}, and {@linkplain org.microbean.assign.Qualifiers#primordialQualifier() the primordial - * qualifier}, indicating that the scope it identifies governs itself.

- */ - public static final Attributes SINGLETON_ID = - Attributes.of("Singleton", pseudoScope, Map.of(), Map.of("Singleton", List.of(qualifier(), SCOPE, primordialQualifier()))); - - /** - * An {@link Attributes} identifying the (well-known and normal) application scope. - */ - public static final Attributes APPLICATION_ID = - Attributes.of("Application", normalScope, Map.of(), Map.of("Application", List.of(qualifier(), SCOPE, SINGLETON_ID))); - - /** - * An {@link Attributes} identifying the (well-known) none pseudo-scope. - */ - public static final Attributes NONE_ID = - Attributes.of("None", pseudoScope, Map.of(), Map.of("None", List.of(qualifier(), SCOPE, SINGLETON_ID))); - /* * Instance fields. @@ -127,19 +101,87 @@ protected Scopelet() { /** * Creates this {@link Scopelet} by simply returning it. * + * @param c a {@link Creation}; may be {@code null} in certain primordial cases + * * @return this {@link Scopelet} */ @Override // Factory @SuppressWarnings("unchecked") - public final S create(final Creation r) { + public final S create(final Creation c) { + if (this.closed()) { + throw new IllegalStateException("closed"); + } if (ME.compareAndSet(this, null, this)) { // volatile write - if (r != null) { - // TODO: emit initialized event + if (c != null) { + this.fireScopeletInitialized(c); } } return (S)this; } + @Override + public final void destroy(final S me, final Destruction creation) { + if (this.closed()) { + throw new IllegalStateException("closed"); + } + if (creation == null) { + Factory.super.destroy(me, creation); + this.me = null; // volatile write + return; + } else if (!(creation instanceof Creation)) { + throw new IllegalArgumentException("creation: " + creation); + } + final Creation c = (Creation)creation; + this.fireScopeletDestroying(c); + Factory.super.destroy(me, creation); + this.me = null; // volatile write + this.fireScopeletDestroyed(c); + } + + /** + * Informs any interested observers that this {@link Scopelet} is about to be destroyed. + * + * @param r a {@link ReferencesSelector}; must not be {@code null} + * + * @exception NullPointerException if {@code r} is {@code null} + */ + protected void fireScopeletDestroying(final ReferencesSelector r) { + + } + + /** + * Informs any interested observers that this {@link Scopelet} has been irrevocably destroyed. + * + * @param r a {@link ReferencesSelector}; must not be {@code null} + * + * @exception NullPointerException if {@code r} is {@code null} + */ + protected void fireScopeletDestroyed(final ReferencesSelector r) { + + } + + /** + * Informs any interested observers that this {@link Scopelet} has just been initialized. + * + * @param r a {@link ReferencesSelector}; must not be {@code null} + * + * @exception NullPointerException if {@code r} is {@code null} + */ + // The specification says scopes should fire an event when they're open for business but there are lots of weird + // ramifications to this. We break this out into a protected method so overrides can do what they want, or nothing at + // all. + protected void fireScopeletInitialized(final ReferencesSelector r) { + // final Domain d = r.domain(); + // final Events e = r.reference(new AttributedType(d.declaredType(d.typeElement(Events.class.getCanonicalName())), + // defaultQualifiers())); + // if (e != null) { + // e.fire(null, // typeArgumentSource; not needed here; maybe could do wild S reflective introspection + // List.of(), // qualifiers/attributes; TODO: @Initialized + // this, // event object; can be anything + // c); + // } + } + /** * Returns this {@link Scopelet} if it has been created via the {@link #create(Creation)} method, or {@code null} if * that method has not yet been invoked. @@ -150,6 +192,9 @@ public final S create(final Creation r) { */ @Override // Factory public final S singleton() { + if (this.closed()) { + throw new IllegalStateException("closed"); + } return this.me; // volatile read } @@ -206,10 +251,14 @@ public boolean active() { * * @exception InactiveScopeletException if this {@link Scopelet} {@linkplain #active() is not active} * - * @exception ClassCastException if {@code creation} is non-{@code null} and does not implement {@link - * org.microbean.bean.Destruction}, a requirement of its contract + * @exception ClassCastException if destruction is called for, {@code creation} is non-{@code null}, and {@code + * creation} does not implement {@link org.microbean.bean.Destruction}, a requirement of its contract * * @see Creation + * + * @see org.microbean.bean.Destruction + * + * @see Factory#destroys() */ public abstract I instance(final Object id, final Factory factory, final Creation creation); @@ -233,6 +282,27 @@ public boolean remove(final Object id) { return false; } + /** + * Returns {@code true} if and only if this {@link Scopelet} stores contextual instances, and hence is capable of + * {@linkplain #remove(Object) removing} them. + * + *

The default implementation of this method returns {@code false}. Subclasses are encouraged to + * override it as appropriate.

+ * + * @return {@code true} if and only if this {@link Scopelet} stores contextual instances, and hence is capable of + * {@linkplain #remove(Object) removing} them + * + * @exception InactiveScopeletException if this {@link Scopelet} is not {@linkplain #active() active} + * + * @see #remove(Object) + */ + public boolean removes() { + if (!this.active()) { + throw new InactiveScopeletException(); + } + return false; + } + /** * Irrevocably closes this {@link Scopelet}, and, by doing so, notionally makes it irrevocably {@linkplain #closed() * closed} and {@linkplain #active() inactive}. diff --git a/src/main/java/org/microbean/scopelet/Scopes.java b/src/main/java/org/microbean/scopelet/Scopes.java new file mode 100644 index 0000000..a28fc2b --- /dev/null +++ b/src/main/java/org/microbean/scopelet/Scopes.java @@ -0,0 +1,218 @@ +/* -*- 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.scopelet; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +import org.microbean.bean.Qualifiers; + +import org.microbean.attributes.Attributes; +import org.microbean.attributes.BooleanValue; +import org.microbean.attributes.Value; + +import static java.util.Objects.requireNonNull; + +/** + * A utility class for working with scopes and their identifiers. + * + * @author Laird Nelson + */ +public class Scopes { // deliberately not final + + private static final Map> NORMAL = Map.of("normal", BooleanValue.of(true)); + + private static final Map> PSEUDO = Map.of("normal", BooleanValue.of(false)); + + private static final Attributes SCOPE = Attributes.of("Scope"); + + private final Attributes NONE; + + private final Attributes SINGLETON; + + private final Qualifiers qualifiers; + + /** + * Creates a new {@link Scopes}. + * + * @param qualifiers a {@link Qualifiers}; must not be {@code null} + * + * @exception NullPointerException if {@code qualifiers} is {@code null} + */ + public Scopes(final Qualifiers qualifiers) { + super(); + this.qualifiers = requireNonNull(qualifiers, "qualifiers"); + this.SINGLETON = + Attributes.of("Singleton", + PSEUDO, + Map.of(), + Map.of("Singleton", + List.of(qualifiers.qualifier(), + SCOPE, + qualifiers.primordialQualifier()))); + this.NONE = + Attributes.of("None", + PSEUDO, + Map.of(), + Map.of("None", + List.of(qualifiers.qualifier(), + SCOPE, + SINGLETON))); + } + + /** + * Returns the first {@link Attributes} that is present in the supplied {@link Collection} of {@link Attributes}, and + * their {@linkplain Attributes#attributes() meta-attributes}, for which an invocation of the {@link + * #scope(Attributes)} method returns {@code true}, or {@code null}, if no such {@link Attributes} exists. + * + *

The search is conducted in a breadth-first manner.

+ * + * @param c a {@link Collection} of {@link Attributes}; must not be {@code null} + * + * @return the first {@link Attributes} that is present in the supplied {@link Collection} of {@link Attributes}, and + * their {@linkplain Attributes#attributes() meta-attributes}, for which an invocation of the {@link + * #scope(Attributes)} method returns {@code true}, or {@code null}, if no such {@link Attributes} exists + * + * @exception NullPointerException if {@code c} is {@code null} + */ + public Attributes findScope(final Collection c) { + if (c.isEmpty()) { + return null; + } + // Breadth first on purpose. Scope Attributes closer to the Attributes they attribute win over Scope Attributes + // further away. + final Queue q = new ArrayDeque<>(c); + while (!q.isEmpty()) { + final Attributes a = q.poll(); + if (this.scope(a)) { + return a; + } + q.addAll(a.attributes()); + } + return null; + } + + /** + * Returns a non-{@code null}, determinate {@link Attributes} representing the identifier for the none + * scope. + * + * @return a non-{@code null}, determinate {@link Attributes} representing the identifier for the none + * scope + */ + public Attributes none() { + return NONE; + } + + /** + * Returns a non-{@code null}, determinate, immutable {@link Map} of {@linkplain Map#size() size} {@code 1} that can + * be used with a {@linkplain #scope() scope} to designate it as a normal scope. + * + * @return a non-{@code null}, determinate, immutable {@link Map} of {@linkplain Map#size() size} {@code 1} that can + * be used with a {@linkplain #scope() scope} to designate it as a normal scope + */ + public Map> normal() { + return NORMAL; + } + + /** + * Returns {@code true} if the supplied {@link Attributes} has elements that might be used to indicate that it is a + * normal scope. + * + * @param a an {@link Attributes}; must not be {@code null} + * + * @return {@code true} if the supplied {@link Attributes} has elements that might be used to indicate that it is a + * normal scope + * + * @exception NullPointerException if {@code a} is {@code null} + * + * @see #scope(Attributes) + * + * @see #normal() + */ + public boolean normal(final Attributes a) { + final BooleanValue v = a.value(this.normal().keySet().iterator().next()); + return v != null && v.value(); + } + + /** + * Returns a non-{@code null}, determinate, immutable {@link Map} of {@linkplain Map#size() size} {@code 1} that can + * be used with a {@linkplain #scope() scope} to designate it as a pseudo scope. + * + * @return a non-{@code null}, determinate, immutable {@link Map} of {@linkplain Map#size() size} {@code 1} that can + * be used with a {@linkplain #scope() scope} to designate it as a pseudo scope + */ + public Map> pseudo() { + return PSEUDO; + } + + /** + * Returns the non-{@code null}, determinate {@link Attributes} that can be used to designate other {@link Attributes} + * as a scope. + * + * @return the non-{@code null}, determinate {@link Attributes} that can be used to designate other {@link Attributes} + * as a scope + */ + public Attributes scope() { + return SCOPE; + } + + /** + * Returns {@code true} if and only if the supplied {@link Attributes} is a scope identifier. + * + * @param a an {@link Attributes}; must not be {@code null} + * + * @return {@code true} if and only if the supplied {@link Attributes} is a scope identifier + * + * @exception NullPointerException if {@code a} is {@code null} + */ + public boolean scope(final Attributes a) { + boolean scopeFound = false; + boolean qualifierFound = false; + for (final Attributes ma : a.attributes()) { + if (scopeFound) { + if (!qualifierFound && ma.equals(this.qualifiers.qualifier())) { + qualifierFound = true; + break; + } + } else if (qualifierFound) { + if (ma.equals(this.scope())) { + scopeFound = true; + break; + } + } else if (ma.equals(this.scope())) { + // a is annotated with @Scope + scopeFound = true; + } else if (ma.equals(this.qualifiers.qualifier())) { + // a is annotated with @Qualifier + qualifierFound = true; + } + } + return scopeFound && qualifierFound; + } + + /** + * Returns a non-{@code null}, determinate {@link Attributes} representing the identifier for the singleton + * pseudo-scope. + * + * @return a non-{@code null}, determinate {@link Attributes} representing the identifier for the singleton + * pseudo-scope + */ + public Attributes singleton() { + return SINGLETON; + } + +} diff --git a/src/main/java/org/microbean/scopelet/SingletonScopelet.java b/src/main/java/org/microbean/scopelet/SingletonScopelet.java index 8a0933b..0d0bf16 100644 --- a/src/main/java/org/microbean/scopelet/SingletonScopelet.java +++ b/src/main/java/org/microbean/scopelet/SingletonScopelet.java @@ -17,12 +17,13 @@ import java.lang.constant.Constable; import java.lang.constant.ConstantDesc; import java.lang.constant.DynamicConstantDesc; -import java.lang.constant.MethodHandleDesc; import java.util.Optional; import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.MethodHandleDesc.ofConstructor; + /** * A {@link MapBackedScopelet} implementation that caches singletons. * @@ -39,9 +40,7 @@ public SingletonScopelet() { @Override // Constable public Optional describeConstable() { - return - Optional.of(DynamicConstantDesc.of(BSM_INVOKE, - MethodHandleDesc.ofConstructor(ClassDesc.of(this.getClass().getName())))); + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, ofConstructor(ClassDesc.of(this.getClass().getName())))); } } 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/scopelet/TestDisposableReferenceStrategy.java b/src/test/java/org/microbean/scopelet/TestDisposableReferenceStrategy.java deleted file mode 100644 index ea900ce..0000000 --- a/src/test/java/org/microbean/scopelet/TestDisposableReferenceStrategy.java +++ /dev/null @@ -1,54 +0,0 @@ -/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- - * - * Copyright © 2023 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.scopelet; - -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; - -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import org.microbean.bean.DisposableReference; - -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 TestDisposableReferenceStrategy { - - private TestDisposableReferenceStrategy() { - super(); - } - - @Test - final void test() { - - final Consumer c = i -> System.out.println("destroying: " + i); - - Object i = Integer.valueOf(1); - WeakReference r = new DisposableReference(i, c); - assertTrue(r.refersTo(i)); - i = null; - // Simulate garbage collection - assertTrue(r.enqueue()); - assertNull(r.get()); - - i = Integer.valueOf(2); - r = new DisposableReference<>(i, c); - assertTrue(r.refersTo(i)); - - } - -} diff --git a/src/test/java/org/microbean/scopelet/TestScopedInstances.java b/src/test/java/org/microbean/scopelet/TestScopedInstances.java deleted file mode 100644 index f11b4dd..0000000 --- a/src/test/java/org/microbean/scopelet/TestScopedInstances.java +++ /dev/null @@ -1,96 +0,0 @@ -/* -*- 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.scopelet; - -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.microbean.bean.Reducer; -import org.microbean.bean.Selectable; - -import org.microbean.attributes.Attributes; -import org.microbean.attributes.BooleanValue; - -import org.microbean.construct.DefaultDomain; -import org.microbean.construct.Domain; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import static org.microbean.assign.Qualifiers.primordialQualifier; -import static org.microbean.assign.Qualifiers.qualifier; - -import static org.microbean.scopelet.Scopelet.NONE_ID; -import static org.microbean.scopelet.Scopelet.SCOPE; -import static org.microbean.scopelet.Scopelet.SINGLETON_ID; - -final class TestScopedInstances { - - private static Domain d; - - private static ScopedInstances i; - - private TestScopedInstances() { - super(); - } - - @BeforeAll - static final void setup() { - d = new DefaultDomain(); - i = new ScopedInstances(d); - } - - @Test - final void testIsScope() { - // The Scope Attributes is not, itself, a scope identifier. - assertFalse(i.isScopeId(SCOPE)); - - // SINGLETON_ID is a scope identifier because it is attributed with the Scope Attributes. - assertTrue(i.isScopeId(SINGLETON_ID)); - - // NONE_ID is a scope identifier for the same reason. - assertTrue(i.isScopeId(NONE_ID)); - - // Even though here the Any Attributes is attributed with NONE_ID, that doesn't make it a scope identifier (it is not - // attributed with the Scope Attributes; the Scope Attributes is not transitive). - final Attributes a = Attributes.of("Any", NONE_ID); - assertFalse(i.isScopeId(a)); - - // An instance of the None Attributes without any additional attributes is equal to the canonical NONE_ID Attributes - // which does have additional attributes. This is because by design only names and values are considered in equality - // comparisons. - assertEquals(Attributes.of("None", Map.of("normal", BooleanValue.of(false)), Map.of(), Map.of()), i.findScopeId(a.attributes())); - } - - @Test - final void testNoneIsNotPrimordial() { - assertSame(NONE_ID, i.findScopeId(List.of(NONE_ID))); - assertFalse(NONE_ID.attributes().contains(primordialQualifier())); - assertSame(SINGLETON_ID, i.findScopeId(NONE_ID.attributes())); - } - - @Test - final void testSingletonIsPrimordial() { - assertSame(SINGLETON_ID, i.findScopeId(List.of(SINGLETON_ID))); - assertTrue(SINGLETON_ID.attributes().contains(primordialQualifier())); - assertNull(i.findScopeId(SINGLETON_ID.attributes())); - } - -} diff --git a/src/test/java/org/microbean/scopelet/TestScopes.java b/src/test/java/org/microbean/scopelet/TestScopes.java new file mode 100644 index 0000000..546f2e9 --- /dev/null +++ b/src/test/java/org/microbean/scopelet/TestScopes.java @@ -0,0 +1,86 @@ +/* -*- 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.scopelet; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.microbean.attributes.Attributes; +import org.microbean.attributes.BooleanValue; + +import org.microbean.bean.Qualifiers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 TestScopes { + + private static Qualifiers qualifiers; + + private static Scopes scopes; + + private TestScopes() { + super(); + } + + @BeforeAll + static final void setup() { + qualifiers = new Qualifiers(); + scopes = new Scopes(qualifiers); + } + + @Test + final void testIsScope() { + // The Scope Attributes is not, itself, a scope identifier. + assertFalse(scopes.scope(scopes.scope())); + + // scopes.singleton() is a scope identifier because it is attributed with the Scope Attributes. + assertTrue(scopes.scope(scopes.singleton())); + + // scopes.none() is a scope identifier for the same reason. + assertTrue(scopes.scope(scopes.none())); + + // Even though here the Any Attributes is attributed with scopes.none(), that doesn't make it a scope identifier (it + // is not attributed with scopes.scope(); scopes.scope() is not transitive). + final Attributes a = Attributes.of("Any", scopes.none()); + assertFalse(scopes.scope(a)); + + // An instance of scopes.none() without any additional attributes is equal to the canonical scopes.none() which does + // have additional attributes. This is because by design only names and values are considered in equality + // comparisons. + assertEquals(Attributes.of("None", Map.of("normal", BooleanValue.of(false)), Map.of(), Map.of()), scopes.findScope(a.attributes())); + } + + @Test + final void testNoneIsNotPrimordial() { + assertSame(scopes.none(), scopes.findScope(List.of(scopes.none()))); + assertFalse(scopes.none().attributes().contains(qualifiers.primordialQualifier())); + assertSame(scopes.singleton(), scopes.findScope(scopes.none().attributes())); + } + + @Test + final void testSingletonIsPrimordial() { + assertSame(scopes.singleton(), scopes.findScope(List.of(scopes.singleton()))); + assertTrue(scopes.singleton().attributes().contains(qualifiers.primordialQualifier())); + assertNull(scopes.findScope((scopes.singleton().attributes()))); + + } + +}