]> source.dussan.org Git - sonarqube.git/blob
2f930c25c1e010caa3eb54796b54c5c26bb5e27e
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.ce.task.projectanalysis.step;
21
22 import java.util.ArrayList;
23 import java.util.List;
24 import org.apache.commons.lang.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;
46
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;
50
51 /**
52  * Compute duplication data measures on files, based on the {@link DuplicationRepository}
53  */
54 public class PersistDuplicationDataStep implements ComputationStep {
55
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;
61
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);
69   }
70
71   @Override
72   public void execute(ComputationStep.Context context) {
73     boolean supportUpsert = dbClient.getDatabase().getDialect().supportsUpsert();
74
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);
85     }
86   }
87
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;
93
94     private DuplicationVisitor(DbSession dbSession, boolean supportUpsert) {
95       super(CrawlerDepthLimit.FILE, PRE_ORDER);
96       this.dbSession = dbSession;
97       this.supportUpsert = supportUpsert;
98     }
99
100     @Override
101     public void visitFile(Component file) {
102       Iterable<Duplication> duplications = duplicationRepository.getDuplications(file);
103       if (!isEmpty(duplications)) {
104         computeDuplications(file, duplications);
105       }
106     }
107
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);
112       persist(false);
113     }
114
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
118       // under control.
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) {
125         return;
126       }
127       if (supportUpsert) {
128         nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().upsert(dbSession, d));
129       } else {
130         nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, d));
131       }
132       insertsOrUpdates += nonPersistedBuffer.size();
133       nonPersistedBuffer.clear();
134       dbSession.commit();
135     }
136
137     @Override
138     public void close() {
139       // persist the measures remaining in the buffer
140       persist(true);
141     }
142
143     private Measure generateMeasure(String componentDbKey, Iterable<Duplication> duplications) {
144       StringBuilder xml = new StringBuilder();
145       xml.append("<duplications>");
146       for (Duplication duplication : duplications) {
147         xml.append("<g>");
148         appendDuplication(xml, componentDbKey, duplication.getOriginal(), false);
149         for (Duplicate duplicate : duplication.getDuplicates()) {
150           processDuplicationBlock(xml, duplicate, componentDbKey);
151         }
152         xml.append("</g>");
153       }
154       xml.append("</duplications>");
155       return Measure.newMeasureBuilder().create(xml.toString());
156     }
157
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);
172       } else {
173         throw new IllegalArgumentException("Unsupported type of Duplicate " + duplicate.getClass().getName());
174       }
175     }
176
177     private void appendDuplication(StringBuilder xml, String componentDbKey, Duplicate duplicate) {
178       appendDuplication(xml, componentDbKey, duplicate.getTextBlock(), false);
179     }
180
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))
187         .append("\"/>");
188     }
189   }
190
191   @Override
192   public String getDescription() {
193     return "Persist duplication data";
194   }
195
196 }