Skip to content

Commit

Permalink
Solves issue 118: support custom annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
stephanenicolas committed Oct 3, 2016
1 parent 85b36dd commit 97a3ed7
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@
## Release version 1.0.0 (to be released)

* Solves issue [#117](https://github.com/stephanenicolas/toothpick/issues/117) Factory code generator should strip the generic part of dependencies.
* Solves issue [#118](https://github.com/stephanenicolas/toothpick/issues/118) Support custom annotations.

## Release version 1.0.0-RC10 (Sep 29th 2016)

Expand Down
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -55,6 +56,16 @@ public abstract class ToothpickProcessor extends AbstractProcessor {
*/
public static final String PARAMETER_EXCLUDES = "toothpick_excludes";

/**
* The name of the annotation processor option to let TP know about custom scope annotation classes.
* This option is needed only in the case where a custom scope annotation is used on a class, and this
* class doesn't use any annotation processed out of the box by TP (i.e. javax.inject.* annotations).
* If you use custom scope annotations, it is a good practice to always use this option so that
* developers can use the new scope annotation in a very free way without having to consider the annotation
* processing internals.
*/
public static final String PARAMETER_ANNOTATION_TYPES = "toothpick_annotations";

/**
* The name annotation processor option to declare in which packages reside the sub-registries used by the generated registry,
* if it is created. Multiple entries are comma separated.
Expand All @@ -70,6 +81,7 @@ public abstract class ToothpickProcessor extends AbstractProcessor {
protected String toothpickRegistryPackageName;
protected List<String> toothpickRegistryChildrenPackageNameList;
protected String toothpickExcludeFilters = "java.*,android.*";
protected Set<String> supportedAnnotationTypes = new HashSet<>();

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
Expand All @@ -85,6 +97,10 @@ public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

public void addSupportedAnnotationType(String typeFQN) {
supportedAnnotationTypes.add(typeFQN);
}

protected boolean writeToFile(CodeGenerator codeGenerator, String fileDescription, Element... originatingElements) {
Writer writer = null;
boolean success = true;
Expand Down Expand Up @@ -112,21 +128,28 @@ protected boolean writeToFile(CodeGenerator codeGenerator, String fileDescriptio

/**
* Reads both annotation compilers {@link ToothpickProcessor#PARAMETER_REGISTRY_PACKAGE_NAME} and
* {@link ToothpickProcessor#PARAMETER_REGISTRY_CHILDREN_PACKAGE_NAMES} options from the arguments
* passed to the processor.
* {@link ToothpickProcessor#PARAMETER_REGISTRY_CHILDREN_PACKAGE_NAMES} and
* {@link ToothpickProcessor#PARAMETER_EXCLUDES}
* options from the arguments passed to the processor.
*/
protected void readProcessorOptions() {
Map<String, String> options = processingEnv.getOptions();
protected void readCommonProcessorOptions() {
readOptionRegistryPackageName();
readOptionRegistryChildrenPackageNames();
readOptionExcludes();
}

//we read options only if it's not defined. Allows tests to bypass options.
private void readOptionRegistryPackageName() {
Map<String, String> options = processingEnv.getOptions();
if (toothpickRegistryPackageName == null) {
toothpickRegistryPackageName = options.get(PARAMETER_REGISTRY_PACKAGE_NAME);
}
if (toothpickRegistryPackageName == null) {
warning("No option -A%s to the compiler." + " No registries will be generated.", PARAMETER_REGISTRY_PACKAGE_NAME);
}
}

//we read options only if it's not defined. Allows tests to bypass options.
private void readOptionRegistryChildrenPackageNames() {
Map<String, String> options = processingEnv.getOptions();
if (toothpickRegistryChildrenPackageNameList == null) {
toothpickRegistryChildrenPackageNameList = new ArrayList<>();
String toothpickRegistryChildrenPackageNames = options.get(PARAMETER_REGISTRY_CHILDREN_PACKAGE_NAMES);
Expand All @@ -140,13 +163,25 @@ protected void readProcessorOptions() {
if (toothpickRegistryChildrenPackageNameList == null) {
warning("No option -A%s was passed to the compiler." + " No sub registries will be used.", PARAMETER_REGISTRY_CHILDREN_PACKAGE_NAMES);
}
}

//getOrDefault could be used here, but it's ony available on jdk 7.
private void readOptionExcludes() {
Map<String, String> options = processingEnv.getOptions();
if (options.containsKey(PARAMETER_EXCLUDES)) {
toothpickExcludeFilters = options.get(PARAMETER_EXCLUDES);
}
}

protected void readOptionAnnotationTypes() {
Map<String, String> options = processingEnv.getOptions();
if (options.containsKey(PARAMETER_ANNOTATION_TYPES)) {
String additionalAnnotationTypes = options.get(PARAMETER_ANNOTATION_TYPES);
for (String additionalAnnotationType : additionalAnnotationTypes.split(",")) {
supportedAnnotationTypes.add(additionalAnnotationType.trim());
}
}
}

protected void error(String message, Object... args) {
processingEnv.getMessager().printMessage(ERROR, String.format(message, args));
}
Expand Down Expand Up @@ -234,9 +269,7 @@ private boolean isValidInjectedElementKind(VariableElement injectedTypeElement)
Element enclosingElement = injectedTypeElement.getEnclosingElement();
if (enclosingElement instanceof TypeElement) {
error(injectedTypeElement, "Field %s#%s is of type %s which is not supported by Toothpick.",
((TypeElement) enclosingElement).getQualifiedName(),
injectedTypeElement.getSimpleName(),
typeElement);
((TypeElement) enclosingElement).getQualifiedName(), injectedTypeElement.getSimpleName(), typeElement);
} else {
Element methodOrConstructorElement = enclosingElement;
enclosingElement = enclosingElement.getEnclosingElement();
Expand All @@ -258,16 +291,11 @@ private boolean isValidProviderOrLazy(Element element) {
if (declaredType.getTypeArguments().isEmpty()) {
Element enclosingElement = element.getEnclosingElement();
if (enclosingElement instanceof TypeElement) {
error(element, "Field %s#%s is not a valid %s.",
((TypeElement) enclosingElement).getQualifiedName(),
element.getSimpleName(),
declaredType);
error(element, "Field %s#%s is not a valid %s.", ((TypeElement) enclosingElement).getQualifiedName(), element.getSimpleName(), declaredType);
} else {
error(element, "Parameter %s in method/constructor %s#%s is not a valid %s.",
element.getSimpleName(), //
error(element, "Parameter %s in method/constructor %s#%s is not a valid %s.", element.getSimpleName(), //
((TypeElement) enclosingElement.getEnclosingElement()).getQualifiedName(), //
enclosingElement.getSimpleName(),
declaredType);
enclosingElement.getSimpleName(), declaredType);
}
return false;
}
Expand Down
@@ -1,16 +1,15 @@
package toothpick.compiler.factory;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
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.SupportedAnnotationTypes;
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;
Expand Down Expand Up @@ -53,27 +52,32 @@
* 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
@SupportedAnnotationTypes({
ToothpickProcessor.INJECT_ANNOTATION_CLASS_NAME, //
ToothpickProcessor.SINGLETON_ANNOTATION_CLASS_NAME, //
ToothpickProcessor.PRODUCES_SINGLETON_ANNOTATION_CLASS_NAME
})
@SupportedOptions({
ToothpickProcessor.PARAMETER_REGISTRY_PACKAGE_NAME, //
ToothpickProcessor.PARAMETER_REGISTRY_CHILDREN_PACKAGE_NAMES, //
ToothpickProcessor.PARAMETER_EXCLUDES
ToothpickProcessor.PARAMETER_EXCLUDES, //
ToothpickProcessor.PARAMETER_ANNOTATION_TYPES
}) //
public class FactoryProcessor extends ToothpickProcessor {

private static final String SUPPRESS_WARNING_ANNOTATION_INJECTABLE_VALUE = "injectable";

private Map<TypeElement, ConstructorInjectionTarget> mapTypeElementToConstructorInjectionTarget = new LinkedHashMap<>();

@Override
public Set<String> getSupportedAnnotationTypes() {
supportedAnnotationTypes.add(ToothpickProcessor.INJECT_ANNOTATION_CLASS_NAME);
supportedAnnotationTypes.add(ToothpickProcessor.SINGLETON_ANNOTATION_CLASS_NAME);
supportedAnnotationTypes.add(ToothpickProcessor.PRODUCES_SINGLETON_ANNOTATION_CLASS_NAME);
readOptionAnnotationTypes();
return supportedAnnotationTypes;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

readProcessorOptions();
findAndParseTargets(roundEnv);
readCommonProcessorOptions();
findAndParseTargets(roundEnv, annotations);

if (!roundEnv.processingOver()) {
return false;
Expand Down Expand Up @@ -109,12 +113,20 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
return false;
}

private void findAndParseTargets(RoundEnvironment roundEnv) {
private void findAndParseTargets(RoundEnvironment roundEnv, Set<? extends TypeElement> annotations) {
createFactoriesForClassesWithInjectAnnotatedConstructors(roundEnv);
createFactoriesForClassesAnnotatedScopeInstances(roundEnv);
createFactoriesForClassesAnnotatedSingleton(roundEnv);
createFactoriesForClassesAnnotatedWith(roundEnv, ProvidesSingletonInScope.class);
createFactoriesForClassesWithInjectAnnotatedFields(roundEnv);
createFactoriesForClassesWithInjectAnnotatedMethods(roundEnv);
createFactoriesForClassesAnnotatedWithScopeAnnotations(roundEnv, annotations);
}

private void createFactoriesForClassesAnnotatedWithScopeAnnotations(RoundEnvironment roundEnv, Set<? extends TypeElement> annotations) {
for (TypeElement annotation : annotations) {
if (annotation.getAnnotationsByType(Scope.class).length != 0) {
createFactoriesForClassesAnnotatedWith(roundEnv, annotation);
}
}
}

private void createFactoriesForClassesWithInjectAnnotatedMethods(RoundEnvironment roundEnv) {
Expand All @@ -129,16 +141,15 @@ private void createFactoriesForClassesWithInjectAnnotatedFields(RoundEnvironment
}
}

private void createFactoriesForClassesAnnotatedScopeInstances(RoundEnvironment roundEnv) {
for (Element singletonAnnotatedElement : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(
ProvidesSingletonInScope.class))) {
private void createFactoriesForClassesAnnotatedWith(RoundEnvironment roundEnv, Class<? extends Annotation> annotationClass) {
for (Element singletonAnnotatedElement : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(annotationClass))) {
TypeElement singletonAnnotatedTypeElement = (TypeElement) singletonAnnotatedElement;
processClassContainingInjectAnnotatedMember(singletonAnnotatedTypeElement, mapTypeElementToConstructorInjectionTarget);
}
}

private void createFactoriesForClassesAnnotatedSingleton(RoundEnvironment roundEnv) {
for (Element singletonAnnotatedElement : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Singleton.class))) {
private void createFactoriesForClassesAnnotatedWith(RoundEnvironment roundEnv, TypeElement annotationType) {
for (Element singletonAnnotatedElement : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(annotationType))) {
TypeElement singletonAnnotatedTypeElement = (TypeElement) singletonAnnotatedElement;
processClassContainingInjectAnnotatedMember(singletonAnnotatedTypeElement, mapTypeElementToConstructorInjectionTarget);
}
Expand Down Expand Up @@ -245,8 +256,8 @@ private boolean isValidInjectAnnotatedConstructor(ExecutableElement element) {
private ConstructorInjectionTarget createConstructorInjectionTarget(ExecutableElement constructorElement) {
TypeElement enclosingElement = (TypeElement) constructorElement.getEnclosingElement();
final String scopeName = getScopeName(enclosingElement);
final boolean hasScopeInstancesAnnotation = enclosingElement.getAnnotation(
ProvidesSingletonInScope.class) != null;
final boolean hasScopeInstancesAnnotation = enclosingElement //
.getAnnotation(ProvidesSingletonInScope.class) != null;
if (hasScopeInstancesAnnotation && scopeName == null) {
error(enclosingElement, "The type %s uses @ProvidesSingletonInScope but doesn't have a scope annotation.",
enclosingElement.getQualifiedName().toString());
Expand All @@ -264,7 +275,8 @@ private ConstructorInjectionTarget createConstructorInjectionTarget(TypeElement
final String scopeName = getScopeName(typeElement);
final boolean hasScopeInstancesAnnotation = typeElement.getAnnotation(ProvidesSingletonInScope.class) != null;
if (hasScopeInstancesAnnotation && scopeName == null) {
error(typeElement, "The type %s uses @ProvidesSingletonInScope but doesn't have a scope annotation.", typeElement.getQualifiedName().toString());
error(typeElement, "The type %s uses @ProvidesSingletonInScope but doesn't have a scope annotation.",
typeElement.getQualifiedName().toString());
}
TypeElement superClassWithInjectedMembers = getMostDirectSuperClassWithInjectedMembers(typeElement, false);

Expand All @@ -285,7 +297,7 @@ private ConstructorInjectionTarget createConstructorInjectionTarget(TypeElement
if (constructorElement.getParameters().isEmpty()) {
if (constructorElement.getModifiers().contains(Modifier.PRIVATE)) {
if (!isInjectableWarningSuppressed(typeElement)) {
warning(constructorElement,
warning(constructorElement, //
"The class %s has a private default constructor, Toothpick can't create a factory for it.",
typeElement.getQualifiedName().toString());
}
Expand All @@ -299,10 +311,9 @@ private ConstructorInjectionTarget createConstructorInjectionTarget(TypeElement
}

if (!isInjectableWarningSuppressed(typeElement)) {
warning(typeElement,
"The class %s has injected fields but has no injected constructor, and no public default constructor."
+ " Toothpick can't create a factory for it.",
typeElement.getQualifiedName().toString());
warning(typeElement, //
"The class %s has injected fields but has no injected constructor, and no public default constructor." //
+ " Toothpick can't create a factory for it.", typeElement.getQualifiedName().toString());
}
return null;
}
Expand Down
Expand Up @@ -49,7 +49,7 @@ public class MemberInjectorProcessor extends ToothpickProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
readProcessorOptions();
readCommonProcessorOptions();
findAndParseTargets(roundEnv);

if (!roundEnv.processingOver()) {
Expand Down

0 comments on commit 97a3ed7

Please sign in to comment.