3 * Copyright (C) 2009-2021 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 java.util.stream.StreamSupport;
30 import javax.annotation.CheckForNull;
31 import javax.annotation.Nullable;
32 import org.apache.commons.lang.StringUtils;
33 import org.sonar.api.resources.Qualifiers;
34 import org.sonar.api.resources.Scopes;
35 import org.sonar.api.utils.System2;
36 import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
37 import org.sonar.ce.task.projectanalysis.component.BranchPersister;
38 import org.sonar.ce.task.projectanalysis.component.Component;
39 import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
40 import org.sonar.ce.task.projectanalysis.component.MutableDisabledComponentsHolder;
41 import org.sonar.ce.task.projectanalysis.component.PathAwareCrawler;
42 import org.sonar.ce.task.projectanalysis.component.PathAwareVisitor;
43 import org.sonar.ce.task.projectanalysis.component.PathAwareVisitorAdapter;
44 import org.sonar.ce.task.projectanalysis.component.ProjectPersister;
45 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
46 import org.sonar.ce.task.step.ComputationStep;
47 import org.sonar.core.util.stream.MoreCollectors;
48 import org.sonar.db.DbClient;
49 import org.sonar.db.DbSession;
50 import org.sonar.db.component.ComponentDto;
51 import org.sonar.db.component.ComponentUpdateDto;
53 import static java.util.Optional.ofNullable;
54 import static org.sonar.api.resources.Qualifiers.PROJECT;
55 import static org.sonar.api.resources.Qualifiers.VIEW;
56 import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
57 import static org.sonar.db.component.ComponentDto.UUID_PATH_OF_ROOT;
58 import static org.sonar.db.component.ComponentDto.UUID_PATH_SEPARATOR;
59 import static org.sonar.db.component.ComponentDto.formatUuidPathFromParent;
62 * Persist report components
64 public class PersistComponentsStep implements ComputationStep {
65 private final DbClient dbClient;
66 private final TreeRootHolder treeRootHolder;
67 private final System2 system2;
68 private final MutableDisabledComponentsHolder disabledComponentsHolder;
69 private final AnalysisMetadataHolder analysisMetadataHolder;
70 private final BranchPersister branchPersister;
71 private final ProjectPersister projectPersister;
73 public PersistComponentsStep(DbClient dbClient, TreeRootHolder treeRootHolder, System2 system2,
74 MutableDisabledComponentsHolder disabledComponentsHolder, AnalysisMetadataHolder analysisMetadataHolder,
75 BranchPersister branchPersister, ProjectPersister projectPersister) {
76 this.dbClient = dbClient;
77 this.treeRootHolder = treeRootHolder;
78 this.system2 = system2;
79 this.disabledComponentsHolder = disabledComponentsHolder;
80 this.analysisMetadataHolder = analysisMetadataHolder;
81 this.branchPersister = branchPersister;
82 this.projectPersister = projectPersister;
86 public String getDescription() {
87 return "Persist components";
91 public void execute(ComputationStep.Context context) {
92 try (DbSession dbSession = dbClient.openSession(false)) {
93 branchPersister.persist(dbSession);
94 projectPersister.persist(dbSession);
96 String projectUuid = treeRootHolder.getRoot().getUuid();
98 // safeguard, reset all rows to b-changed=false
99 dbClient.componentDao().resetBChangedForRootComponentUuid(dbSession, projectUuid);
101 Map<String, ComponentDto> existingDtosByUuids = indexExistingDtosByUuids(dbSession);
102 boolean isRootPrivate = isRootPrivate(treeRootHolder.getRoot(), existingDtosByUuids);
103 String mainBranchProjectUuid = loadProjectUuidOfMainBranch();
105 // Insert or update the components in database. They are removed from existingDtosByUuids
107 new PathAwareCrawler<>(new PersistComponentStepsVisitor(existingDtosByUuids, dbSession, mainBranchProjectUuid))
108 .visit(treeRootHolder.getRoot());
110 disableRemainingComponents(dbSession, existingDtosByUuids.values());
111 ensureConsistentVisibility(dbSession, projectUuid, isRootPrivate, treeRootHolder.getRoot().getType(), treeRootHolder.getRoot().getName());
118 * See {@link ComponentDto#mainBranchProjectUuid} : value is null on main branches, otherwise it is
119 * the uuid of the main branch.
122 private String loadProjectUuidOfMainBranch() {
123 if (!analysisMetadataHolder.getBranch().isMain()) {
124 return analysisMetadataHolder.getProject().getUuid();
129 private void disableRemainingComponents(DbSession dbSession, Collection<ComponentDto> dtos) {
130 Set<String> uuids = dtos.stream()
131 .filter(ComponentDto::isEnabled)
132 .map(ComponentDto::uuid)
133 .collect(MoreCollectors.toSet(dtos.size()));
134 dbClient.componentDao().updateBEnabledToFalse(dbSession, uuids);
135 disabledComponentsHolder.setUuids(uuids);
138 private void ensureConsistentVisibility(DbSession dbSession, String projectUuid, boolean isRootPrivate,
139 Component.Type type, String componentName) {
140 String qualifier = null;
141 if (type == Component.Type.PROJECT) {
143 } else if (type == Component.Type.VIEW) {
146 dbClient.componentDao().setPrivateForRootComponentUuid(dbSession, projectUuid, isRootPrivate,
147 "", qualifier, componentName, false);
150 private static boolean isRootPrivate(Component root, Map<String, ComponentDto> existingDtosByUuids) {
151 ComponentDto rootDto = existingDtosByUuids.get(root.getUuid());
152 if (rootDto == null) {
153 if (Component.Type.VIEW == root.getType()) {
156 throw new IllegalStateException(String.format("The project '%s' is not stored in the database, during a project analysis.", root.getDbKey()));
158 return rootDto.isPrivate();
162 * Returns a mutable map of the components currently persisted in database for the project, including
163 * disabled components.
165 private Map<String, ComponentDto> indexExistingDtosByUuids(DbSession session) {
166 return dbClient.componentDao().selectAllComponentsFromProjectKey(session, treeRootHolder.getRoot().getDbKey())
168 .collect(Collectors.toMap(ComponentDto::uuid, Function.identity()));
171 private class PersistComponentStepsVisitor extends PathAwareVisitorAdapter<ComponentDtoHolder> {
173 private final Map<String, ComponentDto> existingComponentDtosByUuids;
174 private final DbSession dbSession;
176 private final String mainBranchProjectUuid;
178 PersistComponentStepsVisitor(Map<String, ComponentDto> existingComponentDtosByUuids, DbSession dbSession, @Nullable String mainBranchProjectUuid) {
180 CrawlerDepthLimit.LEAVES,
182 new SimpleStackElementFactory<ComponentDtoHolder>() {
184 public ComponentDtoHolder createForAny(Component component) {
185 return new ComponentDtoHolder();
189 public ComponentDtoHolder createForFile(Component file) {
190 // no need to create holder for file since they are always leaves of the Component tree
195 public ComponentDtoHolder createForProjectView(Component projectView) {
196 // no need to create holder for file since they are always leaves of the Component tree
200 this.existingComponentDtosByUuids = existingComponentDtosByUuids;
201 this.dbSession = dbSession;
202 this.mainBranchProjectUuid = mainBranchProjectUuid;
206 public void visitProject(Component project, Path<ComponentDtoHolder> path) {
207 ComponentDto dto = createForProject(project);
208 path.current().setDto(persistAndPopulateCache(project, dto));
212 public void visitDirectory(Component directory, Path<ComponentDtoHolder> path) {
213 ComponentDto dto = createForDirectory(directory, path);
214 path.current().setDto(persistAndPopulateCache(directory, dto));
218 public void visitFile(Component file, Path<ComponentDtoHolder> path) {
219 ComponentDto dto = createForFile(file, path);
220 persistAndPopulateCache(file, dto);
224 public void visitView(Component view, Path<ComponentDtoHolder> path) {
225 ComponentDto dto = createForView(view);
226 path.current().setDto(persistAndPopulateCache(view, dto));
230 public void visitSubView(Component subView, Path<ComponentDtoHolder> path) {
231 ComponentDto dto = createForSubView(subView, path);
232 path.current().setDto(persistAndPopulateCache(subView, dto));
236 public void visitProjectView(Component projectView, Path<ComponentDtoHolder> path) {
237 ComponentDto dto = createForProjectView(projectView, path);
238 persistAndPopulateCache(projectView, dto);
241 private ComponentDto persistAndPopulateCache(Component component, ComponentDto dto) {
242 ComponentDto projectDto = persistComponent(dto);
246 private ComponentDto persistComponent(ComponentDto componentDto) {
247 ComponentDto existingComponent = existingComponentDtosByUuids.remove(componentDto.uuid());
248 if (existingComponent == null) {
249 dbClient.componentDao().insert(dbSession, componentDto);
252 Optional<ComponentUpdateDto> update = compareForUpdate(existingComponent, componentDto);
253 if (update.isPresent()) {
254 ComponentUpdateDto updateDto = update.get();
255 dbClient.componentDao().update(dbSession, updateDto, componentDto.qualifier());
257 // update the fields in memory in order the PathAwareVisitor.Path
259 existingComponent.setDbKey(updateDto.getBKey());
260 existingComponent.setCopyComponentUuid(updateDto.getBCopyComponentUuid());
261 existingComponent.setDescription(updateDto.getBDescription());
262 existingComponent.setEnabled(updateDto.isBEnabled());
263 existingComponent.setUuidPath(updateDto.getBUuidPath());
264 existingComponent.setLanguage(updateDto.getBLanguage());
265 existingComponent.setLongName(updateDto.getBLongName());
266 existingComponent.setModuleUuid(updateDto.getBModuleUuid());
267 existingComponent.setModuleUuidPath(updateDto.getBModuleUuidPath());
268 existingComponent.setName(updateDto.getBName());
269 existingComponent.setPath(updateDto.getBPath());
270 // We don't have a b_scope. The applyBChangesForRootComponentUuid query is using a case ... when to infer scope from the qualifier
271 existingComponent.setScope(componentDto.scope());
272 existingComponent.setQualifier(updateDto.getBQualifier());
274 return existingComponent;
277 public ComponentDto createForProject(Component project) {
278 ComponentDto res = createBase(project);
280 res.setScope(Scopes.PROJECT);
281 res.setQualifier(PROJECT);
282 res.setName(project.getName());
283 res.setLongName(res.name());
284 res.setDescription(project.getDescription());
286 res.setProjectUuid(res.uuid());
287 res.setRootUuid(res.uuid());
288 res.setUuidPath(UUID_PATH_OF_ROOT);
289 res.setModuleUuidPath(UUID_PATH_SEPARATOR + res.uuid() + UUID_PATH_SEPARATOR);
294 public ComponentDto createForDirectory(Component directory, PathAwareVisitor.Path<ComponentDtoHolder> path) {
295 ComponentDto res = createBase(directory);
297 res.setScope(Scopes.DIRECTORY);
298 res.setQualifier(Qualifiers.DIRECTORY);
299 res.setName(directory.getShortName());
300 res.setLongName(directory.getName());
301 res.setPath(directory.getName());
303 setParentModuleProperties(res, path);
308 public ComponentDto createForFile(Component file, PathAwareVisitor.Path<ComponentDtoHolder> path) {
309 ComponentDto res = createBase(file);
311 res.setScope(Scopes.FILE);
312 res.setQualifier(getFileQualifier(file));
313 res.setName(file.getShortName());
314 res.setLongName(file.getName());
315 res.setPath(file.getName());
316 res.setLanguage(file.getFileAttributes().getLanguageKey());
318 setParentModuleProperties(res, path);
323 private ComponentDto createForView(Component view) {
324 ComponentDto res = createBase(view);
326 res.setScope(Scopes.PROJECT);
327 res.setQualifier(view.getViewAttributes().getType().getQualifier());
328 res.setName(view.getName());
329 res.setDescription(view.getDescription());
330 res.setLongName(res.name());
332 res.setProjectUuid(res.uuid());
333 res.setRootUuid(res.uuid());
334 res.setUuidPath(UUID_PATH_OF_ROOT);
335 res.setModuleUuidPath(UUID_PATH_SEPARATOR + res.uuid() + UUID_PATH_SEPARATOR);
340 private ComponentDto createForSubView(Component subView, PathAwareVisitor.Path<ComponentDtoHolder> path) {
341 ComponentDto res = createBase(subView);
343 res.setScope(Scopes.PROJECT);
344 res.setQualifier(Qualifiers.SUBVIEW);
345 res.setName(subView.getName());
346 res.setDescription(subView.getDescription());
347 res.setLongName(res.name());
348 res.setCopyComponentUuid(subView.getSubViewAttributes().getOriginalViewUuid());
350 setRootAndParentModule(res, path);
355 private ComponentDto createForProjectView(Component projectView, PathAwareVisitor.Path<ComponentDtoHolder> path) {
356 ComponentDto res = createBase(projectView);
358 res.setScope(Scopes.FILE);
359 res.setQualifier(PROJECT);
360 res.setName(projectView.getName());
361 res.setLongName(res.name());
362 res.setCopyComponentUuid(projectView.getProjectViewAttributes().getProjectUuid());
364 setRootAndParentModule(res, path);
369 private ComponentDto createBase(Component component) {
370 String componentKey = component.getDbKey();
371 String componentUuid = component.getUuid();
373 ComponentDto componentDto = new ComponentDto();
374 componentDto.setUuid(componentUuid);
375 componentDto.setDbKey(componentKey);
376 componentDto.setMainBranchProjectUuid(mainBranchProjectUuid);
377 componentDto.setEnabled(true);
378 componentDto.setCreatedAt(new Date(system2.now()));
384 * Applies to a node of type either MODULE, SUBVIEW, PROJECT_VIEW
386 private void setRootAndParentModule(ComponentDto res, PathAwareVisitor.Path<ComponentDtoHolder> path) {
387 ComponentDto rootDto = path.root().getDto();
388 res.setRootUuid(rootDto.uuid());
389 res.setProjectUuid(rootDto.uuid());
391 ComponentDto parentModule = path.parent().getDto();
392 res.setUuidPath(formatUuidPathFromParent(parentModule));
393 res.setModuleUuid(parentModule.uuid());
394 res.setModuleUuidPath(parentModule.moduleUuidPath() + res.uuid() + UUID_PATH_SEPARATOR);
399 * Applies to a node of type either DIRECTORY or FILE
401 private static void setParentModuleProperties(ComponentDto componentDto, PathAwareVisitor.Path<ComponentDtoHolder> path) {
402 componentDto.setProjectUuid(path.root().getDto().uuid());
404 ComponentDto parentModule = StreamSupport.stream(path.getCurrentPath().spliterator(), false)
405 .filter(p -> p.getComponent().getType() == Component.Type.PROJECT)
408 .getElement().getDto();
409 componentDto.setUuidPath(formatUuidPathFromParent(path.parent().getDto()));
410 componentDto.setRootUuid(parentModule.uuid());
411 componentDto.setModuleUuid(parentModule.uuid());
412 componentDto.setModuleUuidPath(parentModule.moduleUuidPath());
416 private static Optional<ComponentUpdateDto> compareForUpdate(ComponentDto existing, ComponentDto target) {
417 boolean hasDifferences = !StringUtils.equals(existing.getCopyResourceUuid(), target.getCopyResourceUuid()) ||
418 !StringUtils.equals(existing.description(), target.description()) ||
419 !StringUtils.equals(existing.getDbKey(), target.getDbKey()) ||
420 !existing.isEnabled() ||
421 !StringUtils.equals(existing.getUuidPath(), target.getUuidPath()) ||
422 !StringUtils.equals(existing.language(), target.language()) ||
423 !StringUtils.equals(existing.longName(), target.longName()) ||
424 !StringUtils.equals(existing.moduleUuid(), target.moduleUuid()) ||
425 !StringUtils.equals(existing.moduleUuidPath(), target.moduleUuidPath()) ||
426 !StringUtils.equals(existing.name(), target.name()) ||
427 !StringUtils.equals(existing.path(), target.path()) ||
428 !StringUtils.equals(existing.scope(), target.scope()) ||
429 !StringUtils.equals(existing.qualifier(), target.qualifier());
431 ComponentUpdateDto update = null;
432 if (hasDifferences) {
433 update = ComponentUpdateDto
437 return ofNullable(update);
440 private static String getFileQualifier(Component component) {
441 return component.getFileAttributes().isUnitTest() ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE;
444 private static class ComponentDtoHolder {
445 private ComponentDto dto;
447 public ComponentDto getDto() {
451 public void setDto(ComponentDto dto) {