Skip to content

Commit 8f43be9

Browse files
Sync changes for Jsonb Property Names Must Be Unique, from lsp4Jakarta (#1420)
* Json Utility * Messages and constant updates * Diagnostics for json property unique names * Reformatted source code * Test classes for jsonbUnique testcase * Test cases for jsonb unique diagnostics * Copyright update * Copyright updates * Format source * Nested loop - Refactored
1 parent 6eb1b9c commit 8f43be9

File tree

7 files changed

+261
-6
lines changed

7 files changed

+261
-6
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 IBM Corporation and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*
11+
* Contributors:
12+
* IBM Corporation - initial API and implementation
13+
*******************************************************************************/
14+
package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij;
15+
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
19+
import com.intellij.psi.PsiAnnotation;
20+
import com.intellij.psi.PsiAnnotationMemberValue;
21+
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.jsonb.JsonbConstants;
22+
23+
24+
/**
25+
* Utilities for working with JSON properties and extracting/decoding values from its attribute, annotation etc.
26+
*/
27+
public class JsonPropertyUtils {
28+
29+
/**
30+
* @param propertyName
31+
* @return String
32+
* @description Method decodes unicode property name value to string value
33+
*/
34+
public static String decodeUnicodeName(String propertyName) {
35+
Pattern pattern = Pattern.compile(JsonbConstants.JSONB_PROPERTYNAME_UNICODE); // Pattern for detecting unicode sequence
36+
Matcher matcher = pattern.matcher(propertyName);
37+
StringBuffer decoded = new StringBuffer();
38+
while (matcher.find()) {
39+
String unicode = matcher.group(1);
40+
char decodedChar = (char) Integer.parseInt(unicode, 16);
41+
matcher.appendReplacement(decoded, Character.toString(decodedChar));
42+
}
43+
matcher.appendTail(decoded);
44+
return decoded.toString();
45+
}
46+
47+
/**
48+
* @param annotation
49+
* @return String
50+
* @description Method extracts property name value from the annotation
51+
*/
52+
public static String extractPropertyNameFromJsonField(PsiAnnotation annotation) {
53+
PsiAnnotationMemberValue psiValue = annotation.findAttributeValue("value");
54+
String value = psiValue != null ? psiValue.getText() : null;
55+
// Remove wrapping quotes if it's a string literal
56+
if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
57+
value = value.substring(1, value.length() - 1);
58+
}
59+
return value;
60+
}
61+
}

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/jsonb/JsonbConstants.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2020, 2023 IBM Corporation and others.
2+
* Copyright (c) 2020, 2025 IBM Corporation and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -24,14 +24,16 @@ public class JsonbConstants {
2424
public static final String DIAGNOSTIC_CODE_ANNOTATION = "MultipleJsonbCreatorAnnotations";
2525
public static final String DIAGNOSTIC_CODE_ANNOTATION_TRANSIENT_FIELD = "NonmutualJsonbTransientAnnotation";
2626
public static final String DIAGNOSTIC_CODE_ANNOTATION_TRANSIENT_ACCESSOR = "NonmutualJsonbTransientAnnotationOnAccessor";
27-
27+
public static final String DIAGNOSTIC_CODE_ANNOTATION_DUPLICATE_NAME = "DuplicatePropertyNamesOnJsonbFields";
2828

2929
/* Annotation Constants */
3030
public static final String JSONB_PACKAGE = "jakarta.json.bind.annotation.";
3131
public static final String JSONB_PREFIX = "Jsonb";
3232

3333
public static final String JSONB_CREATOR = JSONB_PACKAGE + JSONB_PREFIX + "Creator";
3434
public static final int MAX_METHOD_WITH_JSONBCREATOR = 1;
35+
public static final int MAX_PROPERTY_COUNT = 1;
36+
public static final String JSONB_PROPERTYNAME_UNICODE = "\\\\u([0-9A-Fa-f]{4})";
3537

3638
public static final String JSONB_TRANSIENT = JSONB_PREFIX + "Transient";
3739
public static final String JSONB_TRANSIENT_FQ_NAME = JSONB_PACKAGE + JSONB_TRANSIENT;

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/jsonb/JsonbDiagnosticsCollector.java

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2020, 2024 IBM Corporation, Matheus Cruz and others.
2+
* Copyright (c) 2020, 2025 IBM Corporation, Matheus Cruz and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -13,18 +13,21 @@
1313

1414
package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.jsonb;
1515

16-
import java.util.ArrayList;
17-
import java.util.List;
16+
import java.util.*;
1817
import java.util.stream.Collectors;
1918

2019
import com.intellij.psi.*;
20+
import com.intellij.psi.impl.PsiClassImplUtil;
21+
import com.intellij.psi.util.InheritanceUtil;
2122
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
2223
import com.google.gson.Gson;
2324
import com.google.gson.JsonArray;
2425
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils;
26+
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JsonPropertyUtils;
2527
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
2628
import org.eclipse.lsp4j.Diagnostic;
2729
import org.eclipse.lsp4j.DiagnosticSeverity;
30+
import org.jetbrains.annotations.NotNull;
2831

2932
/**
3033
* This class contains logic for Jsonb diagnostics:
@@ -70,13 +73,100 @@ public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
7073
}
7174
}
7275
// fields
76+
//Changes to detect if Jsonb property names are not unique
77+
Set<String> uniquePropertyNames = new LinkedHashSet<String>();
7378
for (PsiField field : type.getFields()) {
7479
collectJsonbTransientFieldDiagnostics(unit, type, diagnostics, field);
7580
collectJsonbTransientAccessorDiagnostics(unit, type, diagnostics, field);
81+
collectJsonbUniquePropertyNames(uniquePropertyNames, field);
7682
}
83+
// Collect diagnostics for duplicate property names with fields annotated @JsonbProperty
84+
collectJsonbPropertyUniquenessDiagnostics(unit, diagnostics, uniquePropertyNames, type);
7785
}
7886
}
7987

88+
89+
/**
90+
* @param uniquePropertyNames
91+
* @param field
92+
* @description Method collects distinct property name values to be referenced for finding duplicates
93+
*/
94+
private void collectJsonbUniquePropertyNames(Set<String> uniquePropertyNames, PsiField field) {
95+
for (PsiAnnotation annotation : field.getAnnotations()) {
96+
if (isMatchedAnnotation(annotation, JsonbConstants.JSONB_PROPERTY)) { // Checks whether annotation is JsonbProperty
97+
String propertyName = JsonPropertyUtils.extractPropertyNameFromJsonField(annotation);
98+
if (propertyName != null) {
99+
uniquePropertyNames.add(JsonPropertyUtils.decodeUnicodeName(propertyName));
100+
}
101+
}
102+
}
103+
}
104+
105+
106+
/**
107+
* @param unit
108+
* @param diagnostics
109+
* @param uniquePropertyNames
110+
* @param type
111+
* @description Method to collect JsonbProperty uniqueness diagnostics
112+
*/
113+
private void collectJsonbPropertyUniquenessDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics,
114+
Set<String> uniquePropertyNames, PsiClass type) {
115+
Set<PsiClass> hierarchy = new LinkedHashSet<>(PsiClassImplUtil.getAllSuperClassesRecursively(type));
116+
Map<String, List<PsiField>> jsonbMap = buildPropertyMap(uniquePropertyNames, hierarchy);
117+
118+
for (Map.Entry<String, List<PsiField>> entry : jsonbMap.entrySet()) { // Iterates through set of all key values pairs inside the map
119+
List<PsiField> fields = entry.getValue();
120+
if (fields.size() > JsonbConstants.MAX_PROPERTY_COUNT) {
121+
for (PsiField f : fields) {
122+
if (f.getContainingClass().equals(type)) {// Creates diagnostics in the subclass
123+
createJsonbPropertyUniquenessDiagnostics(unit, diagnostics, f, type);
124+
}
125+
}
126+
}
127+
}
128+
}
129+
130+
/**
131+
* @param unit
132+
* @param diagnostics
133+
* @param field
134+
* @param type
135+
* @description Method creates diagnostics with appropriate message and cursor context
136+
*/
137+
private void createJsonbPropertyUniquenessDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics,
138+
PsiField field, PsiClass type) {
139+
List<String> jsonbAnnotationsForField = getJsonbAnnotationNames(type, field);
140+
String diagnosticErrorMessage = Messages.getMessage("ErrorMessageJsonbPropertyUniquenessField");
141+
diagnostics.add(createDiagnostic(field, unit, diagnosticErrorMessage, JsonbConstants.DIAGNOSTIC_CODE_ANNOTATION_DUPLICATE_NAME,
142+
(JsonArray) (new Gson().toJsonTree(jsonbAnnotationsForField)), DiagnosticSeverity.Error));
143+
}
144+
145+
/**
146+
* @param uniquePropertyNames
147+
* @param hierarchy
148+
* @return Map<String, List < IField>> jsonbMap
149+
* @description This method collects the property name and fields using the same name if it's duplicated and builds it into a Map.
150+
*/
151+
private Map<String, List<PsiField>> buildPropertyMap(Set<String> uniquePropertyNames, Set<PsiClass> hierarchy) {
152+
Map<String, List<PsiField>> jsonbMap = new HashMap<>();
153+
hierarchy.stream()
154+
.flatMap(finalType -> Arrays.stream(finalType.getFields())) // flatten PsiFields
155+
.flatMap(field -> Arrays.stream(field.getAnnotations())
156+
.filter(annotation -> isMatchedAnnotation(annotation, JsonbConstants.JSONB_PROPERTY))
157+
.map(annotation -> Map.entry(annotation, field))) // pair annotation with its field
158+
.map(entry -> {
159+
String propertyName = JsonPropertyUtils.extractPropertyNameFromJsonField(entry.getKey());
160+
propertyName = propertyName != null ? JsonPropertyUtils.decodeUnicodeName(propertyName) : null;
161+
return Map.entry(propertyName, entry.getValue());
162+
})
163+
.filter(entry -> entry.getKey() != null && uniquePropertyNames.contains(entry.getKey()))
164+
.forEach(entry ->
165+
jsonbMap.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).add(entry.getValue())
166+
);
167+
return jsonbMap;
168+
}
169+
80170
private void collectJsonbTransientFieldDiagnostics(PsiJavaFile unit, PsiClass type, List<Diagnostic> diagnostics, PsiField field) {
81171
List<String> jsonbAnnotationsForField = getJsonbAnnotationNames(type, field);
82172
if (jsonbAnnotationsForField.contains(JsonbConstants.JSONB_TRANSIENT_FQ_NAME)) {

src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ RemoveAllEntityParametersExcept = Remove all entity parameters except {0}
130130
ErrorMessageJsonbCreator = Only one constructor or static factory method can be annotated with @JsonbCreator in a given class.
131131
ErrorMessageJsonbTransientOnField = When a class field is annotated with @JsonbTransient, this field, getter or setter must not be annotated with other JSON Binding annotations.
132132
ErrorMessageJsonbTransientOnAccessor = When an accessor is annotated with @JsonbTransient, its field or the accessor must not be annotated with other JSON Binding annotations.
133+
ErrorMessageJsonbPropertyUniquenessField = Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.
133134

134135
# JsonpDiagnosticCollector
135136
CreatePointerErrorMessage = Json.createPointer target must be a sequence of '/' prefixed tokens or an empty String.

src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/jsonb/JsonbDiagnosticsCollectorTest.java

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2021, 2024 IBM Corporation and others.
2+
* Copyright (c) 2021, 2025 IBM Corporation and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -438,4 +438,65 @@ public void JsonbTransientNotMutuallyExclusive() throws Exception {
438438
CodeAction ca8 = JakartaForJavaAssert.ca(uri, "Remove @JsonbAnnotation", d6, te8);
439439
JakartaForJavaAssert.assertJavaCodeAction(codeActionParams5, utils, ca8);
440440
}
441+
442+
@Test
443+
public void JsonbPropertyUniquenessSubClass() throws Exception {
444+
Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample"));
445+
IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject());
446+
447+
VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module)
448+
+ "/src/main/java/io/openliberty/sample/jakarta/jsonb/JsonbTransientDiagnosticSubClass.java");
449+
String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString();
450+
451+
JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams();
452+
diagnosticsParams.setUris(Arrays.asList(uri));
453+
454+
Diagnostic d1 = JakartaForJavaAssert.d(11, 19, 36,
455+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
456+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
457+
d1.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
458+
459+
Diagnostic d2 = JakartaForJavaAssert.d(17, 19, 34,
460+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
461+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
462+
d2.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
463+
464+
Diagnostic d3 = JakartaForJavaAssert.d(20, 19, 34,
465+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
466+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
467+
d3.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
468+
469+
JakartaForJavaAssert.assertJavaDiagnostics(diagnosticsParams, utils, d1, d2, d3);
470+
}
471+
472+
@Test
473+
public void JsonbPropertyUniquenessSubSubClass() throws Exception {
474+
475+
Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample"));
476+
IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject());
477+
478+
VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module)
479+
+ "/src/main/java/io/openliberty/sample/jakarta/jsonb/JsonbTransientDiagnosticSubSubClass.java");
480+
String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString();
481+
482+
JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams();
483+
diagnosticsParams.setUris(Arrays.asList(uri));
484+
485+
Diagnostic d1 = JakartaForJavaAssert.d(8, 19, 31,
486+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
487+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
488+
d1.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
489+
490+
Diagnostic d2 = JakartaForJavaAssert.d(11, 19, 36,
491+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
492+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
493+
d2.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
494+
495+
Diagnostic d3 = JakartaForJavaAssert.d(14, 19, 37,
496+
"Multiple fields or properties with @JsonbProperty must not have JSON members with duplicate names, the member names must be unique.",
497+
DiagnosticSeverity.Error, "jakarta-jsonb", "DuplicatePropertyNamesOnJsonbFields");
498+
d3.setData(new Gson().toJsonTree(Arrays.asList("jakarta.json.bind.annotation.JsonbProperty")));
499+
500+
JakartaForJavaAssert.assertJavaDiagnostics(diagnosticsParams, utils, d1, d2, d3);
501+
}
441502
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.openliberty.sample.jakarta.jsonb;
2+
3+
import jakarta.json.bind.annotation.JsonbProperty;
4+
5+
public class JsonbTransientDiagnosticSubClass extends JsonbTransientDiagnostic {
6+
7+
8+
@JsonbProperty("hello")
9+
private String subFirstName;
10+
11+
@JsonbProperty("fav_lang")
12+
private String subfavoriteEditor; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
13+
14+
@JsonbProperty("fav_lang1")
15+
private String subfavoriteEditor1;
16+
17+
@JsonbProperty("just_in_sub_class")
18+
private String justInSubClass1; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
19+
20+
@JsonbProperty("just_in_sub_class")
21+
private String justInSubClass2; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.openliberty.sample.jakarta.jsonb;
2+
3+
import jakarta.json.bind.annotation.JsonbProperty;
4+
5+
public class JsonbTransientDiagnosticSubSubClass extends JsonbTransientDiagnosticSubClass {
6+
7+
8+
@JsonbProperty("name")
9+
private String subFirstName; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
10+
11+
@JsonbProperty("fav_editor")
12+
private String subfavoriteEditor; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
13+
14+
@JsonbProperty("fav_lang1")
15+
private String subfavoriteEditor2; // Diagnostic: @JsonbProperty property uniqueness in subclass, multiple properties cannot have same property names.
16+
17+
}

0 commit comments

Comments
 (0)