You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/10/14 21:46:15 UTC
(maven) branch worktree-generic-registry-lookup created (now 4380de2719)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch worktree-generic-registry-lookup
in repository https://gitbox.apache.org/repos/asf/maven.git
at 4380de2719 fix: generic project-extension visibility for extensible enum registries
This branch includes the following new commits:
new 4380de2719 fix: generic project-extension visibility for extensible enum registries
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven) 01/01: fix: generic project-extension visibility for extensible enum registries
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch worktree-generic-registry-lookup
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 4380de2719cd872f0e13c5523576005e620524ea
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Oct 14 23:46:01 2026 +0200
fix: generic project-extension visibility for extensible enum registries
SISU visibility filtering only works with explicit runtime lookups, not
constructor injection. Project extensions are bound to a project's
classloader, so constructor-injected provider lists miss them.
Previously only DefaultPackagingRegistry was aware of this (#13306).
This commit generalizes the fix into the base
DefaultExtensibleEnumRegistry class and applies it to all affected
registries:
- DefaultExtensibleEnumRegistry: new constructor accepting Lookup and
provider Class; lookup() queries Lookup.lookupList() dynamically
before falling back to constructor-injected/built-in values.
- DefaultPackagingRegistry: delegates to the generic mechanism via
super(lookup, PackagingProvider.class, providers); only overrides
lookup() for the legacy LifecycleMapping fallback.
- DefaultTypeRegistry: dynamic TypeProvider lookup is now performed
uncached before the ConcurrentHashMap fallback, so project-extension
types don't leak across reactor projects.
- DefaultLifecycleRegistry: stream() merges constructor-injected
providers with dynamically discovered ones via Lookup, deduplicating
by lifecycle id.
Closes #13306
Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
.../internal/impl/DefaultLifecycleRegistry.java | 27 +++-
.../internal/impl/DefaultPackagingRegistry.java | 21 +--
.../maven/internal/impl/DefaultTypeRegistry.java | 41 +++---
.../impl/DefaultPackagingRegistryTest.java | 107 ++++++++++++++++
.../internal/impl/DefaultTypeRegistryTest.java | 141 +++++++++++++++++++++
.../maven/impl/ExtensibleEnumRegistries.java | 24 +++-
.../impl/DefaultExtensibleEnumRegistryTest.java | 118 +++++++++++++++++
7 files changed, 446 insertions(+), 33 deletions(-)
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
index c25269d033..5dd0687059 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
@@ -43,6 +43,7 @@
import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.services.LifecycleRegistry;
+import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.LookupException;
import org.apache.maven.api.spi.ExtensibleEnumProvider;
import org.apache.maven.api.spi.LifecycleProvider;
@@ -101,18 +102,26 @@ public class DefaultLifecycleRegistry implements LifecycleRegistry {
public static final String SCOPE_TEST = DependencyScope.TEST.id();
private final List<LifecycleProvider> providers;
+ private final Lookup lookup;
public DefaultLifecycleRegistry() {
this(Collections.emptyList());
}
- @Inject
public DefaultLifecycleRegistry(List<LifecycleProvider> providers) {
+ this(null, providers);
+ }
+
+ @Inject
+ public DefaultLifecycleRegistry(Lookup lookup, List<LifecycleProvider> providers) {
+ this.lookup = lookup;
List<LifecycleProvider> p = new ArrayList<>(providers);
p.add(() -> List.of(new CleanLifecycle(), new DefaultLifecycle(), new SiteLifecycle()));
this.providers = p;
- // validate lifecycle
- for (Lifecycle lifecycle : this) {
+ // validate lifecycle using only injected providers (safe during construction —
+ // dynamic lookup may not be usable yet)
+ for (Lifecycle lifecycle :
+ providers.stream().flatMap(pr -> pr.provides().stream()).toList()) {
Set<String> set = new HashSet<>();
lifecycle.allPhases().forEach(phase -> {
if (!set.add(phase.name())) {
@@ -135,6 +144,18 @@ public Iterator<Lifecycle> iterator() {
@Override
public Stream<Lifecycle> stream() {
+ if (lookup != null) {
+ // Dynamic discovery includes constructor-injected + project-extension providers.
+ // Built-in lifecycles (clean, default, site) are added as a lambda and not
+ // discoverable via Lookup, so we always include the injected providers too.
+ // Deduplicate by id, with dynamic providers taking priority.
+ Map<String, Lifecycle> result = new LinkedHashMap<>();
+ providers.stream().flatMap(p -> p.provides().stream()).forEach(lf -> result.put(lf.id(), lf));
+ lookup.lookupList(LifecycleProvider.class).stream()
+ .flatMap(p -> p.provides().stream())
+ .forEach(lf -> result.put(lf.id(), lf));
+ return result.values().stream();
+ }
return providers.stream().map(ExtensibleEnumProvider::provides).flatMap(Collection::stream);
}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultPackagingRegistry.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultPackagingRegistry.java
index d865d12b4a..34eb5038da 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultPackagingRegistry.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultPackagingRegistry.java
@@ -66,29 +66,30 @@ public class DefaultPackagingRegistry
@Inject
public DefaultPackagingRegistry(Lookup lookup, TypeRegistry typeRegistry, List<PackagingProvider> providers) {
- super(providers);
+ super(lookup, PackagingProvider.class, providers);
this.lookup = lookup;
this.typeRegistry = typeRegistry;
}
@Override
public Optional<Packaging> lookup(String id) {
- id = id.toLowerCase(Locale.ROOT);
- // TODO: we should be able to inject a Map<String, LifecycleMapping> directly,
- // however, SISU visibility filtering can only happen when an explicit
- // lookup is performed. The whole problem here is caused by "project extensions"
- // which are bound to a project's classloader, without any clear definition
- // of a "project scope"
+ Optional<Packaging> result = super.lookup(id);
+ if (result.isPresent()) {
+ return result;
+ }
+ // Legacy LifecycleMapping fallback: SISU visibility filtering requires
+ // an explicit lookup (project extensions are bound to a project classloader)
+ String packagingId = id.toLowerCase(Locale.ROOT);
LifecycleMapping lifecycleMapping =
- lookup.lookupOptional(LifecycleMapping.class, id).orElse(null);
+ lookup.lookupOptional(LifecycleMapping.class, packagingId).orElse(null);
if (lifecycleMapping == null) {
return Optional.empty();
}
- Type type = typeRegistry.lookup(id).orElse(null);
+ Type type = typeRegistry.lookup(packagingId).orElse(null);
if (type == null) {
return Optional.empty();
}
- return Optional.of(new DefaultPackaging(id, type, getPlugins(lifecycleMapping)));
+ return Optional.of(new DefaultPackaging(packagingId, type, getPlugins(lifecycleMapping)));
}
private Map<String, PluginContainer> getPlugins(LifecycleMapping lifecycleMapping) {
diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultTypeRegistry.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultTypeRegistry.java
index fb925a60ad..7dca3dd89a 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultTypeRegistry.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultTypeRegistry.java
@@ -78,26 +78,29 @@ public Optional<Type> lookup(String id) {
@Nonnull
public Type require(String id) {
requireNonNull(id, "id cannot be null");
- return usedTypes.computeIfAbsent(id, i -> {
- Type type = lookup.lookupList(TypeProvider.class).stream()
- .flatMap(p -> p.provides().stream())
- .filter(t -> Objects.equals(id, t.id()))
- .findFirst()
- .orElse(null);
- if (type == null) {
- // Copy data as the ArtifactHandler is not immutable, but Type should be.
- ArtifactHandler handler = manager.getArtifactHandler(id);
- type = new DefaultType(
- id,
- languageRegistry.lookup(handler.getLanguage()).orElseGet(() -> handler::getLanguage),
- handler.getExtension(),
- handler.getClassifier(),
- handler.isIncludesDependencies(),
- handler.isAddedToClasspath()
- ? new JavaPathType[] {JavaPathType.CLASSES, JavaPathType.MODULES}
- : new JavaPathType[] {});
- }
+ // Dynamic providers are queried first (uncached) so that SISU visibility
+ // filtering works correctly for project-extension-contributed types.
+ // Only the legacy ArtifactHandler fallback is cached.
+ Type type = lookup.lookupList(TypeProvider.class).stream()
+ .flatMap(p -> p.provides().stream())
+ .filter(t -> Objects.equals(id, t.id()))
+ .findFirst()
+ .orElse(null);
+ if (type != null) {
return type;
+ }
+ return usedTypes.computeIfAbsent(id, i -> {
+ // Copy data as the ArtifactHandler is not immutable, but Type should be.
+ ArtifactHandler handler = manager.getArtifactHandler(id);
+ return new DefaultType(
+ id,
+ languageRegistry.lookup(handler.getLanguage()).orElseGet(() -> handler::getLanguage),
+ handler.getExtension(),
+ handler.getClassifier(),
+ handler.isIncludesDependencies(),
+ handler.isAddedToClasspath()
+ ? new JavaPathType[] {JavaPathType.CLASSES, JavaPathType.MODULES}
+ : new JavaPathType[] {});
});
}
}
diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultPackagingRegistryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultPackagingRegistryTest.java
new file mode 100644
index 0000000000..43bb5084f3
--- /dev/null
+++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultPackagingRegistryTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package org.apache.maven.internal.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.maven.api.Packaging;
+import org.apache.maven.api.Type;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.api.services.TypeRegistry;
+import org.apache.maven.api.spi.PackagingProvider;
+import org.apache.maven.lifecycle.mapping.LifecycleMapping;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class DefaultPackagingRegistryTest {
+
+ @Test
+ void findsProviderVisibleThroughProjectLookup() {
+ Lookup lookup = mock(Lookup.class);
+ TypeRegistry types = mock(TypeRegistry.class);
+ Packaging packaging = packaging("app-client");
+ PackagingProvider provider = () -> List.of(packaging);
+ when(lookup.lookupList(PackagingProvider.class)).thenReturn(List.of(provider));
+
+ DefaultPackagingRegistry registry = new DefaultPackagingRegistry(lookup, types, List.of());
+
+ assertSame(packaging, registry.lookup("APP-CLIENT").orElseThrow());
+ }
+
+ @Test
+ void doesNotRetainProviderFromAnotherProjectLookup() {
+ Lookup lookup = mock(Lookup.class);
+ Packaging packaging = packaging("app-client");
+ PackagingProvider provider = () -> List.of(packaging);
+ AtomicReference<List<PackagingProvider>> visible = new AtomicReference<>(List.of(provider));
+ when(lookup.lookupList(PackagingProvider.class)).thenAnswer(invocation -> visible.get());
+
+ DefaultPackagingRegistry registry = new DefaultPackagingRegistry(lookup, mock(TypeRegistry.class), List.of());
+
+ assertSame(packaging, registry.lookup("app-client").orElseThrow());
+ visible.set(List.of());
+ assertTrue(registry.lookup("app-client").isEmpty());
+ }
+
+ @Test
+ void findsProviderInjectedWhenRegistryWasConstructed() {
+ Lookup lookup = mock(Lookup.class);
+ Packaging packaging = packaging("app-client");
+ PackagingProvider provider = () -> List.of(packaging);
+ when(lookup.lookupList(PackagingProvider.class)).thenReturn(List.of());
+
+ DefaultPackagingRegistry registry =
+ new DefaultPackagingRegistry(lookup, mock(TypeRegistry.class), List.of(provider));
+
+ assertSame(packaging, registry.lookup("app-client").orElseThrow());
+ }
+
+ @Test
+ void retainsLegacyLifecycleMappingFallback() {
+ Lookup lookup = mock(Lookup.class);
+ TypeRegistry types = mock(TypeRegistry.class);
+ LifecycleMapping mapping = mock(LifecycleMapping.class);
+ Type type = mock(Type.class);
+ when(lookup.lookupList(PackagingProvider.class)).thenReturn(List.of());
+ when(lookup.lookupOptional(LifecycleMapping.class, "jar")).thenReturn(Optional.of(mapping));
+ when(mapping.getLifecycles()).thenReturn(Map.of());
+ when(types.lookup("jar")).thenReturn(Optional.of(type));
+
+ DefaultPackagingRegistry registry = new DefaultPackagingRegistry(lookup, types, List.of());
+ Packaging packaging = registry.lookup("jar").orElseThrow();
+
+ assertEquals("jar", packaging.id());
+ assertSame(type, packaging.type());
+ assertTrue(packaging.plugins().isEmpty());
+ }
+
+ private static Packaging packaging(String id) {
+ Packaging packaging = mock(Packaging.class);
+ when(packaging.id()).thenReturn(id);
+ return packaging;
+ }
+}
diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultTypeRegistryTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultTypeRegistryTest.java
new file mode 100644
index 0000000000..5c306d4009
--- /dev/null
+++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultTypeRegistryTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+package org.apache.maven.internal.impl;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.maven.api.Language;
+import org.apache.maven.api.PathType;
+import org.apache.maven.api.Type;
+import org.apache.maven.api.services.LanguageRegistry;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.api.spi.TypeProvider;
+import org.apache.maven.artifact.handler.ArtifactHandler;
+import org.apache.maven.artifact.handler.manager.LegacyArtifactHandlerManager;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class DefaultTypeRegistryTest {
+
+ @Test
+ void findsTypeFromDynamicProviderLookup() {
+ Lookup lookup = mock(Lookup.class);
+ Type customType = stubType("app-client");
+ TypeProvider provider = () -> List.of(customType);
+ when(lookup.lookupList(TypeProvider.class)).thenReturn(List.of(provider));
+
+ DefaultTypeRegistry registry =
+ new DefaultTypeRegistry(lookup, mock(LanguageRegistry.class), mock(LegacyArtifactHandlerManager.class));
+
+ assertSame(customType, registry.require("app-client"));
+ }
+
+ @Test
+ void doesNotRetainDynamicTypeAcrossProjects() {
+ Lookup lookup = mock(Lookup.class);
+ LanguageRegistry langReg = mock(LanguageRegistry.class);
+ LegacyArtifactHandlerManager handlerMgr = mock(LegacyArtifactHandlerManager.class);
+ ArtifactHandler fallbackHandler = stubHandler();
+ when(handlerMgr.getArtifactHandler(anyString())).thenReturn(fallbackHandler);
+ when(langReg.lookup(anyString())).thenReturn(java.util.Optional.of(Language.NONE));
+
+ Type customType = stubType("app-client");
+ TypeProvider provider = () -> List.of(customType);
+ AtomicReference<List<TypeProvider>> visible = new AtomicReference<>(List.of(provider));
+ when(lookup.lookupList(TypeProvider.class)).thenAnswer(inv -> visible.get());
+
+ DefaultTypeRegistry registry = new DefaultTypeRegistry(lookup, langReg, handlerMgr);
+
+ assertSame(customType, registry.require("app-client"));
+ visible.set(List.of());
+ // After the provider disappears, the type should no longer resolve to
+ // the custom type (it falls back to the legacy ArtifactHandler)
+ Type fallback = registry.require("app-client");
+ assertEquals("app-client", fallback.id());
+ // This is the legacy fallback, not the custom type
+ assert fallback != customType;
+ }
+
+ @Test
+ void fallsBackToLegacyArtifactHandler() {
+ Lookup lookup = mock(Lookup.class);
+ LanguageRegistry langReg = mock(LanguageRegistry.class);
+ LegacyArtifactHandlerManager handlerMgr = mock(LegacyArtifactHandlerManager.class);
+ ArtifactHandler handler = stubHandler();
+ when(lookup.lookupList(TypeProvider.class)).thenReturn(List.of());
+ when(handlerMgr.getArtifactHandler("jar")).thenReturn(handler);
+ when(langReg.lookup("java")).thenReturn(java.util.Optional.of(Language.JAVA_FAMILY));
+
+ DefaultTypeRegistry registry = new DefaultTypeRegistry(lookup, langReg, handlerMgr);
+ Type type = registry.require("jar");
+
+ assertEquals("jar", type.id());
+ }
+
+ private static Type stubType(String id) {
+ return new Type() {
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public Language getLanguage() {
+ return Language.JAVA_FAMILY;
+ }
+
+ @Override
+ public String getExtension() {
+ return "jar";
+ }
+
+ @Override
+ public String getClassifier() {
+ return null;
+ }
+
+ @Override
+ public boolean isIncludesDependencies() {
+ return false;
+ }
+
+ @Override
+ public Set<PathType> getPathTypes() {
+ return Set.of();
+ }
+ };
+ }
+
+ private static ArtifactHandler stubHandler() {
+ ArtifactHandler handler = mock(ArtifactHandler.class);
+ when(handler.getLanguage()).thenReturn("java");
+ when(handler.getExtension()).thenReturn("jar");
+ when(handler.getClassifier()).thenReturn(null);
+ when(handler.isIncludesDependencies()).thenReturn(false);
+ when(handler.isAddedToClasspath()).thenReturn(true);
+ return handler;
+ }
+}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
index 5716538702..53d514dda2 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/ExtensibleEnumRegistries.java
@@ -35,6 +35,7 @@
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.services.ExtensibleEnumRegistry;
import org.apache.maven.api.services.LanguageRegistry;
+import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.PathScopeRegistry;
import org.apache.maven.api.services.ProjectScopeRegistry;
import org.apache.maven.api.spi.ExtensibleEnumProvider;
@@ -89,15 +90,36 @@ public abstract static class DefaultExtensibleEnumRegistry<
implements ExtensibleEnumRegistry<T> {
protected final Map<String, T> values;
+ private final Lookup lookup;
+ private final Class<P> providerType;
+ @SafeVarargs
public DefaultExtensibleEnumRegistry(List<P> providers, T... builtinValues) {
+ this(null, null, providers, builtinValues);
+ }
+
+ @SafeVarargs
+ public DefaultExtensibleEnumRegistry(
+ Lookup lookup, Class<P> providerType, List<P> providers, T... builtinValues) {
+ this.lookup = lookup;
+ this.providerType = providerType;
values = Stream.concat(Stream.of(builtinValues), providers.stream().flatMap(p -> p.provides().stream()))
.collect(Collectors.toUnmodifiableMap(t -> t.id().toLowerCase(Locale.ROOT), t -> t));
}
@Override
public Optional<T> lookup(String id) {
- return Optional.ofNullable(values.get(requireNonNull(id, "id").toLowerCase(Locale.ROOT)));
+ String key = requireNonNull(id, "id").toLowerCase(Locale.ROOT);
+ if (lookup != null && providerType != null) {
+ Optional<T> result = lookup.lookupList(providerType).stream()
+ .flatMap(p -> p.provides().stream())
+ .filter(t -> key.equals(t.id().toLowerCase(Locale.ROOT)))
+ .findFirst();
+ if (result.isPresent()) {
+ return result;
+ }
+ }
+ return Optional.ofNullable(values.get(key));
}
}
}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultExtensibleEnumRegistryTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultExtensibleEnumRegistryTest.java
new file mode 100644
index 0000000000..984e70bd8f
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultExtensibleEnumRegistryTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.maven.impl;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.maven.api.ExtensibleEnum;
+import org.apache.maven.api.services.Lookup;
+import org.apache.maven.api.spi.ExtensibleEnumProvider;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class DefaultExtensibleEnumRegistryTest {
+
+ private static final TestValue FOO = new TestValue("foo");
+ private static final TestValue BAR = new TestValue("bar");
+ private static final TestValue CUSTOM = new TestValue("custom");
+
+ @Test
+ void lookupFindsBuiltinValues() {
+ var registry = new TestRegistry(List.of(), FOO, BAR);
+ assertSame(FOO, registry.lookup("foo").orElseThrow());
+ assertSame(BAR, registry.lookup("BAR").orElseThrow());
+ }
+
+ @Test
+ void lookupFindsConstructorInjectedProviders() {
+ TestProvider provider = () -> List.of(CUSTOM);
+ var registry = new TestRegistry(List.of(provider));
+ assertSame(CUSTOM, registry.lookup("custom").orElseThrow());
+ }
+
+ @Test
+ void dynamicLookupFindsProjectExtensionProviders() {
+ Lookup lookup = mock(Lookup.class);
+ TestProvider provider = () -> List.of(CUSTOM);
+ when(lookup.lookupList(TestProvider.class)).thenReturn(List.of(provider));
+
+ var registry = new TestRegistry(lookup, List.of());
+ assertSame(CUSTOM, registry.lookup("custom").orElseThrow());
+ }
+
+ @Test
+ void dynamicLookupTakesPriorityOverConstructorInjected() {
+ Lookup lookup = mock(Lookup.class);
+ TestValue builtin = new TestValue("foo");
+ TestValue override = new TestValue("foo");
+ TestProvider dynamicProvider = () -> List.of(override);
+ TestProvider injectedProvider = () -> List.of(builtin);
+ when(lookup.lookupList(TestProvider.class)).thenReturn(List.of(dynamicProvider));
+
+ var registry = new TestRegistry(lookup, List.of(injectedProvider));
+ assertSame(override, registry.lookup("foo").orElseThrow());
+ }
+
+ @Test
+ void dynamicLookupDoesNotRetainProviderAcrossProjects() {
+ Lookup lookup = mock(Lookup.class);
+ TestValue custom = new TestValue("custom");
+ TestProvider provider = () -> List.of(custom);
+ AtomicReference<List<TestProvider>> visible = new AtomicReference<>(List.of(provider));
+ when(lookup.lookupList(TestProvider.class)).thenAnswer(inv -> visible.get());
+
+ var registry = new TestRegistry(lookup, List.of());
+
+ assertSame(custom, registry.lookup("custom").orElseThrow());
+ visible.set(List.of());
+ assertTrue(registry.lookup("custom").isEmpty());
+ }
+
+ @Test
+ void fallsBackToBuiltinWhenDynamicLookupFindsNothing() {
+ Lookup lookup = mock(Lookup.class);
+ when(lookup.lookupList(TestProvider.class)).thenReturn(List.of());
+
+ TestValue foo = new TestValue("foo");
+ var registry = new TestRegistry(lookup, List.of(), foo);
+ assertSame(foo, registry.lookup("foo").orElseThrow());
+ }
+
+ record TestValue(String id) implements ExtensibleEnum {}
+
+ interface TestProvider extends ExtensibleEnumProvider<TestValue> {}
+
+ static class TestRegistry extends ExtensibleEnumRegistries.DefaultExtensibleEnumRegistry<TestValue, TestProvider> {
+
+ @SafeVarargs
+ TestRegistry(List<TestProvider> providers, TestValue... builtins) {
+ super(providers, builtins);
+ }
+
+ @SafeVarargs
+ TestRegistry(Lookup lookup, List<TestProvider> providers, TestValue... builtins) {
+ super(lookup, TestProvider.class, providers, builtins);
+ }
+ }
+}