3 * Copyright (C) 2009-2023 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.Collection;
23 import java.util.Date;
25 import java.util.Optional;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
29 import javax.annotation.CheckForNull;
30 import javax.annotation.Nullable;
31 import org.apache.commons.lang.StringUtils;
32 import org.sonar.api.resources.Qualifiers;
33 import org.sonar.api.resources.Scopes;
34 import org.sonar.api.utils.System2;
35 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
36 import org.sonar.ce.task.projectanalysis.component.BranchPersister;
37 import org.sonar.ce.task.projectanalysis.component.Component;
38 import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
39 import org.sonar.ce.task.projectanalysis.component.MutableDisabledComponentsHolder;
40 import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
41 import org.sonar.ce.task.projectanalysis.component.PathAwareVisitor;
42 import org.sonar.ce.task.projectanalysis.component.PathAwareVisitorAdapter;
43 import org.sonar.ce.task.projectanalysis.component.ProjectPersister;
44 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
45 import org.sonar.ce.task.step.ComputationStep;
46 import org.sonar.core.util.stream.MoreCollectors;
47 import org.sonar.db.DbClient;
48 import org.sonar.db.DbSession;
49 import org.sonar.db.component.ComponentDto;
50 import org.sonar.db.component.ComponentUpdateDto;
52 import static java.util.Optional.ofNullable;
53 import static org.sonar.api.resources.Qualifiers.PROJECT;
54 import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
55 import static org.sonar.db.component.ComponentDto.UUID_PATH_OF_ROOT;
56 import static org.sonar.db.component.ComponentDto.formatUuidPathFromParent;
59 * Persist report components
61 public class PersistComponentsStep implements ComputationStep {
62 private final DbClient dbClient;
63 private final TreeRootHolder treeRootHolder;
64 private final System2 system2;
65 private final MutableDisabledComponentsHolder disabledComponentsHolder;
66 private final AnalysisMetadataHolder analysisMetadataHolder;
67 private final BranchPersister branchPersister;
68 private final ProjectPersister projectPersister;
70 public PersistComponentsStep(DbClient dbClient, TreeRootHolder treeRootHolder, System2 system2,
71 MutableDisabledComponentsHolder disabledComponentsHolder, AnalysisMetadataHolder analysisMetadataHolder,
72 BranchPersister branchPersister, ProjectPersister projectPersister) {
73 this.dbClient = dbClient;
74 this.treeRootHolder = treeRootHolder;
75 this.system2 = system2;
76 this.disabledComponentsHolder = disabledComponentsHolder;
77 this.analysisMetadataHolder = analysisMetadataHolder;
78 this.branchPersister = branchPersister;
79 this.projectPersister = projectPersister;
83 public String getDescription() {
84 return "Persist components";
88 public void execute(ComputationStep.Context context) {
89 try (DbSession dbSession = dbClient.openSession(false)) {
90 branchPersister.persist(dbSession);
91 projectPersister.persist(dbSession);
93 String projectUuid = treeRootHolder.getRoot().getUuid();
95 // safeguard, reset all rows to b-changed=false
96 dbClient.componentDao().resetBChangedForBranchUuid(dbSession, projectUuid);
98 Map<String, ComponentDto> existingDtosByUuids = indexExistingDtosByUuids(dbSession);
99 boolean isRootPrivate = isRootPrivate(treeRootHolder.getRoot(), existingDtosByUuids);
101 // Insert or update the components in database. They are removed from existingDtosByUuids
103 new PathAwareCrawler<>(new PersistComponentStepsVisitor(existingDtosByUuids, dbSession))
104 .visit(treeRootHolder.getRoot());
106 disableRemainingComponents(dbSession, existingDtosByUuids.values());
107 dbClient.componentDao().setPrivateForBranchUuidWithoutAudit(dbSession, projectUuid, isRootPrivate);
112 private void disableRemainingComponents(DbSession dbSession, Collection<ComponentDto> dtos) {
113 Set<String> uuids = dtos.stream()
114 .filter(ComponentDto::isEnabled)
115 .map(ComponentDto::uuid)
116 .collect(MoreCollectors.toSet(dtos.size()));
117 dbClient.componentDao().updateBEnabledToFalse(dbSession, uuids);
118 disabledComponentsHolder.setUuids(uuids);
121 private static boolean isRootPrivate(Component root, Map<String, ComponentDto> existingDtosByUuids) {
122 ComponentDto rootDto = existingDtosByUuids.get(root.getUuid());
123 if (rootDto == null) {
124 if (Component.Type.VIEW == root.getType()) {
127 throw new IllegalStateException(String.format("The project '%s' is not stored in the database, during a project analysis.", root.getKey()));
129 return rootDto.isPrivate();
133 * Returns a mutable map of the components currently persisted in database for the project, including
134 * disabled components.
136 private Map<String, ComponentDto> indexExistingDtosByUuids(DbSession session) {
137 return dbClient.componentDao().selectByBranchUuid(treeRootHolder.getRoot().getUuid(), session)
139 .collect(Collectors.toMap(ComponentDto::uuid, Function.identity()));
142 private class PersistComponentStepsVisitor extends PathAwareVisitorAdapter<ComponentDtoHolder> {
144 private final Map<String, ComponentDto> existingComponentDtosByUuids;
145 private final DbSession dbSession;
147 PersistComponentStepsVisitor(Map<String, ComponentDto> existingComponentDtosByUuids, DbSession dbSession) {
149 CrawlerDepthLimit.LEAVES,
151 new SimpleStackElementFactory<>() {
153 public ComponentDtoHolder createForAny(Component component) {
154 return new ComponentDtoHolder();
158 public ComponentDtoHolder createForFile(Component file) {
159 // no need to create holder for file since they are always leaves of the Component tree
164 public ComponentDtoHolder createForProjectView(Component projectView) {
165 // no need to create holder for file since they are always leaves of the Component tree
169 this.existingComponentDtosByUuids = existingComponentDtosByUuids;
170 this.dbSession = dbSession;
174 public void visitProject(Component project, Path<ComponentDtoHolder> path) {
175 ComponentDto dto = createForProject(project);
176 path.current().setDto(persistComponent(dto));
180 public void visitDirectory(Component directory, Path<ComponentDtoHolder> path) {
181 ComponentDto dto = createForDirectory(directory, path);
182 path.current().setDto(persistComponent(dto));
186 public void visitFile(Component file, Path<ComponentDtoHolder> path) {
187 ComponentDto dto = createForFile(file, path);
188 persistComponent(dto);
192 public void visitView(Component view, Path<ComponentDtoHolder> path) {
193 ComponentDto dto = createForView(view);
194 path.current().setDto(persistComponent(dto));
198 public void visitSubView(Component subView, Path<ComponentDtoHolder> path) {
199 ComponentDto dto = createForSubView(subView, path);
200 path.current().setDto(persistComponent(dto));
204 public void visitProjectView(Component projectView, Path<ComponentDtoHolder> path) {
205 ComponentDto dto = createForProjectView(projectView, path);
206 persistComponent(dto);
209 private ComponentDto persistComponent(ComponentDto componentDto) {
210 ComponentDto existingComponent = existingComponentDtosByUuids.remove(componentDto.uuid());
211 if (existingComponent == null) {
212 if (componentDto.qualifier().equals("APP") && componentDto.scope().equals("PRJ")) {
213 throw new IllegalStateException("Application should already exists: " + componentDto);
215 dbClient.componentDao().insert(dbSession, componentDto, analysisMetadataHolder.getBranch().isMain());
218 Optional<ComponentUpdateDto> update = compareForUpdate(existingComponent, componentDto);
219 if (update.isPresent()) {
220 ComponentUpdateDto updateDto = update.get();
221 dbClient.componentDao().update(dbSession, updateDto, componentDto.qualifier());
223 // update the fields in memory in order the PathAwareVisitor.Path
225 existingComponent.setKey(updateDto.getBKey());
226 existingComponent.setCopyComponentUuid(updateDto.getBCopyComponentUuid());
227 existingComponent.setDescription(updateDto.getBDescription());
228 existingComponent.setEnabled(updateDto.isBEnabled());
229 existingComponent.setUuidPath(updateDto.getBUuidPath());
230 existingComponent.setLanguage(updateDto.getBLanguage());
231 existingComponent.setLongName(updateDto.getBLongName());
232 existingComponent.setName(updateDto.getBName());
233 existingComponent.setPath(updateDto.getBPath());
234 // We don't have a b_scope. The applyBChangesForRootComponentUuid query is using a case ... when to infer scope from the qualifier
235 existingComponent.setScope(componentDto.scope());
236 existingComponent.setQualifier(updateDto.getBQualifier());
238 return existingComponent;
241 public ComponentDto createForProject(Component project) {
242 ComponentDto res = createBase(project);
244 res.setScope(Scopes.PROJECT);
245 res.setQualifier(PROJECT);
246 res.setName(project.getName());
247 res.setLongName(res.name());
248 res.setDescription(project.getDescription());
250 res.setBranchUuid(res.uuid());
251 res.setUuidPath(UUID_PATH_OF_ROOT);
256 public ComponentDto createForDirectory(Component directory, PathAwareVisitor.Path<ComponentDtoHolder> path) {
257 ComponentDto res = createBase(directory);
259 res.setScope(Scopes.DIRECTORY);
260 res.setQualifier(Qualifiers.DIRECTORY);
261 res.setName(directory.getShortName());
262 res.setLongName(directory.getName());
263 res.setPath(directory.getName());
270 public ComponentDto createForFile(Component file, PathAwareVisitor.Path<ComponentDtoHolder> path) {
271 ComponentDto res = createBase(file);
273 res.setScope(Scopes.FILE);
274 res.setQualifier(getFileQualifier(file));
275 res.setName(file.getShortName());
276 res.setLongName(file.getName());
277 res.setPath(file.getName());
278 res.setLanguage(file.getFileAttributes().getLanguageKey());
285 private ComponentDto createForView(Component view) {
286 ComponentDto res = createBase(view);
288 res.setScope(Scopes.PROJECT);
289 res.setQualifier(view.getViewAttributes().getType().getQualifier());
290 res.setName(view.getName());
291 res.setDescription(view.getDescription());
292 res.setLongName(res.name());
294 res.setBranchUuid(res.uuid());
295 res.setUuidPath(UUID_PATH_OF_ROOT);
300 private ComponentDto createForSubView(Component subView, PathAwareVisitor.Path<ComponentDtoHolder> path) {
301 ComponentDto res = createBase(subView);
303 res.setScope(Scopes.PROJECT);
304 res.setQualifier(Qualifiers.SUBVIEW);
305 res.setName(subView.getName());
306 res.setDescription(subView.getDescription());
307 res.setLongName(res.name());
308 res.setCopyComponentUuid(subView.getSubViewAttributes().getOriginalViewUuid());
310 setRootAndParentModule(res, path);
315 private ComponentDto createForProjectView(Component projectView, PathAwareVisitor.Path<ComponentDtoHolder> path) {
316 ComponentDto res = createBase(projectView);
318 res.setScope(Scopes.FILE);
319 res.setQualifier(PROJECT);
320 res.setName(projectView.getName());
321 res.setLongName(res.name());
322 res.setCopyComponentUuid(projectView.getProjectViewAttributes().getUuid());
324 setRootAndParentModule(res, path);
329 private ComponentDto createBase(Component component) {
330 String componentKey = component.getKey();
331 String componentUuid = component.getUuid();
333 ComponentDto componentDto = new ComponentDto();
334 componentDto.setUuid(componentUuid);
335 componentDto.setKey(componentKey);
336 componentDto.setEnabled(true);
337 componentDto.setCreatedAt(new Date(system2.now()));
343 * Applies to a node of type either SUBVIEW or PROJECT_VIEW
345 private void setRootAndParentModule(ComponentDto res, PathAwareVisitor.Path<ComponentDtoHolder> path) {
346 ComponentDto rootDto = path.root().getDto();
347 res.setBranchUuid(rootDto.uuid());
349 ComponentDto parent = path.parent().getDto();
350 res.setUuidPath(formatUuidPathFromParent(parent));
355 * Applies to a node of type either DIRECTORY or FILE
357 private static void setUuids(ComponentDto componentDto, PathAwareVisitor.Path<ComponentDtoHolder> path) {
358 componentDto.setBranchUuid(path.root().getDto().uuid());
359 componentDto.setUuidPath(formatUuidPathFromParent(path.parent().getDto()));
362 private static Optional<ComponentUpdateDto> compareForUpdate(ComponentDto existing, ComponentDto target) {
363 boolean hasDifferences = !StringUtils.equals(existing.getCopyComponentUuid(), target.getCopyComponentUuid()) ||
364 !StringUtils.equals(existing.description(), target.description()) ||
365 !StringUtils.equals(existing.getKey(), target.getKey()) ||
366 !existing.isEnabled() ||
367 !StringUtils.equals(existing.getUuidPath(), target.getUuidPath()) ||
368 !StringUtils.equals(existing.language(), target.language()) ||
369 !StringUtils.equals(existing.longName(), target.longName()) ||
370 !StringUtils.equals(existing.name(), target.name()) ||
371 !StringUtils.equals(existing.path(), target.path()) ||
372 !StringUtils.equals(existing.scope(), target.scope()) ||
373 !StringUtils.equals(existing.qualifier(), target.qualifier());
375 ComponentUpdateDto update = null;
376 if (hasDifferences) {
377 update = ComponentUpdateDto
381 return ofNullable(update);
384 private static String getFileQualifier(Component component) {
385 return component.getFileAttributes().isUnitTest() ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE;
388 private static class ComponentDtoHolder {
389 private ComponentDto dto;
391 public ComponentDto getDto() {
395 public void setDto(ComponentDto dto) {