Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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};
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -61,6 +64,7 @@ private void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics,
List<PsiAnnotation> mapKeyJoinCols = new ArrayList<PsiAnnotation>();
boolean hasMapKeyAnnotation = false;
boolean hasMapKeyClassAnnotation = false;
boolean hasTypeDiagnostics = false;
PsiAnnotation[] allAnnotations = fieldOrProperty.getAnnotations();
for (PsiAnnotation annotation : allAnnotations) {
String matchedAnnotation = getMatchedJavaElementName(type, annotation.getQualifiedName(),
Expand All @@ -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
Expand All @@ -92,6 +104,36 @@ else if (PersistenceConstants.MAPKEYJOINCOLUMN.equals(matchedAnnotation)) {
}
}

private boolean collectTypeDiagnostics(PsiJvmModifiersOwner fieldOrProperty, String attribute, PsiJavaFile unit,
List<Diagnostic> 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<PsiAnnotation> annotations, PsiElement element,
PsiJavaFile unit, List<Diagnostic> diagnostics) {
String message = (element instanceof PsiMethod) ?
Expand All @@ -110,4 +152,42 @@ private void validateMapKeyJoinColumnAnnotations(List<PsiAnnotation> annotations
}
});
}

private void collectAccessorDiagnostics(PsiJvmModifiersOwner fieldOrProperty, PsiClass type, PsiJavaFile unit,
List<Diagnostic> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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<Integer, String> place;

Map<Integer, String> gender;

Map<Integer, String> testMap = new HashMap<>();


@MapKeyClass(Map.class)
public Map<Integer, String> getTestMap() {
return this.testMap;
}


public Integer getAge() {
return this.age;
}

public String getName() {
return this.name;
}

@MapKeyClass(Map.class)
private Map<Integer, String> getPlace() {
return this.place;
}

@MapKey()
public Map<Integer, String> geGender() {
return null;
}

@MapKey()
public Map<Integer, String> getPerform() {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -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<Integer, String> place;

Map<Integer, String> gender;

Map<Integer, String> testMap = new HashMap<>();


public Map<Integer, String> getTestMap() {
return this.testMap;
}

@MapKey()
public Integer getAge() {
return this.age;
}

public String getName() {
return this.name;
}

private Map<Integer, String> getPlace() {
return this.place;
}

}
Loading