SegmentService.java

package io.featureprobe.api.service;

import io.featureprobe.api.base.constants.MessageKey;
import io.featureprobe.api.base.db.Archived;
import io.featureprobe.api.base.util.JsonMapper;
import io.featureprobe.api.base.util.ToggleContentLimitChecker;
import io.featureprobe.api.dao.entity.SegmentVersion;
import io.featureprobe.api.dao.entity.ToggleControlConf;
import io.featureprobe.api.dao.exception.ResourceConflictException;
import io.featureprobe.api.dao.exception.ResourceNotFoundException;
import io.featureprobe.api.dao.repository.SegmentVersionRepository;
import io.featureprobe.api.dao.repository.ToggleControlConfRepository;
import io.featureprobe.api.dao.utils.PageRequestUtil;
import io.featureprobe.api.dto.SegmentCreateRequest;
import io.featureprobe.api.dto.SegmentPublishRequest;
import io.featureprobe.api.dto.SegmentResponse;
import io.featureprobe.api.dto.SegmentSearchRequest;
import io.featureprobe.api.dto.SegmentUpdateRequest;
import io.featureprobe.api.dto.SegmentVersionRequest;
import io.featureprobe.api.dto.SegmentVersionResponse;
import io.featureprobe.api.dto.ToggleSegmentResponse;
import io.featureprobe.api.dao.entity.Environment;
import io.featureprobe.api.dao.entity.Project;
import io.featureprobe.api.dao.entity.Segment;
import io.featureprobe.api.dao.entity.Targeting;
import io.featureprobe.api.dao.entity.TargetingSegment;
import io.featureprobe.api.dao.entity.Toggle;
import io.featureprobe.api.base.enums.ChangeLogType;
import io.featureprobe.api.base.enums.ResourceType;
import io.featureprobe.api.base.enums.ValidateTypeEnum;
import io.featureprobe.api.mapper.SegmentMapper;
import io.featureprobe.api.dao.repository.EnvironmentRepository;
import io.featureprobe.api.dao.repository.ProjectRepository;
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.ToggleRepository;
import io.featureprobe.api.base.model.PaginationRequest;
import io.featureprobe.api.mapper.SegmentVersionMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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 javax.persistence.EntityManager;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.Predicate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@AllArgsConstructor
public class SegmentService {

    private SegmentRepository segmentRepository;

    private TargetingSegmentRepository targetingSegmentRepository;

    private TargetingRepository targetingRepository;

    private ToggleRepository toggleRepository;

    private EnvironmentRepository environmentRepository;

    private ProjectRepository projectRepository;

    private SegmentVersionRepository segmentVersionRepository;

    private ToggleControlConfRepository toggleControlConfRepository;
    private ChangeLogService changeLogService;

    @PersistenceContext
    public EntityManager entityManager;

    public Page<SegmentResponse> list(String projectKey, SegmentSearchRequest searchRequest) {
        Specification<Segment> spec = buildQuerySpec(projectKey, searchRequest.getKeyword());
        return findPagingBySpec(spec, PageRequestUtil.toCreatedTimeDescSortPageable(searchRequest));
    }

    public SegmentResponse create(String projectKey, SegmentCreateRequest createRequest) {
        Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
        validateKey(projectKey, createRequest.getKey());
        validateName(projectKey, createRequest.getName());
        Segment segment = SegmentMapper.INSTANCE.requestToEntity(createRequest);
        segment.setProjectKey(projectKey);
        segment.setUniqueKey(StringUtils.join(projectKey, "$", createRequest.getKey()));
        segment.setRules(JsonMapper.toJSONString(Collections.emptyList()));
        segment.setVersion(1L);
        saveSegmentChangeLog(project);
        Segment savedSegment = segmentRepository.save(segment);
        saveSegmentVersion(buildSegmentVersion(savedSegment, null, null));
        return SegmentMapper.INSTANCE.entityToResponse(savedSegment);
    }

    @Transactional(rollbackFor = Exception.class)
    public SegmentResponse update(String projectKey, String segmentKey, SegmentUpdateRequest updateRequest) {
        Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
        Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
        if (!StringUtils.equals(segment.getName(), updateRequest.getName())) {
            validateName(projectKey, updateRequest.getName());
        }
        SegmentMapper.INSTANCE.mapEntity(updateRequest, segment);
        saveSegmentChangeLog(project);
        return SegmentMapper.INSTANCE.entityToResponse(segmentRepository.save(segment));
    }

    @Transactional(rollbackFor = Exception.class)
    public SegmentResponse publish(String projectKey, String segmentKey, SegmentPublishRequest publishRequest) {
        Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
        Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));

        this.validatePublishConflicts(segment, publishRequest.getBaseVersion());
        SegmentMapper.INSTANCE.mapEntity(publishRequest, segment);
        Long oldVersion = ObjectUtils.isNotEmpty(publishRequest.getBaseVersion())
                ? publishRequest.getBaseVersion() : segment.getVersion();

        ToggleContentLimitChecker.validateSize(segment.getRules());

        Segment updatedSegment = segmentRepository.saveAndFlush(segment);
        if (updatedSegment.getVersion() > oldVersion) {
            saveSegmentVersion(buildSegmentVersion(updatedSegment, publishRequest.getComment(), null));
        }
        saveSegmentChangeLog(project);
        return SegmentMapper.INSTANCE.entityToResponse(updatedSegment);
    }

    protected void validatePublishConflicts(Segment segment, Long baseVersion){
        if (baseVersion != null) {
            if (segment.getVersion() != null && !segment.getVersion().equals(baseVersion)) {
                throw new OptimisticLockException("publish conflict");
            }
        }
    }

    private void saveSegmentChangeLog(Project project) {
        if (CollectionUtils.isNotEmpty(project.getEnvironments())) {
            for (Environment environment : project.getEnvironments()) {
                changeLogService.create(environment, ChangeLogType.CHANGE);
            }
        }
    }

    public Page<SegmentVersionResponse> versions(String projectKey, String segmentKey,
                                                 SegmentVersionRequest versionRequest) {
        Specification<SegmentVersion> spec = buildVersionsQuerySpec(projectKey, segmentKey);
        Page<SegmentVersion> versions = segmentVersionRepository.findAll(spec,
                PageRequestUtil.toPageable(versionRequest, Sort.Direction.DESC, "version"));
        return versions.map(version -> SegmentVersionMapper.INSTANCE.entityToResponse(version));
    }

    public SegmentResponse delete(String projectKey, String segmentKey) {
        Project project = projectRepository.findByKey(projectKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.PROJECT, projectKey));
        if (targetingSegmentRepository.countByProjectKeyAndSegmentKey(projectKey, segmentKey) > 0) {
            throw new IllegalArgumentException(MessageKey.USING);
        }
        Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
        segment.setDeleted(true);
        if (CollectionUtils.isNotEmpty(project.getEnvironments())) {
            for (Environment environment : project.getEnvironments()) {
                changeLogService.create(environment, ChangeLogType.CHANGE);
            }
        }
        return SegmentMapper.INSTANCE.entityToResponse(segmentRepository.save(segment));
    }

    @Archived
    public Page<ToggleSegmentResponse> usingToggles(String projectKey, String segmentKey,
                                                    PaginationRequest paginationRequest) {
        List<TargetingSegment> targetingSegments = targetingSegmentRepository
                .findByProjectKeyAndSegmentKey(projectKey, segmentKey);
        List<Targeting> targetingList = targetingRepository.findAllById(
                targetingSegments.stream().map(TargetingSegment::getTargetingId).collect(Collectors.toList()));
        List<Toggle> toggles = toggleRepository.findAllByProjectKeyAndKeyIn(projectKey,
                targetingList.stream().map(Targeting::getToggleKey).collect(Collectors.toList()));
        Map<String, Toggle> toggleMap = toggles.stream()
                .collect(Collectors.toMap(Toggle::getKey, Function.identity()));
        Map<Long, String> targetingIdToToggleKey = targetingList.stream().
                collect(Collectors.toMap(Targeting::getId, Targeting::getToggleKey));
        List<Long> targetingIds = targetingSegments.stream().filter(targetingSegment ->
            toggleMap.get(targetingIdToToggleKey.get(targetingSegment.getTargetingId())) != null)
                .map(TargetingSegment::getTargetingId).collect(Collectors.toList());
        Pageable pageable = PageRequest.of(paginationRequest.getPageIndex(), paginationRequest.getPageSize(),
                Sort.Direction.DESC, "createdTime");
        Specification<Targeting> spec = (root, query, cb) -> {
            Predicate p0 = root.get("id").in(targetingIds);
            query.where(cb.and(p0));
            return query.getRestriction();
        };
        Page<Targeting> targetingPage = targetingRepository.findAll(spec, pageable);
        Page<ToggleSegmentResponse> res = targetingPage.map(targeting -> {
            Toggle toggle = toggleMap.get(targeting.getToggleKey());
            ToggleSegmentResponse toggleSegmentResponse = SegmentMapper.INSTANCE
                    .toggleToToggleSegment(toggle);
            toggleSegmentResponse.setDisabled(targeting.isDisabled());
            Optional<Environment> environment = environmentRepository
                    .findByProjectKeyAndKey(projectKey, targeting.getEnvironmentKey());
            Optional<ToggleControlConf> toggleControlConfOptional = toggleControlConfRepository
                    .findByProjectKeyAndEnvironmentKeyAndToggleKey(targeting.getProjectKey(),
                            targeting.getEnvironmentKey(), targeting.getToggleKey());
            toggleSegmentResponse.setAnalyzing(
                    toggleControlConfOptional.isPresent() && toggleControlConfOptional.get().isTrackAccessEvents());
            toggleSegmentResponse.setEnvironmentName(environment.get().getName());
            toggleSegmentResponse.setEnvironmentKey(environment.get().getKey());
            return toggleSegmentResponse;
        });
        return res;
    }

    public SegmentResponse queryByKey(String projectKey, String segmentKey) {
        Segment segment = segmentRepository.findByProjectKeyAndKey(projectKey, segmentKey).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.SEGMENT, projectKey + "_" + segmentKey));
        return SegmentMapper.INSTANCE.entityToResponse(segment);
    }

    private SegmentVersion buildSegmentVersion(Segment segment, String comment, Long approvalId) {
        SegmentVersion segmentVersion = new SegmentVersion();
        segmentVersion.setVersion(segment.getVersion());
        segmentVersion.setKey(segment.getKey());
        segmentVersion.setRules(segment.getRules());
        segmentVersion.setComment(comment);
        segmentVersion.setApprovalId(approvalId);
        segmentVersion.setProjectKey(segment.getProjectKey());
        return segmentVersion;
    }

    private void saveSegmentVersion(SegmentVersion segmentVersion) {
        segmentVersionRepository.save(segmentVersion);
    }

    private Specification<Segment> buildQuerySpec(String projectKey, String keyword) {
        return (root, query, cb) -> {
            Predicate p3 = cb.equal(root.get("projectKey"), projectKey);
            if (StringUtils.isNotBlank(keyword)) {
                Predicate p0 = cb.like(root.get("name"), "%" + keyword + "%");
                Predicate p1 = cb.like(root.get("key"), "%" + keyword + "%");
                Predicate p2 = cb.like(root.get("description"), "%" + keyword + "%");
                query.where(cb.or(p0, p1, p2), cb.and(p3));
            } else {
                query.where(p3);
            }
            return query.getRestriction();
        };
    }

    private Page<SegmentResponse> findPagingBySpec(Specification<Segment> spec, Pageable pageable) {
        Page<Segment> segments = segmentRepository.findAll(spec, pageable);
        return segments.map(segment -> SegmentMapper.INSTANCE.entityToResponse(segment));
    }


    private Specification<SegmentVersion> buildVersionsQuerySpec(String projectKey, String key) {
        return (root, query, cb) -> {
            Predicate p1 = cb.equal(root.get("projectKey"), projectKey);
            Predicate p2 = cb.equal(root.get("key"), key);
            return query.where(cb.and(p1, p2)).getRestriction();
        };
    }

    public void validateExists(String projectKey, ValidateTypeEnum type, String value) {
        switch (type) {
            case KEY:
                validateKey(projectKey, value);
                break;
            case NAME:
                validateName(projectKey, value);
                break;
            default:
                break;
        }
    }

    public void validateKey(String projectKey, String key) {
        if (segmentRepository.existsByProjectKeyAndKey(projectKey, key)) {
            throw new ResourceConflictException(ResourceType.SEGMENT);
        }
    }

    public void validateName(String projectKey, String name) {
        if (segmentRepository.existsByProjectKeyAndName(projectKey, name)) {
            throw new ResourceConflictException(ResourceType.SEGMENT);
        }
    }

}