/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.bpm;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.jboss.pnc.bpm.task.BpmBuildTask;
import org.jboss.pnc.common.Configuration;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.moduleconfig.BpmModuleConfig;
import org.jboss.pnc.common.json.moduleprovider.PncConfigProvider;
import org.jboss.pnc.common.util.StringUtils;
import org.jboss.pnc.rest.restmodel.bpm.BpmNotificationRest;
import org.jboss.pnc.spi.exception.CoreException;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.manager.RuntimeEngine;
import org.kie.api.runtime.process.ProcessInstance;
import org.kie.services.client.api.RemoteRuntimeEngineFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static java.lang.Integer.MAX_VALUE;
import static org.jboss.pnc.bpm.BpmEventType.nullableValueOf;
import static org.kie.api.runtime.process.ProcessInstance.STATE_ABORTED;
import static org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED;

/**
 * Responsible for starting, keeping track of, and notifying BPM tasks.
 * Thread safe.
 *
 * @author Jakub Senko
 */
@ApplicationScoped
public class BpmManager {

    private static final Logger log = LoggerFactory.getLogger(BpmManager.class);

    private static final int AUTHENTICATION_TIMEOUT_S = 2 * 60;

    private Configuration configuration;
    private BpmModuleConfig bpmConfig;
    private AtomicInteger nextTaskId = new AtomicInteger(1);
    private Map<Integer, BpmTask> tasks = new ConcurrentHashMap<>();
    private KieSession session;

    private static final String SIGNAL_CANCEL = "CANCELLED";

    private Set<Consumer<BpmTask>> newTaskAddedSubscribes = new HashSet<>();

    @Deprecated
    public BpmManager() { //CDI workaround
    }

    @Inject
    public BpmManager(Configuration configuration) {
        this.configuration = configuration;
    }

    @PostConstruct
    public void init() throws CoreException {
        try {
            bpmConfig = configuration.getModuleConfig(new PncConfigProvider<>(BpmModuleConfig.class));
        } catch (ConfigurationParseException e) {
            throw new CoreException("BPM manager could not get its configuration.", e);
        }
        session = initKieSession();
    }

    protected KieSession initKieSession() throws CoreException {
        RuntimeEngine restSessionFactory;
        try {
            restSessionFactory = RemoteRuntimeEngineFactory.newRestBuilder()
                    .addDeploymentId(bpmConfig.getDeploymentId())
                    .addUrl(new URL(bpmConfig.getBpmInstanceUrl()))
                    .addUserName(bpmConfig.getUsername())
                    .addPassword(bpmConfig.getPassword())
                    .addTimeout(AUTHENTICATION_TIMEOUT_S)
                    .build();
        } catch (Exception e) {
            throw new CoreException("Could not initialize connection to BPM server at '" +
                    bpmConfig.getBpmInstanceUrl() + "' check that the URL is correct.", e);
        }

        return restSessionFactory.getKieSession();
    }

    @PreDestroy
    private void dispose() {
        session.dispose();
    }

    private int getNextTaskId() {
        return nextTaskId.getAndIncrement();
    }

    public boolean startTask(BpmTask task) throws CoreException {
        try {
            task.setTaskId(getNextTaskId());
            task.setBpmConfig(bpmConfig);

            ProcessInstance processInstance = session.startProcess(task.getProcessId(),
                    task.getExtendedProcessParameters());
            if (processInstance == null) {
                log.warn("Failed to create new process instance.");
                return false;
            }

            task.setProcessInstanceId(processInstance.getId());
            task.setProcessName(processInstance.getProcessId());
            tasks.put(task.getTaskId(), task);

            log.debug("Notifying new task added {}.", task.getTaskId());
            notifyNewTaskAdded(task);

            log.debug("Created new process linked to task: {}", task);
            return true;

        } catch (Exception e) {
            throw new CoreException("Could not start BPM task '" + task + "'.", e);
        }
    }

    private void notifyNewTaskAdded(BpmTask task) {
        log.debug("Notifying {} subscribers for new task {}.", newTaskAddedSubscribes.size(), task.getTaskId());
        newTaskAddedSubscribes.forEach(subscriber -> {
            log.debug("Notifying subscriber {}.", subscriber.getClass());
            subscriber.accept(task);
        });
    }

    public boolean subscribeToNewTasks(Consumer<BpmTask> consumer) {
        log.debug("Subscribing new tasks consumer {}.", consumer);
        return newTaskAddedSubscribes.add(consumer);
    }

    public boolean unSubscribeFromNewTasks(Consumer<BpmTask> consumer) {
        return newTaskAddedSubscribes.remove(consumer);
    }

    public boolean cancelTask(BpmTask bpmTask) {
        String cancelEndpointUrl = StringUtils.stripEndingSlash(bpmConfig.getBpmInstanceUrl()) + "/nclcancelhandler";

        URI uri;
        try {
            URIBuilder uriBuilder = new URIBuilder(cancelEndpointUrl);
            uriBuilder.addParameter("processInstanceId", bpmTask.getProcessInstanceId().toString());
            uri = uriBuilder.build();
        } catch (URISyntaxException e) {
            log.error("Unable to cancel process id: " + bpmTask.getProcessId(), e);
            return false;
        }

        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            log.debug("Triggering the cancellation using url: {}", uri.toString());
            HttpGet httpget = new HttpGet(uri);
            httpget.setConfig(RequestConfig.custom()
                    .setConnectionRequestTimeout(bpmConfig.getCancelConnectionRequestTimeout())
                    .setConnectTimeout(bpmConfig.getCancelConnectTimeout())
                    .setSocketTimeout(bpmConfig.getCancelSocketTimeout())
                    .build());
            CloseableHttpResponse httpResponse = httpClient.execute(httpget);
            int statusCode = httpResponse.getStatusLine().getStatusCode();
            httpResponse.close();
            log.info("Cancel request for bpmTask.id: {} and processInstanceId: {} completed with status: {}.",
                    bpmTask.getTaskId(), bpmTask.getProcessInstanceId(), statusCode);
            return statusCode == 200;
        } catch (IOException e) {
            log.error("Unable to cancel process id: " + bpmTask.getProcessId(), e);
            return false;
        }
    }

    public void notify(int taskId, BpmNotificationRest notification) { //TODO do not use RestModel down here.
        log.debug("Will process notification for taskId: {}; BpmNotificationRest: {}.", taskId, notification.toString());
        Optional<BpmTask> maybeTask = getTaskById(taskId);
        if (!maybeTask.isPresent()) {
            log.error("Cannot notify tasks with id: [{}]. Ids of tasks in progress: {}", taskId, tasks.keySet());
        }
        maybeTask.ifPresent(task -> {
            BpmEventType bpmEventType = nullableValueOf(notification.getEventType());
            if (bpmEventType != null && bpmEventType.getType().isInstance(notification)) {
                log.debug("Notifying task: {}, eventType: {}, notification: {}.", task.getTaskId(), bpmEventType, notification.toString());
                task.notify(bpmEventType, notification);
            }
        });

        log.info("finished notifying for taskId: {}", taskId);
    }

    /**
     * Regularly cleans finished BPM tasks asynchronously
     * Immediate cleanup is not usable because of NCL-2300
     */
    public void cleanup() { //TODO remove tasks immediately after the completion, see: BuildTaskEndpoint.buildTaskCompleted
        log.debug("Bpm manager tasks cleanup started");

        if (session == null) {
            log.error("Kie session not available.");
        }

        Map<Integer, BpmTask> clonedTaskMap = new HashMap<>(this.tasks);

        Set<Integer> toBeRemoved = clonedTaskMap.values().stream()
                .filter(bpmTask -> {
                    if (bpmTask == null) {
                        log.warn("Listing invalid entry for removal from the tasks list.");
                        return true;
                    }
                    log.debug("Attempting to fetch process instance for bpmTask: {}.", bpmTask.getTaskId());
                    Long processInstanceId = bpmTask.getProcessInstanceId();
                    ProcessInstance processInstance = session.getProcessInstance(processInstanceId);
                    log.debug("fetched: {}", processInstance);
                    if (processInstance == null) // instance has been terminated from outside
                        return true;
                    int state = processInstance.getState();
                    return state == STATE_COMPLETED || state == STATE_ABORTED;
                })
                .map(BpmTask::getTaskId)
                .collect(Collectors.toSet());
        toBeRemoved.forEach(id -> {
            BpmTask removed = tasks.remove(id);
            if (removed != null) {
                log.debug("Removed bpmTask.id: {}.", removed.getTaskId());
            } else {
                log.warn("Unable to remove bpmTask.id: {}.", id);
            }
        });

        log.debug("Bpm manager tasks cleanup finished");
    }

    /**
     * This method solves backwards compatibility problem.
     * It will be removed soon.
     */
    @Deprecated
    public Integer getTaskIdByBuildId(int buildId) {
        List<Integer> result = tasks.values().stream()
                .filter(t -> t instanceof BpmBuildTask)
                .filter(t -> ((BpmBuildTask) t).getBuildTask().getId() == buildId)
                .map(BpmTask::getTaskId)
                .collect(Collectors.toList());
        if (result.size() > 1)
            throw new IllegalStateException("More that one task with the same build id: " + result);
        return result.size() == 1 ? result.get(0) : null;
    }

    public Collection<BpmTask> getActiveTasks() {
        return Collections.unmodifiableCollection(new HashSet<>(tasks.values()));
    }

    public Optional<BpmTask> getTaskById(int taskId) {
        return Optional.ofNullable(tasks.get(taskId));
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.common.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collector;

/**
 * Created by Matej Lazar on 2015-02-17.
 */
public class StreamCollectors {

    /**
     * Collect element(s) into a list and then return the single element of this list.
     * If there is not exactly one element, throw an exception.
     * @param <T> module config
     * @return
    */
    public static <T> Collector<T, List<T>, T> singletonCollector() {
        return Collector.of(
                ArrayList::new,
                List::add,
                (left, right) -> {
                    left.addAll(right);
                    return left;
                },
                list -> {
                    if (list.size() == 0) {
                        return null;
                    }
                    if (list.size() > 1) {
                        throw new IllegalStateException();
                    }
                    return list.get(0);
                }
        );
    }

    /**
     * Flattening collector.
     * Look at StreamCollectorsTest for example usage
     *
     *
     * @param <T> type of elements in the collections
     * @return flattened list of elements from all of the collections
     */
    public static <T> Collector<Collection<T>, List<T>, List<T>> toFlatList() {
        return Collector.of(
                ArrayList::new,
                List::addAll,
                (left, right) -> {
                    left.addAll(right);
                    return left;
                });
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.coordinator.maintenance;

import org.jboss.pnc.auth.KeycloakServiceClient;
import org.jboss.pnc.common.Configuration;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.moduleconfig.SystemConfig;
import org.jboss.pnc.common.json.moduleprovider.PncConfigProvider;
import org.jboss.pnc.common.util.TimeUtils;
import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigSetRecordRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildRecordRepository;
import org.jboss.pnc.spi.exception.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.inject.Inject;
import java.util.Date;
import java.util.List;

/**
 * Executes regular cleanup of old temporary builds after expiration.
 *
 * @author Jakub Bartecek
 */
@Singleton
public class TemporaryBuildsCleanupScheduler {
    private final Logger log = LoggerFactory.getLogger(TemporaryBuildsCleanupScheduler.class);

    private final int DEFAULT_LIFESPAN = 14;

    private final int TEMPORARY_BUILD_LIFESPAN;

    private BuildRecordRepository buildRecordRepository;

    private BuildConfigSetRecordRepository buildConfigSetRecordRepository;

    private TemporaryBuildsCleaner temporaryBuildsCleaner;

    private KeycloakServiceClient serviceClient;

    @Deprecated
    public TemporaryBuildsCleanupScheduler() {
        log.warn("Deprecated constructor used to init TemporaryBuildsCleanupScheduler. Default values will be used.");
        TEMPORARY_BUILD_LIFESPAN = DEFAULT_LIFESPAN;
    }

    @Inject
    public TemporaryBuildsCleanupScheduler(
            Configuration configuration,
            BuildRecordRepository buildRecordRepository,
            BuildConfigSetRecordRepository buildConfigSetRecordRepository,
            TemporaryBuildsCleaner temporaryBuildsCleaner,
            KeycloakServiceClient serviceClient) {
        int _temporaryBuildLifeSpan;
        try {
            SystemConfig systemConfig = configuration.getModuleConfig(new PncConfigProvider<>(SystemConfig.class));
            _temporaryBuildLifeSpan = systemConfig.getTemporaryBuildsLifeSpan();
        } catch (ConfigurationParseException e) {
            log.warn("TemporaryBuildsCleanupScheduler initialization from config file failed! Default values will be used.");
            _temporaryBuildLifeSpan = DEFAULT_LIFESPAN;
        }
        this.TEMPORARY_BUILD_LIFESPAN = _temporaryBuildLifeSpan;
        this.buildRecordRepository = buildRecordRepository;
        this.buildConfigSetRecordRepository = buildConfigSetRecordRepository;
        this.temporaryBuildsCleaner = temporaryBuildsCleaner;
        this.serviceClient = serviceClient;
    }

    /**
     * Cleanup old temporary builds every midnight
     */
    @Schedule
    public void cleanupExpiredTemporaryBuilds() throws ValidationException {
        log.info("Regular cleanup of expired temporary builds started. Removing builds older than " + TEMPORARY_BUILD_LIFESPAN
                + " days.");

        Date expirationThreshold = TimeUtils.getDateXDaysAgo(TEMPORARY_BUILD_LIFESPAN);

        String authToken = serviceClient.getAuthToken();
        deleteExpiredBuildConfigSetRecords(expirationThreshold, authToken);
        deleteExpiredBuildRecords(expirationThreshold, authToken);

        log.info("Regular cleanup of expired temporary builds finished.");
    }

    private void deleteExpiredBuildConfigSetRecords(Date expirationThreshold, String authToken) throws ValidationException {
        List<BuildConfigSetRecord> expiredBCSRecords = buildConfigSetRecordRepository.findTemporaryBuildConfigSetRecordsOlderThan(expirationThreshold);
        for (BuildConfigSetRecord bcsr : expiredBCSRecords) {
            temporaryBuildsCleaner.deleteTemporaryBuildConfigSetRecord(bcsr.getId(), authToken);
        }
    }

    private void deleteExpiredBuildRecords(Date expirationThreshold, String authToken) throws ValidationException {
        List<BuildRecord> expiredBuilds = buildRecordRepository.findTemporaryBuildsOlderThan(expirationThreshold);
        for (BuildRecord br : expiredBuilds) {
            temporaryBuildsCleaner.deleteTemporaryBuild(br.getId(), authToken);
        }
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.indyrepositorymanager;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.commonjava.indy.client.core.Indy;
import org.commonjava.indy.client.core.IndyClientException;
import org.commonjava.indy.client.core.IndyClientHttp;
import org.commonjava.indy.client.core.IndyClientModule;
import org.commonjava.indy.client.core.auth.IndyClientAuthenticator;
import org.commonjava.indy.client.core.auth.OAuth20BearerTokenAuthenticator;
import org.commonjava.indy.folo.client.IndyFoloAdminClientModule;
import org.commonjava.indy.folo.client.IndyFoloContentClientModule;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.model.core.dto.StoreListingDTO;
import org.commonjava.indy.model.core.io.IndyObjectMapper;
import org.commonjava.indy.promote.client.IndyPromoteClientModule;
import org.commonjava.util.jhttpc.model.SiteConfig;
import org.commonjava.util.jhttpc.model.SiteConfigBuilder;
import org.jboss.pnc.common.Configuration;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.moduleconfig.IndyRepoDriverModuleConfig;
import org.jboss.pnc.common.json.moduleconfig.IndyRepoDriverModuleConfig.IgnoredPathSuffixes;
import org.jboss.pnc.common.json.moduleconfig.IndyRepoDriverModuleConfig.InternalRepoPatterns;
import org.jboss.pnc.common.json.moduleprovider.PncConfigProvider;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.spi.repositorymanager.ArtifactRepository;
import org.jboss.pnc.spi.repositorymanager.BuildExecution;
import org.jboss.pnc.spi.repositorymanager.RepositoryManager;
import org.jboss.pnc.spi.repositorymanager.RepositoryManagerException;
import org.jboss.pnc.spi.repositorymanager.model.RepositorySession;
import org.jboss.pnc.spi.repositorymanager.model.RunningRepositoryDeletion;
import org.jboss.pnc.spi.repositorymanager.model.RunningRepositoryPromotion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.jboss.pnc.enums.RepositoryType;
import static org.commonjava.indy.pkg.maven.model.MavenPackageTypeDescriptor.MAVEN_PKG_KEY;
import static org.commonjava.indy.pkg.npm.model.NPMPackageTypeDescriptor.NPM_PKG_KEY;
import static org.jboss.pnc.indyrepositorymanager.IndyRepositoryConstants.COMMON_BUILD_GROUP_CONSTITUENTS_GROUP;
import static org.jboss.pnc.indyrepositorymanager.IndyRepositoryConstants.DRIVER_ID;
import static org.jboss.pnc.indyrepositorymanager.IndyRepositoryConstants.TEMPORARY_BUILDS_GROUP;

/**
 * Implementation of {@link RepositoryManager} that manages an Indy instance to
 * support repositories for Maven-ish builds.
 *

 * Created by Matej Lazar on 2014-11-25.
 *
 * @author John Casey
 */
@ApplicationScoped
public class RepositoryManagerDriver implements RepositoryManager {

    static final String EXTRA_PUBLIC_REPOSITORIES_KEY = "EXTRA_REPOSITORIES";

    private static final Logger userLog = LoggerFactory.getLogger("org.jboss.pnc._userlog_.build-executor");

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final int DEFAULT_REQUEST_TIMEOUT;

    private final String BUILD_PROMOTION_TARGET;

    private final String TEMP_BUILD_PROMOTION_GROUP;

    private String baseUrl;
    private Map<String, Indy> indyMap = new HashMap<>();

    private InternalRepoPatterns internalRepoPatterns;

    private IgnoredPathSuffixes ignoredPathSuffixes;

    @Deprecated
    public RepositoryManagerDriver() { // workaround for CDI constructor parameter injection bug
        this.DEFAULT_REQUEST_TIMEOUT = 0;
        this.BUILD_PROMOTION_TARGET = "";
        this.TEMP_BUILD_PROMOTION_GROUP = "";
    }

    @Inject
    public RepositoryManagerDriver(Configuration configuration) {
        IndyRepoDriverModuleConfig config;
        try {
            config = configuration
                    .getModuleConfig(new PncConfigProvider<>(IndyRepoDriverModuleConfig.class));
        } catch (ConfigurationParseException e) {
            throw new IllegalStateException("Cannot read configuration for " + DRIVER_ID + ".", e);
        }
        this.DEFAULT_REQUEST_TIMEOUT = config.getDefaultRequestTimeout();
        this.BUILD_PROMOTION_TARGET = config.getBuildPromotionTarget();
        this.TEMP_BUILD_PROMOTION_GROUP = config.getTempBuildPromotionGroup();

        baseUrl = StringUtils.stripEnd(config.getBaseUrl(), "/");
        if (!baseUrl.endsWith("/api")) {
            baseUrl += "/api";
        }

        List<String> constInternalRepoPatterns = new ArrayList<>();
        constInternalRepoPatterns.add(IndyRepositoryConstants.SHARED_IMPORTS_ID);
        internalRepoPatterns = new InternalRepoPatterns();
        internalRepoPatterns.setMaven(constInternalRepoPatterns);
        internalRepoPatterns.setNpm(new ArrayList<>(constInternalRepoPatterns));

        InternalRepoPatterns extraInternalRepoPatterns = config.getInternalRepoPatterns();
        if (extraInternalRepoPatterns != null) {
            internalRepoPatterns.addMaven(extraInternalRepoPatterns.getMaven());
            internalRepoPatterns.addNpm(extraInternalRepoPatterns.getNpm());
        }

        IgnoredPathSuffixes ignoredPathSuffixes = config.getIgnoredPathSuffixes();
        if (ignoredPathSuffixes == null) {
            this.ignoredPathSuffixes = new IgnoredPathSuffixes();
        } else {
            this.ignoredPathSuffixes = ignoredPathSuffixes; // TODO do we need a copy?
        }
    }

    private Indy init(String accessToken) {
        Indy indy = indyMap.get(accessToken);
        if (indy == null) { //TODO use indyConcurrentMap.computeIfAbsent
            IndyClientAuthenticator authenticator = null;
            if (accessToken != null) {
                authenticator = new OAuth20BearerTokenAuthenticator(accessToken);
            }
            try {
                SiteConfig siteConfig = new SiteConfigBuilder("indy", baseUrl)
                        .withRequestTimeoutSeconds(DEFAULT_REQUEST_TIMEOUT)
                        .withMaxConnections(IndyClientHttp.GLOBAL_MAX_CONNECTIONS)
                        .build();

                IndyClientModule[] modules = new IndyClientModule[] {
                        new IndyFoloAdminClientModule(),
                        new IndyFoloContentClientModule(),
                        new IndyPromoteClientModule() };

                indy = new Indy(siteConfig, authenticator, new IndyObjectMapper(true), modules);

                indyMap.put(accessToken, indy);
            } catch (IndyClientException e) {
                throw new IllegalStateException("Failed to create Indy client: " + e.getMessage(), e);
            }
        }
        return indy;
    }

    @Override
    public void close(String accessToken) {
        if (indyMap.containsKey(accessToken)) {
            IOUtils.closeQuietly(indyMap.get(accessToken));
            indyMap.remove(accessToken);
        }
    }

    /**
     * Supports RepositoryType#MAVEN and RepositoryType#NPM
     */
    @Override
    public boolean canManage(RepositoryType managerType) {
        return (managerType == RepositoryType.MAVEN) || (managerType == RepositoryType.NPM);
    }

    /**
     * Use the Indy client API to setup global and build-set level repos and groups, then setup the repo/group needed for this
     * build. Calculate the URL to use for resolving artifacts using the Indy Folo API (Folo is an artifact activity-tracker).
     * Return a new session ({@link IndyRepositorySession}) containing this information.
     *
     * @throws RepositoryManagerException In the event one or more repositories or groups can't be created to support the build
     *                                    (or product, or shared-releases).
     */
    @Override
    public RepositorySession createBuildRepository(BuildExecution buildExecution,
                                                   String accessToken,
                                                   String serviceAccountToken,
                                                   RepositoryType repositoryType,
                                                   Map<String, String> genericParameters) throws RepositoryManagerException {
        Indy indy = init(accessToken);
        Indy serviceAccountIndy = init(serviceAccountToken);
        String packageType = getIndyPackageTypeKey(repositoryType);

        String buildId = buildExecution.getBuildContentId();
        try {
            setupBuildRepos(buildExecution, packageType, serviceAccountIndy, genericParameters);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to setup repository or repository group for this build: %s", e,
                    e.getMessage());
        }

        // since we're setting up a group/hosted repo per build, we can pin the tracking ID to the build repo ID.
        String url;
        String deployUrl;

        try {
            // manually initialize the tracking record, just in case (somehow) nothing gets downloaded/uploaded.
            indy.module(IndyFoloAdminClientModule.class).initReport(buildId);

            StoreKey groupKey = new StoreKey(packageType, StoreType.group, buildId);
            url = indy.module(IndyFoloContentClientModule.class).trackingUrl(buildId, groupKey);

            StoreKey hostedKey = new StoreKey(packageType, StoreType.hosted, buildId);
            deployUrl = indy.module(IndyFoloContentClientModule.class).trackingUrl(buildId, hostedKey);

            logger.info("Using '{}' for {} repository access in build: {}", url, packageType, buildId);
        } catch (IndyClientException e) {
            throw new RepositoryManagerException("Failed to retrieve Indy client module for the artifact tracker: %s", e,
                    e.getMessage());
        }

        boolean tempBuild = buildExecution.isTempBuild();
        String buildPromotionGroup = tempBuild ? TEMP_BUILD_PROMOTION_GROUP : BUILD_PROMOTION_TARGET;
        return new IndyRepositorySession(indy, serviceAccountIndy, buildId, packageType,
                new IndyRepositoryConnectionInfo(url, deployUrl),
                internalRepoPatterns, ignoredPathSuffixes, buildPromotionGroup, tempBuild);
    }

    private String getIndyPackageTypeKey(RepositoryType repoType) {
        switch (repoType) {
            case MAVEN:
                return MAVEN_PKG_KEY;
            case NPM:
                return NPM_PKG_KEY;
            default:
                throw new IllegalArgumentException("Repository type " + repoType
                        + " is not supported by this repository manager driver.");
        }
    }

    /**
     * Create the hosted repository and group necessary to support a single build. The hosted repository holds artifacts
     * uploaded from the build, and the group coordinates access to this hosted repository, along with content from the
     * product-level content group with which this build is associated. The group also provides a tracking target, so the
     * repository manager can keep track of downloads and uploads for the build.
     *
     * @param execution The execution object, which contains the content id for creating the repo, and the build id.
     * @param packageType the package type key used by Indy
     * @throws IndyClientException
     */
    private void setupBuildRepos(BuildExecution execution, String packageType, Indy indy, Map<String, String> genericParameters) throws IndyClientException {

        String buildContentId = execution.getBuildContentId();
        int id = execution.getId();

        // if the build-level group doesn't exist, create it.
        StoreKey groupKey = new StoreKey(packageType, StoreType.group, buildContentId);

        if (!indy.stores().exists(groupKey)) {
            // if the product-level storage repo (for in-progress product builds) doesn't exist, create it.
            StoreKey hostedKey = new StoreKey(packageType, StoreType.hosted, buildContentId);
            boolean tempBuild = execution.isTempBuild();
            if (!indy.stores().exists(hostedKey)) {
                HostedRepository buildArtifacts = new HostedRepository(packageType, buildContentId);
                buildArtifacts.setAllowSnapshots(tempBuild);
                buildArtifacts.setAllowReleases(true);

                buildArtifacts.setDescription(String.format("Build output for PNC %s build #%s", packageType, id));

                indy.stores().create(buildArtifacts, "Creating hosted repository for " + packageType + " build: " + id
                        + " (repo: " + buildContentId + ")", HostedRepository.class);
            }

            Group buildGroup = new Group(packageType, buildContentId);
            String adjective = tempBuild ? "temporary " : "";
            buildGroup.setDescription(String.format("Aggregation group for PNC %sbuild #%s", adjective, id));

            // build-local artifacts
            buildGroup.addConstituent(hostedKey);

            // Global-level repos, for captured/shared artifacts and access to the outside world
            addGlobalConstituents(packageType, buildGroup, tempBuild);

            // add extra repositories removed from poms by the adjust process and set in BC by user
            List<ArtifactRepository> extraDependencyRepositories = extractExtraRepositoriesFromGenericParameters(genericParameters);
            if (execution.getArtifactRepositories() != null) {
                extraDependencyRepositories.addAll(execution.getArtifactRepositories());
            }
            addExtraConstituents(packageType, extraDependencyRepositories, id, buildContentId, indy, buildGroup);

            indy.stores().create(buildGroup, "Creating repository group for resolving artifacts in build: " + id
                    + " (repo: " + buildContentId + ")", Group.class);
        }
    }

    List<ArtifactRepository> extractExtraRepositoriesFromGenericParameters(Map<String, String> genericParameters) {
        String extraReposString = genericParameters.get(EXTRA_PUBLIC_REPOSITORIES_KEY);
        if (extraReposString == null) {
            return new ArrayList<>();
        }

        return Arrays.stream(extraReposString.split("\n"))
                .map((repoString) -> {
                    try {
                        String id = new URL(repoString)
                                .getHost()
                                .replaceAll("\\.", "-");
                        return ArtifactRepository.build(id, id, repoString.trim(), true, false);
                    } catch (MalformedURLException e) {
                        userLog.warn("Malformed repository URL entered: " + repoString + ". Skipping.");
                        return null;
                    }
                })
                .filter((x) -> x != null)
                .collect(Collectors.toList());
    }

    /**
     * Adds extra remote repositories to the build group that are requested for the particular build. For a Maven build
     * these are repositories defined in the root pom removed by PME by the adjust process.
     *
     * @param packageType
     *            the package type key used by Indy
     * @param repositories
     *            the list of repositories to be added
     * @param buildId
     *            build ID
     * @param buildContentId
     *            build content ID
     * @param indy
     *            Indy client
     * @param buildGroup
     *            target build group in which the repositories are added
     *
     * @throws IndyClientException
     *             in case of an issue when communicating with the repository manager
     */
    private void addExtraConstituents(String packageType, List<ArtifactRepository> repositories, int buildId,
            String buildContentId, Indy indy, Group buildGroup) throws IndyClientException {
        if (repositories != null && !repositories.isEmpty()) {
            StoreListingDTO<RemoteRepository> existingRepos = indy.stores().listRemoteRepositories(packageType);
            for (ArtifactRepository repository : repositories) {
                StoreKey remoteKey = null;
                for (RemoteRepository existingRepo : existingRepos) {
                    if (existingRepo.getUrl().equals(repository.getUrl())) {
                        remoteKey = existingRepo.getKey();
                        break;
                    }
                }

                if (remoteKey == null) {
                    // this is basically an implied repo, so using the same prefix "i-"
                    String remoteName = "i-" + convertIllegalCharacters(repository.getId());

                    // find a free repository ID for the newly created repo
                    remoteKey = new StoreKey(packageType, StoreType.remote, remoteName);
                    int i = 2;
                    while (indy.stores().exists(remoteKey)) {
                        remoteKey = new StoreKey(packageType, StoreType.remote, remoteName + "-" + i++);
                    }

                    RemoteRepository remoteRepo = new RemoteRepository(packageType, remoteKey.getName(), repository.getUrl());
                    remoteRepo.setAllowReleases(repository.getReleases());
                    remoteRepo.setAllowSnapshots(repository.getSnapshots());
                    remoteRepo.setDescription("Implicitly created " + packageType + " repo for: " + repository.getName()
                            + " (" + repository.getId() + ") from repository declaration removed by PME in build "
                            + buildId + " (repo: " + buildContentId + ")");
                    indy.stores().create(remoteRepo, "Creating extra remote repository " + repository.getName() + " ("
                            + repository.getId() + ") for build: " + buildId + " (repo: " + buildContentId + ")",
                            RemoteRepository.class);
                }

                buildGroup.addConstituent(remoteKey);
            }
        }
    }

    /**
     * Converts characters in a given string considered as illegal by Indy to underscores.
     *
     * @param name repository name
     * @return string with converted characters
     */
    private String convertIllegalCharacters(String name) {
        char[] result = new char[name.length()];
        for (int i = 0; i < name.length(); i++) {
            char checkedChar = name.charAt(i);
            if (Character.isLetterOrDigit(checkedChar) || checkedChar == '+' || checkedChar == '-' || checkedChar == '.') {
                result[i] = checkedChar;
            } else {
                result[i] = '_';
            }
        }
        return String.valueOf(result);
    }

    /**
     * Add the constituents that every build repository group should contain:
     *

     *
builds-untested (Group)

     *
for temporary builds add also temporary-builds (Group)

     *
shared-imports (Hosted Repo)

     *
public (Group)

     *

     * @param pakageType package type key used by Indy
     */
    private void addGlobalConstituents(String pakageType, Group group, boolean tempBuild) {
        // 1. global builds artifacts
        if (tempBuild) {
            group.addConstituent(new StoreKey(pakageType, StoreType.group, TEMPORARY_BUILDS_GROUP));
        }
        group.addConstituent(new StoreKey(pakageType, StoreType.group, COMMON_BUILD_GROUP_CONSTITUENTS_GROUP));
    }

    /**
     * Convenience method for tests.
     */
    protected Indy getIndy(String accessToken) {
        return init(accessToken);
    }

    /**
     * Promote the hosted repository associated with a given project build to an arbitrary repository group.
     *
     * @return The promotion instance, which won't actually trigger promotion until its
     * {@link RunningRepositoryPromotion#monitor(java.util.function.Consumer, java.util.function.Consumer)} method is
     * called.
     */
    @Override
    public RunningRepositoryPromotion promoteBuild(BuildRecord buildRecord, String pakageType, String toGroup,
            String accessToken) throws RepositoryManagerException {
        Indy indy = init(accessToken);
        return new IndyRunningPromotion(pakageType, StoreType.hosted, buildRecord.getBuildContentId(), toGroup, indy);
    }

    @Override
    public RunningRepositoryDeletion deleteBuild(BuildRecord buildRecord, String pakageType, String accessToken)
            throws RepositoryManagerException {
        Indy indy = init(accessToken);
        return new IndyRunningDeletion(pakageType, StoreType.hosted, buildRecord.getBuildContentId(), indy);
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.rest.notifications.websockets;

import org.jboss.pnc.rest.restmodel.response.error.ErrorResponseRest;
import org.jboss.pnc.common.json.JsonOutputConverterMapper;
import org.jboss.pnc.spi.notifications.AttachedClient;
import org.jboss.pnc.spi.notifications.MessageCallback;
import org.jboss.pnc.spi.notifications.Notifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Optional;

/**
 * Web Sockets notification implementation.
 */
@ServerEndpoint(NotificationsEndpoint.ENDPOINT_PATH)
public class NotificationsEndpoint {

    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    /** broadcasting endpoint, all events are sent to all subscribed users */
    public static final String ENDPOINT_PATH = "/ws/build-records/notifications"; //TODO rename endpoint

    @Inject
    Notifier notifier;

    private final MessageCallback messageCallback = new MessageCallback() {

        @Override
        public void successful(AttachedClient attachedClient) {
            // logger.debug("Successfully sent message to client ", attachedClient);
        }

        @Override
        public void failed(AttachedClient attachedClient, Throwable throwable) {
            logger.error("Notification client threw an error, removing it. ", throwable);
            notifier.detachClient(attachedClient);
        }
    };

    @OnOpen
    public void attach(Session attachedSession) {
        logger.debug("Opened new session id: {}, uri: {}.", attachedSession.getId(), attachedSession.getRequestURI());
        notifier.attachClient(new SessionBasedAttachedClient(attachedSession, notifier));
    }

    @OnClose
    public void detach(Session detachedSession) {
        notifier.detachClient(new SessionBasedAttachedClient(detachedSession, notifier));
    }

    @OnError
    public void onError(Session session, Throwable t) {
        logger.warn("An error occurred in client: " + session + ". Removing it", t);
        notifier.detachClient(new SessionBasedAttachedClient(session, notifier));
    }

    /**
     * Expected message format:
     * {
     *   messageType: 'process-updates',
     *   message: {
     *     _typed_body_
     *   }
     * }
     *
     * Example:
     * {
     *   messageType: 'process-updates',
     *   message: {
     *     action: 'subscribe|unsubscribe',
     *     topic: 'component-build',
     *     id: 123
     *   }
     * }
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {

        RequestParser parser = new RequestParser();
        try {
            if (!parser.parseRequest(message)) {
                respondWithErrorMessage(parser.getErrorMessage(), parser.getFailedStatus(), session);
                return;
            }
        } catch (IOException e) {
            respondWithErrorMessage(parser.getErrorMessage() + " " + e.getMessage(), parser.getFailedStatus(), session, e);
            return;
        }

        MessageType messageType = parser.getMessageType();
        if (MessageType.PROCESS_UPDATES.equals(messageType)) {
            ProgressUpdatesRequest progressUpdatesRequest = parser.<ProgressUpdatesRequest>getData();
            onProgressUpdateRequest(progressUpdatesRequest, session);
        }
    }

    private void respondWithErrorMessage(String errorMessage, Response.Status status, Session session) {
        respondWithErrorMessage(errorMessage, status, session, null);
    }

    private void respondWithErrorMessage(String errorMessage, Response.Status status, Session session, Exception e) {
        String statusCode = Integer.toString(status.getStatusCode());
        String error = JsonOutputConverterMapper.apply(new ErrorResponseRest(statusCode, errorMessage));
        if (e != null) {
            logger.warn(errorMessage, e);
        } else {
            logger.warn(errorMessage);
        }
        session.getAsyncRemote().sendText(error);
    }

    private void onProgressUpdateRequest(ProgressUpdatesRequest progressUpdatesRequest, Session session) {
        Optional<AttachedClient> attachedClient = notifier.getAttachedClient(session.getId());
        AttachedClient client;
        if (attachedClient.isPresent()) {
            client = attachedClient.get();
        } else {
            logger.error("Something went wrong, the client should be attached.");
            return;
        }

        String topic = progressUpdatesRequest.getTopic();
        String messagesId = progressUpdatesRequest.getId();

        if (Action.SUBSCRIBE.equals(progressUpdatesRequest.getAction())) {
            logger.debug("Subscribing new updates listener for topic: {} and messageId: {}.", topic, messagesId);
            client.subscribe(topic, messagesId);
        } else if (Action.UNSUBSCRIBE.equals(progressUpdatesRequest.getAction())) {
            client.unsubscribe(topic, messagesId);
        } else {
            String statusCode = Integer.toString(Response.Status.NOT_ACCEPTABLE.getStatusCode());
            String errorMessage = "Invalid action: " + progressUpdatesRequest.getAction() + ". Supported actions are: {}." + Action.values();
            String error = JsonOutputConverterMapper.apply(new ErrorResponseRest(statusCode, errorMessage));
            logger.warn(errorMessage);
            session.getAsyncRemote().sendText(error);
        }
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.spi.datastore.predicates;

import org.jboss.pnc.model.Artifact;
import org.jboss.pnc.model.Artifact_;
import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildConfigSetRecord_;
import org.jboss.pnc.model.BuildConfigurationSet;
import org.jboss.pnc.model.BuildConfigurationSet_;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.model.BuildRecord_;
import org.jboss.pnc.enums.BuildStatus;
import org.jboss.pnc.model.IdRev;
import org.jboss.pnc.model.ProductMilestone;
import org.jboss.pnc.model.ProductMilestone_;
import org.jboss.pnc.model.User;
import org.jboss.pnc.spi.datastore.repositories.api.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.MapJoin;
import javax.persistence.criteria.SetJoin;

import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Predicates for {@link org.jboss.pnc.model.BuildRecord} entity.
 */
public class BuildRecordPredicates {

    private static Logger logger = LoggerFactory.getLogger(BuildRecordPredicates.class);

    public static Predicate<BuildRecord> withBuildRecordId(Integer buildRecordId) {
        return (root, query, cb) -> cb.equal(root.get(org.jboss.pnc.model.BuildRecord_.id), buildRecordId);
    }

    public static Predicate<BuildRecord> withBuildConfigurationId(Integer configurationId) {
        return (root, query, cb) -> cb.equal(root.get(BuildRecord_.buildConfigurationId), configurationId);
    }

    public static Predicate<BuildRecord> withBuildConfigurationIdAndStatusExecuted(Integer configurationId) {
        return (root, query, cb) ->
            cb.and(
                    withBuildConfigurationId(configurationId).apply(root, query, cb),
                    cb.notEqual(root.get(BuildRecord_.status), BuildStatus.NO_REBUILD_REQUIRED) //TODO 2.0 add CANCEL
            );
    }

    public static Predicate<BuildRecord> withBuildConfigurationIds(Set<Integer> configurationIds) {
        return (root, query, cb) -> root.get(BuildRecord_.buildConfigurationId).in(configurationIds);
    }

    public static Predicate<BuildRecord> withBuildConfigurationIdRev(IdRev idRev) {
        return (root, query, cb) ->
                cb.and(cb.equal(root.get(BuildRecord_.buildConfigurationId), idRev.getId()),
                       cb.equal(root.get(BuildRecord_.buildConfigurationRev), idRev.getRev()));
    }

    public static Predicate<BuildRecord> withSuccess() {
        return (root, query, cb) -> cb.equal(root.get(BuildRecord_.status), BuildStatus.SUCCESS);
    }

    /**
     * Only relevant situation is when (temporary is false) and whole right side is false.
     * Right side is false when BuildRecord is temporary => cb.isFalse(true) (true is not false)
     *  -> conclusion = in case requested build is not temporary (persistent) and record is temporary, record is ignored
     *
     * @param temporary if requested build is temporary
     * @return Predicate that filters out temporary records when dealing with persistent builds
     */
    public static Predicate<BuildRecord> includeTemporary(boolean temporary) {
        return (root, query, cb) -> (temporary) ? cb.and() : cb.isFalse(root.get(BuildRecord_.temporaryBuild));
    }

    public static Predicate<BuildRecord> withBuildConfigSetId(Integer buildConfigSetId) {
        return (root, query, cb) -> {

            Join<BuildRecord, BuildConfigSetRecord> builtConfigSetRecord = root.join(BuildRecord_.buildConfigSetRecord);

            Join<BuildConfigSetRecord, BuildConfigurationSet> buildConfigSet = builtConfigSetRecord.join(BuildConfigSetRecord_.buildConfigurationSet);

            return cb.equal(buildConfigSet.get(BuildConfigurationSet_.id), buildConfigSetId);
        };
    }

    public static Predicate<BuildRecord> withBuildConfigurationIdInSet(Collection<Integer> buildConfigurationIds) {
        if (buildConfigurationIds.isEmpty()) {
            // return an always false predicate if there are no build config ids
            return (root, query, cb) -> cb.disjunction();
        } else {
            return (root, query, cb) -> root.get(BuildRecord_.buildConfigurationId).in(buildConfigurationIds);
        }
    }

    public static Predicate<BuildRecord> withBuildConfigSetRecordId(Integer buildConfigSetRecordId) {
        return (root, query, cb) -> {
            Join<BuildRecord, BuildConfigSetRecord> joinedConfigSetRecord = root.join(BuildRecord_.buildConfigSetRecord);
            return cb.equal(joinedConfigSetRecord.get(BuildConfigSetRecord_.id), buildConfigSetRecordId);
        };
    }

    public static Predicate<BuildRecord> withBuildConfigurationIdRev(List<IdRev> buildConfigurationsWithIdRevs) {
        if (buildConfigurationsWithIdRevs.isEmpty()) {
            return Predicate.nonMatching();
        }

        List<String> idRevs = buildConfigurationsWithIdRevs.stream()
                .map(idRev -> idRev.getId() + "-" + idRev.getRev()).collect(Collectors.toList());

        return (root, query, cb) -> {
            Expression<String> concat = cb.concat(root.get(BuildRecord_.buildConfigurationId).as(String.class), "-");
            Expression<String> buildRecordIdRev = cb.concat(concat, root.get(BuildRecord_.buildConfigurationRev).as(String.class));
            logger.debug("Searching for BuildRecords with {}", idRevs);
            return buildRecordIdRev.in(idRevs);
        };
    }

    public static Predicate<BuildRecord> withArtifactDistributedInMilestone(Integer productMilestoneId) {
        return (root, query, cb) -> {
            SetJoin<BuildRecord, Artifact> builtArtifacts = root.join(BuildRecord_.builtArtifacts);
            SetJoin<Artifact, ProductMilestone> productMilestones = builtArtifacts.join(Artifact_.distributedInProductMilestones);
            return cb.equal(productMilestones.get(ProductMilestone_.id), productMilestoneId);
        };
    }

    public static Predicate<BuildRecord> withPerformedInMilestone(Integer productMilestoneId) {
        return (root, query, cb) -> {
            Join<BuildRecord, ProductMilestone> productMilestone = root.join(BuildRecord_.productMilestone);
            return cb.equal(productMilestone.get(org.jboss.pnc.model.ProductMilestone_.id), productMilestoneId);
        };
    }

    public static Predicate<BuildRecord> withUserId(Integer userId) {
        return (root, query, cb) -> {
            Join<BuildRecord, User> buildRecordJoinedUsers = root.join(BuildRecord_.user);
            return cb.equal(buildRecordJoinedUsers.get(org.jboss.pnc.model.User_.id), userId);
        };
    }

    public static Predicate<BuildRecord> withAttribute(String key, String value) {
        return (root, query, cb) -> {
            MapJoin<Object, Object, Object> mapJoinAttributes = root.joinMap(BuildRecord_.attributes.getName());
            return query.where(cb.and(cb.equal(mapJoinAttributes.key(), key), cb.equal(mapJoinAttributes.value(), value))).getRestriction();
        };
    }

    public static Predicate<BuildRecord> buildFinishedBefore(Date date) {
        return (root, query, cb) -> cb.lessThan(root.get(BuildRecord_.endTime), date);
    }

    public static Predicate<BuildRecord> temporaryBuild() {
        return (root, query, cb) -> cb.isTrue(root.get(BuildRecord_.temporaryBuild));
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.rest.endpoint;

import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import org.jboss.pnc.auth.AuthenticationProvider;
import org.jboss.pnc.auth.AuthenticationProviderFactory;
import org.jboss.pnc.auth.LoggedInUser;
import org.jboss.pnc.bpm.BpmEventType;
import org.jboss.pnc.bpm.BpmManager;
import org.jboss.pnc.bpm.BpmTask;
import org.jboss.pnc.bpm.task.RepositoryCreationTask;
import org.jboss.pnc.common.Configuration;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.moduleconfig.ScmModuleConfig;
import org.jboss.pnc.common.json.moduleprovider.PncConfigProvider;
import org.jboss.pnc.model.BuildConfiguration;
import org.jboss.pnc.model.RepositoryConfiguration;
import org.jboss.pnc.rest.provider.BuildConfigurationSetProvider;
import org.jboss.pnc.rest.provider.RepositoryConfigurationProvider;
import org.jboss.pnc.rest.provider.collection.CollectionInfo;
import org.jboss.pnc.rest.restmodel.BuildConfigurationRest;
import org.jboss.pnc.rest.restmodel.RepositoryConfigurationRest;
import org.jboss.pnc.rest.restmodel.bpm.BpmNotificationRest;
import org.jboss.pnc.rest.restmodel.bpm.BpmStringMapNotificationRest;
import org.jboss.pnc.rest.restmodel.bpm.BpmTaskRest;
import org.jboss.pnc.rest.restmodel.bpm.RepositoryCreationProcessRest;
import org.jboss.pnc.rest.restmodel.bpm.RepositoryCreationResultRest;
import org.jboss.pnc.rest.restmodel.bpm.RepositoryCreationUrlAutoRest;
import org.jboss.pnc.common.json.JsonOutputConverterMapper;
import org.jboss.pnc.rest.validation.ValidationBuilder;
import org.jboss.pnc.rest.validation.exceptions.EmptyEntityException;
import org.jboss.pnc.rest.validation.exceptions.InvalidEntityException;
import org.jboss.pnc.rest.validation.exceptions.RestValidationException;
import org.jboss.pnc.rest.validation.groups.WhenCreatingNew;
import org.jboss.pnc.rest.validation.validators.ScmUrlValidator;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigurationRepository;
import org.jboss.pnc.spi.datastore.repositories.RepositoryConfigurationRepository;
import org.jboss.pnc.spi.exception.CoreException;
import org.jboss.pnc.spi.notifications.Notifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.jboss.pnc.rest.configuration.SwaggerConstants.PAGE_INDEX_DEFAULT_VALUE;
import static org.jboss.pnc.rest.configuration.SwaggerConstants.PAGE_INDEX_QUERY_PARAM;
import static org.jboss.pnc.rest.configuration.SwaggerConstants.PAGE_SIZE_DEFAULT_VALUE;
import static org.jboss.pnc.rest.configuration.SwaggerConstants.PAGE_SIZE_QUERY_PARAM;

/**
 * This endpoint is used for starting and interacting
 * with BPM processes.
 *
 * @author Jakub Senko
 */
@Hidden
@Path("/bpm")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BpmEndpoint extends AbstractEndpoint {

    private static final Logger LOG = LoggerFactory.getLogger(BpmEndpoint.class);

    private BpmManager bpmManager;

    private BuildConfigurationSetProvider bcSetProvider;

    private Notifier wsNotifier;

    private AuthenticationProvider authenticationProvider;

    private RepositoryConfigurationRepository repositoryConfigurationRepository;
    private RepositoryConfigurationProvider repositoryConfigurationProvider;
    private BuildConfigurationRepository buildConfigurationRepository;

    private ScmModuleConfig moduleConfig;

    @Deprecated
    public BpmEndpoint() {
    } // CDI workaround

    @Inject
    public BpmEndpoint(BpmManager bpmManager,
            BuildConfigurationSetProvider bcSetProvider,
            AuthenticationProviderFactory authenticationProviderFactory,
            Notifier wsNotifier,
            RepositoryConfigurationRepository repositoryConfigurationRepository,
            RepositoryConfigurationProvider repositoryConfigurationProvider,
            BuildConfigurationRepository buildConfigurationRepository,
            Configuration configuration) throws ConfigurationParseException {
        this.bpmManager = bpmManager;
        this.bcSetProvider = bcSetProvider;
        this.wsNotifier = wsNotifier;
        this.authenticationProvider = authenticationProviderFactory.getProvider();
        this.repositoryConfigurationRepository = repositoryConfigurationRepository;
        this.repositoryConfigurationProvider = repositoryConfigurationProvider;
        this.buildConfigurationRepository = buildConfigurationRepository;
        this.moduleConfig = configuration.getModuleConfig(new PncConfigProvider<>(ScmModuleConfig.class));
    }

    @POST
    @Path("/tasks/{taskId}/notify")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response notifyTask(
            @Context HttpServletRequest request,
            @PathParam("taskId") int taskId) throws CoreException {

        String content;
        JsonNode node;
        try {
            content = readContent(request.getInputStream());
            node = JsonOutputConverterMapper.getMapper().readTree(content);
        } catch (IOException e) {
            throw new CoreException("Could not get JSON from request data. " +
                    "Verify it is not empty and in the correct format.", e);
        }
        if (!node.has("eventType")) {
            throw new CoreException("Request JSON does not contain required \"eventType\" field.");
        }
        String eventTypeName = node.get("eventType").asText();
        BpmEventType eventType = BpmEventType.valueOf(eventTypeName);
        BpmNotificationRest notification;
        try {
            notification = JsonOutputConverterMapper.getMapper().readValue(node.traverse(), eventType.getType());
        } catch (IOException e) {
            throw new CoreException("Could not deserialize JSON request for event type '" + eventTypeName + "' " +
                    " into '" + eventType.getType() + "'. JSON value: " + content, e);
        }
        LOG.debug("Received notification {} for BPM task with id {}.", notification, taskId);
        bpmManager.notify(taskId, notification);
        return Response.ok().build();
    }

    private String readContent(InputStream inputStream) throws IOException {
        try (InputStreamReader streamReader = new InputStreamReader(inputStream, "UTF-8");
             BufferedReader reader = new BufferedReader(streamReader)) {
            StringBuilder result = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                result.append(line).append("\n");
            }

            return result.toString();
        }
    }

    /**
     * Given the successful BC creation, add the BC into the BC sets. This solution has been
     * selected because if this was done in BPM process there would have to be a foreach cycle and
     * at least two REST requests for each BC Set ID. The process would become too complicated.
     * Notification listeners are ideal for these kind of operations.
     */
    private void onRCCreationSuccess(BpmNotificationRest notification, BuildConfigurationRest buildConfigurationRest) {
        LOG.debug("Received BPM event RC_CREATION_SUCCESS: " + notification);

        BpmStringMapNotificationRest repositoryCreationTaskResult = (BpmStringMapNotificationRest) notification;

        int repositoryConfigurationId = -1;
        int buildConfigurationSavedId = -1;
        try {
            repositoryConfigurationId = Integer.valueOf(repositoryCreationTaskResult.getData().get("repositoryConfigurationId"));
        } catch (NumberFormatException ex) {
            String errorMessage = "Receive notification about successful BC creation '" + repositoryCreationTaskResult
                    + "' but the ID of the newly created RC '" + repositoryCreationTaskResult.getData()
                    .get("repositoryConfigurationId")
                    + "' is not a number. It should be present under 'repositoryConfigurationId' key.";

            LOG.error(errorMessage, ex);
            sendErrorMessage(repositoryConfigurationId, buildConfigurationSavedId, errorMessage);
            return;
        }

        RepositoryConfiguration repositoryConfiguration = repositoryConfigurationRepository.queryById(repositoryConfigurationId);
        if (repositoryConfiguration == null) {
            String errorMessage = "Repository Configuration was not found in database.";
            LOG.error(errorMessage);
            sendErrorMessage(repositoryConfigurationId, buildConfigurationSavedId, errorMessage);
            return;
        }

        if (buildConfigurationRest != null) { //TODO test me
            BuildConfiguration buildConfiguration = buildConfigurationRest.toDBEntityBuilder()
                    .repositoryConfiguration(repositoryConfiguration)
                    .build();
            BuildConfiguration buildConfigurationSaved = buildConfigurationRepository.save(buildConfiguration);
            buildConfigurationSavedId = buildConfigurationSaved.getId();

            Set<Integer> bcSetIds = buildConfigurationRest.getBuildConfigurationSetIds();
            try {
                if (bcSetIds != null) {
                    addBuildConfigurationToSet(buildConfigurationSaved, bcSetIds);
                }
            } catch (Exception e) {
                LOG.error(e.getMessage());
                sendErrorMessage(repositoryConfigurationId, buildConfigurationSavedId, e.getMessage());
                return;
            }
        }

        RepositoryCreationResultRest repositoryCreationResultRest
                = new RepositoryCreationResultRest(
                        repositoryConfigurationId,
                        buildConfigurationSavedId,
                        RepositoryCreationResultRest.EventType.RC_CREATION_SUCCESS,
                        null);

        wsNotifier.sendMessage(repositoryCreationResultRest); //TODO test me!
    }

    private void sendErrorMessage(int repositoryConfigurationId, int buildConfigurationId, String message) {
        RepositoryCreationResultRest repositoryCreationResultRest =
                new RepositoryCreationResultRest(
                        repositoryConfigurationId,
                        buildConfigurationId,
                        RepositoryCreationResultRest.EventType.RC_CREATION_ERROR,
                        message);
        wsNotifier.sendMessage(repositoryCreationResultRest); //TODO test me!
    }

    private Response checkIfInternalUrlExits(String internalUrl) throws InvalidEntityException {
        if (internalUrl != null) {
            RepositoryConfiguration repositoryConfiguration = repositoryConfigurationRepository.queryByInternalScm(internalUrl);
            if (repositoryConfiguration != null) {
                String message = "{ \"repositoryConfigurationId\" : " + repositoryConfiguration.getId() + "}";
                return Response.status(Response.Status.CONFLICT).entity(message).build();
            }
        }
        return null;
    }

    private Response checkIfExternalUrlExits(String externalUrl) throws InvalidEntityException {
        if (externalUrl != null) {
            RepositoryConfiguration repositoryConfiguration = repositoryConfigurationRepository.queryByExternalScm(externalUrl);
            if (repositoryConfiguration != null) {
                String message = "{ \"repositoryConfigurationId\" : " + repositoryConfiguration.getId() + "}";
                return Response.status(Response.Status.CONFLICT).entity(message).build();
            }
        }
        return null;
    }

    @POST
    @Path("/tasks/start-repository-configuration-creation-url-auto")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response startRCreationTaskWithSingleUrl(
            RepositoryCreationUrlAutoRest repositoryCreationUrlAutoRest,
            @Context HttpServletRequest httpServletRequest) throws CoreException, InvalidEntityException, EmptyEntityException {

        LOG.debug("Received request to start RC creation with url autodetect: " + repositoryCreationUrlAutoRest);
        final BuildConfigurationRest buildConfigurationRest = repositoryCreationUrlAutoRest.getBuildConfigurationRest();

        ValidationBuilder.validateObject(repositoryCreationUrlAutoRest, WhenCreatingNew.class)
                .validateAnnotations();
        ValidationBuilder.validateObject(buildConfigurationRest, WhenCreatingNew.class)
                .validateAnnotations();

        RepositoryConfigurationRest.RepositoryConfigurationRestBuilder repositoryConfigurationBuilder = RepositoryConfigurationRest.builder();

        String internalScmAuthority = moduleConfig.getInternalScmAuthority();
        String scmUrl = repositoryCreationUrlAutoRest.getScmUrl();
        if (!ScmUrlValidator.isValid(scmUrl)) {
            throw new InvalidEntityException("Invalid scmUrl: " + scmUrl);
        }
        Boolean isUrlInternal = scmUrl.contains(internalScmAuthority);

        if (isUrlInternal) {
            repositoryConfigurationProvider.validateInternalRepository(scmUrl);
            Response message = checkIfInternalUrlExits(scmUrl);
            if (message != null) {
                return message;
            }
            repositoryConfigurationBuilder.internalUrl(scmUrl);
        } else {
            Response message = checkIfExternalUrlExits(scmUrl);
            if (message != null) {
                return message;
            }
            //when creating new SCM config with external Url, enable preBuildSync if it is not specified
            if (repositoryCreationUrlAutoRest.getPreBuildSyncEnabled() != null) {
                repositoryConfigurationBuilder.preBuildSyncEnabled(repositoryCreationUrlAutoRest.getPreBuildSyncEnabled());
            } else {
                repositoryConfigurationBuilder.preBuildSyncEnabled(true);
            }
            repositoryConfigurationBuilder.externalUrl(scmUrl);
        }

        RepositoryConfigurationRest repositoryConfigurationRest = repositoryConfigurationBuilder.build();
        repositoryConfigurationRest.validate();

        RepositoryCreationTask task = startRCreationTask(repositoryConfigurationRest, buildConfigurationRest, httpServletRequest);
        return Response.ok(task.getTaskId()).build();
    }

    private RepositoryCreationTask startRCreationTask(RepositoryConfigurationRest repositoryConfigurationRest, BuildConfigurationRest buildConfigurationRest,
            HttpServletRequest httpServletRequest) throws CoreException, InvalidEntityException, EmptyEntityException {
        LoggedInUser loginInUser = authenticationProvider.getLoggedInUser(httpServletRequest);

        String revision = null;
        if (buildConfigurationRest != null) {
            revision = buildConfigurationRest.getScmRevision();
        }

        RepositoryCreationProcessRest repositoryConfigurationProcessRest = new RepositoryCreationProcessRest(
                repositoryConfigurationRest, revision);

        RepositoryCreationTask repositoryCreationTask = new RepositoryCreationTask(repositoryConfigurationProcessRest, loginInUser.getTokenString());

        repositoryCreationTask.addListener(BpmEventType.RC_CREATION_SUCCESS,
                x -> onRCCreationSuccess(x, buildConfigurationRest));

        repositoryCreationTask.addListener(BpmEventType.RC_CREATION_ERROR, x -> {
            LOG.debug("Received BPM event RC_CREATION_ERROR: " + x);
        });

        addWebsocketForwardingListeners(repositoryCreationTask);

        try {
            bpmManager.startTask(repositoryCreationTask);
        } catch (CoreException e) {
            throw new CoreException("Could not start BPM task: " + repositoryCreationTask, e);
        }
        return repositoryCreationTask;
    }

    private void addBuildConfigurationToSet(BuildConfiguration buildConfiguration, Set<Integer> bcSetIds) throws Exception {
        for (Integer setId : bcSetIds) {
            try {
                bcSetProvider.addConfiguration(setId, buildConfiguration.getId());
            } catch (RestValidationException e) {
                throw new Exception("Could not add BC with ID '" + buildConfiguration.getId() +
                        "' to a BC Set with id '" + setId + "'.", e);
            }
        }
    }

    /**
     * This method will add listeners to all important RCC event types
     * and forward the event to WS clients.
     */
    private void addWebsocketForwardingListeners(RepositoryCreationTask task) {
        Consumer<? extends BpmNotificationRest> doNotify = (e) -> wsNotifier.sendMessage(e);
        task.addListener(BpmEventType.RC_REPO_CREATION_SUCCESS, doNotify);
        task.addListener(BpmEventType.RC_REPO_CREATION_ERROR, doNotify);
        task.addListener(BpmEventType.RC_REPO_CLONE_SUCCESS, doNotify);
        task.addListener(BpmEventType.RC_REPO_CLONE_ERROR, doNotify);
        //clients are notified from callback in startRCreationTask
        //task.addListener(BpmEventType.RC_CREATION_SUCCESS, doNotify);
        task.addListener(BpmEventType.RC_CREATION_ERROR, doNotify);
    }

    @GET
    @Path("/tasks")
    public Response getBPMTasks(
            @QueryParam(PAGE_INDEX_QUERY_PARAM) @DefaultValue(PAGE_INDEX_DEFAULT_VALUE) int pageIndex,
            @QueryParam(PAGE_SIZE_QUERY_PARAM) @DefaultValue(PAGE_SIZE_DEFAULT_VALUE) int pageSize
    ) {

        Collection<BpmTask> tasks = bpmManager.getActiveTasks();
        int totalPages = (tasks.size() / pageSize) + (tasks.size() % pageSize == 0 ? 0 : 1);
        List<BpmTaskRest> pagedTasks = tasks.stream()
                .sorted().skip(pageIndex * pageSize).limit(pageSize)
                .map(mapTask).collect(Collectors.toList());
        return fromCollection(new CollectionInfo<>(pageIndex, pageSize, totalPages, pagedTasks));
    }

    private static Function<BpmTask, BpmTaskRest> mapTask = (BpmTask t) -> new BpmTaskRest(
            t.getTaskId(),
            t.getProcessInstanceId(),
            t.getProcessName(),
            t.getEvents());

    @GET
    @Path("/tasks/{taskId}")
    public Response getBPMTaskById(
            @PathParam("taskId") int taskId
    ) {
        Optional<BpmTask> task = bpmManager.getTaskById(taskId);
        if (task.isPresent()) {
            return fromSingleton(mapTask.apply(task.get()));
        } else {
            return Response.status(Response.Status.NOT_FOUND).build();
        }
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.mock.model.builders;

import org.jboss.pnc.model.Artifact;
import org.jboss.pnc.model.TargetRepository;

import java.time.Instant;
import java.util.Date;

import org.jboss.pnc.enums.RepositoryType;

/**
 * @author Matej Lazar
 */
public class ArtifactBuilder {

    public static final String IDENTIFIER_PREFIX = "org.jboss.pnc:mock.artifact";

    private static Artifact.Builder getArtifactBuilder(int id) {
        return Artifact.Builder.newBuilder()
                .id(id)
                .identifier(IDENTIFIER_PREFIX + ":" + id)
                .md5("md-fake-ABCDABCD" + id)
                .sha1("sha1-fake-ABCDABCD" + id)
                .sha256("sha256-fake-ABCDABCD" + id)
                .size(12342L)
                .deployPath("http://myrepo.com/org/jboss/mock/artifactFile" + id + ".jar")
                .targetRepository(mockTargetRepository(""))
                .filename("artifactFile" + id + ".jar");
    }

    /**
     * Create a generic mock artifact with no associated build record or import URL
     */
    public static Artifact mockArtifact(int id) {
        return getArtifactBuilder(id).build();
    }

    /**
     * Create an artifact with an import date and origin url
     */
    public static Artifact mockImportedArtifact(int id) {
        return getArtifactBuilder(id)
                .importDate(Date.from(Instant.now()))
                .originUrl("http://central.maven.org/org/jboss/mock/artifactFile" + id + ".jar")
                .build();
    }

    public static TargetRepository mockTargetRepository(String path) {
        return TargetRepository.newBuilder()
                .identifier("indy-maven")
                .repositoryPath(path)
                .repositoryType(RepositoryType.MAVEN)
                .temporaryRepo(false)
                .build();
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 org.jboss.pnc.auth.keycloakutil.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContexts;
import org.jboss.pnc.auth.AuthenticationException;
import org.jboss.pnc.auth.keycloakutil.httpcomponents.HttpDelete;
import org.jboss.pnc.auth.keycloakutil.operations.LocalSearch;
import org.jboss.pnc.auth.keycloakutil.operations.RoleOperations;
import org.keycloak.util.JsonSerialization;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.keycloak.common.util.ObjectUtil.capitalize;

/**
 * @author Marko Strukelj
 */
public class HttpUtil {

    public static final String APPLICATION_XML = "application/xml";
    public static final String APPLICATION_JSON = "application/json";
    public static final String APPLICATION_FORM_URL_ENCODED = "application/x-www-form-urlencoded";
    public static final String UTF_8 = "utf-8";

    private static HttpClient httpClient;
    private static SSLConnectionSocketFactory sslsf;
    private static boolean sslRequired;

    public static InputStream doGet(String url, String acceptType, String authorization) {
        try {
            HttpGet request = new HttpGet(url);
            request.setHeader(HttpHeaders.ACCEPT, acceptType);
            return doRequest(authorization, request);
        } catch (IOException e) {
            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
        }
    }

    public static InputStream doPost(String url, String contentType, String acceptType, String content, String authorization) {
        try {
            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPost(url));
        } catch (IOException e) {
            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
        }
    }

    public static InputStream doPut(String url, String contentType, String acceptType, String content, String authorization) {
        try {
            return doPostOrPut(contentType, acceptType, content, authorization, new HttpPut(url));
        } catch (IOException e) {
            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
        }
    }

    public static void doDelete(String url, String authorization) {
        try {
            HttpDelete request = new HttpDelete(url);
            doRequest(authorization, request);
        } catch (IOException e) {
            throw new RuntimeException("Failed to send request - " + e.getMessage(), e);
        }
    }

    public static HeadersBodyStatus doGet(String url, HeadersBody request) throws IOException {
        return doRequest("get", url, request);
    }

    public static HeadersBodyStatus doPost(String url, HeadersBody request) throws IOException {
        return doRequest("post", url, request);
    }

    public static HeadersBodyStatus doPut(String url, HeadersBody request) throws IOException {
        return doRequest("put", url, request);
    }

    public static HeadersBodyStatus doDelete(String url, HeadersBody request) throws IOException {
        return doRequest("delete", url, request);
    }

    public static HeadersBodyStatus doRequest(String type, String url, HeadersBody request) throws IOException {
        HttpRequestBase req;
        switch (type) {
            case "get":
                req = new HttpGet(url);
                break;
            case "post":
                req = new HttpPost(url);
                break;
            case "put":
                req = new HttpPut(url);
                break;
            case "delete":
                req = new HttpDelete(url);
                break;
            case "options":
                req = new HttpOptions(url);
                break;
            case "head":
                req = new HttpHead(url);
                break;
            default:
                throw new RuntimeException("Method not supported: " + type);
        }
        addHeaders(req, request.getHeaders());

        if (request.getBody() != null) {
            if (req instanceof HttpEntityEnclosingRequestBase == false) {
                throw new RuntimeException("Request type does not support body: " + type);
            }
            ((HttpEntityEnclosingRequestBase) req).setEntity(new InputStreamEntity(request.getBody()));
        }

        HttpResponse res = getHttpClient().execute(req);
        InputStream responseStream = null;
        if (res.getEntity() != null) {
            responseStream = res.getEntity().getContent();
        } else {
            responseStream = new InputStream() {
                @Override
                public int read () throws IOException {
                    return -1;
                }
            };
        }

        Headers headers = new Headers();
        HeaderIterator it = res.headerIterator();
        while (it.hasNext()) {
            org.apache.http.Header header = it.nextHeader();
            headers.add(header.getName(), header.getValue());
        }

        return new HeadersBodyStatus(res.getStatusLine().toString(), headers, responseStream);
    }

    private static void addHeaders(HttpRequestBase request, Headers headers) {
        for (Header header: headers) {
            request.setHeader(header.getName(), header.getValue());
        }
    }

    private static InputStream doPostOrPut(String contentType, String acceptType, String content, String authorization, HttpEntityEnclosingRequestBase request) throws IOException {
        request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
        request.setHeader(HttpHeaders.ACCEPT, acceptType);
        if (content != null) {
            request.setEntity(new StringEntity(content));
        }

        return doRequest(authorization, request);
    }

    private static InputStream doRequest(String authorization, HttpRequestBase request) throws IOException {
        addAuth(request, authorization);

        HttpResponse response = getHttpClient().execute(request);
        InputStream responseStream = null;
        if (response.getEntity() != null) {
            responseStream = response.getEntity().getContent();
        }

        int code = response.getStatusLine().getStatusCode();
        if (code >= 200 && code < 300) {
            return responseStream;
        } else {
            Map<String, String> error = null;
            try {
                org.apache.http.Header header = response.getEntity().getContentType();
                if (header != null && APPLICATION_JSON.equals(header.getValue())) {
                    error = JsonSerialization.readValue(responseStream, Map.class);
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to read error response - " + e.getMessage(), e);
            } finally {
                responseStream.close();
            }

            String message = null;
            if (error != null) {
                message = error.get("error_description") + " [" + error.get("error") + "]";
            }
            throw new RuntimeException(message != null ? message : response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
        }
    }

    private static void addAuth(HttpRequestBase request, String authorization) {
        if (authorization != null) {
            request.setHeader(HttpHeaders.AUTHORIZATION, authorization);
        }
    }

    public static HttpClient getHttpClient() {
        if (httpClient == null) {
            HttpClientBuilder clientBuilder = HttpClientBuilder.create().useSystemProperties();
            if (sslRequired) {
                if (sslsf != null) {
                    clientBuilder.setSSLSocketFactory(sslsf);
                }
            } else {
                SSLContext sslContext;
                try {
                    sslContext = SSLContext.getInstance("SSL");

                    // set up a TrustManager that trusts everything
                    sslContext.init(null, new TrustManager[] { new X509TrustManager() {

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }

                        @Override
                        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        }

                        @Override
                        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        }
                    } }, new SecureRandom());
                } catch (NoSuchAlgorithmException ex) {
                    throw new AuthenticationException("Cannot get SSLContext instance for \"SSL\" protocol.", ex);
                } catch (KeyManagementException ex) {
                    throw new AuthenticationException("SSLContext initialization failed.", ex);
                }

                clientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).setSSLContext(sslContext);
            }
            httpClient = clientBuilder.build();
        }
        return httpClient;
    }

    public static String urlencode(String value) {
        try {
            return URLEncoder.encode(value, UTF_8);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Failed to urlencode", e);
        }
    }

    /**
     * Sets flag telling if SSL hostname validation should be done which also clears the cached httpClient. This method
     * is not thread-safe and could cause race conditions if the class is used with different settings at the same
     * time, but that is not expected.
     *
     * @param sslRequired the desired value
     */
    public static void setSslRequired(boolean sslRequired) {
        if (HttpUtil.sslRequired != sslRequired) {
            HttpUtil.sslRequired = sslRequired;
            HttpUtil.httpClient = null;
        }
    }

    public static void setTruststore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
        if (!file.isFile()) {
            throw new RuntimeException("Truststore file not found: " + file.getAbsolutePath());
        }
        SSLContext theContext = SSLContexts.custom()
                .useProtocol("TLS")
                .loadTrustMaterial(file, password == null ? null : password.toCharArray())
                .build();
        sslsf = new SSLConnectionSocketFactory(theContext);
    }

    public static String extractIdFromLocation(String location) {
        int last = location.lastIndexOf("/");
        if (last != -1) {
            return location.substring(last + 1);
        }
        return null;
    }

    public static String addQueryParamsToUri(String uri, String ... queryParams) {
        if (queryParams == null) {
            return uri;
        }

        if (queryParams.length % 2 != 0) {
            throw new RuntimeException("Value missing for query parameter: " + queryParams[queryParams.length-1]);
        }

        Map<String, String> params = new LinkedHashMap<>();
        for (int i = 0; i < queryParams.length; i += 2) {
            params.put(queryParams[i], queryParams[i+1]);
        }
        return addQueryParamsToUri(uri, params);
    }

    public static String addQueryParamsToUri(String uri, Map<String, String> queryParams) {

        if (queryParams.size() == 0) {
            return uri;
        }

        StringBuilder query = new StringBuilder();
        for (Map.Entry<String, String> params: queryParams.entrySet()) {
            try {
                if (query.length() > 0) {
                    query.append("&");
                }
                query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8"));
            } catch (Exception e) {
                throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue());
            }
        }

        return uri + (uri.indexOf("?") == -1 ? "?" : "&") + query;
    }

    public static String composeResourceUrl(String adminRoot, String realm, String uri) {
        if (!uri.startsWith("http:") && !uri.startsWith("https:")) {
            if ("realms".equals(uri) || uri.startsWith("realms/")) {
                uri = normalize(adminRoot) + uri;
            } else if ("serverinfo".equals(uri)) {
                    uri = normalize(adminRoot) + uri;
            } else {
                uri = normalize(adminRoot) + "realms/" + realm + "/" + uri;
            }
        }
        return uri;
    }

    public static String normalize(String value) {
        return value.endsWith("/") ? value : value + "/";
    }

    public static void checkSuccess(String url, HeadersBodyStatus response) {
        try {
            response.checkSuccess();
        } catch (HttpResponseException e) {
            if (e.getStatusCode() == 404) {
                throw new RuntimeException("Resource not found for url: " + url, e);
            }
            throw e;
        }
    }

    public static <T> T doGetJSON(Class<T> type, String resourceUrl, String auth) {

        Headers headers = new Headers();
        if (auth != null) {
            headers.add("Authorization", auth);
        }
        headers.add("Accept", "application/json");

        HeadersBodyStatus response;
        try {
            response = HttpUtil.doRequest("get", resourceUrl, new HeadersBody(headers));
        } catch (IOException e) {
            throw new RuntimeException("HTTP request failed: GET " + resourceUrl, e);
        }

        checkSuccess(resourceUrl, response);

        T result;
        try {
            result = JsonSerialization.readValue(response.getBody(), type);
        } catch (IOException e) {
            throw new RuntimeException("Failed to read JSON response", e);
        }

        return result;
    }

    public static void doPostJSON(String resourceUrl, String auth, Object content) {
        Headers headers = new Headers();
        if (auth != null) {
            headers.add("Authorization", auth);
        }
        headers.add("Content-Type", "application/json");

        HeadersBodyStatus response;

        byte[] body;
        try {
            body = JsonSerialization.writeValueAsBytes(content);
        } catch (IOException e) {
            throw new RuntimeException("Failed to serialize JSON", e);
        }

        try {
            response = HttpUtil.doRequest("post", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
        } catch (IOException e) {
            throw new RuntimeException("HTTP request failed: POST " + resourceUrl + "\n" + new String(body), e);
        }

        checkSuccess(resourceUrl, response);
    }

    public static void doDeleteJSON(String resourceUrl, String auth, Object content) {
        Headers headers = new Headers();
        if (auth != null) {
            headers.add("Authorization", auth);
        }
        headers.add("Content-Type", "application/json");

        HeadersBodyStatus response;

        byte[] body;
        try {
            body = JsonSerialization.writeValueAsBytes(content);
        } catch (IOException e) {
            throw new RuntimeException("Failed to serialize JSON", e);
        }

        try {
            response = HttpUtil.doRequest("delete", resourceUrl, new HeadersBody(headers, new ByteArrayInputStream(body)));
        } catch (IOException e) {
            throw new RuntimeException("HTTP request failed: DELETE " + resourceUrl + "\n" + new String(body), e);
        }

        checkSuccess(resourceUrl, response);
    }

    public static String getIdForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue) {

        return getAttrForType(rootUrl, realm, auth, resourceEndpoint, attrName, attrValue, "id");
    }

    public static String getAttrForType(String rootUrl, String realm, String auth, String resourceEndpoint, String attrName, String attrValue, String returnAttrName) {

        String resourceUrl = composeResourceUrl(rootUrl, realm, resourceEndpoint);
        resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, attrName, attrValue, "first", "0", "max", "2");

        List<ObjectNode> users = doGetJSON(RoleOperations.LIST_OF_NODES.class, resourceUrl, auth);

        ObjectNode user;
        try {
            user = new LocalSearch(users).exactMatchOne(attrValue, attrName);
        } catch (Exception e) {
            throw new RuntimeException("Multiple " + resourceEndpoint + " found for " + attrName + ": " + attrValue, e);
        }

        String typeName = singularize(resourceEndpoint);
        if (user == null) {
            throw new RuntimeException(capitalize(typeName) + " not found for " + attrName + ": " + attrValue);
        }

        JsonNode attr = user.get(returnAttrName);
        if (attr == null) {
            throw new RuntimeException("Returned " + typeName + " info has no '" + returnAttrName + "' attribute");
        }
        return attr.asText();
    }

    public static String singularize(String value) {
        return value.substring(0, value.length()-1);
    }
}