3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.step;
22 import java.util.ArrayList;
23 import java.util.List;
24 import org.apache.commons.lang3.StringEscapeUtils;
25 import org.sonar.ce.task.projectanalysis.component.Component;
26 import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
27 import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
28 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
29 import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
30 import org.sonar.ce.task.projectanalysis.duplication.CrossProjectDuplicate;
31 import org.sonar.ce.task.projectanalysis.duplication.Duplicate;
32 import org.sonar.ce.task.projectanalysis.duplication.Duplication;
33 import org.sonar.ce.task.projectanalysis.duplication.DuplicationRepository;
34 import org.sonar.ce.task.projectanalysis.duplication.InExtendedProjectDuplicate;
35 import org.sonar.ce.task.projectanalysis.duplication.InProjectDuplicate;
36 import org.sonar.ce.task.projectanalysis.duplication.InnerDuplicate;
37 import org.sonar.ce.task.projectanalysis.duplication.TextBlock;
38 import org.sonar.ce.task.projectanalysis.measure.Measure;
39 import org.sonar.ce.task.projectanalysis.measure.MeasureToMeasureDto;
40 import org.sonar.ce.task.projectanalysis.metric.Metric;
41 import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
42 import org.sonar.ce.task.step.ComputationStep;
43 import org.sonar.db.DbClient;
44 import org.sonar.db.DbSession;
45 import org.sonar.db.measure.LiveMeasureDto;
47 import static com.google.common.collect.Iterables.isEmpty;
48 import static org.sonar.api.measures.CoreMetrics.DUPLICATIONS_DATA_KEY;
49 import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
52 * Compute duplication data measures on files, based on the {@link DuplicationRepository}
54 public class PersistDuplicationDataStep implements ComputationStep {
56 private final DbClient dbClient;
57 private final TreeRootHolder treeRootHolder;
58 private final DuplicationRepository duplicationRepository;
59 private final MeasureToMeasureDto measureToMeasureDto;
60 private final Metric duplicationDataMetric;
62 public PersistDuplicationDataStep(DbClient dbClient, TreeRootHolder treeRootHolder, MetricRepository metricRepository,
63 DuplicationRepository duplicationRepository, MeasureToMeasureDto measureToMeasureDto) {
64 this.dbClient = dbClient;
65 this.treeRootHolder = treeRootHolder;
66 this.duplicationRepository = duplicationRepository;
67 this.measureToMeasureDto = measureToMeasureDto;
68 this.duplicationDataMetric = metricRepository.getByKey(DUPLICATIONS_DATA_KEY);
72 public void execute(ComputationStep.Context context) {
73 boolean supportUpsert = dbClient.getDatabase().getDialect().supportsUpsert();
75 // batch mode of DB session does not have benefits:
76 // - on postgres the multi-row upserts are the major optimization and have exactly the same
77 // performance between batch and non-batch sessions
78 // - on other dbs the sequence of inserts and updates, in order to emulate upserts,
79 // breaks the constraint of batch sessions (consecutive requests should have the same
80 // structure (same PreparedStatement))
81 try (DbSession dbSession = dbClient.openSession(false);
82 DuplicationVisitor visitor = new DuplicationVisitor(dbSession, supportUpsert)) {
83 new DepthTraversalTypeAwareCrawler(visitor).visit(treeRootHolder.getRoot());
84 context.getStatistics().add("insertsOrUpdates", visitor.insertsOrUpdates);
88 private class DuplicationVisitor extends TypeAwareVisitorAdapter implements AutoCloseable {
89 private final DbSession dbSession;
90 private final boolean supportUpsert;
91 private final List<LiveMeasureDto> nonPersistedBuffer = new ArrayList<>();
92 private int insertsOrUpdates = 0;
94 private DuplicationVisitor(DbSession dbSession, boolean supportUpsert) {
95 super(CrawlerDepthLimit.FILE, PRE_ORDER);
96 this.dbSession = dbSession;
97 this.supportUpsert = supportUpsert;
101 public void visitFile(Component file) {
102 Iterable<Duplication> duplications = duplicationRepository.getDuplications(file);
103 if (!isEmpty(duplications)) {
104 computeDuplications(file, duplications);
108 private void computeDuplications(Component component, Iterable<Duplication> duplications) {
109 Measure measure = generateMeasure(component.getKey(), duplications);
110 LiveMeasureDto dto = measureToMeasureDto.toLiveMeasureDto(measure, duplicationDataMetric, component);
111 nonPersistedBuffer.add(dto);
115 private void persist(boolean force) {
116 // Persist a bunch of 100 or less measures. That prevents from having more than 100 XML documents
117 // in memory. Consumption of memory does not explode with the number of duplications and is kept
119 // Measures are upserted and transactions are committed every 100 rows (arbitrary number to
120 // maximize the performance of a multi-rows request on PostgreSQL).
121 // On PostgreSQL, a bunch of 100 measures is persisted into a single request (multi-rows upsert).
122 // On other DBs, measures are persisted one by one, with update-or-insert requests.
123 boolean shouldPersist = !nonPersistedBuffer.isEmpty() && (force || nonPersistedBuffer.size() > 100);
124 if (!shouldPersist) {
128 nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().upsert(dbSession, d));
130 nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, d));
132 insertsOrUpdates += nonPersistedBuffer.size();
133 nonPersistedBuffer.clear();
138 public void close() {
139 // persist the measures remaining in the buffer
143 private Measure generateMeasure(String componentDbKey, Iterable<Duplication> duplications) {
144 StringBuilder xml = new StringBuilder();
145 xml.append("<duplications>");
146 for (Duplication duplication : duplications) {
148 appendDuplication(xml, componentDbKey, duplication.getOriginal(), false);
149 for (Duplicate duplicate : duplication.getDuplicates()) {
150 processDuplicationBlock(xml, duplicate, componentDbKey);
154 xml.append("</duplications>");
155 return Measure.newMeasureBuilder().create(xml.toString());
158 private void processDuplicationBlock(StringBuilder xml, Duplicate duplicate, String componentDbKey) {
159 if (duplicate instanceof InnerDuplicate) {
160 // Duplication is on the same file
161 appendDuplication(xml, componentDbKey, duplicate);
162 } else if (duplicate instanceof InExtendedProjectDuplicate inExtendedProjectDuplicate) {
163 // Duplication is on a different file that is not saved in the DB
164 appendDuplication(xml, inExtendedProjectDuplicate.getFile().getKey(), duplicate.getTextBlock(), true);
165 } else if (duplicate instanceof InProjectDuplicate inProjectDuplicate) {
166 // Duplication is on a different file
167 appendDuplication(xml, inProjectDuplicate.getFile().getKey(), duplicate);
168 } else if (duplicate instanceof CrossProjectDuplicate crossProjectDuplicate) {
169 // Only componentKey is set for cross project duplications
170 String crossProjectComponentKey = crossProjectDuplicate.getFileKey();
171 appendDuplication(xml, crossProjectComponentKey, duplicate);
173 throw new IllegalArgumentException("Unsupported type of Duplicate " + duplicate.getClass().getName());
177 private void appendDuplication(StringBuilder xml, String componentDbKey, Duplicate duplicate) {
178 appendDuplication(xml, componentDbKey, duplicate.getTextBlock(), false);
181 private void appendDuplication(StringBuilder xml, String componentDbKey, TextBlock textBlock, boolean disableLink) {
182 int length = textBlock.getEnd() - textBlock.getStart() + 1;
183 xml.append("<b s=\"").append(textBlock.getStart())
184 .append("\" l=\"").append(length)
185 .append("\" t=\"").append(disableLink)
186 .append("\" r=\"").append(StringEscapeUtils.escapeXml(componentDbKey))
192 public String getDescription() {
193 return "Persist duplication data";