/* * Copyright 2000-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.buildServer.server.rest.data; import com.google.common.collect.ComparisonChain; import com.intellij.openapi.diagnostic.Logger; import java.util.*; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import java.util.stream.Stream; import jetbrains.buildServer.ServiceLocator; import jetbrains.buildServer.parameters.ParametersProvider; import jetbrains.buildServer.parameters.impl.AbstractMapParametersProvider; import jetbrains.buildServer.server.rest.data.build.TagFinder; import jetbrains.buildServer.server.rest.data.problem.TestFinder; import jetbrains.buildServer.server.rest.data.problem.TestOccurrenceFinder; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.errors.LocatorProcessException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.server.rest.model.ItemsProviders; import jetbrains.buildServer.server.rest.model.PagerData; import jetbrains.buildServer.server.rest.model.agent.Agent; import jetbrains.buildServer.server.rest.model.build.Build; import jetbrains.buildServer.server.rest.request.Constants; import jetbrains.buildServer.server.rest.util.StreamUtil; import jetbrains.buildServer.serverSide.*; import jetbrains.buildServer.serverSide.auth.AccessDeniedException; import jetbrains.buildServer.serverSide.auth.Permission; import jetbrains.buildServer.serverSide.dependency.BuildDependency; import jetbrains.buildServer.serverSide.metadata.BuildMetadataEntry; import jetbrains.buildServer.serverSide.metadata.impl.MetadataStorageEx; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.util.CollectionsUtil; import jetbrains.buildServer.util.Converter; import jetbrains.buildServer.util.ItemProcessor; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.util.filters.Filter; import jetbrains.buildServer.vcs.SVcsRoot; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Yegor.Yarko * Date: 20.08.2014 */ public class BuildPromotionFinder extends AbstractFinder { private static final Logger LOG = Logger.getInstance(BuildPromotionFinder.class.getName()); //DIMENSION_ID - id of a build or id of build promotion which will get associated build with the id public static final String PROMOTION_ID = BuildFinder.PROMOTION_ID; protected static final String PROMOTION_ID_ALIAS = "promotionId"; protected static final String BUILD_ID = "buildId"; //this is experimental, for debug purposes only public static final String BUILD_TYPE = "buildType"; public static final String PROJECT = "project"; // BuildFinder (used prior to 9.0) treats "project" as "affectedProject" and thus this behavior is different from BuildFinder private static final String AFFECTED_PROJECT = "affectedProject"; public static final String AGENT = "agent"; public static final String AGENT_NAME = "agentName"; public static final String AGENT_TYPE_ID = "agentTypeId"; public static final String PERSONAL = "personal"; public static final String USER = "user"; protected static final String BRANCH = "branch"; protected static final String BRANCHED = "branched"; //experimental protected static final String PROPERTY = "property"; protected static final String STATISTIC_VALUE = "statisticValue"; public static final String STATE = "state"; public static final String STATE_QUEUED = "queued"; public static final String STATE_RUNNING = "running"; public static final String STATE_FINISHED = "finished"; protected static final String STATE_ANY = "any"; protected static final String NUMBER = "number"; protected static final String STATUS = "status"; protected static final String CANCELED = "canceled"; protected static final String FAILED_TO_START = "failedToStart"; protected static final String PINNED = "pinned"; protected static final String RUNNING = "running"; protected static final String HANGING = "hanging"; protected static final String COMPOSITE = "composite"; protected static final String SNAPSHOT_DEP = "snapshotDependency"; protected static final String ARTIFACT_DEP = "artifactDependency"; protected static final String COMPATIBLE_AGENTS_COUNT = "compatibleAgentsCount"; protected static final String TAGS = "tags"; //legacy support only protected static final String TAG = "tag"; protected static final String COMPATIBLE_AGENT = "compatibleAgent"; protected static final String HISTORY = "history"; protected static final String TEST_OCCURRENCE = "testOccurrence"; protected static final String TEST = "test"; protected static final String SINCE_BUILD = "sinceBuild"; //use startDate:(build:(),condition:after) instead protected static final String SINCE_DATE = "sinceDate"; //use startDate:(date:,condition:after) instead protected static final String UNTIL_BUILD = "untilBuild"; //use startDate:(build:(),condition:before) instead protected static final String UNTIL_DATE = "untilDate"; //use startDate:(date:,condition:before) instead protected static final String QUEUED_TIME = "queuedDate"; protected static final String STARTED_TIME = "startDate"; protected static final String FINISHED_TIME = "finishDate"; protected static final String DEFAULT_FILTERING = "defaultFilter"; protected static final String SINCE_BUILD_ID_LOOK_AHEAD_COUNT = "sinceBuildIdLookAheadCount"; /*experimental*/ public static final String ORDERED = "ordered"; /*experimental*/ public static final String STROB = "strob"; /*experimental*/ //might need a better name public static final String BY_PROMOTION = "byPromotion"; //used in BuildFinder public static final String EQUIVALENT = "equivalent"; /*experimental*/ public static final String METADATA = "metadata"; /*experimental*/ public static final String REVISION = "revision"; /*experimental*/ public static final BuildPromotionComparator BUILD_PROMOTIONS_COMPARATOR = new BuildPromotionComparator(); public static final SnapshotDepsTraverser SNAPSHOT_DEPENDENCIES_TRAVERSER = new SnapshotDepsTraverser(); public static final ArtifactDepsTraverser ARTIFACT_DEPENDENCIES_TRAVERSER = new ArtifactDepsTraverser(); protected static final String STROB_BUILD_LOCATOR = "locator"; private final BuildPromotionManager myBuildPromotionManager; private final BuildQueue myBuildQueue; private final BuildsManager myBuildsManager; private final VcsRootFinder myVcsRootFinder; private final ProjectFinder myProjectFinder; private final BuildTypeFinder myBuildTypeFinder; private final UserFinder myUserFinder; private final AgentFinder myAgentFinder; private final BranchFinder myBranchFinder; private final MetadataStorageEx myMetadataStorage; private final TimeCondition myTimeCondition; private final PermissionChecker myPermissionChecker; @NotNull private final ServiceLocator myServiceLocator; @NotNull public static String getLocator(@NotNull final BuildPromotion buildPromotion) { return Locator.getStringLocator(DIMENSION_ID, String.valueOf(getBuildId(buildPromotion))); } @NotNull @Override public String getItemLocator(@NotNull final BuildPromotion buildPromotion) { return BuildPromotionFinder.getLocator(buildPromotion); } public BuildPromotionFinder(final BuildPromotionManager buildPromotionManager, final BuildQueue buildQueue, final BuildsManager buildsManager, final VcsRootFinder vcsRootFinder, final ProjectFinder projectFinder, final BuildTypeFinder buildTypeFinder, final UserFinder userFinder, final AgentFinder agentFinder, final BranchFinder branchFinder, final TimeCondition timeCondition, final PermissionChecker permissionChecker, final MetadataStorageEx metadataStorage, @NotNull final ServiceLocator serviceLocator) { super(DIMENSION_ID, PROMOTION_ID, PROJECT, AFFECTED_PROJECT, BUILD_TYPE, BRANCH, AGENT, AGENT_NAME, AGENT_TYPE_ID, USER, PERSONAL, STATE, TAG, PROPERTY, COMPATIBLE_AGENT, NUMBER, STATUS, CANCELED, PINNED, QUEUED_TIME, STARTED_TIME, FINISHED_TIME, SINCE_BUILD, SINCE_DATE, UNTIL_BUILD, UNTIL_DATE, FAILED_TO_START, SNAPSHOT_DEP, ARTIFACT_DEP, HANGING, COMPOSITE, HISTORY, DEFAULT_FILTERING, SINCE_BUILD_ID_LOOK_AHEAD_COUNT, Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); setHiddenDimensions(TAGS, RUNNING, //compatibility BY_PROMOTION, //switch for legacy behavior COMPATIBLE_AGENTS_COUNT, //experimental for queued builds only EQUIVALENT, REVISION, PROMOTION_ID_ALIAS, BUILD_ID, METADATA, STATISTIC_VALUE, TEST_OCCURRENCE, TEST, //experimental SINCE_BUILD_ID_LOOK_AHEAD_COUNT, //experimental ORDERED, //experimental STROB, //experimental BRANCHED //experimental ); myPermissionChecker = permissionChecker; myBuildPromotionManager = buildPromotionManager; myBuildQueue = buildQueue; myBuildsManager = buildsManager; myVcsRootFinder = vcsRootFinder; myProjectFinder = projectFinder; myBuildTypeFinder = buildTypeFinder; myUserFinder = userFinder; myAgentFinder = agentFinder; myBranchFinder = branchFinder; myMetadataStorage = metadataStorage; myTimeCondition = timeCondition; myServiceLocator = serviceLocator; } @NotNull public static String getLocator(@NotNull final SBuildType buildType, @Nullable final Branch branch, @Nullable final String additionalLocator){ String result = Locator.getStringLocator(BUILD_TYPE, BuildTypeFinder.getLocator(buildType)); if (branch != null) { result = Locator.setDimension(result, BRANCH, BranchFinder.getLocator(branch)); } if (additionalLocator == null) return result; return Locator.merge(result, additionalLocator); } @Override public Long getDefaultPageItemsCount() { return (long)Constants.getDefaultPageItemsCount(); } @Override public Long getDefaultLookupLimit() { final long defaultLookupLimit = TeamCityProperties.getLong("rest.request.builds.defaultLookupLimit", 5000); if (defaultLookupLimit != 0) { return defaultLookupLimit; } return null; } @NotNull @Override public TreeSet createContainerSet() { return new TreeSet<>(new Comparator() { @Override public int compare(final BuildPromotion o1, final BuildPromotion o2) { return ComparisonChain.start() .compare(o1.getId(), o2.getId()) .result(); } }); } @Nullable @Override public BuildPromotion findSingleItem(@NotNull final Locator locator) { if (locator.isSingleValue()) { @NotNull final Long singleValueAsLong; try { //noinspection ConstantConditions singleValueAsLong = locator.getSingleValueAsLong(); } catch (LocatorProcessException e) { throw new BadRequestException("Invalid single value: '" + locator.getSingleValue() + "'. Should be a numeric build id", e); } // difference from 9.0 behavior where we never searched by promotion id in case of single value locators return getBuildPromotionById(singleValueAsLong, myBuildPromotionManager, myBuildsManager); } Long promotionId = locator.getSingleDimensionValueAsLong(PROMOTION_ID); if (promotionId == null){ promotionId = locator.getSingleDimensionValueAsLong(PROMOTION_ID_ALIAS); //support TeamCity 8.0 dimension } if (promotionId != null) { return BuildFinder.getBuildPromotion(promotionId, myBuildPromotionManager); } Long buildId = locator.getSingleDimensionValueAsLong(BUILD_ID); if (buildId != null) { final SBuild build = myBuildsManager.findBuildInstanceById(buildId); if (build != null) { return build.getBuildPromotion(); } throw new NotFoundException("No build found by build id '" + buildId + "'."); } final Long id = locator.getSingleDimensionValueAsLong(DIMENSION_ID); if (id != null) { return getBuildPromotionById(id, myBuildPromotionManager, myBuildsManager); } return null; } @NotNull @Override public ItemFilter getFilter(@NotNull final Locator locator) { final MultiCheckerFilter result = new MultiCheckerFilter(); //checking permissions to view - workaround for TW-45544 result.add(item -> { try { ensureCanView(item); return true; } catch (AccessDeniedException e) { return false; //excluding from the lists as secure wrappers usually do } }); if (locator.isSingleValue()) { try { long foundPromotionId = getBuildPromotionById(locator.getSingleValueAsLong(), myBuildPromotionManager, myBuildsManager).getId(); result.add(item -> foundPromotionId == item.getId()); } catch (NotFoundException e) { result.add(item -> false); } } if (locator.isUnused(DEFAULT_FILTERING)) { //basically, mark as used if it is not yet processed, but is unset or is set to false final Boolean defaultFiltering = locator.getSingleDimensionValueAsBoolean(DEFAULT_FILTERING); if (defaultFiltering != null && defaultFiltering) { locator.markUnused(DEFAULT_FILTERING); } } final Long id = locator.getSingleDimensionValueAsLong(DIMENSION_ID); if (id != null) { try { long foundPromotionId = getBuildPromotionById(id, myBuildPromotionManager, myBuildsManager).getId(); result.add(item -> foundPromotionId == item.getId()); } catch (NotFoundException e) { result.add(item -> false); } } final Long promotionId = locator.getSingleDimensionValueAsLong(PROMOTION_ID); if (promotionId != null) { try { long foundPromotionId = BuildFinder.getBuildPromotion(promotionId, myBuildPromotionManager).getId(); result.add(item -> foundPromotionId == item.getId()); } catch (NotFoundException e) { result.add(item -> false); } } final Long promotionIdAlias = locator.getSingleDimensionValueAsLong(PROMOTION_ID_ALIAS); if (promotionIdAlias != null) { try { long foundPromotionId = BuildFinder.getBuildPromotion(promotionIdAlias, myBuildPromotionManager).getId(); result.add(item -> foundPromotionId == item.getId()); } catch (NotFoundException e) { result.add(item -> false); } } final Long buildId = locator.getSingleDimensionValueAsLong(BUILD_ID); if (buildId != null) { result.add(item -> buildId.equals(item.getAssociatedBuildId())); } Locator stateLocator = getStateLocator(locator); if (!isStateIncluded(stateLocator, STATE_QUEUED)) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return item.getQueuedBuild() == null; } }); } if (!isStateIncluded(stateLocator, STATE_RUNNING)) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild associatedBuild = item.getAssociatedBuild(); return associatedBuild == null || associatedBuild.isFinished(); } }); } if (!isStateIncluded(stateLocator, STATE_FINISHED)) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild associatedBuild = item.getAssociatedBuild(); return associatedBuild == null || !associatedBuild.isFinished(); } }); } final Boolean composite = locator.getSingleDimensionValueAsBoolean(COMPOSITE); if (composite != null) { result.add(item -> FilterUtil.isIncludedByBooleanFilter(composite, item.isCompositeBuild())); } if (locator.isUnused(PROJECT)) { SProject project = getProjectFromDimension(locator, PROJECT); if (project != null) { result.add(item -> { final SBuildType buildType = item.getBuildType(); return buildType != null && project.equals(buildType.getProject()); }); } } if (locator.isUnused(AFFECTED_PROJECT)) { SProject affectedProject = getProjectFromDimension(locator, AFFECTED_PROJECT); if (affectedProject != null && !affectedProject.isRootProject()) { result.add(item -> { final SBuildType buildType = item.getBuildType(); return buildType != null && ProjectFinder.isSameOrParent(affectedProject, buildType.getProject()); }); } } if (locator.isUnused(BUILD_TYPE)) { final String buildTypeLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (buildTypeLocator != null) { final Set buildTypes = new HashSet<>(myBuildTypeFinder.getBuildTypes(getProjectFromDimension(locator, PROJECT), buildTypeLocator)); if (buildTypes.isEmpty()) { throw new NotFoundException("No build types found for locator '" + buildTypeLocator + "'"); } result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return buildTypes.contains(item.getParentBuildType()); } //todo: use build types Filter instead }); } } final String branchLocatorValue = locator.getSingleDimensionValue(BRANCH); if (branchLocatorValue != null) { PagedSearchResult branches = null; BadRequestException exceptinoOnFullSearch = null; try { branches = myBranchFinder.getItemsIfValidBranchListLocator(locator.getSingleDimensionValue(BUILD_TYPE), branchLocatorValue); } catch (BadRequestException e) { // not a valid branches listing locator exceptinoOnFullSearch = e; } if (branches != null) { //branches found - use them Set branchNames = getBranchNamesSet(branches.myEntries); Set branchDisplayNames = getBranchDisplayNamesSet(branches.myEntries); boolean defaultBranchIncluded = branches.myEntries.stream().anyMatch(Branch::isDefaultBranch); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final Branch buildBranch = BranchData.fromBuild(item); return (defaultBranchIncluded && buildBranch.isDefaultBranch()) || branchNames.contains(buildBranch.getName()) || branchDisplayNames.contains(buildBranch.getDisplayName()); } }); } else { //branches not found by locator - try to use filter BranchFinder.BranchFilterDetails branchFilterDetails; try { branchFilterDetails = myBranchFinder.getBranchFilterDetails(branchLocatorValue); } catch (LocatorProcessException locatorException) { throw new BadRequestException("Invalid sub-locator '" + BRANCH + "': Cannot" + (exceptinoOnFullSearch != null ? " find branches: " + exceptinoOnFullSearch.getMessage() +"; and cannot " : "") + " create filter: " + locatorException.getMessage(), locatorException); } if (!branchFilterDetails.isAnyBranch()) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return branchFilterDetails.isIncluded(item); } }); } } } final Boolean branched = locator.getSingleDimensionValueAsBoolean(BRANCHED); if (branched != null) { result.add(item -> FilterUtil.isIncludedByBooleanFilter(branched, item.getBranch() != null)); } if (locator.isUnused(AGENT)) { final String agentLocator = locator.getSingleDimensionValue(AGENT); if (agentLocator != null) { Set agentIds = myAgentFinder.getItemsNotEmpty(agentLocator).myEntries.stream().map(agent -> agent.getId()).filter(i -> i != Agent.UNKNOWN_AGENT_ID).collect(Collectors.toSet()); result.add(item -> { final SQueuedBuild queuedBuild = item.getQueuedBuild(); //for queued build using compatible agents if (queuedBuild != null) { return queuedBuild.getCanRunOnAgents().stream().anyMatch(agent -> agentIds.contains(agent.getId())); } final SBuild build = item.getAssociatedBuild(); if (build != null) { return agentIds.contains(build.getAgent().getId()); } return false; }); } } //compatibility support final String tags = locator.getSingleDimensionValue(TAGS); if (tags != null) { final List tagsList = Arrays.asList(tags.split(",")); if (tagsList.size() > 0) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return item.getTags().containsAll(tagsList); } }); } } final List tag = locator.getDimensionValue(TAG); //need to pass through the filter even though some builds might have been pre-filtered - exact filter still needs to be applied if (!tag.isEmpty()) { if (tag.size() == 1 && tag.get(0).startsWith("format:extended")) { //pre-9.1 compatibility //todo: log this? result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { try { return isTagsMatchLocator(item.getTags(), new Locator(tag.get(0))); } catch (LocatorProcessException e) { throw new BadRequestException("Invalid locator 'tag' (legacy format is used): " + e.getMessage(), e); } } }); } else { for (String singleTag : tag) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return TagFinder.isIncluded(item, singleTag, myUserFinder); } }); } } } final String compatibleAagentLocator = locator.getSingleDimensionValue(COMPATIBLE_AGENT); //experimental, only for queued builds if (compatibleAagentLocator != null) { final SBuildAgent agent = myAgentFinder.getItem(compatibleAagentLocator); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return myAgentFinder.canActuallyRun(agent, item); } }); } final Long compatibleAgentsCount = locator.getSingleDimensionValueAsLong(COMPATIBLE_AGENTS_COUNT); //experimental, only for queued builds if (compatibleAgentsCount != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { long count = 0; for (SBuildAgent agent : myAgentFinder.getItems(null).myEntries) { //or should process unauthorized as well? if (myAgentFinder.canActuallyRun(agent, item)) count++; if (count > compatibleAgentsCount) return false; } return count == compatibleAgentsCount; } }); } final Boolean personal = locator.getSingleDimensionValueAsBoolean(PERSONAL); if (personal != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return FilterUtil.isIncludedByBooleanFilter(personal, item.isPersonal()); } }); } final Boolean history = locator.getSingleDimensionValueAsBoolean(HISTORY); if (history != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return FilterUtil.isIncludedByBooleanFilter(history, item.isOutOfChangesSequence()); } }); } final String userDimension = locator.getSingleDimensionValue(USER); if (userDimension != null) { final SUser user = myUserFinder.getItem(userDimension); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { SUser actualUser = null; final SBuild build = item.getAssociatedBuild(); if (build != null) { actualUser = build.getTriggeredBy().getUser(); } final SQueuedBuild queuedBuild = item.getQueuedBuild(); if (queuedBuild != null) { actualUser = queuedBuild.getTriggeredBy().getUser(); } return actualUser != null && user.getId() == actualUser.getId(); } }); } final List properties = locator.getDimensionValue(PROPERTY); if (!properties.isEmpty()) { final Matcher parameterCondition = ParameterCondition.create(properties); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { if (!Build.canViewRuntimeData(myPermissionChecker, item)) return false; //does not correspond to Build.getProperties() which includes less parameters return parameterCondition.matches(Build.getBuildResultingParameters(item)); //TeamCity open API issue } }); } if (locator.getUnusedDimensions().contains(SNAPSHOT_DEP)) { //performance optimization: do not filter if already processed final String snapshotDepDimension = locator.getSingleDimensionValue(SNAPSHOT_DEP); if (snapshotDepDimension != null) { final Set snapshotRelatedBuilds = new HashSet<>(getSnapshotRelatedBuilds(snapshotDepDimension)); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return snapshotRelatedBuilds.contains(item); } }); } } if (locator.getUnusedDimensions().contains(ARTIFACT_DEP)) { //performance optimization: do not filter if already processed final String artifactDepDimension = locator.getSingleDimensionValue(ARTIFACT_DEP); if (artifactDepDimension != null) { final Set artifactRelatedBuilds = new HashSet<>(getArtifactRelatedBuilds(artifactDepDimension)); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return artifactRelatedBuilds.contains(item); } }); } } if (locator.getUnusedDimensions().contains(EQUIVALENT)) { //performance optimization: do not filter if already processed final String equivalent = locator.getSingleDimensionValue(EQUIVALENT); if (equivalent != null) { final Set filter = new HashSet(((BuildPromotionEx)getItem(equivalent)).getStartedEquivalentPromotions(-1)); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return filter.contains(item); } }); } } if (locator.getUnusedDimensions().contains(METADATA)) { //performance optimization: do not filter if already processed final String metadata = locator.getSingleDimensionValue(METADATA); if (metadata != null) { final Iterator metadataEntries = getBuildMetadataEntryIterator(metadata); final Set buildIds = new HashSet(); while (metadataEntries.hasNext()) { BuildMetadataEntry metadataEntry = metadataEntries.next(); buildIds.add(metadataEntry.getBuildId()); } result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { if (!Build.canViewRuntimeData(myPermissionChecker, item)) return false; return buildIds.contains(item.getAssociatedBuildId()); } }); } } if (locator.getUnusedDimensions().contains(ORDERED)) { //performance optimization: do not filter if already processed final String graphLocator = locator.getSingleDimensionValue(ORDERED); if (graphLocator != null) { final GraphFinder graphFinder = new BuildPromotionOrderedFinder(BuildPromotionFinder.this); final Set filter = new HashSet(graphFinder.getItems(graphLocator).myEntries); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return filter.contains(item); } }); } } final String sinceBuildDimension = locator.getSingleDimensionValue(SINCE_BUILD); BuildPromotion sinceBuildPromotion = null; Long sinceBuildId = null; if (sinceBuildDimension != null) { try { sinceBuildPromotion = getItem(sinceBuildDimension); final SQueuedBuild queuedBuild = sinceBuildPromotion.getQueuedBuild(); if (queuedBuild != null) { //compare queued builds by id (triggering sequence) final long buildPromotionId = getBuildId(sinceBuildPromotion); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return buildPromotionId < getBuildId(item); } }); } else { // for started build, compare by start time final SBuild limitingBuild = sinceBuildPromotion.getAssociatedBuild(); if (limitingBuild != null) { final Date startDate = limitingBuild.getStartDate(); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild build = item.getAssociatedBuild(); if (build == null) return true; if (startDate.equals(build.getStartDate()) && limitingBuild.getBuildId() != build.getBuildId()) return true; return startDate.before(build.getStartDate()); } }); } } } catch (NotFoundException e) { //build not found by sinceBuild locator, extract id ad filter using it sinceBuildId = getBuildId(sinceBuildDimension); final long sinceBuildIdFinal = sinceBuildId; result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return sinceBuildIdFinal < getBuildId(item); } }); } } final String untilBuild = locator.getSingleDimensionValue(UNTIL_BUILD); if (untilBuild != null) { try { final BuildPromotion untilBuildPromotion = getItem(untilBuild); final SQueuedBuild queuedBuild = untilBuildPromotion.getQueuedBuild(); if (queuedBuild != null) { //compare queued builds by id (triggering sequence) final long buildPromotionId = getBuildId(untilBuildPromotion); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return !(buildPromotionId < getBuildId(item)); } }); } else { // for started build, compare by start time final SBuild limitingBuild = untilBuildPromotion.getAssociatedBuild(); if (limitingBuild != null) { final Date startDate = limitingBuild.getStartDate(); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild build = item.getAssociatedBuild(); return build == null || !startDate.before(build.getStartDate()); } }); } } } catch (NotFoundException e) { //build not found by sinceBuild locator, extract id ad filter using it final long untilBuildId = getBuildId(untilBuild); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { return !(untilBuildId < getBuildId(item)); } }); } } TimeCondition.FilterAndLimitingDate queuedFiltering = myTimeCondition.processTimeConditions(QUEUED_TIME, locator, TimeCondition.QUEUED_BUILD_TIME, TimeCondition.QUEUED_BUILD_TIME); if (queuedFiltering != null) result.add(queuedFiltering.getFilter()); TimeCondition.FilterAndLimitingDate startedFiltering = myTimeCondition.processTimeConditions(STARTED_TIME, locator, TimeCondition.STARTED_BUILD_TIME, TimeCondition.STARTED_BUILD_TIME); @Nullable Date sinceStartDate = null; if (startedFiltering != null) { result.add(startedFiltering.getFilter()); sinceStartDate = startedFiltering.getLimitingDate(); } //todo: add processing cut of based on assumption of max build time (say, a week); for other times as well TimeCondition.FilterAndLimitingDate finishFiltering = myTimeCondition.processTimeConditions(FINISHED_TIME, locator, TimeCondition.FINISHED_BUILD_TIME, TimeCondition.FINISHED_BUILD_TIME); if (finishFiltering != null) result.add(finishFiltering.getFilter()); final String revisionLocatorText = locator.getSingleDimensionValue(REVISION); if (revisionLocatorText != null) { final Locator revisionLocator = new Locator(revisionLocatorText, "version", "internalVersion", "vcsRoot", Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); final String revision = revisionLocator.getSingleValue(); if (revision != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final List buildRevisions = item.getRevisions(); for (BuildRevision rev : buildRevisions) { if (revision.equals(rev.getRevisionDisplayName())) { return true; } } return false; } }); } else { final String vcsRootLocator = revisionLocator.getSingleDimensionValue("vcsRoot"); final SVcsRoot vcsRoot = vcsRootLocator == null ? null : myVcsRootFinder.getItem(vcsRootLocator); final ValueCondition versionCondition = ParameterCondition.createValueCondition(revisionLocator.getSingleDimensionValue("version")); final ValueCondition internalVersionCondition = ParameterCondition.createValueCondition(revisionLocator.getSingleDimensionValue("internalVersion")); revisionLocator.checkLocatorFullyProcessed(); if (vcsRoot != null || versionCondition != null || internalVersionCondition != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final List revisions = item.getRevisions(); for (BuildRevision rev : revisions) { if ((vcsRoot == null || vcsRoot.getId() == rev.getRoot().getParent().getId()) && (versionCondition == null || versionCondition.matches(rev.getRevisionDisplayName())) && (internalVersionCondition == null || internalVersionCondition.matches(rev.getRevision()))) { return true; } } return false; } }); } } } final MultiCheckerFilter buildFilter = getBuildFilter(locator); if (buildFilter.getSubFiltersCount() > 0) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild build = item.getAssociatedBuild(); if (build == null) { return false; } return buildFilter.isIncluded(build); } }); } sinceStartDate = TimeCondition.maxDate(sinceStartDate, DataProvider.parseDate(locator.getSingleDimensionValue(SINCE_DATE))); //see also filtering in getBuildFilter final Boolean canceled = locator.getSingleDimensionValueAsBoolean(CANCELED); if (canceled != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild build = item.getAssociatedBuild(); return FilterUtil.isIncludedByBooleanFilter(canceled, build != null && build.getCanceledInfo() != null); } }); } final Boolean failedToStart = locator.getSingleDimensionValueAsBoolean(FAILED_TO_START); if (failedToStart != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final BuildPromotion item) { final SBuild build = item.getAssociatedBuild(); return FilterUtil.isIncludedByBooleanFilter(failedToStart, build != null && build.isInternalError()); } }); } return getFilterWithProcessingCutOff(result, locator.getSingleDimensionValueAsLong(SINCE_BUILD_ID_LOOK_AHEAD_COUNT), sinceBuildPromotion, sinceBuildId, sinceStartDate); } @Nullable private SProject getProjectFromDimension(final @NotNull Locator locator, @NotNull final String dimension) { final String projectLocator = locator.getSingleDimensionValue(dimension); if (projectLocator == null) { return null; } return myProjectFinder.getItem(projectLocator); //todo: support multiple } private Set getBranchNamesSet(final List branches) { final HashSet result = new HashSet<>(branches.size()); for (Branch branch : branches) { result.add(branch.getName()); } return result; } private Set getBranchDisplayNamesSet(final List branches) { final HashSet result = new HashSet<>(branches.size()); for (Branch branch : branches) { result.add(branch.getDisplayName()); } return result; } private ItemFilter getFilterWithProcessingCutOff(@NotNull final MultiCheckerFilter result, @Nullable final Long lookupLimit, @Nullable final BuildPromotion sinceBuildPromotion, @Nullable Long sinceBuildId, @Nullable Date sinceStartDate) { if (sinceBuildPromotion == null && sinceBuildId == null && sinceStartDate == null) { return result; } if (sinceBuildPromotion != null) { sinceBuildId = sinceBuildId != null ? Math.max(sinceBuildId, getBuildId(sinceBuildPromotion)) : getBuildId(sinceBuildPromotion); final SBuild sinceBuild = sinceBuildPromotion.getAssociatedBuild(); if (sinceBuild != null) { sinceStartDate = TimeCondition.maxDate(sinceStartDate, sinceBuild.getStartDate()); } } //cut off builds traversing final long lookAheadCount = lookupLimit != null ? lookupLimit : TeamCityProperties.getLong("rest.request.builds.sinceBuildIdLookAheadCount", 50); final Long sinceBuildIdFinal = sinceBuildId; final Date sinceStartDateFinal = sinceStartDate; return new ItemFilter() { private long currentLookAheadCount = 0; public boolean isIncluded(@NotNull final BuildPromotion item) { return result.isIncluded(item); } public boolean shouldStop(@NotNull final BuildPromotion item) { if (result.shouldStop(item)) return true; final SBuild build = item.getAssociatedBuild(); if (build == null || !build.isFinished()) return false; //do not stop while processing queued and running builds if (sinceStartDateFinal != null && sinceStartDateFinal.after(build.getStartDate())) return true; if (sinceBuildIdFinal != null) { if (sinceBuildIdFinal.equals(getBuildId(item))) return true; //found exactly the limiting build - stop here if (sinceBuildIdFinal > getBuildId(item)) { currentLookAheadCount++; } else { currentLookAheadCount = 0; //reset the counter } return currentLookAheadCount > lookAheadCount; // stop only after finding more than lookAheadCount builds with lesser id (try to take into account builds reordering) } return false; } }; } @NotNull private Locator getStateLocator(@NotNull final Locator locator) { final String stateDimension = locator.getSingleDimensionValue(STATE); if (stateDimension != null) { return createStateLocator(stateDimension); } final String stateRunningDimension = locator.getSingleDimensionValue(RUNNING); //compatibility with pre-9.1 if (stateRunningDimension != null) { final Boolean legacyRunning = locator.getSingleDimensionValueAsBoolean(RUNNING); if (legacyRunning == null) { return createStateLocator(Locator.getStringLocator(STATE_FINISHED, "true", STATE_RUNNING, "true", STATE_QUEUED, "false")); } if (legacyRunning) { return createStateLocator(STATE_RUNNING); } else { return createStateLocator(STATE_FINISHED); } } return createStateLocator(STATE_ANY); // default to all the builds } @NotNull private MultiCheckerFilter getBuildFilter(@NotNull final Locator locator) { final MultiCheckerFilter result = new MultiCheckerFilter(); final String buildNumber = locator.getSingleDimensionValue(NUMBER); if (buildNumber != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return buildNumber.equals(item.getBuildNumber()); } }); } final String status = locator.getSingleDimensionValue(STATUS); if (status != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return status.equalsIgnoreCase(item.getStatusDescriptor().getStatus().getText()); } }); } final Boolean pinned = locator.getSingleDimensionValueAsBoolean(PINNED); if (pinned != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return FilterUtil.isIncludedByBooleanFilter(pinned, item.isPinned()); } }); } final Boolean hanging = locator.getSingleDimensionValueAsBoolean(HANGING); if (hanging != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { if (item.isFinished()) return !hanging; return FilterUtil.isIncludedByBooleanFilter(hanging, ((SRunningBuild)item).isProbablyHanging()); } }); } if (locator.isUnused(AGENT_NAME)) { final String agentName = locator.getSingleDimensionValue(AGENT_NAME); if (agentName != null) { final ValueCondition agentNameCondition = ParameterCondition.createValueCondition(agentName); result.add(item -> agentNameCondition.matches(item.getAgentName())); } } if (locator.isUnused(AGENT_TYPE_ID)) { final Long agentTypeId = locator.getSingleDimensionValueAsLong(AGENT_TYPE_ID); if (agentTypeId != null) { result.add(item -> agentTypeId.intValue() == item.getAgent().getAgentTypeId()); } } final Date sinceDate = DataProvider.parseDate(locator.getSingleDimensionValue(SINCE_DATE)); //see also settings cut off date in main filter if (sinceDate != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return sinceDate.before(item.getStartDate()); } }); } final Date untilDate = DataProvider.parseDate(locator.getSingleDimensionValue(UNTIL_DATE)); if (untilDate != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return !(untilDate.before(item.getStartDate())); } }); } final List statisticValues = locator.getDimensionValue(STATISTIC_VALUE); if (!statisticValues.isEmpty()) { final Matcher parameterCondition = ParameterCondition.create(statisticValues); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return parameterCondition.matches(new AbstractMapParametersProvider(Build.getBuildStatisticsValues(item))); } }); } if (locator.isUnused(TEST_OCCURRENCE)) { final String testOccurrence = locator.getSingleDimensionValue(TEST_OCCURRENCE); if (testOccurrence != null) { TestOccurrenceFinder testOccurrenceFinder = myServiceLocator.getSingletonService(TestOccurrenceFinder.class); Set buildPromotionIds = testOccurrenceFinder.getItems(testOccurrence).myEntries.stream().map(sTestRun -> sTestRun.getBuild().getBuildPromotion().getId()).collect(Collectors.toSet()); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { return buildPromotionIds.contains(item.getBuildPromotion().getId()); } }); } } final String test = locator.getSingleDimensionValue(TEST); if (test != null) { TestFinder testFinder = myServiceLocator.getSingletonService(TestFinder.class); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SBuild item) { String locator = new Locator(test).setDimension(TestFinder.BUILD, getLocator(item.getBuildPromotion())).setDimension(PagerData.COUNT, "1").getStringRepresentation(); return !testFinder.getItems(locator).myEntries.isEmpty(); } }); } return result; } @NotNull public static BuildPromotion getBuildPromotionById(@NotNull final Long id, @NotNull final BuildPromotionManager buildPromotionManager, @NotNull final BuildsManager buildsManager) { //the logic should match that of getBuildId(String) final BuildPromotion buildPromotion = buildPromotionManager.findPromotionOrReplacement(id); if (buildPromotion != null && (getBuildId(buildPromotion) == buildPromotion.getId())) { ensureCanView(buildPromotion); return buildPromotion; } final SBuild build = buildsManager.findBuildInstanceById(id); if (build != null) { return build.getBuildPromotion(); } throw new NotFoundException("No build found by id '" + id + "'."); } public static void ensureCanView(@NotNull final BuildPromotion buildPromotion) { //checking permissions to view - workaround for TW-45544 try { buildPromotion.getBuildType(); } catch (AccessDeniedException e) { //concealing the message which contains build configuration id: You do not have enough permissions to access build type with id: XXX String message = "Not enough permissions to access build with id: " + buildPromotion.getId(); LOG.debug(message, e); throw new AccessDeniedException(e.getAuthorityHolder(), message); } } @NotNull private Long getBuildId(@Nullable final String buildLocator) { //the logic should match that of findSingleItem if (buildLocator == null) { throw new BadRequestException("Cannot find build or build id for empty locator. Try specifying '" + DIMENSION_ID + "' locator dimension"); } final Locator locator = new Locator(buildLocator); final Long id = locator.getSingleDimensionValueAsLong(DIMENSION_ID); if (id != null && locator.getUnusedDimensions().isEmpty()) { return id; } throw new BadRequestException("Cannot find build or build id for locator '" + buildLocator + "'. Try specifying '" + DIMENSION_ID + "' locator dimension"); } @NotNull public static Long getBuildId(@NotNull final BuildPromotion buildPromotion) { final Long buildId = buildPromotion.getAssociatedBuildId(); //it is important to get id from the build as that might be different from promotion id return buildId != null ? buildId : buildPromotion.getId(); // there should be no queued builds with old ids (TW-38777) } private boolean isTagsMatchLocator(final List buildTags, final Locator tagsLocator) { if (!"extended".equals(tagsLocator.getSingleDimensionValue("format"))) { throw new BadRequestException("Only 'extended' value is supported for 'format' dimension of 'tag' dimension"); } final Boolean present = tagsLocator.getSingleDimensionValueAsBoolean("present", true); final String patternString = tagsLocator.getSingleDimensionValue("regexp"); if (present == null) { return true; } Boolean tagsMatchPattern = null; if (patternString != null) { if (StringUtil.isEmpty(patternString)) { throw new BadRequestException("'regexp' sub-dimension should not be empty for 'tag' dimension"); } try { tagsMatchPattern = tagsMatchPattern(buildTags, patternString); } catch (PatternSyntaxException e) { throw new BadRequestException( "Bad syntax for Java regular expression in 'regexp' sub-dimension of 'tag' dimension: " + e.getMessage(), e); } } if (tagsMatchPattern == null) { if ((present && buildTags.size() != 0) || (!present && (buildTags.size() == 0))) { return true; } } else { if (present && tagsMatchPattern) { return true; } else if (!present && !tagsMatchPattern) { return true; } } return false; } private Boolean tagsMatchPattern(@NotNull final List tags, @NotNull final String patternString) throws PatternSyntaxException { final Pattern pattern = Pattern.compile(patternString); boolean atLestOneMatches = false; for (String tag : tags) { atLestOneMatches = atLestOneMatches || pattern.matcher(tag).matches(); } return atLestOneMatches; } @NotNull @Override public ItemHolder getPrefilteredItems(@NotNull Locator locator) { if (!myPermissionChecker.hasPermissionInAnyProject(Permission.VIEW_PROJECT)){ return getItemHolder(Collections.emptyList()); //TeamCity issue: should be handled in core. Do not spend any resources on user without permissions } final Boolean byPromotion = locator.getSingleDimensionValueAsBoolean(BY_PROMOTION); if (byPromotion != null && !byPromotion) { throw new BadRequestException("Found '" + BY_PROMOTION + "' locator set to 'false' which is not supported"); } final String strob = locator.getSingleDimensionValue(STROB); if (strob != null) { final Locator strobLocator = new Locator(strob, BUILD_TYPE, BRANCH, STROB_BUILD_LOCATOR); List partialLocators = Collections.singletonList(Locator.createEmptyLocator()); partialLocators = getPartialLocatorsForStrobDimension(partialLocators, strobLocator, BUILD_TYPE, myBuildTypeFinder); partialLocators = getPartialLocatorsForStrobDimension(partialLocators, strobLocator, BRANCH, myBranchFinder); final String strobBuildLocator = strobLocator.getSingleDimensionValue(STROB_BUILD_LOCATOR); final AggregatingItemHolder strobResult = new AggregatingItemHolder<>(); for (Locator partialLocator : partialLocators) { partialLocator.setDimensionIfNotPresent(PagerData.COUNT, "1"); //limit to single item per strob item by default final String finalBuildLocator = Locator.createLocator(strobBuildLocator, partialLocator, new String[]{}).getStringRepresentation(); strobResult.add(getItemHolder(getItems(finalBuildLocator).myEntries)); } strobLocator.checkLocatorFullyProcessed(); return strobResult; } setLocatorDefaults(locator); final String equivalent = locator.getSingleDimensionValue(EQUIVALENT); if (equivalent != null) { final BuildPromotionEx build = (BuildPromotionEx)getItem(equivalent); final List result = build.getStartedEquivalentPromotions(-1); final Set convertedResult = new TreeSet(BUILD_PROMOTIONS_COMPARATOR); for (BuildPromotionEx item : result) { convertedResult.add(item); } return getItemHolder(convertedResult); } final String metadata = locator.getSingleDimensionValue(METADATA); if (metadata != null) { final Iterator metadataEntries = getBuildMetadataEntryIterator(metadata); return new ItemHolder() { @Override public void process(@NotNull final ItemProcessor processor) { while (metadataEntries.hasNext()) { BuildMetadataEntry metadataEntry = metadataEntries.next(); SBuild build = myBuildsManager.findBuildInstanceById(metadataEntry.getBuildId()); if (build != null && Build.canViewRuntimeData(myPermissionChecker, build.getBuildPromotion())) { processor.processItem(build.getBuildPromotion()); } } } }; } final String graphLocator = locator.getSingleDimensionValue(ORDERED); if (graphLocator != null) { final GraphFinder graphFinder = new BuildPromotionOrderedFinder(this); //consider performance optimization by converting id to build only on actual retrieve (use GraphFinder) return getItemHolder(graphFinder.getItems(graphLocator).myEntries); } final String snapshotDepDimension = locator.getSingleDimensionValue(SNAPSHOT_DEP); if (snapshotDepDimension != null) { return getItemHolder(getSnapshotRelatedBuilds(snapshotDepDimension)); } final String artifactDepDimension = locator.getSingleDimensionValue(ARTIFACT_DEP); if (artifactDepDimension != null) { return getItemHolder(getArtifactRelatedBuilds(artifactDepDimension)); } final String number = locator.getSingleDimensionValue(NUMBER); if (number != null) { final String buildTypeLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (buildTypeLocator != null) { final List buildTypes = myBuildTypeFinder.getBuildTypes(null, buildTypeLocator); final Set builds = new TreeSet(BUILD_PROMOTIONS_COMPARATOR); for (SBuildType buildType : buildTypes) { builds.addAll(BuildFinder.toBuildPromotions(myBuildsManager.findBuildInstancesByBuildNumber(buildType.getBuildTypeId(), number))); //todo: ensure due builds sorting } return getItemHolder(builds); } else{ // if build type is not specified, search by scanning (performance impact) locator.markUnused(NUMBER, BUILD_TYPE); } } if (TeamCityProperties.getBoolean("rest.request.builds.prefilterByTag")) { //this is temporary logic, can be dropped Locator stateLocator = getStateLocator(new Locator(locator)); //using locator copy so that no dimensions are marked as used if (isStateIncluded(stateLocator, STATE_FINISHED)) {//no sense in going further here if no finished builds are requested final List tagLocators = locator.lookupDimensionValue(TAG); //not marking as used to enforce filter processing Stream finishedBuilds = TagFinder.getPrefilteredFinishedBuildPromotions(tagLocators, myServiceLocator); if (finishedBuilds != null) { // all queued - to be filtered by the filter Stream queuedBuilds = isStateIncluded(stateLocator, STATE_QUEUED) ? myBuildQueue.getItems().stream().map(sQueuedBuild -> sQueuedBuild.getBuildPromotion()) : null; // all running - to be filtered by the filter Stream runningBuilds = isStateIncluded(stateLocator, STATE_RUNNING) ? myBuildsManager.getRunningBuilds().stream().map(sQueuedBuild -> sQueuedBuild.getBuildPromotion()) : null; return processor -> { if (queuedBuilds != null) queuedBuilds.forEach(buildPromotion -> processor.processItem(buildPromotion)); if (runningBuilds != null) runningBuilds.forEach(buildPromotion -> processor.processItem(buildPromotion)); finishedBuilds.forEach(buildPromotion -> processor.processItem(buildPromotion)); }; } } } final String testOccurrence = locator.getSingleDimensionValue(TEST_OCCURRENCE); if (testOccurrence != null) { TestOccurrenceFinder testOccurrenceFinder = myServiceLocator.getSingletonService(TestOccurrenceFinder.class); return FinderDataBinding.getItemHolder(testOccurrenceFinder.getItems(testOccurrence).myEntries.stream().map(sTestRun -> sTestRun.getBuild().getBuildPromotion())); } SBuildAgent agent; final String agentLocator = locator.getSingleDimensionValue(AGENT); if (agentLocator != null) { List agents = myAgentFinder.getItemsNotEmpty(agentLocator).myEntries; if (agents.size() == 1) { agent = agents.get(0); } else { //only if builds processor cannot handle this Stream result = Stream.empty(); Locator stateLocator = getStateLocator(locator); if (isStateIncluded(stateLocator, STATE_QUEUED)) { //todo: should sort backwards as currently the order does not seem right... result = Stream.concat(result, myBuildQueue.getItems().stream().filter(build -> !CollectionsUtil.intersect(build.getCanRunOnAgents(), agents).isEmpty()) .map(build -> build.getBuildPromotion())); } if (isStateIncluded(stateLocator, STATE_RUNNING)) { //todo: address an issue when a build can appear twice in the output // agent instance can be different when disconnecting, so need to check id Set agentIds = agents.stream().map(a -> a.getId()).collect(Collectors.toSet()); Set agentNames = agents.stream().map(a -> a.getName()).collect(Collectors.toSet()); result = Stream.concat(result, myBuildsManager.getRunningBuilds().stream().filter(build -> { SBuildAgent buildAgent = build.getAgent(); int agentId = buildAgent.getId(); return agentId > 0 ? agentIds.contains(agentId) : agentNames.contains(buildAgent.getName()); }).map(build -> build.getBuildPromotion())); } if (isStateIncluded(stateLocator, STATE_FINISHED)) { //todo: optimize for user and canceled Stream finishedBuilds = StreamUtil.merge( agents.stream().map(a -> a.getBuildHistory(null, true).stream().map(b -> b.getBuildPromotion())), BUILD_PROMOTIONS_COMPARATOR); result = Stream.concat(result, finishedBuilds); } return FinderDataBinding.getItemHolder(result); } } else { agent = null; } // process by build states final ArrayList result = new ArrayList(); @Nullable Set buildTypes = getBuildTypes(locator); String agentName; String agentNameCondition = locator.lookupSingleDimensionValue(AGENT_NAME); if (agentNameCondition != null) { agentName = ParameterCondition.createValueCondition(agentNameCondition).getConstantValueIfSimpleEqualsCondition(); if (agentName != null) locator.markUsed(Collections.singleton(AGENT_NAME)); } else { agentName = null; } Long agentTypeId = locator.getSingleDimensionValueAsLong(AGENT_TYPE_ID); Locator stateLocator = getStateLocator(locator); if (isStateIncluded(stateLocator, STATE_QUEUED)) { //todo: should sort backwards as currently the order does not seem right... Stream builds = myBuildQueue.getItems().stream(); if (buildTypes != null) { //make sure buildTypes retrieved from the locator are used builds = builds.filter(qb -> buildTypes.contains(qb.getBuildPromotion().getParentBuildType())); } if (agentTypeId != null) { builds = builds.filter(build -> build.getCanRunOnAgents().stream().anyMatch(a -> a.getAgentTypeId() == agentTypeId.intValue())); } if (agent != null) { builds = builds.filter(build -> build.getCanRunOnAgents().stream().anyMatch(a -> a.equals(agent))); //todo: check } if (agentName != null) { builds = builds.filter(build -> build.getCanRunOnAgents().stream().anyMatch(a -> a.getName().equals(agentName))); } result.addAll(builds.map(b -> b.getBuildPromotion()).collect(Collectors.toList())); } if (isStateIncluded(stateLocator, STATE_RUNNING)) { //todo: address an issue when a build can appear twice in the output Stream builds = myBuildsManager.getRunningBuilds().stream(); if (buildTypes != null) { //make sure buildTypes retrieved from the locator are used builds = builds.filter(b -> buildTypes.contains(b.getBuildPromotion().getParentBuildType())); } if (agentTypeId != null) { builds = builds.filter(build -> build.getAgent().getAgentTypeId() == agentTypeId.intValue()); } if (agent != null) { builds = builds.filter(build -> { SBuildAgent buildAgent = build.getAgent(); int agentId = buildAgent.getId(); return agentId > 0 ? agent.getId() == agentId : agent.getName().equals(buildAgent.getName()); }); } if (agentName != null) { builds = builds.filter(build -> build.getAgentName().equals(agentName)); } result.addAll(builds.map(qb -> qb.getBuildPromotion()).collect(Collectors.toList())); } ItemHolder finishedBuilds = null; if (isStateIncluded(stateLocator, STATE_FINISHED)) { final BuildQueryOptions options = new BuildQueryOptions(); if (buildTypes != null) { options.setBuildTypeIds(buildTypes.stream().map(bt -> bt.getBuildTypeId()).collect(Collectors.toList())); } if (agentTypeId != null) { options.setAgentTypeId(agentTypeId.intValue()); } if (agent != null) { options.setAgent(agent); } if (agentName != null) { options.setAgentName(agentName); } final Boolean personal = locator.lookupSingleDimensionValueAsBoolean(PERSONAL); if (personal == null || personal) { final String userDimension = locator.getSingleDimensionValue(USER); options.setIncludePersonal(true, userDimension == null ? null : myUserFinder.getItem(userDimension)); } else { options.setIncludePersonal(false, null); } final Boolean failedToStart = locator.lookupSingleDimensionValueAsBoolean(FAILED_TO_START); final Boolean canceled = locator.lookupSingleDimensionValueAsBoolean(CANCELED); if (canceled == null || canceled || failedToStart == null || failedToStart) { options.setIncludeCanceled(true); //also includes failed to start builds, TW-32060 } else { options.setIncludeCanceled(false); } TagFinder.FilterOptions tagFilterOptions = TagFinder.getFilterOptions(locator.lookupDimensionValue(TAG), myServiceLocator); if (tagFilterOptions != null) { options.setTagName(tagFilterOptions.getTagName(), tagFilterOptions.getTagOwner()); } final String branchLocatorValue = locator.lookupSingleDimensionValue(BRANCH); // do not mark dimension as used as not all can be used from it if (branchLocatorValue != null) { BranchFinder.BranchFilterDetails branchFilterDetails; try { branchFilterDetails = myBranchFinder.getBranchFilterDetailsWithoutLocatorCheck(branchLocatorValue); // parsed OK, setting options if (branchFilterDetails.isAnyBranch()) { options.setMatchAllBranches(true); } else { if (branchFilterDetails.isDefaultBranchOrNotBranched()) { options.setMatchAllBranches(false); options.setBranch(Branch.DEFAULT_BRANCH_NAME); } if (branchFilterDetails.getBranchName() != null) { options.setMatchAllBranches(false); options.setBranch(branchFilterDetails.getBranchName()); } } } catch (LocatorProcessException e) { // not parsed, cannot extract name or default status options.setMatchAllBranches(true); } } else { options.setMatchAllBranches(true); } options.setIncludeRunning(false); //running builds are retrieved separately and appear before finished ones options.setOrderByChanges(false); finishedBuilds = new ItemHolder() { public void process(@NotNull final ItemProcessor processor) { myBuildsManager.processBuilds(options, new ItemProcessor() { public boolean processItem(SBuild item) { return processor.processItem(item.getBuildPromotion()); } }); } }; } stateLocator.checkLocatorFullyProcessed(); final ItemHolder finishedBuildsFinal = finishedBuilds; return new ItemHolder() { public void process(@NotNull final ItemProcessor processor) { new CollectionItemHolder(result).process(processor); if (finishedBuildsFinal != null) { finishedBuildsFinal.process(processor); } } }; } private HashSet getBuildTypes(final @NotNull Locator locator) { SProject project = getProjectFromDimension(locator, PROJECT); final String buildTypeLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (buildTypeLocator != null) { List result = myBuildTypeFinder.getBuildTypes(project, buildTypeLocator); if (result.isEmpty()) { throw new NotFoundException("No build types found by locator '" + buildTypeLocator + "'" + (project != null ? " in the project " + project.describe(false) : "")); } return new HashSet<>(result); } if (project != null) { List result = project.getOwnBuildTypes(); if (result.isEmpty()) { throw new NotFoundException("No build types found in the project " + project.describe(false)); } return new HashSet<>(result); } SProject affectedProject = getProjectFromDimension(locator, AFFECTED_PROJECT); if (affectedProject != null && !affectedProject.isRootProject()) { List result = affectedProject.getBuildTypes(); if (result.isEmpty()) { throw new NotFoundException("No build types found under the affected project " + affectedProject.describe(false)); } return new HashSet<>(result); } return null; } @NotNull private Iterator getBuildMetadataEntryIterator(@NotNull final String metadataLocatorText) { Locator metadataLocator = new Locator(metadataLocatorText, "providerId", "key"); String providerId = metadataLocator.getSingleDimensionValue("providerId"); if (providerId == null) { throw new BadRequestException("Metadata locator '" + metadataLocatorText + "' should contain '" + "providerId" + "' dimension."); } String key = metadataLocator.getSingleDimensionValue("key"); metadataLocator.checkLocatorFullyProcessed(); return key == null ? myMetadataStorage.getAllEntries(providerId) : myMetadataStorage.getEntriesByKey(providerId, key); } @NotNull private List getPartialLocatorsForStrobDimension(@NotNull final List resultStrobBuildLocators, @NotNull final Locator strobLocator, @NotNull final String strobDimension, @NotNull final Finder strobTypeFinder) { final String strobDimensionValue = strobLocator.getSingleDimensionValue(strobDimension); if (strobDimensionValue == null) { return resultStrobBuildLocators; } List result = new ArrayList<>(); for (Locator strobBuildLocator : resultStrobBuildLocators) { String patchedStrobDimensionValue = strobDimensionValue; // preprocess locator before use if (BUILD_TYPE.equals(strobDimension)) { patchedStrobDimensionValue = Locator.setDimensionIfNotPresent(strobDimensionValue, BuildTypeFinder.TEMPLATE_FLAG_DIMENSION_NAME, "false"); } else { final String buildTypeLocator = strobBuildLocator.getSingleDimensionValue(BUILD_TYPE); if (BRANCH.equals(strobDimension) && buildTypeLocator != null) { patchedStrobDimensionValue = Locator.setDimensionIfNotPresent(strobDimensionValue, BranchFinder.BUILD_TYPE, buildTypeLocator); } } final PagedSearchResult items = strobTypeFinder.getItems(patchedStrobDimensionValue); for (ITEM item : items.myEntries) { result.add(new Locator(strobBuildLocator).setDimensionIfNotPresent(strobDimension, strobTypeFinder.getCanonicalLocator(item))); } } return result; } private void setLocatorDefaults(@NotNull final Locator locator) { if (locator.isSingleValue()) { return; } final Boolean defaultFiltering = locator.getSingleDimensionValueAsBoolean(DEFAULT_FILTERING); if (defaultFiltering != null && !defaultFiltering) { return; } //this is added for consistency - currently this never triggers as findSingleItem treats the dimensions without ever calling setLocatorDefaults if (defaultFiltering == null && locator.isAnyPresent(DIMENSION_ID, PROMOTION_ID, PROMOTION_ID_ALIAS, BUILD_ID)) { return; } if (!locator.isAnyPresent(STATE, RUNNING)) { locator.setDimension(STATE, STATE_FINISHED); } if (defaultFiltering == null) { // if it is set, then use the value ("true" if we got there) if (TeamCityProperties.getBooleanOrTrue("rest.buildPromotionFinder.varyingDefaults")) { if (locator.isAnyPresent(AGENT, AGENT_NAME, USER, HANGING, EQUIVALENT)) { // users usually expect that any build will be returned for such locators, see TW-45140 return; } Locator stateLocator = getStateLocator(new Locator(locator)); //creating locator copy here not to affect used dimensions if (!isStateIncluded(stateLocator, STATE_FINISHED)) { // also including all builds if the only requested are running or queued builds return; } } } locator.setDimensionIfNotPresent(PERSONAL, "false"); locator.setDimensionIfNotPresent(CANCELED, "false"); locator.setDimensionIfNotPresent(FAILED_TO_START, "false"); if (defaultFiltering != null || !locator.isAnyPresent(SNAPSHOT_DEP, EQUIVALENT, ORDERED)) { //do not force branch to default for some locators locator.setDimensionIfNotPresent(BRANCH, myBranchFinder.getDefaultBranchLocator()); } } @NotNull public BuildPromotion getBuildPromotion(final @Nullable SBuildType buildType, @Nullable final String locatorText) { if (buildType == null) { return getItem(locatorText); } final Locator locator = locatorText != null ? new Locator(locatorText) : Locator.createEmptyLocator(); if (locator.isEmpty() || !locator.isSingleValue()) { return getItem(patchLocatorWithBuildType(buildType, locator)); } //single value locator //use logic like BuildFinder: if there is build type and single value, assume it's build number final String buildNumber = locator.getSingleValue(); assert buildNumber != null; SBuild build = myBuildsManager.findBuildInstanceByBuildNumber(buildType.getInternalId(), buildNumber); if (build != null) return build.getBuildPromotion(); throw new NotFoundException("No build can be found by number '" + buildNumber + "' in the build type with id '" + buildType.getExternalId() + "'"); /* final BuildPromotion singleItem = findSingleItem(locator); if (singleItem != null) { //will find it the regular way, go for it with all due checks return getItem(locator.getStringRepresentation()); } */ } @NotNull public PagedSearchResult getBuildPromotions(final @Nullable SBuildType buildType, final @Nullable String locatorText) { if (buildType == null) { return getItems(locatorText); } final Locator locator = locatorText != null ? new Locator(locatorText) : Locator.createEmptyLocator(); if (locator.isEmpty() || !locator.isSingleValue()) { return getItems(patchLocatorWithBuildType(buildType, locator)); //todo: test empty locator with not empty build type } //single value return new PagedSearchResult(Collections.singletonList(getBuildPromotion(buildType, locatorText)), null, null); } @NotNull private String patchLocatorWithBuildType(@Nullable final SBuildType buildType, @NotNull final Locator locator) { if (buildType != null) { return locator.setDimensionIfNotPresent(BuildPromotionFinder.BUILD_TYPE, BuildTypeFinder.getLocator(buildType)).getStringRepresentation(); } return locator.getStringRepresentation(); } @NotNull private List getSnapshotRelatedBuilds(@NotNull final String snapshotDepDimension) { final GraphFinder graphFinder = new GraphFinder(this, SNAPSHOT_DEPENDENCIES_TRAVERSER); final List result = graphFinder.getItems(snapshotDepDimension).myEntries; Collections.sort(result, BUILD_PROMOTIONS_COMPARATOR); return result; //todo: patch branch locator, personal, etc.??? } @NotNull private List getArtifactRelatedBuilds(@NotNull final String depDimension) { final GraphFinder graphFinder = new GraphFinder(this, ARTIFACT_DEPENDENCIES_TRAVERSER); final List result = graphFinder.getItems(depDimension).myEntries; Collections.sort(result, BUILD_PROMOTIONS_COMPARATOR); return result; //todo: patch branch locator, personal, etc.??? } @NotNull private Locator createStateLocator(@NotNull final String stateDimension) { final Locator locator = new Locator(stateDimension, Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME, STATE_QUEUED, STATE_RUNNING, STATE_FINISHED); if (locator.isSingleValue()) { //check single value validity if (!stateDimension.equals(STATE_QUEUED) && !stateDimension.equals(STATE_RUNNING) && !stateDimension.equals(STATE_FINISHED) && !stateDimension.equals(STATE_ANY)) { throw new BadRequestException("Unsupported value of '" + STATE + "' dimension: '" + stateDimension + "'. Should be one of the build states('" + STATE_QUEUED + "', '" + STATE_RUNNING + "', '" + STATE_FINISHED + "') or '" + STATE_ANY + "'"); } } return locator; } private boolean isStateIncluded(@NotNull final Locator stateLocator, @NotNull final String state) { final String singleValue = stateLocator.getSingleValue(); if (singleValue != null && (STATE_ANY.equals(singleValue) || state.equals(singleValue))) { return true; } //noinspection RedundantIfStatement if (!stateLocator.isSingleValue() && FilterUtil.isIncludedByBooleanFilter(stateLocator.getSingleDimensionValueAsBoolean(state, false), true)) { return true; } return false; } public static boolean buildIdDiffersFromPromotionId(@NotNull final BuildPromotion buildPromotion) { return buildPromotion.getAssociatedBuildId() != null && buildPromotion.getId() != buildPromotion.getAssociatedBuildId(); } private static class BuildPromotionComparator implements Comparator { public int compare(final BuildPromotion o1, final BuildPromotion o2) { final SQueuedBuild qb1 = o1.getQueuedBuild(); final SQueuedBuild qb2 = o2.getQueuedBuild(); if (qb1 != null) { if (qb2 != null) { return -qb1.getItemId().compareTo(qb2.getItemId()); } return -1; } if (qb2 != null) { return 1; } final SBuild b1 = o1.getAssociatedBuild(); final SBuild b2 = o2.getAssociatedBuild(); if (b1 != null) { if (b2 != null) { if (b1.isFinished()) { if (b2.isFinished()) { final int resultByStartDate = b1.getStartDate().compareTo(b2.getStartDate()); if (resultByStartDate == 0) return -Long.valueOf(b1.getBuildId()).compareTo(b2.getBuildId()); return -resultByStartDate; } return 1; } if (b2.isFinished()) { return -1; } final int resultByStartDate = b1.getStartDate().compareTo(b2.getStartDate()); if (resultByStartDate == 0) return -Long.valueOf(b1.getBuildId()).compareTo(b2.getBuildId()); return -resultByStartDate; } return 1; } return -1; } } private static class SnapshotDepsTraverser implements GraphFinder.Traverser { @NotNull public GraphFinder.LinkRetriever getChildren() { return new GraphFinder.LinkRetriever() { @NotNull public List getLinked(@NotNull final BuildPromotion item) { return CollectionsUtil.convertCollection(item.getDependencies(), new Converter() { public BuildPromotion createFrom(@NotNull final BuildDependency source) { return source.getDependOn(); } }); } }; } @NotNull public GraphFinder.LinkRetriever getParents() { return new GraphFinder.LinkRetriever() { @NotNull public List getLinked(@NotNull final BuildPromotion item) { return CollectionsUtil.convertCollection(item.getDependedOnMe(), new Converter() { public BuildPromotion createFrom(@NotNull final BuildDependency source) { return source.getDependent(); } }); } }; } } private static class ArtifactDepsTraverser implements GraphFinder.Traverser { @NotNull public GraphFinder.LinkRetriever getChildren() { return new GraphFinder.LinkRetriever() { @NotNull public List getLinked(@NotNull final BuildPromotion item) { //see also jetbrains.buildServer.server.rest.model.build.Build.getBuildArtifactDependencies SBuild build = item.getAssociatedBuild(); if (build == null) return Collections.emptyList(); return build.getDownloadedArtifacts().getArtifacts().keySet().stream().map(v -> ((SBuild)v).getBuildPromotion()) .sorted(new Build.BuildPromotionDependenciesComparator()).collect(Collectors.toList()); } }; } @NotNull public GraphFinder.LinkRetriever getParents() { return new GraphFinder.LinkRetriever() { @NotNull public List getLinked(@NotNull final BuildPromotion item) { SBuild build = item.getAssociatedBuild(); if (build == null) return Collections.emptyList(); return build.getProvidedArtifacts().getArtifacts().keySet().stream().map(v -> ((SBuild)v).getBuildPromotion()) .sorted(new Build.BuildPromotionDependenciesComparator()).collect(Collectors.toList()); } }; } } private class BuildPromotionOrderedFinder extends GraphFinder { public BuildPromotionOrderedFinder(@NotNull final BuildPromotionFinder finder) { super(finder, new BuildPromotionOrderSupportTraverser()); } @Override protected void collectLinked(@NotNull final Set result, @NotNull final Collection toProcess, @NotNull final Collection stopItems, final Long lookupLimit, @NotNull final LinkRetriever linkRetriever, final boolean recursive) { if (!recursive) { throw new BadRequestException("Builds traversal is only supported in 'recursive:true' mode"); } for (BuildPromotion item : toProcess) { if (stopItems.contains(item)) { result.add(item); } else { final List linked = linkRetriever.getLinked(item); for (BuildPromotion promotion : linked) { result.add(promotion); if (stopItems.contains(item)) { break; } } } } } } private class BuildPromotionOrderSupportTraverser implements GraphFinder.Traverser { @NotNull @Override public GraphFinder.LinkRetriever getChildren() { return new GraphFinder.LinkRetriever() { @NotNull @Override public List getLinked(@NotNull final BuildPromotion item) { final SBuildType buildType = item.getBuildType(); if (buildType == null) { return Collections.emptyList(); } final SBuild associatedBuild = item.getAssociatedBuild(); if (associatedBuild != null) { final List buildsBefore = ((BuildTypeEx)buildType).getBuildTypeOrderedBuilds().getBuildsBefore(associatedBuild); //consider performance optimization by converting id to build only on actual retrieve return CollectionsUtil.convertAndFilterNulls(buildsBefore, new Converter() { @Override public BuildPromotion createFrom(@NotNull final OrderedBuild source) { return myBuildPromotionManager.findPromotionById(source.getPromotionId()); } }); } else { final Branch branch = item.getBranch(); if (branch == null) { return Collections.emptyList(); } final List buildsBefore = ((BuildTypeEx)buildType).getBuildTypeOrderedBuilds().getBuildsBeforeInBranches(item, new Filter() { @Override public boolean accept(@NotNull final String data) { return true; } }); return CollectionsUtil.convertAndFilterNulls(buildsBefore, new Converter() { @Override public BuildPromotion createFrom(@NotNull final OrderedBuild source) { return myBuildPromotionManager.findPromotionById(source.getPromotionId()); } }); } } }; } @NotNull @Override public GraphFinder.LinkRetriever getParents() { return new GraphFinder.LinkRetriever() { @NotNull @Override public List getLinked(@NotNull final BuildPromotion item) { final SBuildType buildType = item.getBuildType(); if (buildType == null) { return Collections.emptyList(); } final SBuild associatedBuild = item.getAssociatedBuild(); if (associatedBuild != null) { final List buildsBefore = ((BuildTypeEx)buildType).getBuildTypeOrderedBuilds().getBuildsAfter(associatedBuild); return CollectionsUtil.convertAndFilterNulls(buildsBefore, new Converter() { @Override public BuildPromotion createFrom(@NotNull final OrderedBuild source) { return myBuildPromotionManager.findPromotionById(source.getPromotionId()); } }); } else { return Collections.emptyList(); } } }; } } @NotNull public ItemsProviders.ItemsProvider getLazyResult() { return new ItemsProviders.ItemsProvider() { @NotNull @Override public List getItems(@Nullable final String locatorText) { return BuildPromotionFinder.this.getItems(locatorText).myEntries; } @Nullable @Override public Integer getCheapCount(@Nullable final String locatorText) { /* This emulates build's isUsedByOtherBuilds() via a request like: .../app/rest/builds?fields=build(id,related(builds(count,$locator(count:1,defaultFilter:false,item:(count:1,defaultFilter:false,snapshotDependency:(from:(id:$context.build.id),recursive:false)),item:(count:1,defaultFilter:false,artifactDependency:(from:(id:$context.build.id),recursive:false)))))) */ Locator locator; try { locator = createLocator(locatorText, null); } catch (Exception e) { return null; } setLocatorDefaults(locator); Long count = locator.getSingleDimensionValueAsLong(PagerData.COUNT); Integer result = null; final List itemDimension = locator.getDimensionValue(DIMENSION_ITEM); if (!itemDimension.isEmpty()) { if (count != null) { int[] max = new int[1]; max[0] = -1; boolean exceedsCount = itemDimension.stream().map(l -> getCheapCount(l)).anyMatch(cheapCount -> { if (cheapCount == null) { max[0] = -2; //there is not cheap count return false; } if (max[0] != -2) { max[0] = Math.max(max[0], cheapCount); } return cheapCount >= count; }); if (exceedsCount) { result = count.intValue(); } else if (max[0] == 0) { //all counts were cheap and all were 0 result = 0; } } } else { //optimization method counts all builds and these can be set by defaultFilter if (locator.getSingleDimensionValueAsBoolean(PERSONAL) != null) return null; if (locator.getSingleDimensionValueAsBoolean(CANCELED) != null) return null; if (locator.getSingleDimensionValueAsBoolean(FAILED_TO_START) != null) return null; if (locator.getSingleDimensionValueAsBoolean(BRANCH) != null) return null; final String snapshotDepDimension = locator.getSingleDimensionValue(SNAPSHOT_DEP); if (snapshotDepDimension != null) { result = getSnapshotRelatedBuildsCheapCount(snapshotDepDimension, count == null ? null : count.intValue()); } if (result == null) { if (count == null) { return null; } final String artifactDepDimension = locator.getSingleDimensionValue(ARTIFACT_DEP); if (artifactDepDimension != null) { result = getArtifactRelatedBuildsCheapCount(artifactDepDimension, count.intValue()); } } } if (!locator.getUnusedDimensions().isEmpty()) return null; if (result == null) return null; return count != null ? Math.min(count.intValue(), result) : result; } }; } @Nullable private Integer getSnapshotRelatedBuildsCheapCount(@NotNull final String snapshotDepDimension, @Nullable final Integer limitingCount) { final GraphFinder graphFinder = new GraphFinder(this, SNAPSHOT_DEPENDENCIES_TRAVERSER); GraphFinder.ParsedLocator parsedLocator = graphFinder.getParsedLocator(snapshotDepDimension); Integer count = parsedLocator.getCount(); if (count == null && limitingCount != null) { count = limitingCount; } else if (count != null && limitingCount != null) { count = Math.min(count, limitingCount); } List fromItems = parsedLocator.getFromItems(); Optional maxOptional = fromItems.stream().map(b -> b.getNumberOfDependedOnMe()).max(Integer::compare); //cannot sum as they can intersect, so using max //can optimize by finding first greater then "count" if set int result = 0; if (maxOptional.isPresent()) { result = maxOptional.get(); } boolean recursive = parsedLocator.isRecursive(); if (parsedLocator.isIncludeInitial()) result++; if (!parsedLocator.isAllDimensionsUsed()) return null; if (count != null && count <= result) return count; if (!recursive) { return count != null ? Math.min(count, result) : result; } return null; } @Nullable private Integer getArtifactRelatedBuildsCheapCount(@NotNull final String artifactDepDimension, @Nullable final Integer limitingCount) { final GraphFinder graphFinder = new GraphFinder(this, ARTIFACT_DEPENDENCIES_TRAVERSER); GraphFinder.ParsedLocator parsedLocator = graphFinder.getParsedLocator(artifactDepDimension); Integer count = parsedLocator.getCount(); if (count == null && limitingCount != null) { count = limitingCount; } else if (count != null && limitingCount != null) { count = Math.min(count, limitingCount); } if (count == null || count > 2) return null; List fromItems = parsedLocator.getFromItems(); DownloadedArtifactsLogger artifactsLogger = myServiceLocator.getSingletonService(DownloadedArtifactsLogger.class); if (fromItems.stream().map(b -> b.getAssociatedBuildId()).filter(Objects::nonNull).noneMatch(id -> artifactsLogger.buildArtifactsWereDownloaded(id))) { return 0; } int result = 1; parsedLocator.isRecursive(); //just all and mark as used - can only increase the number if (parsedLocator.isIncludeInitial()) result++; if (!parsedLocator.isAllDimensionsUsed()) return null; if (count <= result) return count; return null; } }