/**
 * 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.datastore;

import org.jboss.pnc.enums.RepositoryType;
import org.jboss.pnc.model.Artifact;
import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildConfiguration;
import org.jboss.pnc.model.BuildConfigurationAudited;
import org.jboss.pnc.model.BuildConfigurationSet;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.model.IdRev;
import org.jboss.pnc.model.TargetRepository;
import org.jboss.pnc.model.User;
import org.jboss.pnc.spi.coordinator.BuildTask;
import org.jboss.pnc.spi.datastore.Datastore;
import org.jboss.pnc.spi.datastore.repositories.ArtifactRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigSetRecordRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigurationAuditedRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigurationRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildRecordRepository;
import org.jboss.pnc.spi.datastore.repositories.SequenceHandlerRepository;
import org.jboss.pnc.spi.datastore.repositories.TargetRepositoryRepository;
import org.jboss.pnc.spi.datastore.repositories.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import java.lang.invoke.MethodHandles;
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.Set;

import static org.jboss.pnc.common.util.CollectionUtils.ofNullableCollection;
import static org.jboss.pnc.common.util.StreamCollectors.toFlatList;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withOriginUrl;
import static org.jboss.pnc.spi.datastore.predicates.BuildConfigurationPredicates.withBuildConfigurationSetId;
import static org.jboss.pnc.spi.datastore.predicates.UserPredicates.withUserName;

@Stateless
public class DefaultDatastore implements Datastore {

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

    private ArtifactRepository artifactRepository;

    private BuildRecordRepository buildRecordRepository;

    private BuildConfigurationRepository buildConfigurationRepository;

    private BuildConfigurationAuditedRepository buildConfigurationAuditedRepository;

    private BuildConfigSetRecordRepository buildConfigSetRecordRepository;

    private UserRepository userRepository;

    private SequenceHandlerRepository sequenceHandlerRepository;

    private TargetRepositoryRepository targetRepositoryRepository;

    public DefaultDatastore() {
    }

    @Inject
    public DefaultDatastore(ArtifactRepository artifactRepository,
                            BuildRecordRepository buildRecordRepository,
                            BuildConfigurationRepository buildConfigurationRepository,
                            BuildConfigurationAuditedRepository buildConfigurationAuditedRepository,
                            BuildConfigSetRecordRepository buildConfigSetRecordRepository,
                            UserRepository userRepository,
                            SequenceHandlerRepository sequenceHandlerRepository,
                            TargetRepositoryRepository targetRepositoryRepository) {
        this.artifactRepository = artifactRepository;
        this.buildRecordRepository = buildRecordRepository;
        this.buildConfigurationRepository = buildConfigurationRepository;
        this.buildConfigurationAuditedRepository = buildConfigurationAuditedRepository;
        this.buildConfigSetRecordRepository = buildConfigSetRecordRepository;
        this.userRepository = userRepository;
        this.sequenceHandlerRepository = sequenceHandlerRepository;
        this.targetRepositoryRepository = targetRepositoryRepository;
    }

    private static final String ARITFACT_ORIGIN_URL_IDENTIFIER_CONFLICT_MESSAGE = "Another artifact with the same originUrl but a different identifier already exists";
    private static final String ARITFACT_ORIGIN_URL_CHECKSUM_CONFLICT_MESSAGE = "Another artifact with the same originUrl but a different checksum already exists";

    @Override
    public Map<Artifact, String> checkForConflictingArtifacts(Collection<Artifact> artifacts) {
        Map<Artifact, String> conflicts = new HashMap<>();
        for (Artifact artifact : artifacts) {
            // Check for matching URL with different identifier or checksum
            if (artifact.getOriginUrl() != null) {
                Artifact artifactFromDb = artifactRepository.queryByPredicates(withOriginUrl(artifact.getOriginUrl()));
                if (artifactFromDb.getIdentifier().equals(artifact.getIdentifier())) {
                    conflicts.put(artifact, ARITFACT_ORIGIN_URL_IDENTIFIER_CONFLICT_MESSAGE);
                }
                if (artifactFromDb.getSha256().equals(artifact.getSha256())) {
                    conflicts.put(artifact, ARITFACT_ORIGIN_URL_CHECKSUM_CONFLICT_MESSAGE);
                }
            }
        }
        return conflicts;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public BuildRecord storeCompletedBuild(BuildRecord.Builder buildRecordBuilder) {
        BuildRecord buildRecord = buildRecordBuilder.build(true);
        logger.debug("Storing completed build {}.", buildRecord);
        if (logger.isTraceEnabled()) {
            logger.trace("Build Log: {}.", buildRecord.getBuildLog());
        }

        Map<String, TargetRepository> repositoriesCache = new HashMap<>();
        Map<Artifact.IdentifierSha256, Artifact> artifactCache = new HashMap<>();

        /** Built artifacts must be saved before the dependencies.
         *  In case an artifact is built and the dependency (re-downloaded),
         *  it must be linked to built artifacts repository.
         */
        logger.debug("Saving built artifacts ...");
        buildRecord.setBuiltArtifacts(saveArtifacts(buildRecord.getBuiltArtifacts(), repositoriesCache, artifactCache));

        logger.debug("Saving dependencies ...");
        buildRecord.setDependencies(saveArtifacts(buildRecord.getDependencies(), repositoriesCache, artifactCache));

        logger.debug("Done saving artifacts.");
        logger.trace("Saving build record {}.", buildRecord);
        buildRecord = buildRecordRepository.save(buildRecord);
        logger.debug("Build record {} saved.", buildRecord.getId());

        return buildRecord;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public BuildRecord storeRecordForNoRebuild(BuildRecord buildRecord) {
        logger.debug("Storing record for not required build {}.", buildRecord);

        buildRecord = buildRecordRepository.save(buildRecord);
        logger.debug("Build record {} saved.", buildRecord.getId());

        return buildRecord;
    }

    /**
     * Checks the given list against the existing database and creates a new list containing
     * artifacts which have been saved to or loaded from the database.
     *
     * @param artifacts of in-memory artifacts to either insert to the database or find the matching record in the db
     * @param artifactCache
     * @return Set of up to date JPA artifact entities
     */
    private Set<Artifact> saveArtifacts(Collection<Artifact> artifacts,
            Map<String, TargetRepository> repositoriesCache,
            Map<Artifact.IdentifierSha256, Artifact> artifactCache) {
        logger.debug("Saving {} artifacts.", artifacts.size());

        Set<Artifact> savedArtifacts = new HashSet<>();

        Set<Artifact.IdentifierSha256> artifactConstraints = new HashSet<>();

        for (Artifact artifact : artifacts) {
            artifactConstraints.add(new Artifact.IdentifierSha256(artifact.getIdentifier(), artifact.getSha256()));
        }

        Set<Artifact> artifactsInDb = null;
        if (artifactConstraints.size() > 0) {
            artifactsInDb = artifactRepository.withIdentifierAndSha256s(artifactConstraints);
        }

        if (artifactsInDb != null) {
            for (Artifact artifact : artifactsInDb) {
                logger.trace("Found in DB, adding to cache. Artifact {}", artifact);
                artifactCache.put(artifact.getIdentifierSha256(), artifact);
            }
        }

        for (Artifact artifact : artifacts) {
            TargetRepository targetRepository = artifact.getTargetRepository();
            linkTargetRepository(repositoriesCache, artifact, targetRepository);

            Artifact artifactFromDb;
            if (RepositoryType.GENERIC_PROXY.equals(targetRepository.getRepositoryType())) {
                artifactFromDb = saveHttpArtifact(artifact);
            } else {
                artifactFromDb = getOrSaveRepositoryArtifact(artifact, artifactCache);
            }

            savedArtifacts.add(artifactFromDb);
        }

        logger.debug("Artifacts saved: {}.", artifacts);
        return savedArtifacts;
    }

    private void linkTargetRepository(
            Map<String, TargetRepository> repositoriesCache,
            Artifact artifact,
            TargetRepository targetRepository) {

        String repositoriesCacheKey = targetRepository.getIdentifier() + "$$" + targetRepository.getRepositoryPath();
        TargetRepository targetRepositoryFromDb =
                repositoriesCache.computeIfAbsent(repositoriesCacheKey, k -> getOrSaveTargetRepository(targetRepository));

        artifact.setTargetRepository(targetRepositoryFromDb);
    }

    private TargetRepository getOrSaveTargetRepository(TargetRepository targetRepository) {
        logger.trace("Saving target repository {}.", targetRepository);
        TargetRepository targetRepositoryFromDb = targetRepositoryRepository
                .queryByIdentifierAndPath(targetRepository.getIdentifier(), targetRepository.getRepositoryPath());

        if (targetRepositoryFromDb == null) {
            logger.trace("Target repository is not in DB. Saving target repository {}.", targetRepository);
            targetRepositoryFromDb = targetRepositoryRepository.save(targetRepository);
            logger.debug("Target repository saved {}.", targetRepositoryFromDb);
        } else {
            logger.debug("Target repository already present in DB {}.", targetRepositoryFromDb);
        }

        return targetRepositoryFromDb;
    }

    private Artifact getOrSaveRepositoryArtifact(Artifact artifact, Map<Artifact.IdentifierSha256, Artifact> artifactCache) {
        logger.trace("Saving repository artifact {}.", artifact);
        Artifact artifactFromDb = artifactCache.get(artifact.getIdentifierSha256()); //TODO http artifacts

        if (artifactFromDb == null) {
            logger.trace("Artifact is not in DB. Saving artifact {}.", artifact);

            //Relation owner (BuildRecord) must be saved first, the relation is saved when the BR is saved
            artifact.setBuildRecords(Collections.emptySet());
            artifact.setDependantBuildRecords(Collections.emptySet());
            artifactFromDb = artifactRepository.save(artifact);

            logger.trace("Saved new artifact {}.", artifactFromDb);
        } else {
            logger.trace("Artifact already present in DB {}", artifactFromDb);
        }

        return artifactFromDb;
    }

    private Artifact saveHttpArtifact(Artifact artifact) {
        logger.trace("Saving http artifact {}.", artifact);
        // NONE OF THE ARTIFACTS CAN BE IN THE DB BECAUSE OF PER-BUILD REPOS
        logger.trace("Artifact is not in DB. Saving artifact {}.", artifact);

        //Relation owner (BuildRecord) must be saved first, the relation is saved when the BR is saved
        artifact.setBuildRecords(Collections.emptySet());
        artifact.setDependantBuildRecords(Collections.emptySet());
        Artifact artifactFromDb = artifactRepository.save(artifact);

        logger.trace("Saved new artifact {}.", artifactFromDb);

        return artifactFromDb;
    }

    @Override
    public User retrieveUserByUsername(String username) {
        return userRepository.queryByPredicates(withUserName(username));
    }

    @Override
    public void createNewUser(User user) {
        userRepository.save(user);
    }

    @Override
    public int getNextBuildRecordId() {

        Long nextId = sequenceHandlerRepository.getNextID(BuildRecord.SEQUENCE_NAME);
        logger.debug("Build Record nextId: {}", nextId);

        return nextId.intValue();
    }

    /**
     * Save a build config set record to the db.  This requires a new transaction to ensure that
     * the record is immediately committed to the database so that it's available to use by the
     * foreign keys set in the individual build records.
     */
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public BuildConfigSetRecord saveBuildConfigSetRecord(BuildConfigSetRecord buildConfigSetRecord) {
        return buildConfigSetRecordRepository.save(buildConfigSetRecord);
    }

    /**
     * Get the latest audited revision for the given build configuration ID
     *
     * @param buildConfigurationId
     * @return The latest revision of the given build configuration
     */
    @Override
    public BuildConfigurationAudited getLatestBuildConfigurationAudited(Integer buildConfigurationId) {
        List<BuildConfigurationAudited> buildConfigRevs = buildConfigurationAuditedRepository.findAllByIdOrderByRevDesc(buildConfigurationId);
        if (buildConfigRevs.isEmpty()) {
            logger.error("Did not find any BuildConfiguration revisions.");
            return null;
        }

        return buildConfigRevs.get(0);
    }

    @Override
    public BuildConfigurationAudited getLatestBuildConfigurationAuditedLoadBCDependencies(Integer buildConfigurationId) {
        BuildConfigurationAudited buildConfigurationAudited = getLatestBuildConfigurationAudited(buildConfigurationId);
        buildConfigurationAudited.setBuildConfiguration(buildConfigurationRepository.queryById(buildConfigurationAudited.getBuildConfiguration().getId()));
        buildConfigurationAudited.getBuildConfiguration().getIndirectDependencies();

        return buildConfigurationAudited;
    }

    @Override
    public BuildConfigSetRecord getBuildConfigSetRecordById(Integer buildConfigSetRecordId) {
        return buildConfigSetRecordRepository.queryById(buildConfigSetRecordId);
    }

    /**
     * Rebuild is required if Build Configuration has been modified or a dependency has been rebuilt since last successful build.
     */
    @Override
    public Set<BuildConfiguration> getBuildConfigurations(BuildConfigurationSet buildConfigurationSet) {
        return new HashSet<>(buildConfigurationRepository.queryWithPredicates(withBuildConfigurationSetId(buildConfigurationSet.getId())));
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public boolean requiresRebuild(BuildConfigurationAudited buildConfigurationAudited,
            boolean checkImplicitDependencies,
            boolean temporaryBuild) {
        IdRev idRev = buildConfigurationAudited.getIdRev();
        BuildRecord latestSuccessfulBuildRecord = buildRecordRepository.getLatestSuccessfulBuildRecord(idRev, temporaryBuild);
        if (latestSuccessfulBuildRecord == null) {
            logger.debug("Rebuild of buildConfiguration.idRev: {} required as there is no successful BuildRecord.", idRev);
            return true;
        }
        if (!isLatestSuccessBRFromThisBCA(buildConfigurationAudited, temporaryBuild)) {
            return true;
        }
        if (checkImplicitDependencies) {
            boolean rebuild = hasARebuiltImplicitDependency(latestSuccessfulBuildRecord, temporaryBuild);
            logger.debug("Implicit dependency check for rebuild of buildConfiguration.idRev: {} required: {}.", idRev, rebuild);
            if (rebuild) {
                return rebuild;
            }
        }
        // check explicit dependencies
        Set<BuildConfiguration> dependencies = buildConfigurationAudited.getBuildConfiguration().getDependencies();
        boolean rebuild = hasARebuiltExplicitDependency(latestSuccessfulBuildRecord, dependencies, temporaryBuild);
        logger.debug("Explicit dependency check for rebuild of buildConfiguration.idRev: {} required: {}.", idRev, rebuild);
        return rebuild;
    }

    @Deprecated
    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public boolean requiresRebuild(BuildTask task) {
        return requiresRebuild(task.getBuildConfigurationAudited(),
                task.getBuildOptions().isImplicitDependenciesCheck(),
                task.getBuildOptions().isTemporaryBuild());
    }

    /**
     * @return true when the latest success {@link BuildRecord} of {@link BuildConfiguration} is build from this {@link BuildConfigurationAudited}
     * if a build is persistent {@param temporaryBuild} , then it skips temporary build during evaluation
     */
    private boolean isLatestSuccessBRFromThisBCA(BuildConfigurationAudited buildConfigurationAudited, boolean temporaryBuild) {
        BuildRecord latestSuccessfulBuildRecord = buildRecordRepository.getLatestSuccessfulBuildRecord(buildConfigurationAudited.getId(), temporaryBuild);
        if (latestSuccessfulBuildRecord == null) {
            logger.warn("The check should be done once it's known there is a successful BuildRecord. There is no successful BuildRecord for BuildConfigurationAudited {}.", buildConfigurationAudited.getIdRev());
        }
        if (latestSuccessfulBuildRecord.getBuildConfigurationAuditedIdRev().equals(buildConfigurationAudited.getIdRev())) {
            return true;
        } else {
            logger.debug("Last successful BuildRecord id {} is not from this BuildConfigurationAudited idRev {}.", latestSuccessfulBuildRecord.getId(), buildConfigurationAudited.getIdRev());
            return false;
        }
    }

    /**
     * Check is some of the dependencies from the previous build were rebuild.
     * Checking is done based on captured dependencies which are stored in the Build Record.
     */
    private boolean hasARebuiltImplicitDependency(BuildRecord latestSuccessfulBuildRecord, boolean temporaryBuild) {
        Collection<BuildRecord> lastBuiltFrom = getRecordsUsedFor(latestSuccessfulBuildRecord);
        return lastBuiltFrom.stream()
                .anyMatch(br -> hasNewerVersion(br, temporaryBuild));
    }

    /**
     * Check is some of the dependencies defined on BuildConfiguration has newer version.
     */
    private boolean hasARebuiltExplicitDependency(BuildRecord latestSuccessfulBuildRecord, Set<BuildConfiguration> dependencies, boolean temporaryBuild) {
        for (BuildConfiguration dependencyBuildConfiguration : dependencies) {
            BuildRecord dependencyLatestSuccessfulBuildRecord = buildRecordRepository.getLatestSuccessfulBuildRecord(dependencyBuildConfiguration.getId(), temporaryBuild);
            if (dependencyLatestSuccessfulBuildRecord == null) {
                return true;
            }
            boolean newer = dependencyLatestSuccessfulBuildRecord.getEndTime().after(latestSuccessfulBuildRecord.getEndTime());
            if (newer) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if there is newer successful BuildRecord for the same buildConfiguration.idRev
     */
    private boolean hasNewerVersion(BuildRecord buildRecord, boolean temporaryBuild) {
        BuildRecord latestSuccessfulBuildRecord = buildRecordRepository.getLatestSuccessfulBuildRecord(buildRecord.getBuildConfigurationAuditedIdRev(), temporaryBuild);
        if (latestSuccessfulBuildRecord == null) {
            logger.error("Something went wrong, the buildRecord should be successful (ot this lattest or the BuildRecord that produced artifacts.).");
        }
        return !buildRecord.getId().equals(latestSuccessfulBuildRecord.getId());
    }

    /**
     * @return BuildRecords that produced captured dependencies artifacts
     */
    private Collection<BuildRecord> getRecordsUsedFor(BuildRecord record) {
        return ofNullableCollection(record.getDependencies())
                .stream()
                .map(Artifact::getBuildRecords)
                .collect(toFlatList());
    }

}


/**
 * 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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.jboss.pnc.common.json.moduleconfig.BpmModuleConfig;
import org.jboss.pnc.rest.restmodel.bpm.BpmNotificationRest;
import org.jboss.pnc.spi.exception.CoreException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;

import static java.util.Objects.requireNonNull;

/**
 * Parent type of all BPM tasks. Task is a representation of
 * BPM process execution, keeps track of the remote process instance
 * and handles notifications.
 * Class implements Comparable based on the taskId,
 * so they can be sorted at REST endpoint.
 * BPM tasks do not have to be thread safe.
 *
 * @author Jakub Senko
 */
@EqualsAndHashCode(of = "taskId")
public abstract class BpmTask implements Comparable<BpmTask> {

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

    protected static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * This is an internal identifier, not one provided by BPM server.
     */
    private Integer taskId;

    private Long processInstanceId;

    @Getter
    private final List<BpmNotificationRest> events = new ArrayList<>();

    private String processName;

    protected BpmModuleConfig config;

    private ConcurrentMap<BpmEventType, List<Consumer<?>>> listeners = new ConcurrentHashMap<>();

    /**
     * Users OAuth token used to authenticate requests on remote services
     */
    private String accessToken;

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getProcessName() {
        return processName;
    }

    /* package */ void setProcessName(String processName) {
        this.processName = processName;
    }

    /**
     * Get the name of the BPM process for this task.
     * Warning: The process MUST be asynchronous so it
     * does not block BPM manager.
     */
    protected abstract String getProcessId();

    /**
     * Provide parameters to the BPM process.
     * The JSON representation of this map will be available
     * in the BPM process as "processParameters" variable.
     * Some parameters, such as "taskId" are automatically added,
     * not into the "processParameters" variable, but 'one level higher'
     * and directly accessible
     * from process variables.
     *
     * @return a map of process parameters
     * @throws CoreException
     */
    protected abstract Serializable getProcessParameters() throws CoreException;

    /* package */ void setBpmConfig(BpmModuleConfig config) {
        this.config = config;
    }

    public Integer getTaskId() {
        return taskId;
    }

    /* package */ void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public Long getProcessInstanceId() {
        return processInstanceId;
    }

    /* package */ void setProcessInstanceId(Long processInstanceId) {
        this.processInstanceId = processInstanceId;
    }

    /**
     * Listen to notifications from BPM process for this task.
     *
     * @param eventType event to follow
     */
    public <T extends BpmNotificationRest> void addListener(BpmEventType eventType, Consumer<T> listener) {
        List<Consumer<?>> consumers = listeners.computeIfAbsent(eventType, (k) -> new ArrayList<>());
        consumers.add(listener);
    }

    public <T extends BpmNotificationRest> void notify(BpmEventType eventType, T data) {
        List<Consumer<?>> listeners = this.listeners.computeIfAbsent(eventType, (k) -> new ArrayList<>());
        log.debug("will notify bpm listeners for eventType: {}, matching listeners: {}, all listeners: {}", eventType, listeners, this.listeners);
        listeners.forEach(listener -> {
            // Cast is OK because there is no unchecked method declaration to put wrong types
            ((Consumer<T>) listener).accept(data);
        });
        events.add(data);
    }

    /**
     * Extend process parameters from the task with additional useful information,
     * such as pncBaseUrl and taskId, needed for notifications.
     * Before use, taskId MUST be assigned.
     *
     * @throws CoreException
     */
    Map<String, Object> getExtendedProcessParameters() throws CoreException {
        Serializable processParameters = getProcessParameters();
        requireNonNull(processParameters);
        Map<String, Object> actualParameters = new HashMap<>();
        try {
            actualParameters.put("processParameters", MAPPER.writeValueAsString(processParameters));
        } catch (JsonProcessingException e) {
            throw new CoreException("Could not serialize process processParameters '" +
                    processParameters + "'.", e);
        }

        //global not process related parameters
        actualParameters.put("taskId", taskId);
        actualParameters.put("usersAuthToken", accessToken);

        return actualParameters;
    }

    @Override
    public int compareTo(BpmTask other) {
        requireNonNull(other);
        if (taskId == null)
            return other.getTaskId() == null ? 0 : -1;
        if (other.getTaskId() == null)
            return 1;
        return taskId.compareTo(other.getTaskId());
    }

    @Override
    public String toString() {
        return "BpmTask{" +
                "taskId=" + taskId +
                ", processInstanceId=" + processInstanceId +
                ", events=" + events +
                ", processName='" + processName + '\'' +
                ", config=" + config +
                ", listeners=" + listeners +
                ", accessToken='***'" +
                '}';
    }
}


/**
 * 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.builder;

import org.jboss.pnc.common.logging.MDCUtils;
import org.jboss.pnc.coordinator.builder.datastore.DatastoreAdapter;
import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildConfiguration;
import org.jboss.pnc.model.BuildConfigurationAudited;
import org.jboss.pnc.model.BuildConfigurationSet;
import org.jboss.pnc.model.ProductMilestone;
import org.jboss.pnc.model.User;
import org.jboss.pnc.model.utils.ContentIdentityManager;
import org.jboss.pnc.spi.BuildOptions;
import org.jboss.pnc.spi.coordinator.BuildSetTask;
import org.jboss.pnc.spi.coordinator.BuildTask;
import org.jboss.pnc.spi.datastore.DatastoreException;
import org.jboss.pnc.spi.exception.CoreException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

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

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

    private DatastoreAdapter datastoreAdapter; //TODO remove datastore dependency

    private Date temporaryBuildExpireDate;

    public BuildTasksInitializer(DatastoreAdapter datastoreAdapter, Date temporaryBuildExpireDate) {
        this.datastoreAdapter = datastoreAdapter;
        this.temporaryBuildExpireDate = temporaryBuildExpireDate;
    }

    public BuildSetTask createBuildSetTask(BuildConfigurationAudited buildConfigurationAudited,
                                           User user,
                                           BuildOptions buildOptions,
                                           Supplier<Integer> buildTaskIdProvider,
                                           Set<BuildTask> submittedBuildTasks) {
      BuildSetTask buildSetTask =
                BuildSetTask.Builder.newBuilder()
                        .buildOptions(buildOptions)
                        .startTime(new Date()).build();

        Set<BuildConfigurationAudited> toBuild = new HashSet<>();
        collectBuildTasks(buildConfigurationAudited, buildOptions, toBuild);
        log.debug("Collected build tasks for the BuildConfigurationAudited: {}. Collected: {}.",
                buildConfigurationAudited, toBuild.stream().map(BuildConfigurationAudited::toString).collect(Collectors.joining(", ")));

        fillBuildTaskSet(
                buildSetTask,
                user,
                buildTaskIdProvider,
                buildConfigurationAudited.getBuildConfiguration().getCurrentProductMilestone(),
                toBuild,
                submittedBuildTasks,
                buildOptions);

        return buildSetTask;
    }

    private void collectBuildTasks(BuildConfigurationAudited buildConfigurationAudited, BuildOptions buildOptions, Set<BuildConfigurationAudited> toBuild) {
        log.debug("will create build tasks for scope: {} and configuration: {}", buildOptions, buildConfigurationAudited);
        Set<BuildConfiguration> visited = new HashSet<>();
        if (toBuild.contains(buildConfigurationAudited)) {
            return;
        }

        toBuild.add(buildConfigurationAudited);
        if (buildOptions.isBuildDependencies()) {
            buildConfigurationAudited.getBuildConfiguration().getDependencies().forEach(dependencyConfiguration ->
                    collectDependentConfigurations(
                            dependencyConfiguration,
                            datastoreAdapter.getLatestBuildConfigurationAuditedInitializeBCDependencies(dependencyConfiguration.getId()),
                            toBuild,
                            visited,
                            buildOptions.isImplicitDependenciesCheck(),
                            buildOptions.isForceRebuild(),
                            buildOptions.isTemporaryBuild()));
        }
    }

    /**
     * Collects all BuildConfigurationAudited entities, that needs to be built.
     *
     * @param buildConfiguration Current BuildConfiguration used to resolve dependencies.
     * @param buildConfigurationAudited Specific revision of a BuildConfiguration (passed as first parameter) to be potentially built
     * @param toBuild Set of BuildConfigurationAudited entities planned to be built
     * @param visited Set of BuildConfigurations, which were already evaluated, if should be built
     * @param checkImplicitDependencies
     * @return Returns true, if the buildConfiguration should be rebuilt, otherwise returns false.
     */
    private boolean collectDependentConfigurations(BuildConfiguration buildConfiguration,
            BuildConfigurationAudited buildConfigurationAudited,
            Set<BuildConfigurationAudited> toBuild,
            Set<BuildConfiguration> visited,
            boolean checkImplicitDependencies,
            boolean forceRebuild,
            boolean temporaryBuild) {
        if (visited.contains(buildConfiguration)) {
            return toBuild.contains(buildConfigurationAudited);
        }
        visited.add(buildConfiguration);

        boolean requiresRebuild = forceRebuild || datastoreAdapter.requiresRebuild(buildConfigurationAudited, checkImplicitDependencies, temporaryBuild);
        for (BuildConfiguration dependency : buildConfiguration.getDependencies()) {
            boolean dependencyRequiresRebuild = collectDependentConfigurations(dependency,
                    datastoreAdapter.getLatestBuildConfigurationAuditedInitializeBCDependencies(dependency.getId()),
                    toBuild,
                    visited,
                    checkImplicitDependencies,
                    forceRebuild,
                    temporaryBuild);

            requiresRebuild = requiresRebuild || dependencyRequiresRebuild;

        }
        log.debug("Configuration {} requires rebuild: ", buildConfiguration.getId(), requiresRebuild);
        if (requiresRebuild) {
            toBuild.add(buildConfigurationAudited);
        }

        return requiresRebuild;
    }

    /**
     * Create a BuildSetTask of latest revisions of BuildConfigurations contained in the BuildConfigurationSet
     *
     * @param buildConfigurationSet BuildConfigurationSet to be built
     * @param user A user, who triggered the build
     * @param buildOptions Build options
     * @param buildTaskIdProvider Provider to get build task ID
     * @param submittedBuildTasks Already submitted build tasks
     * @return Prepared BuildSetTask
     * @throws CoreException Thrown if the BuildConfigSetRecord cannot be stored
     */
    public BuildSetTask createBuildSetTask(
            BuildConfigurationSet buildConfigurationSet,
            User user,
            BuildOptions buildOptions,
            Supplier<Integer> buildTaskIdProvider,
            Set<BuildTask> submittedBuildTasks) throws CoreException {

        return createBuildSetTask(buildConfigurationSet, Collections.emptyMap(), user, buildOptions,
                buildTaskIdProvider, submittedBuildTasks);
    }

    /**
     * Create a BuildSetTask of BuildConfigurations contained in the BuildConfigurationSet.
     *
     * A specific revision of the BuildConfigurations contained in the set is used,
     * if it's available in the buildConfigurationAuditedsMap parameter.
     * If it's not available, latest revision of the BuildConfiguration is used.
     *
     * @param buildConfigurationSet BuildConfigurationSet to be built
     * @param buildConfigurationAuditedsMap A map BuildConfiguration::id:BuildConfigurationAudited of specific revisions of BuildConfigurations contained in the buildConfigurationSet
     * @param user A user, who triggered the build
     * @param buildOptions Build options
     * @param buildTaskIdProvider Provider to get build task ID
     * @param submittedBuildTasks Already submitted build tasks
     * @return Prepared BuildSetTask
     * @throws CoreException Thrown if the BuildConfigSetRecord cannot be stored
     */
    public BuildSetTask createBuildSetTask(
            BuildConfigurationSet buildConfigurationSet,
            Map<Integer, BuildConfigurationAudited> buildConfigurationAuditedsMap,
            User user,
            BuildOptions buildOptions,
            Supplier<Integer> buildTaskIdProvider,
            Set<BuildTask> submittedBuildTasks) throws CoreException {
        BuildSetTask buildSetTask = initBuildSetTask(buildConfigurationSet, user, buildOptions);

        Set<BuildConfigurationAudited> buildConfigurationAuditeds = new HashSet<>();
        for (BuildConfiguration buildConfiguration : datastoreAdapter.getBuildConfigurations(buildConfigurationSet)) {
            BuildConfigurationAudited buildConfigurationAudited = buildConfigurationAuditedsMap.get(buildConfiguration.getId());
            if(buildConfigurationAudited == null) {
                buildConfigurationAudited = datastoreAdapter.getLatestBuildConfigurationAuditedInitializeBCDependencies(buildConfiguration.getId());
            }
            buildConfigurationAuditeds.add(buildConfigurationAudited);
        }

        // initializeBuildTasksInSet
        log.debug("Initializing BuildTasks In Set for BuildConfigurationAuditeds: {}.",
                buildConfigurationAuditeds.stream().map(bc ->bc.toString()).collect(Collectors.joining("; ")));
        fillBuildTaskSet(
                buildSetTask,
                user,
                buildTaskIdProvider,
                buildConfigurationSet.getCurrentProductMilestone(),
                buildConfigurationAuditeds,
                submittedBuildTasks,
                buildOptions);
        return buildSetTask;
    }

    private BuildSetTask initBuildSetTask(BuildConfigurationSet buildConfigurationSet, User user, BuildOptions buildOptions) throws CoreException {
        BuildConfigSetRecord buildConfigSetRecord = BuildConfigSetRecord.Builder.newBuilder()
                .buildConfigurationSet(buildConfigurationSet)
                .user(user)
                .startTime(new Date())
                .status(org.jboss.pnc.enums.BuildStatus.BUILDING)
                .temporaryBuild(buildOptions.isTemporaryBuild())
                .build();

        final BuildConfigSetRecord configSetRecord;
        try {
            configSetRecord = saveBuildConfigSetRecord(buildConfigSetRecord);
        } catch (DatastoreException e) {
            log.error("Failed to store build config set record: " + e);
            throw new CoreException(e);
        }

        return BuildSetTask.Builder.newBuilder()
                .buildConfigSetRecord(configSetRecord)
                .buildOptions(buildOptions).build();
    }

    /**
     * Creates build tasks and sets up the appropriate dependency relations
     *
     * @param buildSetTask The build set task which will contain the build tasks.  This must already have
     *                     initialized the BuildConfigSet, BuildConfigSetRecord, Milestone, etc.
     */
    private void fillBuildTaskSet(
            BuildSetTask buildSetTask,
            User user,
            Supplier<Integer> buildTaskIdProvider,
            ProductMilestone productMilestone,
            Set<BuildConfigurationAudited> toBuild,
            Set<BuildTask> alreadySubmittedBuildTasks,
            BuildOptions buildOptions) {
        for (BuildConfigurationAudited buildConfigAudited : toBuild) {
            Optional<BuildTask> taskOptional = alreadySubmittedBuildTasks.stream()
                    .filter(bt -> bt.getBuildConfigurationAudited().equals(buildConfigAudited))
                    .findAny();

            BuildTask buildTask;
            if (taskOptional.isPresent()) {
                buildTask = taskOptional.get();
                log.debug("Linking BuildConfigurationAudited {} to existing task {}.", buildConfigAudited, buildTask);
            } else {
                int buildId = buildTaskIdProvider.get();
                String buildContentId = ContentIdentityManager.getBuildContentId(buildId);
                MDCUtils.addBuildContext(buildContentId, buildOptions.isTemporaryBuild(), temporaryBuildExpireDate);

                buildTask = BuildTask.build(
                        buildConfigAudited,
                        buildSetTask.getBuildOptions(),
                        user,
                        buildId,
                        buildSetTask,
                        buildSetTask.getStartTime(),
                        productMilestone,
                        buildContentId);
                log.debug("Created new buildTask {} for BuildConfigurationAudited {}.", buildTask, buildConfigAudited);
            }

            buildSetTask.addBuildTask(buildTask);
        }

        // Loop again to set dependencies
        for (BuildTask buildTask : buildSetTask.getBuildTasks()) {
            for (BuildTask checkDepBuildTask : buildSetTask.getBuildTasks()) {
                if (buildTask.hasDirectConfigDependencyOn(checkDepBuildTask)) {
                    buildTask.addDependency(checkDepBuildTask);
                }
            }
        }
    }

    /**
     * Save the build config set record using a single thread for all db operations.
     * This ensures that database operations are done in the correct sequence, for example
     * in the case of a build config set.
     *
     * @param buildConfigSetRecord The bcs record to save
     * @return The build config set record which has been saved to the db
     * @throws org.jboss.pnc.spi.datastore.DatastoreException if there is a db problem which prevents this record being stored
     */
    private BuildConfigSetRecord saveBuildConfigSetRecord(BuildConfigSetRecord buildConfigSetRecord) throws DatastoreException {
        return datastoreAdapter.saveBuildConfigSetRecord(buildConfigSetRecord);
    }
}


/**
 * 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.provider;

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.moduleprovider.PncConfigProvider;
import org.jboss.pnc.common.util.UrlUtils;
import org.jboss.pnc.model.Artifact;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.model.TargetRepository;
import org.jboss.pnc.rest.provider.collection.CollectionInfo;
import org.jboss.pnc.rest.provider.collection.CollectionInfoCollector;
import org.jboss.pnc.rest.restmodel.ArtifactRest;
import org.jboss.pnc.rest.validation.exceptions.RestValidationException;
import org.jboss.pnc.spi.datastore.repositories.ArtifactRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildRecordRepository;
import org.jboss.pnc.spi.datastore.repositories.PageInfoProducer;
import org.jboss.pnc.spi.datastore.repositories.SortInfoProducer;
import org.jboss.pnc.spi.datastore.repositories.api.RSQLPredicateProducer;
import org.jboss.pnc.spi.datastore.repositories.api.SortInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.Stateless;
import javax.inject.Inject;

import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.pnc.enums.RepositoryType;
import static org.jboss.pnc.common.util.StreamHelper.nullableStreamOf;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withBuildRecordId;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withDependantBuildRecordId;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withMd5;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withSha1;
import static org.jboss.pnc.spi.datastore.predicates.ArtifactPredicates.withSha256;

@Stateless
public class ArtifactProvider extends AbstractProvider<Artifact, ArtifactRest> {

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

    /**
     * Used only in a deprecated method
     */
    @Deprecated
    private BuildRecordRepository buildRecordRepository;
    private IndyRepoDriverModuleConfig moduleConfig;

    public ArtifactProvider() {
    }

    @Inject
    public ArtifactProvider(ArtifactRepository artifactRepository, RSQLPredicateProducer rsqlPredicateProducer,
            SortInfoProducer sortInfoProducer, PageInfoProducer pageInfoProducer, BuildRecordRepository buildRecordRepository,
            Configuration configuration) {
        super(artifactRepository, rsqlPredicateProducer, sortInfoProducer, pageInfoProducer);
        this.buildRecordRepository = buildRecordRepository;

        try {
            moduleConfig = configuration.getModuleConfig(new PncConfigProvider<>(IndyRepoDriverModuleConfig.class));
        } catch (ConfigurationParseException e) {
            logger.error("Cannot read configuration", e);
        }
    }

    /**
     * @deprecated is uses in-memory sort and order
     */
    @Deprecated
    public CollectionInfo<ArtifactRest> getAllForBuildRecord(int pageIndex, int pageSize, String sortingRsql, String query,
            int buildRecordId) {
        BuildRecord buildRecord = buildRecordRepository.queryById(buildRecordId);

        Set<Artifact> fullArtifactList = new HashSet<>();
        fullArtifactList.addAll(buildRecord.getBuiltArtifacts());
        fullArtifactList.addAll(buildRecord.getDependencies());

        return filterAndSort(pageIndex, pageSize, sortingRsql, query,
                ArtifactRest.class, fullArtifactList);
    }

    /**
     * Lookups built artifacts for the specified BuildRecord
     *
     * @return Returns requested artifacts or empty collection if BuildRecord with the specified ID doesn't exists
     */
    public CollectionInfo<ArtifactRest> getBuiltArtifactsForBuildRecord(int pageIndex, int pageSize, String sortingRsql, String query,
            int buildRecordId) {
        return queryForCollection(pageIndex, pageSize, sortingRsql, query, withBuildRecordId(buildRecordId));
    }

    @Deprecated
    private <DTO, Model> CollectionInfo<ArtifactRest> filterAndSort(int pageIndex, int pageSize, String sortingRsql, String query,
                                                       Class<ArtifactRest> selectingClass, Set<Artifact> artifacts) {
        Predicate<ArtifactRest> queryPredicate = rsqlPredicateProducer.getStreamPredicate(selectingClass, query);
        SortInfo sortInfo = sortInfoProducer.getSortInfo(sortingRsql);

        Stream<ArtifactRest> filteredStream = nullableStreamOf(artifacts)
                .map(artifact -> new ArtifactRest(artifact, getDeployUrl(artifact), getPublicUrl(artifact)))
                .filter(queryPredicate).sorted(sortInfo.getComparator());
        List<ArtifactRest> filteredList = filteredStream.collect(Collectors.toList());

        return filteredList.stream()
                .skip(pageIndex * pageSize)
                .limit(pageSize).collect(new CollectionInfoCollector<>(pageIndex, pageSize, (filteredList.size() + pageSize -1)/pageSize));
    }

    private String getDeployUrl(Artifact artifact) {
        RepositoryType repositoryType = artifact.getTargetRepository().getRepositoryType();
        if (repositoryType.equals(RepositoryType.MAVEN) || repositoryType.equals(RepositoryType.NPM)) {
            if (artifact.getDeployPath() == null || artifact.getDeployPath().equals("")) {
                return "";
            } else {
                try {
                    if (repositoryType.equals(RepositoryType.MAVEN)) {
                        return UrlUtils.buildUrl(moduleConfig.getInternalRepositoryMvnPath(),
                                artifact.getTargetRepository().getRepositoryPath(),
                                artifact.getDeployPath());
                    } else {
                        return UrlUtils.buildUrl(moduleConfig.getInternalRepositoryNpmPath(),
                                artifact.getTargetRepository().getRepositoryPath(),
                                artifact.getDeployPath());
                    }
                } catch (MalformedURLException e) {
                    logger.error("Cannot construct internal artifact URL.", e);
                    return null;
                }
            }
        } else {
            return artifact.getOriginUrl();
        }
    }

    private String getPublicUrl(Artifact artifact) {
        RepositoryType repositoryType = artifact.getTargetRepository().getRepositoryType();
        if (repositoryType.equals(RepositoryType.MAVEN) || repositoryType.equals(RepositoryType.NPM)) {
            if (artifact.getDeployPath() == null || artifact.getDeployPath().equals("")) {
                return "";
            } else {
                try {
                    if (repositoryType.equals(RepositoryType.MAVEN)) {
                        return UrlUtils.buildUrl(moduleConfig.getExternalRepositoryMvnPath(),
                                artifact.getTargetRepository().getRepositoryPath(),
                                artifact.getDeployPath());
                    } else {
                        return UrlUtils.buildUrl(moduleConfig.getExternalRepositoryNpmPath(),
                                artifact.getTargetRepository().getRepositoryPath(),
                                artifact.getDeployPath());
                    }
                } catch (MalformedURLException e) {
                    logger.error("Cannot construct public artifact URL.", e);
                    return null;
                }
            }
        } else {
            return artifact.getOriginUrl();
        }
    }

    public CollectionInfo<ArtifactRest> getDependencyArtifactsForBuildRecord(int pageIndex, int pageSize, String sortingRsql, String query,
            int buildRecordId) {
        return queryForCollection(pageIndex, pageSize, sortingRsql, query, withDependantBuildRecordId(buildRecordId));
    }

    public CollectionInfo<ArtifactRest> getAll(int pageIndex, int pageSize, String sortingRsql, String query,
            Optional<String> sha256, Optional<String> md5, Optional<String> sha1) {
        return queryForCollection(pageIndex, pageSize, sortingRsql, query, withSha256(sha256),
                withMd5(md5),withSha1(sha1));
    }

    @Override
    public ArtifactRest getSpecific(Integer id) {
        Artifact artifact = repository.queryById(id);
        if (artifact != null) {
            return new ArtifactRest(artifact, getDeployUrl(artifact), getPublicUrl(artifact));
        }
        return null;
    }

    @Override
    public Integer store(ArtifactRest restEntity) throws RestValidationException {
        throw new UnsupportedOperationException("Direct artifact manipulation is not available.");
    }

    @Override
    public void update(Integer id, ArtifactRest restEntity) throws RestValidationException {
        throw new UnsupportedOperationException("Direct artifact manipulation is not available.");
    }

    @Override
    public void delete(Integer id) throws RestValidationException {
        throw new UnsupportedOperationException("Direct artifact manipulation is not available.");
    }

    @Override
    protected Function<? super Artifact, ? extends ArtifactRest> toRESTModel() {
        return (artifact) -> new ArtifactRest(artifact, getDeployUrl(artifact), getPublicUrl(artifact));
    }

    @Override
    protected Function<? super ArtifactRest, ? extends Artifact> toDBModel() {
        throw new UnsupportedOperationException();
    }
}


/**
 * 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 org.jboss.util.StringPropertyReplacer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * Created by Matej Lazar on 2015-01-01.
 */
public class StringUtils {

    /**
     * Replace environment variables in string.
     * Environment variables are expected in format ${env.ENV_PROPERTY},
     * where "env" is static prefix and ENV_PROPERTY is name of environment property.
     *
     * @param configString String with environment variables
     * @return String with replaced environment variables
     */
    public static String replaceEnv(String configString) {
        Properties properties = new Properties();

        for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
            properties.put("env." + entry.getKey(), entry.getValue());
        }
        String replaced = StringPropertyReplacer.replaceProperties(configString, properties);
        //remove env placeholders that were not replaced with values
        return replaced.replaceAll("\\$\\{env\\..+\\}", "");
    }

    /**
     * Check if the given string is null or contains only whitespace characters.
     *
     * @param string String to check for non-whitespace characters
     * @return boolean True if the string is null, empty, or contains only whitespace (empty when trimmed).
     * Otherwise return false.
     */
    public static boolean isEmpty(String string) {
        if (string == null ) {
            return true;
        }
        return string.trim().isEmpty();
    }

    public static String toCVS(Set<Integer> buildRecordSetIds) {
        StringJoiner joiner = new StringJoiner(",");
        buildRecordSetIds.forEach(el -> joiner.add(el.toString()));
        return joiner.toString();
    }

    /**
     * Remove ending slash if present and return the string without ending slash
     *
     * @param string
     * @return
     */
    public static String stripEndingSlash(String string) {
        if (string == null) {
            return null;
        }
        if (string.endsWith("/")) {
            string = string.substring(0, string.length() - 1);
        }
        return string;
    }

    /**
     * Remove ending slash if present and return the string without ending slash
     *
     * @param string
     * @return
     */
    public static String stripTrailingSlash(String string) {
        if (string == null) {
            return null;
        }
        if (string.startsWith("/")) {
            string = string.substring(1, string.length());
        }
        return string;
    }

    /**
     * Adds ending slash if it is not present.
     *
     * @param string
     * @return
     */
    public static String addEndingSlash(String string) {
        if (string == null) {
            return null;
        }
        if (!string.endsWith("/")) {
            string += "/";
        }
        return string;
    }

    public static String trim(String string, int maxLength) {
        if (string == null) {
            return null;
        }

        if (string.length() > maxLength) {
            return string.substring(0, maxLength - 1) + "...";
        } else {
            return string;
        }
    }

    public static String stripSuffix(String string, String suffix) {
        if (string == null) {
            return null;
        }
        if (suffix == null) {
            return string;
        }

        if (string.endsWith(suffix)) {
            return string.substring(0, string.length() - suffix.length());
        } else {
            return string;
        }
    }

    public static String stripProtocol(String url) {
        if (url == null) {
            return null;
        }

        String protocolDivider = "://";
        int protocolDividerIndex = url.indexOf(protocolDivider);

        if (protocolDividerIndex > -1) {
            return url.substring(protocolDividerIndex + protocolDivider.length());
        } else {
            return url;
        }
    }

    public static void readStream(InputStream inputStream, Charset charset, ArrayDeque<String> lines, int maxMessageSize, Consumer<String> droppedLinesConsumer) throws
            IOException {
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset);
        BufferedReader reader=new BufferedReader(inputStreamReader);

        int messageSize = 0;
        while (true) {
            String line = reader.readLine();
            if (line == null) {
                break;
            }
            if (maxMessageSize > -1) {
                messageSize += line.length();
                while (messageSize > maxMessageSize) {
                    String removedLine = lines.removeFirst();
                    messageSize -= removedLine.length();
                    droppedLinesConsumer.accept(removedLine);
                }
            }
            lines.add(line);
        }
    }

    /**
     * Parse comma separated string to Integer array.
     * @return An empty array when the string parameter is empty or null.
     */
    public static Integer[] deserializeInt(String string) {
        if (string == null) {
            return new Integer[0];
        }
        return Arrays.stream(string.split(","))
                .filter(s -> !s.equals(""))
                .map(Integer::parseInt).toArray(Integer[]::new);
    }

    /**
     * Serialize Integer array to comma separated string.
     * @return An empty string when the Integer array parameter is empty or null.
     */
    public static String serializeInt(Integer[] integers) {
        if (integers == null) {
            return "";
        }
        return Arrays.stream(integers)
                .map(i -> Integer.toString(i)).collect(Collectors.joining(","));
    }

    public static Integer parseInt(String s, int defaultValue) {
        if (isEmpty(s)) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }
}


/**
 * 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.gerrit;

import javax.enterprise.context.ApplicationScoped;
import javax.validation.constraints.NotNull;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;

/**
 * Class to deal with Gerrit information
 */
@ApplicationScoped
public class Gerrit {

    private static final String GERRIT_DOWNLOAD_URL_TEMPLATE = "https://{0}/gerrit/gitweb?p={1};a=snapshot;h={2};sf=tgz";
    private static final String GERRIT_GITWEB_SHORTLOG_URL_TEMPLATE = "https://{0}/gerrit/gitweb?p={1};a=shortlog;h={2}";
    private static final String GERRIT_GITWEB_COMMIT_URL_TEMPLATE = "https://{0}/gerrit/gitweb?p={1};a=commit;h={2}";
    private static final String GERRIT_GITWEB_URL_TEMPLATE = "https://{0}/gerrit/gitweb?p={1};a=summary";

    /**
     * Generate a download url for a Gerrit snapshot using the gerrit url of the project and the ref
     *
     * @param gerritUrl URL has to be the 'git clone' link (either http or git+ssh)
     * @param ref The ref to generate a snapshot. It can be a sha, branch, tag. If left empty, master is used
     *
     * @return Download url
     * @throws GerritException thrown if the Gerrit Url is not valid
     */
    public String generateDownloadUrlWithGerritGitweb(@NotNull String gerritUrl, @NotNull String ref) throws GerritException {

        return MessageFormat.format(GERRIT_DOWNLOAD_URL_TEMPLATE, getHost(gerritUrl), getProject(gerritUrl), ref);
    }

    /**
     * Generate an https URL generated from the 'git clone' Gerrit url. If ref is specified, URL will point to it
     * The url generated points to the git log based on the ref
     *
     * Useful to list the repository's Gerrit gitweb if we don't know the ref
     *
     * @param gerritUrl 'git clone' gerrit url. Can be http or git+ssh
     *
     * @param ref if not null or empty, this is used to generate a Gerrit gitweb url to the ref
     *
     * @return Gerrit Gitweb URL
     */
    public String generateGerritGitwebLogUrl(@NotNull String gerritUrl, String ref) throws GerritException {

        String host = getHost(gerritUrl);
        String project = getProject(gerritUrl);

        if (ref == null || ref.isEmpty()) {
            return MessageFormat.format(GERRIT_GITWEB_URL_TEMPLATE, host, project);
        } else {
            return MessageFormat.format(GERRIT_GITWEB_SHORTLOG_URL_TEMPLATE, host, project, ref);
        }
    }

    /**
     * Generate an https URL generated from the 'git clone' Gerrit url.
     * The url generated points to the specific commit based on the ref. If ref is a branch,
     * it'll point to the latest commit
     *
     * @param gerritUrl 'git clone' gerrit url. Can be http or git+ssh
     *
     * @param ref this is used to generate a Gerrit gitweb url to the ref
     *
     * @return Gerrit Gitweb URL
     */
    public String generateGerritGitwebCommitUrl(@NotNull String gerritUrl, @NotNull String ref) throws GerritException {

        return MessageFormat.format(GERRIT_GITWEB_COMMIT_URL_TEMPLATE, getHost(gerritUrl), getProject(gerritUrl), ref);
    }

    private URI getURI(String gerritUrl) throws GerritException {

        try {
            return new URI(gerritUrl);
        } catch (URISyntaxException e) {
            throw new GerritException("Gerrit URL: " + gerritUrl + " cannot be parsed as a url!", e);
        }
    }

    private String getHost(String gerritUrl) throws GerritException {
        return getURI(gerritUrl).getHost();
    }

    private String getProject(String gerritUrl) throws GerritException {

        URI uri = getURI(gerritUrl);
        String project =  uri.getPath();

        // remove the gerrit part in the path
        project = project.replaceFirst("/gerrit", "");

        validatePathNotEmpty(project, "The project is not specified in the Gerrit Url: " + gerritUrl);

        // remove leading slash in path
        project = project.substring(1);

        // add the '.git' in project if missing
        if (!project.endsWith(".git")) {
            project = project + ".git";
        }

        return project;
    }

    private void validatePathNotEmpty(String toValidate, String reasonFailure) throws GerritException {

        if (toValidate == null || toValidate.isEmpty() || toValidate.equals("/")) {
            throw new GerritException(reasonFailure);
        }
    }
}


/**
 * 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.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

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

    /**
     * Converts a {@code TimeUnit} to a {@code ChronoUnit}.
     *

     * This handles the seven units declared in {@code TimeUnit}.
     *
     * @param unit the unit to convert, not null
     * @return the converted unit, not null
     */
    public static ChronoUnit chronoUnit(TimeUnit unit) {
        Objects.requireNonNull(unit, "unit");
        switch (unit) {
            case NANOSECONDS:
                return ChronoUnit.NANOS;
            case MICROSECONDS:
                return ChronoUnit.MICROS;
            case MILLISECONDS:
                return ChronoUnit.MILLIS;
            case SECONDS:
                return ChronoUnit.SECONDS;
            case MINUTES:
                return ChronoUnit.MINUTES;
            case HOURS:
                return ChronoUnit.HOURS;
            case DAYS:
                return ChronoUnit.DAYS;
            default:
                throw new IllegalArgumentException("Unknown TimeUnit constant");
        }
    }

    /**
     * Converts a {@code ChronoUnit} to a {@code TimeUnit}.
     *

     * This handles the seven units declared in {@code TimeUnit}.
     *
     * @param unit the unit to convert, not null
     * @return the converted unit, not null
     * @throws IllegalArgumentException if the unit cannot be converted
     */
    public static TimeUnit timeUnit(ChronoUnit unit) {
        Objects.requireNonNull(unit, "unit");
        switch (unit) {
            case NANOS:
                return TimeUnit.NANOSECONDS;
            case MICROS:
                return TimeUnit.MICROSECONDS;
            case MILLIS:
                return TimeUnit.MILLISECONDS;
            case SECONDS:
                return TimeUnit.SECONDS;
            case MINUTES:
                return TimeUnit.MINUTES;
            case HOURS:
                return TimeUnit.HOURS;
            case DAYS:
                return TimeUnit.DAYS;
            default:
                throw new IllegalArgumentException("ChronoUnit cannot be converted to TimeUnit: " + unit);
        }
    }

    /**
     * Generate timestamp in the format tYYYYMMDD-HHMMSS-XXX  (XXX = miliseconds)
     *
     * @param generationEnabled Flag indicating, if the generation of timestamp is enabled
     * @param dateInstant Time instant for which the timestamp will be generated
     * @return Timestamp string or null, if generation is disabled
     */
    public static String generateTimestamp(boolean generationEnabled, Date dateInstant) {
        if(!generationEnabled)
            return null;

        Calendar instant = new Calendar.Builder().
                setInstant(dateInstant)
                .setTimeZone(TimeZone.getTimeZone("UTC"))
                .build();

        return String.format("t%d%02d%02d-%02d%02d%02d-%03d", instant.get(Calendar.YEAR), instant.get(Calendar.MONTH) + 1,
                instant.get(Calendar.DAY_OF_MONTH), instant.get(Calendar.HOUR_OF_DAY), instant.get(Calendar.MINUTE),
                instant.get(Calendar.SECOND), instant.get(Calendar.MILLISECOND));
    }

    /**
     * Get date object representing timestamp X days ago specified by parameter
     *
     * @param numberOfDays Number of days to shift the time back
     * @return New Date object representing a timestamp X days before now
     */
    public static Date getDateXDaysAgo(int numberOfDays) {
        long unixMilisecondsExpirationThreshold = new Date().getTime() - (1000 * 60 * 60 * 24 * numberOfDays);
        return new Date(unixMilisecondsExpirationThreshold);
    }

    public static Instant toInstant(Date date) {
        if (date == null) {
            return null;
        } else {
            return date.toInstant();
        }
    }
}


/**
 * 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.builder;

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.model.BuildConfiguration;
import org.jboss.pnc.model.BuildConfigurationAudited;
import org.jboss.pnc.spi.coordinator.BuildSetTask;
import org.jboss.pnc.spi.coordinator.BuildTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
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.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;

/**
 *
Build task queue.

 *
 * The queue consists of 5 collections:
 *

 *
taskSets - set of currently processed task sets

 *
tasksInProgress - set of tasks that are being executed at the moment

 *
readyTasks - queue of tasks that are ready to be executed but are waiting for a free executor (and throttling mechanism)

 *
waitingTasksWithCallbacks - tasks waiting for a dependency. As soon as their dependencies are built, they are moved to readyTasks.
 * The waiting tasks are mapped to callbacks that are executed upon the transfer
 *

 *
unfinishedTasks - tasks either waiting, ready or in progress.
 * This collection is introduced to fix the race condition in {@link #take()},
 * where a task is taken from readyTask, and later put into tasksInProgress and the method cannot be synchronized

 *

 *
 * TODO: 1. taskSets can probably be removed

 * TODO: 2. Currently it throttles the number of tasks in progress. Is this necessary?
 *

 * Author: Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
 * Date: 4/18/16
 * Time: 12:47 PM
 */
@ApplicationScoped
public class BuildQueue {

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

    private Configuration configuration;

    private final Set<BuildTask> unfinishedTasks = new HashSet<>();

    private final BlockingQueue<BuildTask> readyTasks = new LinkedBlockingQueue<>();
    private final Map<BuildTask, Runnable> waitingTasksWithCallbacks = new HashMap<>();
    private final Set<BuildTask> tasksInProgress = ConcurrentHashMap.newKeySet();
    private final Set<BuildSetTask> taskSets = new HashSet<>();

    private final Semaphore availableBuildSlots = new Semaphore(0);

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

    @SuppressWarnings("unused")
    @Deprecated
    public BuildQueue() {
    }

    /**
     * Add a new, ready to build task to queue
     *
     * @param task task to be enqueued
     */
    public synchronized boolean addReadyTask(BuildTask task) {
        if (!task.readyToBuild()) {
            throw new IllegalArgumentException("a not ready task added to the queue: " + task);
        }
        unfinishedTasks.add(task);
        log.debug("adding task: {}", task);
        readyTasks.add(task);
        return true;
    }

    /**
     * Add a task that is waiting for dependencies
     * @param task task that is not ready to build
     * @param taskReadyCallback a callback to be invoked when the task becomes ready
     */
    public synchronized void addWaitingTask(BuildTask task, Runnable taskReadyCallback) {
        unfinishedTasks.add(task);
        log.debug("adding waiting task: {}", task);
        waitingTasksWithCallbacks.put(task, taskReadyCallback);
    }

    /**
     * Enqueue all tasks of a task set
     *
     * @param taskSet task set to be built
     */
    public synchronized void enqueueTaskSet(BuildSetTask taskSet) {
        log.debug("adding task set: {}", taskSet);
        taskSets.add(taskSet);
    }

    /**
     * remove task set from queue. This method should be invoked after whole task set is processed
     *
     * @param taskSet processed task set
     */
    public synchronized void removeSet(BuildSetTask taskSet) {
        log.debug("removing task set: {}", taskSet);
        taskSets.remove(taskSet);
    }

    /**
     * remove task from the queue.
     * This method should be invoked if the task is completed, either successfully, or with error (including rejected build)
     *
     * @param task task to be removed
     */
    public synchronized void removeTask(BuildTask task) {
        log.debug("removing task: {}", task);
        if (tasksInProgress.remove(task)) {
            availableBuildSlots.release();
        }
        if (readyTasks.remove(task)) {
            log.debug("The task {} has been removed from readyTasks.", task);
        }

        if (waitingTasksWithCallbacks.remove(task) != null) {
            log.debug("The task {} has been removed from waitingTasks.", task);
        }

        if (unfinishedTasks.remove(task)) {
            log.debug("The task {} has been removed from unfinishedTasks.", task);
        }
    }

    /**
     * Trigger searching for ready tasks in the waiting queue.
     * This method should be invoked if one task has finished and there's a possibility that other tasks became ready to be built.
     */
    public synchronized void executeNewReadyTasks() {
        List<BuildTask> newReadyTasks = extractReadyTasks();
        log.debug("starting new ready tasks. New ready tasks: {}", newReadyTasks);
        readyTasks.addAll(newReadyTasks);
    }

    /**
     * Get build task for given build configuration from the queue.
     *
     * @param buildConfigAudited build configuration
     * @return Optional.of(build task for the configuration) if build task is enqueued/in progress, Optional.empty() otherwise
     */
    public synchronized Optional<BuildTask> getTask(BuildConfigurationAudited buildConfigAudited) {
        Optional<BuildTask> ready = readyTasks.stream().filter(bt -> bt.getBuildConfigurationAudited().equals(buildConfigAudited)).findAny();
        Optional<BuildTask> waiting = waitingTasksWithCallbacks.keySet().stream().filter(bt -> bt.getBuildConfigurationAudited().equals(buildConfigAudited)).findAny();
        Optional<BuildTask> inProgress = tasksInProgress.stream().filter(bt -> bt.getBuildConfigurationAudited().equals(buildConfigAudited)).findAny();
        return ready.isPresent() ? ready : waiting.isPresent() ? waiting : inProgress;
    }

    /**
     * List all waiting, ready and in progress tasks
     *
     * @return list of all build tasks in the queue
     */
    public synchronized List<BuildTask> getSubmittedBuildTasks() {

        return new ArrayList<>(unfinishedTasks);

    }

    public BuildTask take() throws InterruptedException {
        availableBuildSlots.acquire();
        log.info("Consumer is ready to go, waiting for task");
        //FIXME not thread safe: when a task is taken from readyTasks it is not in the tasksInProgress for a short time
        // race condition hit while working on SkippingBuiltConfigsTest.shouldNotTriggerTheSameBuildConfigurationViaDependency
        // to avoid race condition getUnfinishedTask is used instead of getTask
        BuildTask task = readyTasks.take();
        log.info("Got task: {}, will start processing", task);
        tasksInProgress.add(task);
        return task;
    }

    public synchronized boolean isBuildAlreadySubmitted(BuildTask buildTask) {
        return unfinishedTasks.contains(buildTask);
    }

    public synchronized Optional<BuildTask> getUnfinishedTask(BuildConfigurationAudited buildConfigurationAudited) {
        return unfinishedTasks.stream().filter(buildTask -> buildTask.getBuildConfigurationAudited().equals(buildConfigurationAudited)).findFirst();
    }

    public synchronized Set<BuildTask> getUnfinishedTasks() {
        return new HashSet<>(unfinishedTasks);
    }

    private List<BuildTask> extractReadyTasks() {
        List<BuildTask> noLongerWaitingTasks = waitingTasksWithCallbacks.keySet().stream()
                .filter(BuildTask::readyToBuild)
                .collect(Collectors.toList());

        noLongerWaitingTasks.forEach(task -> {
            waitingTasksWithCallbacks.get(task).run();
            waitingTasksWithCallbacks.remove(task);
        });

        return noLongerWaitingTasks;
    }

    @PostConstruct
    public void initSemaphore()  {
        int maxConcurrentBuilds = 10;
        try {
            SystemConfig systemConfig = configuration.getModuleConfig(new PncConfigProvider<>(SystemConfig.class));

            maxConcurrentBuilds = systemConfig.getCoordinatorMaxConcurrentBuilds();
        } catch (ConfigurationParseException e) {
            log.error("Error parsing configuration, using 10 max concurrent builds in BuildQueue", e);
        }
        availableBuildSlots.release(maxConcurrentBuilds);
    }

    @Override
    public synchronized String toString() {
        return "BuildQueue{" +
                "readyTasks=" + readyTasks +
                ", waitingTasks=" + waitingTasksWithCallbacks +
                ", tasksInProgress=" + tasksInProgress +
                ", taskSets=" + taskSets +
                '}';
    }

    public synchronized boolean isEmpty() {
        return tasksInProgress.isEmpty() && waitingTasksWithCallbacks.isEmpty() && readyTasks.isEmpty()
                && unfinishedTasks.isEmpty() && taskSets.isEmpty();
    }

    public synchronized String getDebugInfo() {
        String info = "=====================\nQUEUE STATE:\n=====================\n" +
                "Available build slots: " + availableBuildSlots.availablePermits() + "\n" +
                "Queue length:" + availableBuildSlots.getQueueLength() + "\n" +
                "\n=====================\nTASKS IN PROGRESS:\n=====================\n" +
                tasksInProgress +
                "\n=====================\nREADY TASKS:\n=====================\n" +
                readyTasks +
                "\n=====================\nWAITING TASKS:\n=====================\n" +
                waitingTasksWithCallbacks.keySet() +
                "\n=====================\nALL UNFINISHED TASKS:\n=====================\n" +
                unfinishedTasks +
                "\n=====================\nTASK SETS:\n=====================\n" +
                taskSets;

        return info;
    }
}


/**
 * 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.model;

import org.jboss.pnc.enums.RepositoryType;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import java.util.Set;

/**
 * Contains information related to a repository of build artifacts (i.e. Maven, NPM, etc)
 */
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "identifier", "repositoryPath" }) )
public class TargetRepository implements GenericEntity<Integer> {

    private static final long serialVersionUID = 1L;

    public static final String SEQUENCE_NAME = "target_repository_repo_id_seq";

    @Id
    @SequenceGenerator(name = SEQUENCE_NAME, sequenceName = SEQUENCE_NAME, initialValue = 100, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE_NAME)
    private Integer id;

    /**
     * Flag that the repository is temporary.
     */
    @NotNull
    @Column(updatable=false)
    private Boolean temporaryRepo;

    /**
     * Identifier to link repository configurations (eg. hostname)
     */
    @NotNull
    @Size(max=255)
    @Column(updatable=false)
    private String identifier;

    /**
     * Path that need to be appended to the hostname
     * eg. "ga" for https://maven.repository.redhat.com/ga/
     * or "maven2" for https://repo1.maven.org/maven2/
     * or "" (empty string) when the repository content starts at root
     */
    @NotNull
    @Size(max=255)
    @Column(updatable=false)
    private String repositoryPath;

    /**
     * The type of repository which hosts this artifact (Maven, NPM, etc).  This field determines
     * the format of the identifier string.
     */
    @NotNull
    @Column(updatable=false)
    @Enumerated(EnumType.STRING)
    private RepositoryType repositoryType;

    @OneToMany(mappedBy = "targetRepository")
    private Set<Artifact> artifacts;

    public TargetRepository() {
    }

    private TargetRepository(Builder builder) {
        setId(builder.id);
        setTemporaryRepo(builder.temporaryRepo);
        setIdentifier(builder.identifier);
        setRepositoryPath(builder.repositoryPath);
        setRepositoryType(builder.repositoryType);
        setArtifacts(builder.artifacts);
    }

    public static Builder newBuilder() {
        return new Builder();
    }

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    public Boolean getTemporaryRepo() {
        return temporaryRepo;
    }

    public void setTemporaryRepo(Boolean temporaryRepo) {
        this.temporaryRepo = temporaryRepo;
    }

    public String getIdentifier() {
        return identifier;
    }

    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }

    public String getRepositoryPath() {
        return repositoryPath;
    }

    public void setRepositoryPath(String repositoryPath) {
        this.repositoryPath = repositoryPath;
    }

    public RepositoryType getRepositoryType() {
        return repositoryType;
    }

    public void setRepositoryType(RepositoryType repositoryType) {
        this.repositoryType = repositoryType;
    }

    public Set<Artifact> getArtifacts() {
        return artifacts;
    }

    public void setArtifacts(Set<Artifact> artifacts) {
        this.artifacts = artifacts;
    }

    @Override
    public String toString() {
        return "TargetRepository{" +
                "id=" + id +
                ", temporaryRepo=" + temporaryRepo +
                ", identifier='" + identifier + '\'' +
                ", repositoryPath='" + repositoryPath + '\'' +
                ", repositoryType=" + repositoryType +
                '}';
    }

    /**
     * List of trusted repository URLs.  These repositories contain only artifacts which have been built
     * from source in a trusted build environment.
     */
    public static final String [] TRUSTED_REPOSITORY_URLS = {
            "http://mead/eap6",
            "http://mead/eap7"
    };

    /**
     * Check if a given artifact originates from a trusted source.  Compares the given artifactOriginUrl
     * to the list of trusted repositories.
     *
     * @param artifactOriginUrl The URL from which the artifact was downloaded
     * @param targetRepository
     * @return true if the artifact url came from a trusted repo, false otherwise
     */
    static boolean isTrusted(String artifactOriginUrl, TargetRepository targetRepository) {
        if (targetRepository.temporaryRepo) {
            return false;
        }
        if (artifactOriginUrl == null || artifactOriginUrl.isEmpty()) {
            return false;
        }
        for (String trustedRepoUrl : TRUSTED_REPOSITORY_URLS) {
            if (artifactOriginUrl.startsWith(trustedRepoUrl)) {
                return true;
            }
        }
        return false;
    }

    public static final class Builder {

        private Integer id;

        private Boolean temporaryRepo;

        private String identifier;

        private String repositoryPath;

        private RepositoryType repositoryType;

        private Set<Artifact> artifacts;

        private Builder() {
        }

        public Builder id(Integer val) {
            id = val;
            return this;
        }

        public Builder temporaryRepo(Boolean val) {
            temporaryRepo = val;
            return this;
        }

        public Builder identifier(String val) {
            identifier = val;
            return this;
        }

        public Builder repositoryPath(String val) {
            repositoryPath = val;
            return this;
        }

        public Builder repositoryType(RepositoryType val) {
            repositoryType = val;
            return this;
        }

        public Builder artifacts(Set<Artifact> val) {
            artifacts = val;
            return this;
        }

        public TargetRepository build() {
            return new TargetRepository(this);
        }
    }
}


/**
 * 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.monitor;

import org.jboss.pnc.common.concurrent.MDCExecutors;
import org.jboss.pnc.common.util.ObjectWrapper;
import org.jboss.pnc.common.util.ReadEnvProperty;
import org.jboss.pnc.common.util.TimeUtils;
import org.jboss.util.collection.ConcurrentSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 *
 * @author Matej Lazar
 * @author Jakub Bartecek <jbartece@redhat.com>
 */
@ApplicationScoped
public class PullingMonitor {
    private static final Logger log = LoggerFactory.getLogger(PullingMonitor.class);

    /** Time how long to wait until all services are fully up and running (in seconds) */
    private static final int DEFAULT_TIMEOUT = 300;
    private static final String PULLING_MONITOR_TIMEOUT_KEY = "pulling_monitor_timeout";

    /** Interval between two checks if the services are fully up and running (in second) */
    private static final int DEFAULT_CHECK_INTERVAL = 1;
    private static final String PULLING_MONITOR_CHECK_INTERVAL_KEY = "pulling_monitor_check_interval";

    /** */
    private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;

    private static final String PULLING_MONITOR_THREADPOOL_KEY = "pulling_monitor_threadpool";
    private static final int DEFAULT_EXECUTOR_THREADPOOL_SIZE = 4;

    private ScheduledExecutorService executorService;
    private ScheduledExecutorService timeOutVerifierService;

    private ConcurrentSet<RunningTask> runningTasks;

    private int timeout;
    private int checkInterval;

    public PullingMonitor() {

        ReadEnvProperty reader = new ReadEnvProperty();

        timeout = reader.getIntValueFromPropertyOrDefault(PULLING_MONITOR_TIMEOUT_KEY, DEFAULT_TIMEOUT);
        checkInterval = reader.getIntValueFromPropertyOrDefault(PULLING_MONITOR_CHECK_INTERVAL_KEY, DEFAULT_CHECK_INTERVAL);

        int threadSize = reader.getIntValueFromPropertyOrDefault(PULLING_MONITOR_THREADPOOL_KEY, DEFAULT_EXECUTOR_THREADPOOL_SIZE);

        runningTasks = new ConcurrentSet<>();
        startTimeOutVerifierService();
        executorService = MDCExecutors.newScheduledThreadPool(threadSize);
    }

    public RunningTask monitor(Runnable onMonitorComplete, Consumer<Exception> onMonitorError, Supplier<Boolean> condition) {
        return monitor(onMonitorComplete, onMonitorError, condition, checkInterval, timeout, DEFAULT_TIME_UNIT);
    }

    /**
     * Periodically checks the condition and calls onMonitorComplete when it returns true.
     * If timeout is reached onMonitorError is called.
     *
     * @param onMonitorComplete
     * @param onMonitorError
     * @param condition
     * @param checkInterval
     * @param timeout
     * @param timeUnit Unit used for checkInterval and timeout
     */
    public RunningTask monitor(Runnable onMonitorComplete, Consumer<Exception> onMonitorError, Supplier<Boolean> condition, int checkInterval, int timeout, TimeUnit timeUnit) {

        ObjectWrapper<RunningTask> runningTaskReference = new ObjectWrapper<>();
        Runnable monitor = () -> {
            RunningTask runningTask = runningTaskReference.get();

            if (runningTask == null) {
                /* There might be a situation where this runnable is called before runningTaskReference is set with the
                 * running task since this runnable is scheduled to run before runningTaskReference is set. In that
                 * case, we just skip this runnable and re-run on the next scheduled interval with the assumption that
                 * runningTaskReference will be set before the next re-run
                 */
                log.debug("runningTask not set yet inside the 'monitor' runnable! Skipping!");
                return;
            }
            try {
                // Check if given condition is satisfied
                if (condition.get()) {
                    runningTasks.remove(runningTask);
                    runningTask.cancel();
                    onMonitorComplete.run();
                }
            } catch (Exception e) {
                log.error("Exception in monitor runnable", e);
                runningTasks.remove(runningTask);
                runningTask.cancel();
                onMonitorError.accept(e);
            }
        };
        ScheduledFuture<?> future = executorService.scheduleWithFixedDelay(monitor, 0L, checkInterval, timeUnit);
        Consumer<RunningTask> onTimeout = (runningTask) -> {
            runningTasks.remove(runningTask);
            onMonitorError.accept(new MonitorException("Service was not ready in: " + timeout + " " + timeUnit.toString()));
        };
        RunningTask runningTask = new RunningTask(future, timeout, TimeUtils.chronoUnit(timeUnit), onTimeout);
        runningTasks.add(runningTask);
        runningTaskReference.set(runningTask);

        return runningTask;
    }

    public void cancelRunningTask(RunningTask task) {
        runningTasks.remove(task);
        task.cancel();
    }

    public ScheduledFuture<?> timer(Runnable task, long delay, TimeUnit timeUnit) {
        return executorService.schedule(task, delay, timeUnit);
    }

    private void startTimeOutVerifierService() {
        Runnable terminateTimedOutTasks = () -> {
            runningTasks.parallelStream()
                    .forEach(runningTask -> runningTask.terminateIfTimedOut());
        };
        timeOutVerifierService = MDCExecutors.newScheduledThreadPool(1);
        timeOutVerifierService.scheduleWithFixedDelay(terminateTimedOutTasks, 0L, 250, TimeUnit.MILLISECONDS);
    }

    @PreDestroy
    public void destroy() {
        executorService.shutdownNow();
        timeOutVerifierService.shutdownNow();
    }

}