BaseServerService.java

package io.featureprobe.api.service;

import io.featureprobe.api.base.db.ExcludeTenant;
import io.featureprobe.api.base.enums.EventTypeEnum;
import io.featureprobe.api.base.enums.ResourceType;
import io.featureprobe.api.base.model.JSEvent;
import io.featureprobe.api.base.util.JsonMapper;
import io.featureprobe.api.builder.ServerSegmentBuilder;
import io.featureprobe.api.builder.ServerToggleBuilder;
import io.featureprobe.api.dao.entity.Dictionary;
import io.featureprobe.api.dao.entity.Environment;
import io.featureprobe.api.dao.entity.Segment;
import io.featureprobe.api.dao.entity.ServerEventEntity;
import io.featureprobe.api.dao.entity.ServerSegmentEntity;
import io.featureprobe.api.dao.entity.ServerToggleEntity;
import io.featureprobe.api.dao.entity.Targeting;
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.DictionaryRepository;
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.ToggleControlConfRepository;
import io.featureprobe.api.dao.repository.ToggleRepository;
import io.featureprobe.api.dto.SdkKeyResponse;
import io.featureprobe.api.dto.ServerResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.plexus.util.StringUtils;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Slf4j
@Service
@AllArgsConstructor
@ExcludeTenant
public class BaseServerService {

    private EnvironmentRepository environmentRepository;

    private SegmentRepository segmentRepository;

    private ToggleRepository toggleRepository;

    private TargetingRepository targetingRepository;

    private DictionaryRepository dictionaryRepository;

    private ToggleControlConfRepository toggleControlConfRepository;

    @PersistenceContext
    public EntityManager entityManager;

    public static final String SDK_KEY_DICTIONARY_KEY = "all_sdk_key_map";

    public SdkKeyResponse queryAllSdkKeys() {
        SdkKeyResponse sdkKeyResponse = new SdkKeyResponse();
        List<Environment> environments = environmentRepository.findAllByArchivedAndDeleted(false,
                false);
        environments.stream().forEach(environment -> sdkKeyResponse.put(environment.getClientSdkKey(),
                environment.getServerSdkKey()));
        Dictionary dictionary = dictionaryRepository.findByKey(SDK_KEY_DICTIONARY_KEY)
                .orElseThrow(() -> new ResourceNotFoundException(ResourceType.DICTIONARY, SDK_KEY_DICTIONARY_KEY));
        sdkKeyResponse.setVersion(Long.parseLong(dictionary.getValue()));
        return sdkKeyResponse;
    }

    public ServerResponse queryServerTogglesByServerSdkKey(String serverSdkKey) {
        Environment environment = environmentRepository.findByServerSdkKeyOrClientSdkKey(serverSdkKey, serverSdkKey)
                .orElseThrow(() -> new ResourceNotFoundException(ResourceType.ENVIRONMENT, serverSdkKey));
        return new ServerResponse(queryTogglesBySdkKey(environment.getServerSdkKey()),
                querySegmentsBySdkKey(environment.getServerSdkKey()), queryEventsBySdkKey(serverSdkKey),
                environment.getVersion(), environment.getDebuggerUntilTime());
    }

    public Map<String, byte[]> queryAllServerToggle() {
        List<ServerToggleEntity> allServerToggle = environmentRepository.findAllServerToggle();
        List<ServerSegmentEntity> allServerSegment = environmentRepository.findAllServerSegment();
        List<ServerEventEntity> allServerEvent = environmentRepository.findAllServerEvent();
        return buildAllServerToggle(allServerToggle, allServerSegment, allServerEvent);
    }

    private Map<String, byte[]>  buildAllServerToggle(List<ServerToggleEntity> allServerToggle,
                                                      List<ServerSegmentEntity> allServerSegment,
                                                      List<ServerEventEntity> allServerEvent) {
        Map<String, List<Segment>> segmentsMap = new HashMap<>();
        allServerSegment.stream().forEach(serverSegment -> {
            if (segmentsMap.containsKey(getServerSegmentUniqueKey(serverSegment))) {
                segmentsMap.get(getServerSegmentUniqueKey(serverSegment)).add(toSegment(serverSegment));
            } else {
                List<Segment> segments = new ArrayList<>();
                segments.add(toSegment(serverSegment));
                segmentsMap.put(getServerSegmentUniqueKey(serverSegment), segments);
            }
        });
        Map<String, List<JSEvent>> eventMap = new HashMap<>();
        allServerEvent.stream()
                .filter(e -> (EventTypeEnum.PAGE_VIEW.equals(e.getType()) ||
                        (EventTypeEnum.CLICK.equals(e.getType()))))
                .forEach(serverEvent -> {
                    if (eventMap.containsKey(serverEvent.getServerSdkKey())) {
                        eventMap.get(serverEvent.getServerSdkKey()).add(toEvent(serverEvent));
                    } else {
                        List<JSEvent> events = new ArrayList<>();
                        events.add(toEvent(serverEvent));
                        eventMap.put(serverEvent.getServerSdkKey(), events);
                    }
                });
        Map<String, List<Toggle>> toggleMap = new HashMap<>();
        Map<String, List<Targeting>> targetingMap = new HashMap<>();
        Map<String, ServerEnv> serverEnvMap = new HashMap<>();
        Map<String, List<ToggleControlConf>> controlConfMap = new HashMap<>();
        toggleSplit(toggleMap, targetingMap, serverEnvMap, controlConfMap, allServerToggle);
        Map<String, byte[]> allServerResponse = new HashMap<>();
        for (String serverSdkKey : targetingMap.keySet()) {
            List<Segment> segments = segmentsMap.getOrDefault(serverEnvMap.get(serverSdkKey)
                            .getServerSegmentUniqueKey(), Collections.emptyList()).stream()
                    .filter(distinctByKey(Segment::uniqueKey)).collect(Collectors.toList());
            List<Toggle> toggles = toggleMap.getOrDefault(serverEnvMap.get(serverSdkKey)
                            .getServerToggleUniqueKey(), Collections.emptyList()).stream()
                    .filter(distinctByKey(Toggle::uniqueKey)).collect(Collectors.toList());
            List<Targeting> targetingList = targetingMap.getOrDefault(serverSdkKey, Collections.emptyList()).stream()
                    .filter(distinctByKey(Targeting::uniqueKey)).collect(Collectors.toList());
            List<ToggleControlConf> controlConfList = controlConfMap
                    .getOrDefault(serverSdkKey, Collections.emptyList()).stream()
                    .filter(distinctByKey(ToggleControlConf::uniqueKey))
                    .collect(Collectors.toList());
            ServerResponse serverResponse = new ServerResponse(
                    buildServerToggles(segments, toggles, targetingList, controlConfList),
                    buildServerSegments(segments),
                    eventMap.getOrDefault(serverSdkKey,  Collections.emptyList())
                            .stream().filter(distinctByKey(JSEvent::getName)).collect(Collectors.toList()),
                    serverEnvMap.get(serverSdkKey).getEnvVersion(),
                    serverEnvMap.get(serverSdkKey).getDebugUntilTime());
            allServerResponse.put(serverSdkKey, JsonMapper.toJSONString(serverResponse).getBytes());
        }
        return allServerResponse;
    }

    private void toggleSplit(Map<String, List<Toggle>> toggleMap,
                             Map<String, List<Targeting>> targetingMap ,
                             Map<String, ServerEnv> serverEnvMap,
                             Map<String, List<ToggleControlConf>> controlConfMap,
                             List<ServerToggleEntity> allServerToggle)  {
        allServerToggle.stream().forEach(serverToggle -> {
            if (toggleMap.containsKey(getServerToggleUniqueKey(serverToggle))) {
                toggleMap.get(getServerToggleUniqueKey(serverToggle)).add(toToggle(serverToggle));
            } else if (Objects.nonNull(serverToggle.getToggleKey())) {
                List<Toggle> toggles = new ArrayList<>();
                toggles.add(toToggle(serverToggle));
                toggleMap.put(getServerToggleUniqueKey(serverToggle), toggles);
            }

            if (targetingMap.containsKey(serverToggle.getServerSdkKey()) &&
                    Objects.nonNull(serverToggle.getToggleKey())) {
                targetingMap.get(serverToggle.getServerSdkKey()).add(toTargeting(serverToggle));
            } else {
                List<Targeting> targetingList = new ArrayList<>();
                if (Objects.nonNull(serverToggle.getToggleKey())) {
                    targetingList.add(toTargeting(serverToggle));
                }
                targetingMap.put(serverToggle.getServerSdkKey(), targetingList);
            }

            if (!serverEnvMap.containsKey(serverToggle.getServerSdkKey())) {
                serverEnvMap.put(serverToggle.getServerSdkKey(), toServerEnv(serverToggle));
            }

            if (controlConfMap.containsKey(serverToggle.getServerSdkKey())) {
                controlConfMap.get(serverToggle.getServerSdkKey()).add(toControlConf(serverToggle));
            } else {
                List<ToggleControlConf> controlConfList = new ArrayList<>();
                controlConfList.add(toControlConf(serverToggle));
                controlConfMap.put(serverToggle.getServerSdkKey(), controlConfList);
            }
        });
    }

    private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

    private String getServerSegmentUniqueKey(ServerSegmentEntity serverSegment) {
        return serverSegment.getProjectKey() + "$" + serverSegment.getOrganizationId();
    }

    private String getServerToggleUniqueKey(ServerToggleEntity serverToggle) {
        return serverToggle.getProjectKey() + "$" + serverToggle.getOrganizationId();
    }

    private Segment toSegment(ServerSegmentEntity serverSegment) {
        Segment segment = new Segment();
        segment.setKey(serverSegment.getSegmentKey());
        segment.setUniqueKey(serverSegment.getSegmentUniqueKey());
        segment.setVersion(serverSegment.getSegmentVersion());
        segment.setRules(serverSegment.getSegmentRules());
        return segment;
    }

    private Toggle toToggle(ServerToggleEntity serverToggle) {
        Toggle toggle = new Toggle();
        toggle.setProjectKey(serverToggle.getProjectKey());
        toggle.setKey(serverToggle.getToggleKey());
        toggle.setReturnType(serverToggle.getReturnType());
        toggle.setClientAvailability(serverToggle.getClientAvailability());
        return toggle;
    }

    private Targeting toTargeting(ServerToggleEntity serverToggle) {
        Targeting targeting = new Targeting();
        targeting.setToggleKey(serverToggle.getToggleKey());
        targeting.setDisabled(serverToggle.getTargetingDisabled());
        targeting.setVersion(serverToggle.getTargetingVersion());
        targeting.setContent(serverToggle.getTargetingContent());
        targeting.setOrganizationId(serverToggle.getOrganizationId());
        targeting.setProjectKey(serverToggle.getProjectKey());
        targeting.setEnvironmentKey(serverToggle.getEnvKey());
        targeting.setPublishTime(serverToggle.getPublishTime());
        return targeting;
    }

    private ServerEnv toServerEnv(ServerToggleEntity serverToggle) {
        ServerEnv serverEnv = new ServerEnv();
        serverEnv.setEnvVersion(serverToggle.getEnvVersion());
        Long debugUntilTime = (Objects.isNull(serverToggle.getDebugUntilTime()) ||
                serverToggle.getDebugUntilTime() == 0)
                ? null : serverToggle.getDebugUntilTime();
        serverEnv.setDebugUntilTime(debugUntilTime);
        serverEnv.setOrganizationId(serverToggle.getOrganizationId());
        serverEnv.setProjectKey(serverToggle.getProjectKey());
        serverEnv.setEnvKey(serverToggle.getEnvKey());
        return serverEnv;
    }

    private JSEvent toEvent(ServerEventEntity serverEvent){
        JSEvent event = new JSEvent();
        if (EventTypeEnum.CLICK.equals(serverEvent.getType()) && StringUtils.isBlank(serverEvent.getSelector())) {
            event.setType(EventTypeEnum.PAGE_VIEW);
        } else {
            event.setType(serverEvent.getType());
        }
        event.setName(serverEvent.getName());
        event.setMatcher(serverEvent.getMatcher());
        event.setUrl(serverEvent.getUrl());
        event.setSelector(serverEvent.getSelector());
        return event;
    }

    private ToggleControlConf toControlConf(ServerToggleEntity toggleEntity) {
        ToggleControlConf controlConf = new ToggleControlConf();
        controlConf.setOrganizationId(toggleEntity.getOrganizationId());
        controlConf.setProjectKey(toggleEntity.getProjectKey());
        controlConf.setEnvironmentKey(toggleEntity.getEnvKey());
        controlConf.setToggleKey(toggleEntity.getToggleKey());
        controlConf.setTrackAccessEvents(
                !Objects.isNull(toggleEntity.getTrackAccessEvents()) && toggleEntity.getTrackAccessEvents());
        return controlConf;
    }

    private List<com.featureprobe.sdk.server.model.Toggle> queryTogglesBySdkKey(String serverSdkKey) {
        Environment environment = environmentRepository.findByServerSdkKey(serverSdkKey).get();
        if (Objects.isNull(environment)) {
            return Collections.emptyList();
        }
        List<Segment> segments = segmentRepository.findAllByProjectKeyAndOrganizationIdAndDeleted(
                environment.getProject().getKey(), environment.getOrganizationId(), false);
        List<Toggle> toggles = toggleRepository.findAllByProjectKeyAndOrganizationIdAndArchivedAndDeleted(
                environment.getProject().getKey(), environment.getOrganizationId(), false, false);
        List<Targeting> targetingList = targetingRepository
                .findAllByProjectKeyAndEnvironmentKeyAndOrganizationIdAndDeleted(environment.getProject().getKey(),
                        environment.getKey(), environment.getOrganizationId(), false);
        List<ToggleControlConf> controlConfList = toggleControlConfRepository
                .findByProjectKeyAndEnvironmentKeyAndOrganizationId(environment.getProject().getKey(),
                        environment.getKey(), environment.getOrganizationId());
        return buildServerToggles(segments, toggles, targetingList, controlConfList);
    }

    private List<com.featureprobe.sdk.server.model.Segment> querySegmentsBySdkKey(String serverSdkKey) {
        Environment environment = environmentRepository.findByServerSdkKey(serverSdkKey).get();
        if (Objects.isNull(environment)) {
            return Collections.emptyList();
        }
        List<Segment> segments = segmentRepository.findAllByProjectKeyAndOrganizationIdAndDeleted(
                environment.getProject().getKey(), environment.getOrganizationId(), false);
        return buildServerSegments(segments);
    }

    private List<JSEvent> queryEventsBySdkKey(String serverSdkKey) {
        List<ServerEventEntity> serverEventEntities = environmentRepository.findAllServerEventBySdkKey(serverSdkKey);
        return serverEventEntities.stream()
                .filter(serverEvent -> (EventTypeEnum.PAGE_VIEW.equals(serverEvent.getType())
                        || (EventTypeEnum.CLICK.equals(serverEvent.getType()))))
                .filter(distinctByKey(ServerEventEntity::getName))
                .map(serverEvent -> toEvent(serverEvent))
                .collect(Collectors.toList());
    }

    private List<com.featureprobe.sdk.server.model.Segment> buildServerSegments(List<Segment> segments) {
        return segments.stream().map(segment -> {
            try {
                return new ServerSegmentBuilder().builder()
                        .uniqueId(segment.getUniqueKey())
                        .version(segment.getVersion())
                        .rules(segment.getRules())
                        .build();
            } catch (Exception e) {
                log.error("Build server segment failed, unique key: {}, segment key: {}",
                        segment.getUniqueKey(), segment.getKey(), e);
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

    private List<com.featureprobe.sdk.server.model.Toggle> buildServerToggles(
            List<Segment> segments,
            List<Toggle> toggles,
            List<Targeting> targetingList,
            List<ToggleControlConf> controlConfList) {
        Map<String, Targeting> targetingByKey = targetingList.stream()
                .collect(Collectors.toMap(Targeting::getToggleKey, Function.identity()));
        Map<String, ToggleControlConf> controlConfByKey = controlConfList.stream()
                .collect(Collectors.toMap(ToggleControlConf::getToggleKey, Function.identity()));
        return toggles.stream().map(toggle -> {
            Targeting targeting = targetingByKey.get(toggle.getKey());
            ToggleControlConf controlConf = controlConfByKey.get(toggle.getKey());
            try {
                return new ServerToggleBuilder().builder()
                        .key(toggle.getKey())
                        .disabled(targeting.isDisabled())
                        .version(targeting.getVersion())
                        .returnType(toggle.getReturnType())
                        .forClient(toggle.getClientAvailability())
                        .rules(targeting.getContent())
                        .trackAccessEvents(!Objects.isNull(controlConf) && controlConf.isTrackAccessEvents())
                        .lastModified(targeting.getPublishTime())
                        .segments(segments.stream().collect(Collectors.toMap(Segment::getKey, Function.identity())))
                        .build();
            } catch (Exception e) {
                log.warn("Build server toggle failed, OrganizationId : {}, projectKey:{}, " +
                                " toggle key: {}", toggle.getOrganizationId(), toggle.getProjectKey(),
                        toggle.getKey(), e);
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }
    @Data
    class ServerEnv {

        Long envVersion;

        Long debugUntilTime;
        Long organizationId;

        String projectKey;

        String envKey;

        private String getServerSegmentUniqueKey() {
            return projectKey + "$" + organizationId;
        }

        private String getServerToggleUniqueKey() {
            return projectKey + "$" + organizationId;
        }

    }

}