toothpick.compiler.factory.FactoryProcessor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of toothpick-compiler Show documentation
Show all versions of toothpick-compiler Show documentation
'Annotation Processors of toothpick'
The newest version!
/*
* Copyright 2019 Stephane Nicolas
* Copyright 2019 Daniel Molinero Reguera
*
* 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 toothpick.compiler.factory;
import static java.lang.String.format;
import static javax.lang.model.element.Modifier.PRIVATE;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.inject.Inject;
import javax.inject.Scope;
import javax.inject.Singleton;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import toothpick.Factory;
import toothpick.InjectConstructor;
import toothpick.ProvidesReleasable;
import toothpick.ProvidesSingleton;
import toothpick.Releasable;
import toothpick.compiler.common.ToothpickProcessor;
import toothpick.compiler.factory.generators.FactoryGenerator;
import toothpick.compiler.factory.targets.ConstructorInjectionTarget;
/**
* This processor's role is to create {@link Factory}. We create factories in different situations :
*
*
* - When a class {@code Foo} has an {@link javax.inject.Inject} annotated constructor :
* --> we create a Factory to create {@code Foo} instances.
*
*
* The processor will also try to relax the constraints to generate factories in a few cases. These
* factories are helpful as they require less work from developers :
*
*
* - When a class {@code Foo} is annotated with {@link javax.inject.Singleton} :
* --> it will use the annotated constructor or the default constructor if possible. Otherwise
* an error is raised.
* - When a class {@code Foo} is annotated with {@link ProvidesSingleton} :
* --> it will use the annotated constructor or the default constructor if possible. Otherwise
* an error is raised.
* - When a class {@code Foo} has an {@link javax.inject.Inject} annotated field {@code @Inject
* B b} :
* --> it will use the annotated constructor or the default constructor if possible. Otherwise
* an error is raised.
* - When a class {@code Foo} has an {@link javax.inject.Inject} method {@code @Inject m()} :
*
* --> it will use the annotated constructor or the default constructor if possible. Otherwise
* an error is raised.
*
*
* Note that if a class is abstract, the relax mechanism doesn't generate a factory and raises no
* error.
*/
// http://stackoverflow.com/a/2067863/693752
@SupportedOptions({
ToothpickProcessor.PARAMETER_EXCLUDES, //
ToothpickProcessor.PARAMETER_ANNOTATION_TYPES, //
ToothpickProcessor.PARAMETER_CRASH_WHEN_NO_FACTORY_CAN_BE_CREATED, //
}) //
public class FactoryProcessor extends ToothpickProcessor {
private static final String SUPPRESS_WARNING_ANNOTATION_INJECTABLE_VALUE = "injectable";
private Map mapTypeElementToConstructorInjectionTarget;
private Boolean crashWhenNoFactoryCanBeCreated;
private Map allRoundsGeneratedToTypeElement = new HashMap<>();
@Override
public Set getSupportedAnnotationTypes() {
supportedAnnotationTypes.add(ToothpickProcessor.INJECT_ANNOTATION_CLASS_NAME);
supportedAnnotationTypes.add(ToothpickProcessor.SINGLETON_ANNOTATION_CLASS_NAME);
supportedAnnotationTypes.add(ToothpickProcessor.PRODUCES_SINGLETON_ANNOTATION_CLASS_NAME);
supportedAnnotationTypes.add(ToothpickProcessor.INJECT_CONSTRUCTOR_ANNOTATION_CLASS_NAME);
readOptionAnnotationTypes();
return supportedAnnotationTypes;
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
readCommonProcessorOptions();
readCrashWhenNoFactoryCanBeCreatedOption();
mapTypeElementToConstructorInjectionTarget = new LinkedHashMap<>();
findAndParseTargets(roundEnv, annotations);
// Generate Factories
for (Map.Entry entry :
mapTypeElementToConstructorInjectionTarget.entrySet()) {
ConstructorInjectionTarget constructorInjectionTarget = entry.getValue();
FactoryGenerator factoryGenerator =
new FactoryGenerator(constructorInjectionTarget, typeUtils);
TypeElement typeElement = entry.getKey();
String fileDescription = format("Factory for type %s", typeElement);
writeToFile(factoryGenerator, fileDescription, typeElement);
allRoundsGeneratedToTypeElement.put(factoryGenerator.getFqcn(), typeElement);
}
return false;
}
private void readCrashWhenNoFactoryCanBeCreatedOption() {
Map options = processingEnv.getOptions();
if (crashWhenNoFactoryCanBeCreated == null) {
crashWhenNoFactoryCanBeCreated =
Boolean.parseBoolean(options.get(PARAMETER_CRASH_WHEN_NO_FACTORY_CAN_BE_CREATED));
}
}
private void findAndParseTargets(
RoundEnvironment roundEnv, Set extends TypeElement> annotations) {
createFactoriesForClassesAnnotatedWithInjectConstructor(roundEnv);
createFactoriesForClassesWithInjectAnnotatedConstructors(roundEnv);
createFactoriesForClassesAnnotatedWith(roundEnv, ProvidesSingleton.class);
createFactoriesForClassesWithInjectAnnotatedFields(roundEnv);
createFactoriesForClassesWithInjectAnnotatedMethods(roundEnv);
createFactoriesForClassesAnnotatedWithScopeAnnotations(roundEnv, annotations);
}
private void createFactoriesForClassesAnnotatedWithScopeAnnotations(
RoundEnvironment roundEnv, Set extends TypeElement> annotations) {
for (TypeElement annotation : annotations) {
if (annotation.getAnnotation(Scope.class) != null) {
checkScopeAnnotationValidity(annotation);
createFactoriesForClassesAnnotatedWith(roundEnv, annotation);
}
}
}
private void createFactoriesForClassesWithInjectAnnotatedMethods(RoundEnvironment roundEnv) {
for (ExecutableElement methodElement :
ElementFilter.methodsIn(roundEnv.getElementsAnnotatedWith(Inject.class))) {
processClassContainingInjectAnnotatedMember(
methodElement.getEnclosingElement(), mapTypeElementToConstructorInjectionTarget);
}
}
private void createFactoriesForClassesWithInjectAnnotatedFields(RoundEnvironment roundEnv) {
for (VariableElement fieldElement :
ElementFilter.fieldsIn(roundEnv.getElementsAnnotatedWith(Inject.class))) {
processClassContainingInjectAnnotatedMember(
fieldElement.getEnclosingElement(), mapTypeElementToConstructorInjectionTarget);
}
}
private void createFactoriesForClassesAnnotatedWith(
RoundEnvironment roundEnv, Class extends Annotation> annotationClass) {
for (Element annotatedElement :
ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(annotationClass))) {
TypeElement annotatedTypeElement = (TypeElement) annotatedElement;
processClassContainingInjectAnnotatedMember(
annotatedTypeElement, mapTypeElementToConstructorInjectionTarget);
}
}
private void createFactoriesForClassesAnnotatedWith(
RoundEnvironment roundEnv, TypeElement annotationType) {
for (Element annotatedElement :
ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(annotationType))) {
TypeElement annotatedTypeElement = (TypeElement) annotatedElement;
processClassContainingInjectAnnotatedMember(
annotatedTypeElement, mapTypeElementToConstructorInjectionTarget);
}
}
private void createFactoriesForClassesWithInjectAnnotatedConstructors(RoundEnvironment roundEnv) {
for (ExecutableElement constructorElement :
ElementFilter.constructorsIn(roundEnv.getElementsAnnotatedWith(Inject.class))) {
TypeElement enclosingElement = (TypeElement) constructorElement.getEnclosingElement();
if (!isSingleInjectAnnotatedConstructor(constructorElement)) {
error(
constructorElement,
"Class %s cannot have more than one @Inject annotated constructor.",
enclosingElement.getQualifiedName());
}
processInjectAnnotatedConstructor(
constructorElement, mapTypeElementToConstructorInjectionTarget);
}
}
private void createFactoriesForClassesAnnotatedWithInjectConstructor(RoundEnvironment roundEnv) {
for (Element annotatedElement :
ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(InjectConstructor.class))) {
TypeElement annotatedTypeElement = (TypeElement) annotatedElement;
List constructorElements =
ElementFilter.constructorsIn(annotatedTypeElement.getEnclosedElements());
if (constructorElements.size() != 1
|| constructorElements.get(0).getAnnotation(Inject.class) != null) {
error(
constructorElements.get(0),
"Class %s is annotated with @InjectInjectConstructor. Therefore, It must have one unique constructor and it should not be annotated with @Inject.",
annotatedTypeElement.getQualifiedName());
}
processInjectAnnotatedConstructor(
constructorElements.get(0), mapTypeElementToConstructorInjectionTarget);
}
}
private void processClassContainingInjectAnnotatedMember(
Element enclosingElement,
Map mapTypeElementToConstructorInjectionTarget) {
final TypeElement typeElement = (TypeElement) typeUtils.asElement(enclosingElement.asType());
if (mapTypeElementToConstructorInjectionTarget.containsKey(typeElement)) {
// the class is already known
return;
}
if (isExcludedByFilters(typeElement)) {
return;
}
// Verify common generated code restrictions.
if (!canTypeHaveAFactory(typeElement)) {
return;
}
ConstructorInjectionTarget constructorInjectionTarget =
createConstructorInjectionTarget(typeElement);
if (constructorInjectionTarget != null) {
mapTypeElementToConstructorInjectionTarget.put(typeElement, constructorInjectionTarget);
}
}
private boolean isSingleInjectAnnotatedConstructor(Element constructorElement) {
TypeElement enclosingElement = (TypeElement) constructorElement.getEnclosingElement();
boolean isSingleInjectedConstructor = true;
List constructorElements =
ElementFilter.constructorsIn(enclosingElement.getEnclosedElements());
for (ExecutableElement constructorElementInClass : constructorElements) {
if (constructorElementInClass.getAnnotation(Inject.class) != null
&& !constructorElement.equals(constructorElementInClass)) {
isSingleInjectedConstructor = false;
}
}
return isSingleInjectedConstructor;
}
private void processInjectAnnotatedConstructor(
ExecutableElement constructorElement,
Map targetClassMap) {
TypeElement enclosingElement = (TypeElement) constructorElement.getEnclosingElement();
// Verify common generated code restrictions.
if (!isValidInjectAnnotatedConstructor(constructorElement)) {
return;
}
if (isExcludedByFilters(enclosingElement)) {
return;
}
if (!canTypeHaveAFactory(enclosingElement)) {
error(
enclosingElement,
"The class %s is abstract or private. It cannot have an injected constructor.",
enclosingElement.getQualifiedName());
return;
}
targetClassMap.put(enclosingElement, createConstructorInjectionTarget(constructorElement));
}
private boolean isValidInjectAnnotatedConstructor(ExecutableElement element) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify modifiers.
Set modifiers = element.getModifiers();
if (modifiers.contains(PRIVATE)) {
error(
element,
"@Inject constructors must not be private in class %s.",
enclosingElement.getQualifiedName());
return false;
}
// Verify parentScope modifiers.
Set parentModifiers = enclosingElement.getModifiers();
if (parentModifiers.contains(PRIVATE)) {
error(
element,
"Class %s is private. @Inject constructors are not allowed in private classes.",
enclosingElement.getQualifiedName());
return false;
}
if (isNonStaticInnerClass(enclosingElement)) {
return false;
}
for (VariableElement paramElement : element.getParameters()) {
if (!isValidInjectedType(paramElement)) {
return false;
}
}
return true;
}
private ConstructorInjectionTarget createConstructorInjectionTarget(
ExecutableElement constructorElement) {
TypeElement enclosingElement = (TypeElement) constructorElement.getEnclosingElement();
final String scopeName = getScopeName(enclosingElement);
final boolean hasSingletonAnnotation = hasSingletonAnnotation(enclosingElement);
final boolean hasReleasableAnnotation = hasReleasableAnnotation(enclosingElement);
final boolean hasProvidesSingletonInScopeAnnotation =
hasProvidesSingletonInScopeAnnotation(enclosingElement);
final boolean hasProvidesReleasableAnnotation =
hasProvidesReleasableAnnotation(enclosingElement);
checkReleasableAnnotationValidity(
enclosingElement, hasReleasableAnnotation, hasSingletonAnnotation);
checkProvidesReleasableAnnotationValidity(
enclosingElement, hasReleasableAnnotation, hasSingletonAnnotation);
if (hasProvidesSingletonInScopeAnnotation && scopeName == null) {
error(
enclosingElement,
"The type %s uses @ProvidesSingleton but doesn't have a scope annotation.",
enclosingElement.getQualifiedName().toString());
}
TypeElement superClassWithInjectedMembers =
getMostDirectSuperClassWithInjectedMembers(enclosingElement, false);
ConstructorInjectionTarget constructorInjectionTarget =
new ConstructorInjectionTarget(
enclosingElement,
scopeName,
hasSingletonAnnotation,
hasReleasableAnnotation,
hasProvidesSingletonInScopeAnnotation,
hasProvidesReleasableAnnotation,
superClassWithInjectedMembers);
constructorInjectionTarget.parameters.addAll(getParamInjectionTargetList(constructorElement));
constructorInjectionTarget.throwsThrowable = !constructorElement.getThrownTypes().isEmpty();
return constructorInjectionTarget;
}
private ConstructorInjectionTarget createConstructorInjectionTarget(TypeElement typeElement) {
final String scopeName = getScopeName(typeElement);
final boolean hasSingletonAnnotation = hasSingletonAnnotation(typeElement);
final boolean hasReleasableAnnotation = hasReleasableAnnotation(typeElement);
final boolean hasProvidesSingletonInScopeAnnotation =
hasProvidesSingletonInScopeAnnotation(typeElement);
final boolean hasProvidesReleasableAnnotation = hasProvidesReleasableAnnotation(typeElement);
checkReleasableAnnotationValidity(typeElement, hasReleasableAnnotation, hasSingletonAnnotation);
checkProvidesReleasableAnnotationValidity(
typeElement, hasReleasableAnnotation, hasSingletonAnnotation);
if (hasProvidesSingletonInScopeAnnotation && scopeName == null) {
error(
typeElement,
"The type %s uses @ProvidesSingleton but doesn't have a scope annotation.",
typeElement.getQualifiedName().toString());
}
TypeElement superClassWithInjectedMembers =
getMostDirectSuperClassWithInjectedMembers(typeElement, false);
List constructorElements =
ElementFilter.constructorsIn(typeElement.getEnclosedElements());
// we just need to deal with the case of the default constructor only.
// like Guice, we will call it by default in the optimistic factory
// injected constructors will be handled at some point in the compilation cycle
// if there is an injected constructor, it will be caught later, just leave
for (ExecutableElement constructorElement : constructorElements) {
if (constructorElement.getAnnotation(Inject.class) != null) {
return null;
}
}
final String cannotCreateAFactoryMessage =
" Toothpick can't create a factory for it." //
+ " If this class is itself a DI entry point (i.e. you call TP.inject(this) at some point), " //
+ " then you can remove this warning by adding @SuppressWarnings(\"Injectable\") to the class." //
+ " A typical example is a class using injection to assign its fields, that calls TP.inject(this)," //
+ " but it needs a parameter for its constructor and this parameter is not injectable.";
// search for default constructor
for (ExecutableElement constructorElement : constructorElements) {
if (constructorElement.getParameters().isEmpty()) {
if (constructorElement.getModifiers().contains(Modifier.PRIVATE)) {
if (!isInjectableWarningSuppressed(typeElement)) {
String message =
format(
"The class %s has a private default constructor. "
+ cannotCreateAFactoryMessage, //
typeElement.getQualifiedName().toString());
crashOrWarnWhenNoFactoryCanBeCreated(constructorElement, message);
}
return null;
}
ConstructorInjectionTarget constructorInjectionTarget =
new ConstructorInjectionTarget(
typeElement,
scopeName,
hasSingletonAnnotation,
hasReleasableAnnotation,
hasProvidesSingletonInScopeAnnotation,
hasProvidesReleasableAnnotation,
superClassWithInjectedMembers);
return constructorInjectionTarget;
}
}
if (!isInjectableWarningSuppressed(typeElement)) {
String message =
format(
"The class %s has injected members or a scope annotation "
+ "but has no @Inject annotated (non-private) constructor " //
+ " nor a non-private default constructor. "
+ cannotCreateAFactoryMessage, //
typeElement.getQualifiedName().toString());
crashOrWarnWhenNoFactoryCanBeCreated(typeElement, message);
}
return null;
}
private void crashOrWarnWhenNoFactoryCanBeCreated(Element element, String message) {
if (crashWhenNoFactoryCanBeCreated != null && crashWhenNoFactoryCanBeCreated) {
error(element, message);
} else {
warning(element, message);
}
}
/**
* Lookup {@link javax.inject.Scope} annotated annotations to provide the name of the scope the
* {@code typeElement} belongs to. The method logs an error if the {@code typeElement} has
* multiple scope annotations.
*
* @param typeElement the element for which a scope is to be found.
* @return the scope of this {@code typeElement} or {@code null} if it has no scope annotations.
*/
private String getScopeName(TypeElement typeElement) {
String scopeName = null;
boolean hasScopeAnnotation = false;
for (AnnotationMirror annotationMirror : typeElement.getAnnotationMirrors()) {
TypeElement annotationTypeElement =
(TypeElement) annotationMirror.getAnnotationType().asElement();
boolean isSingletonAnnotation =
annotationTypeElement.getQualifiedName().contentEquals("javax.inject.Singleton");
if (!isSingletonAnnotation && annotationTypeElement.getAnnotation(Scope.class) != null) {
checkScopeAnnotationValidity(annotationTypeElement);
if (scopeName != null) {
error(typeElement, "Only one @Scope qualified annotation is allowed : %s", scopeName);
}
scopeName = annotationTypeElement.getQualifiedName().toString();
}
if (isSingletonAnnotation) {
hasScopeAnnotation = true;
}
}
if (hasScopeAnnotation && scopeName == null) {
scopeName = "javax.inject.Singleton";
}
return scopeName;
}
private boolean hasSingletonAnnotation(TypeElement typeElement) {
return typeElement.getAnnotation(Singleton.class) != null;
}
private boolean hasReleasableAnnotation(TypeElement typeElement) {
return typeElement.getAnnotation(Releasable.class) != null;
}
private boolean hasProvidesSingletonInScopeAnnotation(TypeElement typeElement) {
return typeElement.getAnnotation(ProvidesSingleton.class) != null;
}
private boolean hasProvidesReleasableAnnotation(TypeElement typeElement) {
return typeElement.getAnnotation(ProvidesReleasable.class) != null;
}
private void checkReleasableAnnotationValidity(
TypeElement typeElement, boolean hasReleasableAnnotation, boolean hasSingletonAnnotation) {
if (hasReleasableAnnotation && !hasSingletonAnnotation) {
error(
typeElement,
"Class %s is annotated with @Releasable, "
+ "it should also be annotated with either @Singleton.",
typeElement.getQualifiedName());
}
}
private void checkProvidesReleasableAnnotationValidity(
TypeElement typeElement,
boolean hasProvidesReleasableAnnotation,
boolean hasProvideSingletonInScopeAnnotation) {
if (hasProvidesReleasableAnnotation && !hasProvideSingletonInScopeAnnotation) {
error(
typeElement,
"Class %s is annotated with @ProvidesReleasable, "
+ "it should also be annotated with either @ProvidesSingleton.",
typeElement.getQualifiedName());
}
}
private void checkScopeAnnotationValidity(TypeElement annotation) {
if (annotation.getAnnotation(Scope.class) == null) {
error(
annotation,
"Scope Annotation %s does not contain Scope annotation.",
annotation.getQualifiedName());
return;
}
Retention retention = annotation.getAnnotation(Retention.class);
if (retention == null || retention.value() != RetentionPolicy.RUNTIME) {
error(
annotation,
"Scope Annotation %s does not have RUNTIME retention policy.",
annotation.getQualifiedName());
}
}
/**
* Checks if the injectable warning is suppressed for the TypeElement, through the usage
* of @SuppressWarning("Injectable").
*
* @param typeElement the element to check if the warning is suppressed.
* @return true is the injectable warning is suppressed, false otherwise.
*/
private boolean isInjectableWarningSuppressed(TypeElement typeElement) {
return hasWarningSuppressed(typeElement, SUPPRESS_WARNING_ANNOTATION_INJECTABLE_VALUE);
}
private boolean canTypeHaveAFactory(TypeElement typeElement) {
boolean isAbstract = typeElement.getModifiers().contains(Modifier.ABSTRACT);
boolean isPrivate = typeElement.getModifiers().contains(Modifier.PRIVATE);
return !isAbstract && !isPrivate;
}
// used for testing only
void setToothpickExcludeFilters(String toothpickExcludeFilters) {
this.toothpickExcludeFilters = toothpickExcludeFilters;
}
// used for testing only
void setCrashWhenNoFactoryCanBeCreated(boolean crashWhenNoFactoryCanBeCreated) {
this.crashWhenNoFactoryCanBeCreated = crashWhenNoFactoryCanBeCreated;
}
// used for testing only
TypeElement getOriginatingElement(String generatedQualifiedName) {
return allRoundsGeneratedToTypeElement.get(generatedQualifiedName);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy