/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * 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 io.gravitee.rest.api.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.gravitee.common.component.Lifecycle;
import io.gravitee.common.http.HttpMethod;
import io.gravitee.definition.model.*;
import io.gravitee.definition.model.endpoint.HttpEndpoint;
import io.gravitee.definition.model.services.discovery.EndpointDiscoveryService;
import io.gravitee.repository.exceptions.TechnicalException;
import io.gravitee.repository.management.api.ApiQualityRuleRepository;
import io.gravitee.repository.management.api.ApiRepository;
import io.gravitee.repository.management.api.search.ApiCriteria;
import io.gravitee.repository.management.api.search.ApiFieldExclusionFilter;
import io.gravitee.repository.management.model.Api;
import io.gravitee.repository.management.model.ApiLifecycleState;
import io.gravitee.repository.management.model.Visibility;
import io.gravitee.repository.management.model.*;
import io.gravitee.rest.api.model.EventType;
import io.gravitee.rest.api.model.MembershipMemberType;
import io.gravitee.rest.api.model.MembershipReferenceType;
import io.gravitee.rest.api.model.MetadataFormat;
import io.gravitee.rest.api.model.*;
import io.gravitee.rest.api.model.alert.AlertReferenceType;
import io.gravitee.rest.api.model.alert.AlertTriggerEntity;
import io.gravitee.rest.api.model.api.*;
import io.gravitee.rest.api.model.api.header.ApiHeaderEntity;
import io.gravitee.rest.api.model.application.ApplicationListItem;
import io.gravitee.rest.api.model.documentation.PageQuery;
import io.gravitee.rest.api.model.notification.GenericNotificationConfigEntity;
import io.gravitee.rest.api.model.parameters.Key;
import io.gravitee.rest.api.model.permissions.RoleScope;
import io.gravitee.rest.api.model.permissions.SystemRole;
import io.gravitee.rest.api.model.plan.PlanQuery;
import io.gravitee.rest.api.model.subscription.SubscriptionQuery;
import io.gravitee.rest.api.service.*;
import io.gravitee.rest.api.service.common.GraviteeContext;
import io.gravitee.rest.api.service.common.RandomString;
import io.gravitee.rest.api.service.exceptions.*;
import io.gravitee.rest.api.service.impl.search.SearchResult;
import io.gravitee.rest.api.service.impl.upgrade.DefaultMetadataUpgrader;
import io.gravitee.rest.api.service.jackson.ser.api.ApiSerializer;
import io.gravitee.rest.api.service.notification.ApiHook;
import io.gravitee.rest.api.service.notification.HookScope;
import io.gravitee.rest.api.service.notification.NotificationParamsBuilder;
import io.gravitee.rest.api.service.processor.ApiSynchronizationProcessor;
import io.gravitee.rest.api.service.sanitizer.UrlSanitizerUtils;
import io.gravitee.rest.api.service.search.SearchEngineService;
import io.gravitee.rest.api.service.search.query.Query;
import io.gravitee.rest.api.service.search.query.QueryBuilder;
import io.gravitee.rest.api.service.spring.ImportConfiguration;
import io.vertx.core.buffer.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.gravitee.repository.management.model.Api.AuditEvent.*;
import static io.gravitee.repository.management.model.Visibility.PUBLIC;
import static io.gravitee.rest.api.model.EventType.PUBLISH_API;
import static io.gravitee.rest.api.model.ImportSwaggerDescriptorEntity.Type.INLINE;
import static io.gravitee.rest.api.model.PageType.SWAGGER;
import static io.gravitee.rest.api.model.WorkflowReferenceType.API;
import static io.gravitee.rest.api.model.WorkflowState.DRAFT;
import static io.gravitee.rest.api.model.WorkflowType.REVIEW;
import static java.util.Collections.*;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.*;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * @author David BRASSELY (david.brassely at graviteesource.com)
 * @author Nicolas GERAUD (nicolas.geraud at graviteesource.com)
 * @author Azize ELAMRANI (azize at graviteesource.com)
 * @author GraviteeSource Team
 */
@Component
public class ApiServiceImpl extends AbstractService implements ApiService {

    private final static Logger LOGGER = LoggerFactory.getLogger(ApiServiceImpl.class);

    private static final Pattern DUPLICATE_SLASH_REMOVER = Pattern.compile("(?<!(http:|https:))[//]+");

    private static final String URI_PATH_SEPARATOR = "/";

    @Autowired
    private ApiRepository apiRepository;
    @Autowired
    private ApiQualityRuleRepository apiQualityRuleRepository;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private EventService eventService;
    @Autowired
    private UserService userService;
    @Autowired
    private PageService pageService;
    @Autowired
    private MembershipService membershipService;
    @Autowired
    private GroupService groupService;
    @Autowired
    private PlanService planService;
    @Autowired
    private ApiSynchronizationProcessor apiSynchronizationProcessor;
    @Autowired
    private ApiMetadataService apiMetadataService;
    @Autowired
    private SubscriptionService subscriptionService;
    @Autowired
    private AuditService auditService;
    @Autowired
    private TopApiService topApiService;
    @Autowired
    private GenericNotificationConfigService genericNotificationConfigService;
    @Autowired
    private PortalNotificationConfigService portalNotificationConfigService;
    @Autowired
    private NotifierService notifierService;
    @Autowired
    private SwaggerService swaggerService;
    @Autowired
    private SearchEngineService searchEngineService;
    @Autowired
    private ApiHeaderService apiHeaderService;
    @Autowired
    private Configuration freemarkerConfiguration;
    @Autowired
    private ParameterService parameterService;
    @Autowired
    private TagService tagService;
    @Autowired
    private EntrypointService entrypointService;
    @Autowired
    private WorkflowService workflowService;
    @Autowired
    private HttpClientService httpClientService;
    @Autowired
    private VirtualHostService virtualHostService;
    @Autowired
    private AlertService alertService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private ApplicationService applicationService;
    @Autowired
    private ImportConfiguration importConfiguration;

    private static final Pattern LOGGING_MAX_DURATION_PATTERN = Pattern.compile("(?<before>.*)\\#request.timestamp\\s*\\<\\=?\\s*(?<timestamp>\\d*)l(?<after>.*)");
    private static final String LOGGING_MAX_DURATION_CONDITION = "#request.timestamp <= %dl";
    private static final String LOGGING_DELIMITER_BASE = "\\s+(\\|\\||\\&\\&)\\s+";
    private static final String ENDPOINTS_DELIMITER = "\n";

    @Override
    public ApiEntity create(final NewApiEntity newApiEntity, final String userId) throws ApiAlreadyExistsException {
        return create(newApiEntity, userId, null, null);
    }

    @Override
    public ApiEntity create(final SwaggerApiEntity swaggerApiEntity, final String userId,
                            final ImportSwaggerDescriptorEntity swaggerDescriptor) throws ApiAlreadyExistsException {

        final NewApiEntity newApiEntity = new NewApiEntity();
        newApiEntity.setVersion(swaggerApiEntity.getVersion());
        newApiEntity.setName(swaggerApiEntity.getName());
        newApiEntity.setContextPath(swaggerApiEntity.getContextPath());
        newApiEntity.setDescription(swaggerApiEntity.getDescription());
        newApiEntity.setEndpoint(String.join(ENDPOINTS_DELIMITER, swaggerApiEntity.getEndpoint()));

        return create(newApiEntity, userId, swaggerDescriptor, swaggerApiEntity);
    }

    private ApiEntity create(final NewApiEntity newApiEntity, final String userId,
                             final ImportSwaggerDescriptorEntity swaggerDescriptor, final SwaggerApiEntity swaggerApiEntity) throws ApiAlreadyExistsException {
        UpdateApiEntity apiEntity = new UpdateApiEntity();

        apiEntity.setName(newApiEntity.getName());
        apiEntity.setDescription(newApiEntity.getDescription());
        apiEntity.setVersion(newApiEntity.getVersion());
        // check the existence of groups
        if (newApiEntity.getGroups() != null && !newApiEntity.getGroups().isEmpty()) {
            try {
                groupService.findByIds(newApiEntity.getGroups());
            } catch (GroupsNotFoundException gnfe) {
                throw new InvalidDataException("Groups [" + newApiEntity.getGroups() + "] does not exist");
            }
        }
        apiEntity.setGroups(newApiEntity.getGroups());

        Proxy proxy = new Proxy();
        proxy.setVirtualHosts(singletonList(new VirtualHost(newApiEntity.getContextPath())));
        EndpointGroup group = new EndpointGroup();
        group.setName("default-group");

        String[] endpoints = null;
        if (newApiEntity.getEndpoint() != null) {
            endpoints = newApiEntity.getEndpoint().split(ENDPOINTS_DELIMITER);
        }

        if (endpoints == null) {
            group.setEndpoints(singleton(new HttpEndpoint("default", null)));
        } else if (endpoints.length == 1) {
            group.setEndpoints(singleton(new HttpEndpoint("default", endpoints[0])));
        } else {
            group.setEndpoints(new HashSet<>());
            for (int i = 0; i < endpoints.length; i++) {
                group.getEndpoints().add(new HttpEndpoint("server" + (i + 1), endpoints[i]));
            }
        }
        proxy.setGroups(singleton(group));
        apiEntity.setProxy(proxy);

        final List<String> declaredPaths = newApiEntity.getPaths() != null ? newApiEntity.getPaths() : new ArrayList<>();
        if (!declaredPaths.contains("/")) {
            declaredPaths.add(0, "/");
        }

        // Initialize with a default path and provided paths
        Map<String, Path> paths = declaredPaths.stream().map(sPath -> {
            Path path = new Path();
            path.setPath(sPath);
            return path;
        }).collect(toMap(Path::getPath, path -> path));

        apiEntity.setPaths(paths);
        apiEntity.setPathMappings(new HashSet<>(declaredPaths));

        if (swaggerApiEntity != null && swaggerDescriptor != null) {
            if (swaggerDescriptor.isWithPolicyPaths()) {
                apiEntity.setPaths(swaggerApiEntity.getPaths());
            }

            if (swaggerDescriptor.isWithPathMapping()) {
                apiEntity.setPathMappings(swaggerApiEntity.getPathMappings());
            }
        }

        final ApiEntity createdApi = create0(apiEntity, userId);

        createOrUpdateDocumentation(swaggerDescriptor, createdApi, true);

        return createdApi;
    }

    private void createOrUpdateDocumentation(final ImportSwaggerDescriptorEntity swaggerDescriptor,
                                             final ApiEntity api, boolean isForCreation) {
        List<PageEntity> apiDocs = pageService.search(new PageQuery.Builder()
                .api(api.getId())
                .type(PageType.SWAGGER)
                .build());

        if (swaggerDescriptor != null && swaggerDescriptor.isWithDocumentation()) {
            if (isForCreation || (apiDocs == null || apiDocs.isEmpty())) {
                final NewPageEntity page = new NewPageEntity();
                page.setName("Swagger");
                page.setType(SWAGGER);
                page.setOrder(1);
                if (INLINE.equals(swaggerDescriptor.getType())) {
                    page.setContent(swaggerDescriptor.getPayload());
                } else {
                    final PageSourceEntity source = new PageSourceEntity();
                    page.setSource(source);
                    source.setType("http-fetcher");
                    source.setConfiguration(objectMapper.convertValue(singletonMap("url", swaggerDescriptor.getPayload()), JsonNode.class));
                }
                pageService.createPage(api.getId(), page);
            } else if (apiDocs.size() == 1) {
                PageEntity pageToUpdate = apiDocs.get(0);
                final UpdatePageEntity page = new UpdatePageEntity();
                page.setName(pageToUpdate.getName());
                page.setOrder(pageToUpdate.getOrder());
                page.setHomepage(pageToUpdate.isHomepage());
                page.setPublished(pageToUpdate.isPublished());
                page.setParentId(pageToUpdate.getParentId());
                page.setConfiguration(pageToUpdate.getConfiguration());
                if (INLINE.equals(swaggerDescriptor.getType())) {
                    page.setContent(swaggerDescriptor.getPayload());
                } else {
                    final PageSourceEntity source = new PageSourceEntity();
                    page.setSource(source);
                    source.setType("http-fetcher");
                    source.setConfiguration(objectMapper.convertValue(singletonMap("url", swaggerDescriptor.getPayload()), JsonNode.class));
                }
                pageService.update(pageToUpdate.getId(), page);
            }
        }
    }

    private ApiEntity create0(UpdateApiEntity api, String userId) throws ApiAlreadyExistsException {
        return this.create0(api, userId, true);
    }

    private ApiEntity create0(UpdateApiEntity api, String userId, boolean createSystemFolder) throws ApiAlreadyExistsException {
        try {
            LOGGER.debug("Create {} for user {}", api, userId);

            String id = RandomString.generate();
            Optional<Api> checkApi = apiRepository.findById(id);
            if (checkApi.isPresent()) {
                throw new ApiAlreadyExistsException(id);
            }

            // if user changes sharding tags, then check if he is allowed to do it
            checkShardingTags(api, null);

            // format context-path and check if context path is unique
            virtualHostService.validate(api.getProxy().getVirtualHosts());

            // check endpoints name
            checkEndpointsName(api);

            // check HC inheritance
            checkHealthcheckInheritance(api);

            addLoggingMaxDuration(api.getProxy().getLogging());

            // check if there is regex errors in plaintext fields
            validateRegexfields(api);

            Api repoApi = convert(id, api);

            if (repoApi != null) {
                repoApi.setId(id);
                repoApi.setEnvironmentId(GraviteeContext.getCurrentEnvironment());
                // Set date fields
                repoApi.setCreatedAt(new Date());
                repoApi.setUpdatedAt(repoApi.getCreatedAt());

                // Be sure that lifecycle is set to STOPPED by default and visibility is private
                repoApi.setLifecycleState(LifecycleState.STOPPED);
                if (api.getVisibility() == null) {
                    repoApi.setVisibility(Visibility.PRIVATE);
                } else {
                    repoApi.setVisibility(Visibility.valueOf(api.getVisibility().toString()));
                }

                // Add Default groups
                Set<String> defaultGroups = groupService.findByEvent(GroupEvent.API_CREATE).
                        stream().
                        map(GroupEntity::getId).
                        collect(toSet());
                if (!defaultGroups.isEmpty() && repoApi.getGroups() == null) {
                    repoApi.setGroups(defaultGroups);
                } else if (!defaultGroups.isEmpty()) {
                    repoApi.getGroups().addAll(defaultGroups);
                }

                repoApi.setApiLifecycleState(ApiLifecycleState.CREATED);
                if (parameterService.findAsBoolean(Key.API_REVIEW_ENABLED)) {
                    workflowService.create(WorkflowReferenceType.API, id, REVIEW, userId, DRAFT, "");
                }

                Api createdApi = apiRepository.create(repoApi);

                if (createSystemFolder) {
                    //Create SystemFolder
                    NewPageEntity asideSystemFolder = new NewPageEntity();
                    asideSystemFolder.setName(SystemFolderType.ASIDE.folderName());
                    asideSystemFolder.setPublished(true);
                    asideSystemFolder.setType(PageType.SYSTEM_FOLDER);
                    pageService.createPage(createdApi.getId(), asideSystemFolder);
                }

                // Audit
                auditService.createApiAuditLog(
                        createdApi.getId(),
                        Collections.emptyMap(),
                        API_CREATED,
                        createdApi.getCreatedAt(),
                        null,
                        createdApi);

                // Add the primary owner of the newly created API
                UserEntity primaryOwner = userService.findById(userId);
                if (primaryOwner != null) {
                    membershipService.addRoleToMemberOnReference(
                            new MembershipService.MembershipReference(MembershipReferenceType.API, createdApi.getId()),
                            new MembershipService.MembershipMember(userId, null, MembershipMemberType.USER),
                            new MembershipService.MembershipRole(RoleScope.API, SystemRole.PRIMARY_OWNER.name()));

                    // create the default mail notification
                    final String emailMetadataValue = "${(api.primaryOwner.email)!''}";

                    GenericNotificationConfigEntity notificationConfigEntity = new GenericNotificationConfigEntity();
                    notificationConfigEntity.setName("Default Mail Notifications");
                    notificationConfigEntity.setReferenceType(HookScope.API.name());
                    notificationConfigEntity.setReferenceId(createdApi.getId());
                    notificationConfigEntity.setHooks(Arrays.stream(ApiHook.values()).map(Enum::name).collect(toList()));
                    notificationConfigEntity.setNotifier(NotifierServiceImpl.DEFAULT_EMAIL_NOTIFIER_ID);
                    notificationConfigEntity.setConfig(emailMetadataValue);
                    genericNotificationConfigService.create(notificationConfigEntity);

                    // create the default mail support metadata
                    NewApiMetadataEntity newApiMetadataEntity = new NewApiMetadataEntity();
                    newApiMetadataEntity.setFormat(MetadataFormat.MAIL);
                    newApiMetadataEntity.setName(DefaultMetadataUpgrader.METADATA_EMAIL_SUPPORT_KEY);
                    newApiMetadataEntity.setDefaultValue(emailMetadataValue);
                    newApiMetadataEntity.setValue(emailMetadataValue);
                    newApiMetadataEntity.setApiId(createdApi.getId());
                    apiMetadataService.create(newApiMetadataEntity);

                    //TODO add membership log
                    ApiEntity apiEntity = convert(createdApi, primaryOwner, null);
                    searchEngineService.index(apiEntity, false);
                    return apiEntity;
                } else {
                    LOGGER.error("Unable to create API {} because primary owner role has not been found.", api.getName());
                    throw new TechnicalManagementException("Unable to create API " + api.getName() + " because primary owner role has not been found");
                }
            } else {
                LOGGER.error("Unable to create API {} because of previous error.", api.getName());
                throw new TechnicalManagementException("Unable to create API " + api.getName());
            }
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to create {} for user {}", api, userId, ex);
            throw new TechnicalManagementException("An error occurs while trying create " + api + " for user " + userId, ex);
        }
    }

    private void checkEndpointsName(UpdateApiEntity api) {
        if (api.getProxy() != null && api.getProxy().getGroups() != null) {
            for (EndpointGroup group : api.getProxy().getGroups()) {
                assertEndpointNameNotContainsInvalidCharacters(group.getName());
                if (group.getEndpoints() != null) {
                    for (Endpoint endpoint : group.getEndpoints()) {
                        assertEndpointNameNotContainsInvalidCharacters(endpoint.getName());
                    }
                }
            }
        }
    }

    private void checkEndpointsExists(UpdateApiEntity api) {
        if (api.getProxy().getGroups() == null
                || api.getProxy().getGroups().isEmpty()) {
            throw new EndpointMissingException();
        }

        EndpointGroup endpointGroup = api.getProxy().getGroups().iterator().next();
        //Is service discovery enabled ?
        EndpointDiscoveryService endpointDiscoveryService = endpointGroup.getServices() == null ? null : endpointGroup.getServices().get(EndpointDiscoveryService.class);
        if ((endpointDiscoveryService == null || !endpointDiscoveryService.isEnabled()) &&
                (endpointGroup.getEndpoints() == null || endpointGroup.getEndpoints().isEmpty())) {
            throw new EndpointMissingException();
        }
    }

    private void checkHealthcheckInheritance(UpdateApiEntity api) {
        boolean inherit = false;

        if (api.getProxy() != null && api.getProxy().getGroups() != null) {
            for (EndpointGroup group : api.getProxy().getGroups()) {
                if (group.getEndpoints() != null) {
                    for (Endpoint endpoint : group.getEndpoints()) {
                        if (endpoint instanceof HttpEndpoint) {
                            HttpEndpoint httpEndpoint = (HttpEndpoint) endpoint;
                            if (httpEndpoint.getHealthCheck() != null && httpEndpoint.getHealthCheck().isInherit()) {
                                inherit = true;
                                break;
                            }
                        }
                    }
                }
            }
        }

        if (inherit) {
            //if endpoints are set to inherit HC configuration, this configuration must exists.
            boolean hcServiceExists = false;
            if (api.getServices() != null) {
                for (Service service : api.getServices().getAll()) {
                    if (service instanceof HealthCheckService) {
                        hcServiceExists = true;
                        break;
                    }
                }
            }

            if (!hcServiceExists) {
                throw new HealthcheckInheritanceException();
            }
        }
    }

    private void assertEndpointNameNotContainsInvalidCharacters(String name) {
        if (name != null && name.contains(":")) {
            throw new EndpointNameInvalidException(name);
        }
    }

    private void addLoggingMaxDuration(Logging logging) {
        if (logging != null && !LoggingMode.NONE.equals(logging.getMode())) {
            Optional<Long> optionalMaxDuration = parameterService.findAll(Key.LOGGING_DEFAULT_MAX_DURATION, Long::valueOf).stream().findFirst();
            if (optionalMaxDuration.isPresent() && optionalMaxDuration.get() > 0) {

                long maxEndDate = System.currentTimeMillis() + optionalMaxDuration.get();

                // if no condition set, add one
                if (logging.getCondition() == null || logging.getCondition().isEmpty()) {
                    logging.setCondition(String.format(LOGGING_MAX_DURATION_CONDITION, maxEndDate));
                } else {
                    Matcher matcher = LOGGING_MAX_DURATION_PATTERN.matcher(logging.getCondition());
                    if (matcher.matches()) {
                        String currentDurationAsStr = matcher.group("timestamp");
                        String before = formatExpression(matcher, "before");
                        String after = formatExpression(matcher, "after");
                        try {
                            final long currentDuration = Long.parseLong(currentDurationAsStr);
                            if (currentDuration > maxEndDate || (!before.isEmpty() || !after.isEmpty())) {
                                logging.setCondition(before + String.format(LOGGING_MAX_DURATION_CONDITION, maxEndDate) + after);
                            }
                        } catch (NumberFormatException nfe) {
                            LOGGER.error("Wrong format of the logging condition. Add the default one", nfe);
                            logging.setCondition(before + String.format(LOGGING_MAX_DURATION_CONDITION, maxEndDate) + after);
                        }
                    } else {
                        logging.setCondition(String.format(LOGGING_MAX_DURATION_CONDITION, maxEndDate) + " && " + logging.getCondition());
                    }
                }
            }
        }
    }

    private String formatExpression(final Matcher matcher, final String group) {
        final String matchedExpression = matcher.group(group);
        final boolean expressionBlank = matchedExpression == null || "".equals(matchedExpression);
        final boolean after = "after".equals(group);

        String expression;
        if (after) {
            expression = expressionBlank ? "" : " && (" + matchedExpression + ")";
            expression = expression.replaceAll("\\(" + LOGGING_DELIMITER_BASE, "\\(");
        } else {
            expression = expressionBlank ? "" : "(" + matchedExpression + ") && ";
            expression = expression.replaceAll(LOGGING_DELIMITER_BASE + "\\)", "\\)");
        }
        return expression;
    }

    @Override
    public ApiEntity findById(String apiId) {
        try {
            LOGGER.debug("Find API by ID: {}", apiId);

            Optional<Api> api = apiRepository.findById(apiId);

            if (api.isPresent()) {
                ApiEntity apiEntity = convert(api.get(), getPrimaryOwner(api.get()), null);

                // Compute entrypoints
                calculateEntrypoints(apiEntity);

                return apiEntity;
            }

            throw new ApiNotFoundException(apiId);
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to find an API using its ID: {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to find an API using its ID: " + apiId, ex);
        }
    }

    private UserEntity getPrimaryOwner(Api api) throws TechnicalException {
        MembershipEntity primaryOwnerMemberEntity = membershipService.getPrimaryOwner(io.gravitee.rest.api.model.MembershipReferenceType.API, api.getId());
        if (primaryOwnerMemberEntity == null) {
            LOGGER.error("The API {} doesn't have any primary owner.", api.getId());
            throw new TechnicalException("The API " + api.getId() + " doesn't have any primary owner.");
        }

        return userService.findById(primaryOwnerMemberEntity.getMemberId());
    }

    private void calculateEntrypoints(ApiEntity api) {
        List<ApiEntrypointEntity> apiEntrypoints = new ArrayList<>();

        if (api.getProxy() != null) {
            String defaultEntrypoint = parameterService.find(Key.PORTAL_ENTRYPOINT);
            final String scheme = getScheme(defaultEntrypoint);
            if (api.getTags() != null && !api.getTags().isEmpty()) {
                List<EntrypointEntity> entrypoints = entrypointService.findAll();
                entrypoints.forEach(entrypoint -> {
                    Set<String> tagEntrypoints = new HashSet<>(Arrays.asList(entrypoint.getTags()));
                    tagEntrypoints.retainAll(api.getTags());

                    if (tagEntrypoints.size() == entrypoint.getTags().length) {
                        api.getProxy().getVirtualHosts().forEach(virtualHost -> {
                            String targetHost = (virtualHost.getHost() == null || !virtualHost.isOverrideEntrypoint()) ?
                                    entrypoint.getValue() : virtualHost.getHost();
                            if (!targetHost.toLowerCase().startsWith("http")) {
                                targetHost = scheme + "://" + targetHost;
                            }
                            apiEntrypoints.add(new ApiEntrypointEntity(
                                    tagEntrypoints,
                                    DUPLICATE_SLASH_REMOVER
                                            .matcher(targetHost + URI_PATH_SEPARATOR + virtualHost.getPath())
                                            .replaceAll(URI_PATH_SEPARATOR),
                                    virtualHost.getHost())
                            );
                        });
                    }
                });
            }

            // If empty, get the default entrypoint
            if (apiEntrypoints.isEmpty()) {
                api.getProxy().getVirtualHosts().forEach(virtualHost -> {
                    String targetHost = (virtualHost.getHost() == null || !virtualHost.isOverrideEntrypoint()) ?
                            defaultEntrypoint : virtualHost.getHost();
                    if (!targetHost.toLowerCase().startsWith("http")) {
                        targetHost = scheme + "://" + targetHost;
                    }
                    apiEntrypoints.add(new ApiEntrypointEntity(
                            DUPLICATE_SLASH_REMOVER
                                    .matcher(targetHost + URI_PATH_SEPARATOR + virtualHost.getPath())
                                    .replaceAll(URI_PATH_SEPARATOR), virtualHost.getHost())
                    );
                });
            }
        }

        api.setEntrypoints(apiEntrypoints);
    }

    private String getScheme(String defaultEntrypoint) {
        String scheme = "https";
        if (defaultEntrypoint != null) {
            try {
                scheme = new URL(defaultEntrypoint).getProtocol();
            } catch (MalformedURLException e) {
                // return default scheme
            }
        }
        return scheme;
    }

    @Override
    public Set<ApiEntity> findByVisibility(io.gravitee.rest.api.model.Visibility visibility) {
        try {
            LOGGER.debug("Find APIs by visibility {}", visibility);
            return convert(apiRepository.search(new ApiCriteria.Builder().environmentId(GraviteeContext.getCurrentEnvironment()).visibility(Visibility.valueOf(visibility.name())).build()));
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to find all APIs", ex);
            throw new TechnicalManagementException("An error occurs while trying to find all APIs", ex);
        }
    }

    @Override
    public Set<ApiEntity> findAll() {
        try {
            LOGGER.debug("Find all APIs");
            return convert(apiRepository.search(new ApiCriteria.Builder().environmentId(GraviteeContext.getCurrentEnvironment()).build()));
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to find all APIs", ex);
            throw new TechnicalManagementException("An error occurs while trying to find all APIs", ex);
        }
    }

    @Override
    public Set<ApiEntity> findAllLight() {
        try {
            LOGGER.debug("Find all APIs without some fields (definition, picture...)");
            return convert(apiRepository.search(new ApiCriteria.Builder().environmentId(GraviteeContext.getCurrentEnvironment()).build(),
                    new ApiFieldExclusionFilter.Builder().excludeDefinition().excludePicture().build()));
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to find all APIs light", ex);
            throw new TechnicalManagementException("An error occurs while trying to find all APIs light", ex);
        }
    }

    @Override
    public Set<ApiEntity> findByUser(String userId, ApiQuery apiQuery, boolean portal) {
        try {
            LOGGER.debug("Find APIs by user {}", userId);

            //get all public apis
            List<Api> publicApis;
            if (portal) {
                publicApis = apiRepository.search(queryToCriteria(apiQuery).visibility(PUBLIC).build());
            } else {
                publicApis = emptyList();
            }

            List<Api> userApis = emptyList();
            List<Api> groupApis = emptyList();
            List<Api> subscribedApis = emptyList();

            // for others API, user must be authenticated
            if (userId != null) {
                // get user apis
                final String[] userApiIds = membershipService
                        .getMembershipsByMemberAndReference(MembershipMemberType.USER, userId, MembershipReferenceType.API).stream()
                        .map(MembershipEntity::getReferenceId)
                        .toArray(String[]::new);
                if (userApiIds.length > 0) {
                    userApis = apiRepository.search(queryToCriteria(apiQuery).ids(userApiIds).build());
                }

                // get user groups apis
                final String[] groupIds = membershipService
                        .getMembershipsByMemberAndReference(MembershipMemberType.USER, userId, MembershipReferenceType.GROUP).stream()
                        .filter(m -> m.getRoleId() != null && roleService.findById(m.getRoleId()).getScope().equals(RoleScope.API))
                        .map(MembershipEntity::getReferenceId)
                        .toArray(String[]::new);
                if (groupIds.length > 0 && groupIds[0] != null) {
                    groupApis = apiRepository.search(queryToCriteria(apiQuery).groups(groupIds).build());
                }

                // get user subscribed apis, useful when an API becomes private and an app owner is not anymore in members
                if (portal) {
                    final Set<String> applications =
                            applicationService.findByUser(userId).stream().map(ApplicationListItem::getId).collect(toSet());
                    if (!applications.isEmpty()) {
                        final SubscriptionQuery query = new SubscriptionQuery();
                        query.setApplications(applications);
                        final Collection<SubscriptionEntity> subscriptions = subscriptionService.search(query);
                        if (subscriptions != null && !subscriptions.isEmpty()) {
                            subscribedApis = apiRepository
                                    .search(queryToCriteria(apiQuery).ids(subscriptions.stream()
                                            .map(SubscriptionEntity::getApi).distinct().toArray(String[]::new)).build());
                        }
                    }
                }
            }

            // merge all apis
            final Set<ApiEntity> apis = new HashSet<>();
            apis.addAll(convert(publicApis));
            apis.addAll(convert(userApis));
            apis.addAll(convert(groupApis));
            apis.addAll(convert(subscribedApis));
            return filterApiByQuery(apis.stream(), apiQuery).collect(toSet());
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to find APIs for user {}", userId, ex);
            throw new TechnicalManagementException("An error occurs while trying to find APIs for user " + userId, ex);
        }
    }

    @Override
    public Set<ApiEntity> findPublishedByUser(String userId, ApiQuery apiQuery) {
        if (apiQuery == null) {
            apiQuery = new ApiQuery();
        }
        apiQuery.setLifecycleStates(Arrays.asList(io.gravitee.rest.api.model.api.ApiLifecycleState.PUBLISHED));
        return findByUser(userId, apiQuery, true);
    }

    @Override
    public Set<ApiEntity> findPublishedByUser(String userId) {
        return findPublishedByUser(userId, null);
    }

    private Stream<ApiEntity> filterApiByQuery(Stream<ApiEntity> apiEntityStream, ApiQuery query) {
        if (query == null) {
            return apiEntityStream;
        }
        return apiEntityStream
                .filter(api -> query.getTag() == null || (api.getTags() != null && api.getTags().contains(query.getTag())))
                .filter(api -> query.getContextPath() == null || api.getProxy().getVirtualHosts().stream().anyMatch(
                        virtualHost -> query.getContextPath().equals(virtualHost.getPath())));
    }

    @Override
    public ApiEntity update(String apiId, SwaggerApiEntity swaggerApiEntity, ImportSwaggerDescriptorEntity swaggerDescriptor) {
        final ApiEntity apiEntityToUpdate = this.findById(apiId);
        final UpdateApiEntity updateApiEntity = convert(apiEntityToUpdate);

        // Overwrite from swagger
        updateApiEntity.setVersion(swaggerApiEntity.getVersion());
        updateApiEntity.setName(swaggerApiEntity.getName());
        updateApiEntity.setDescription(swaggerApiEntity.getDescription());

        // Overwrite from swagger, if asked
        if (swaggerApiEntity != null) {
            updateApiEntity.setPaths(swaggerApiEntity.getPaths());

            if (swaggerDescriptor.isWithPathMapping()) {
                updateApiEntity.setPathMappings(swaggerApiEntity.getPathMappings());
            }
        }

        createOrUpdateDocumentation(swaggerDescriptor, apiEntityToUpdate, false);

        return update(apiId, updateApiEntity);
    }

    @Override
    public ApiEntity update(String apiId, UpdateApiEntity updateApiEntity) {
        try {
            LOGGER.debug("Update API {}", apiId);

            Optional<Api> optApiToUpdate = apiRepository.findById(apiId);
            if (!optApiToUpdate.isPresent()) {
                throw new ApiNotFoundException(apiId);
            }

            // check if entrypoints are unique
            virtualHostService.validate(updateApiEntity.getProxy().getVirtualHosts(), apiId);

            // check endpoints presence
            checkEndpointsExists(updateApiEntity);

            // check endpoints name
            checkEndpointsName(updateApiEntity);

            // check HC inheritance
            checkHealthcheckInheritance(updateApiEntity);

            addLoggingMaxDuration(updateApiEntity.getProxy().getLogging());

            // check if there is regex errors in plaintext fields
            validateRegexfields(updateApiEntity);

            final ApiEntity apiToCheck = convert(optApiToUpdate.get());

            // if user changes sharding tags, then check if he is allowed to do it
            checkShardingTags(updateApiEntity, apiToCheck);

            // if lifecycle state not provided, set the saved one
            if (updateApiEntity.getLifecycleState() == null) {
                updateApiEntity.setLifecycleState(apiToCheck.getLifecycleState());
            }

            // check lifecycle state
            checkLifecycleState(updateApiEntity, apiToCheck);

            // check the existence of groups
            if (updateApiEntity.getGroups() != null && !updateApiEntity.getGroups().isEmpty()) {
                try {
                    groupService.findByIds(updateApiEntity.getGroups());
                } catch (GroupsNotFoundException gnfe) {
                    throw new InvalidDataException("Groups [" + updateApiEntity.getGroups() + "] does not exist");
                }
            }

            // add a default path
            if (updateApiEntity.getPaths() == null || updateApiEntity.getPaths().isEmpty()) {
                updateApiEntity.setPaths(singletonMap("/", new Path()));
            }

            Api apiToUpdate = optApiToUpdate.get();
            Api api = convert(apiId, updateApiEntity);

            if (api != null) {
                api.setId(apiId.trim());
                api.setUpdatedAt(new Date());

                // Copy fields from existing values
                api.setEnvironmentId(apiToUpdate.getEnvironmentId());
                api.setDeployedAt(apiToUpdate.getDeployedAt());
                api.setCreatedAt(apiToUpdate.getCreatedAt());
                api.setLifecycleState(apiToUpdate.getLifecycleState());
                if (updateApiEntity.getPicture() == null) {
                    api.setPicture(apiToUpdate.getPicture());
                }
                if (updateApiEntity.getGroups() == null) {
                    api.setGroups(apiToUpdate.getGroups());
                }
                if (updateApiEntity.getLabels() == null) {
                    api.setLabels(apiToUpdate.getLabels());
                }
                if (updateApiEntity.getCategories() == null) {
                    api.setCategories(apiToUpdate.getCategories());
                }

                if (ApiLifecycleState.DEPRECATED.equals(api.getApiLifecycleState())) {
                    planService.findByApi(api.getId()).forEach(plan -> {
                        if (PlanStatus.PUBLISHED.equals(plan.getStatus()) || PlanStatus.STAGING.equals(plan.getStatus())) {
                            planService.depreciate(plan.getId());
                        }
                    });
                    notifierService.trigger(ApiHook.API_DEPRECATED, apiId,
                            new NotificationParamsBuilder()
                                    .api(apiToCheck)
                                    .user(userService.findById(getAuthenticatedUsername()))
                                    .build());
                }

                Api updatedApi = apiRepository.update(api);

                // Audit
                auditService.createApiAuditLog(
                        updatedApi.getId(),
                        Collections.emptyMap(),
                        API_UPDATED,
                        updatedApi.getUpdatedAt(),
                        apiToUpdate,
                        updatedApi);

                if (parameterService.findAsBoolean(Key.LOGGING_AUDIT_TRAIL_ENABLED)) {
                    // Audit API logging if option is enabled
                    auditApiLogging(apiToUpdate, updatedApi);
                }

                ApiEntity apiEntity = convert(singletonList(updatedApi)).iterator().next();
                searchEngineService.index(apiEntity, false);
                return apiEntity;
            } else {
                LOGGER.error("Unable to update API {} because of previous error.", apiId);
                throw new TechnicalManagementException("Unable to update API " + apiId);
            }
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to update API {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to update API " + apiId, ex);
        }
    }

    private void checkShardingTags(final UpdateApiEntity updateApiEntity, final ApiEntity existingAPI) {
        if (!isAdmin()) {
            final Set<String> tagsToUpdate = updateApiEntity.getTags() == null ? new HashSet<>() : updateApiEntity.getTags();
            final Set<String> updatedTags;
            if (existingAPI == null) {
                updatedTags = tagsToUpdate;
            } else {
                final Set<String> existingAPITags = existingAPI.getTags() == null ? new HashSet<>() : existingAPI.getTags();
                updatedTags = existingAPITags.stream().filter(tag -> !tagsToUpdate.contains(tag)).collect(toSet());
                updatedTags.addAll(tagsToUpdate.stream().filter(tag -> !existingAPITags.contains(tag)).collect(toSet()));
            }
            if (updatedTags != null && !updatedTags.isEmpty()) {
                final Set<String> userTags = tagService.findByUser(getAuthenticatedUsername());
                if (!userTags.containsAll(updatedTags)) {
                    final String[] notAllowedTags = updatedTags.stream().filter(tag -> !userTags.contains(tag)).toArray(String[]::new);
                    throw new TagNotAllowedException(notAllowedTags);
                }
            }
        }
    }

    private void validateRegexfields(final UpdateApiEntity updateApiEntity) {
        // validate regex on paths
        if (updateApiEntity.getPaths() != null) {
            updateApiEntity.getPaths().forEach((path, v) -> {
                try {
                    Pattern.compile(path);
                } catch (java.util.regex.PatternSyntaxException pse) {
                    LOGGER.error("An error occurs while trying to parse the path {}", path, pse);
                    throw new TechnicalManagementException("An error occurs while trying to parse the path " + path, pse);
                }
            });
        }

        // validate regex on pathMappings
        if (updateApiEntity.getPathMappings() != null) {
            updateApiEntity.getPathMappings().forEach(pathMapping -> {
                try {
                    Pattern.compile(pathMapping);
                } catch (java.util.regex.PatternSyntaxException pse) {
                    LOGGER.error("An error occurs while trying to parse the path mapping {}", pathMapping, pse);
                    throw new TechnicalManagementException("An error occurs while trying to parse the path mapping" + pathMapping, pse);
                }
            });
        }
    }

    private void checkLifecycleState(final UpdateApiEntity updateApiEntity, final ApiEntity existingAPI) {
        if (io.gravitee.rest.api.model.api.ApiLifecycleState.DEPRECATED.equals(existingAPI.getLifecycleState())) {
            throw new LifecycleStateChangeNotAllowedException(updateApiEntity.getLifecycleState().name());
        }
        if (existingAPI.getLifecycleState().name().equals(updateApiEntity.getLifecycleState().name())) {
            return;
        }
        if (io.gravitee.rest.api.model.api.ApiLifecycleState.ARCHIVED.equals(existingAPI.getLifecycleState())) {
            if (!io.gravitee.rest.api.model.api.ApiLifecycleState.ARCHIVED.equals(updateApiEntity.getLifecycleState())) {
                throw new LifecycleStateChangeNotAllowedException(updateApiEntity.getLifecycleState().name());
            }
        } else if (io.gravitee.rest.api.model.api.ApiLifecycleState.UNPUBLISHED.equals(existingAPI.getLifecycleState())) {
            if (io.gravitee.rest.api.model.api.ApiLifecycleState.CREATED.equals(updateApiEntity.getLifecycleState())) {
                throw new LifecycleStateChangeNotAllowedException(updateApiEntity.getLifecycleState().name());
            }
        } else if (io.gravitee.rest.api.model.api.ApiLifecycleState.CREATED.equals(existingAPI.getLifecycleState())) {
            if (WorkflowState.IN_REVIEW.equals(existingAPI.getWorkflowState())) {
                throw new LifecycleStateChangeNotAllowedException(updateApiEntity.getLifecycleState().name());
            }
        }
    }

    @Override
    public void delete(String apiId) {
        try {
            LOGGER.debug("Delete API {}", apiId);

            Optional<Api> optApi = apiRepository.findById(apiId);
            if (!optApi.isPresent()) {
                throw new ApiNotFoundException(apiId);
            }

            if (optApi.get().getLifecycleState() == LifecycleState.STARTED) {
                throw new ApiRunningStateException(apiId);
            } else {
                // Delete plans
                Set<PlanEntity> plans = planService.findByApi(apiId);
                Set<String> plansNotClosed = plans.stream()
                        .filter(plan -> plan.getStatus() == PlanStatus.PUBLISHED)
                        .map(PlanEntity::getName)
                        .collect(toSet());

                if (!plansNotClosed.isEmpty()) {
                    throw new ApiNotDeletableException(plansNotClosed);
                }

                Collection<SubscriptionEntity> subscriptions = subscriptionService.findByApi(apiId);
                subscriptions.forEach(sub -> subscriptionService.delete(sub.getId()));

                for (PlanEntity plan : plans) {
                    planService.delete(plan.getId());
                }

                // Delete events
                final EventQuery query = new EventQuery();
                query.setApi(apiId);
                eventService.search(query)
                        .forEach(event -> eventService.delete(event.getId()));

                // Delete pages
                final List<PageEntity> pages = pageService.search(new PageQuery.Builder().api(apiId).build());
                pages.forEach(pageEntity -> pageService.delete(pageEntity.getId()));

                // Delete top API
                topApiService.delete(apiId);
                // Delete API
                apiRepository.delete(apiId);
                // Delete memberships
                membershipService.deleteReference(MembershipReferenceType.API, apiId);
                // Delete notifications
                genericNotificationConfigService.deleteReference(NotificationReferenceType.API, apiId);
                portalNotificationConfigService.deleteReference(NotificationReferenceType.API, apiId);
                // Delete alerts
                final List<AlertTriggerEntity> alerts = alertService.findByReference(AlertReferenceType.API, apiId);
                alerts.forEach(alert -> alertService.delete(alert.getId(), alert.getReferenceId()));
                // delete all reference on api quality rule
                apiQualityRuleRepository.deleteByApi(apiId);
                // Audit
                auditService.createApiAuditLog(
                        apiId,
                        Collections.emptyMap(),
                        API_DELETED,
                        new Date(),
                        optApi.get(),
                        null);
                // remove from search engine
                searchEngineService.delete(convert(optApi.get()), false);
            }
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to delete API {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to delete API " + apiId, ex);
        }
    }

    @Override
    public ApiEntity start(String apiId, String userId) {
        try {
            LOGGER.debug("Start API {}", apiId);
            ApiEntity apiEntity = updateLifecycle(apiId, LifecycleState.STARTED, userId);
            notifierService.trigger(
                    ApiHook.API_STARTED,
                    apiId,
                    new NotificationParamsBuilder()
                            .api(apiEntity)
                            .user(userService.findById(userId))
                            .build());
            return apiEntity;
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to start API {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to start API " + apiId, ex);
        }
    }

    @Override
    public ApiEntity stop(String apiId, String userId) {
        try {
            LOGGER.debug("Stop API {}", apiId);
            ApiEntity apiEntity = updateLifecycle(apiId, LifecycleState.STOPPED, userId);
            notifierService.trigger(
                    ApiHook.API_STOPPED,
                    apiId,
                    new NotificationParamsBuilder()
                            .api(apiEntity)
                            .user(userService.findById(userId))
                            .build());
            return apiEntity;
        } catch (TechnicalException ex) {
            LOGGER.error("An error occurs while trying to stop API {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to stop API " + apiId, ex);
        }
    }

    @Override
    public boolean isSynchronized(String apiId) {
        try {
            // 1_ First, check the API state
            ApiEntity api = findById(apiId);

            Map<String, Object> properties = new HashMap<>();
            properties.put(Event.EventProperties.API_ID.getValue(), apiId);

            io.gravitee.common.data.domain.Page<EventEntity> events =
                    eventService.search(Arrays.asList(PUBLISH_API, EventType.UNPUBLISH_API),
                            properties, 0, 0, 0, 1);

            if (!events.getContent().isEmpty()) {
                // According to page size, we know that we have only one element in the list
                EventEntity lastEvent = events.getContent().get(0);

                //TODO: Done only for backward compatibility with 0.x. Must be removed later (1.1.x ?)
                boolean enabled = objectMapper.getDeserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                Api payloadEntity = objectMapper.readValue(lastEvent.getPayload(), Api.class);
                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, enabled);

                final ApiEntity deployedApi = convert(payloadEntity);
                // Remove policy description from sync check
                removeDescriptionFromPolicies(api);
                removeDescriptionFromPolicies(deployedApi);

                boolean sync = apiSynchronizationProcessor.processCheckSynchronization(deployedApi, api);

                // 2_ If API definition is synchronized, check if there is any modification for API's plans
                // but only for published or closed plan
                if (sync) {
                    Set<PlanEntity> plans = planService.findByApi(api.getId());
                    sync = plans.stream()
                            .filter(plan -> (plan.getStatus() != PlanStatus.STAGING))
                            .filter(plan -> plan.getNeedRedeployAt().after(api.getDeployedAt())).count() == 0;
                }

                return sync;
            }
        } catch (Exception e) {
            LOGGER.error("An error occurs while trying to check API synchronization state {}", apiId, e);
        }

        return false;
    }

    private void removeDescriptionFromPolicies(final ApiEntity api) {
        if (api.getPaths() != null) {
            api.getPaths().forEach((s, path) -> {
                if (path.getRules() != null) {
                    path.getRules().forEach(rule -> rule.setDescription(""));
                }
            });
        }

    }

    @Override
    public ApiEntity deploy(String apiId, String userId, EventType eventType) {
        try {
            LOGGER.debug("Deploy API : {}", apiId);

            return deployCurrentAPI(apiId, userId, eventType);
        } catch (Exception ex) {
            LOGGER.error("An error occurs while trying to deploy API: {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to deploy API: " + apiId, ex);
        }
    }

    @Override
    public ApiEntity rollback(String apiId, UpdateApiEntity api) {
        LOGGER.debug("Rollback API : {}", apiId);
        try {
            // Audit
            auditService.createApiAuditLog(
                    apiId,
                    Collections.emptyMap(),
                    API_ROLLBACKED,
                    new Date(),
                    null,
                    null);

            return update(apiId, api);
        } catch (Exception ex) {
            LOGGER.error("An error occurs while trying to rollback API: {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while trying to rollback API: " + apiId, ex);
        }
    }

    private ApiEntity deployCurrentAPI(String apiId, String userId, EventType eventType) throws Exception {
        Optional<Api> api = apiRepository.findById(apiId);

        if (api.isPresent()) {
            // add deployment date
            Api apiValue = api.get();
            apiValue.setUpdatedAt(new Date());
            apiValue.setDeployedAt(apiValue.getUpdatedAt());
            apiValue = apiRepository.update(apiValue);

            Map<String, String> properties = new HashMap<>();
            properties.put(Event.EventProperties.API_ID.getValue(), apiValue.getId());
            properties.put(Event.EventProperties.USER.getValue(), userId);

            // Clear useless field for history
            apiValue.setPicture(null);

            // And create event
            eventService.create(eventType, objectMapper.writeValueAsString(apiValue), properties);

            return convert(singletonList(apiValue)).iterator().next();
        } else {
            throw new ApiNotFoundException(apiId);
        }
    }

    /**
     * Allows to deploy the last published API
     * @param apiId the API id
     * @param userId the user id
     * @param eventType the event type
     * @return The persisted API or null
     * @throws TechnicalException if an exception occurs while saving the API
     */
    private ApiEntity deployLastPublishedAPI(String apiId, String userId, EventType eventType) throws TechnicalException {
        final EventQuery query = new EventQuery();
        query.setApi(apiId);
        query.setTypes(singleton(PUBLISH_API));

        final Optional<EventEntity> optEvent =
                eventService.search(query).stream().max(comparing(EventEntity::getCreatedAt));
        try {
            if (optEvent.isPresent()) {
                EventEntity event = optEvent.get();
                JsonNode node = objectMapper.readTree(event.getPayload());
                Api lastPublishedAPI = objectMapper.convertValue(node, Api.class);
                lastPublishedAPI.setLifecycleState(convert(eventType));
                lastPublishedAPI.setUpdatedAt(new Date());
                lastPublishedAPI.setDeployedAt(new Date());
                Map<String, String> properties = new HashMap<>();
                properties.put(Event.EventProperties.API_ID.getValue(), lastPublishedAPI.getId());
                properties.put(Event.EventProperties.USER.getValue(), userId);

                // Clear useless field for history
                lastPublishedAPI.setPicture(null);

                // And create event
                eventService.create(eventType, objectMapper.writeValueAsString(lastPublishedAPI), properties);
                return null;
            } else {
                // this is the first time we start the api without previously deployed id.
                // let's do it.
                return this.deploy(apiId, userId, PUBLISH_API);
            }
        } catch (Exception e) {
            LOGGER.error("An error occurs while trying to deploy last published API {}", apiId, e);
            throw new TechnicalException("An error occurs while trying to deploy last published API " + apiId, e);
        }
    }

    @Override
    public String exportAsJson(final String apiId, String exportVersion, String... filteredFields) {
        ApiEntity apiEntity = findById(apiId);

        // set metadata for serialize process
        Map<String, Object> metadata = new HashMap<>();
        metadata.put(ApiSerializer.METADATA_EXPORT_VERSION, exportVersion);
        metadata.put(ApiSerializer.METADATA_FILTERED_FIELDS_LIST, Arrays.asList(filteredFields));
        apiEntity.setMetadata(metadata);

        try {
            return objectMapper.writeValueAsString(apiEntity);
        } catch (final Exception e) {
            LOGGER.error("An error occurs while trying to JSON serialize the API {}", apiEntity, e);
        }
        return "";
    }

    @Override
    public ApiEntity createOrUpdateWithDefinition(final ApiEntity apiEntity, String apiDefinitionOrURL, String userId) {

        String apiDefinition = apiDefinitionOrURL;
        if (apiDefinitionOrURL.toUpperCase().startsWith("HTTP")) {
            apiDefinition = fetchApiDefinitionContentFromURL(apiDefinitionOrURL);
        }

        try {
            final UpdateApiEntity importedApi = objectMapper
                    // because definition could contains other values than the api itself (pages, members)
                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                    .readValue(apiDefinition, UpdateApiEntity.class);

            // Initialize with a default path
            if (importedApi.getPaths() == null || importedApi.getPaths().isEmpty()) {
                Path path = new Path();
                path.setPath("/");
                importedApi.setPaths(Collections.singletonMap("/", path));
            }

            //create group if not exist & replace groupName by groupId
            if (importedApi.getGroups() != null) {
                Set<String> groupNames = new HashSet<>(importedApi.getGroups());
                importedApi.getGroups().clear();
                for (String name : groupNames) {
                    List<GroupEntity> groupEntities = groupService.findByName(name);
                    GroupEntity group;
                    if (groupEntities.isEmpty()) {
                        NewGroupEntity newGroupEntity = new NewGroupEntity();
                        newGroupEntity.setName(name);
                        group = groupService.create(newGroupEntity);
                    } else {
                        group = groupEntities.get(0);
                    }
                    importedApi.getGroups().add(group.getId());
                }
            }

            // Read the whole definition
            final JsonNode jsonNode = objectMapper.readTree(apiDefinition);

            // Views & Categories
            // Before 3.0.2, API 'categories' were called 'views'. This is for compatibility.
            final JsonNode viewsDefinition = jsonNode.path("views");
            if (viewsDefinition != null && viewsDefinition.isArray()) {
                Set<String> categories = new HashSet<>();
                for (JsonNode viewNode : viewsDefinition) {
                    categories.add(viewNode.asText());
                }
                importedApi.setCategories(categories);
            }

            ApiEntity createdOrUpdatedApiEntity;
            if (apiEntity == null || apiEntity.getId() == null) {
                createdOrUpdatedApiEntity = create0(importedApi, userId);
            } else {
                createdOrUpdatedApiEntity = update(apiEntity.getId(), importedApi);
            }


            // Members
            final JsonNode membersToImport = jsonNode.path("members");
            if (membersToImport != null && membersToImport.isArray()) {
                // get current members of the api
                Set<MemberToImport> membersAlreadyPresent = membershipService
                        .getMembersByReference(MembershipReferenceType.API, createdOrUpdatedApiEntity.getId())
                        .stream()
                        .map(member -> {
                            UserEntity userEntity = userService.findById(member.getId());
                            return new MemberToImport(userEntity.getSource(), userEntity.getSourceId(), member.getRoles().stream().map(RoleEntity::getId).collect(Collectors.toList()), null);
                        }).collect(toSet());
                // get the current PO
                Optional<RoleEntity> optPoRole = roleService.findByScopeAndName(RoleScope.API, SystemRole.PRIMARY_OWNER.name());
                if (optPoRole.isPresent()) {
                    String poRoleId = optPoRole.get().getId();
                    MemberToImport currentPo = membersAlreadyPresent.stream()
                            .filter(memberToImport -> memberToImport.getRoles().contains(poRoleId))
                            .findFirst()
                            .orElse(new MemberToImport());

                    List<String> roleUsedInTransfert = null;
                    MemberToImport futurePO = null;


                    // upsert members
                    for (final JsonNode memberNode : membersToImport) {
                        MemberToImport memberToImport = objectMapper.readValue(memberNode.toString(), MemberToImport.class);
                        String roleToAdd = memberToImport.getRole();
                        List<String> rolesToImport = memberToImport.getRoles();
                        if (roleToAdd != null && !roleToAdd.isEmpty()) {
                            if (rolesToImport == null) {
                                rolesToImport = new ArrayList<>();
                                memberToImport.setRoles(rolesToImport);
                            }
                            Optional<RoleEntity> optRoleToAddEntity = roleService.findByScopeAndName(RoleScope.API, roleToAdd);
                            if (optRoleToAddEntity.isPresent()) {
                                rolesToImport.add(optRoleToAddEntity.get().getId());
                            } else {
                                LOGGER.warn("Role {} does not exist", roleToAdd);
                            }
                        }
                        if (rolesToImport != null) {
                            rolesToImport.sort(Comparator.naturalOrder());
                        }
                        boolean presentWithSameRole = memberToImport.getRoles() != null && !memberToImport.getRoles().isEmpty() && membersAlreadyPresent
                                .stream()
                                .anyMatch(m -> {
                                    m.getRoles().sort(Comparator.naturalOrder());
                                    return
                                            m.getRoles().equals(memberToImport.getRoles())
                                                    && (m.getSourceId().equals(memberToImport.getSourceId())
                                                    && m.getSource().equals(memberToImport.getSource()));
                                });

                        // add/update members if :
                        //  - not already present with the same role
                        //  - not the new PO
                        //  - not the current PO
                        if (!presentWithSameRole
                                && (memberToImport.getRoles() != null && !memberToImport.getRoles().isEmpty() && !memberToImport.getRoles().contains(poRoleId))
                                && !(memberToImport.getSourceId().equals(currentPo.getSourceId())
                                && memberToImport.getSource().equals(currentPo.getSource()))) {
                            try {
                                UserEntity userEntity = userService.findBySource(memberToImport.getSource(), memberToImport.getSourceId(), false);

                                rolesToImport.forEach(role ->
                                        membershipService.addRoleToMemberOnReference(
                                                MembershipReferenceType.API,
                                                createdOrUpdatedApiEntity.getId(),
                                                MembershipMemberType.USER,
                                                userEntity.getId(),
                                                role)
                                );
                            } catch (UserNotFoundException unfe) {

                            }
                        }

                        // get the future role of the current PO
                        if (currentPo.getSourceId().equals(memberToImport.getSourceId())
                                && currentPo.getSource().equals(memberToImport.getSource())
                                && !rolesToImport.contains(poRoleId)) {
                            roleUsedInTransfert = rolesToImport;
                        }

                        if (rolesToImport.contains(poRoleId)) {
                            futurePO = memberToImport;
                        }
                    }

                    // transfer the ownership
                    if (futurePO != null
                            && !(currentPo.getSource().equals(futurePO.getSource())
                            && currentPo.getSourceId().equals(futurePO.getSourceId()))) {
                        try {
                            UserEntity userEntity = userService.findBySource(futurePO.getSource(), futurePO.getSourceId(), false);
                            List<RoleEntity> roleEntity = null;
                            if (roleUsedInTransfert != null && !roleUsedInTransfert.isEmpty()) {
                                roleEntity = roleUsedInTransfert.stream().map(roleService::findById).collect(Collectors.toList());
                            }
                            membershipService.transferApiOwnership(
                                    createdOrUpdatedApiEntity.getId(),
                                    new MembershipService.MembershipMember(userEntity.getId(), null, MembershipMemberType.USER),
                                    roleEntity);
                        } catch (UserNotFoundException unfe) {

                        }
                    }
                }
            }

            //Pages
            final JsonNode pagesDefinition = jsonNode.path("pages");
            if (pagesDefinition != null && pagesDefinition.isArray()) {
                List<PageEntity> pagesList = objectMapper.readValue(pagesDefinition.toString(),
                        objectMapper.getTypeFactory().constructCollectionType(List.class, PageEntity.class));
                PageEntityTreeNode documentationTree = new PageEntityTreeNode(new PageEntity());
                documentationTree.appendListToTree(pagesList);
                createOrUpdateChildrenPages(createdOrUpdatedApiEntity.getId(), null, documentationTree.children);
            }

            //Plans
            final JsonNode plansDefinition = jsonNode.path("plans");
            if (plansDefinition != null && plansDefinition.isArray()) {
                for (JsonNode planNode : plansDefinition) {
                    PlanQuery query = new PlanQuery.Builder().
                            api(createdOrUpdatedApiEntity.getId()).
                            name(planNode.get("name").asText()).
                            security(PlanSecurityType.valueOf(planNode.get("security").asText().toUpperCase())).
                            build();
                    List<PlanEntity> planEntities = planService.search(query).stream()
                            .filter(planEntity -> !PlanStatus.CLOSED.equals(planEntity.getStatus()))
                            .collect(toList());
                    if (planEntities.isEmpty()) {
                        NewPlanEntity newPlanEntity = objectMapper.readValue(planNode.toString(), NewPlanEntity.class);
                        newPlanEntity.setApi(createdOrUpdatedApiEntity.getId());
                        planService.create(newPlanEntity);
                    } else if (planEntities.size() == 1) {
                        UpdatePlanEntity updatePlanEntity = objectMapper.readValue(planNode.toString(), UpdatePlanEntity.class);
                        updatePlanEntity.setId(planEntities.iterator().next().getId());
                        planService.update(updatePlanEntity);
                    } else {
                        LOGGER.error("Not able to identify the plan to update: {}. Too much plan with the same name", planNode.get("name").asText());
                        throw new TechnicalManagementException("Not able to identify the plan to update: " + planNode.get("name").asText() + ". Too much plan with the same name");
                    }
                }
            }
            // Metadata
            final JsonNode metadataDefinition = jsonNode.path("metadata");
            if (metadataDefinition != null && metadataDefinition.isArray()) {
                try {
                    for (JsonNode metadataNode : metadataDefinition) {
                        UpdateApiMetadataEntity updateApiMetadataEntity = objectMapper.readValue(metadataNode.toString(), UpdateApiMetadataEntity.class);
                        updateApiMetadataEntity.setApiId(createdOrUpdatedApiEntity.getId());
                        apiMetadataService.update(updateApiMetadataEntity);
                    }
                } catch (Exception ex) {
                    LOGGER.error("An error occurs while creating API metadata", ex);
                    throw new TechnicalManagementException("An error occurs while creating API Metadata", ex);
                }
            }
            return createdOrUpdatedApiEntity;
        } catch (final IOException e) {
            LOGGER.error("An error occurs while trying to JSON deserialize the API {}", apiDefinition, e);
            throw new TechnicalManagementException("An error occurs while trying to JSON deserialize the API definition.");
        }
    }

    private String fetchApiDefinitionContentFromURL(String apiDefinitionOrURL) {
        UrlSanitizerUtils.checkAllowed(apiDefinitionOrURL, importConfiguration.getImportWhitelist(), importConfiguration.isAllowImportFromPrivate());
        Buffer buffer = httpClientService.request(HttpMethod.GET, apiDefinitionOrURL, null, null, null);
        return buffer.toString();
    }

    class PageEntityTreeNode {

        PageEntity data;
        PageEntityTreeNode parent;
        List<PageEntityTreeNode> children;

        public PageEntityTreeNode(PageEntity data) {
            this.data = data;
            this.children = new LinkedList<>();
        }

        public PageEntityTreeNode addChild(PageEntity child) {
            PageEntityTreeNode childNode = new PageEntityTreeNode(child);
            childNode.parent = this;
            this.children.add(childNode);
            return childNode;
        }

        private PageEntityTreeNode findById(String id) {
            if (id.equals(data.getId())) {
                return this;
            }
            for (PageEntityTreeNode child : children) {
                PageEntityTreeNode result = child.findById(id);
                if (result != null) {
                    return result;
                }
            }
            return null;
        }

        public void appendListToTree(List<PageEntity> pagesList) {
            List<PageEntity> orphans = new ArrayList<>();
            for (PageEntity newPage : pagesList) {
                if (newPage.getParentId() == null || newPage.getParentId().isEmpty()) {
                    this.addChild(newPage);
                } else {
                    PageEntityTreeNode parentNode = this.findById(newPage.getParentId());
                    if (parentNode != null) {
                        parentNode.addChild(newPage);
                    } else {
                        orphans.add(newPage);
                    }
                }
            }
            if (!orphans.isEmpty() && orphans.size() < pagesList.size()) {
                appendListToTree(orphans);
            }
        }
    }

    private void createOrUpdateChildrenPages(String apiId, String parentId, List<PageEntityTreeNode> children) {
        for (final PageEntityTreeNode child : children) {
            PageEntity pageEntityToImport = child.data;
            pageEntityToImport.setParentId(parentId);

            PageQuery query = new PageQuery.Builder().
                    api(apiId).
                    name(pageEntityToImport.getName()).
                    type(PageType.valueOf(pageEntityToImport.getType())).
                    build();
            List<PageEntity> pageEntities = pageService.search(query);

            PageEntity createdOrUpdatedPage = null;
            if (pageEntities == null || pageEntities.isEmpty()) {
                NewPageEntity newPage = new NewPageEntity();
                newPage.setConfiguration(pageEntityToImport.getConfiguration());
                newPage.setContent(pageEntityToImport.getContent());
                newPage.setExcludedGroups(pageEntityToImport.getExcludedGroups());
                newPage.setHomepage(pageEntityToImport.isHomepage());
                newPage.setLastContributor(pageEntityToImport.getLastContributor());
                newPage.setName(pageEntityToImport.getName());
                newPage.setOrder(pageEntityToImport.getOrder());
                newPage.setParentId(pageEntityToImport.getParentId());
                newPage.setPublished(pageEntityToImport.isPublished());
                newPage.setSource(pageEntityToImport.getSource());
                newPage.setType(PageType.valueOf(pageEntityToImport.getType()));

                createdOrUpdatedPage = pageService.createPage(apiId, newPage);
            } else if (pageEntities.size() == 1) {
                UpdatePageEntity updatePageEntity = new UpdatePageEntity();
                updatePageEntity.setConfiguration(pageEntityToImport.getConfiguration());
                updatePageEntity.setContent(pageEntityToImport.getContent());
                updatePageEntity.setExcludedGroups(pageEntityToImport.getExcludedGroups());
                updatePageEntity.setHomepage(pageEntityToImport.isHomepage());
                updatePageEntity.setLastContributor(pageEntityToImport.getLastContributor());
                updatePageEntity.setName(pageEntityToImport.getName());
                updatePageEntity.setOrder(pageEntityToImport.getOrder());
                updatePageEntity.setParentId(pageEntityToImport.getParentId());
                updatePageEntity.setPublished(pageEntityToImport.isPublished());
                updatePageEntity.setSource(pageEntityToImport.getSource());

                createdOrUpdatedPage = pageService.update(pageEntities.get(0).getId(), updatePageEntity);
            } else {
                LOGGER.error("Not able to identify the page to update: {}. Too much page with the same name", pageEntityToImport.getName());
                throw new TechnicalManagementException("Not able to identify the page to update: " + pageEntityToImport.getName() + ". Too much page with the same name");
            }

            if (child.children != null && !child.children.isEmpty()) {
                this.createOrUpdateChildrenPages(apiId, createdOrUpdatedPage.getId(), child.children);
            }
        }
    }

    @Override
    public InlinePictureEntity getPicture(String apiId) {
        ApiEntity apiEntity = findById(apiId);
        InlinePictureEntity imageEntity = new InlinePictureEntity();
        if (apiEntity.getPicture() != null) {
            String[] parts = apiEntity.getPicture().split(";", 2);
            imageEntity.setType(parts[0].split(":")[1]);
            String base64Content = apiEntity.getPicture().split(",", 2)[1];
            imageEntity.setContent(DatatypeConverter.parseBase64Binary(base64Content));
        }

        return imageEntity;
    }

    @Override
    public void deleteCategoryFromAPIs(final String categoryId) {
        findAll().forEach(api -> {
            if (api.getCategories() != null && api.getCategories().contains(categoryId)) {
                removeCategory(api.getId(), categoryId);
            }
        });
    }

    private void removeCategory(String apiId, String categoryId) throws TechnicalManagementException {
        try {
            Optional<Api> optApi = apiRepository.findById(apiId);
            if (optApi.isPresent()) {
                Api api = optApi.get();
                Api previousApi = new Api(api);
                api.getCategories().remove(categoryId);
                api.setUpdatedAt(new Date());
                apiRepository.update(api);
                // Audit
                auditService.createApiAuditLog(
                        apiId,
                        Collections.emptyMap(),
                        API_UPDATED,
                        api.getUpdatedAt(),
                        previousApi,
                        api);
            } else {
                throw new ApiNotFoundException(apiId);
            }
        } catch (Exception ex) {
            LOGGER.error("An error occurs while removing category from API: {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while removing category from API: " + apiId, ex);
        }
    }

    @Override
    public void deleteTagFromAPIs(final String tagId) {
        findAll().forEach(api -> {
            if (api.getTags() != null && api.getTags().contains(tagId)) {
                removeTag(api.getId(), tagId);
            }
        });
    }

    @Override
    public ApiModelEntity findByIdForTemplates(String apiId, boolean decodeTemplate) {
        final ApiEntity apiEntity = findById(apiId);

        final ApiModelEntity apiModelEntity = new ApiModelEntity();

        apiModelEntity.setId(apiEntity.getId());
        apiModelEntity.setName(apiEntity.getName());
        apiModelEntity.setDescription(apiEntity.getDescription());
        apiModelEntity.setCreatedAt(apiEntity.getCreatedAt());
        apiModelEntity.setDeployedAt(apiEntity.getDeployedAt());
        apiModelEntity.setUpdatedAt(apiEntity.getUpdatedAt());
        apiModelEntity.setGroups(apiEntity.getGroups());
        apiModelEntity.setVisibility(apiEntity.getVisibility());
        apiModelEntity.setCategories(apiEntity.getCategories());
        apiModelEntity.setVersion(apiEntity.getVersion());
        apiModelEntity.setState(apiEntity.getState());
        apiModelEntity.setTags(apiEntity.getTags());
        apiModelEntity.setServices(apiEntity.getServices());
        apiModelEntity.setPaths(apiEntity.getPaths());
        apiModelEntity.setPicture(apiEntity.getPicture());
        apiModelEntity.setPrimaryOwner(apiEntity.getPrimaryOwner());
        apiModelEntity.setProperties(apiEntity.getProperties());
        apiModelEntity.setProxy(convert(apiEntity.getProxy()));
        apiModelEntity.setLifecycleState(apiEntity.getLifecycleState());

        final List<ApiMetadataEntity> metadataList = apiMetadataService.findAllByApi(apiId);

        if (metadataList != null) {
            final Map<String, String> mapMetadata = new HashMap<>(metadataList.size());
            metadataList.forEach(metadata -> mapMetadata.put(metadata.getKey(),
                    metadata.getValue() == null ? metadata.getDefaultValue() : metadata.getValue()));
            apiModelEntity.setMetadata(mapMetadata);
            if (decodeTemplate) {
                try {
                    Template apiMetadataTemplate = new Template(apiModelEntity.getId(), new StringReader(mapMetadata.toString()), freemarkerConfiguration);
                    String decodedValue = FreeMarkerTemplateUtils.processTemplateIntoString(apiMetadataTemplate, Collections.singletonMap("api", apiModelEntity));
                    Map<String, String> metadataDecoded = Arrays
                            .stream(decodedValue.substring(1, decodedValue.length() - 1).split(", "))
                            .map(entry -> entry.split("="))
                            .collect(Collectors.toMap(entry -> entry[0], entry -> entry.length > 1 ? entry[1] : ""));
                    apiModelEntity.setMetadata(metadataDecoded);
                } catch (Exception ex) {
                    throw new TechnicalManagementException("An error occurs while evaluating API metadata", ex);
                }
            }
        }
        return apiModelEntity;
    }

    @Override
    public boolean exists(final String apiId) {
        try {
            return apiRepository.findById(apiId).isPresent();
        } catch (final TechnicalException te) {
            final String msg = "An error occurs while checking if the API exists: " + apiId;
            LOGGER.error(msg, te);
            throw new TechnicalManagementException(msg, te);
        }
    }

    @Override
    public ApiEntity importPathMappingsFromPage(final ApiEntity apiEntity, final String page) {
        final PageEntity pageEntity = pageService.findById(page);
        if (SWAGGER.name().equals(pageEntity.getType())) {
            final ImportSwaggerDescriptorEntity importSwaggerDescriptorEntity = new ImportSwaggerDescriptorEntity();
            importSwaggerDescriptorEntity.setPayload(pageEntity.getContent());
            final SwaggerApiEntity swaggerApiEntity = swaggerService.createAPI(importSwaggerDescriptorEntity);
            apiEntity.getPathMappings().addAll(swaggerApiEntity.getPathMappings());
        }

        return update(apiEntity.getId(), ApiService.convert(apiEntity));
    }

    public Collection<ApiEntity> search(final ApiQuery query) {
        try {
            LOGGER.debug("Search APIs by {}", query);
            return filterApiByQuery(this.convert(apiRepository.search(queryToCriteria(query).build())).stream(), query)
                    .collect(toList());
        } catch (TechnicalException ex) {
            final String errorMessage = "An error occurs while trying to search for APIs: " + query;
            LOGGER.error(errorMessage, ex);
            throw new TechnicalManagementException(errorMessage, ex);
        }
    }

    @Override
    public Collection<ApiEntity> search(String query, Map<String, Object> filters) {
        Query<ApiEntity> apiQuery = QueryBuilder.create(ApiEntity.class)
                .setQuery(query)
                .setFilters(filters)
                .build();

        SearchResult matchApis = searchEngineService.search(apiQuery);
        return matchApis.getDocuments().stream().map(this::findById).collect(toList());
    }

    @Override
    public List<ApiHeaderEntity> getPortalHeaders(String apiId) {
        List<ApiHeaderEntity> entities = apiHeaderService.findAll();
        ApiModelEntity apiEntity = this.findByIdForTemplates(apiId);
        Map<String, Object> model = new HashMap<>();
        model.put("api", apiEntity);
        entities.forEach(entity -> {
            if (entity.getValue().contains("${")) {
                try {
                    Template template = new Template(entity.getId() + entity.getUpdatedAt().toString(), entity.getValue(), freemarkerConfiguration);
                    entity.setValue(FreeMarkerTemplateUtils.processTemplateIntoString(template, model));
                } catch (IOException | TemplateException e) {
                    LOGGER.error("Unable to apply templating on api headers ", e);
                    throw new TechnicalManagementException("Unable to apply templating on api headers", e);
                }
            }
        });
        return entities.stream()
                .filter(apiHeaderEntity -> apiHeaderEntity.getValue() != null && !apiHeaderEntity.getValue().isEmpty())
                .collect(Collectors.toList());
    }

    @Override
    public ApiEntity askForReview(final String apiId, final String userId, final ReviewEntity reviewEntity) {
        LOGGER.debug("Ask for review API {}", apiId);
        return updateWorkflowReview(apiId, userId, ApiHook.ASK_FOR_REVIEW, WorkflowState.IN_REVIEW, reviewEntity.getMessage());
    }

    @Override
    public ApiEntity acceptReview(final String apiId, final String userId, final ReviewEntity reviewEntity) {
        LOGGER.debug("Accept review API {}", apiId);
        return updateWorkflowReview(apiId, userId, ApiHook.REVIEW_OK, WorkflowState.REVIEW_OK, reviewEntity.getMessage());
    }

    @Override
    public ApiEntity rejectReview(final String apiId, final String userId, final ReviewEntity reviewEntity) {
        LOGGER.debug("Reject review API {}", apiId);
        return updateWorkflowReview(apiId, userId, ApiHook.REQUEST_FOR_CHANGES, WorkflowState.REQUEST_FOR_CHANGES, reviewEntity.getMessage());
    }

    @Override
    public ApiEntity duplicate(final String apiId, final DuplicateApiEntity duplicateApiEntity) {
        LOGGER.debug("Duplicate API {}", apiId);
        final ApiEntity apiEntity = findById(apiId);

        final UpdateApiEntity newApiEntity = convert(apiEntity);
        final Proxy proxy = apiEntity.getProxy();
        proxy.setVirtualHosts(singletonList(new VirtualHost(duplicateApiEntity.getContextPath())));
        newApiEntity.setProxy(proxy);
        newApiEntity.setVersion(duplicateApiEntity.getVersion() == null ? apiEntity.getVersion() : duplicateApiEntity.getVersion());

        if (duplicateApiEntity.getFilteredFields().contains("groups")) {
            newApiEntity.setGroups(null);
        } else {
            newApiEntity.setGroups(apiEntity.getGroups());
        }
        final ApiEntity duplicatedApi = create0(newApiEntity, getAuthenticatedUsername(), false);

        if (!duplicateApiEntity.getFilteredFields().contains("members")) {
            final Set<MembershipEntity> membershipsToDuplicate =
                    membershipService.getMembershipsByReference(io.gravitee.rest.api.model.MembershipReferenceType.API, apiId);
            Optional<RoleEntity> optPrimaryOwnerRole = roleService.findByScopeAndName(RoleScope.API, SystemRole.PRIMARY_OWNER.name());
            if (optPrimaryOwnerRole.isPresent()) {
                String primaryOwnerRoleId = optPrimaryOwnerRole.get().getId();
                membershipsToDuplicate.forEach(membership -> {
                    String roleId = membership.getRoleId();
                    if (!primaryOwnerRoleId.equals(roleId)) {
                        membershipService.addRoleToMemberOnReference(io.gravitee.rest.api.model.MembershipReferenceType.API, duplicatedApi.getId(), membership.getMemberType(), membership.getMemberId(), roleId);
                    }
                });
            }
        }

        if (!duplicateApiEntity.getFilteredFields().contains("pages")) {
            final List<PageEntity> pages = pageService.search(new PageQuery.Builder().api(apiId).build(), true);
            pages.forEach(page -> pageService.create(duplicatedApi.getId(), page));
        }

        if (!duplicateApiEntity.getFilteredFields().contains("plans")) {
            final Set<PlanEntity> plans = planService.findByApi(apiId);
            plans.forEach(plan -> planService.create(duplicatedApi.getId(), plan));
        }

        return duplicatedApi;
    }

    private UpdateApiEntity convert(final ApiEntity apiEntity) {
        final UpdateApiEntity updateApiEntity = new UpdateApiEntity();
        updateApiEntity.setDescription(apiEntity.getDescription());
        updateApiEntity.setName(apiEntity.getName());
        updateApiEntity.setVersion(apiEntity.getVersion());
        updateApiEntity.setGroups(apiEntity.getGroups());
        updateApiEntity.setLabels(apiEntity.getLabels());
        updateApiEntity.setLifecycleState(apiEntity.getLifecycleState());
        updateApiEntity.setPicture(apiEntity.getPicture());
        updateApiEntity.setProperties(apiEntity.getProperties());
        updateApiEntity.setProxy(apiEntity.getProxy());
        updateApiEntity.setResources(apiEntity.getResources());
        updateApiEntity.setResponseTemplates(apiEntity.getResponseTemplates());
        updateApiEntity.setServices(apiEntity.getServices());
        updateApiEntity.setTags(apiEntity.getTags());
        updateApiEntity.setCategories(apiEntity.getCategories());
        updateApiEntity.setVisibility(apiEntity.getVisibility());
        updateApiEntity.setPaths(apiEntity.getPaths());
        updateApiEntity.setPathMappings(apiEntity.getPathMappings());
        return updateApiEntity;
    }

    private ApiEntity updateWorkflowReview(final String apiId, final String userId, final ApiHook hook,
                                           final WorkflowState workflowState, final String workflowMessage) {
        workflowService.create(WorkflowReferenceType.API, apiId, REVIEW, userId, workflowState, workflowMessage);
        final ApiEntity apiEntity = findById(apiId);
        apiEntity.setWorkflowState(workflowState);

        notifierService.trigger(hook, apiId,
                new NotificationParamsBuilder()
                        .api(apiEntity)
                        .user(userService.findById(userId))
                        .build());
        return apiEntity;
    }

    private ApiCriteria.Builder queryToCriteria(ApiQuery query) {
        final ApiCriteria.Builder builder = new ApiCriteria.Builder().environmentId(GraviteeContext.getCurrentEnvironment());
        if (query == null) {
            return builder;
        }
        builder.label(query.getLabel())
                .name(query.getName())
                .version(query.getVersion());

        if (!isBlank(query.getCategory())) {
            builder.category(categoryService.findById(query.getCategory()).getId());
        }
        if (query.getGroups() != null && !query.getGroups().isEmpty()) {
            builder.groups(query.getGroups().toArray(new String[0]));
        }
        if (!isBlank(query.getState())) {
            builder.state(LifecycleState.valueOf(query.getState()));
        }
        if (query.getVisibility() != null) {
            builder.visibility(Visibility.valueOf(query.getVisibility().name()));
        }
        if (query.getLifecycleStates() != null) {
            builder.lifecycleStates(query.getLifecycleStates().stream()
                    .map(apiLifecycleState -> ApiLifecycleState.valueOf(apiLifecycleState.name()))
                    .collect(toList()));
        }

        return builder;
    }

    private void removeTag(String apiId, String tagId) throws TechnicalManagementException {
        try {
            ApiEntity apiEntity = this.findById(apiId);
            apiEntity.getTags().remove(tagId);
            update(apiId, ApiService.convert(apiEntity));
        } catch (Exception ex) {
            LOGGER.error("An error occurs while removing tag from API: {}", apiId, ex);
            throw new TechnicalManagementException("An error occurs while removing tag from API: " + apiId, ex);
        }
    }

    private ApiEntity updateLifecycle(String apiId, LifecycleState lifecycleState, String username) throws TechnicalException {
        Optional<Api> optApi = apiRepository.findById(apiId);
        if (optApi.isPresent()) {
            Api api = optApi.get();
            Api previousApi = new Api(api);
            api.setUpdatedAt(new Date());
            api.setLifecycleState(lifecycleState);
            ApiEntity apiEntity = convert(apiRepository.update(api), getPrimaryOwner(api), null);
            // Audit
            auditService.createApiAuditLog(
                    apiId,
                    Collections.emptyMap(),
                    API_UPDATED,
                    api.getUpdatedAt(),
                    previousApi,
                    api);

            EventType eventType = null;
            switch (lifecycleState) {
                case STARTED:
                    eventType = EventType.START_API;
                    break;
                case STOPPED:
                    eventType = EventType.STOP_API;
                    break;
                default:
                    break;
            }
            final ApiEntity deployedApi = deployLastPublishedAPI(apiId, username, eventType);
            if (deployedApi != null) {
                return deployedApi;
            }
            return apiEntity;
        } else {
            throw new ApiNotFoundException(apiId);
        }
    }

    private void auditApiLogging(Api apiToUpdate, Api apiUpdated) {
        try {
            // get old logging configuration
            io.gravitee.definition.model.Api apiToUpdateDefinition = objectMapper.readValue(apiToUpdate.getDefinition(), io.gravitee.definition.model.Api.class);
            Logging loggingToUpdate = apiToUpdateDefinition.getProxy().getLogging();

            // get new logging configuration
            io.gravitee.definition.model.Api apiUpdatedDefinition = objectMapper.readValue(apiUpdated.getDefinition(), io.gravitee.definition.model.Api.class);
            Logging loggingUpdated = apiUpdatedDefinition.getProxy().getLogging();

            // no changes for logging configuration, continue
            if (loggingToUpdate == loggingUpdated ||
                    (loggingToUpdate != null && loggingUpdated != null
                            && Objects.equals(loggingToUpdate.getMode(), loggingUpdated.getMode())
                            && Objects.equals(loggingToUpdate.getCondition(), loggingUpdated.getCondition()))) {
                return;
            }

            // determine the audit event type
            Api.AuditEvent auditEvent;
            if ((loggingToUpdate == null || loggingToUpdate.getMode().equals(LoggingMode.NONE)) && (!loggingUpdated.getMode().equals(LoggingMode.NONE))) {
                auditEvent = Api.AuditEvent.API_LOGGING_ENABLED;
            } else if ((loggingToUpdate != null && !loggingToUpdate.getMode().equals(LoggingMode.NONE)) && (loggingUpdated.getMode().equals(LoggingMode.NONE))) {
                auditEvent = Api.AuditEvent.API_LOGGING_DISABLED;
            } else {
                auditEvent = Api.AuditEvent.API_LOGGING_UPDATED;
            }

            // Audit
            auditService.createApiAuditLog(
                    apiUpdated.getId(),
                    Collections.emptyMap(),
                    auditEvent,
                    new Date(),
                    loggingToUpdate,
                    loggingUpdated);
        } catch (Exception ex) {
            LOGGER.error("An error occurs while auditing API logging configuration for API: {}", apiUpdated.getId(), ex);
            throw new TechnicalManagementException("An error occurs while auditing API logging configuration for API: " + apiUpdated.getId(), ex);
        }
    }

    private Set<ApiEntity> convert(final List<Api> apis) throws TechnicalException {
        if (apis == null || apis.isEmpty()) {
            return Collections.emptySet();
        }
        Optional<RoleEntity> optPrimaryOwnerRole = roleService.findByScopeAndName(RoleScope.API, SystemRole.PRIMARY_OWNER.name());
        if (!optPrimaryOwnerRole.isPresent()) {
            throw new RoleNotFoundException("API_PRIMARY_OWNER");
        }
        //find primary owners usernames of each apis
        final List<String> apiIds = apis.stream().map(Api::getId).collect(toList());

        Set<MemberEntity> memberships = membershipService.getMembersByReferencesAndRole(MembershipReferenceType.API, apiIds, optPrimaryOwnerRole.get().getId());
        int poMissing = apis.size() - memberships.size();
        Stream<Api> streamApis = apis.stream();
        if (poMissing > 0) {
            Set<String> apiMembershipsIds = memberships.stream().map(MemberEntity::getReferenceId).collect(toSet());

            apiIds.removeAll(apiMembershipsIds);
            Optional<String> optionalApisAsString = apiIds.stream().reduce((a, b) -> a + " / " + b);
            String apisAsString = "?";
            if (optionalApisAsString.isPresent()) {
                apisAsString = optionalApisAsString.get();
            }
            LOGGER.error("{} apis has no identified primary owners in this list {}.", poMissing, apisAsString);
            streamApis = streamApis.filter(api -> !apiIds.contains(api.getId()));
        }

        Map<String, String> apiToUser = new HashMap<>(memberships.size());
        memberships.forEach(membership -> apiToUser.put(membership.getReferenceId(), membership.getId()));

        Map<String, UserEntity> userIdToUserEntity = new HashMap<>(memberships.size());
        userService.findByIds(memberships.stream().map(MemberEntity::getId).collect(toList()))
                .forEach(userEntity -> userIdToUserEntity.put(userEntity.getId(), userEntity));

        final List<CategoryEntity> categories = categoryService.findAll();
        return streamApis
                .map(publicApi -> this.convert(publicApi, userIdToUserEntity.get(apiToUser.get(publicApi.getId())), categories))
                .collect(toSet());
    }

    private ApiEntity convert(Api api) {
        return convert(api, null, null);
    }

    private ApiEntity convert(Api api, UserEntity primaryOwner, List<CategoryEntity> categories) {
        ApiEntity apiEntity = new ApiEntity();

        apiEntity.setId(api.getId());
        apiEntity.setName(api.getName());
        apiEntity.setDeployedAt(api.getDeployedAt());
        apiEntity.setCreatedAt(api.getCreatedAt());
        apiEntity.setGroups(api.getGroups());

        if (api.getDefinition() != null) {
            try {
                io.gravitee.definition.model.Api apiDefinition = objectMapper.readValue(api.getDefinition(),
                        io.gravitee.definition.model.Api.class);

                apiEntity.setProxy(apiDefinition.getProxy());
                apiEntity.setPaths(apiDefinition.getPaths());
                apiEntity.setServices(apiDefinition.getServices());
                apiEntity.setResources(apiDefinition.getResources());
                apiEntity.setProperties(apiDefinition.getProperties());
                apiEntity.setTags(apiDefinition.getTags());

                // Issue https://github.com/gravitee-io/issues/issues/3356
                if (apiDefinition.getProxy().getVirtualHosts() != null &&
                        !apiDefinition.getProxy().getVirtualHosts().isEmpty()) {
                    apiEntity.setContextPath(apiDefinition.getProxy().getVirtualHosts().get(0).getPath());
                }

                if (apiDefinition.getPathMappings() != null) {
                    apiEntity.setPathMappings(new HashSet<>(apiDefinition.getPathMappings().keySet()));
                }
                apiEntity.setResponseTemplates(apiDefinition.getResponseTemplates());
            } catch (IOException ioe) {
                LOGGER.error("Unexpected error while generating API definition", ioe);
            }
        }
        apiEntity.setUpdatedAt(api.getUpdatedAt());
        apiEntity.setVersion(api.getVersion());
        apiEntity.setDescription(api.getDescription());
        apiEntity.setPicture(api.getPicture());
        apiEntity.setLabels(api.getLabels());

        final Set<String> apiCategories = api.getCategories();
        if (apiCategories != null) {
            if (categories == null) {
                categories = categoryService.findAll();
            }
            final Set<String> newApiCategories = new HashSet<>(apiCategories.size());
            for (final String apiView : apiCategories) {
                final Optional<CategoryEntity> optionalView = categories.stream().filter(c -> apiView.equals(c.getId())).findAny();
                optionalView.ifPresent(category -> newApiCategories.add(category.getKey()));
            }
            apiEntity.setCategories(newApiCategories);
        }
        final LifecycleState state = api.getLifecycleState();
        if (state != null) {
            apiEntity.setState(Lifecycle.State.valueOf(state.name()));
        }
        if (api.getVisibility() != null) {
            apiEntity.setVisibility(io.gravitee.rest.api.model.Visibility.valueOf(api.getVisibility().toString()));
        }

        if (primaryOwner != null) {
            apiEntity.setPrimaryOwner(new PrimaryOwnerEntity(primaryOwner));
        }
        final ApiLifecycleState lifecycleState = api.getApiLifecycleState();
        if (lifecycleState != null) {
            apiEntity.setLifecycleState(io.gravitee.rest.api.model.api.ApiLifecycleState.valueOf(lifecycleState.name()));
        }

        if (parameterService.findAsBoolean(Key.API_REVIEW_ENABLED)) {
            final List<Workflow> workflows = workflowService.findByReferenceAndType(API, api.getId(), REVIEW);
            if (workflows != null && !workflows.isEmpty()) {
                apiEntity.setWorkflowState(WorkflowState.valueOf(workflows.get(0).getState()));
            }
        }

        return apiEntity;
    }

    private Api convert(String apiId, UpdateApiEntity updateApiEntity) {
        Api api = new Api();

        if (updateApiEntity.getVisibility() != null) {
            api.setVisibility(Visibility.valueOf(updateApiEntity.getVisibility().toString()));
        }

        api.setVersion(updateApiEntity.getVersion().trim());
        api.setName(updateApiEntity.getName().trim());
        api.setDescription(updateApiEntity.getDescription().trim());
        api.setPicture(updateApiEntity.getPicture());

        final Set<String> apiCategories = updateApiEntity.getCategories();
        if (apiCategories != null) {
            final List<CategoryEntity> categories = categoryService.findAll();
            final Set<String> newApiCategories = new HashSet<>(apiCategories.size());
            for (final String apiCategory : apiCategories) {
                final Optional<CategoryEntity> optionalCategory =
                        categories.stream().filter(c -> apiCategory.equals(c.getKey()) || apiCategory.equals(c.getId())).findAny();
                optionalCategory.ifPresent(category -> newApiCategories.add(category.getId()));
            }
            api.setCategories(newApiCategories);
        }

        if (updateApiEntity.getLabels() != null) {
            api.setLabels(new ArrayList(new LinkedHashSet<>(updateApiEntity.getLabels())));
        }

        api.setGroups(updateApiEntity.getGroups());

        try {
            io.gravitee.definition.model.Api apiDefinition = new io.gravitee.definition.model.Api();
            apiDefinition.setId(apiId);
            apiDefinition.setName(updateApiEntity.getName());
            apiDefinition.setVersion(updateApiEntity.getVersion());
            apiDefinition.setProxy(updateApiEntity.getProxy());

            apiDefinition.setPaths(updateApiEntity.getPaths());
            if (updateApiEntity.getPathMappings() != null) {
                apiDefinition.setPathMappings(updateApiEntity.getPathMappings().stream()
                        .collect(toMap(pathMapping -> pathMapping, pathMapping -> Pattern.compile(""))));
            }

            apiDefinition.setServices(updateApiEntity.getServices());
            apiDefinition.setResources(updateApiEntity.getResources());
            apiDefinition.setProperties(updateApiEntity.getProperties());
            apiDefinition.setTags(updateApiEntity.getTags());

            apiDefinition.setResponseTemplates(updateApiEntity.getResponseTemplates());

            String definition = objectMapper.writeValueAsString(apiDefinition);
            api.setDefinition(definition);
            if (updateApiEntity.getLifecycleState() != null) {
                api.setApiLifecycleState(ApiLifecycleState.valueOf(updateApiEntity.getLifecycleState().name()));
            }
            return api;
        } catch (JsonProcessingException jse) {
            LOGGER.error("Unexpected error while generating API definition", jse);
        }

        return null;
    }

    private LifecycleState convert(EventType eventType) {
        LifecycleState lifecycleState;
        switch (eventType) {
            case START_API:
                lifecycleState = LifecycleState.STARTED;
                break;
            case STOP_API:
                lifecycleState = LifecycleState.STOPPED;
                break;
            default:
                throw new IllegalArgumentException("Unknown EventType " + eventType.toString() + " to convert EventType into Lifecycle");
        }
        return lifecycleState;
    }

    private static class MemberToImport {
        private String source;
        private String sourceId;
        private List<String> roles; // After v3
        private String role; // Before v3

        public MemberToImport() {
        }

        public MemberToImport(String source, String sourceId, List<String> roles, String role) {
            this.source = source;
            this.sourceId = sourceId;
            this.roles = roles;
            this.role = role;
        }

        public String getSource() {
            return source;
        }

        public void setSource(String source) {
            this.source = source;
        }

        public String getSourceId() {
            return sourceId;
        }

        public void setSourceId(String sourceId) {
            this.sourceId = sourceId;
        }

        public List<String> getRoles() {
            return roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = roles;
        }

        public String getRole() {
            return role;
        }

        public void setRole(String role) {
            this.role = role;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            MemberToImport that = (MemberToImport) o;

            return source.equals(that.source) && sourceId.equals(that.sourceId);
        }

        @Override
        public int hashCode() {
            int result = source.hashCode();
            result = 31 * result + sourceId.hashCode();
            return result;
        }
    }

    private ProxyModelEntity convert(Proxy proxy) {
        ProxyModelEntity proxyModelEntity = new ProxyModelEntity();

        proxyModelEntity.setCors(proxy.getCors());
        proxyModelEntity.setFailover(proxy.getFailover());
        proxyModelEntity.setGroups(proxy.getGroups());
        proxyModelEntity.setLogging(proxy.getLogging());
        proxyModelEntity.setPreserveHost(proxy.isPreserveHost());
        proxyModelEntity.setStripContextPath(proxy.isStripContextPath());
        proxyModelEntity.setVirtualHosts(proxy.getVirtualHosts());

        //add a default context-path to preserve compatibility on old templates
        if (proxy.getVirtualHosts() != null && !proxy.getVirtualHosts().isEmpty()) {
            proxyModelEntity.setContextPath(proxy.getVirtualHosts().get(0).getPath());
        }

        return proxyModelEntity;
    }
}