Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

LoadPeriodsStep.java 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2019 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. import java.time.Duration;
  22. import java.time.Instant;
  23. import java.time.LocalDate;
  24. import java.time.ZoneId;
  25. import java.time.format.DateTimeParseException;
  26. import java.time.temporal.ChronoUnit;
  27. import java.util.Arrays;
  28. import java.util.List;
  29. import java.util.Optional;
  30. import java.util.function.Supplier;
  31. import javax.annotation.CheckForNull;
  32. import javax.annotation.Nullable;
  33. import org.sonar.api.utils.DateUtils;
  34. import org.sonar.api.utils.MessageException;
  35. import org.sonar.api.utils.System2;
  36. import org.sonar.api.utils.log.Logger;
  37. import org.sonar.api.utils.log.Loggers;
  38. import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
  39. import org.sonar.ce.task.projectanalysis.analysis.Branch;
  40. import org.sonar.ce.task.projectanalysis.component.Component;
  41. import org.sonar.ce.task.projectanalysis.component.ConfigurationRepository;
  42. import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
  43. import org.sonar.ce.task.projectanalysis.period.Period;
  44. import org.sonar.ce.task.projectanalysis.period.PeriodHolder;
  45. import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl;
  46. import org.sonar.ce.task.step.ComputationStep;
  47. import org.sonar.db.DbClient;
  48. import org.sonar.db.DbSession;
  49. import org.sonar.db.component.BranchDto;
  50. import org.sonar.db.component.BranchType;
  51. import org.sonar.db.component.SnapshotDto;
  52. import org.sonar.db.component.SnapshotQuery;
  53. import org.sonar.db.event.EventDto;
  54. import static com.google.common.base.Preconditions.checkState;
  55. import static java.lang.String.format;
  56. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD;
  57. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DATE;
  58. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_DAYS;
  59. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_MANUAL_BASELINE;
  60. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_PREVIOUS_VERSION;
  61. import static org.sonar.core.config.CorePropertyDefinitions.LEAK_PERIOD_MODE_VERSION;
  62. import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
  63. import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE;
  64. import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.ASC;
  65. /**
  66. * Populates the {@link PeriodHolder}
  67. * <p/>
  68. * Here is how these periods are computed :
  69. * - Read the period property ${@link org.sonar.core.config.CorePropertyDefinitions#LEAK_PERIOD}
  70. * - Try to find the matching snapshots from the property
  71. * - If a snapshot is found, a period is set to the repository, otherwise fail with MessageException
  72. */
  73. public class LoadPeriodsStep implements ComputationStep {
  74. private static final Logger LOG = Loggers.get(LoadPeriodsStep.class);
  75. private final AnalysisMetadataHolder analysisMetadataHolder;
  76. private final TreeRootHolder treeRootHolder;
  77. private final PeriodHolderImpl periodsHolder;
  78. private final System2 system2;
  79. private final DbClient dbClient;
  80. private final ConfigurationRepository configRepository;
  81. public LoadPeriodsStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder treeRootHolder, PeriodHolderImpl periodsHolder,
  82. System2 system2, DbClient dbClient, ConfigurationRepository configRepository) {
  83. this.analysisMetadataHolder = analysisMetadataHolder;
  84. this.treeRootHolder = treeRootHolder;
  85. this.periodsHolder = periodsHolder;
  86. this.system2 = system2;
  87. this.dbClient = dbClient;
  88. this.configRepository = configRepository;
  89. }
  90. @Override
  91. public String getDescription() {
  92. return "Load new code period";
  93. }
  94. @Override
  95. public void execute(ComputationStep.Context context) {
  96. if (analysisMetadataHolder.isFirstAnalysis() || !analysisMetadataHolder.isLongLivingBranch()) {
  97. periodsHolder.setPeriod(null);
  98. return;
  99. }
  100. periodsHolder.setPeriod(resolvePeriod(treeRootHolder.getRoot()).orElse(null));
  101. }
  102. private Optional<Period> resolvePeriod(Component projectOrView) {
  103. String currentVersion = projectOrView.getProjectAttributes().getProjectVersion();
  104. Optional<String> propertyValue = configRepository.getConfiguration().get(LEAK_PERIOD)
  105. .filter(t -> !t.isEmpty());
  106. checkPeriodProperty(propertyValue.isPresent(), "", "property is undefined or value is empty");
  107. try (DbSession dbSession = dbClient.openSession(false)) {
  108. Optional<Period> manualBaselineOpt = resolveByManualBaseline(dbSession, projectOrView.getUuid());
  109. if (manualBaselineOpt.isPresent()) {
  110. return manualBaselineOpt;
  111. }
  112. return resolve(dbSession, projectOrView.getUuid(), currentVersion, propertyValue.get());
  113. }
  114. }
  115. private Optional<Period> resolveByManualBaseline(DbSession dbSession, String projectUuid) {
  116. Branch branch = analysisMetadataHolder.getBranch();
  117. if (branch.getType() != BranchType.LONG) {
  118. return Optional.empty();
  119. }
  120. return dbClient.branchDao().selectByUuid(dbSession, projectUuid)
  121. .map(branchDto -> resolveByManualBaseline(dbSession, projectUuid, branchDto));
  122. }
  123. private Period resolveByManualBaseline(DbSession dbSession, String projectUuid, BranchDto branchDto) {
  124. String baselineAnalysisUuid = branchDto.getManualBaseline();
  125. if (baselineAnalysisUuid == null) {
  126. return null;
  127. }
  128. LOG.debug("Resolving new code period by manual baseline");
  129. SnapshotDto baseline = dbClient.snapshotDao().selectByUuid(dbSession, baselineAnalysisUuid)
  130. .filter(t -> t.getComponentUuid().equals(projectUuid))
  131. .orElseThrow(() -> new IllegalStateException("Analysis '" + baselineAnalysisUuid + "' of project '" + projectUuid
  132. + "' defined as manual baseline does not exist"));
  133. return newPeriod(LEAK_PERIOD_MODE_MANUAL_BASELINE, null, baseline);
  134. }
  135. private Optional<Period> resolve(DbSession dbSession, String projectUuid, String analysisProjectVersion, String propertyValue) {
  136. Integer days = parseDaysQuietly(propertyValue);
  137. if (days != null) {
  138. return resolveByDays(dbSession, projectUuid, days, propertyValue);
  139. }
  140. Instant date = parseDate(propertyValue);
  141. if (date != null) {
  142. return resolveByDate(dbSession, projectUuid, date, propertyValue);
  143. }
  144. List<EventDto> versions = dbClient.eventDao().selectVersionsByMostRecentFirst(dbSession, projectUuid);
  145. if (versions.isEmpty()) {
  146. return resolveWhenNoExistingVersion(dbSession, projectUuid, analysisProjectVersion, propertyValue);
  147. }
  148. String mostRecentVersion = Optional.ofNullable(versions.iterator().next().getName())
  149. .orElseThrow(() -> new IllegalStateException("selectVersionsByMostRecentFirst returned a DTO which didn't have a name"));
  150. boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue);
  151. if (previousVersionPeriod) {
  152. if (versions.size() == 1) {
  153. return resolvePreviousVersionWithOnlyOneExistingVersion(dbSession, projectUuid);
  154. }
  155. return resolvePreviousVersion(dbSession, analysisProjectVersion, versions, mostRecentVersion);
  156. }
  157. return resolveVersion(dbSession, versions, propertyValue);
  158. }
  159. @CheckForNull
  160. private static Instant parseDate(String propertyValue) {
  161. try {
  162. LocalDate localDate = LocalDate.parse(propertyValue);
  163. return localDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
  164. } catch (DateTimeParseException e) {
  165. boolean invalidDate = e.getCause() == null || e.getCause() == e || !e.getCause().getMessage().contains("Invalid date");
  166. checkPeriodProperty(invalidDate, propertyValue, "Invalid date");
  167. return null;
  168. }
  169. }
  170. private Optional<Period> resolveByDays(DbSession dbSession, String projectUuid, Integer days, String propertyValue) {
  171. checkPeriodProperty(days > 0, propertyValue, "number of days is <= 0");
  172. long analysisDate = analysisMetadataHolder.getAnalysisDate();
  173. List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(dbSession, createCommonQuery(projectUuid).setCreatedBefore(analysisDate).setSort(BY_DATE, ASC));
  174. ensureNotOnFirstAnalysis(!snapshots.isEmpty());
  175. Instant targetDate = DateUtils.addDays(Instant.ofEpochMilli(analysisDate), -days);
  176. LOG.debug("Resolving new code period by {} days: {}", days, supplierToString(() -> logDate(targetDate)));
  177. SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate);
  178. return Optional.of(newPeriod(LEAK_PERIOD_MODE_DAYS, String.valueOf((int) days), snapshot));
  179. }
  180. private Optional<Period> resolveByDate(DbSession dbSession, String projectUuid, Instant date, String propertyValue) {
  181. Instant now = Instant.ofEpochMilli(system2.now());
  182. checkPeriodProperty(date.compareTo(now) <= 0, propertyValue,
  183. "date is in the future (now: '%s')", supplierToString(() -> logDate(now)));
  184. LOG.debug("Resolving new code period by date: {}", supplierToString(() -> logDate(date)));
  185. Optional<Period> period = findFirstSnapshot(dbSession, createCommonQuery(projectUuid).setCreatedAfter(date.toEpochMilli()).setSort(BY_DATE, ASC))
  186. .map(dto -> newPeriod(LEAK_PERIOD_MODE_DATE, DateUtils.formatDate(date), dto));
  187. checkPeriodProperty(period.isPresent(), propertyValue, "No analysis found created after date '%s'", supplierToString(() -> logDate(date)));
  188. return period;
  189. }
  190. private Optional<Period> resolveWhenNoExistingVersion(DbSession dbSession, String projectUuid, String currentVersion, String propertyValue) {
  191. LOG.debug("Resolving first analysis as new code period as there is no existing version");
  192. boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue);
  193. boolean currentVersionPeriod = currentVersion.equals(propertyValue);
  194. checkPeriodProperty(previousVersionPeriod || currentVersionPeriod, propertyValue,
  195. "No existing version. Property should be either '%s' or the current version '%s' (actual: '%s')",
  196. LEAK_PERIOD_MODE_PREVIOUS_VERSION, currentVersion, propertyValue);
  197. String periodMode = previousVersionPeriod ? LEAK_PERIOD_MODE_PREVIOUS_VERSION : LEAK_PERIOD_MODE_VERSION;
  198. return findOldestAnalysis(dbSession, periodMode, projectUuid);
  199. }
  200. private Optional<Period> resolvePreviousVersionWithOnlyOneExistingVersion(DbSession dbSession, String projectUuid) {
  201. LOG.debug("Resolving first analysis as new code period as there is only one existing version");
  202. return findOldestAnalysis(dbSession, LEAK_PERIOD_MODE_PREVIOUS_VERSION, projectUuid);
  203. }
  204. private Optional<Period> findOldestAnalysis(DbSession dbSession, String periodMode, String projectUuid) {
  205. Optional<Period> period = dbClient.snapshotDao().selectOldestSnapshot(dbSession, projectUuid)
  206. .map(dto -> newPeriod(periodMode, null, dto));
  207. ensureNotOnFirstAnalysis(period.isPresent());
  208. return period;
  209. }
  210. private Optional<Period> resolvePreviousVersion(DbSession dbSession, String currentVersion, List<EventDto> versions, String mostRecentVersion) {
  211. EventDto previousVersion = versions.get(currentVersion.equals(mostRecentVersion) ? 1 : 0);
  212. LOG.debug("Resolving new code period by previous version: {}", previousVersion.getName());
  213. return newPeriod(dbSession, LEAK_PERIOD_MODE_PREVIOUS_VERSION, previousVersion);
  214. }
  215. private Optional<Period> resolveVersion(DbSession dbSession, List<EventDto> versions, String propertyValue) {
  216. LOG.debug("Resolving new code period by version: {}", propertyValue);
  217. Optional<EventDto> version = versions.stream().filter(t -> propertyValue.equals(t.getName())).findFirst();
  218. checkPeriodProperty(version.isPresent(), propertyValue,
  219. "version is none of the existing ones: %s", supplierToString(() -> toVersions(versions)));
  220. return newPeriod(dbSession, LEAK_PERIOD_MODE_VERSION, version.get());
  221. }
  222. private Optional<Period> newPeriod(DbSession dbSession, String periodMode, EventDto previousVersion) {
  223. Optional<Period> period = dbClient.snapshotDao().selectByUuid(dbSession, previousVersion.getAnalysisUuid())
  224. .map(dto -> newPeriod(periodMode, previousVersion.getName(), dto));
  225. if (!period.isPresent()) {
  226. throw new IllegalStateException(format("Analysis '%s' for version event '%s' has been deleted",
  227. previousVersion.getAnalysisUuid(), previousVersion.getName()));
  228. }
  229. return period;
  230. }
  231. private static String toVersions(List<EventDto> versions) {
  232. return Arrays.toString(versions.stream().map(EventDto::getName).toArray(String[]::new));
  233. }
  234. private static Object supplierToString(Supplier<String> s) {
  235. return new Object() {
  236. @Override
  237. public String toString() {
  238. return s.get();
  239. }
  240. };
  241. }
  242. private static Period newPeriod(String mode, @Nullable String modeParameter, SnapshotDto dto) {
  243. return new Period(mode, modeParameter, dto.getCreatedAt(), dto.getUuid());
  244. }
  245. private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) {
  246. if (!test) {
  247. LOG.debug("Invalid code period '{}': {}", propertyValue, supplierToString(() -> format(testDescription, args)));
  248. throw MessageException.of(format("Invalid new code period. '%s' is not one of: " +
  249. "integer > 0, date before current analysis j, \"previous_version\", or version string that exists in the project' \n" +
  250. "Please contact a project administrator to correct this setting", propertyValue));
  251. }
  252. }
  253. private Optional<SnapshotDto> findFirstSnapshot(DbSession session, SnapshotQuery query) {
  254. return dbClient.snapshotDao().selectAnalysesByQuery(session, query)
  255. .stream()
  256. .findFirst();
  257. }
  258. private static void ensureNotOnFirstAnalysis(boolean expression) {
  259. checkState(expression, "Attempting to resolve period while no analysis exist for project");
  260. }
  261. @CheckForNull
  262. private static Integer parseDaysQuietly(String property) {
  263. try {
  264. return Integer.parseInt(property);
  265. } catch (NumberFormatException e) {
  266. // Nothing to, it means that the property is not a number of days
  267. return null;
  268. }
  269. }
  270. private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Instant targetDate) {
  271. // FIXME shouldn't this be the first analysis after targetDate?
  272. Duration bestDuration = null;
  273. SnapshotDto nearest = null;
  274. for (SnapshotDto snapshot : snapshots) {
  275. Instant createdAt = Instant.ofEpochMilli(snapshot.getCreatedAt());
  276. Duration duration = Duration.between(targetDate, createdAt).abs();
  277. if (bestDuration == null || duration.compareTo(bestDuration) <= 0) {
  278. bestDuration = duration;
  279. nearest = snapshot;
  280. }
  281. }
  282. return nearest;
  283. }
  284. private static SnapshotQuery createCommonQuery(String projectUuid) {
  285. return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED);
  286. }
  287. private static String logDate(Instant instant) {
  288. return DateUtils.formatDate(instant.truncatedTo(ChronoUnit.SECONDS));
  289. }
  290. }