/* * SonarQube * Copyright (C) 2009-2025 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.db.measure; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.measures.CoreMetrics; import org.sonar.core.metric.SoftwareQualitiesMetrics; import org.sonar.core.util.CloseableIterator; import org.sonar.db.DatabaseUtils; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; import org.sonar.db.component.ComponentQualifiers; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY; import static org.sonar.api.utils.KeyValueFormat.parseStringInt; import static org.sonar.db.component.DbTagsReader.readDbTags; public class ProjectMeasuresIndexerIterator extends CloseableIterator { public static final Set METRIC_KEYS = ImmutableSortedSet.of( NCLOC_KEY, CoreMetrics.LINES_KEY, CoreMetrics.DUPLICATED_LINES_DENSITY_KEY, CoreMetrics.COVERAGE_KEY, CoreMetrics.SQALE_RATING_KEY, CoreMetrics.RELIABILITY_RATING_KEY, CoreMetrics.SECURITY_RATING_KEY, CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY, CoreMetrics.SECURITY_REVIEW_RATING_KEY, CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY, CoreMetrics.ALERT_STATUS_KEY, CoreMetrics.NEW_SECURITY_RATING_KEY, CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY, CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY, CoreMetrics.NEW_COVERAGE_KEY, CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY, CoreMetrics.NEW_LINES_KEY, CoreMetrics.NEW_RELIABILITY_RATING_KEY, //Ratings based on software quality SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING_KEY, SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING_KEY, SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING_KEY, SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY, SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING_KEY ); private static final Gson GSON = new Gson(); private static final String SQL_PROJECTS = """ SELECT p.uuid, p.kee, p.name, p.created_at, s.created_at, p.tags, p.qualifier, p.ncloc FROM projects p INNER JOIN project_branches pb ON pb.project_uuid = p.uuid AND pb.is_main = ? LEFT OUTER JOIN snapshots s ON s.root_component_uuid=pb.uuid AND s.islast=? WHERE p.qualifier in (?, ?)"""; private static final String PROJECT_FILTER = " AND pb.project_uuid=?"; private static final String SQL_GENERIC_METRICS = """ SELECT m.name FROM metrics m WHERE m.enabled = ?"""; private static final String SQL_MEASURES = """ SELECT m.json_value FROM measures m INNER JOIN project_branches pb ON pb.uuid = m.component_uuid WHERE pb.project_uuid = ? AND pb.is_main = ?"""; private static final boolean ENABLED = true; private final DbSession dbSession; private final DbClient dbClient; private final PreparedStatement measuresStatement; private final Iterator projects; private final List metrics; private ProjectMeasuresIndexerIterator(DbSession dbSession, DbClient dbClient, PreparedStatement measuresStatement, List projects, List metrics) { this.dbSession = dbSession; this.dbClient = dbClient; this.measuresStatement = measuresStatement; this.projects = projects.iterator(); this.metrics = metrics; } public static ProjectMeasuresIndexerIterator create(DbSession session, DbClient dbClient, @Nullable String projectUuid) { List projects = selectProjects(session, projectUuid); PreparedStatement measuresStatement = createMeasuresStatement(session); return new ProjectMeasuresIndexerIterator(session, dbClient, measuresStatement, projects, selectMetrics(session)); } private static List selectMetrics(DbSession session) { List metrics = new ArrayList<>(); try (PreparedStatement stmt = createMetricsStatement(session); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { String key = rs.getString(1); metrics.add(key); } return metrics; } catch (SQLException e) { throw new IllegalStateException("Fail to execute request to select all metrics", e); } } private static PreparedStatement createMetricsStatement(DbSession session) { try { PreparedStatement stmt = session.getConnection().prepareStatement(SQL_GENERIC_METRICS); stmt.setBoolean(1, ENABLED); return stmt; } catch (SQLException e) { throw new IllegalStateException("Fail to prepare SQL request to select all metrics", e); } } private static List selectProjects(DbSession session, @Nullable String projectUuid) { List projects = new ArrayList<>(); try (PreparedStatement stmt = createProjectsStatement(session, projectUuid); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { String uuid = rs.getString(1); String key = rs.getString(2); String name = rs.getString(3); Long creationDate = rs.getLong(4); Long analysisDate = DatabaseUtils.getLong(rs, 5); List tags = readDbTags(DatabaseUtils.getString(rs, 6)); String qualifier = rs.getString(7); Long ncloc = DatabaseUtils.getLong(rs, 8); Project project = new Project.Builder() .setUuid(uuid) .setKey(key) .setName(name) .setQualifier(qualifier) .setTags(tags) .setCreationDate(creationDate) .setAnalysisDate(analysisDate) .setNcloc(ncloc) .build(); projects.add(project); } return projects; } catch (SQLException e) { throw new IllegalStateException("Fail to execute request to select all projects", e); } } private static PreparedStatement createProjectsStatement(DbSession session, @Nullable String projectUuid) { try { StringBuilder sql = new StringBuilder(SQL_PROJECTS); if (projectUuid != null) { sql.append(PROJECT_FILTER); } PreparedStatement stmt = session.getConnection().prepareStatement(sql.toString()); stmt.setBoolean(1, true); stmt.setBoolean(2, true); stmt.setString(3, ComponentQualifiers.PROJECT); stmt.setString(4, ComponentQualifiers.APP); if (projectUuid != null) { stmt.setString(5, projectUuid); } return stmt; } catch (SQLException e) { throw new IllegalStateException("Fail to prepare SQL request to select all project measures", e); } } private static PreparedStatement createMeasuresStatement(DbSession session) { try { return session.getConnection().prepareStatement(SQL_MEASURES); } catch (SQLException e) { throw new IllegalStateException("Fail to prepare SQL request to select measures", e); } } @Override @CheckForNull protected ProjectMeasures doNext() { if (!projects.hasNext()) { return null; } Project project = projects.next(); Measures measures = selectMeasures(project); return new ProjectMeasures(project, measures); } private Measures selectMeasures(Project project) { String projectUuid = project.getUuid(); try { Measures measures = getMeasures(projectUuid); project.getNcloc().ifPresent(ncloc -> readNclocDistributionFromBiggestBranch(projectUuid, ncloc, measures)); return measures; } catch (Exception e) { throw new IllegalStateException(String.format("Fail to execute request to select measures of project %s", projectUuid), e); } } private Measures getMeasures(String projectUuid) throws SQLException { Measures measures = new Measures(); measuresStatement.setString(1, projectUuid); measuresStatement.setBoolean(2, true); try (ResultSet rs = measuresStatement.executeQuery()) { while (rs.next()) { readMeasureLine(rs, measures); } } return measures; } private void readMeasureLine(ResultSet rs, Measures measures) throws SQLException { String jsonValue = rs.getString(1); Map metricValues = GSON.fromJson(jsonValue, new TypeToken>() { }.getType()); metricValues.forEach((metricKey, value) -> readMeasure(measures, metricKey, value)); } private void readMeasure(Measures measures, String metricKey, Object value) { if (METRIC_KEYS.contains(metricKey) && metrics.contains(metricKey)) { if (ALERT_STATUS_KEY.equals(metricKey) && value instanceof String qualityGate) { measures.setQualityGateStatus(qualityGate); return; } if (NCLOC_LANGUAGE_DISTRIBUTION_KEY.equals(metricKey) && value instanceof String distribution) { measures.setNclocByLanguages(distribution); return; } if (value instanceof Double doubleValue) { measures.addNumericMeasure(metricKey, doubleValue); } } } private void readNclocDistributionFromBiggestBranch(String projectUuid, long ncloc, Measures measures) { List branchUuids = dbClient.branchDao().selectByProjectUuid(dbSession, projectUuid).stream().map(BranchDto::getUuid).toList(); List measureDtos = dbClient.measureDao().selectByComponentUuidsAndMetricKeys(dbSession, branchUuids, List.of(NCLOC_KEY, NCLOC_LANGUAGE_DISTRIBUTION_KEY)); for (MeasureDto measureDto : measureDtos) { Long branchNcloc = measureDto.getLong(NCLOC_KEY); if (branchNcloc != null && branchNcloc == ncloc) { String nclocLanguageDistribution = measureDto.getString(NCLOC_LANGUAGE_DISTRIBUTION_KEY); if (nclocLanguageDistribution != null) { measures.setNclocByLanguages(nclocLanguageDistribution); } break; } } } @Override protected void doClose() throws Exception { measuresStatement.close(); } public static class Project { private final String uuid; private final String key; private final String name; private final String qualifier; private final Long creationDate; private final Long analysisDate; private final List tags; private final Long ncloc; private Project(Builder builder) { this.uuid = builder.uuid; this.key = builder.key; this.name = builder.name; this.qualifier = builder.qualifier; this.tags = builder.tags; this.analysisDate = builder.analysisDate; this.creationDate = builder.creationDate; this.ncloc = builder.ncloc; } public String getUuid() { return uuid; } public String getKey() { return key; } public String getName() { return name; } public String getQualifier() { return qualifier; } public List getTags() { return tags; } @CheckForNull public Long getAnalysisDate() { return analysisDate; } public Long getCreationDate() { return creationDate; } public Optional getNcloc() { return Optional.ofNullable(ncloc); } private static class Builder { private String uuid; private String key; private String name; private String qualifier; private Long creationDate; private Long analysisDate; private List tags; private Long ncloc; public Builder setUuid(String uuid) { this.uuid = uuid; return this; } public Builder setKey(String key) { this.key = key; return this; } public Builder setName(String name) { this.name = name; return this; } public Builder setQualifier(String qualifier) { this.qualifier = qualifier; return this; } public Builder setCreationDate(Long creationDate) { this.creationDate = creationDate; return this; } public Builder setAnalysisDate(@Nullable Long analysisDate) { this.analysisDate = analysisDate; return this; } public Builder setTags(List tags) { this.tags = tags; return this; } public Builder setNcloc(@Nullable Long ncloc) { this.ncloc = ncloc; return this; } public Project build() { return new Project(this); } } } public static class Measures { private final Map numericMeasures = new HashMap<>(); private String qualityGateStatus; private Map nclocByLanguages = new LinkedHashMap<>(); Measures addNumericMeasure(String metricKey, double value) { numericMeasures.put(metricKey, value); return this; } public Map getNumericMeasures() { return numericMeasures; } Measures setQualityGateStatus(@Nullable String qualityGateStatus) { this.qualityGateStatus = qualityGateStatus; return this; } @CheckForNull public String getQualityGateStatus() { return qualityGateStatus; } Measures setNclocByLanguages(String nclocByLangues) { this.nclocByLanguages = ImmutableMap.copyOf(parseStringInt(nclocByLangues)); return this; } public Map getNclocByLanguages() { return nclocByLanguages; } } public static class ProjectMeasures { private final Project project; private final Measures measures; public ProjectMeasures(Project project, Measures measures) { this.project = project; this.measures = measures; } public Project getProject() { return project; } public Measures getMeasures() { return measures; } } }