package restx.factory.processor; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.*; import com.google.common.io.CharStreams; import com.samskivert.mustache.Template; import restx.common.processor.RestxAbstractProcessor; import restx.factory.*; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedOptions; import javax.inject.Inject; import javax.inject.Named; import javax.lang.model.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeMirror; import java.io.*; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import static restx.common.Mustaches.compile; /** * User: xavierhanin * Date: 1/18/13 * Time: 10:02 PM */ @SupportedAnnotationTypes({ "restx.factory.Component", "restx.factory.Module", "restx.factory.Provides", "restx.factory.Alternative", "restx.factory.Machine" }) @SupportedOptions({ "debug" }) public class FactoryAnnotationProcessor extends RestxAbstractProcessor { final Template componentMachineTpl; final Template conditionalMachineTpl; final Template moduleMachineTpl; private final FactoryAnnotationProcessor.ServicesDeclaration machinesDeclaration; public FactoryAnnotationProcessor() { componentMachineTpl = compile(FactoryAnnotationProcessor.class, "ComponentMachine.mustache"); conditionalMachineTpl = compile(FactoryAnnotationProcessor.class, "ConditionalMachine.mustache"); moduleMachineTpl = compile(FactoryAnnotationProcessor.class, "ModuleMachine.mustache"); machinesDeclaration = new ServicesDeclaration("restx.factory.FactoryMachine"); } @Override protected boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws IOException { machinesDeclaration.processing(); if (roundEnv.processingOver()) { machinesDeclaration.generate(); } else { processComponents(roundEnv); processAlternatives(roundEnv); processModules(roundEnv); processMachines(roundEnv); } return true; } private void processModules(RoundEnvironment roundEnv) throws IOException { for (Element annotation : roundEnv.getElementsAnnotatedWith(Module.class)) { try { if (!(annotation instanceof TypeElement)) { error("annotating element " + annotation + " of type " + annotation.getKind().name() + " with @Module is not supported", annotation); continue; } TypeElement typeElem = (TypeElement) annotation; Module mod = typeElem.getAnnotation(Module.class); When classWhen = typeElem.getAnnotation(When.class); ModuleClass module = new ModuleClass(typeElem.getQualifiedName().toString(), typeElem, mod.priority()); for (Element element : typeElem.getEnclosedElements()) { // look for Provides or Alternative elements Provides provides = element.getAnnotation(Provides.class); Alternative alternative = element.getAnnotation(Alternative.class); if (element instanceof ExecutableElement && element.getKind() == ElementKind.METHOD) { ExecutableElement exec = (ExecutableElement) element; When methodWhen = exec.getAnnotation(When.class); // multiple cases, provides only, provides with when, and alternative if (provides != null && methodWhen == null && classWhen == null) { // add a provider method to the module processProviderMethod(mod, module, provides, exec); } else { // When can be either defined at class level, or on the method. But both are not allowed. When whenToUse; if (classWhen != null) { if (methodWhen != null) { error("the module class is annotated with @When, so methods are not allowed to be annotated with @When", exec); continue; } whenToUse = classWhen; } else { whenToUse = methodWhen; } if (provides != null) { // we need to create a conditional provider method processConditionalProviderMethod( mod, module, exec.getReturnType().toString(), getInjectionName(exec.getAnnotation(Named.class)).or(exec.getSimpleName().toString()), provides.priority() == 0 ? mod.priority() : provides.priority(), whenToUse, "Conditional", exec ); } else if (alternative != null) { // when annotation is required with alternative if (whenToUse == null) { error("an Alternative MUST be annotated with @When to tell when it must be activated, or the whole module must be annotated with @When", exec); continue; } TypeElement alternativeTo = null; try { alternative.to(); } catch (MirroredTypeException mte) { alternativeTo = asTypeElement(mte.getTypeMirror()); } String namedAttribute = alternative.named(); Optional<String> injectionName = getInjectionName(alternativeTo.getAnnotation(Named.class)); String componentName; if (!namedAttribute.isEmpty()) { // the conditional component name is the one specified in @Alternative annotation componentName = namedAttribute; } else if (injectionName.isPresent()) { // or the Name of the reference class componentName = injectionName.get(); } else { // or the simple name of the produced class componentName = alternativeTo.getSimpleName().toString(); } // add a conditional provider method to the module processConditionalProviderMethod( mod, module, alternativeTo.getQualifiedName().toString(), componentName, alternative.priority(), whenToUse, "Alternative", exec ); } } } } // finally generate the machine with all methods found generateMachineFile(module); } catch (IOException e) { fatalError("error when processing " + annotation, e, annotation); } } } private void processProviderMethod(Module mod, ModuleClass module, Provides provides, ExecutableElement exec) { ProviderMethod m = new ProviderMethod( exec.getReturnType().toString(), exec.getSimpleName().toString(), provides.priority() == 0 ? mod.priority() : provides.priority(), getInjectionName(exec.getAnnotation(Named.class)), exec); buildInjectableParams(exec, m.parameters); buildCheckedExceptions(exec, m.exceptions); module.providerMethods.add(m); } private void processConditionalProviderMethod(Module mod, ModuleClass module, String componentType, String componentName, int priority, When when, String factoryMachineNameSuffix, ExecutableElement exec) { ConditionalProviderMethod m = new ConditionalProviderMethod( componentType, componentName, exec.getSimpleName().toString(), priority == 0 ? mod.priority() : priority, when.name(), when.value(), factoryMachineNameSuffix, exec); buildInjectableParams(exec, m.parameters); buildCheckedExceptions(exec, m.exceptions); module.conditionalProviderMethods.add(m); } private void processMachines(RoundEnvironment roundEnv) throws IOException { for (Element annotation : roundEnv.getElementsAnnotatedWith(Machine.class)) { try { if (!(annotation instanceof TypeElement)) { error("annotating element " + annotation + " of type " + annotation.getKind().name() + " with @Machine is not supported", annotation); continue; } TypeElement typeElem = (TypeElement) annotation; machinesDeclaration.declareService(typeElem.getQualifiedName().toString()); } catch (Exception e) { fatalError("error when processing " + annotation, e, annotation); } } } private void processComponents(RoundEnvironment roundEnv) throws IOException { for (Element elem : roundEnv.getElementsAnnotatedWith(Component.class)) { try { if (!(elem instanceof TypeElement)) { error("annotating element " + elem + " of type " + elem.getKind().name() + " with @Component is not supported", elem); continue; } TypeElement component = (TypeElement) elem; ExecutableElement exec = findInjectableConstructor(component); Component componentAnnotation = component.getAnnotation(Component.class); TypeElement asClass = null; try { componentAnnotation.asClass(); } catch (MirroredTypeException mte) { asClass = asTypeElement(mte.getTypeMirror()); } if (asClass == null) { // no class as been forced, so use the annotated class asClass = component; } ComponentClass componentClass = new ComponentClass( component.getQualifiedName().toString(), getPackage(component).getQualifiedName().toString(), component.getSimpleName().toString(), asClass.getQualifiedName().toString(), getInjectionName(component.getAnnotation(Named.class)), componentAnnotation.priority(), component); buildInjectableParams(exec, componentClass.parameters); When when = component.getAnnotation(When.class); if (when == null) { generateMachineFile(componentClass); } else { generateMachineFile(componentClass, when); } } catch (Exception e) { fatalError("error when processing " + elem, e, elem); } } } private void processAlternatives(RoundEnvironment roundEnv) throws IOException { for (Element elem : roundEnv.getElementsAnnotatedWith(Alternative.class)) { try { if (elem instanceof ExecutableElement && elem.getKind() == ElementKind.METHOD) { // skip this annotation, if it is in a module, it will been managed by processModules continue; } if (!(elem instanceof TypeElement)) { error("annotating element " + elem + " of type " + elem.getKind().name() + " with @Alternative is not supported", elem); continue; } TypeElement component = (TypeElement) elem; ExecutableElement exec = findInjectableConstructor(component); Alternative alternative = component.getAnnotation(Alternative.class); TypeElement alternativeTo = null; if (alternative != null) { try { alternative.to(); } catch (MirroredTypeException mte) { alternativeTo = asTypeElement(mte.getTypeMirror()); } } // generate the name for the alternative, could be: // - the "named" value if defined // - the value of @Named of the referenced component if defined // - the referenced component simple name class, if none of the above String namedAttribute = alternative.named(); Optional<String> injectionName; if (!namedAttribute.isEmpty()) { injectionName = Optional.of(namedAttribute); } else { injectionName = getInjectionName(alternativeTo.getAnnotation(Named.class)); } ComponentClass componentClass = new ComponentClass( component.getQualifiedName().toString(), getPackage(component).getQualifiedName().toString(), component.getSimpleName().toString(), getInjectionName(component.getAnnotation(Named.class)), alternative.priority(), component); ComponentClass alternativeToComponentClass = new ComponentClass( alternativeTo.getQualifiedName().toString(), getPackage(alternativeTo).getQualifiedName().toString(), alternativeTo.getSimpleName().toString(), injectionName, alternative.priority(), alternativeTo); When when = component.getAnnotation(When.class); if (when == null) { error("an Alternative MUST be annotated with @When to tell when it must be activated", elem); continue; } Named named = component.getAnnotation(Named.class); if (named != null) { warn("to specify a 'name' for an Alternative use 'named' attribute, Named annotation will be ignored", elem); } buildInjectableParams(exec, componentClass.parameters); generateMachineFile(componentClass, alternativeToComponentClass, when); } catch (Exception e) { fatalError("error when processing " + elem, e, elem); } } } private ExecutableElement findInjectableConstructor(TypeElement component) { ExecutableElement exec = null; for (Element element : component.getEnclosedElements()) { if (element instanceof ExecutableElement && element.getKind() == ElementKind.CONSTRUCTOR) { if (exec == null || element.getAnnotation(Inject.class) != null) { exec = (ExecutableElement) element; if (exec.getAnnotation(Inject.class) != null) { // if a constructor is marked with @Inject we use it whatever other constructors are available return exec; } } } } return exec; } private void buildCheckedExceptions(ExecutableElement executableElement, List<String> exceptions) { for (TypeMirror e : executableElement.getThrownTypes()) { // Assuming Exceptions never have type arguments. Qualified names include type arguments. String exception = ((TypeElement) ((DeclaredType) e).asElement()).getQualifiedName().toString(); exceptions.add(exception); } } private void buildInjectableParams(ExecutableElement executableElement, List<InjectableParameter> parameters) { for (VariableElement p : executableElement.getParameters()) { parameters.add(new InjectableParameter( p.asType(), p.getSimpleName().toString(), getInjectionName(p.getAnnotation(Named.class)) )); } } private Optional<String> getInjectionName(Named named) { return named != null ? Optional.of(named.value()) : Optional.<String>absent(); } private void generateMachineFile(ModuleClass moduleClass) throws IOException { List<ImmutableMap<String, Object>> engines = Lists.newArrayList(); List<ImmutableMap<String, Object>> conditionalsEngines = Lists.newArrayList(); for (ProviderMethod method : moduleClass.providerMethods) { engines.add(ImmutableMap.<String, Object>builder() .put("type", method.type) .put("name", method.name) .put("enginePriority", method.priority) .put("injectionName", method.injectionName.isPresent() ? method.injectionName.get() : method.name) .put("queriesDeclarations", Joiner.on("\n").join(buildQueriesDeclarationsCode(method.parameters))) .put("queries", Joiner.on(",\n").join(buildQueriesNames(method.parameters))) .put("parameters", Joiner.on(",\n").join(buildParamFromSatisfiedBomCode(method.parameters))) .put("exceptions", method.exceptions.isEmpty() ? false : Joiner.on("|").join(method.exceptions)) .build()); } for (ConditionalProviderMethod method : moduleClass.conditionalProviderMethods) { conditionalsEngines.add(ImmutableMap.<String, Object>builder() .put("componentType", method.componentType) .put("componentName", method.componentName) .put("conditionalFactoryMachineName", method.methodName + method.componentName + method.factoryMachineNameSuffix) .put("whenName", method.whenName) .put("whenValue", method.whenValue) .put("priority", method.priority) .put("queriesDeclarations", Joiner.on("\n").join(buildQueriesDeclarationsCode(method.parameters))) .put("methodName", method.methodName) .put("queries", Joiner.on(",\n").join(buildQueriesNames(method.parameters))) .put("parameters", Joiner.on(",\n").join(buildParamFromSatisfiedBomCode(method.parameters))) .put("exceptions", method.exceptions.isEmpty() ? false : Joiner.on("|").join(method.exceptions)) .build()); } ImmutableMap<String, Object> ctx = ImmutableMap.<String, Object>builder() .put("package", moduleClass.pack) .put("machine", moduleClass.name + "FactoryMachine") .put("moduleFqcn", moduleClass.fqcn) .put("moduleType", moduleClass.name) .put("priority", moduleClass.priority) .put("engines", engines) .put("conditionalsEngines", conditionalsEngines) .build(); generateJavaClass(moduleClass.fqcn + "FactoryMachine", moduleMachineTpl, ctx, Collections.singleton(moduleClass.originatingElement)); } private void generateMachineFile(ComponentClass componentClass, ComponentClass alternativeTo, When when) throws IOException { ImmutableMap<String, Object> ctx = ImmutableMap.<String, Object>builder() .put("package", componentClass.pack) .put("machine", componentClass.name + "FactoryMachine") .put("imports", ImmutableList.of(componentClass.fqcn, alternativeTo.fqcn)) .put("componentType", componentClass.name) .put("componentInjectionType", alternativeTo.name) .put("priority", String.valueOf(componentClass.priority)) .put("whenName", when.name()) .put("whenValue", when.value()) .put("componentInjectionName", alternativeTo.injectionName.or(alternativeTo.name)) .put("conditionalFactoryMachineName", componentClass.name + alternativeTo.name + "Alternative") .put("queriesDeclarations", Joiner.on("\n").join(buildQueriesDeclarationsCode(componentClass.parameters))) .put("queries", Joiner.on(",\n").join(buildQueriesNames(componentClass.parameters))) .put("parameters", Joiner.on(",\n").join(buildParamFromSatisfiedBomCode(componentClass.parameters))) .build(); generateJavaClass(componentClass.pack + "." + componentClass.name + "FactoryMachine", conditionalMachineTpl, ctx, Collections.singleton(componentClass.originatingElement)); } private void generateMachineFile(ComponentClass componentClass, When when) throws IOException { ImmutableMap<String, Object> ctx = ImmutableMap.<String, Object>builder() .put("package", componentClass.pack) .put("machine", componentClass.name + "FactoryMachine") .put("imports", ImmutableList.of(componentClass.fqcn)) .put("componentType", componentClass.name) .put("componentInjectionType", componentClass.producedName) .put("priority", String.valueOf(componentClass.priority)) .put("whenName", when.name()) .put("whenValue", when.value()) .put("componentInjectionName", componentClass.injectionName.isPresent() ? componentClass.injectionName.get() : componentClass.name) .put("conditionalFactoryMachineName", componentClass.name + componentClass.name + "Conditional") .put("queriesDeclarations", Joiner.on("\n").join(buildQueriesDeclarationsCode(componentClass.parameters))) .put("queries", Joiner.on(",\n").join(buildQueriesNames(componentClass.parameters))) .put("parameters", Joiner.on(",\n").join(buildParamFromSatisfiedBomCode(componentClass.parameters))) .build(); generateJavaClass(componentClass.pack + "." + componentClass.name + "FactoryMachine", conditionalMachineTpl, ctx, Collections.singleton(componentClass.originatingElement)); } private void generateMachineFile(ComponentClass componentClass) throws IOException { ImmutableMap<String, String> ctx = ImmutableMap.<String, String>builder() .put("package", componentClass.pack) .put("machine", componentClass.name + "FactoryMachine") .put("componentFqcn", componentClass.fqcn) .put("componentType", componentClass.name) .put("componentProducedType", componentClass.producedName) .put("priority", String.valueOf(componentClass.priority)) .put("componentInjectionName", componentClass.injectionName.isPresent() ? componentClass.injectionName.get() : componentClass.name) .put("queriesDeclarations", Joiner.on("\n").join(buildQueriesDeclarationsCode(componentClass.parameters))) .put("queries", Joiner.on(",\n").join(buildQueriesNames(componentClass.parameters))) .put("parameters", Joiner.on(",\n").join(buildParamFromSatisfiedBomCode(componentClass.parameters))) .build(); generateJavaClass(componentClass.pack + "." + componentClass.name + "FactoryMachine", componentMachineTpl, ctx, Collections.singleton(componentClass.originatingElement)); } private List<String> buildQueriesDeclarationsCode(List<InjectableParameter> parameters) { List<String> parametersCode = Lists.newArrayList(); for (InjectableParameter parameter : parameters) { parametersCode.add(parameter.getQueryDeclarationCode()); } return parametersCode; } private List<String> buildQueriesNames(List<InjectableParameter> parameters) { List<String> parametersCode = Lists.newArrayList(); for (InjectableParameter parameter : parameters) { parametersCode.add(parameter.name); } return parametersCode; } private List<String> buildParamFromSatisfiedBomCode(List<InjectableParameter> parameters) { List<String> parametersCode = Lists.newArrayList(); for (InjectableParameter parameter : parameters) { parametersCode.add(parameter.getFromSatisfiedBomCode()); } return parametersCode; } private static class ComponentClass { final String fqcn; final List<InjectableParameter> parameters = Lists.newArrayList(); final Element originatingElement; final String pack; final String name; final String producedName; final int priority; final Optional<String> injectionName; ComponentClass(String fqcn, String pack, String name, Optional<String> injectionName, int priority, Element originatingElement) { this(fqcn, pack, name, name, injectionName, priority, originatingElement); } ComponentClass(String fqcn, String pack, String name, String producedName, Optional<String> injectionName, int priority, Element originatingElement) { this.fqcn = fqcn; this.injectionName = injectionName; this.priority = priority; this.pack = pack; this.name = name; this.producedName = producedName; this.originatingElement = originatingElement; } } private static class InjectableParameter { private static final Class[] iterableClasses = new Class[]{ Iterable.class, Collection.class, List.class, Set.class, ImmutableList.class, ImmutableSet.class}; final TypeMirror baseType; final String name; final Optional<String> injectionName; private InjectableParameter(TypeMirror baseType, String name, Optional<String> injectionName) { this.baseType = baseType; this.name = name; this.injectionName = injectionName; } public String getQueryDeclarationCode() { TypeMirror targetType = targetType(baseType); String optionalOrNotQueryQualifier = isGuavaOptionalType(baseType) || isJava8OptionalType(baseType) || isMultiType(baseType) ? "optional()" : "mandatory()"; if (injectionName.isPresent()) { return String.format("private final Factory.Query<%s> %s = Factory.Query.byName(Name.of(%s, \"%s\")).%s;", targetType, name, targetType + ".class", injectionName.get(), optionalOrNotQueryQualifier); } else { return String.format("private final Factory.Query<%s> %s = Factory.Query.byClass(%s).%s;", targetType, name, targetType + ".class", optionalOrNotQueryQualifier); } } public String getFromSatisfiedBomCode() { if (isGuavaOptionalType(baseType)) { return String.format("satisfiedBOM.getOneAsComponent(%s)", name); } else if (isJava8OptionalType(baseType)) { return String.format("java.util.Optional.ofNullable(satisfiedBOM.getOneAsComponent(%s).orNull())", name); } else if (isNamedComponentType(baseType)) { return String.format("satisfiedBOM.getOne(%s).get()", name); } else if (isMultiType(baseType)) { TypeMirror pType = parameterType(baseType).get(); String code; if (isNamedComponentType(pType)) { code = String.format("satisfiedBOM.get(%s)", name); } else { code = String.format("satisfiedBOM.getAsComponents(%s)", name); } if (baseType.toString().startsWith(Collection.class.getCanonicalName()) || baseType.toString().startsWith(List.class.getCanonicalName())) { code = String.format("com.google.common.collect.Lists.newArrayList(%s)", code); } else if (baseType.toString().startsWith(Set.class.getCanonicalName())) { code = String.format("com.google.common.collect.Sets.newLinkedHashSet(%s)", code); } else if (baseType.toString().startsWith(ImmutableList.class.getCanonicalName())) { code = String.format("com.google.common.collect.ImmutableList.copyOf(%s)", code); } else if (baseType.toString().startsWith(ImmutableSet.class.getCanonicalName())) { code = String.format("com.google.common.collect.ImmutableSet.copyOf(%s)", code); } return code; } else { return String.format("satisfiedBOM.getOne(%s).get().getComponent()", name); } } private TypeMirror targetType(TypeMirror type) { if (isGuavaOptionalType(type) || isJava8OptionalType(type) || isMultiType(type) || isNamedComponentType(type)) { Optional<TypeMirror> pType = parameterType(type); if (!pType.isPresent()){ throw new RuntimeException( "Optional | Collection | NamedComponent type for parameter " + name + " needs" + " parameterized type (generics) to be processed correctly"); } return targetType(pType.get()); } else { return type; } } private Optional<TypeMirror> parameterType(TypeMirror type) { if (type instanceof DeclaredType) { DeclaredType declaredBaseType = (DeclaredType) type; if(declaredBaseType.getTypeArguments().isEmpty()){ return Optional.absent(); } return Optional.of(declaredBaseType.getTypeArguments().get(0)); } else { return Optional.absent(); } } private boolean isGuavaOptionalType(TypeMirror type) { return type.toString().startsWith(Optional.class.getCanonicalName()); } private boolean isJava8OptionalType(TypeMirror type) { return type.toString().startsWith("java.util.Optional"); } private boolean isNamedComponentType(TypeMirror type) { return type.toString().startsWith(NamedComponent.class.getCanonicalName()); } private boolean isMultiType(TypeMirror type) { for (Class it : iterableClasses) { if (type.toString().startsWith(it.getCanonicalName())) { return true; } } return false; } } private static class ModuleClass { final String fqcn; final List<ProviderMethod> providerMethods = Lists.newArrayList(); final List<ConditionalProviderMethod> conditionalProviderMethods = Lists.newArrayList(); final Element originatingElement; final String pack; final String name; final int priority; ModuleClass(String fqcn, Element originatingElement, int priority) { this.fqcn = fqcn; this.pack = fqcn.substring(0, fqcn.lastIndexOf('.')); this.name = fqcn.substring(fqcn.lastIndexOf('.') + 1); this.originatingElement = originatingElement; this.priority = priority; } } private static class ProviderMethod { final Element originatingElement; final String type; final String name; final int priority; final Optional<String> injectionName; final List<InjectableParameter> parameters = Lists.newArrayList(); final List<String> exceptions = Lists.newArrayList(); ProviderMethod(String type, String name, int priority, Optional<String> injectionName, Element originatingElement) { this.type = type; this.name = name; this.priority = priority; this.injectionName = injectionName; this.originatingElement = originatingElement; } } private static class ConditionalProviderMethod { final Element originatingElement; final String componentType; final String componentName; final String methodName; final int priority; final String whenName; final String whenValue; final String factoryMachineNameSuffix; final List<InjectableParameter> parameters = Lists.newArrayList(); final List<String> exceptions = Lists.newArrayList(); ConditionalProviderMethod(String componentType, String componentName, String methodName, int priority, String whenName, String whenValue, String factoryMachineNameSuffix, Element originatingElement) { this.componentType = componentType; this.componentName = componentName; this.methodName = methodName; this.priority = priority; this.whenName = whenName; this.whenValue = whenValue; this.originatingElement = originatingElement; this.factoryMachineNameSuffix = factoryMachineNameSuffix; } } private class ServicesDeclaration extends ResourceDeclaration { private final Set<String> declaredServices = Sets.newHashSet(); private ServicesDeclaration(String targetFile) { super("META-INF/services/" + targetFile); } @Override protected boolean requireGeneration() { return declaredServices.size() > 0; } @Override protected void clearContent() { declaredServices.clear(); } @Override protected void writeContent(Writer writer) throws IOException { for (String declaredService : Ordering.natural().sortedCopy(declaredServices)) { writer.write(declaredService + "\n"); } } @Override protected void readContent(Reader reader) throws IOException { declaredServices.addAll(CharStreams.readLines(reader)); } void declareService(String service) { declaredServices.add(service); } } }