/* * SonarQube * Copyright (C) 2009-2024 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.ce.task.projectanalysis.step; import java.util.ArrayList; import java.util.List; import org.apache.commons.text.StringEscapeUtils; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit; import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter; import org.sonar.ce.task.projectanalysis.duplication.CrossProjectDuplicate; import org.sonar.ce.task.projectanalysis.duplication.Duplicate; import org.sonar.ce.task.projectanalysis.duplication.Duplication; import org.sonar.ce.task.projectanalysis.duplication.DuplicationRepository; import org.sonar.ce.task.projectanalysis.duplication.InExtendedProjectDuplicate; import org.sonar.ce.task.projectanalysis.duplication.InProjectDuplicate; import org.sonar.ce.task.projectanalysis.duplication.InnerDuplicate; import org.sonar.ce.task.projectanalysis.duplication.TextBlock; import org.sonar.ce.task.projectanalysis.measure.Measure; import org.sonar.ce.task.projectanalysis.measure.MeasureToMeasureDto; import org.sonar.ce.task.projectanalysis.metric.Metric; import org.sonar.ce.task.projectanalysis.metric.MetricRepository; import org.sonar.ce.task.step.ComputationStep; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.measure.LiveMeasureDto; import static com.google.common.collect.Iterables.isEmpty; import static org.sonar.api.measures.CoreMetrics.DUPLICATIONS_DATA_KEY; import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER; /** * Compute duplication data measures on files, based on the {@link DuplicationRepository} */ public class PersistDuplicationDataStep implements ComputationStep { private final DbClient dbClient; private final TreeRootHolder treeRootHolder; private final DuplicationRepository duplicationRepository; private final MeasureToMeasureDto measureToMeasureDto; private final Metric duplicationDataMetric; public PersistDuplicationDataStep(DbClient dbClient, TreeRootHolder treeRootHolder, MetricRepository metricRepository, DuplicationRepository duplicationRepository, MeasureToMeasureDto measureToMeasureDto) { this.dbClient = dbClient; this.treeRootHolder = treeRootHolder; this.duplicationRepository = duplicationRepository; this.measureToMeasureDto = measureToMeasureDto; this.duplicationDataMetric = metricRepository.getByKey(DUPLICATIONS_DATA_KEY); } @Override public void execute(ComputationStep.Context context) { boolean supportUpsert = dbClient.getDatabase().getDialect().supportsUpsert(); // batch mode of DB session does not have benefits: // - on postgres the multi-row upserts are the major optimization and have exactly the same // performance between batch and non-batch sessions // - on other dbs the sequence of inserts and updates, in order to emulate upserts, // breaks the constraint of batch sessions (consecutive requests should have the same // structure (same PreparedStatement)) try (DbSession dbSession = dbClient.openSession(false); DuplicationVisitor visitor = new DuplicationVisitor(dbSession, supportUpsert)) { new DepthTraversalTypeAwareCrawler(visitor).visit(treeRootHolder.getRoot()); context.getStatistics().add("insertsOrUpdates", visitor.insertsOrUpdates); } } private class DuplicationVisitor extends TypeAwareVisitorAdapter implements AutoCloseable { private final DbSession dbSession; private final boolean supportUpsert; private final List nonPersistedBuffer = new ArrayList<>(); private int insertsOrUpdates = 0; private DuplicationVisitor(DbSession dbSession, boolean supportUpsert) { super(CrawlerDepthLimit.FILE, PRE_ORDER); this.dbSession = dbSession; this.supportUpsert = supportUpsert; } @Override public void visitFile(Component file) { Iterable duplications = duplicationRepository.getDuplications(file); if (!isEmpty(duplications)) { computeDuplications(file, duplications); } } private void computeDuplications(Component component, Iterable duplications) { Measure measure = generateMeasure(component.getKey(), duplications); LiveMeasureDto dto = measureToMeasureDto.toLiveMeasureDto(measure, duplicationDataMetric, component); nonPersistedBuffer.add(dto); persist(false); } private void persist(boolean force) { // Persist a bunch of 100 or less measures. That prevents from having more than 100 XML documents // in memory. Consumption of memory does not explode with the number of duplications and is kept // under control. // Measures are upserted and transactions are committed every 100 rows (arbitrary number to // maximize the performance of a multi-rows request on PostgreSQL). // On PostgreSQL, a bunch of 100 measures is persisted into a single request (multi-rows upsert). // On other DBs, measures are persisted one by one, with update-or-insert requests. boolean shouldPersist = !nonPersistedBuffer.isEmpty() && (force || nonPersistedBuffer.size() > 100); if (!shouldPersist) { return; } if (supportUpsert) { nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().upsert(dbSession, d)); } else { nonPersistedBuffer.forEach(d -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, d)); } insertsOrUpdates += nonPersistedBuffer.size(); nonPersistedBuffer.clear(); dbSession.commit(); } @Override public void close() { // persist the measures remaining in the buffer persist(true); } private Measure generateMeasure(String componentDbKey, Iterable duplications) { StringBuilder xml = new StringBuilder(); xml.append(""); for (Duplication duplication : duplications) { xml.append(""); appendDuplication(xml, componentDbKey, duplication.getOriginal(), false); for (Duplicate duplicate : duplication.getDuplicates()) { processDuplicationBlock(xml, duplicate, componentDbKey); } xml.append(""); } xml.append(""); return Measure.newMeasureBuilder().create(xml.toString()); } private void processDuplicationBlock(StringBuilder xml, Duplicate duplicate, String componentDbKey) { if (duplicate instanceof InnerDuplicate) { // Duplication is on the same file appendDuplication(xml, componentDbKey, duplicate); } else if (duplicate instanceof InExtendedProjectDuplicate inExtendedProjectDuplicate) { // Duplication is on a different file that is not saved in the DB appendDuplication(xml, inExtendedProjectDuplicate.getFile().getKey(), duplicate.getTextBlock(), true); } else if (duplicate instanceof InProjectDuplicate inProjectDuplicate) { // Duplication is on a different file appendDuplication(xml, inProjectDuplicate.getFile().getKey(), duplicate); } else if (duplicate instanceof CrossProjectDuplicate crossProjectDuplicate) { // Only componentKey is set for cross project duplications String crossProjectComponentKey = crossProjectDuplicate.getFileKey(); appendDuplication(xml, crossProjectComponentKey, duplicate); } else { throw new IllegalArgumentException("Unsupported type of Duplicate " + duplicate.getClass().getName()); } } private void appendDuplication(StringBuilder xml, String componentDbKey, Duplicate duplicate) { appendDuplication(xml, componentDbKey, duplicate.getTextBlock(), false); } private void appendDuplication(StringBuilder xml, String componentDbKey, TextBlock textBlock, boolean disableLink) { int length = textBlock.getEnd() - textBlock.getStart() + 1; xml.append(""); } } @Override public String getDescription() { return "Persist duplication data"; } }