diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceConstants.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceConstants.java index 2799f6597..edf32ed52 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceConstants.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceConstants.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020, 2022 IBM Corporation, Ankush Sharma and others. + * Copyright (c) 2020, 2025 IBM Corporation, Ankush Sharma and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -36,6 +36,11 @@ public class PersistenceConstants { /* MapKey Codes */ public static final String DIAGNOSTIC_CODE_INVALID_ANNOTATION = "RemoveMapKeyorMapKeyClass"; public static final String DIAGNOSTIC_CODE_MISSING_ATTRIBUTES = "SupplyAttributesToAnnotations"; + public static final String DIAGNOSTIC_CODE_INVALID_ACCESS_SPECIFIER = "InvalidMethodAccessSpecifier"; + public static final String DIAGNOSTIC_CODE_INVALID_METHOD_NAME = "InvalidMethodName"; + public static final String DIAGNOSTIC_CODE_FIELD_NOT_EXIST = "InvalidMapKeyAnnotationsFieldNotFound"; + public static final String DIAGNOSTIC_CODE_INVALID_RETURN_TYPE = "InvalidReturnTypeOfMethod"; + public static final String DIAGNOSTIC_CODE_INVALID_TYPE = "InvalidTypeOfField"; public final static String[] SET_OF_PERSISTENCE_ANNOTATIONS = {MAPKEY, MAPKEYCLASS, MAPKEYJOINCOLUMN}; } diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceMapKeyDiagnosticsCollector.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceMapKeyDiagnosticsCollector.java index 3e0e7de8b..6c1d12bf0 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceMapKeyDiagnosticsCollector.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceMapKeyDiagnosticsCollector.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020, 2024 IBM Corporation, Ankush Sharma and others. + * Copyright (c) 2020, 2025 IBM Corporation, Ankush Sharma and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -14,11 +14,14 @@ package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.persistence; import com.intellij.psi.*; +import com.intellij.psi.util.InheritanceUtil; import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector; import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages; +import org.apache.commons.lang3.StringUtils; import org.eclipse.lsp4j.Diagnostic; import org.eclipse.lsp4j.DiagnosticSeverity; +import java.beans.Introspector; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -61,6 +64,7 @@ private void collectDiagnostics(PsiJavaFile unit, List diagnostics, List mapKeyJoinCols = new ArrayList(); boolean hasMapKeyAnnotation = false; boolean hasMapKeyClassAnnotation = false; + boolean hasTypeDiagnostics = false; PsiAnnotation[] allAnnotations = fieldOrProperty.getAnnotations(); for (PsiAnnotation annotation : allAnnotations) { String matchedAnnotation = getMatchedJavaElementName(type, annotation.getQualifiedName(), @@ -75,7 +79,15 @@ else if (PersistenceConstants.MAPKEYJOINCOLUMN.equals(matchedAnnotation)) { } } } - if (hasMapKeyAnnotation && hasMapKeyClassAnnotation) { + if (hasMapKeyAnnotation) { + hasTypeDiagnostics = collectTypeDiagnostics(fieldOrProperty, "@MapKey", unit, diagnostics); + collectAccessorDiagnostics(fieldOrProperty, type, unit, diagnostics); + } + if (hasMapKeyClassAnnotation) { + hasTypeDiagnostics = collectTypeDiagnostics(fieldOrProperty, "@MapKeyClass", unit, diagnostics); + collectAccessorDiagnostics(fieldOrProperty, type, unit, diagnostics); + } + if (!hasTypeDiagnostics && (hasMapKeyAnnotation && hasMapKeyClassAnnotation)) { //A single field or property cannot be annotated with both @MapKey and @MapKeyClass //Specification References: //https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/mapkey @@ -92,6 +104,36 @@ else if (PersistenceConstants.MAPKEYJOINCOLUMN.equals(matchedAnnotation)) { } } + private boolean collectTypeDiagnostics(PsiJvmModifiersOwner fieldOrProperty, String attribute, PsiJavaFile unit, + List diagnostics) { + final String MAP_INTERFACE_FQDN = "java.util.Map"; + boolean hasTypeDiagnostics = false; + PsiType fieldOrPropertyType = null; + boolean isMapOrSubtype = false; + String messageKey = null; + String code = null; + + if (fieldOrProperty instanceof PsiMethod method) { + fieldOrPropertyType = method.getReturnType(); + messageKey = "MapKeyAnnotationsReturnTypeOfMethod"; + code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_RETURN_TYPE; + } else if (fieldOrProperty instanceof PsiField field) { + fieldOrPropertyType = field.getType(); + messageKey = "MapKeyAnnotationsTypeOfField"; + code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_TYPE; + } + if (fieldOrPropertyType instanceof PsiClassType classType) { + PsiClass psiClass = classType.resolve(); + isMapOrSubtype = InheritanceUtil.isInheritor(psiClass, MAP_INTERFACE_FQDN); + } + if (!isMapOrSubtype) { + hasTypeDiagnostics = true; + diagnostics.add(createDiagnostic(fieldOrProperty, unit, Messages.getMessage(messageKey, attribute), + code, null, DiagnosticSeverity.Error)); + } + return hasTypeDiagnostics; + } + private void validateMapKeyJoinColumnAnnotations(List annotations, PsiElement element, PsiJavaFile unit, List diagnostics) { String message = (element instanceof PsiMethod) ? @@ -110,4 +152,42 @@ private void validateMapKeyJoinColumnAnnotations(List annotations } }); } + + private void collectAccessorDiagnostics(PsiJvmModifiersOwner fieldOrProperty, PsiClass type, PsiJavaFile unit, + List diagnostics) { + String messageKey = null; + String code = null; + if (fieldOrProperty instanceof PsiMethod method) { + String methodName = method.getName(); + boolean isPublic = method.getModifierList().hasModifierProperty(PsiModifier.PUBLIC); + boolean isStartsWithGet = methodName.startsWith("get"); + boolean isPropertyExist = false; + + if (isStartsWithGet) { + isPropertyExist = hasField(method, type); + } + if (!isPublic) { + messageKey = "MapKeyAnnotationsInvalidMethodAccessSpecifier"; + code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_ACCESS_SPECIFIER; + } else if (!isStartsWithGet) { + messageKey = "MapKeyAnnotationsOnInvalidMethod"; + code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_METHOD_NAME; + } else if (!isPropertyExist) { + messageKey = "MapKeyAnnotationsFieldNotFound"; + code = PersistenceConstants.DIAGNOSTIC_CODE_FIELD_NOT_EXIST; + } + if (messageKey != null) { + diagnostics.add(createDiagnostic(fieldOrProperty, unit, Messages.getMessage(messageKey), + code, null, DiagnosticSeverity.Warning)); + } + } + } + + private boolean hasField(PsiMethod method, PsiClass type) { + String methodName = method.getName(); + // Exclude 'get' from method name and decapitalize the first letter + String expectedFieldName = (methodName.startsWith("get") && methodName.length() > 3) ? Introspector.decapitalize(methodName.substring(3)) : null; + PsiField expectedField = StringUtils.isNotBlank(expectedFieldName) ? type.findFieldByName(expectedFieldName, false) : null; + return expectedField != null; + } } diff --git a/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties b/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties index 66e26c44e..b03831784 100644 --- a/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties +++ b/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties @@ -1,5 +1,5 @@ # /******************************************************************************* -# * Copyright (c) 2022, 2024 IBM Corporation and others. +# * Copyright (c) 2022, 2025 IBM Corporation and others. # * # * This program and the accompanying materials are made available under the # * terms of the Eclipse Public License v. 2.0 which is available at @@ -145,6 +145,11 @@ AddNoArgPublicConstructor = Add a no-arg public constructor to this class MapKeyAnnotationsNotOnSameField = @MapKeyClass and @MapKey annotations cannot be used on the same field or property. MultipleMapKeyJoinColumnMethod = A method with multiple @MapKeyJoinColumn annotations must specify both the name and referencedColumnName attributes in the corresponding @MapKeyJoinColumn annotations. MultipleMapKeyJoinColumnField = A field with multiple @MapKeyJoinColumn annotations must specify both the name and referencedColumnName attributes in the corresponding @MapKeyJoinColumn annotations. +MapKeyAnnotationsInvalidMethodAccessSpecifier = Method is not public and may not be accessible as expected. +MapKeyAnnotationsFieldNotFound = Method has no matching field name. +MapKeyAnnotationsOnInvalidMethod = This method does not conform to persistent property getter naming conventions. +MapKeyAnnotationsTypeOfField = `{0}` annotation can only be applied to fields of type java.util.Map. +MapKeyAnnotationsReturnTypeOfMethod = `{0}` annotation can only be applied to methods with a return type of java.util.Map. # CompleteFilterAnnotationQuickFix AddTheAttributeTo = Add the `{0}` attribute to {1} diff --git a/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/persistence/JakartaPersistenceTest.java b/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/persistence/JakartaPersistenceTest.java index 46ab1be30..fc408a9c5 100644 --- a/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/persistence/JakartaPersistenceTest.java +++ b/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/persistence/JakartaPersistenceTest.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2021, 2024 IBM Corporation and others. +* Copyright (c) 2021, 2025 IBM Corporation and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -310,4 +310,55 @@ public void removeFinalModifiers() throws Exception { assertJavaCodeAction(codeActionParams5, utils, ca5); } + + @Test + public void testMethodOrFieldType() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsType.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + Diagnostic d1 = d(27, 19, 25, + "`@MapKey` annotation can only be applied to methods with a return type of java.util.Map.", + DiagnosticSeverity.Error, "jakarta-persistence", "InvalidReturnTypeOfMethod"); + + Diagnostic d2 = d(13, 11, 15, + "`@MapKey` annotation can only be applied to fields of type java.util.Map.", + DiagnosticSeverity.Error, "jakarta-persistence", "InvalidTypeOfField"); + + assertJavaDiagnostics(diagnosticsParams, utils, d1, d2); + } + + @Test + public void testAccessorAndNamingConventions() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsGetterConvention.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + Diagnostic d1 = d(37, 33, 41, + "Method is not public and may not be accessible as expected.", + DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMethodAccessSpecifier"); + + Diagnostic d2 = d(42, 33, 41, + "This method does not conform to persistent property getter naming conventions.", + DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMethodName"); + + Diagnostic d3 = d(47, 32, 42, + "Method has no matching field name.", + DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMapKeyAnnotationsFieldNotFound"); + + assertJavaDiagnostics(diagnosticsParams, utils, d1, d2, d3); + } + } \ No newline at end of file diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsGetterConvention.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsGetterConvention.java new file mode 100644 index 000000000..e1627d536 --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsGetterConvention.java @@ -0,0 +1,52 @@ +package io.openliberty.sample.jakarta.persistence; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.persistence.MapKey; +import jakarta.persistence.MapKeyClass; + +public class MapKeyAnnotationsGetterConvention { + + Integer age; + + + String name; + + Map place; + + Map gender; + + Map testMap = new HashMap<>(); + + + @MapKeyClass(Map.class) + public Map getTestMap() { + return this.testMap; + } + + + public Integer getAge() { + return this.age; + } + + public String getName() { + return this.name; + } + + @MapKeyClass(Map.class) + private Map getPlace() { + return this.place; + } + + @MapKey() + public Map geGender() { + return null; + } + + @MapKey() + public Map getPerform() { + return null; + } + +} diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsType.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsType.java new file mode 100644 index 000000000..281813c5f --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsType.java @@ -0,0 +1,40 @@ +package io.openliberty.sample.jakarta.persistence; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.persistence.MapKey; +import jakarta.persistence.MapKeyClass; + +public class MapKeyAnnotationsType { + + Integer age; + + @MapKey() + String name; + + Map place; + + Map gender; + + Map testMap = new HashMap<>(); + + + public Map getTestMap() { + return this.testMap; + } + + @MapKey() + public Integer getAge() { + return this.age; + } + + public String getName() { + return this.name; + } + + private Map getPlace() { + return this.place; + } + +}