TargetingService.java
package io.featureprobe.api.service;
import io.featureprobe.api.auth.TokenHelper;
import io.featureprobe.api.base.enums.OrganizationRoleEnum;
import io.featureprobe.api.base.model.BaseRule;
import io.featureprobe.api.base.model.PrerequisiteModel;
import io.featureprobe.api.base.model.SegmentRuleModel;
import io.featureprobe.api.base.model.TargetingContent;
import io.featureprobe.api.base.model.ToggleRule;
import io.featureprobe.api.base.model.Variation;
import io.featureprobe.api.base.tenant.TenantContext;
import io.featureprobe.api.base.util.ToggleContentLimitChecker;
import io.featureprobe.api.config.AppConfig;
import io.featureprobe.api.dao.entity.Prerequisite;
import io.featureprobe.api.dao.entity.Segment;
import io.featureprobe.api.dao.entity.Toggle;
import io.featureprobe.api.dao.entity.ToggleControlConf;
import io.featureprobe.api.dao.exception.ResourceNotFoundException;
import io.featureprobe.api.dao.repository.PrerequisiteRepository;
import io.featureprobe.api.dao.repository.ToggleRepository;
import io.featureprobe.api.dao.utils.PageRequestUtil;
import io.featureprobe.api.dto.AfterTargetingVersionResponse;
import io.featureprobe.api.dto.ApprovalResponse;
import io.featureprobe.api.dto.CancelSketchRequest;
import io.featureprobe.api.dto.DependentToggleRequest;
import io.featureprobe.api.dto.DependentToggleResponse;
import io.featureprobe.api.dto.PrerequisiteToggleRequest;
import io.featureprobe.api.dto.PrerequisiteToggleResponse;
import io.featureprobe.api.dto.TargetingApprovalRequest;
import io.featureprobe.api.dto.TargetingDiffResponse;
import io.featureprobe.api.dto.TargetingPublishRequest;
import io.featureprobe.api.dto.TargetingResponse;
import io.featureprobe.api.dto.TargetingVersionRequest;
import io.featureprobe.api.dto.TargetingVersionResponse;
import io.featureprobe.api.dto.ToggleControlConfRequest;
import io.featureprobe.api.dto.UpdateApprovalStatusRequest;
import io.featureprobe.api.dao.entity.ApprovalRecord;
import io.featureprobe.api.dao.entity.Environment;
import io.featureprobe.api.dao.entity.Targeting;
import io.featureprobe.api.dao.entity.TargetingSegment;
import io.featureprobe.api.dao.entity.TargetingSketch;
import io.featureprobe.api.dao.entity.TargetingVersion;
import io.featureprobe.api.dao.entity.VariationHistory;
import io.featureprobe.api.base.enums.ApprovalStatusEnum;
import io.featureprobe.api.base.enums.ChangeLogType;
import io.featureprobe.api.base.enums.ResourceType;
import io.featureprobe.api.base.enums.SketchStatusEnum;
import io.featureprobe.api.base.enums.ToggleReleaseStatusEnum;
import io.featureprobe.api.mapper.ApprovalRecordMapper;
import io.featureprobe.api.mapper.TargetingMapper;
import io.featureprobe.api.mapper.TargetingVersionMapper;
import io.featureprobe.api.base.model.ConditionValue;
import io.featureprobe.api.base.model.PaginationRequest;
import io.featureprobe.api.dao.repository.ApprovalRecordRepository;
import io.featureprobe.api.dao.repository.EnvironmentRepository;
import io.featureprobe.api.dao.repository.SegmentRepository;
import io.featureprobe.api.dao.repository.TargetingRepository;
import io.featureprobe.api.dao.repository.TargetingSegmentRepository;
import io.featureprobe.api.dao.repository.TargetingSketchRepository;
import io.featureprobe.api.dao.repository.TargetingVersionRepository;
import io.featureprobe.api.dao.repository.VariationHistoryRepository;
import io.featureprobe.api.base.util.JsonMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Slf4j
@AllArgsConstructor
@Service
public class TargetingService {
private static final Pattern dateTimeRegex = Pattern.compile("[0-9]{3}[0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]" +
"(:[0-6][0-9]){2}\\+[0-2][0-9]:[0-1][0-9]");
private static final Pattern versionRegex = Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" +
"(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))" +
"?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
private TargetingRepository targetingRepository;
private SegmentRepository segmentRepository;
private TargetingSegmentRepository targetingSegmentRepository;
private TargetingVersionRepository targetingVersionRepository;
private VariationHistoryRepository variationHistoryRepository;
private EnvironmentRepository environmentRepository;
private ApprovalRecordRepository approvalRecordRepository;
private TargetingSketchRepository targetingSketchRepository;
private ToggleRepository toggleRepository;
private PrerequisiteRepository prerequisiteRepository;
private ChangeLogService changeLogService;
private ToggleControlConfService toggleControlConfService;
private MetricService metricService;
private AppConfig appConfig;
@PersistenceContext
public EntityManager entityManager;
@Transactional(rollbackFor = Exception.class)
public TargetingResponse publish(String projectKey, String environmentKey, String toggleKey,
TargetingPublishRequest targetingPublishRequest) {
if (Objects.nonNull(targetingPublishRequest.getContent())) {
List<PrerequisiteModel> prerequisites = targetingPublishRequest.getContent().getPrerequisites();
if (!CollectionUtils.isEmpty(prerequisites)){
if (hasDependencyCycle(projectKey, environmentKey, toggleKey, new HashSet<>(prerequisites),
appConfig.getMaximumDependencyDepth())) {
throw new IllegalArgumentException("validate.prerequisite.dependency.cycle");
}
updateDependentToggles(projectKey, environmentKey, toggleKey, prerequisites);
}
}
Environment environment = selectEnvironment(projectKey, environmentKey);
TargetingResponse response = publishTargeting(projectKey, environmentKey, toggleKey,
targetingPublishRequest, null);
changeLogService.create(environment, ChangeLogType.CHANGE);
return response;
}
@Transactional(rollbackFor = Exception.class)
public ApprovalResponse approval(String projectKey, String environmentKey, String toggleKey,
TargetingApprovalRequest approvalRequest) {
validateTargetingContent(projectKey, approvalRequest.getContent());
List<PrerequisiteModel> prerequisites = approvalRequest.getContent().getPrerequisites();
if (!CollectionUtils.isEmpty(prerequisites) &&
hasDependencyCycle(projectKey, environmentKey, toggleKey, new HashSet<>(prerequisites),
appConfig.getMaximumDependencyDepth())){
throw new IllegalArgumentException("validate.prerequisite.dependency.cycle");
}
Environment environment = selectEnvironment(projectKey, environmentKey);
if (environment.isEnableApproval()) {
List<String> reviews = JsonMapper.toListObject(environment.getReviewers(), String.class);
approvalRequest.setReviewers(reviews);
Targeting targeting = selectTargeting(projectKey, environmentKey, toggleKey);
if (targeting.getStatus() == ToggleReleaseStatusEnum.PENDING_APPROVAL) {
throw new IllegalArgumentException("validate.approval.repeat");
}
targeting.setStatus(ToggleReleaseStatusEnum.PENDING_APPROVAL);
targetingRepository.save(targeting);
ApprovalRecord approvalRecord = submitApproval(projectKey, environmentKey,
toggleKey, approvalRequest);
Targeting approval = new Targeting();
approval.setDisabled(approvalRequest.getDisabled());
approval.setContent(JsonMapper.toJSONString(approvalRequest.getContent()));
return buildApprovalResponse(approvalRecord, targeting, approval);
}
throw new IllegalArgumentException("validate.approval.disable");
}
private void updateDependentToggles(String projectKey, String environmentKey, String toggleKey,
List<PrerequisiteModel> prerequisiteModels) {
prerequisiteRepository.deleteAllByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey, environmentKey,
toggleKey);
List<Prerequisite> prerequisites = prerequisiteModels.stream().map(prerequisiteModel ->
buildPrerequisite(projectKey, environmentKey, toggleKey, prerequisiteModel))
.collect(Collectors.toList());
prerequisiteRepository.saveAll(prerequisites);
}
private Prerequisite buildPrerequisite(String projectKey, String environmentKey, String toggleKey,
PrerequisiteModel prerequisiteModel) {
Prerequisite prerequisite = new Prerequisite();
prerequisite.setProjectKey(projectKey);
prerequisite.setEnvironmentKey(environmentKey);
prerequisite.setToggleKey(toggleKey);
prerequisite.setParentToggleKey(prerequisiteModel.getKey());
prerequisite.setDependentValue(prerequisiteModel.getValue());
return prerequisite;
}
private ApprovalResponse buildApprovalResponse(ApprovalRecord approvalRecord, Targeting currentData,
Targeting approvalData) {
ApprovalResponse approvalResponse = ApprovalRecordMapper.INSTANCE.entityToApprovalResponse(approvalRecord);
Map<String, Object> current = new HashMap<>();
current.put("disabled", currentData.isDisabled());
current.put("content", currentData.getContent());
approvalResponse.setCurrentData(current);
Map<String, Object> approval = new HashMap<>();
approval.put("disabled", approvalData.isDisabled());
approval.put("content", approvalData.getContent());
approvalResponse.setApprovalData(approval);
return approvalResponse;
}
private boolean hasDependencyCycle(String projectKey, String environmentKey , String rootToggleKey,
Set<PrerequisiteModel> parentPrerequisites, int deep) {
if (deep == 0) {
throw new IllegalArgumentException("validate.prerequisite.deep.limit");
}
if (CollectionUtils.isEmpty(parentPrerequisites)) {
return false;
}
Set<String> parentToggleKeys = parentPrerequisites.stream().map(PrerequisiteModel::getKey)
.collect(Collectors.toSet());
if (parentToggleKeys.contains(rootToggleKey)) {
return true;
}
List<Targeting> targetingList = targetingRepository.findByProjectKeyAndEnvironmentKeyAndToggleKeyIn(projectKey,
environmentKey, parentToggleKeys);
Set<PrerequisiteModel> prerequisite = targetingList.stream()
.map(Targeting::getContent).map(content -> JsonMapper.toObject(content, TargetingContent.class)
.getPrerequisites()).filter(Objects::nonNull)
.flatMap(List::stream).collect(Collectors.toSet());
return hasDependencyCycle(projectKey, environmentKey, rootToggleKey, prerequisite, deep - 1);
}
@Transactional(rollbackFor = Exception.class)
public TargetingResponse publishSketch(String projectKey, String environmentKey, String toggleKey,
ToggleControlConfRequest controlConfRequest) {
Optional<ApprovalRecord> approvalRecordOptional = queryNewestApprovalRecord(projectKey,
environmentKey, toggleKey);
Optional<TargetingSketch> targetingSketchOptional = queryNewestTargetingSketch(projectKey, environmentKey,
toggleKey);
Environment environment = selectEnvironment(projectKey, environmentKey);
if (approvalRecordOptional.isPresent() && targetingSketchOptional.isPresent() &&
publishableStatus(approvalRecordOptional.get())) {
TargetingSketch sketch = targetingSketchOptional.get();
sketch.setStatus(SketchStatusEnum.RELEASE);
targetingSketchRepository.save(sketch);
TargetingPublishRequest targetingPublishRequest = new TargetingPublishRequest(
JsonMapper.toObject(sketch.getContent(), TargetingContent.class),
sketch.getComment(), sketch.getDisabled(), controlConfRequest);
changeLogService.create(environment, ChangeLogType.CHANGE);
return publishTargeting(projectKey, environmentKey, toggleKey, targetingPublishRequest,
approvalRecordOptional.get().getId());
}
return null;
}
@Transactional(rollbackFor = Exception.class)
public TargetingResponse cancelSketch(String projectKey, String environmentKey, String toggleKey,
CancelSketchRequest cancelSketchRequest) {
Optional<ApprovalRecord> approvalRecordOptional = queryNewestApprovalRecord(projectKey,
environmentKey, toggleKey);
Optional<TargetingSketch> targetingSketchOptional = queryNewestTargetingSketch(projectKey, environmentKey,
toggleKey);
Targeting targeting = selectTargeting(projectKey, environmentKey, toggleKey);
if (approvalRecordOptional.isPresent() && targetingSketchOptional.isPresent()) {
TargetingSketch sketch = targetingSketchOptional.get();
sketch.setStatus(SketchStatusEnum.CANCEL);
sketch.setComment(cancelSketchRequest.getComment());
targeting.setStatus(ToggleReleaseStatusEnum.RELEASE);
targetingSketchRepository.save(sketch);
Targeting save = targetingRepository.save(targeting);
return TargetingMapper.INSTANCE.entityToResponse(save);
}
return null;
}
@Transactional(rollbackFor = Exception.class)
public ApprovalResponse updateApprovalStatus(String projectKey, String environmentKey, String toggleKey,
UpdateApprovalStatusRequest updateRequest) {
Optional<ApprovalRecord> approvalRecordOptional = queryNewestApprovalRecord(projectKey,
environmentKey, toggleKey);
Optional<TargetingSketch> targetingSketchOptional = queryNewestTargetingSketch(projectKey, environmentKey,
toggleKey);
Targeting targeting = selectTargeting(projectKey, environmentKey, toggleKey);
if (approvalRecordOptional.isPresent() && targetingSketchOptional.isPresent() &&
checkStateMachine(approvalRecordOptional.get(), updateRequest.getStatus())) {
if (updateRequest.getStatus() == ApprovalStatusEnum.REVOKE) {
TargetingSketch sketch = targetingSketchOptional.get();
sketch.setStatus(SketchStatusEnum.REVOKE);
targetingSketchRepository.save(sketch);
}
ApprovalRecord approvalRecord = approvalRecordOptional.get();
approvalRecord.setStatus(updateRequest.getStatus());
approvalRecord.setComment(updateRequest.getComment());
approvalRecord.setApprovedBy(TokenHelper.getAccount());
approvalRecordRepository.saveAndFlush(approvalRecord);
if (updateRequest.getStatus() == ApprovalStatusEnum.JUMP) {
publishSketch(projectKey, environmentKey, toggleKey, updateRequest);
}
if (updateRequest.getStatus() == ApprovalStatusEnum.PASS) {
targeting.setStatus(ToggleReleaseStatusEnum.PENDING_RELEASE);
} else if (updateRequest.getStatus() == ApprovalStatusEnum.REJECT) {
targeting.setStatus(ToggleReleaseStatusEnum.REJECT);
} else {
targeting.setStatus(ToggleReleaseStatusEnum.RELEASE);
}
Targeting current = targetingRepository.save(targeting);
Targeting approval = new Targeting();
approval.setDisabled(targetingSketchOptional.get().getDisabled());
approval.setContent(targetingSketchOptional.get().getContent());
return buildApprovalResponse(approvalRecord, current, approval);
}
throw new IllegalArgumentException();
}
private boolean checkStateMachine(ApprovalRecord approvalRecord, ApprovalStatusEnum status) {
switch (status) {
case PASS:
case REJECT:
return approvalRecord.getStatus() == ApprovalStatusEnum.PENDING &&
JsonMapper.toListObject(approvalRecord.getReviewers(), String.class)
.contains(TokenHelper.getAccount());
case REVOKE:
case JUMP:
return approvalRecord.getStatus() == ApprovalStatusEnum.PENDING &&
TenantContext.getCurrentOrganization().getRole() == OrganizationRoleEnum.OWNER;
default:
return false;
}
}
public Page<TargetingVersionResponse> queryVersions(String projectKey, String environmentKey, String toggleKey,
TargetingVersionRequest targetingVersionRequest) {
Page<TargetingVersion> targetingVersions;
if (Objects.isNull(targetingVersionRequest.getVersion())) {
targetingVersions = targetingVersionRepository
.findAllByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey, environmentKey, toggleKey,
PageRequestUtil.toCreatedTimeDescSortPageable(targetingVersionRequest));
} else {
targetingVersions = targetingVersionRepository
.findAllByProjectKeyAndEnvironmentKeyAndToggleKeyAndVersionLessThanOrderByVersionDesc(
projectKey, environmentKey, toggleKey, targetingVersionRequest.getVersion(),
PageRequestUtil.toCreatedTimeDescSortPageable(targetingVersionRequest));
}
return targetingVersions.map(targetingVersion -> translateTargetingVersionResponse(targetingVersion));
}
public AfterTargetingVersionResponse queryAfterVersion(String projectKey, String environmentKey, String toggleKey,
Long version) {
List<TargetingVersion> targetingVersions = targetingVersionRepository
.findAllByProjectKeyAndEnvironmentKeyAndToggleKeyAndVersionGreaterThanEqualOrderByVersionDesc(
projectKey, environmentKey, toggleKey, version);
List<TargetingVersionResponse> versions = targetingVersions.stream().map(targetingVersion ->
translateTargetingVersionResponse(targetingVersion)).collect(Collectors.toList());
long total = targetingVersionRepository.countByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey,
environmentKey, toggleKey);
return new AfterTargetingVersionResponse(total, versions);
}
private TargetingVersionResponse translateTargetingVersionResponse(TargetingVersion targetingVersion) {
TargetingVersionResponse targetingVersionResponse = TargetingVersionMapper.INSTANCE
.entityToResponse(targetingVersion);
if (Objects.nonNull(targetingVersion.getApprovalId())) {
Optional<ApprovalRecord> approvalRecord = approvalRecordRepository
.findById(targetingVersion.getApprovalId());
targetingVersionResponse.setApprovalStatus(approvalRecord.get().getStatus());
targetingVersionResponse.setApprovalTime(approvalRecord.get().getModifiedTime());
targetingVersionResponse.setApprovalBy(approvalRecord.get().getApprovedBy());
targetingVersionResponse.setApprovalComment(approvalRecord.get().getComment());
}
return targetingVersionResponse;
}
public TargetingDiffResponse diff(String projectKey, String environmentKey, String toggleKey) {
TargetingDiffResponse diffResponse = new TargetingDiffResponse();
Optional<TargetingSketch> targetingSketch = queryNewestTargetingSketch(projectKey, environmentKey, toggleKey);
Optional<Targeting> targeting = targetingRepository.findByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey,
environmentKey, toggleKey);
if (targetingSketch.isPresent() && targeting.isPresent()) {
diffResponse.setCurrentDisabled(targetingSketch.get().getDisabled());
diffResponse.setCurrentContent(JsonMapper.toObject(targetingSketch.get().getContent(),
TargetingContent.class));
diffResponse.setOldDisabled(targeting.get().isDisabled());
diffResponse.setOldContent(JsonMapper.toObject(targeting.get().getContent(), TargetingContent.class));
}
return diffResponse;
}
public TargetingResponse queryByKey(String projectKey, String environmentKey, String toggleKey) {
Environment environment = selectEnvironment(projectKey, environmentKey);
Targeting targeting = targetingRepository.findByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey,
environmentKey, toggleKey).get();
TargetingResponse targetingResponse = TargetingMapper.INSTANCE.entityToResponse(targeting);
Optional<ApprovalRecord> newestApprovalRecord = queryNewestApprovalRecord(projectKey, environmentKey,
toggleKey);
Optional<TargetingSketch> targetingSketch = queryNewestTargetingSketch(projectKey, environmentKey, toggleKey);
if (newestApprovalRecord.isPresent() && targetingSketch.isPresent() && locked(targetingSketch.get())) {
targetingResponse.setContent(JsonMapper.toObject(targetingSketch.get().getContent(),
TargetingContent.class));
targetingResponse.setDisabled(targetingSketch.get().getDisabled());
targetingResponse.setVersion(targetingSketch.get().getOldVersion() + 1);
targetingResponse.setStatus(newestApprovalRecord.get().getStatus().name());
targetingResponse.setReviewers(JsonMapper.toListObject(newestApprovalRecord.get().getReviewers(),
String.class));
targetingResponse.setSubmitBy(newestApprovalRecord.get().getSubmitBy());
targetingResponse.setApprovalBy(newestApprovalRecord.get().getApprovedBy());
targetingResponse.setApprovalComment(newestApprovalRecord.get().getComment());
targetingResponse.setLocked(true);
targetingResponse.setLockedTime(newestApprovalRecord.get().getCreatedTime());
} else {
targetingResponse.setStatus(SketchStatusEnum.RELEASE.name());
if (environment.isEnableApproval()) {
targetingResponse.setReviewers(JsonMapper.toListObject(environment.getReviewers(), String.class));
}
}
targetingResponse.setEnableApproval(environment.isEnableApproval());
targetingResponse.setPublishTime(targeting.getPublishTime());
Boolean trackAccessEvents = toggleControlConfService.queryToggleControlConf(targeting)
.isTrackAccessEvents();
targetingResponse.setTrackAccessEvents(trackAccessEvents);
targetingResponse.setAllowEnableTrackAccessEvents(BooleanUtils.isFalse(trackAccessEvents)
&& metricService.existsMetric(targeting.getProjectKey(),
targeting.getEnvironmentKey(),
targeting.getToggleKey()));
if (Objects.isNull(targetingResponse.getContent().getPrerequisites())) {
targetingResponse.getContent().setPrerequisites(Collections.emptyList());
}
return targetingResponse;
}
private boolean locked(TargetingSketch targetingSketch) {
return targetingSketch.getStatus() == SketchStatusEnum.PENDING;
}
private boolean publishableStatus(ApprovalRecord approvalRecord) {
return approvalRecord.getStatus() == ApprovalStatusEnum.JUMP ||
approvalRecord.getStatus() == ApprovalStatusEnum.PASS;
}
private ApprovalRecord submitApproval(String projectKey, String environmentKey, String toggleKey,
TargetingApprovalRequest approvalRequest) {
Targeting targeting = selectTargeting(projectKey, environmentKey, toggleKey);
ApprovalRecord approvalRecord = approvalRecordRepository.save(buildApprovalRecord(projectKey, environmentKey,
toggleKey, approvalRequest));
targetingSketchRepository.save(buildTargetingSketch(projectKey, environmentKey, toggleKey,
approvalRecord.getId(), targeting.getVersion(), approvalRequest));
return approvalRecord;
}
private TargetingResponse publishTargeting(String projectKey, String environmentKey, String toggleKey,
TargetingPublishRequest targetingPublishRequest, Long approvalId) {
Targeting latestTargeting = selectTargeting(projectKey, environmentKey, toggleKey);
if (targetingPublishRequest.isUpdateTargetingRules()) {
latestTargeting = updateTargeting(projectKey, latestTargeting, targetingPublishRequest);
saveTargetingSegmentRefs(projectKey, latestTargeting, targetingPublishRequest.getContent());
saveTargetingVersion(buildTargetingVersion(latestTargeting,
targetingPublishRequest.getComment(), approvalId));
saveVariationHistory(latestTargeting, targetingPublishRequest.getContent());
latestTargeting.setStatus(ToggleReleaseStatusEnum.RELEASE);
}
ToggleControlConf toggleControlConf = toggleControlConfService.updateTrackAccessEvents(latestTargeting,
targetingPublishRequest.getTrackAccessEvents());
TargetingResponse targetingResponse = TargetingMapper.INSTANCE.entityToResponse(latestTargeting);
targetingResponse.setTrackAccessEvents(toggleControlConf.isTrackAccessEvents());
return targetingResponse;
}
private ApprovalRecord buildApprovalRecord(String projectKey, String environmentKey, String toggleKey,
TargetingApprovalRequest approvalRequest) {
ApprovalRecord approvalRecord = new ApprovalRecord();
approvalRecord.setProjectKey(projectKey);
approvalRecord.setEnvironmentKey(environmentKey);
approvalRecord.setToggleKey(toggleKey);
approvalRecord.setTitle(approvalRequest.getComment());
approvalRecord.setSubmitBy(TokenHelper.getAccount());
approvalRecord.setReviewers(JsonMapper.toJSONString(approvalRequest.getReviewers()));
approvalRecord.setStatus(ApprovalStatusEnum.PENDING);
return approvalRecord;
}
private TargetingSketch buildTargetingSketch(String projectKey, String environmentKey, String toggleKey,
Long approvalId, Long oldVersion,
TargetingApprovalRequest approvalRequest) {
TargetingSketch sketch = new TargetingSketch();
sketch.setApprovalId(approvalId);
sketch.setProjectKey(projectKey);
sketch.setEnvironmentKey(environmentKey);
sketch.setToggleKey(toggleKey);
sketch.setOldVersion(oldVersion);
sketch.setContent(JsonMapper.toJSONString(approvalRequest.getContent()));
sketch.setComment(approvalRequest.getComment());
sketch.setDisabled(approvalRequest.getDisabled());
sketch.setStatus(SketchStatusEnum.PENDING);
return sketch;
}
private Targeting updateTargeting(String projectKey, Targeting currentTargeting,
TargetingPublishRequest updateTargetingPublishRequest) {
TargetingContent currentTargetingContent = JsonMapper.toObject(currentTargeting.getContent(),
TargetingContent.class);
TargetingMapper.INSTANCE.mapContentEntity(updateTargetingPublishRequest.getContent(), currentTargetingContent);
validatePublishConflicts(currentTargeting, updateTargetingPublishRequest.getBaseVersion());
validateTargetingContent(projectKey, currentTargetingContent);
updateTargetingPublishRequest.setContent(currentTargetingContent);
TargetingMapper.INSTANCE.mapEntity(updateTargetingPublishRequest, currentTargeting);
currentTargeting.setVersion(currentTargeting.getVersion() + 1);
currentTargeting.setPublishTime(new Date());
return targetingRepository.saveAndFlush(currentTargeting);
}
private TargetingVersion buildTargetingVersion(Targeting targeting, String comment, Long approvalId) {
TargetingVersion targetingVersion = new TargetingVersion();
targetingVersion.setProjectKey(targeting.getProjectKey());
targetingVersion.setEnvironmentKey(targeting.getEnvironmentKey());
targetingVersion.setToggleKey(targeting.getToggleKey());
targetingVersion.setContent(targeting.getContent());
targetingVersion.setDisabled(targeting.isDisabled());
targetingVersion.setVersion(targeting.getVersion());
targetingVersion.setComment(comment);
targetingVersion.setApprovalId(approvalId);
return targetingVersion;
}
@Transactional(rollbackFor = Exception.class)
public void createDefaultTargetingEntities(String projectKey, Toggle toggle) {
List<Environment> environments = environmentRepository.findAllByProjectKey(projectKey);
if (org.apache.commons.collections4.CollectionUtils.isEmpty(environments)) {
log.info("{} environment is empty, ignore create targeting", projectKey);
return;
}
List<Targeting> targetingList = environments.stream()
.map(environment -> createDefaultTargeting(toggle, environment)).collect(Collectors.toList());
environments.stream().forEach(environment -> changeLogService.create(environment, ChangeLogType.CHANGE));
this.createTargetingEntities(targetingList);
}
@Transactional(rollbackFor = Exception.class)
public void createTargetingEntities(List<Targeting> targetingList) {
List<Targeting> savedTargetingList = targetingRepository.saveAll(targetingList);
for (Targeting targeting : savedTargetingList) {
saveTargetingVersion(buildTargetingVersion(targeting, ""));
saveVariationHistory(targeting);
}
}
private void saveTargetingVersion(TargetingVersion targetingVersion) {
targetingVersionRepository.save(targetingVersion);
}
private void saveVariationHistory(Targeting targeting) {
List<Variation> variations = JsonMapper.toObject(targeting.getContent(), TargetingContent.class)
.getVariations();
List<VariationHistory> variationHistories = IntStream.range(0, variations.size())
.mapToObj(index -> convertVariationToEntity(targeting, index,
variations.get(index)))
.collect(Collectors.toList());
variationHistoryRepository.saveAll(variationHistories);
}
private VariationHistory convertVariationToEntity(Targeting targeting, int index, Variation variation) {
VariationHistory variationHistory = new VariationHistory();
variationHistory.setEnvironmentKey(targeting.getEnvironmentKey());
variationHistory.setProjectKey(targeting.getProjectKey());
variationHistory.setToggleKey(targeting.getToggleKey());
variationHistory.setValue(variation.getValue());
variationHistory.setName(variation.getName());
variationHistory.setToggleVersion(targeting.getVersion());
variationHistory.setValueIndex(index);
return variationHistory;
}
private TargetingVersion buildTargetingVersion(Targeting targeting, String comment) {
TargetingVersion targetingVersion = new TargetingVersion();
targetingVersion.setProjectKey(targeting.getProjectKey());
targetingVersion.setEnvironmentKey(targeting.getEnvironmentKey());
targetingVersion.setToggleKey(targeting.getToggleKey());
targetingVersion.setContent(targeting.getContent());
targetingVersion.setDisabled(targeting.isDisabled());
targetingVersion.setVersion(targeting.getVersion());
targetingVersion.setComment(comment);
return targetingVersion;
}
private Targeting createDefaultTargeting(Toggle toggle, Environment environment) {
Targeting targeting = new Targeting();
targeting.setDeleted(false);
targeting.setVersion(1L);
targeting.setProjectKey(toggle.getProjectKey());
targeting.setDisabled(true);
targeting.setContent(TargetingContent.newDefault(toggle.getVariations(),
toggle.getDisabledServe()).toJson());
targeting.setToggleKey(toggle.getKey());
targeting.setEnvironmentKey(environment.getKey());
targeting.setPublishTime(new Date());
return targeting;
}
private void saveTargetingSegmentRefs(String projectKey, Targeting targeting, TargetingContent targetingContent) {
targetingSegmentRepository.deleteByTargetingId(targeting.getId());
List<TargetingSegment> targetingSegmentList = getTargetingSegments(projectKey, targeting, targetingContent);
if (!CollectionUtils.isEmpty(targetingSegmentList)) {
targetingSegmentRepository.saveAll(targetingSegmentList);
}
}
private List<TargetingSegment> getTargetingSegments(String projectKey, Targeting targeting,
TargetingContent targetingContent) {
Set<String> segmentKeys = new TreeSet<>();
targetingContent.getRules().forEach(toggleRule -> toggleRule.getConditions()
.stream()
.filter(ConditionValue::isSegmentType)
.forEach(conditionValue -> segmentKeys.addAll(conditionValue.getObjects())));
return segmentKeys.stream().map(segmentKey -> new TargetingSegment(targeting.getId(), segmentKey, projectKey))
.collect(Collectors.toList());
}
private void saveVariationHistory(Targeting targeting,
TargetingContent targetingContent) {
List<Variation> variations = targetingContent.getVariations();
List<VariationHistory> variationHistories = IntStream.range(0, targetingContent
.getVariations().size())
.mapToObj(index -> convertVariationToEntity(targeting, index,
variations.get(index)))
.collect(Collectors.toList());
variationHistoryRepository.saveAll(variationHistories);
}
private Optional<ApprovalRecord> queryNewestApprovalRecord(String projectKey, String environmentKey,
String toggleKey) {
Specification<ApprovalRecord> spec = (root, query, cb) -> {
Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
Predicate p2 = cb.equal(root.get("environmentKey"), environmentKey);
Predicate p3 = cb.equal(root.get("toggleKey"), toggleKey);
return query.where(p1, p2, p3).getRestriction();
};
Pageable pageable = PageRequestUtil.toPageable(new PaginationRequest(), Sort.Direction.DESC,
"createdTime");
Page<ApprovalRecord> approvalRecords = approvalRecordRepository.findAll(spec, pageable);
if (CollectionUtils.isEmpty(approvalRecords.getContent())) {
return Optional.empty();
}
return Optional.of(approvalRecords.getContent().get(0));
}
private Optional<TargetingSketch> queryNewestTargetingSketch(String projectKey, String environmentKey,
String toggleKey) {
Specification<TargetingSketch> spec = (root, query, cb) -> {
Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
Predicate p2 = cb.equal(root.get("environmentKey"), environmentKey);
Predicate p3 = cb.equal(root.get("toggleKey"), toggleKey);
return query.where(p1, p2, p3).getRestriction();
};
Pageable pageable = PageRequestUtil.toPageable(new PaginationRequest(), Sort.Direction.DESC,
"createdTime");
Page<TargetingSketch> targetingSketches = targetingSketchRepository.findAll(spec, pageable);
if (CollectionUtils.isEmpty(targetingSketches.getContent())) {
return Optional.empty();
}
return Optional.of(targetingSketches.getContent().get(0));
}
protected void validatePublishConflicts(Targeting targeting, Long baseVersion){
if (baseVersion != null) {
if (targeting.getVersion() != null && !targeting.getVersion().equals(baseVersion)) {
throw new OptimisticLockException("publish conflict");
}
}
}
protected void validateTargetingContent(String projectKey, TargetingContent content) {
if (Objects.isNull(content) && CollectionUtils.isEmpty(content.getRules())) {
return;
}
ToggleContentLimitChecker.validateSize(content.toJson());
content.getRules()
.stream()
.filter(BaseRule::isNotEmptyConditions)
.forEach(toggleRule -> {
validateRuleRefSegmentExists(projectKey, toggleRule);
validateNumber(toggleRule);
validateDatetime(toggleRule);
validateSemVer(toggleRule);
});
}
private void validateRuleRefSegmentExists(String projectKey, ToggleRule toggleRule) {
toggleRule.getConditions().stream().filter(ConditionValue::isSegmentType)
.forEach(conditionValue -> conditionValue.getObjects().stream().forEach(segmentKey -> {
if (!segmentRepository.existsByProjectKeyAndKey(projectKey, segmentKey)) {
throw new ResourceNotFoundException(ResourceType.SEGMENT, segmentKey);
}
}));
}
private void validateNumber(ToggleRule toggleRule) {
toggleRule.getConditions().stream().filter(ConditionValue::isNumberType)
.forEach(conditionValue -> conditionValue.getObjects().stream().forEach(number -> {
try {
Double.parseDouble(number);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("validate.number_format_error");
}
}));
}
private void validateDatetime(ToggleRule toggleRule) {
toggleRule.getConditions().stream().filter(ConditionValue::isDatetimeType)
.forEach(conditionValue -> conditionValue.getObjects().stream().forEach(datetime -> {
if (!dateTimeRegex.matcher(datetime).matches()) {
throw new IllegalArgumentException("validate.datetime_format_error");
}
}));
}
private void validateSemVer(ToggleRule toggleRule) {
toggleRule.getConditions().stream().filter(ConditionValue::isSemVerType)
.forEach(conditionValue -> conditionValue.getObjects().stream().forEach(semVer -> {
if (!versionRegex.matcher(semVer).matches()) {
throw new IllegalArgumentException("validate.version_format_error");
}
}));
}
private Environment selectEnvironment(String projectKey, String environmentKey) {
return environmentRepository.findByProjectKeyAndKey(projectKey, environmentKey).orElseThrow(() ->
new ResourceNotFoundException(ResourceType.ENVIRONMENT, projectKey + "-" + environmentKey));
}
private Targeting selectTargeting(String projectKey, String environmentKey, String toggleKey) {
return targetingRepository.findByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey, environmentKey,
toggleKey).orElseThrow(() -> new ResourceNotFoundException(ResourceType.TARGETING,
projectKey + "-" + environmentKey + "-" + toggleKey));
}
public List<String> attributes(String projectKey, String environmentKey, String toggleKey) {
List<String> res = new ArrayList<>();
Targeting targeting = targetingRepository.findByProjectKeyAndEnvironmentKeyAndToggleKey(projectKey,
environmentKey, toggleKey).orElseThrow(() -> new ResourceNotFoundException(ResourceType.TARGETING,
projectKey + "-" + environmentKey + "-" + toggleKey));
TargetingContent targetingContent = JsonMapper.toObject(targeting.getContent(), TargetingContent.class);
List<ToggleRule> rules = targetingContent.getRules();
if (CollectionUtils.isEmpty(rules)) return res;
for (ToggleRule rule : rules) {
List<ConditionValue> conditions = rule.getConditions();
if (CollectionUtils.isEmpty(conditions)) {
break;
}
for (ConditionValue condition : conditions) {
if ("segment".equals(condition.getType())) {
res.addAll(getSegmentAttributes(projectKey, condition.getObjects()));
} else {
res.add(condition.getSubject());
}
}
}
return res.stream().distinct().collect(Collectors.toList());
}
private List<String> getSegmentAttributes(String projectKey, List<String> keys) {
List<String> res = new ArrayList<>();
if (CollectionUtils.isEmpty(keys)) return res;
for (String key : keys) {
Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, key).orElseThrow(() ->
new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + key));
List<SegmentRuleModel> segmentRules = JsonMapper.toListObject(segment.getRules(), SegmentRuleModel.class);
if (CollectionUtils.isEmpty(segmentRules)) return res;
for (SegmentRuleModel rule : segmentRules) {
List<ConditionValue> conditions = rule.getConditions();
if (CollectionUtils.isEmpty(conditions)) break;
for (ConditionValue condition : conditions) {
res.add(condition.getSubject());
}
}
}
return res;
}
public List<PrerequisiteToggleResponse> preToggles(String projectKey, String environmentKey, String toggleKey,
PrerequisiteToggleRequest query) {
List<Toggle> nonSelfToggles = getNonSelfToggles(projectKey, toggleKey, query);
List<Targeting> nonSelfTargetingList = getNonSelfTargetingList(projectKey, environmentKey, toggleKey);
Map<String, Targeting> targetingMap = nonSelfTargetingList.stream()
.collect(Collectors.toMap(Targeting::getToggleKey, Function.identity()));
return nonSelfToggles.stream().map(toggle -> buildPrerequisiteToggle(toggle, targetingMap))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public Page<DependentToggleResponse> getDependentToggles(String projectKey, String environmentKey,
String toggleKey, DependentToggleRequest requestParam) {
Page<Prerequisite> prerequisites = prerequisiteRepository
.findAllByProjectKeyAndEnvironmentKeyAndParentToggleKey(projectKey, environmentKey, toggleKey,
PageRequestUtil.toPageable(requestParam, Sort.Direction.DESC, "createdTime"));
if (prerequisites.isEmpty()) return Page.empty();
Set<String> dependentToggleKeys = prerequisites.stream().map(Prerequisite::getToggleKey)
.collect(Collectors.toSet());
Map<String, Toggle> toggleMap = toggleRepository.findAllByProjectKeyAndKeyIn(projectKey, dependentToggleKeys)
.stream().collect(Collectors.toMap(Toggle::getKey, Function.identity()));
Map<String, Targeting> targetingMap = targetingRepository
.findByProjectKeyAndEnvironmentKeyAndToggleKeyIn(projectKey, environmentKey, dependentToggleKeys)
.stream().collect(Collectors.toMap(Targeting::getToggleKey, Function.identity()));
return prerequisites.map(prerequisite -> buildDependentToggle(prerequisite, toggleMap, targetingMap));
}
private DependentToggleResponse buildDependentToggle(Prerequisite prerequisite, Map<String, Toggle> toggleMap,
Map<String, Targeting> targetingMap) {
DependentToggleResponse dependentToggle = new DependentToggleResponse();
dependentToggle.setDependentValue(prerequisite.getDependentValue());
dependentToggle.setKey(prerequisite.getToggleKey());
dependentToggle.setName(toggleMap.get(prerequisite.getToggleKey()).getName());
dependentToggle.setDisabled(targetingMap.get(prerequisite.getToggleKey()).isDisabled());
return dependentToggle;
}
private PrerequisiteToggleResponse buildPrerequisiteToggle(Toggle toggle, Map<String, Targeting> targetingMap) {
PrerequisiteToggleResponse prerequisiteToggle = new PrerequisiteToggleResponse();
Targeting targeting = targetingMap.get(toggle.getKey());
if (Objects.isNull(targeting)) return null;
prerequisiteToggle.setName(toggle.getName());
prerequisiteToggle.setKey(toggle.getKey());
prerequisiteToggle.setReturnType(toggle.getReturnType());
prerequisiteToggle.setDisabled(targeting.isDisabled());
List<Variation> variations = JsonMapper.toObject(targeting.getContent(),
TargetingContent.class).getVariations();
prerequisiteToggle.setVariations(variations);
return prerequisiteToggle;
}
private List<Toggle> getNonSelfToggles(String projectKey, String toggleKey,
PrerequisiteToggleRequest params) {
Specification<Toggle> spec = (root, query, cb) -> {
Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
Predicate p2 = root.get("key").in(toggleKey).not();
if (StringUtils.isNotBlank(params.getKey())) {
Predicate p3 = cb.equal(root.get("key"), params.getKey());
return query.where(cb.and(p1, p2, p3)).getRestriction();
} else if (StringUtils.isNotBlank(params.getLikeNameAndKey())) {
Predicate p3 = cb.like(root.get("name"), "%" + params.getLikeNameAndKey() + "%");
Predicate p4 = cb.like(root.get("key"), "%" + params.getLikeNameAndKey() + "%");
return query.where(cb.and(p1, p2), cb.or(p3, p4)).getRestriction();
}
return query.where(cb.and(p1, p2)).getRestriction();
};
return toggleRepository.findAll(spec);
}
private List<Targeting> getNonSelfTargetingList(String projectKey, String environmentKey, String toggleKey) {
Specification<Targeting> spec = (root, query, cb) -> {
Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
Predicate p2 = cb.equal(root.get("environmentKey"), environmentKey);
Predicate p3 = root.get("toggleKey").in(toggleKey).not();
return query.where(cb.and(p1, p2, p3)).getRestriction();
};
return targetingRepository.findAll(spec);
}
}