package de.plushnikov.intellij.plugin.processor.handler;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiReferenceList;
import com.intellij.psi.PsiSubstitutor;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import de.plushnikov.intellij.plugin.problem.ProblemBuilder;
import de.plushnikov.intellij.plugin.processor.ShouldGenerateFullCodeBlock;
import de.plushnikov.intellij.plugin.psi.LombokLightMethodBuilder;
import de.plushnikov.intellij.plugin.util.PsiAnnotationUtil;
import de.plushnikov.intellij.plugin.util.PsiElementUtil;
import de.plushnikov.intellij.plugin.util.PsiMethodUtil;
import de.plushnikov.intellij.plugin.util.PsiTypeUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
/**
* Handler for Delegate annotation processing, for fields and for methods
*/
public class DelegateHandler {
public DelegateHandler() {
}
public boolean validate(@NotNull PsiModifierListOwner psiModifierListOwner, @NotNull PsiType psiType, @NotNull PsiAnnotation psiAnnotation, @NotNull ProblemBuilder builder) {
boolean result = true;
if (psiModifierListOwner.hasModifierProperty(PsiModifier.STATIC)) {
builder.addError("@Delegate is legal only on instance fields or no-argument instance methods.");
result = false;
}
final Collection<PsiType> types = collectDelegateTypes(psiAnnotation, psiType);
result &= validateTypes(types, builder);
final Collection<PsiType> excludes = collectExcludeTypes(psiAnnotation);
result &= validateTypes(excludes, builder);
return result;
}
private Collection<PsiType> collectDelegateTypes(PsiAnnotation psiAnnotation, PsiType psiType) {
Collection<PsiType> types = PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "types", PsiType.class);
if (types.isEmpty()) {
types = Collections.singletonList(psiType);
}
return types;
}
private boolean validateTypes(Collection<PsiType> psiTypes, ProblemBuilder builder) {
boolean result = true;
for (PsiType type : psiTypes) {
if (!checkConcreteClass(type)) {
builder.addError("'@Delegate' can only use concrete class types, not wildcards, arrays, type variables, or primitives. '%s' is wrong class type",
type.getCanonicalText());
result = false;
}
}
return result;
}
private boolean checkConcreteClass(@NotNull PsiType psiType) {
if (psiType instanceof PsiClassType) {
PsiClass psiClass = ((PsiClassType) psiType).resolve();
return !(psiClass instanceof PsiTypeParameter);
}
return false;
}
private Collection<PsiType> collectExcludeTypes(PsiAnnotation psiAnnotation) {
return PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "excludes", PsiType.class);
}
public <T extends PsiMember & PsiNamedElement> void generateElements(@NotNull T psiElement, @NotNull PsiType psiElementType, @NotNull PsiAnnotation psiAnnotation, @NotNull List<? super PsiElement> target) {
final Project project = psiElement.getProject();
final PsiManager manager = psiElement.getContainingFile().getManager();
final Collection<Pair<PsiMethod, PsiSubstitutor>> includesMethods = new HashSet<Pair<PsiMethod, PsiSubstitutor>>();
final Collection<PsiType> types = collectDelegateTypes(psiAnnotation, psiElementType);
addMethodsOfTypes(types, includesMethods);
final Collection<Pair<PsiMethod, PsiSubstitutor>> excludeMethods = new HashSet<Pair<PsiMethod, PsiSubstitutor>>();
PsiClassType javaLangObjectType = PsiType.getJavaLangObject(manager, GlobalSearchScope.allScope(project));
addMethodsOfType(javaLangObjectType, excludeMethods);
final Collection<PsiType> excludes = collectExcludeTypes(psiAnnotation);
addMethodsOfTypes(excludes, excludeMethods);
final Collection<Pair<PsiMethod, PsiSubstitutor>> methodsToDelegate = findMethodsToDelegate(includesMethods, excludeMethods);
if (!methodsToDelegate.isEmpty()) {
final PsiClass psiClass = psiElement.getContainingClass();
if (null != psiClass) {
for (Pair<PsiMethod, PsiSubstitutor> pair : methodsToDelegate) {
target.add(generateDelegateMethod(psiClass, psiElement, psiAnnotation, pair.getFirst(), pair.getSecond()));
}
}
}
}
private void addMethodsOfTypes(Collection<PsiType> types, Collection<Pair<PsiMethod, PsiSubstitutor>> includesMethods) {
for (PsiType type : types) {
addMethodsOfType(type, includesMethods);
}
}
private void addMethodsOfType(PsiType psiType, Collection<Pair<PsiMethod, PsiSubstitutor>> allMethods) {
final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(psiType);
final PsiClass psiClass = resolveResult.getElement();
if (null != psiClass) {
collectAllMethods(allMethods, psiClass, resolveResult.getSubstitutor());
}
}
private void collectAllMethods(Collection<Pair<PsiMethod, PsiSubstitutor>> allMethods, @NotNull PsiClass psiStartClass, @NotNull PsiSubstitutor classSubstitutor) {
PsiClass psiClass = psiStartClass;
while (null != psiClass) {
PsiMethod[] psiMethods = psiClass.getMethods();
for (PsiMethod psiMethod : psiMethods) {
if (!psiMethod.isConstructor() && psiMethod.hasModifierProperty(PsiModifier.PUBLIC) && !psiMethod.hasModifierProperty(PsiModifier.STATIC)) {
Pair<PsiMethod, PsiSubstitutor> newMethodSubstitutorPair = new Pair<PsiMethod, PsiSubstitutor>(psiMethod, classSubstitutor);
boolean acceptMethod = true;
for (Pair<PsiMethod, PsiSubstitutor> uniquePair : allMethods) {
if (PsiElementUtil.methodMatches(newMethodSubstitutorPair, uniquePair)) {
acceptMethod = false;
break;
}
}
if (acceptMethod) {
allMethods.add(newMethodSubstitutorPair);
}
}
}
for (PsiClass interfaceClass : psiClass.getInterfaces()) {
classSubstitutor = TypeConversionUtil.getSuperClassSubstitutor(interfaceClass, psiClass, classSubstitutor);
collectAllMethods(allMethods, interfaceClass, classSubstitutor);
}
psiClass = psiClass.getSuperClass();
}
}
private Collection<Pair<PsiMethod, PsiSubstitutor>> findMethodsToDelegate(Collection<Pair<PsiMethod, PsiSubstitutor>> includesMethods, Collection<Pair<PsiMethod, PsiSubstitutor>> excludeMethods) {
final Collection<Pair<PsiMethod, PsiSubstitutor>> result = new ArrayList<Pair<PsiMethod, PsiSubstitutor>>();
for (Pair<PsiMethod, PsiSubstitutor> includesMethodPair : includesMethods) {
boolean acceptMethod = true;
for (Pair<PsiMethod, PsiSubstitutor> excludeMethodPair : excludeMethods) {
if (PsiElementUtil.methodMatches(includesMethodPair, excludeMethodPair)) {
acceptMethod = false;
break;
}
}
if (acceptMethod) {
result.add(includesMethodPair);
}
}
return result;
}
@NotNull
private <T extends PsiModifierListOwner & PsiNamedElement> PsiMethod generateDelegateMethod(@NotNull PsiClass psiClass, @NotNull T psiElement, @NotNull PsiAnnotation psiAnnotation, @NotNull PsiMethod psiMethod, @NotNull PsiSubstitutor psiSubstitutor) {
final PsiType returnType = psiSubstitutor.substitute(psiMethod.getReturnType());
final LombokLightMethodBuilder methodBuilder = new LombokLightMethodBuilder(psiClass.getManager(), psiMethod.getName())
.withModifier(PsiModifier.PUBLIC)
.withMethodReturnType(returnType)
.withContainingClass(psiClass)
//Have to go to original method, or some refactoring action will not work (like extract parameter oder change signature)
.withNavigationElement(psiMethod);
for (PsiTypeParameter typeParameter : psiMethod.getTypeParameters()) {
methodBuilder.withTypeParameter(typeParameter);
}
final PsiReferenceList throwsList = psiMethod.getThrowsList();
for (PsiClassType psiClassType : throwsList.getReferencedTypes()) {
methodBuilder.withException(psiClassType);
}
final PsiParameterList parameterList = psiMethod.getParameterList();
final PsiParameter[] psiParameters = parameterList.getParameters();
for (int parameterIndex = 0; parameterIndex < psiParameters.length; parameterIndex++) {
final PsiParameter psiParameter = psiParameters[parameterIndex];
final PsiType psiParameterType = psiSubstitutor.substitute(psiParameter.getType());
final String generatedParameterName = StringUtils.defaultIfEmpty(psiParameter.getName(), "p" + parameterIndex);
methodBuilder.withParameter(generatedParameterName, psiParameterType);
}
methodBuilder.withBody(createCodeBlock(psiClass, psiElement, psiMethod, returnType, psiParameters));
return methodBuilder;
}
@NotNull
private <T extends PsiModifierListOwner & PsiNamedElement> PsiCodeBlock createCodeBlock(@NotNull PsiClass psiClass, @NotNull T psiElement, @NotNull PsiMethod psiMethod, @NotNull PsiType returnType, @NotNull PsiParameter[] psiParameters) {
final String blockText;
if (isShouldGenerateFullBodyBlock()) {
final StringBuilder paramString = new StringBuilder();
for (int parameterIndex = 0; parameterIndex < psiParameters.length; parameterIndex++) {
final PsiParameter psiParameter = psiParameters[parameterIndex];
final String generatedParameterName = StringUtils.defaultIfEmpty(psiParameter.getName(), "p" + parameterIndex);
paramString.append(generatedParameterName).append(',');
}
if (paramString.length() > 0) {
paramString.deleteCharAt(paramString.length() - 1);
}
final boolean isMethodCall = psiElement instanceof PsiMethod;
blockText = String.format("%sthis.%s%s.%s(%s);",
PsiType.VOID.equals(returnType) ? "" : "return ",
psiElement.getName(),
isMethodCall ? "()" : "",
psiMethod.getName(),
paramString.toString());
} else {
blockText = "return " + PsiTypeUtil.getReturnValueOfType(returnType) + ";";
}
return PsiMethodUtil.createCodeBlockFromText(blockText, psiClass);
}
private boolean isShouldGenerateFullBodyBlock() {
return ShouldGenerateFullCodeBlock.getInstance().isStateActive();
}
}