package com.maxifier.guice.jpa;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInspection.*;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiElementFilter;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.maxifier.guice.jpa.GuiceJPAInspection.*;
/**
* Created by: Aleksey Didik
* Date: 3/16/11
* Time: 4:30 PM
* <p/>
* Copyright (c) 1999-2011 Maxifier Ltd. All Rights Reserved.
* Code proprietary and confidential.
* Use is subject to license terms.
*
* @author Aleksey Didik
*/
public class EntityManagerInspection extends AbstractDBInspection {
private static final String EM_TYPE = "EntityManager";
private static final String EM_FULL_TYPE = "javax.persistence.EntityManager";
private static final String TRANSACTION_ATTRIBUTE = "transaction";
private static final String DB_FIX_ACTIONS_FAMILY = "@DB fix actions";
@NotNull
@Override
public String getID() {
return "EntityManagerInspection";
}
@Override
public String getAlternativeID() {
return "EntityManagerInspection";
}
@Nls
@NotNull
@Override
public String getGroupDisplayName() {
return INSPECTIONS_GROUP_NAME;
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return "EntityManager usage";
}
@NotNull
@Override
public String getShortName() {
return "entity-manager";
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@NotNull
@Override
public HighlightDisplayLevel getDefaultLevel() {
return HighlightDisplayLevel.ERROR;
}
@Override
public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) {
List<ProblemDescriptor> problemDescriptors = new ArrayList<ProblemDescriptor>();
PsiElement[] classes = PsiTreeUtil.collectElements(file, new PsiClassesFilter());
for (PsiElement psiClass : classes) {
checkClass((PsiClass) psiClass, manager, problemDescriptors, isOnTheFly);
}
return problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()]);
}
private void checkClass(PsiClass psiClass, InspectionManager inspectionManager, List<ProblemDescriptor> problemDescriptors, boolean onTheFly) {
PsiIdentifier nameIdentifier = psiClass.getNameIdentifier();
if (nameIdentifier == null) {
return;
}
final PsiField emField = checkEMField(psiClass, inspectionManager, problemDescriptors, onTheFly);
//check is emField injected
boolean isEmInjected = isEmInjected(psiClass, emField);
if (emField != null && isEmInjected) {
checkUsageWithoutDB(psiClass, inspectionManager, problemDescriptors, emField, onTheFly);
checkNecessityOfTransaction(psiClass, emField, inspectionManager, problemDescriptors, onTheFly);
checkRawTransactionUsage(psiClass, emField, inspectionManager, problemDescriptors, onTheFly);
}
//check annotated but without usages
checkAnnotatedWithoutUsages(psiClass, inspectionManager, problemDescriptors, emField, onTheFly);
}
private boolean isEmInjected(PsiClass psiClass, PsiField emField) {
// PsiMethod[] constructors = psiClass.getConstructors();
// for (PsiMethod constructor : constructors) {
// PsiElement[] fieldUsages = getFieldUsages(emField, constructor);
//
// }
return true;
}
private void checkRawTransactionUsage(PsiClass psiClass,
PsiField emField,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
boolean onTheFly) {
for (PsiMethod psiMethod : psiClass.getMethods()) {
PsiAnnotation dbAnnotation = getAnnotation(psiMethod, DB_NAME);
if (dbAnnotation != null) {
for (PsiElement emFieldUsage : getFieldUsages(emField, psiMethod)) {
if (checkMethodCall(emFieldUsage, "getTransaction", "joinTransaction")) {
problemDescriptors.add(inspectionManager.createProblemDescriptor(
emFieldUsage,
"Usage of getTransaction() or joinTransaction() methods is disallowed for @DB annotated methods," +
" use @DB(transaction=REQUIRED) instead.",
onTheFly,
LocalQuickFix.EMPTY_ARRAY,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING
));
}
}
}
}
}
private void checkAnnotatedWithoutUsages(PsiClass psiClass,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
PsiField emField,
boolean onTheFly) {
for (PsiMethod psiMethod : psiClass.getMethods()) {
PsiAnnotation dbAnnotation = getAnnotation(psiMethod, DB_NAME);
if (dbAnnotation != null) {
if (emField == null || getFieldUsages(emField, psiMethod).length == 0) {
problemDescriptors.add(inspectionManager.createProblemDescriptor(
dbAnnotation,
"@DB annotation is not required for this method",
onTheFly,
new LocalQuickFix[]{new DeleteAnnotationFixAction(dbAnnotation)},
ProblemHighlightType.LIKE_UNUSED_SYMBOL
));
}
}
}
}
private void checkNecessityOfTransaction(PsiClass psiClass,
final PsiField emField,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
boolean onTheFly) {
for (PsiMethod psiMethod : psiClass.getMethods()) {
PsiAnnotation dbAnnotation = getAnnotation(psiMethod, DB_NAME);
if (dbAnnotation != null) {
PsiAnnotationMemberValue transactionAttribute = dbAnnotation.findAttributeValue(TRANSACTION_ATTRIBUTE);
@SuppressWarnings({"ConstantConditions"})
boolean isTransaction = !transactionAttribute.getText().contains("NOT_REQUIRED");
PsiElement[] methodCalls = getFieldUsages(emField, psiMethod);
boolean trRequired = false;
for (PsiElement methodCall : methodCalls) {
if (checkMethodCall(methodCall, "persist", "remove", "merge")) {
trRequired = true;
if (!isTransaction)
problemDescriptors.add(inspectionManager.createProblemDescriptor(
methodCall.getParent(),
"Transaction support is required for this usage of EntityManager",
onTheFly,
new LocalQuickFix[]{new AddTransactionRequiredFixAction(dbAnnotation)},
ProblemHighlightType.GENERIC_ERROR_OR_WARNING
));
}
}
if (!trRequired && isTransaction) {
problemDescriptors.add(inspectionManager.createProblemDescriptor(
dbAnnotation.getParameterList(),
"Transaction support is not required for this method",
true,
new LocalQuickFix[]{new DeleteTransactionRequiredFixAction(dbAnnotation)},
ProblemHighlightType.LIKE_UNUSED_SYMBOL
));
}
}
}
}
private void checkUsageWithoutDB(PsiClass psiClass,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
final PsiField emField,
boolean onTheFly) {
for (PsiMethod psiMethod : psiClass.getMethods()) {
if (getAnnotation(psiMethod, DB_NAME) == null && psiMethod.getBody() != null) {
PsiElement[] emFieldRefs = getFieldUsages(emField, psiMethod);
if (emFieldRefs.length != 0) {
//noinspection ConstantConditions
problemDescriptors.add(inspectionManager.createProblemDescriptor(
psiMethod.getNameIdentifier(),
"Method which use EntityManager should be annotated with @DB annotation",
onTheFly,
new LocalQuickFix[]{new AddAnnotationFixAction(psiMethod)},
ProblemHighlightType.GENERIC_ERROR_OR_WARNING
));
}
}
}
}
private void checkFieldModifiers(PsiField psiField,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
boolean onTheFly) {
PsiModifierList modifierList = psiField.getModifierList();
if (modifierList != null) {
if (modifierList.hasModifierProperty(STATIC_MODIFIER)) {
PsiElement staticModifier = getModifier(modifierList, STATIC_MODIFIER);
problemDescriptors.add(
inspectionManager.createProblemDescriptor(
staticModifier,
"EntityManager field should not be static",
new DeleteModifierFixAction(staticModifier),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
onTheFly
)
);
}
if (!modifierList.hasModifierProperty(FINAL_MODIFIER)) {
problemDescriptors.add(
inspectionManager.createProblemDescriptor(
psiField,
"EntityManager field should be final",
new AddModifierFixAction(modifierList, FINAL_MODIFIER),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
true
)
);
}
}
}
private PsiField checkEMField(PsiClass psiClass,
InspectionManager inspectionManager,
List<ProblemDescriptor> problemDescriptors,
boolean onTheFly) {
for (PsiField psiField : psiClass.getAllFields()) {
String typeName = psiField.getType().getCanonicalText();
if (typeName.equals(EM_TYPE) || typeName.equals(EM_FULL_TYPE)) {
//noinspection ConstantConditions
if (psiField.getContainingClass().equals(psiClass)) {
checkFieldModifiers(psiField, inspectionManager, problemDescriptors, onTheFly);
}
return psiField;
}
}
// //add error that we need to have EM field
// //noinspection ConstantConditions
// problemDescriptors.add(
// inspectionManager.createProblemDescriptor(
// psiClass.getNameIdentifier(),
// "Class with @DB annotated methods should have an EntityManager field",
// true,
// LocalQuickFix.EMPTY_ARRAY,
// ProblemHighlightType.GENERIC_ERROR_OR_WARNING
//
// )
// );
return null;
}
private PsiElement[] getFieldUsages(final PsiField emField, PsiMethod psiMethod) {
return PsiTreeUtil.collectElements(psiMethod.getBody(), new PsiRefToFilter(emField));
}
private boolean checkMethodCall(PsiElement fieldRef, String... methods) {
String methodCall = fieldRef.getText();
for (String method : methods) {
if (methodCall.contains(method + "(")) {
return true;
}
}
return false;
}
private static class AddAnnotationFixAction extends IntentionAndQuickFixAction {
private final PsiMethod psiMethod;
AddAnnotationFixAction(PsiMethod psiMethod) {
this.psiMethod = psiMethod;
}
@NotNull
@Override
public String getName() {
return "Add @DB annotation to method";
}
@NotNull
@Override
public String getFamilyName() {
return DB_FIX_ACTIONS_FAMILY;
}
@Override
public void applyFix(Project project, PsiFile psiFile, @Nullable Editor editor) {
psiMethod.getModifierList().addAnnotation("DB");
}
}
private static class AddTransactionRequiredFixAction extends IntentionAndQuickFixAction {
private final PsiAnnotation psiAnnotation;
AddTransactionRequiredFixAction(PsiAnnotation psiAnnotation) {
this.psiAnnotation = psiAnnotation;
}
@NotNull
@Override
public String getName() {
return "Add transaction required attribute to @DB";
}
@NotNull
@Override
public String getFamilyName() {
return DB_FIX_ACTIONS_FAMILY;
}
@Override
public void applyFix(Project project, PsiFile psiFile, @Nullable Editor editor) {
PsiAnnotationOwner modList = psiAnnotation.getOwner();
psiAnnotation.delete();
PsiElement[] psiElements = PsiTreeUtil.collectElements(psiFile, new RequiredStaticImportFilter());
if (psiElements.length == 0) {
modList.addAnnotation("DB(transaction = DB.Transaction.REQUIRED)");
} else {
modList.addAnnotation("DB(transaction = REQUIRED)");
}
}
private static class RequiredStaticImportFilter implements PsiElementFilter {
@Override
public boolean isAccepted(PsiElement element) {
if (element instanceof PsiImportStaticStatement) {
PsiImportStaticStatement importStatement = (PsiImportStaticStatement) element;
String referenceName = importStatement.getReferenceName();
if (referenceName != null && referenceName.equals("REQUIRED")) {
return true;
}
}
return false;
}
}
}
private static class DeleteTransactionRequiredFixAction extends IntentionAndQuickFixAction {
private final PsiAnnotation psiAnnotation;
DeleteTransactionRequiredFixAction(PsiAnnotation psiAnnotation) {
this.psiAnnotation = psiAnnotation;
}
@NotNull
@Override
public String getName() {
return "Delete transaction required attribute to @DB";
}
@NotNull
@Override
public String getFamilyName() {
return DB_FIX_ACTIONS_FAMILY;
}
@Override
public void applyFix(Project project, PsiFile psiFile, @Nullable Editor editor) {
PsiAnnotationOwner modList = psiAnnotation.getOwner();
psiAnnotation.delete();
modList.addAnnotation("DB");
}
}
private static class DeleteAnnotationFixAction extends IntentionAndQuickFixAction {
private final PsiAnnotation psiAnnotation;
DeleteAnnotationFixAction(PsiAnnotation psiAnnotation) {
this.psiAnnotation = psiAnnotation;
}
@NotNull
@Override
public String getName() {
return "Delete not required annotation";
}
@NotNull
@Override
public String getFamilyName() {
return DB_FIX_ACTIONS_FAMILY;
}
@Override
public void applyFix(Project project, PsiFile psiFile, @Nullable Editor editor) {
psiAnnotation.delete();
}
}
private static class PsiClassesFilter implements PsiElementFilter {
@Override
public boolean isAccepted(PsiElement psiElement) {
if (!(psiElement instanceof PsiClass)) {
return false;
}
PsiClass psiClass = (PsiClass) psiElement;
return !(psiClass.isEnum() || psiClass.isInterface());
}
}
private static class PsiRefToFilter implements PsiElementFilter {
private final PsiField emField;
public PsiRefToFilter(PsiField emField) {
this.emField = emField;
}
@Override
public boolean isAccepted(PsiElement element) {
if (element instanceof PsiMethodCallExpression) {
PsiMethodCallExpression methodCall = (PsiMethodCallExpression) element;
PsiExpression qualifierExpression = methodCall.getMethodExpression().getQualifierExpression();
if (qualifierExpression != null) {
PsiReference reference = qualifierExpression.getReference();
if (reference != null && reference.isReferenceTo(emField)) {
return true;
}
}
PsiExpressionList argumentList = methodCall.getArgumentList();
for (PsiExpression psiExpression : argumentList.getExpressions()) {
PsiReference reference = psiExpression.getReference();
if ((reference != null) && reference.isReferenceTo(emField)) {
return true;
}
}
}
return false;
}
}
}