/* * 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.intellij.openapi.diagnostic.Logger; import java.util.*; import java.util.function.Predicate; 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.errors.AuthorizationFailedException; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.server.rest.model.change.VcsRoot; import jetbrains.buildServer.server.rest.request.Constants; import jetbrains.buildServer.server.rest.util.BuildTypeOrTemplate; import jetbrains.buildServer.serverSide.ProjectManager; import jetbrains.buildServer.serverSide.SBuildType; import jetbrains.buildServer.serverSide.SProject; import jetbrains.buildServer.serverSide.auth.Permission; import jetbrains.buildServer.serverSide.versionedSettings.VersionedSettingsManager; import jetbrains.buildServer.util.CollectionsUtil; import jetbrains.buildServer.vcs.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Yegor.Yarko * Date: 23.03.13 */ public class VcsRootInstanceFinder extends AbstractFinder { private static final Logger LOG = Logger.getInstance(VcsRootInstanceFinder.class.getName()); public static final String VCS_ROOT_DIMENSION = "vcsRoot"; public static final String REPOSITORY_ID_STRING = "repositoryIdString"; protected static final String TYPE = "type"; protected static final String PROJECT = "project"; protected static final String AFFECTED_PROJECT = "affectedProject"; protected static final String PROPERTY = "property"; protected static final String BUILD_TYPE = "buildType"; protected static final String BUILD = "build"; protected static final String STATUS = "status"; protected static final String FINISH_VCS_CHECKING_FOR_CHANGES = "checkingForChangesFinishDate"; // experimental protected static final String REPOSITORY_STATE = "repositoryState"; // experimental protected static final String HAS_VERSIONED_SETTINGS_ONLY = "versionedSettings"; //whether to include usages in project's versioned settings or not. By default "false" if "buildType" dimension is present and "any" otherwise protected static final String COMMIT_HOOK_MODE = "commitHookMode"; // experimental protected static final Comparator VCS_ROOT_INSTANCE_COMPARATOR = new Comparator() { public int compare(final VcsRootInstance o1, final VcsRootInstance o2) { return (int)(o1.getId() - o2.getId()); } }; @NotNull private final VcsRootFinder myVcsRootFinder; @NotNull private final VcsManager myVcsManager; @NotNull private final ProjectFinder myProjectFinder; @NotNull private final BuildTypeFinder myBuildTypeFinder; @NotNull private final ProjectManager myProjectManager; @NotNull private final PermissionChecker myPermissionChecker; @NotNull private final VersionedSettingsManager myVersionedSettingsManager; @NotNull private final TimeCondition myTimeCondition; @NotNull private final ServiceLocator myServiceLocator; public VcsRootInstanceFinder(@NotNull VcsRootFinder vcsRootFinder, @NotNull VcsManager vcsManager, @NotNull ProjectFinder projectFinder, @NotNull BuildTypeFinder buildTypeFinder, @NotNull ProjectManager projectManager, @NotNull VersionedSettingsManager versionedSettingsManager, @NotNull TimeCondition timeCondition, final @NotNull PermissionChecker permissionChecker, @NotNull final ServiceLocator serviceLocator) { super(DIMENSION_ID, TYPE, PROJECT, AFFECTED_PROJECT, PROPERTY, REPOSITORY_ID_STRING, BUILD_TYPE, BUILD, VCS_ROOT_DIMENSION, HAS_VERSIONED_SETTINGS_ONLY, Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); myServiceLocator = serviceLocator; myVersionedSettingsManager = versionedSettingsManager; myTimeCondition = timeCondition; setHiddenDimensions(STATUS, FINISH_VCS_CHECKING_FOR_CHANGES, REPOSITORY_STATE, COMMIT_HOOK_MODE); myVcsRootFinder = vcsRootFinder; myVcsManager = vcsManager; myProjectFinder = projectFinder; myBuildTypeFinder = buildTypeFinder; myProjectManager = projectManager; myPermissionChecker = permissionChecker; } @Override public Long getDefaultPageItemsCount() { return (long)Constants.getDefaultPageItemsCount(); } @NotNull @Override public String getItemLocator(@NotNull final VcsRootInstance vcsRootInstance) { return VcsRootInstanceFinder.getLocator(vcsRootInstance); } @NotNull public static String getLocator(@NotNull final VcsRootInstance vcsRootInstance) { return Locator.getStringLocator(DIMENSION_ID, String.valueOf(vcsRootInstance.getId())); } @NotNull public static String getLocatorByVcsRoot(@NotNull final SVcsRoot vcsRoot) { return Locator.getStringLocator(VCS_ROOT_DIMENSION, VcsRootFinder.getLocator(vcsRoot)); } @Nullable @Override public VcsRootInstance findSingleItem(@NotNull final Locator locator) { if (locator.isSingleValue()) { // no dimensions found, assume it's root instance id return getVcsRootInstanceById(locator.getSingleValueAsLong()); } final Long id = locator.getSingleDimensionValueAsLong(DIMENSION_ID); if (id != null) { return getVcsRootInstanceById(id); } return null; } @NotNull private VcsRootInstance getVcsRootInstanceById(final @Nullable Long parsedId) { if (parsedId == null) { throw new BadRequestException("Expecting VCS root instance id, found empty value."); } VcsRootInstance root = myVcsManager.findRootInstanceById(parsedId); if (root == null) { throw new NotFoundException("No VCS root instance can be found by id '" + parsedId + "'."); } checkPermission(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, root); return root; } private void setLocatorDefaults(@NotNull final Locator locator) { if (locator.isSingleValue()) { return; } if (locator.isAnyPresent(BUILD_TYPE)) { locator.setDimensionIfNotPresent(HAS_VERSIONED_SETTINGS_ONLY, "false"); } } @NotNull @Override public ItemFilter getFilter(@NotNull final Locator locator) { final MultiCheckerFilter result = new MultiCheckerFilter(); result.add(item -> hasPermission(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, item)); final String type = locator.getSingleDimensionValue(TYPE); if (type != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return type.equals(item.getVcsName()); } }); } if (locator.isUnused(BUILD)) { final String build = locator.getSingleDimensionValue(BUILD); if (build != null) { Set vcsRootInstanceIds = getVcsRootInstancesByBuilds(build).map(vcsRE -> vcsRE.getId()).collect(Collectors.toSet()); result.add(item -> vcsRootInstanceIds.contains(item.getId())); } } if (locator.isUnused(VCS_ROOT_DIMENSION)) { final String vcsRootLocator = locator.getSingleDimensionValue(VCS_ROOT_DIMENSION); if (vcsRootLocator != null) { final List vcsRoots = myVcsRootFinder.getItems(vcsRootLocator).myEntries; result.add(item -> vcsRoots.contains(item.getParent())); } } //todo: rework to be "there are usages directly in the project", also add to getPrefilteredItems //todo: support usage as versioned settings root final String projectLocator = locator.getSingleDimensionValue(PROJECT); //todo: support multiple here for "from all not archived projects" case if (projectLocator != null) { final SProject project = myProjectFinder.getItem(projectLocator); VcsRootInstance settingsInstance = myVersionedSettingsManager.getVersionedSettingsVcsRootInstance(project); final Boolean nonVersionedSettings = locator.lookupSingleDimensionValueAsBoolean(HAS_VERSIONED_SETTINGS_ONLY); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return project.equals(VcsRoot.getProjectByRoot(item.getParent())) || //todo: rework project dimensions for the instance to mean smth. more meaningful (nonVersionedSettings == null || nonVersionedSettings) && item.equals(settingsInstance); } }); } final String repositoryIdString = locator.getSingleDimensionValue(REPOSITORY_ID_STRING); if (repositoryIdString != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return VcsRootFinder.repositoryIdStringMatches(item, repositoryIdString, myVcsManager); } }); } final List properties = locator.getDimensionValue(PROPERTY); if (!properties.isEmpty()) { final Matcher parameterCondition = ParameterCondition.create(properties); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return parameterCondition.matches(new AbstractMapParametersProvider(item.getProperties())); } }); } final String status = locator.getSingleDimensionValue(STATUS); if (status != null) { TypedFinderBuilder builder = new TypedFinderBuilder(); builder.dimensionEnum(TypedFinderBuilder.Dimension.single(), VcsRootStatus.Type.class).description("status of the VCS root instance"). valueForDefaultFilter(root -> root.getStatus().getType()); final TypedFinderBuilder statusFilterBuilder = new TypedFinderBuilder(); statusFilterBuilder.dimensionEnum(new TypedFinderBuilder.Dimension<>("status"), VcsRootStatus.Type.class).description("type of operation") .valueForDefaultFilter(vcsRootCheckStatus -> vcsRootCheckStatus.myStatus.getType()); statusFilterBuilder.dimensionTimeCondition(new TypedFinderBuilder.Dimension<>("timestamp"), myTimeCondition).description("time of the operation start") .valueForDefaultFilter(vcsRootCheckStatus -> vcsRootCheckStatus.myStatus.getTimestamp()); statusFilterBuilder.dimensionEnum(new TypedFinderBuilder.Dimension<>("requestorType"), OperationRequestor.class).description("requestor of the operation") .valueForDefaultFilter(vcsRootCheckStatus -> vcsRootCheckStatus.myRequestor); Finder vcsRootCheckStatusFinder = statusFilterBuilder.build(); builder.dimensionFinderFilter(new TypedFinderBuilder.Dimension<>("current"), vcsRootCheckStatusFinder, "VCS check status condition") .description("current VCS root status").valueForDefaultFilter(root -> new VcsRootCheckStatus(root.getStatus(), root.getLastRequestor())); builder.dimensionFinderFilter(new TypedFinderBuilder.Dimension<>("previous"), vcsRootCheckStatusFinder, "VCS check status condition") .description("previous VCS root status").valueForDefaultFilter(root -> new VcsRootCheckStatus(root.getPreviousStatus(), null)); final ItemFilter filter = builder.build().getFilter(status); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return filter.isIncluded((VcsRootInstanceEx)item); } }); } final Boolean commitHookMode = locator.getSingleDimensionValueAsBoolean("commitHookMode"); if (commitHookMode != null){ result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return FilterUtil.isIncludedByBooleanFilter(commitHookMode, !((VcsRootInstanceEx)item).isPollingMode()); } }); } TimeCondition.FilterAndLimitingDate finishFiltering = myTimeCondition.processTimeConditions(FINISH_VCS_CHECKING_FOR_CHANGES, locator, (vcsRootInstance) -> getFinishCheckingForChanges(vcsRootInstance), null); if (finishFiltering != null) result.add(finishFiltering.getFilter()); final String repositoryState = locator.getSingleDimensionValue(REPOSITORY_STATE); if (repositoryState != null) { TypedFinderBuilder builder = new TypedFinderBuilder(); builder.dimensionTimeCondition(new TypedFinderBuilder.Dimension<>("timestamp"), myTimeCondition).description("time of the repository state creation"). valueForDefaultFilter(item -> item.getCreateTimestamp()); builder.dimensionValueCondition(new TypedFinderBuilder.Dimension<>("branchName")).description("branch name").filter((valueCondition, item) -> { for (String branchName : item.getBranchRevisions().keySet()) { if (valueCondition.matches(branchName)) return true; } return false; }); final ItemFilter filter = builder.build().getFilter(repositoryState); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return filter.isIncluded(((VcsRootInstanceEx)item).getLastUsedState()); } }); } if (locator.isUnused(BUILD_TYPE)) { final String buildTypesLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (buildTypesLocator != null) { Set vcsRootInstances = getInstances(buildTypesLocator, locator.lookupSingleDimensionValueAsBoolean(HAS_VERSIONED_SETTINGS_ONLY)); result.add(item -> vcsRootInstances.contains(item)); } } final String affectedProjectLocator = locator.getSingleDimensionValue(AFFECTED_PROJECT); //todo: support multiple here if (affectedProjectLocator != null) { final Set vcsRootInstances = getVcsRootInstancesUnderProject(myProjectFinder.getItem(affectedProjectLocator), locator.getSingleDimensionValueAsBoolean(HAS_VERSIONED_SETTINGS_ONLY)); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final VcsRootInstance item) { return vcsRootInstances.contains(item); } }); } // should check HAS_VERSIONED_SETTINGS_ONLY only in prefiltered items as it should consider the current scope - no way to filter in Filter return result; } @NotNull private Stream getVcsRootInstancesByBuilds(@NotNull final String buildsLocator) { return myServiceLocator.getSingletonService(BuildPromotionFinder.class).getItemsNotEmpty(buildsLocator).myEntries.stream(). flatMap(buildPromotion -> buildPromotion.getVcsRootEntries().stream().map(vcsE -> vcsE.getVcsRoot())).distinct(); } private static class VcsRootCheckStatus { @NotNull final VcsRootStatus myStatus; @Nullable final OperationRequestor myRequestor; public VcsRootCheckStatus(@NotNull final VcsRootStatus status, @Nullable final OperationRequestor requestor) { myStatus = status; myRequestor = requestor; } } private static Date getFinishCheckingForChanges(@NotNull final VcsRootInstance vcsRootInstance) { return ((VcsRootInstanceEx)vcsRootInstance).getLastFinishChangesCollectingTime(); } @NotNull private List getBuildTypeOrTemplates(@NotNull final String buildTypeLocator) { List buildTypes = myBuildTypeFinder.getItemsNotEmpty(buildTypeLocator).myEntries; buildTypes.forEach(bt -> BuildTypeFinder.check(bt.get(), myPermissionChecker)); return buildTypes; } @NotNull @Override public ItemHolder getPrefilteredItems(@NotNull Locator locator) { setLocatorDefaults(locator); Boolean versionedSettingsUsagesOnly = locator.getSingleDimensionValueAsBoolean(HAS_VERSIONED_SETTINGS_ONLY); // should check it not in Filter as it considers current scope final String build = locator.getSingleDimensionValue(BUILD); if (build != null) { return FinderDataBinding.getItemHolder(getVcsRootInstancesByBuilds(build)); } final String vcsRootLocator = locator.getSingleDimensionValue(VCS_ROOT_DIMENSION); if (vcsRootLocator != null) { final List vcsRoots = myVcsRootFinder.getItemsNotEmpty(vcsRootLocator).myEntries; final Set result = new TreeSet<>(VCS_ROOT_INSTANCE_COMPARATOR); final String buildTypesLocator = locator.getSingleDimensionValue(BUILD_TYPE); Predicate filter; Set projects; if (buildTypesLocator != null) { if (versionedSettingsUsagesOnly == null || !versionedSettingsUsagesOnly) { //is used below in the same condition ItemFilter buildTypeFilter = myBuildTypeFinder.getFilter(buildTypesLocator); filter = sBuildType -> buildTypeFilter.isIncluded(new BuildTypeOrTemplate(sBuildType)); } else { filter = (a) -> true; } if (versionedSettingsUsagesOnly == null || versionedSettingsUsagesOnly) { //is used below in the same condition projects = myBuildTypeFinder.getItemsNotEmpty(buildTypesLocator).myEntries.stream().map(BuildTypeOrTemplate::getProject).collect(Collectors.toSet()); } else { projects = null; } } else { filter = (a) -> true; projects = null; } for (SVcsRoot vcsRoot : vcsRoots) { if (versionedSettingsUsagesOnly == null || !versionedSettingsUsagesOnly) { vcsRoot.getUsagesInConfigurations().stream().filter(filter).collect(Collectors.toList()).stream().map(buildType -> buildType.getVcsRootInstanceForParent(vcsRoot)).filter(Objects::nonNull) .filter(rootInstance -> hasPermission(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, rootInstance)) //minor performance optimization not to return roots which will be filtered in the filter .forEach(result::add); } if (versionedSettingsUsagesOnly == null || versionedSettingsUsagesOnly) { Set projectsBySettingsRoot = myVersionedSettingsManager.getProjectsBySettingsRoot(vcsRoot); result.addAll(getSettingsRootInstances(projects == null ? projectsBySettingsRoot : CollectionsUtil.intersect(projectsBySettingsRoot, projects))); } } return getItemHolder(result); } final String buildTypesLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (buildTypesLocator != null) { return getItemHolder(getInstances(buildTypesLocator, versionedSettingsUsagesOnly)); } final String projectLocator = locator.getSingleDimensionValue(AFFECTED_PROJECT); //todo: support multiple here for "from all not archived projects" case if (projectLocator != null) { return getItemHolder(getVcsRootInstancesUnderProject(myProjectFinder.getItem(projectLocator), versionedSettingsUsagesOnly)); } //todo: (TeamCity) open API is there a better way to do this? //if reworked, can use checkPermission(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, item); // when implemented, can also add to jetbrains.buildServer.usageStatistics.impl.providers.StaticServerUsageStatisticsProvider.publishNumberOfVcsRoots() final Set result = new TreeSet<>(VCS_ROOT_INSTANCE_COMPARATOR); if (versionedSettingsUsagesOnly == null || !versionedSettingsUsagesOnly) { for (SBuildType buildType : myProjectManager.getAllBuildTypes()) { if (myPermissionChecker.isPermissionGranted(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, buildType.getProjectId())) { result.addAll(buildType.getVcsRootInstances()); } } } if (versionedSettingsUsagesOnly == null || versionedSettingsUsagesOnly) { result.addAll(getSettingsRootInstances(myProjectManager.getProjects())); } return getItemHolder(result); } @NotNull private TreeSet getVcsRootInstancesUnderProject(@NotNull final SProject project, @Nullable final Boolean versionedSettingsUsagesOnly) { TreeSet result = new TreeSet<>(VCS_ROOT_INSTANCE_COMPARATOR); if (versionedSettingsUsagesOnly == null || !versionedSettingsUsagesOnly){ result.addAll((project.getVcsRootInstances())); //todo: includes versioned settings??? } if (versionedSettingsUsagesOnly == null || versionedSettingsUsagesOnly){ result.addAll(getSettingsRootInstances(Collections.singleton(project))); result.addAll(getSettingsRootInstances(project.getProjects())); } return result; } @NotNull private Set getInstances(final @NotNull String buildTypesLocator, @Nullable final Boolean versionedSettingsUsagesOnly) { List buildTypes = getBuildTypeOrTemplates(buildTypesLocator); TreeSet result = new TreeSet<>(VCS_ROOT_INSTANCE_COMPARATOR); if (versionedSettingsUsagesOnly == null || !versionedSettingsUsagesOnly) { buildTypes.stream().flatMap(bt -> bt.getVcsRootInstanceEntries().stream()).map(vcsRE -> vcsRE.getVcsRoot()).forEach(result::add); } if (versionedSettingsUsagesOnly == null || versionedSettingsUsagesOnly) { result.addAll(getSettingsRootInstances(buildTypes.stream().map(bt -> bt.getProject()).collect(Collectors.toSet()))); } return result; } //todo: use getAllProjectUsages here? private Set getSettingsRootInstances(@NotNull final Collection projectsInRoot) { Set result = new TreeSet<>(VCS_ROOT_INSTANCE_COMPARATOR); for (SProject project : projectsInRoot) { VcsRootInstance instance = myVersionedSettingsManager.getVersionedSettingsVcsRootInstance(project); if (instance != null) { if (hasPermission(Permission.VIEW_BUILD_CONFIGURATION_SETTINGS, instance)) { //minor performance optimization not to return roots which will be filtered in the filter result.add(instance); } } } return result; } private boolean hasPermission(@NotNull final Permission permission, @NotNull final VcsRootInstance rootInstance) { try { myVcsRootFinder.checkPermission(permission, rootInstance.getParent()); return true; } catch (AuthorizationFailedException e) { return false; } } public void checkPermission(@NotNull final Permission permission, @NotNull final VcsRootInstance rootInstance) { //todo: check and use AuthUtil.hasReadAccessTo(jetbrains.buildServer.serverSide.auth.AuthorityHolder, jetbrains.buildServer.vcs.VcsRootInstance) myVcsRootFinder.checkPermission(permission, rootInstance.getParent()); //todo: make this more precise, currently too demanding } }