Skip to content

Commit da9cf72

Browse files
committed
Add computeIfAbsent variants to stores for non-nullable types
Since both `ExtensionContext.Store` and its backing `NamespaceAwareStore` implementation allow storing `null` values, the `getOrComputeIfAbsent` methods potentially returned `null`, even if documented otherwise (in case of `getOrComputeIfAbsent(Class)` in `ExtensionContext.Store`). Now that the API uses JSpecify's nullability annotations, this became more apparent and also caused Jupiter `Extension` implementations to have to deal with values being potentially `null` despite their best intentions to only use `getOrComputeIfAbsent` to access a certain `key` with a function that never returned `null`. Therefore, this commit introduces null-safe variants called `computeIfAbsent` that treat `null` as absent in addition to the key not being present in the backing `Map`.
1 parent 9794f19 commit da9cf72

File tree

25 files changed

+410
-87
lines changed

25 files changed

+410
-87
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ repository on GitHub.
2929
* `ConversionSupport` now converts `String` to `Locale` using the IETF BCP 47 language tag
3030
format supported by the `Locale.forLanguageTag(String)` factory method instead of the
3131
format used by the deprecated `Locale(String)` constructor.
32+
* Deprecate `getOrComputeIfAbsent(...)` methods in `NamespacedHierarchicalStore` in favor
33+
of the new `computeIfAbsent(...)` methods.
3234

3335
[[release-notes-6.0.0-M2-junit-platform-new-features-and-improvements]]
3436
==== New Features and Improvements
@@ -51,6 +53,8 @@ repository on GitHub.
5153
* Provide cancellation support for the Suite and Vintage test engines
5254
* Introduce `TestTask.getTestDescriptor()` method for use in
5355
`HierarchicalTestExecutorService` implementations.
56+
* Introduce `computeIfAbsent(...)` methods in `NamespacedHierarchicalStore` to simplify
57+
working with non-nullable types.
5458

5559

5660
[[release-notes-6.0.0-M2-junit-jupiter]]
@@ -72,12 +76,16 @@ repository on GitHub.
7276
configuration parameter. `Locale` conversions are now always performed using the IETF
7377
BCP 47 language tag format supported by the `Locale.forLanguageTag(String)` factory
7478
method.
79+
* Deprecate `getOrComputeIfAbsent(...)` methods in `ExtensionContext.Store` in favor of
80+
the new `computeIfAbsent(...)` methods.
7581

7682
[[release-notes-6.0.0-M2-junit-jupiter-new-features-and-improvements]]
7783
==== New Features and Improvements
7884

7985
* Reason strings supplied to `ConditionEvaluationResult` APIs are now officially declared
8086
as `@Nullable`.
87+
* Introduce `computeIfAbsent(...)` methods in `ExtensionContext.Store` to ease simplify
88+
with non-nullable types.
8189

8290

8391
[[release-notes-6.0.0-M2-junit-vintage]]

documentation/src/test/java/example/FirstCustomEngine.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,14 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId
4848
return new EngineDescriptor(uniqueId, "First Custom Test Engine");
4949
}
5050

51-
//end::user_guide[]
52-
@SuppressWarnings("NullAway")
53-
//tag::user_guide[]
5451
@Override
5552
public void execute(ExecutionRequest request) {
5653
request.getEngineExecutionListener()
5754
// tag::custom_line_break[]
5855
.executionStarted(request.getRootTestDescriptor());
5956

6057
NamespacedHierarchicalStore<Namespace> store = request.getStore();
61-
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
58+
socket = store.computeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
6259
try {
6360
return new ServerSocket(0, 50, getLoopbackAddress());
6461
}

documentation/src/test/java/example/SecondCustomEngine.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,14 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId
4848
return new EngineDescriptor(uniqueId, "Second Custom Test Engine");
4949
}
5050

51-
//end::user_guide[]
52-
@SuppressWarnings("NullAway")
53-
//tag::user_guide[]
5451
@Override
5552
public void execute(ExecutionRequest request) {
5653
request.getEngineExecutionListener()
5754
// tag::custom_line_break[]
5855
.executionStarted(request.getRootTestDescriptor());
5956

6057
NamespacedHierarchicalStore<Namespace> store = request.getStore();
61-
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
58+
socket = store.computeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
6259
try {
6360
return new ServerSocket(0, 50, getLoopbackAddress());
6461
}

documentation/src/test/java/example/extensions/HttpServerExtension.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,13 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
2828
return HttpServer.class.equals(parameterContext.getParameter().getType());
2929
}
3030

31-
//end::user_guide[]
32-
@SuppressWarnings({ "DataFlowIssue", "NullAway" })
33-
//tag::user_guide[]
3431
@Override
3532
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
3633

3734
ExtensionContext rootContext = extensionContext.getRoot();
3835
ExtensionContext.Store store = rootContext.getStore(Namespace.GLOBAL);
3936
Class<HttpServerResource> key = HttpServerResource.class;
40-
HttpServerResource resource = store.getOrComputeIfAbsent(key, __ -> {
37+
HttpServerResource resource = store.computeIfAbsent(key, __ -> {
4138
try {
4239
HttpServerResource serverResource = new HttpServerResource(0);
4340
serverResource.start();

documentation/src/test/java/example/session/GlobalSetupTeardownListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void testPlanExecutionStarted(TestPlan testPlan) {
4343
}
4444
//tag::user_guide[]
4545
NamespacedHierarchicalStore<Namespace> store = session.getStore(); // <1>
46-
store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2>
46+
store.computeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2>
4747
InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0);
4848
HttpServer server;
4949
try {

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -569,10 +569,9 @@ interface CloseableResource {
569569
*
570570
* @param key the key; never {@code null}
571571
* @param requiredType the required type of the value; never {@code null}
572-
* @param defaultValue the default value
572+
* @param defaultValue the default value; never {@code null}
573573
* @param <V> the value type
574-
* @return the value; potentially {@code null} if {@code defaultValue}
575-
* is {@code null}
574+
* @return the value; never {@code null}
576575
* @since 5.5
577576
* @see #get(Object, Class)
578577
*/
@@ -592,13 +591,13 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
592591
* the type of object we wish to retrieve from the store.
593592
*
594593
* <pre style="code">
595-
* X x = store.getOrComputeIfAbsent(X.class, key -&gt; new X(), X.class);
594+
* X x = store.computeIfAbsent(X.class, key -&gt; new X(), X.class);
596595
* // Equivalent to:
597-
* // X x = store.getOrComputeIfAbsent(X.class);
596+
* // X x = store.computeIfAbsent(X.class);
598597
* </pre>
599598
*
600-
* <p>See {@link #getOrComputeIfAbsent(Object, Function, Class)} for
601-
* further details.
599+
* <p>See {@link #computeIfAbsent(Object, Function, Class)} for further
600+
* details.
602601
*
603602
* <p>If {@code type} implements {@link CloseableResource} or
604603
* {@link AutoCloseable} (unless the
@@ -610,14 +609,56 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
610609
* @param <V> the key and value type
611610
* @return the object; never {@code null}
612611
* @since 5.1
613-
* @see #getOrComputeIfAbsent(Object, Function)
614-
* @see #getOrComputeIfAbsent(Object, Function, Class)
612+
* @see #computeIfAbsent(Class)
613+
* @see #computeIfAbsent(Object, Function)
614+
* @see #computeIfAbsent(Object, Function, Class)
615615
* @see CloseableResource
616616
* @see AutoCloseable
617+
*
618+
* @deprecated Please use {@link #computeIfAbsent(Class)} instead.
617619
*/
618-
@API(status = STABLE, since = "5.1")
619-
default <V> @Nullable V getOrComputeIfAbsent(Class<V> type) {
620-
return getOrComputeIfAbsent(type, ReflectionSupport::newInstance, type);
620+
@Deprecated
621+
@API(status = DEPRECATED, since = "6.0")
622+
default <V> V getOrComputeIfAbsent(Class<V> type) {
623+
return computeIfAbsent(type);
624+
}
625+
626+
/**
627+
* Return the object of type {@code type} if it is present and not
628+
* {@code null} in this {@code Store} (<em>keyed</em> by {@code type});
629+
* otherwise, invoke the default constructor for {@code type} to
630+
* generate the object, store it, and return it.
631+
*
632+
* <p>This method is a shortcut for the following, where {@code X} is
633+
* the type of object we wish to retrieve from the store.
634+
*
635+
* <pre style="code">
636+
* X x = store.computeIfAbsent(X.class, key -&gt; new X(), X.class);
637+
* // Equivalent to:
638+
* // X x = store.computeIfAbsent(X.class);
639+
* </pre>
640+
*
641+
* <p>See {@link #computeIfAbsent(Object, Function, Class)} for further
642+
* details.
643+
*
644+
* <p>If {@code type} implements {@link CloseableResource} or
645+
* {@link AutoCloseable} (unless the
646+
* {@code junit.jupiter.extensions.store.close.autocloseable.enabled}
647+
* configuration parameter is set to {@code false}), then the {@code close()}
648+
* method will be invoked on the stored object when the store is closed.
649+
*
650+
* @param type the type of object to retrieve; never {@code null}
651+
* @param <V> the key and value type
652+
* @return the object; never {@code null}
653+
* @since 6.0
654+
* @see #computeIfAbsent(Object, Function)
655+
* @see #computeIfAbsent(Object, Function, Class)
656+
* @see CloseableResource
657+
* @see AutoCloseable
658+
*/
659+
@API(status = MAINTAINED, since = "6.0")
660+
default <V> V computeIfAbsent(Class<V> type) {
661+
return computeIfAbsent(type, ReflectionSupport::newInstance, type);
621662
}
622663

623664
/**
@@ -631,7 +672,7 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
631672
* the {@code key} as input), stored, and returned.
632673
*
633674
* <p>For greater type safety, consider using
634-
* {@link #getOrComputeIfAbsent(Object, Function, Class)} instead.
675+
* {@link #computeIfAbsent(Object, Function, Class)} instead.
635676
*
636677
* <p>If the created value is an instance of {@link CloseableResource} or
637678
* {@link AutoCloseable} (unless the
@@ -645,14 +686,56 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
645686
* @param <K> the key type
646687
* @param <V> the value type
647688
* @return the value; potentially {@code null}
648-
* @see #getOrComputeIfAbsent(Class)
649-
* @see #getOrComputeIfAbsent(Object, Function, Class)
689+
* @see #computeIfAbsent(Class)
690+
* @see #computeIfAbsent(Object, Function)
691+
* @see #computeIfAbsent(Object, Function, Class)
650692
* @see CloseableResource
651693
* @see AutoCloseable
694+
*
695+
* @deprecated Please use {@link #computeIfAbsent(Object, Function)}
696+
* instead.
652697
*/
698+
@Deprecated
699+
@API(status = DEPRECATED, since = "6.0")
653700
<K, V extends @Nullable Object> @Nullable Object getOrComputeIfAbsent(K key,
654701
Function<? super K, ? extends V> defaultCreator);
655702

703+
/**
704+
* Return the value of the specified required type that is stored under
705+
* the supplied {@code key}.
706+
*
707+
* <p>If no value is stored in the current {@link ExtensionContext}
708+
* for the supplied {@code key}, ancestors of the context will be queried
709+
* for a value with the same {@code key} in the {@code Namespace} used
710+
* to create this store. If no value is found for the supplied {@code key}
711+
* or the value is {@code null}, a new value will be computed by the
712+
* {@code defaultCreator} (given the {@code key} as input), stored, and
713+
* returned.
714+
*
715+
* <p>For greater type safety, consider using
716+
* {@link #computeIfAbsent(Object, Function, Class)} instead.
717+
*
718+
* <p>If the created value is an instance of {@link CloseableResource} or
719+
* {@link AutoCloseable} (unless the
720+
* {@code junit.jupiter.extensions.store.close.autocloseable.enabled}
721+
* configuration parameter is set to {@code false}), then the {@code close()}
722+
* method will be invoked on the stored object when the store is closed.
723+
*
724+
* @param key the key; never {@code null}
725+
* @param defaultCreator the function called with the supplied {@code key}
726+
* to create a new value; never {@code null} and must not return
727+
* {@code null}
728+
* @param <K> the key type
729+
* @param <V> the value type
730+
* @return the value; never {@code null}
731+
* @since 6.0
732+
* @see #computeIfAbsent(Class)
733+
* @see #computeIfAbsent(Object, Function, Class)
734+
* @see CloseableResource
735+
* @see AutoCloseable
736+
*/
737+
<K, V> Object computeIfAbsent(K key, Function<? super K, ? extends V> defaultCreator);
738+
656739
/**
657740
* Get the value of the specified required type that is stored under the
658741
* supplied {@code key}.
@@ -664,7 +747,7 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
664747
* a new value will be computed by the {@code defaultCreator} (given
665748
* the {@code key} as input), stored, and returned.
666749
*
667-
* <p>If {@code requiredType} implements {@link CloseableResource} or
750+
* <p>If the created value implements {@link CloseableResource} or
668751
* {@link AutoCloseable} (unless the
669752
* {@code junit.jupiter.extensions.store.close.autocloseable.enabled}
670753
* configuration parameter is set to {@code false}), then the {@code close()}
@@ -677,14 +760,50 @@ default <V> V getOrDefault(Object key, Class<V> requiredType, V defaultValue) {
677760
* @param <K> the key type
678761
* @param <V> the value type
679762
* @return the value; potentially {@code null}
680-
* @see #getOrComputeIfAbsent(Class)
681-
* @see #getOrComputeIfAbsent(Object, Function)
763+
* @see #computeIfAbsent(Class)
764+
* @see #computeIfAbsent(Object, Function)
765+
* @see #computeIfAbsent(Object, Function, Class)
682766
* @see CloseableResource
683767
* @see AutoCloseable
684768
*/
769+
@Deprecated
770+
@API(status = DEPRECATED, since = "6.0")
685771
<K, V extends @Nullable Object> @Nullable V getOrComputeIfAbsent(K key,
686772
Function<? super K, ? extends V> defaultCreator, Class<V> requiredType);
687773

774+
/**
775+
* Get the value of the specified required type that is stored under the
776+
* supplied {@code key}.
777+
*
778+
* <p>If no value is stored in the current {@link ExtensionContext}
779+
* for the supplied {@code key}, ancestors of the context will be queried
780+
* for a value with the same {@code key} in the {@code Namespace} used
781+
* to create this store. If no value is found for the supplied {@code key}
782+
* or the value is {@code null}, a new value will be computed by the
783+
* {@code defaultCreator} (given the {@code key} as input), stored, and
784+
* returned.
785+
*
786+
* <p>If the created value is an instance of {@link CloseableResource} or
787+
* {@link AutoCloseable} (unless the
788+
* {@code junit.jupiter.extensions.store.close.autocloseable.enabled}
789+
* configuration parameter is set to {@code false}), then the {@code close()}
790+
* method will be invoked on the stored object when the store is closed.
791+
*
792+
* @param key the key; never {@code null}
793+
* @param defaultCreator the function called with the supplied {@code key}
794+
* to create a new value; never {@code null} and must not return
795+
* {@code null}
796+
* @param requiredType the required type of the value; never {@code null}
797+
* @param <K> the key type
798+
* @param <V> the value type
799+
* @return the value; never {@code null}
800+
* @see #computeIfAbsent(Class)
801+
* @see #computeIfAbsent(Object, Function)
802+
* @see CloseableResource
803+
* @see AutoCloseable
804+
*/
805+
<K, V> V computeIfAbsent(K key, Function<? super K, ? extends V> defaultCreator, Class<V> requiredType);
806+
688807
/**
689808
* Store a {@code value} for later retrieval under the supplied {@code key}.
690809
*

junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/fixtures/TrackLogRecords.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.lang.annotation.RetentionPolicy;
1616
import java.lang.annotation.Target;
1717

18+
import org.jspecify.annotations.NullMarked;
1819
import org.junit.jupiter.api.extension.AfterEachCallback;
1920
import org.junit.jupiter.api.extension.BeforeEachCallback;
2021
import org.junit.jupiter.api.extension.ExtendWith;
@@ -41,6 +42,7 @@
4142
* @see LoggerFactory
4243
* @see LogRecordListener
4344
*/
45+
@NullMarked
4446
@Target({ ElementType.TYPE, ElementType.PARAMETER })
4547
@Retention(RetentionPolicy.RUNTIME)
4648
@ExtendWith(TrackLogRecords.Extension.class)
@@ -71,7 +73,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
7173
}
7274

7375
private LogRecordListener getListener(ExtensionContext context) {
74-
return getStore(context).getOrComputeIfAbsent(LogRecordListener.class);
76+
return getStore(context).computeIfAbsent(LogRecordListener.class);
7577
}
7678

7779
private Store getStore(ExtensionContext context) {

0 commit comments

Comments
 (0)