package de.plushnikov.intellij.plugin.processor.clazz;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.util.PsiTreeUtil;
import de.plushnikov.intellij.plugin.problem.LombokProblem;
import de.plushnikov.intellij.plugin.problem.ProblemBuilder;
import de.plushnikov.intellij.plugin.problem.ProblemEmptyBuilder;
import de.plushnikov.intellij.plugin.problem.ProblemNewBuilder;
import de.plushnikov.intellij.plugin.processor.AbstractProcessor;
import de.plushnikov.intellij.plugin.quickfix.PsiQuickFixFactory;
import de.plushnikov.intellij.plugin.thirdparty.LombokUtils;
import de.plushnikov.intellij.plugin.util.LombokProcessorUtil;
import de.plushnikov.intellij.plugin.util.PsiAnnotationSearchUtil;
import de.plushnikov.intellij.plugin.util.PsiAnnotationUtil;
import de.plushnikov.intellij.plugin.util.PsiClassUtil;
import de.plushnikov.intellij.plugin.util.PsiMethodUtil;
import lombok.Data;
import lombok.Getter;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
/**
* Base lombok processor class for class annotations
*
* @author Plushnikov Michail
*/
public abstract class AbstractClassProcessor extends AbstractProcessor implements ClassProcessor {
protected AbstractClassProcessor(@NotNull Class<? extends PsiElement> supportedClass,
@NotNull Class<? extends Annotation> supportedAnnotationClass,
@NotNull Class<? extends Annotation>... equivalentAnnotationClasses) {
super(supportedClass, supportedAnnotationClass, equivalentAnnotationClasses);
}
@NotNull
@Override
public List<? super PsiElement> process(@NotNull PsiClass psiClass) {
List<? super PsiElement> result = Collections.emptyList();
PsiAnnotation psiAnnotation = PsiAnnotationSearchUtil.findAnnotation(psiClass, getSupportedAnnotationClasses());
if (null != psiAnnotation) {
if (validate(psiAnnotation, psiClass, ProblemEmptyBuilder.getInstance())) {
result = new ArrayList<PsiElement>();
generatePsiElements(psiClass, psiAnnotation, result);
}
}
return result;
}
@NotNull
public Collection<PsiAnnotation> collectProcessedAnnotations(@NotNull PsiClass psiClass) {
Collection<PsiAnnotation> result = new ArrayList<PsiAnnotation>();
PsiAnnotation psiAnnotation = PsiAnnotationSearchUtil.findAnnotation(psiClass, getSupportedAnnotationClasses());
if (null != psiAnnotation) {
result.add(psiAnnotation);
}
return result;
}
@NotNull
@Override
public Collection<LombokProblem> verifyAnnotation(@NotNull PsiAnnotation psiAnnotation) {
Collection<LombokProblem> result = Collections.emptyList();
// check first for fields, methods and filter it out, because PsiClass is parent of all annotations and will match other parents too
PsiElement psiElement = PsiTreeUtil.getParentOfType(psiAnnotation, PsiField.class, PsiMethod.class, PsiClass.class);
if (psiElement instanceof PsiClass) {
ProblemNewBuilder problemNewBuilder = new ProblemNewBuilder();
validate(psiAnnotation, (PsiClass) psiElement, problemNewBuilder);
result = problemNewBuilder.getProblems();
}
return result;
}
protected abstract boolean validate(@NotNull PsiAnnotation psiAnnotation, @NotNull PsiClass psiClass, @NotNull ProblemBuilder builder);
protected abstract void generatePsiElements(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation, @NotNull List<? super PsiElement> target);
protected void validateOfParam(PsiClass psiClass, ProblemBuilder builder, PsiAnnotation psiAnnotation, Collection<String> ofProperty) {
for (String fieldName : ofProperty) {
if (!StringUtil.isEmptyOrSpaces(fieldName)) {
PsiField fieldByName = psiClass.findFieldByName(fieldName, false);
if (null == fieldByName) {
final String newPropertyValue = calcNewPropertyValue(ofProperty, fieldName);
builder.addWarning(String.format("The field '%s' does not exist", fieldName),
PsiQuickFixFactory.createChangeAnnotationParameterFix(psiAnnotation, "of", newPropertyValue));
}
}
}
}
protected void validateExcludeParam(PsiClass psiClass, ProblemBuilder builder, PsiAnnotation psiAnnotation, Collection<String> excludeProperty) {
for (String fieldName : excludeProperty) {
if (!StringUtil.isEmptyOrSpaces(fieldName)) {
PsiField fieldByName = psiClass.findFieldByName(fieldName, false);
if (null == fieldByName) {
final String newPropertyValue = calcNewPropertyValue(excludeProperty, fieldName);
builder.addWarning(String.format("The field '%s' does not exist", fieldName),
PsiQuickFixFactory.createChangeAnnotationParameterFix(psiAnnotation, "exclude", newPropertyValue));
} else {
if (fieldName.startsWith(LombokUtils.LOMBOK_INTERN_FIELD_MARKER) || fieldByName.hasModifierProperty(PsiModifier.STATIC)) {
final String newPropertyValue = calcNewPropertyValue(excludeProperty, fieldName);
builder.addWarning(String.format("The field '%s' would have been excluded anyway", fieldName),
PsiQuickFixFactory.createChangeAnnotationParameterFix(psiAnnotation, "exclude", newPropertyValue));
}
}
}
}
}
private String calcNewPropertyValue(Collection<String> allProperties, String fieldName) {
String result = null;
final Collection<String> restProperties = new ArrayList<String>(allProperties);
restProperties.remove(fieldName);
if (!restProperties.isEmpty()) {
final StringBuilder builder = new StringBuilder();
builder.append('{');
for (final String property : restProperties) {
builder.append('"').append(property).append('"').append(',');
}
builder.deleteCharAt(builder.length() - 1);
builder.append('}');
result = builder.toString();
}
return result;
}
protected Collection<PsiField> filterFields(@NotNull PsiClass psiClass, @NotNull PsiAnnotation psiAnnotation, boolean filterTransient) {
final boolean explicitOf = PsiAnnotationUtil.hasDeclaredProperty(psiAnnotation, "of");
final boolean explicitExclude = PsiAnnotationUtil.hasDeclaredProperty(psiAnnotation, "exclude");
//Having both exclude and of generates a warning; the exclude parameter will be ignored in that case.
final Collection<String> ofProperty;
final Collection<String> excludeProperty;
if (!explicitOf) {
excludeProperty = makeSet(PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "exclude", String.class));
ofProperty = Collections.emptyList();
} else {
ofProperty = makeSet(PsiAnnotationUtil.getAnnotationValues(psiAnnotation, "of", String.class));
excludeProperty = Collections.emptyList();
}
final Collection<PsiField> psiFields = PsiClassUtil.collectClassFieldsIntern(psiClass);
final Collection<PsiField> result = new ArrayList<PsiField>(psiFields.size());
for (PsiField classField : psiFields) {
if (classField.hasModifierProperty(PsiModifier.STATIC) || (filterTransient && classField.hasModifierProperty(PsiModifier.TRANSIENT))) {
continue;
}
final String fieldName = classField.getName();
if (null == fieldName) {
continue;
}
if (explicitExclude && excludeProperty.contains(fieldName)) {
continue;
}
if (explicitOf && !ofProperty.contains(fieldName)) {
continue;
}
if (fieldName.startsWith(LombokUtils.LOMBOK_INTERN_FIELD_MARKER) && !ofProperty.contains(fieldName)) {
continue;
}
result.add(classField);
}
return result;
}
protected String buildAttributeNameString(boolean doNotUseGetters, @NotNull PsiField classField, @NotNull PsiClass psiClass) {
final String fieldName = classField.getName();
if (doNotUseGetters) {
return fieldName;
} else {
final String getterName = getGetterName(classField);
final boolean hasGetter;
if (PsiAnnotationSearchUtil.isAnnotatedWith(psiClass, Data.class, Value.class, lombok.experimental.Value.class, Getter.class)) {
final PsiAnnotation getterLombokAnnotation = PsiAnnotationSearchUtil.findAnnotation(psiClass, Getter.class);
hasGetter = null == getterLombokAnnotation || null != LombokProcessorUtil.getMethodModifier(getterLombokAnnotation);
} else {
hasGetter = PsiMethodUtil.hasMethodByName(PsiClassUtil.collectClassMethodsIntern(psiClass), getterName);
}
return hasGetter ? getterName + "()" : fieldName;
}
}
private Collection<String> makeSet(@NotNull Collection<String> exclude) {
if (exclude.isEmpty()) {
return Collections.emptySet();
}
return new HashSet<String>(exclude);
}
}