package io.norberg.automatter.processor;
import com.google.common.base.Joiner;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
/**
* Holds information about an automatter annotated interface and the entities it generates.
*/
class Descriptor {
private final DeclaredType valueType;
private final TypeElement valueTypeElement;
private final List<? extends TypeMirror> valueTypeArguments;
private final String packageName;
private final String valueTypeName;
private final String builderName;
private final List<ExecutableElement> fields;
private final Map<ExecutableElement, TypeMirror> fieldTypes;
private final boolean isPublic;
private final String concreteBuilderName;
private final String fullyQualifiedBuilderName;
private boolean isGeneric;
private boolean toBuilder;
public Descriptor(final Element element, final Elements elements, final Types types)
throws AutoMatterProcessorException {
if (!element.getKind().isInterface()) {
throw new AutoMatterProcessorException("@AutoMatter target must be an interface", element);
}
this.valueType = (DeclaredType) element.asType();
this.valueTypeElement = (TypeElement) element;
this.valueTypeArguments = valueType.getTypeArguments();
this.valueTypeName = nestedName(valueTypeElement, elements);
this.isGeneric = !valueTypeArguments.isEmpty();
this.packageName = elements.getPackageOf(element).getQualifiedName().toString();
this.builderName = element.getSimpleName().toString() + "Builder";
final String typeParameterization = isGeneric ? "<" + Joiner.on(',').join(valueTypeArguments) + ">" : "";
this.concreteBuilderName = builderName + typeParameterization;
this.fullyQualifiedBuilderName = fullyQualifedName(packageName, concreteBuilderName);
this.fields = new ArrayList<>();
this.fieldTypes = new HashMap<>();
this.isPublic = element.getModifiers().contains(PUBLIC);
enumerateFields(types);
}
private static String nestedName(final TypeElement element, final Elements elements) {
final String qualifiedName = element.getQualifiedName().toString();
final String packageName = elements.getPackageOf(element).getQualifiedName().toString();
if (packageName.isEmpty()) {
return qualifiedName;
}
return qualifiedName.substring(packageName.length() + 1);
}
private void enumerateFields(final Types types)
throws AutoMatterProcessorException {
final List<ExecutableElement> methods = methods(valueTypeElement);
for (final Element member : methods) {
if (member.getKind() != ElementKind.METHOD ||
isStaticOrDefault(member)) {
continue;
}
final ExecutableElement method = (ExecutableElement) member;
if (member.getSimpleName().toString().equals("builder")) {
final TypeMirror returnType = (method).getReturnType();
// TODO: javac does not seem to want to provide the name of the return type if it is not yet present and generic
if (!isGeneric &&
!returnType.toString().equals(concreteBuilderName) &&
!returnType.toString().equals(fullyQualifiedBuilderName)) {
throw new AutoMatterProcessorException(
"builder() return type must be " + concreteBuilderName, valueTypeElement);
}
toBuilder = true;
continue;
}
fields.add(method);
final ExecutableType methodType = (ExecutableType) types.asMemberOf(valueType, member);
final TypeMirror fieldType = methodType.getReturnType();
fieldTypes.put(method, fieldType);
}
}
private List<ExecutableElement> methods(final TypeElement element) {
final Map<String, ExecutableElement> methodMap = new LinkedHashMap<>();
enumerateMethods(element, methodMap);
return new ArrayList<>(methodMap.values());
}
private void enumerateMethods(final TypeElement element, final Map<String, ExecutableElement> methods) {
for (final TypeMirror interfaceType : element.getInterfaces()) {
final TypeElement interfaceElement = (TypeElement) ((DeclaredType) interfaceType).asElement();
enumerateMethods(interfaceElement, methods);
}
for (final Element member : element.getEnclosedElements()) {
if (member.getKind() != ElementKind.METHOD) {
continue;
}
methods.put(member.getSimpleName().toString(), (ExecutableElement) member);
}
}
private static boolean isStaticOrDefault(final Element member) {
final Set<Modifier> modifiers = member.getModifiers();
for (final Modifier modifier : modifiers) {
if (modifier == STATIC) {
return true;
}
// String comparison to avoid requiring JDK 8
if (modifier.name().equals("DEFAULT")) {
return true;
}
}
return false;
}
public String packageName() {
return this.packageName;
}
public String builderName() {
return this.builderName;
}
public String valueTypeName() {
return this.valueTypeName;
}
public boolean isPublic() {
return this.isPublic;
}
public boolean isGeneric() {
return this.isGeneric;
}
public List<ExecutableElement> fields() {
return fields;
}
public Map<ExecutableElement, TypeMirror> fieldTypes() {
return fieldTypes;
}
public boolean hasToBuilder() {
return this.toBuilder;
}
private static String fullyQualifedName(final String packageName, final String simpleName) {
return packageName.isEmpty() ? simpleName : packageName + "." + simpleName;
}
public List<TypeVariableName> typeVariables() {
final List<TypeVariableName> variables = new ArrayList<>();
if (isGeneric) {
for (final TypeMirror argument : valueTypeArguments) {
final TypeVariable typeVariable = (TypeVariable) argument;
variables.add(TypeVariableName.get(typeVariable));
}
}
return variables;
}
public TypeName[] typeArguments() {
final List<TypeVariableName> variables = typeVariables();
return variables.toArray(new TypeName[variables.size()]);
}
}