/*
* Copyright 2017 ThoughtWorks, Inc.
*
* 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 com.thoughtworks.go.server.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.thoughtworks.go.config.CaseInsensitiveString;
import com.thoughtworks.go.config.MingleConfig;
import com.thoughtworks.go.config.PipelineConfig;
import com.thoughtworks.go.config.TrackingTool;
import com.thoughtworks.go.domain.MaterialRevision;
import com.thoughtworks.go.domain.Pipeline;
import com.thoughtworks.go.domain.materials.Material;
import com.thoughtworks.go.domain.materials.MaterialConfig;
import com.thoughtworks.go.domain.materials.Modification;
import com.thoughtworks.go.domain.materials.Modifications;
import com.thoughtworks.go.domain.materials.dependency.DependencyMaterialRevision;
import com.thoughtworks.go.i18n.LocalizedMessage;
import com.thoughtworks.go.server.dao.PipelineSqlMapDao;
import com.thoughtworks.go.server.domain.PipelineConfigDependencyGraph;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.persistence.MaterialRepository;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.ui.ModificationForPipeline;
import com.thoughtworks.go.server.web.PipelineRevisionRange;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ChangesetService {
private PipelineSqlMapDao pipelineDao;
private MaterialRepository materialRepository;
private final GoConfigService goConfigService;
private SecurityService securityService;
@Autowired
public ChangesetService(SecurityService securityService, PipelineSqlMapDao pipelineDao, MaterialRepository materialRepository, GoConfigService goConfigService) {
this.securityService = securityService;
this.pipelineDao = pipelineDao;
this.materialRepository = materialRepository;
this.goConfigService = goConfigService;
}
public List<MaterialRevision> revisionsBetween(List<PipelineRevisionRange> pipelineRevisionRanges, Username username, HttpLocalizedOperationResult result) {
ArrayList<MaterialRevision> revisions = new ArrayList<>();
for (PipelineRevisionRange pipelineRevisionRange : pipelineRevisionRanges) {
DependencyMaterialRevision fromDmr = DependencyMaterialRevision.create(pipelineRevisionRange.getFromRevision(), null);
DependencyMaterialRevision toDmr = DependencyMaterialRevision.create(pipelineRevisionRange.getToRevision(), null);
revisions.addAll(revisionsBetween(pipelineRevisionRange.getPipelineName(), fromDmr.getPipelineCounter(), toDmr.getPipelineCounter(), username, result, true, false));
}
return deduplicateMaterialRevisionsForCommonMaterials(revisions);
}
public List<MaterialRevision> revisionsBetween(String pipelineName, Integer fromCounter, Integer toCounter, Username username, HttpLocalizedOperationResult result, boolean skipCheckForMingle,
boolean showBisect) {
if (!securityService.hasViewPermissionForPipeline(username, pipelineName)) {
result.unauthorized(LocalizedMessage.cannotViewPipeline(pipelineName), HealthStateType.general(HealthStateScope.forPipeline(pipelineName)));
return new ArrayList<>();
}
if (!goConfigService.hasPipelineNamed(new CaseInsensitiveString(pipelineName))) {
result.notFound(LocalizedMessage.string("RESOURCE_NOT_FOUND", "pipeline", pipelineName), HealthStateType.general(HealthStateScope.forPipeline(pipelineName)));
return new ArrayList<>();
}
if(fromCounter.equals(toCounter)){
fromCounter -= 1;
}
if (fromCounter < 0 || toCounter <= 0) {
result.badRequest(LocalizedMessage.string("NEGATIVE_PIPELINE_COUNTER"));
return new ArrayList<>();
}
if (fromCounter > toCounter) {
return revisionsBetween(pipelineName, toCounter, fromCounter, username, result, skipCheckForMingle, showBisect);
}
if (!showBisect) {
if (fromCounter != 0) {
Pipeline toPipeline = pipelineDao.findPipelineByNameAndCounter(pipelineName, toCounter);
Pipeline fromPipeline = pipelineDao.findPipelineByNameAndCounter(pipelineName, fromCounter);
if (toPipeline.isBisect() || fromPipeline.isBisect()) {
return new ArrayList<>();
}
}
}
List<MaterialRevision> allMaterialRevisions = modificationsPerMaterialBetween(pipelineName, fromCounter, toCounter);
return filterReachableFingerprintHolders(allMaterialRevisions, new FingerprintLoader<MaterialRevision>() {
public String getFingerprint(MaterialRevision materialRevision) {
return materialRevision.getMaterial().getFingerprint();
}
}, pipelineName, username, skipCheckForMingle, false);
}
private <T> List<T> filterReachableFingerprintHolders(List<T> allFingerprintHolders, final FingerprintLoader<T> fingerprintLoader, String pipelineName, Username username,
boolean skipCheckForMingle, boolean skipTrackingToolMatch) {
PipelineConfigDependencyGraph graph = goConfigService.upstreamDependencyGraphOf(pipelineName);
Set<String> allMaterialFingerprints = graph.allMaterialFingerprints();
Set<String> reachableMaterialfingerprints = populateReachableFingerprints(graph, username, skipCheckForMingle, skipTrackingToolMatch);
return filterFingerprintHolders(allFingerprintHolders, reachableMaterialfingerprints, allMaterialFingerprints, fingerprintLoader);
}
private Set<String> populateReachableFingerprints(PipelineConfigDependencyGraph graph, Username username, boolean skipCheckForMingle, boolean skipTrackingToolMatch) {
Set<String> fingerprints = new HashSet<>();
populateViewableMaterialsStartingAt(graph, username, fingerprints, graph.getCurrent().getMingleConfig(), graph.getCurrent().trackingTool(), skipCheckForMingle, skipTrackingToolMatch);
return fingerprints;
}
private interface FingerprintLoader<T> {
String getFingerprint(T t);
}
private <T> List<T> filterFingerprintHolders(List<T> fingerprintHolders, Set<String> reachableFingerprints, Set<String> allMaterialFingerprints, FingerprintLoader<T> fingerprintLoader) {
List<T> results = new ArrayList<>();
for (T fingerprintHolder : fingerprintHolders) {
String fingerprint = fingerprintLoader.getFingerprint(fingerprintHolder);
if (reachableFingerprints.contains(fingerprint)) {
results.add(fingerprintHolder);
} else {
if (!allMaterialFingerprints.contains(fingerprint)) {
results.add(fingerprintHolder);
}
}
}
return results;
}
private void populateViewableMaterialsStartingAt(PipelineConfigDependencyGraph graph, Username username, Set<String> fingerprints, MingleConfig mingleConfig, TrackingTool trackingTool,
boolean skipCheckForMingle, boolean skipTrackingToolMatch) {
for (MaterialConfig materialConfig : graph.getCurrent().materialConfigs()) {
fingerprints.add(materialConfig.getFingerprint());
}
for (PipelineConfigDependencyGraph upstream : graph.getUpstreamDependencies()) {
if (canView(username, upstream.getCurrent()) &&
(skipCheckForMingle || mingleConfigMatches(upstream.getCurrent(), mingleConfig)) &&
(skipTrackingToolMatch || trackingToolMatches(upstream.getCurrent(), trackingTool))) {
populateViewableMaterialsStartingAt(upstream, username, fingerprints, mingleConfig, trackingTool, skipCheckForMingle, skipTrackingToolMatch);
}
}
}
private boolean trackingToolMatches(PipelineConfig pipeline, TrackingTool trackingTool) {
TrackingTool otherTrackingTool = pipeline.trackingTool();
return isNullOrNotDefined(trackingTool) || isNullOrNotDefined(otherTrackingTool) || trackingTool.equals(otherTrackingTool);
}
boolean mingleConfigMatches(PipelineConfig pipeline, MingleConfig mingleConfig) {
MingleConfig otherMingleConfig = pipeline.getMingleConfig();
if (isNullOrNotDefined(mingleConfig) || isNullOrNotDefined(otherMingleConfig)) {
return true;
}
return mingleConfig.isDifferentFrom(otherMingleConfig);
}
private boolean isNullOrNotDefined(MingleConfig mingleConfig) {
return mingleConfig == null || !mingleConfig.isDefined();
}
private boolean isNullOrNotDefined(TrackingTool trackingTool) {
return trackingTool == null || !trackingTool.isDefined();
}
private boolean canView(Username username, PipelineConfig pipeline) {
return securityService.hasViewPermissionForPipeline(username, CaseInsensitiveString.str(pipeline.name()));
}
private List<MaterialRevision> modificationsPerMaterialBetween(String pipelineName, Integer fromCounter, Integer toCounter) {
List<Modification> modifications = materialRepository.getModificationsForPipelineRange(pipelineName, fromCounter, toCounter);
return deduplicateRevisionsForMaterial(modifications);
}
private List<MaterialRevision> deduplicateMaterialRevisionsForCommonMaterials(List<MaterialRevision> materialRevisions) {
List<Modification> modificationsWithDuplicates = new ArrayList<>();
for (MaterialRevision revision : materialRevisions) {
for (Modification modification : revision.getModifications()) {
if (! modificationsWithDuplicates.contains(modification)) {//change this with a better data-structure so lookup is not O(n)
modificationsWithDuplicates.add(modification);
}
}
}
return deduplicateRevisionsForMaterial(modificationsWithDuplicates);
}
private List<MaterialRevision> deduplicateRevisionsForMaterial(Collection<Modification> modifications) {
Map<Material, Modifications> grouped = groupModsByMaterial(modifications);
return toMaterialRevisionList(grouped);
}
Map<Material, Modifications> groupModsByMaterial(Collection<Modification> modifications) {
Map<Material, Modifications> grouped = new LinkedHashMap<>();
for (Modification modification : modifications) {
Material material = modification.getMaterialInstance().toOldMaterial(null, null, null);
Modifications mods = mapContainsMaterialWithFingerprint(grouped, material.getFingerprint());
if (mods == null) {
mods = new Modifications();
grouped.put(material, mods);
}
mods.add(modification);
}
return grouped;
}
private Modifications mapContainsMaterialWithFingerprint(Map<Material, Modifications> grouped, String fingerPrint) {
for (Material material : grouped.keySet()) {
if (material.getFingerprint().equals(fingerPrint)) {
return grouped.get(material);
}
}
return null;
}
private List<MaterialRevision> toMaterialRevisionList(Map<Material, Modifications> map) {
List<MaterialRevision> materialRevisionsAcrossPipelines = new ArrayList<>();
for (Map.Entry<Material, Modifications> materialToModifications : map.entrySet()) {
Modifications modifications = new Modifications(new ArrayList<>(materialToModifications.getValue()));
materialRevisionsAcrossPipelines.add(new MaterialRevision(materialToModifications.getKey(), modifications));
}
return materialRevisionsAcrossPipelines;
}
public Map<Long, List<ModificationForPipeline>> modificationsOfPipelines(List<Long> pipelineIds, String pipelineName, Username username) {
Map<Long, List<ModificationForPipeline>> modificationsForPipelineIds = materialRepository.findModificationsForPipelineIds(pipelineIds);
PipelineConfigDependencyGraph graph = goConfigService.upstreamDependencyGraphOf(pipelineName);
Set<String> allMaterialFingerprints = graph.allMaterialFingerprints();
Set<String> reachableMaterialfingerprints = populateReachableFingerprints(graph, username, true, true);
FingerprintLoader<ModificationForPipeline> loader = new FingerprintLoader<ModificationForPipeline>() {
public String getFingerprint(ModificationForPipeline modificationForPipeline) {
return modificationForPipeline.getMaterialFingerprint();
}
};
for (Map.Entry<Long, List<ModificationForPipeline>> pipelineIdAndModifications : modificationsForPipelineIds.entrySet()) {
List<ModificationForPipeline> visibleModifications = filterFingerprintHolders(pipelineIdAndModifications.getValue(), reachableMaterialfingerprints, allMaterialFingerprints, loader);
pipelineIdAndModifications.setValue(visibleModifications);
}
return modificationsForPipelineIds;
}
}