io.avaje.inject.generator.ScopeInfo Maven / Gradle / Ivy
package io.avaje.inject.generator;
import static io.avaje.inject.generator.ProcessingContext.*;
import static io.avaje.inject.generator.APContext.*;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.processing.FilerException;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
final class ScopeInfo {
enum Type {
/**
* Default scope.
*/
DEFAULT("AvajeModule"),
/**
* Custom scope.
*/
CUSTOM("AvajeModule.Custom"),
/**
* Built-in Test scope.
*/
TEST("io.avaje.inject.test.TestModule");
final String type;
Type(String type) {
this.type = type;
}
String type() {
return type;
}
}
/**
* Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
*/
private final Map metaData = new HashMap<>();
private final Map constructorDependencies = new LinkedHashMap<>();
private final List beanReaders = new ArrayList<>();
private final Set readBeans = new HashSet<>();
private final Set pluginProvided = new HashSet<>();
private final Set requires = new LinkedHashSet<>();
private final Set provides = new LinkedHashSet<>();
private final Set requiresPackages = new LinkedHashSet<>();
private final List requirePkg = new ArrayList<>();
private final boolean defaultScope;
private final TypeElement annotationType;
private final AllScopes scopes;
private boolean moduleInitialised;
private boolean moduleWritten;
private String name;
private String modulePackage;
private String moduleFullName;
private String moduleShortName;
private JavaFileObject moduleFile;
private boolean emptyModule;
private boolean ignoreSingleton;
/**
* Create for the main/global module scope.
*/
ScopeInfo() {
this.scopes = null;
this.defaultScope = true;
this.annotationType = null;
}
/**
* Create for custom scope.
*/
ScopeInfo(TypeElement type, AllScopes scopes) {
this.scopes = scopes;
this.defaultScope = false;
this.annotationType = type;
}
@Override
public String toString() {
return "ScopeInfo{" +
"name=" + name +
", metaData=" + metaData +
'}';
}
void pluginProvided(String pluginProvides) {
pluginProvided.add(pluginProvides);
}
boolean includeSingleton() {
return !ignoreSingleton;
}
void details(String name, Element contextElement) {
if (name == null || name.isEmpty()) {
final String simpleName = contextElement.getSimpleName().toString();
this.name = ScopeUtil.name(simpleName);
} else {
this.name = ScopeUtil.name(name);
}
read(contextElement);
}
private void read(Element element) {
final var injectModule = InjectModulePrism.getInstanceOn(element);
if (injectModule == null) {
return;
}
ignoreSingleton = injectModule.ignoreSingleton();
injectModule.requires().stream().map(Object::toString).forEach(requires::add);
injectModule.provides().stream().map(Object::toString).forEach(provides::add);
injectModule.requiresPackages().stream()
.map(Object::toString)
.forEach(
require -> {
requiresPackages.add(require);
requirePkg.add(ProcessorUtils.packageOf(require) + ".");
});
}
private String initName(String topPackage) {
if (name == null || name.isEmpty()) {
name = ScopeUtil.initName(topPackage);
}
return name;
}
void initialiseName(String topPackage) throws IOException {
emptyModule = topPackage == null;
if (!emptyModule) {
modulePackage = topPackage;
final String name = initName(modulePackage);
moduleShortName = name + "Module";
moduleFullName = modulePackage + "." + moduleShortName;
moduleFile = createSourceFile(moduleFullName);
}
}
JavaFileObject moduleFile() {
return moduleFile;
}
String modulePackage() {
return modulePackage;
}
String moduleFullName() {
return moduleFullName;
}
String moduleShortName() {
return moduleShortName;
}
String name() {
return name;
}
Set requires() {
return requires;
}
Set provides() {
return provides;
}
Set pluginProvided() {
return pluginProvided;
}
void writeBeanHelpers() {
for (BeanReader beanReader : beanReaders) {
try {
if (!beanReader.isWrittenToFile()) {
if (beanReader.isGenerateProxy()) {
SimpleBeanProxyWriter proxyWriter = new SimpleBeanProxyWriter(beanReader);
proxyWriter.write();
} else {
SimpleBeanWriter writer = new SimpleBeanWriter(beanReader);
writer.write();
}
beanReader.setWrittenToFile();
}
} catch (FilerException e) {
logWarn("FilerException to write $DI class %s %s", beanReader.beanType(), e.getMessage());
} catch (IOException e) {
logError(beanReader.beanType(), "Failed to write $DI class %s", e);
}
}
}
private void initialiseModule() {
if (!moduleInitialised) {
try {
initialiseName(MetaTopPackage.of(metaData.values()));
addModule(moduleFullName);
moduleInitialised = true;
} catch (IOException e) {
if (isDuplicateModule(moduleFullName)) {
String msg = "Attempting to create 2 modules both called " + moduleFullName
+ ". This can occur when a custom scope (named from it's annotation) has a name clash with" +
" the default module which can be named from the package. Look to resolve this by either " +
"changing the name of the custom scope annotation, or explicitly naming the default scope " +
"using @InjectModule(name), or changing the top level package used by the default scope";
throw new IllegalStateException(msg);
}
logError("Failed to create module filer %s", e.getMessage());
}
}
}
void writeModule() {
if (moduleWritten) {
return;
}
final Collection meta = metaData.values().stream().sorted().collect(Collectors.toList());
if (emptyModule) {
// typically nothing in the default scope, only custom scopes
if (!meta.isEmpty()) {
String msg = meta + " is being ignored by avaje-inject as there is no 'default' module. This is expected when " +
meta + " is a @Singleton being generated by an annotation processor but there is no 'default' module " +
"(only custom modules are being used).";
logWarn(msg);
}
return;
}
final MetaDataOrdering ordering = new MetaDataOrdering(meta, this);
final int remaining = ordering.processQueue();
if (remaining > 0) {
ordering.logWarnings();
}
try {
SimpleModuleWriter factoryWriter = new SimpleModuleWriter(ordering, this);
factoryWriter.write(type());
moduleWritten = true;
} catch (FilerException e) {
logWarn("FilerException trying to write factory %s", e.getMessage());
} catch (IOException e) {
logError("Failed to write factory %s", e.getMessage());
}
}
/**
* Return the type of this scope.
*/
Type type() {
return annotationType == null ? Type.DEFAULT : Constants.TESTSCOPE.equals(annotationType.getQualifiedName().toString()) ? Type.TEST : Type.CUSTOM;
}
/**
* Merge the changed bean meta data into the existing (factory) metaData.
*/
void mergeMetaData() {
for (BeanReader beanReader : beanReaders) {
if (!beanReader.isRequestScopedController()) {
MetaData metaData = this.metaData.get(beanReader.metaKey());
if (metaData == null) {
addMeta(beanReader);
} else {
updateMeta(metaData, beanReader);
}
}
}
}
/**
* Add a new previously unknown bean.
*/
private void addMeta(BeanReader beanReader) {
MetaData meta = beanReader.createMeta();
metaData.put(meta.key(), meta);
for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
metaData.put(methodMeta.key(), methodMeta);
}
}
/**
* Update the meta data on a previously known bean.
*/
private void updateMeta(MetaData metaData, BeanReader beanReader) {
metaData.update(beanReader);
}
/**
* Read the dependency injection meta data for the given bean.
*/
private void readBeanMeta(TypeElement typeElement, boolean factory, boolean importedComponent) {
if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
logNote("skipping annotation type %s", typeElement);
return;
}
var reader = new BeanReader(typeElement, factory, importedComponent).read();
if (reader.isDelayed() && ProcessingContext.delayUntilNextRound(typeElement)) {
readBeans.remove(typeElement.toString());
} else {
beanReaders.add(reader);
}
}
void readBuildMethodDependencyMeta(Element element) {
Name simpleName = element.getSimpleName();
if (simpleName.toString().startsWith("build_")) {
// read a build method - DependencyMeta
DependencyMetaPrism meta = DependencyMetaPrism.getInstanceOn(element);
if (meta == null) {
logError("Missing @DependencyMeta on method %s", simpleName);
} else {
final MetaData metaData = new MetaData(meta);
this.metaData.put(metaData.key(), metaData);
}
}
}
void read(TypeElement element, boolean factory, boolean importedComponent) {
if (readBeans.add(element.toString())) {
readBeanMeta(element, factory, importedComponent);
} else {
logNote("skipping already processed bean %s", element);
}
}
/**
* Write Custom modules during processing (not last round) so that they are
* visible to code in src/main. This means that Custom scopes do NOT support
* other annotation processors generating beans that use those Custom scopes.
*/
void writeCustomModule() {
if (type() == Type.CUSTOM && !metaData.isEmpty()) {
writeModule();
}
}
void write(boolean processingOver) {
mergeMetaData();
writeBeanHelpers();
initialiseModule();
if (processingOver && !metaData.isEmpty()) {
ProcessingContext.registerExternalProvidedTypes(this);
writeModule();
}
}
void buildAtInjectModule(Append writer) {
writer.append(Constants.AT_GENERATED).eol();
writer.append("@InjectModule(");
boolean leadingComma = false;
if (!provides.isEmpty()) {
attributeClasses(false, writer, "provides", provides);
leadingComma = true;
}
if (!requires.isEmpty()) {
attributeClasses(leadingComma, writer, "requires", requires);
leadingComma = true;
}
if (!requiresPackages.isEmpty()) {
attributeClasses(leadingComma, writer, "requiresPackages", requiresPackages);
leadingComma = true;
}
if (annotationType != null) {
if (leadingComma) {
writer.append(", ");
}
writer.append("customScopeType = \"%s\"", annotationType.getQualifiedName().toString());
}
writer.append(")").eol();
}
private void attributeClasses(boolean leadingComma, Append writer, String prefix, Set classNames) {
if (leadingComma) {
writer.append(", ");
}
writer.append("%s = {", prefix);
int c = 0;
for (final String value : classNames) {
if (c++ > 0) {
writer.append(",");
}
writer.append(value).append(".class");
}
writer.append("}");
}
void buildProvides(Append writer) {
if (!provides.isEmpty()) {
buildProvidesMethod(writer, "provides", provides);
}
if (!requires.isEmpty()) {
buildProvidesMethod(writer, "requires", requires);
}
if (!requiresPackages.isEmpty()) {
buildProvidesMethod(writer, "requiresPackages", requiresPackages);
}
}
private void buildProvidesMethod(Append writer, String fieldName, Set types) {
writer.append(" @Override").eol();
final var arrayType = fieldName.contains("Aspects") ? "Class>" : "Type";
writer.append(" public %s[] %s() {", arrayType, fieldName).eol();
writer.append(" return new %s[] {", arrayType).eol();
for (final String rawType : types) {
if (rawType.contains(":")) {
continue;
}
if (rawType.contains("<")) {
writer.append(" new GenericType<%s>(){},", rawType).eol();
} else {
writer.append(" %s.class,", rawType).eol();
}
}
writer.append(" };").eol();
writer.append(" }").eol().eol();
}
void buildAutoProvides(Append writer, Set autoProvides) {
autoProvides.removeAll(provides);
if (!autoProvides.isEmpty()) {
buildProvidesMethod(writer, "autoProvides", autoProvides);
}
}
void buildAutoProvidesAspects(Append writer, Set autoProvidesAspects) {
autoProvidesAspects.removeAll(provides);
if (!autoProvidesAspects.isEmpty()) {
buildProvidesMethod(writer, "autoProvidesAspects", autoProvidesAspects);
}
}
void buildAutoRequires(Append writer, Set autoRequires) {
autoRequires.removeAll(requires);
if (!autoRequires.isEmpty()) {
buildProvidesMethod(writer, "autoRequires", autoRequires);
}
}
void buildAutoRequiresAspects(Append writer, Set autoRequires) {
autoRequires.removeAll(requires);
if (!autoRequires.isEmpty()) {
buildProvidesMethod(writer, "autoRequiresAspects", autoRequires);
}
}
void readModuleMetaData(TypeElement moduleType) {
final InjectModulePrism module = InjectModulePrism.getInstanceOn(moduleType);
final String name = module == null ? null : module.name();
details(name, moduleType);
readFactoryMetaData(moduleType);
}
private void readFactoryMetaData(TypeElement moduleType) {
final List extends Element> elements = moduleType.getEnclosedElements();
if (elements != null) {
for (Element element : elements) {
if (ElementKind.METHOD == element.getKind()) {
readBuildMethodDependencyMeta(element);
}
}
}
}
/**
* Return true if the scope is a custom scope and the dependency is provided
* by the "default" module. We could/should move to be tighter here at some point.
*/
boolean providedByOtherScope(String dependency) {
if (defaultScope) {
return false;
}
if (scopes.providedByDefaultScope(dependency)) {
return true;
}
return providesDependencyRecursive(dependency);
}
/**
* Recursively search including 'parent' scopes.
*/
private boolean providesDependencyRecursive(String dependency) {
if (providesDependencyLocally(dependency)) {
return true;
}
// look for required scopes ...
for (String require : requires) {
final ScopeInfo requiredScope = scopes.get(require);
// recursively search parent scope
if ((requiredScope != null) && requiredScope.providesDependencyRecursive(dependency)) {
// logWarn("dependency " + dependency + " provided by other scope " + requiredScope.name);
return true;
}
}
return false;
}
/**
* Return true if this module provides the dependency (non-recursive, local only).
*/
boolean providesDependencyLocally(String dependency) {
if (requires.contains(dependency) || pluginProvided.contains(dependency)) {
return true;
}
final String aspectDependency = aspectDependency(dependency);
for (MetaData meta : metaData.values()) {
if (dependency.equals(meta.type())) {
return true;
}
if (aspectDependency != null) {
if (aspectDependency.equals(meta.providesAspect())) {
return true;
}
} else {
final List provides = meta.provides();
if (provides != null && !provides.isEmpty()) {
for (String provide : provides) {
if (dependency.equals(provide)) {
return true;
}
}
}
}
}
return false;
}
private String aspectDependency(String dependency) {
if (Util.isAspectProvider(dependency)) {
return Util.extractAspectType(dependency);
}
return null;
}
boolean providedByPackage(String dependency) {
for (String pkg : requirePkg) {
if (dependency.startsWith(pkg)) {
return true;
}
}
return false;
}
boolean providedByOther(Dependency dependency) {
return dependency.isSoftDependency()
|| providedByPackage(dependency.name())
|| providedByOtherScope(dependency.name());
}
Set initModuleDependencies(Set importTypes) {
if (defaultScope || requires.isEmpty()) {
return importTypes;
}
for (String require : requires) {
final ScopeInfo otherScope = scopes.get(require);
if (otherScope == null) {
importTypes.add(require);
final String type = Util.shortName(require);
final String var = Util.initLower(type);
constructorDependencies.put(type, var);
}
}
return importTypes;
}
boolean addModuleConstructor() {
return !constructorDependencies.isEmpty();
}
Map constructorDependencies() {
return constructorDependencies;
}
boolean addWithBeans() {
return !constructorDependencies.isEmpty();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy