EnvironmentService.java

package io.featureprobe.api.service;

import io.featureprobe.api.auth.TokenHelper;
import io.featureprobe.api.base.component.SpringBeanManager;
import io.featureprobe.api.base.db.Archived;
import io.featureprobe.api.base.db.ExcludeTenant;
import io.featureprobe.api.base.model.TargetingContent;
import io.featureprobe.api.dao.exception.ResourceConflictException;
import io.featureprobe.api.dao.exception.ResourceNotFoundException;
import io.featureprobe.api.dao.exception.ResourceOverflowException;
import io.featureprobe.api.dto.EnvironmentCreateRequest;
import io.featureprobe.api.dto.EnvironmentQueryRequest;
import io.featureprobe.api.dto.EnvironmentResponse;
import io.featureprobe.api.dto.EnvironmentUpdateRequest;
import io.featureprobe.api.dao.entity.Environment;
import io.featureprobe.api.dao.entity.Project;
import io.featureprobe.api.dao.entity.Targeting;
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.mapper.EnvironmentMapper;
import io.featureprobe.api.dao.repository.EnvironmentRepository;
import io.featureprobe.api.dao.repository.ProjectRepository;
import io.featureprobe.api.dao.repository.TargetingRepository;
import io.featureprobe.api.base.util.KeyGenerateUtil;
import com.featureprobe.sdk.server.FPUser;
import com.featureprobe.sdk.server.FeatureProbe;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@AllArgsConstructor
@Service
public class EnvironmentService {

    private EnvironmentRepository environmentRepository;

    private ProjectRepository projectRepository;

    private TargetingRepository targetingRepository;

    private ChangeLogService changeLogService;

    private IncludeArchivedToggleService includeArchivedToggleService;

    private TargetingService targetingService;

    @PersistenceContext
    public EntityManager entityManager;

    private static final String LIMITER_TOGGLE_KEY = "FeatureProbe_env_limiter";

    @Transactional(rollbackFor = Exception.class)
    public EnvironmentResponse create(String projectKey, EnvironmentCreateRequest createRequest) {
        validateLimit(projectKey);
        Project project = projectRepository.findByKey(projectKey).get();
        Environment environment = EnvironmentMapper.INSTANCE.requestToEntity(createRequest);
        environment.setServerSdkKey(KeyGenerateUtil.getServerSdkKey());
        environment.setClientSdkKey(KeyGenerateUtil.getClientSdkKey());
        environment.setProject(project);
        createEnvironmentTargetingList(projectKey, createRequest.getKey(), createRequest.getCopyFrom());
        changeLogService.create(environment, ChangeLogType.ADD);

        return EnvironmentMapper.INSTANCE.entityToResponse(environmentRepository.save(environment));
    }

    @Transactional(rollbackFor = Exception.class)
    public EnvironmentResponse update(String projectKey, String environmentKey,
                                      EnvironmentUpdateRequest updateRequest) {
        Environment environment = environmentRepository.findByProjectKeyAndKey(projectKey, environmentKey)
                .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ENVIRONMENT, environmentKey));
        if (!StringUtils.equals(environment.getName(), updateRequest.getName())) {
            validateEnvironmentByName(projectKey, updateRequest.getName());
        }
        EnvironmentMapper.INSTANCE.mapEntity(updateRequest, environment);
        if (updateRequest.isResetServerSdk()) {
            environment.setServerSdkKey(KeyGenerateUtil.getServerSdkKey());
        }
        if (updateRequest.isResetClientSdk()) {
            environment.setClientSdkKey(KeyGenerateUtil.getClientSdkKey());
        }
        return EnvironmentMapper.INSTANCE.entityToResponse(environmentRepository.save(environment));
    }


    @Transactional(rollbackFor = Exception.class)
    @Archived
    public EnvironmentResponse offline(String projectKey, String environmentKey) {
        Environment environment = environmentRepository.findByProjectKeyAndKeyAndArchived(projectKey,
                environmentKey, false).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.ENVIRONMENT, environmentKey));
        changeLogService.create(environment, ChangeLogType.DELETE);
        environment.setArchived(true);
        return EnvironmentMapper.INSTANCE.entityToResponse(environmentRepository.save(environment));
    }

    @Transactional(rollbackFor = Exception.class)
    @Archived
    public EnvironmentResponse restore(String projectKey, String environmentKey) {
        Environment environment = environmentRepository.findByProjectKeyAndKeyAndArchived(projectKey,
                environmentKey, true).orElseThrow(() ->
                new ResourceNotFoundException(ResourceType.ENVIRONMENT, environmentKey));
        changeLogService.create(environment, ChangeLogType.ADD);
        environment.setArchived(false);
        return EnvironmentMapper.INSTANCE.entityToResponse(environmentRepository.save(environment));
    }

    public EnvironmentResponse query(String projectKey, String environmentKey) {
        Environment environment = environmentRepository.findByProjectKeyAndKey(projectKey, environmentKey)
                .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ENVIRONMENT, environmentKey));
        return EnvironmentMapper.INSTANCE.entityToResponse(environment);
    }

    @Archived
    public List<EnvironmentResponse> list(String projectKey, EnvironmentQueryRequest queryRequest) {
        List<Environment> environments = environmentRepository
                .findAllByProjectKeyAndArchivedOrderByCreatedTimeAsc(projectKey, queryRequest.isArchived());
        return environments.stream().map(environment -> EnvironmentMapper.INSTANCE.entityToResponse(environment))
                .collect(Collectors.toList());
    }

    private void validateLimit(String projectKey) {
        long total = environmentRepository.countByProjectKey(projectKey);
        FPUser user = new FPUser(String.valueOf(TokenHelper.getUserId()));
        user.with("account", TokenHelper.getAccount());
        double limitNum = SpringBeanManager.getBeanByType(FeatureProbe.class)
                .numberValue(LIMITER_TOGGLE_KEY, user, -1);
        if (limitNum > 0 && total >= limitNum) {
            throw new ResourceOverflowException(ResourceType.ENVIRONMENT);
        }
    }

    private List<Targeting> createEnvironmentTargetingList(String projectKey, String environmentKey,
                                                           String copyFromEnvKey) {
        List<Targeting> targetingList;
        if (StringUtils.isBlank(copyFromEnvKey)) {
            targetingList = includeArchivedToggleService.findAllByProjectKey(projectKey).stream()
                    .map(toggle -> createTargetingByToggle(toggle, environmentKey))
                    .collect(Collectors.toList());
        } else {
            targetingList = targetingRepository.findAllByProjectKeyAndEnvironmentKey(projectKey, copyFromEnvKey)
                    .stream()
                    .map(targeting -> copyTargeting(environmentKey, targeting)).collect(Collectors.toList());
        }
        if (targetingList.isEmpty()) {
            return Collections.emptyList();
        }
        targetingService.createTargetingEntities(targetingList);
        return targetingList;
    }

    private Targeting copyTargeting(String newEnvironmentKey, Targeting sourceTargeting) {
        Targeting newTargeting = this.createTargeting(sourceTargeting.getProjectKey(), newEnvironmentKey,
                sourceTargeting.getToggleKey(), sourceTargeting.getContent());
        newTargeting.setDeleted(sourceTargeting.isDeleted());
        newTargeting.setDisabled(sourceTargeting.isDisabled());
        newTargeting.setStatus(sourceTargeting.getStatus());
        return newTargeting;
    }

    private Targeting createTargetingByToggle(Toggle toggle, String environmentKey) {
        String defaultContent = TargetingContent.newDefault(toggle.getVariations(),
                toggle.getDisabledServe()).toJson();
        return this.createTargeting(toggle.getProjectKey(), environmentKey, toggle.getKey(), defaultContent);
    }

    private Targeting createTargeting(String projectKey, String environmentKey, String toggleKey,
                                      String content) {
        Targeting targeting = new Targeting();
        targeting.setDisabled(true);
        targeting.setDeleted(false);
        targeting.setContent(content);
        targeting.setVersion(1L);
        targeting.setProjectKey(projectKey);
        targeting.setToggleKey(toggleKey);
        targeting.setEnvironmentKey(environmentKey);
        targeting.setPublishTime(new Date());
        return targeting;
    }


    private void validateEnvironmentByName(String projectKey, String name) {
        if (environmentRepository.existsByProjectKeyAndName(projectKey, name)) {
            throw new ResourceConflictException(ResourceType.ENVIRONMENT);
        }
    }

    @ExcludeTenant
    public String getSdkServerKey(String serverKeyOrClientKey) {
        return environmentRepository.findByServerSdkKeyOrClientSdkKey(serverKeyOrClientKey, serverKeyOrClientKey)
                .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ENVIRONMENT, serverKeyOrClientKey))
                .getServerSdkKey();
    }
}