ExtensibleEnumRegistries.java
/*
* 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.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.api.Dialect;
import org.apache.maven.api.ExtensibleEnum;
import org.apache.maven.api.Language;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.ProjectScope;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.SessionScoped;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.services.DialectRegistry;
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.DialectProvider;
import org.apache.maven.api.spi.ExtensibleEnumProvider;
import org.apache.maven.api.spi.LanguageProvider;
import org.apache.maven.api.spi.ModelDialectProvider;
import org.apache.maven.api.spi.PathScopeProvider;
import org.apache.maven.api.spi.ProjectScopeProvider;
import static java.util.Objects.requireNonNull;
public class ExtensibleEnumRegistries {
private static final ClassValue<Optional<Method>> GET_IMPORT_REALMS = new ClassValue<>() {
@Override
protected Optional<Method> computeValue(Class<?> type) {
try {
return Optional.of(type.getMethod("getImportRealms"));
} catch (NoSuchMethodException | SecurityException e) {
return Optional.empty();
}
}
};
/**
* Checks whether a provider is visible from the given classloader.
* Providers from project extensions are loaded by extension-specific classloaders
* (realms) that are only imported into the project realm that declared the extension.
* This prevents extension-scoped providers from leaking across reactor modules
* during parallel builds.
*
* @param provider the SPI provider to check
* @param classLoader the classloader to check visibility from (typically the TCCL)
* @return {@code true} if the provider is visible, {@code false} otherwise
*/
public static boolean isVisibleFrom(Object provider, ClassLoader classLoader) {
if (classLoader == null) {
return true;
}
Class<?> providerClass = provider.getClass();
ClassLoader providerCl = providerClass.getClassLoader();
if (providerCl == null) {
return true;
}
try {
if (classLoader.loadClass(providerClass.getName()) == providerClass) {
return true;
}
} catch (ClassNotFoundException | LinkageError e) {
// Not loadable by name (hidden class, realm-ID import, or broken link) ��� check further
}
for (ClassLoader cl = classLoader; cl != null; cl = cl.getParent()) {
if (cl == providerCl) {
return true;
}
}
return isImportedRealm(classLoader, providerCl);
}
private static boolean isImportedRealm(ClassLoader classLoader, ClassLoader providerCl) {
return GET_IMPORT_REALMS
.get(classLoader.getClass())
.map(method -> {
try {
Collection<?> importedRealms = (Collection<?>) method.invoke(classLoader);
return importedRealms.contains(providerCl);
} catch (ReflectiveOperationException | ClassCastException e) {
return false;
}
})
.orElse(false);
}
@Named
@SessionScoped
public static class DefaultPathScopeRegistry extends DefaultExtensibleEnumRegistry<PathScope, PathScopeProvider>
implements PathScopeRegistry {
@Inject
public DefaultPathScopeRegistry(List<PathScopeProvider> providers) {
super(
providers,
PathScope.MAIN_COMPILE,
PathScope.MAIN_RUNTIME,
PathScope.TEST_COMPILE,
PathScope.TEST_RUNTIME);
}
}
@Named
@SessionScoped
public static class DefaultProjectScopeRegistry
extends DefaultExtensibleEnumRegistry<ProjectScope, ProjectScopeProvider> implements ProjectScopeRegistry {
@Inject
public DefaultProjectScopeRegistry(List<ProjectScopeProvider> providers) {
super(providers, ProjectScope.MAIN, ProjectScope.TEST);
}
}
@Named
@Singleton
public static class DefaultLanguageRegistry extends DefaultExtensibleEnumRegistry<Language, LanguageProvider>
implements LanguageRegistry {
@Inject
public DefaultLanguageRegistry(List<LanguageProvider> providers) {
super(providers, Language.NONE, Language.RESOURCES, Language.SCRIPT, Language.JAVA_FAMILY);
}
}
@Named
@SessionScoped
public static class DefaultDialectRegistry extends DefaultExtensibleEnumRegistry<Dialect, DialectProvider>
implements DialectRegistry {
@Inject
public DefaultDialectRegistry(
List<DialectProvider> providers, List<ModelDialectProvider> modelDialectProviders) {
// Collect all dialect ids already covered by DialectProvider(s) and the built-in XML.
// Only seed dialects from ModelDialectProvider that are not already registered,
// so that extensions which implement only ModelDialectProvider (without a separate
// DialectProvider) still have their dialect appear in the DialectRegistry.
super(providers, buildBuiltins(providers, modelDialectProviders));
}
private static Dialect[] buildBuiltins(
List<DialectProvider> providers, List<ModelDialectProvider> modelDialectProviders) {
// Ids already covered by built-in XML and DialectProvider contributions
Set<String> coveredIds = new HashSet<>();
coveredIds.add(Dialect.XML.id().toLowerCase(Locale.ROOT));
providers.stream()
.flatMap(p -> p.provides().stream())
.map(d -> d.id().toLowerCase(Locale.ROOT))
.forEach(coveredIds::add);
// Collect ModelDialectProvider dialects that fill gaps not covered by DialectProvider
Dialect[] extra = modelDialectProviders.stream()
.map(ModelDialectProvider::getDialect)
.filter(d -> coveredIds.add(d.id().toLowerCase(Locale.ROOT))) // add returns true if new
.toArray(Dialect[]::new);
// Prepend XML to the builtins; extra entries are new (not yet in coveredIds before the filter ran)
return Stream.concat(Stream.of(Dialect.XML), Stream.of(extra)).toArray(Dialect[]::new);
}
}
public abstract static class DefaultExtensibleEnumRegistry<
T extends ExtensibleEnum, P extends ExtensibleEnumProvider<T>>
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));
}
/**
* Looks up an enum value by id, checking dynamic providers before the static registry.
*
* <p><b>Cardinality assumption:</b> when dynamic lookup is enabled, this method iterates
* all registered providers and all their values on every call ��� O(n��m) where {@code n} is
* the number of providers and {@code m} is the average number of values per provider.
* Both are expected to be O(tens) in practice (a handful of SPI providers, each contributing
* a small set of values). This method is <em>not</em> designed for cardinalities in the
* thousands; callers that need to look up the same value repeatedly in a tight loop should
* cache the result themselves.</p>
*
* <p><b>Classloader visibility:</b> providers contributed by project extensions are only
* visible when the current thread's context classloader can load the provider's class.
* This prevents extension-scoped providers from leaking across reactor modules during
* parallel builds.</p>
*
* @param id the enum value id, must not be {@code null}
* @return the matching value, or {@code Optional.empty()} if not found
*/
@Override
public Optional<T> lookup(String id) {
String key = requireNonNull(id, "id").toLowerCase(Locale.ROOT);
if (lookup != null && providerType != null) {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Optional<T> result = lookup.lookupList(providerType).stream()
.filter(p -> isVisibleFrom(p, tccl))
.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));
}
}
}