Skip to content
Open
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
Expand Up @@ -16,12 +16,14 @@
*/
package org.sonar.java.model.springcontext;

import com.google.gson.annotations.JsonAdapter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.serialization.BeanDefinitionHolderTypeAdapter;
import org.sonar.java.telemetry.SizeEstimable;
import org.sonar.java.telemetry.SizeEstimator;

Expand Down Expand Up @@ -233,6 +235,7 @@ public BeanDefinitionHolder build() {
* @param dependencies The bean's dependencies, mapped by required type FQN to the injection points that require them.
* @param typeHierarchy The fully-qualified names of the bean's own type and of all its ancestors and interfaces.
*/
@JsonAdapter(BeanDefinitionHolderTypeAdapter.class)
Comment thread
gitar-bot[bot] marked this conversation as resolved.
public record InputFileData(
String beanName,
String type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
*/
package org.sonar.java.model.springcontext;

import com.google.gson.annotations.JsonAdapter;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.java.serialization.InjectionPointTypeAdapter;
import org.sonar.java.telemetry.SizeEstimable;
import org.sonar.java.telemetry.SizeEstimator;

Expand Down Expand Up @@ -51,6 +53,7 @@ public long estimateSize(SizeEstimator estimator) {
* @param span the text span of the injection point within its own file
* @param multiple whether all matching beans are collected here, as in {@link InjectionPoint#multiple()}
*/
@JsonAdapter(InjectionPointTypeAdapter.class)
public record InputFileData(String name, AnalyzerMessage.TextSpan span, boolean multiple) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.sonar.java.model.springcontext;

import com.google.gson.annotations.JsonAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand All @@ -25,17 +26,20 @@
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.java.serialization.SpringContextGatheringModelTypeAdapter;

/**
* Stores the data collected per-module, per-file during Spring context collection.
*/
@JsonAdapter(SpringContextGatheringModelTypeAdapter.class)
public class SpringContextGatheringModel {

/**
* The Spring data collected for a given file in a module.
*
* @param beans The bean definitions gathered in the file.
* @param packages The packages covered by the file.
* @param inputFile The source file, absent after JSON deserialization.
* @param beans The bean definitions gathered in the file.
* @param packages The packages covered by the file.
*/
public record InputFileData(@Nullable InputFile inputFile, List<BeanDefinitionHolder.InputFileData> beans, Set<String> packages) {
}
Expand All @@ -45,6 +49,14 @@ public record InputFileData(@Nullable InputFile inputFile, List<BeanDefinitionHo
*/
private final Map<String, Map<String, InputFileData>> filesData = new HashMap<>();

public Map<String, Map<String, InputFileData>> filesData() {
return Map.copyOf(filesData);
}

public void ensureModule(String moduleKey) {
filesData.computeIfAbsent(moduleKey, k -> new HashMap<>());
}

public void collectBeans(String moduleKey, String fileKey, @Nullable InputFile inputFile, List<BeanDefinitionHolder.InputFileData> beans) {
filesData
.computeIfAbsent(moduleKey, k -> new HashMap<>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public static BeanDefinitionHolderTypeAdapter getInstance() {

@Override
public void write(JsonWriter out, BeanDefinitionHolder.InputFileData bean) throws IOException {
boolean serializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
out.beginObject();
out.name(NAME).value(bean.beanName());
out.name(TYPE).value(bean.type());
Expand All @@ -82,6 +84,7 @@ public void write(JsonWriter out, BeanDefinitionHolder.InputFileData bean) throw
out.name(TYPE_HIERARCHY);
writeStrings(out, bean.typeHierarchy());
out.endObject();
out.setSerializeNulls(serializeNulls);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ public static InjectionPointTypeAdapter getInstance() {

@Override
public void write(JsonWriter out, InjectionPoint.InputFileData injectionPoint) throws IOException {
boolean serializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
out.beginObject();
out.name(NAME).value(injectionPoint.name());
out.name(SPAN);
TextSpanTypeAdapter.getInstance().write(out, injectionPoint.span());
out.name(MULTIPLE).value(injectionPoint.multiple());
out.endObject();
out.setSerializeNulls(serializeNulls);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class JsonUtils {
private static final String VERSION = "version";

// Spring properties:
public static final String FILES_DATA = "filesData";
public static final String BEANS = "beans";
public static final String PACKAGES = "packages";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.serialization;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.sonar.java.model.springcontext.BeanDefinitionHolder;
import org.sonar.java.model.springcontext.SpringContextGatheringModel;

import static org.sonar.java.serialization.JsonUtils.BEANS;
import static org.sonar.java.serialization.JsonUtils.FILES_DATA;
import static org.sonar.java.serialization.JsonUtils.PACKAGES;
import static org.sonar.java.serialization.JsonUtils.missingProperty;
import static org.sonar.java.serialization.JsonUtils.readStrings;
import static org.sonar.java.serialization.JsonUtils.required;
import static org.sonar.java.serialization.JsonUtils.writeStrings;

/**
* JSON representation of the Spring context data collected by module and file. Null properties in bean definitions
* remain explicit so the bean adapter can read them back with Gson's default settings.
*/
public final class SpringContextGatheringModelTypeAdapter extends TypeAdapter<SpringContextGatheringModel> {

private static final SpringContextGatheringModelTypeAdapter INSTANCE = new SpringContextGatheringModelTypeAdapter();

private SpringContextGatheringModelTypeAdapter() {
}

public static SpringContextGatheringModelTypeAdapter getInstance() {
return INSTANCE;
}

@Override
public void write(JsonWriter out, SpringContextGatheringModel model) throws IOException {
boolean serializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
try {
out.beginObject();
out.name(FILES_DATA);
out.beginObject();
for (Map.Entry<String, Map<String, SpringContextGatheringModel.InputFileData>> module : model.filesData().entrySet()) {
out.name(module.getKey());
out.beginObject();
for (Map.Entry<String, SpringContextGatheringModel.InputFileData> file : module.getValue().entrySet()) {
out.name(file.getKey());
writeInputFileData(out, file.getValue());
}
out.endObject();
}
out.endObject();
out.endObject();
} finally {
out.setSerializeNulls(serializeNulls);
}
}

@Override
public SpringContextGatheringModel read(JsonReader in) throws IOException {
var model = new SpringContextGatheringModel();
boolean filesDataRead = false;
in.beginObject();
while (in.hasNext()) {
if (FILES_DATA.equals(in.nextName())) {
readFilesData(in, model);
filesDataRead = true;
} else {
in.skipValue();
}
}
in.endObject();
if (!filesDataRead) {
throw missingProperty(FILES_DATA);
}
return model;
}

private static void writeInputFileData(JsonWriter out, SpringContextGatheringModel.InputFileData fileData) throws IOException {
out.beginObject();
out.name(BEANS);
out.beginArray();
for (BeanDefinitionHolder.InputFileData bean : fileData.beans()) {
BeanDefinitionHolderTypeAdapter.getInstance().write(out, bean);
}
out.endArray();
out.name(PACKAGES);
writeStrings(out, fileData.packages());
out.endObject();
}

private static void readFilesData(JsonReader in, SpringContextGatheringModel model) throws IOException {
in.beginObject();
while (in.hasNext()) {
String moduleKey = in.nextName();
model.ensureModule(moduleKey);
in.beginObject();
while (in.hasNext()) {
readInputFileData(in, model, moduleKey, in.nextName());
}
in.endObject();
}
in.endObject();
}

private static void readInputFileData(JsonReader in, SpringContextGatheringModel model, String moduleKey, String fileKey) throws IOException {
List<BeanDefinitionHolder.InputFileData> beans = null;
Set<String> packages = null;
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case BEANS -> beans = readBeans(in);
case PACKAGES -> packages = readStrings(in);
default -> in.skipValue();
}
}
in.endObject();
model.collectBeans(moduleKey, fileKey, null, required(beans, BEANS));
model.collectPackages(moduleKey, fileKey, null, required(packages, PACKAGES));
}

private static List<BeanDefinitionHolder.InputFileData> readBeans(JsonReader in) throws IOException {
List<BeanDefinitionHolder.InputFileData> beans = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
beans.add(BeanDefinitionHolderTypeAdapter.getInstance().read(in));
}
in.endArray();
return beans;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.serialization;

import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.java.TestUtils;
import org.sonar.java.model.springcontext.BeanDefinitionHolder;
import org.sonar.java.model.springcontext.ProfileExpression;
import org.sonar.java.model.springcontext.SpringContextGatheringModel;
import org.sonar.java.reporting.AnalyzerMessage.TextSpan;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class SpringContextGatheringModelTypeAdapterTest {

private static final Gson GSON = new Gson();
private static final InputFile INPUT_FILE = TestUtils.inputFile(new File("src/test/files/springcontext/SimpleComponent.java"));

@Test
void round_trips_module_and_file_data_without_serializing_input_files() {
var bean = new BeanDefinitionHolder.InputFileData("bean", "example.Bean", "example", new TextSpan(1, 0, 1, 4),
false, ProfileExpression.UNCONDITIONAL, null, Map.of(), Set.of("example.Bean"));
var model = new SpringContextGatheringModel();
model.collectBeans("module-a", "file-a", INPUT_FILE, List.of(bean));
model.collectPackages("module-a", "file-a", INPUT_FILE, Set.of("example"));
model.collectBeans("module-a", "file-b", INPUT_FILE, List.of());
model.collectPackages("module-b", "file-c", INPUT_FILE, Set.of("other"));

String json = GSON.toJson(model);

var filesData = JsonParser.parseString(json).getAsJsonObject().getAsJsonObject("filesData");
assertThat(filesData.keySet()).containsExactlyInAnyOrder("module-a", "module-b");
var file = filesData.getAsJsonObject("module-a").getAsJsonObject("file-a");
assertThat(file.has("inputFile")).isFalse();
assertThat(file.getAsJsonArray("beans")).hasSize(1);
assertThat(file.getAsJsonArray("packages").get(0).getAsString()).isEqualTo("example");

var restored = GSON.fromJson(json, SpringContextGatheringModel.class);
assertThat(restored.filesData().get("module-a")).containsOnlyKeys("file-a", "file-b");
assertThat(restored.filesData().get("module-b")).containsOnlyKeys("file-c");
assertThat(restored.filesData().get("module-a").get("file-a").beans()).containsExactly(bean);
assertThat(restored.filesData().get("module-a").get("file-a").packages()).containsExactly("example");
assertThat(restored.filesData().get("module-a").get("file-b").beans()).isEmpty();
assertThat(restored.filesData().get("module-b").get("file-c").packages()).containsExactly("other");
restored.filesData().values().stream()
.flatMap(files -> files.values().stream())
.forEach(data -> assertThat(data.inputFile()).isNull());
}

@Test
void ignores_input_file_property_when_reading_json() {
String json = """
{"filesData":{"module":{"file":{"inputFile":{"key":"ignored"},"beans":[],"packages":[]}}}}
""";

var restored = GSON.fromJson(json, SpringContextGatheringModel.class);

assertThat(restored.filesData().get("module").get("file").inputFile()).isNull();
}

@Test
void preserves_an_empty_module() {
String json = "{\"filesData\":{\"module\":{}}}";

var restored = GSON.fromJson(json, SpringContextGatheringModel.class);

assertThat(restored.filesData().get("module")).isEmpty();
assertThat(GSON.toJson(restored)).isEqualTo(json);
}

@Test
void rejects_missing_file_data() {
assertThatThrownBy(() -> GSON.fromJson("{}", SpringContextGatheringModel.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Missing JSON property 'filesData'");
}

@Test
void rejects_missing_InputFileData_field() {
String missingPackages = "{\"filesData\":{\"module\":{\"file\":{\"beans\":[]}}}}";
assertThatThrownBy(() -> GSON.fromJson(missingPackages, SpringContextGatheringModel.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Missing JSON property 'packages'");
}
}
Loading