/* * SonarQube * Copyright (C) 2009-2017 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.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; 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.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Scopes; import org.sonar.core.util.CloseableIterator; import org.sonar.db.DatabaseUtils; import org.sonar.db.DbSession; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY; import static org.sonar.api.measures.Metric.ValueType.BOOL; import static org.sonar.api.measures.Metric.ValueType.FLOAT; import static org.sonar.api.measures.Metric.ValueType.INT; import static org.sonar.api.measures.Metric.ValueType.LEVEL; import static org.sonar.api.measures.Metric.ValueType.MILLISEC; import static org.sonar.api.measures.Metric.ValueType.PERCENT; import static org.sonar.api.measures.Metric.ValueType.RATING; import static org.sonar.api.measures.Metric.ValueType.WORK_DUR; import static org.sonar.api.utils.KeyValueFormat.parseStringInt; import static org.sonar.db.DatabaseUtils.repeatCondition; import static org.sonar.db.component.DbTagsReader.readDbTags; public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMeasuresIndexerIterator.ProjectMeasures> { private static final Set<String> METRIC_TYPES = ImmutableSet.of(INT.name(), FLOAT.name(), PERCENT.name(), BOOL.name(), MILLISEC.name(), LEVEL.name(), RATING.name(), WORK_DUR.name()); private static final Joiner METRICS_JOINER = Joiner.on("','"); private static final String SQL_PROJECTS = "SELECT p.organization_uuid, p.uuid, p.kee, p.name, s.uuid, s.created_at, p.tags " + "FROM projects p " + "LEFT OUTER JOIN snapshots s ON s.component_uuid=p.uuid AND s.islast=? " + "WHERE p.enabled=? AND p.scope=? AND p.qualifier=?"; private static final String PROJECT_FILTER = " AND p.uuid=?"; private static final String SQL_METRICS = "SELECT m.id, m.name FROM metrics m " + "WHERE (m.val_type IN ('" + METRICS_JOINER.join(METRIC_TYPES) + "') OR m.name=?)" + "AND m.enabled=?"; private static final String SQL_MEASURES = "SELECT pm.metric_id, pm.value, pm.variation_value_1, pm.text_value FROM project_measures pm " + "WHERE pm.component_uuid = ? AND pm.analysis_uuid = ? " + "AND pm.metric_id IN ({metricIds}) " + "AND (pm.value IS NOT NULL OR pm.variation_value_1 IS NOT NULL OR pm.text_value IS NOT NULL) " + "AND pm.person_id IS NULL "; private final PreparedStatement measuresStatement; private final Map<Long, String> metricKeysByIds; private final Iterator<Project> projects; private ProjectMeasuresIndexerIterator(PreparedStatement measuresStatement, Map<Long, String> metricKeysByIds, List<Project> projects) { this.measuresStatement = measuresStatement; this.metricKeysByIds = metricKeysByIds; this.projects = projects.iterator(); } public static ProjectMeasuresIndexerIterator create(DbSession session, @Nullable String projectUuid) { try { Map<Long, String> metrics = selectMetricKeysByIds(session); List<Project> projects = selectProjects(session, projectUuid); PreparedStatement projectsStatement = createMeasuresStatement(session, metrics.keySet()); return new ProjectMeasuresIndexerIterator(projectsStatement, metrics, projects); } catch (SQLException e) { throw new IllegalStateException("Fail to execute request to select all project measures", e); } } private static Map<Long, String> selectMetricKeysByIds(DbSession session) { Map<Long, String> metrics = new HashMap<>(); try (PreparedStatement stmt = createMetricsStatement(session); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { metrics.put(rs.getLong(1), rs.getString(2)); } return metrics; } catch (SQLException e) { throw new IllegalStateException("Fail to execute request to select all metrics", e); } } private static PreparedStatement createMetricsStatement(DbSession session) throws SQLException { PreparedStatement stmt = session.getConnection().prepareStatement(SQL_METRICS); stmt.setString(1, NCLOC_LANGUAGE_DISTRIBUTION_KEY); stmt.setBoolean(2, true); return stmt; } private static List<Project> selectProjects(DbSession session, @Nullable String projectUuid) { List<Project> projects = new ArrayList<>(); try (PreparedStatement stmt = createProjectsStatement(session, projectUuid); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { String orgUuid = rs.getString(1); String uuid = rs.getString(2); String key = rs.getString(3); String name = rs.getString(4); String analysisUuid = DatabaseUtils.getString(rs, 5); Long analysisDate = DatabaseUtils.getLong(rs, 6); List<String> tags = readDbTags(DatabaseUtils.getString(rs, 7)); Project project = new Project(orgUuid, uuid, key, name, tags, analysisUuid, analysisDate); 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 { String sql = SQL_PROJECTS; sql += projectUuid == null ? "" : PROJECT_FILTER; PreparedStatement stmt = session.getConnection().prepareStatement(sql); stmt.setBoolean(1, true); stmt.setBoolean(2, true); stmt.setString(3, Scopes.PROJECT); stmt.setString(4, Qualifiers.PROJECT); 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, Set<Long> metricIds) throws SQLException { try { String sql = StringUtils.replace(SQL_MEASURES, "{metricIds}", repeatCondition("?", metricIds.size(), ",")); PreparedStatement stmt = session.getConnection().prepareStatement(sql); int index = 3; for (Long metricId : metricIds) { stmt.setLong(index, metricId); index++; } return stmt; } 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.getUuid(), project.getAnalysisUuid()); return new ProjectMeasures(project, measures); } private Measures selectMeasures(String projectUuid, @Nullable String analysisUuid) { Measures measures = new Measures(); if (analysisUuid == null || metricKeysByIds.isEmpty()) { return measures; } ResultSet rs = null; try { measuresStatement.setString(1, projectUuid); measuresStatement.setString(2, analysisUuid); rs = measuresStatement.executeQuery(); while (rs.next()) { readMeasure(rs, measures); } return measures; } catch (Exception e) { throw new IllegalStateException(String.format("Fail to execute request to select measures of project %s, analysis %s", projectUuid, analysisUuid), e); } finally { DatabaseUtils.closeQuietly(rs); } } private void readMeasure(ResultSet rs, Measures measures) throws SQLException { String metricKey = metricKeysByIds.get(rs.getLong(1)); Optional<Double> value = metricKey.startsWith("new_") ? getDouble(rs, 3) : getDouble(rs, 2); if (value.isPresent()) { measures.addNumericMeasure(metricKey, value.get()); return; } if (ALERT_STATUS_KEY.equals(metricKey)) { readTextValue(rs, measures::setQualityGateStatus); return; } if (NCLOC_LANGUAGE_DISTRIBUTION_KEY.equals(metricKey)) { readTextValue(rs, measures::setLanguages); return; } } private static void readTextValue(ResultSet rs, Consumer<String> action) throws SQLException { String textValue = rs.getString(4); if (!rs.wasNull()) { action.accept(textValue); } } @Override protected void doClose() throws Exception { measuresStatement.close(); } private static Optional<Double> getDouble(ResultSet rs, int index) { try { Double value = rs.getDouble(index); if (!rs.wasNull()) { return Optional.of(value); } return Optional.empty(); } catch (SQLException e) { throw new IllegalStateException("Fail to get double value", e); } } public static class Project { private final String organizationUuid; private final String uuid; private final String key; private final String name; private final String analysisUuid; private final Long analysisDate; private final List<String> tags; public Project(String organizationUuid, String uuid, String key, String name, List<String> tags, @Nullable String analysisUuid, @Nullable Long analysisDate) { this.organizationUuid = organizationUuid; this.uuid = uuid; this.key = key; this.name = name; this.tags = tags; this.analysisUuid = analysisUuid; this.analysisDate = analysisDate; } public String getOrganizationUuid() { return organizationUuid; } public String getUuid() { return uuid; } public String getKey() { return key; } public String getName() { return name; } public List<String> getTags() { return tags; } @CheckForNull public String getAnalysisUuid() { return analysisUuid; } @CheckForNull public Long getAnalysisDate() { return analysisDate; } } public static class Measures { private Map<String, Double> numericMeasures = new HashMap<>(); private String qualityGateStatus; private List<String> languages = new ArrayList<>(); Measures addNumericMeasure(String metricKey, double value) { numericMeasures.put(metricKey, value); return this; } public Map<String, Double> getNumericMeasures() { return numericMeasures; } Measures setQualityGateStatus(@Nullable String qualityGateStatus) { this.qualityGateStatus = qualityGateStatus; return this; } @CheckForNull public String getQualityGateStatus() { return qualityGateStatus; } Measures setLanguages(String languageDistributionValue) { this.languages = ImmutableList.copyOf(parseStringInt(languageDistributionValue).keySet()); return this; } public List<String> getLanguages() { return languages; } } public static class ProjectMeasures { private Project project; private Measures measures; public ProjectMeasures(Project project, Measures measures) { this.project = project; this.measures = measures; } public Project getProject() { return project; } public Measures getMeasures() { return measures; } } }